Suggestions from the linen v1.6.1 dependency review (2026-09-28), re-checked
for the bumps to linen v1.6.2 (0.3.2) and v1.7.0 (0.3.3): nothing here blocks
them — ledger uses none of the modules linen 1.6.x or 1.7.0 changed (1.6.2
only touches raw!; 1.7.0 adds modules moved from lode and lun). Each item names where it comes from;
re-check before acting.
Moves into linen follow linen's AGENTS.md ("Importing external code"): the
linen change and the deletion of ledger's copy happen in the same pass.
-
Close the reserve race.
Ledger/Sql/Reserve.lean'sinsert … select … where (sum delta) − (sum held) ≥ amountis one statement, but underREAD COMMITTED/REPEATABLE READtwo concurrent reserves for the same org each read a snapshot without the other's uncommitted hold and can both insert — write skew, i.e. overspend. Nothing locks or constrains it; the module doc's "no advisory lock" is the gap, not a feature. Options: takepg_advisory_xact_lock(hashtextextended(org_id::text, 0))inside the statement (e.g. as a CTE), lock the org's row (select … from orgs where id = $1 for update), or require callers to run it underSERIALIZABLEand retry on40001. Any of these changes the contractliaison/Liaison/Budget.leanrepeats — coordinate the change there (see "Reservation SQL is shared with liaison" below), updateLedgerTests/Ledger/Sql/ReserveTest.lean, and add a live-Postgres concurrency test (none exists today). Until then the README flags it (the Guarantees table's "only underSERIALIZABLE" row and "Known gap" note, the Features bullet, the layout table), and the repository'sdouble-spend-preventiontopic states the intent, not a current guarantee. When it is closed, remove those flags and correctReserve.lean's module doc, which still says the statement "actually prevents double-spend". (M) -
Consider putting the concurrency requirements in the types. Today no-overspend is enforced only by SQL text and by callers remembering the isolation level, while the rest of the guarantees are held by Lean types. Possible directions:
- Index the transaction type by its isolation level, so that running
reserveoutsideSERIALIZABLEdoes not type-check. Check first whether linen'sSession/transaction API can carry this; if not, the change belongs in linen. - If the race is closed with a lock instead, make
reservetake a token (e.g.OrgLock org) that can only be obtained by acquiring the per-org lock in the same transaction. - State no-overspend as a theorem over interleavings of reserve transactions against a model of Postgres, with the isolation level as an explicit hypothesis. This proves the design under stated assumptions, not the running database.
Limits to keep in mind: types only constrain callers that go through Lean code. That can include
liaisononce it imports the reservation module (see "Reservation SQL is shared with liaison" below), but not writers that issue raw SQL, and it does not verify the SQL text itself. Depends on the choice made in "Close the reserve race" above. (M-L) - Index the transaction type by its isolation level, so that running
- Build the executable, not only
LedgerTests. CI runslake build LedgerTests(.github/workflows/lean_action_ci.yml:16); no test importsMain, so the copied libpq link recipe is only exercised by the Docker publish. linen'sAGENTS.mdrecords a bug that only an executable link (Scrt1.o) exposes. Addlake build ledger, and a macOS leg for the lakefile's.dylibbranch (lakefile.lean:49). (S) - Verify the Docker image after the Actions bump. The publish workflow
now uses
docker/*-actionv4/v6/v7 andactions/checkout@v7(Node 24, deprecated inputs removed — none of which ledger uses), untested until the first push tomain. The image could not be built locally (Docker Desktop and the registry need a corporate sign-in). Once it builds, consider moving the runtime fromdebian:bookworm-slimtotrixie-slim(current stable;libpq.so.5is the same soname), together with liaison, which uses the same base. (S)
- Typed environment readers.
Main.lean:11-18(DATABASE_URL→PoolSettings) is the same as liaison'sMain.lean:14-26; lun and lode duplicatesecondsEnv. ledger range-checks the port (Main.lean:30-36); lun, lode and liaison usetoUInt16, which wraps. ASystem.Environmentmodule in linen plusSettings.fromEnv, adopted by all four services. (S–M) - A health check with a probe. linen's
healthCheck(Linen/Network/WebApp/Extra/Middleware/HealthCheckEndpoint.lean:18-23) always answers 200, soLedger/Health.lean:31-42hand-rolls a 503 for a stopped sweeper; lun and liaison hand-roll theirs too. AhealthCheckWith path probein linen. (S) - Reservation SQL is shared with liaison (
liaison/Liaison/Budget.leanrepeatsLedger/Sql/Reserve.lean). Application logic, so not linen: expose a pure module here that liaison imports. (M)
- libpq for consumers' executables.
lakefile.lean:26-76copiespkgConfig/pkgAbsoluteLibs(and arun_cmddefining the link flags) because Lake does not pass a dependency'smoreLinkArgsto a dependent's executable; six repos carry the copy. An idea to try in linen: Lake 4.34 adds each imported library'smoreLinkObjsto the executable link (Lake/Build/Module.lean:1297-1303), so amoreLinkObjstarget yielding libpq's absolute path might carry it without the-Lthat shadows glibc. Unverified — prove it in linen's consumer CI job first. (M) - One consumer native-dependency list. linen 1.7.0 ships it
(
ci/native-deps/apt.txt, and thesetup-native-depsaction withkeyring: false); CI and the Dockerfile read it at the linen versionlakefile.leanrequires (0.3.3). (S)
-
Ledger/Sweeper.lean:56sleeps with aUInt32of milliseconds, which overflows past ~49 days of interval. (XS)