docs: C6 5.4 close-out dispositions (design, packet section 10, architecture row, ledger)

Lens findings on b4938c31 disposed: the ENOLCK tier is a recorded name tier with a
per-caller refusal (money only), the unprobeable-directory answer is enforced and
UNCACHED, the reset-beside-archive verdict lasts until the fresh ledger's epoch
passes the surviving segments (not "for the life of the install"), a UNIX socket
is corruption on the dir-fd shape and skipped by the path shape's stat-before-open,
and pid_is_alive's EPERM->alive flip is named as the shared liveness primitive it
is (custody settlements, claim reclaims, staging reaps defer too). Packet section 10
and the ledger carry the red-first rows and the two disclosed-not-fixed items
(heartbeat-ownership pin, R3 pin) with the reason: both usage suites sit at their
line caps, an owner decision still owed.
This commit is contained in:
Ouroboros 2026-09-02 14:55:30 +00:00
parent fc645a1b2e
commit 8fb08d44b5
4 changed files with 81 additions and 26 deletions

File diff suppressed because one or more lines are too long

View file

@ -264,7 +264,8 @@ rounding, folding of dispatched rows — each flips at least one pin.
`O_NONBLOCK`, so it cannot hang the question either *(qualified, round 5.4:
an entry that OPENS but is not a regular file is skipped; one the kernel
refuses to open at all — a UNIX socket, ENXIO — is corruption like any
other unopenable entry)*; an entry the scan
other unopenable entry on the dir-fd shape; the path shape's stat-before-open
classifies a socket as not regular and skips it)*; an entry the scan
cannot list, open or read IS corruption — the scan did not complete, the
data root's own handle included since round 5.3, and since round 5.4 every
path inspection the reader makes (the symlink bounds on both archive levels
@ -273,8 +274,10 @@ rounding, folding of dispatched rows — each flips at least one pin.
escaping as a bare `OSError` or a silent empty answer. Disclosed since
round 5.4: with a stamp-less live file every parsable regular file in the
archive that is not its byte-prefix is `generation newer`, so a ledger
reset beside a surviving archive is a PERMANENT corruption verdict on every
history question (reset both together) and an operator's stray JSON file
reset beside a surviving archive is a `generation newer` corruption verdict
on every history question until the fresh ledger's epoch passes the
surviving segments, which are then silently ignored (reset both together)
and an operator's stray JSON file
there is corruption on a stamp-less ledger only; and the readers being
lock-free, a compaction that commits between a question's live-header read
and its anchor scan makes that ONE question UNKNOWN (`generation newer`),
@ -303,11 +306,16 @@ rounding, folding of dispatched rows — each flips at least one pin.
EOPNOTSUPP/ENOTSUP/ENOSYS *(correction, round 5.4: ENOLCK was in the set
until then — "no locks available" is a missing lock daemon OR an exhausted
kernel lock table, not a capability answer, and it selected the tier where
the round-3 race returns; it now keeps the enforced tier and every live
acquisition the kernel refuses with it fails closed, so a lockd-less NFS
refuses every monetary write with `UsageAccountingError` instead of running
the name protocol; the per-directory verdict is decided ONCE under a module
lock, so racing threads share one probe — §9 "Round 5.4", R1)*, and since
the round-3 race returns; round 5.4 made it keep the enforced tier and fail
EVERY live acquisition closed — product-wide, the lenses showed, since the
primitive is shared — so the close-out made ENOLCK the name tier with its
errno RECORDED and a per-caller refusal: only the monetary lock names it
(`refuse_name_tier_errnos={ENOLCK}`), so a lockd-less NFS refuses every
monetary write with `UsageAccountingError` instead of running the name
protocol while every other lock keeps the protocol it always ran there;
the per-directory verdict is decided ONCE under a module lock, so racing
threads share one probe; an unprobeable directory answers enforced for
that call, uncached — §9 "Round 5.4" and §10, R1)*, and since
round 5.3 the two Win32 answers of a volume without byte-range locks —
ERROR_INVALID_FUNCTION, which LockFileEx answers on `\\wsl$`
(microsoft/WSL#5762), and ERROR_NOT_SUPPORTED, error 50 on a Samba share —
@ -334,7 +342,9 @@ rounding, folding of dispatched rows — each flips at least one pin.
mechanism: `pid_is_alive` read EPERM as DEAD, so another user's recycled
pid was reclaimed through the age path (flock-guarded on the enforced
tier) and only a same-uid recycle wedged — the opposite of what round 5.3
wrote; EPERM now reads alive (the process exists), so ANY live impostor —
wrote; EPERM now reads alive (the process exists) — in `pid_is_alive`, the
one liveness primitive shared by every consumer (custody settlements, claim
reclaims, staging reaps defer for such a pid too) — so ANY live impostor —
same uid or another — wedges the lock from the 90 s staleness window
(`usage_ledger._locked`, `stale_sec=90.0`) until it exits, the probe
flock deliberately unconsulted while the pid reads alive (a mixed-tier
@ -857,3 +867,21 @@ change), `d99ff6a9` (R3 + R4), `ea4d4337` (R5), `9306f962` (R6),
ARCHITECTURE row, PERSISTENCE, this section and the ledger — follows. Gate
evidence: `docs/v7next/LEDGER_CORRECTIONS.md` §"From the C6 micro-round 5.4
(owner batch №12 A, base 096437c2)".
## 10. Round 5.4 close-out — three read-only lenses on `b4938c31`, operator disposition (owner batch №12 A)
Verdicts: 3 × NEEDS_FIXES, no HIGH; 3 MEDIUM + 7 LOW. Fixed here (base `b4938c31`), pinned red-first:
| finding | disposition | pin → pre-fix shape → observed red |
|---|---|---|
| MEDIUM R1 (two lenses) — ENOLCK fail-closed landed in the SHARED primitive: on a lockd-less NFS `state/` every `acquire_exclusive_file_lock` caller failed, no model call could dispatch; the owner decided "compaction refuses", not this | **fixed**: ENOLCK is the name tier with its errno recorded beside the verdict (`_KERNEL_LOCK_TIER[dir] = (enforced, errno)`); `acquire_exclusive_file_lock(refuse_name_tier_errnos=…)` lets a caller fail closed on a recorded errno; only `usage_ledger._named_lock` names ENOLCK. Ordinary locks keep the name protocol they always ran there; money refuses typed | `test_the_capability_probe_decides_once_and_leaves_no_residue` (ENOLCK clause) and `test_enolck_is_the_name_tier_for_ordinary_locks_and_a_typed_refusal_for_money` → `platform_layer.py`/`usage_ledger.py` @ `b4938c31` → `assert True == (True, 5)` (a bare bool cached, ENOLCK enforced) / `assert True is False` |
| MEDIUM R4 — `_segment_path` resolved with `Path.resolve(strict=False)` one line BEFORE the typed `is_symlink()`: a symlink loop escaped as `RuntimeError("Symlink loop …")`, a readlink race as bare `OSError` | **fixed**: `os.path.realpath` (non-strict, never raises on a loop) inside the same `try`, `except (OSError, RuntimeError)` → `UsageLedgerCorrupt` | `test_a_path_inspection_the_reader_cannot_make_is_typed_corruption` (self-loop clause) → `usage_compaction.py` @ `b4938c31` → `RuntimeError: Symlink loop from …` and `OSError: [Errno 40] Too many levels of symbolic links` |
| LOW R3 — the stamp-less ENOENT exemption used a FOLLOWING `stat`: a dangling link at either archive level answered a silent empty set where the stamped reader answers corruption | **fixed**: `lstat` both levels first; `S_ISLNK` → typed `usage archive path is a symlink`, other `OSError` → typed `cannot be inspected`; pin deferred (the compaction suite sits at its 1600-line cap, disclosed below); mutation-verified by hand on this host (dangling link at `archive/` → typed) | — |
| LOW R5 — the old-inode witness was opened by PATH before the proof and not tied to the inode the precondition proved; a vanished ledger at the witness open was a bare `OSError` | **fixed**: `owned_and_intact` also proves `fstat(old_fd)` and `stat(path)` name one inode; the witness open is wrapped into `_Abort` (an abort by policy) | — (behaviour-preserving strengthening; no pin, disclosed) |
| LOW R1 — DESIGN §8 "decides once … cached" was absolute; an unprobeable directory answers enforced UNCACHED | **docs**: DESIGN §8, packet §5.10/§9, ARCHITECTURE row | — |
| LOW R6 — the strengthened heartbeat pin proves renewal + True, not ownership: a lock-TOUCHING stub survives it | **disclosed, not fixed**: the pin proves the callable renews THIS lock file's age (the production wire's only observable) — a stub that touches the production lock path is a contrived mutation; the suite is at its line cap | — |
| LOW R7 — EPERM→alive is a flip of a primitive shared by 12 non-test consumers, disclosed only for the monetary lock | **docs**: DESIGN §8 residual, packet §9 R7, ARCHITECTURE/PERSISTENCE wording name the shared primitive and the consumers that now defer | — |
| LOW R8d — "PERMANENT … for the life of the install" over-stated: the verdict lasts until the fresh ledger's epoch passes the surviving segments, which are then silently ignored | **docs**: DESIGN §10, packet §5.9, ARCHITECTURE row | — |
| LOW R8e — the socket qualification introduced its own absolute: a UNIX socket is corruption on the dir-fd shape only; the path shape's stat-before-open skips it | **docs**: DESIGN §10/§12.5, packet §5.9, ARCHITECTURE row | — |
Sizes after the close-out: `platform_layer.py` 1500/1500 (band ceiling; net +3 on the policy, paid by rewrapping two prose blocks — no contract text dropped), `usage_compaction.py` 1262 (band), `tests/test_usage_compaction.py` **1600/1600** (owner decision still owed: the next pin in this suite needs either the archive-reader tests moved to their own module — a natural organ boundary, `archived_attempt_ids` vs the pass — or a cap decision), `tests/test_lockfile_helpers.py` 568.

View file

@ -211,17 +211,25 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
own winerror→errno table lands both on `EINVAL` and would leave the name
tier structurally unreachable there: a Windows volume without byte-range
locks would fail EVERY monetary append closed instead of degrading to it.
`ENOLCK` ("no locks available") is NOT in that set (round 5.4; until then
it selected the name tier): a filesystem without a lock daemon answers it,
but so does an exhausted kernel lock table, and neither is the kernel
saying this filesystem cannot — the probe keeps the enforced tier and every
live acquisition the kernel refuses with it fails closed, so an install
whose `state/` answers ENOLCK persistently (bare NFS without lockd) refuses
every monetary write with `UsageAccountingError` — no lock, no append, no
pass — instead of running the name protocol; moving `state/` onto a
filesystem that locks is the repair. The verdict is cached per process per
directory under one module lock, so racing threads share one probe and one
answer rather than two probes that could disagree.
`ENOLCK` ("no locks available" — a filesystem without a lock daemon, or an
exhausted kernel lock table) is the third answer (round 5.4 close-out; round
5.4 proper made it fail EVERY caller closed, which the lenses showed to be
product-wide: the same primitive locks state singletons, task results and
custody, so a lockd-less NFS `state/` would have stopped every locked write
and every model dispatch — a capability the name protocol had always
provided there): it selects the **name tier** like a filesystem that cannot,
but the probe RECORDS the errno beside the verdict, and a caller may refuse
that tier by errno (`acquire_exclusive_file_lock(refuse_name_tier_errnos=…)`).
Only the monetary lock does: `usage_ledger._named_lock` names `ENOLCK`, so on
such an install every monetary write refuses typed (`UsageAccountingError` —
no lock, no append, no pass; money never runs the name protocol where locks
might merely be unavailable) while every other lock keeps the name protocol
it always ran there; moving `state/` onto a filesystem that locks is the
repair. The verdict is cached per process per directory under one module
lock, so racing threads share one probe and one answer rather than two
probes that could disagree — except a directory where the scratch probe
cannot be created, which answers enforced for that call and is probed again
next time (not cached).
*Enforced tier* (POSIX `fcntl.flock`, Windows `LockFileEx` — both held on
the lock fd): every other hold of this lock is milliseconds; a compaction
pass over a multi-megabyte ledger can legitimately exceed its 90 s
@ -286,7 +294,11 @@ exactly the per-row branch taken `weight` times with the sums pre-added.
reclaimed through the age path — the probe flock guarding it on the
enforced tier — while only a same-uid recycle wedged; this note named the
opposite mechanism). So a lock whose owner died and whose pid was reused
is never reclaimed by age while the impostor lives: the wedge begins once
is never reclaimed by age while the impostor lives (`pid_is_alive` is the
ONE liveness primitive, shared by every consumer — custody settlements,
claim reclaims, staging reaps — so a pid recycled onto another user's
process reads alive everywhere and those defer while the impostor lives,
exactly as they do for a same-uid recycle): the wedge begins once
the dead owner's file is older than the 90 s staleness window
(`usage_ledger._locked`, `stale_sec=90.0` — a literal there, not a
`config.py` constant) and ends when the impostor exits, even though the
@ -473,7 +485,9 @@ the live replay, so this lane ships the join surface the sweep must use:
FIFO, a device — is no segment: segments are regular files by
construction, no generation lives there, and it is skipped; one the
kernel refuses to open at all (a UNIX socket, `ENXIO`) is corruption like
any other unopenable entry. Where a dir-fd is held that classification is
any other unopenable entry — on the dir-fd shape; without a held handle
(Windows, no `O_DIRECTORY`) the `stat` before the open classifies a socket
as not regular and skips it. Where a dir-fd is held that classification is
an `fstat` after an `O_NONBLOCK` open; where none is (Windows, or any os
without `O_DIRECTORY`) it is a `stat` BEFORE the open, because there the
open is the step a directory refuses and a writer-less FIFO blocks on. A
@ -494,8 +508,10 @@ the live replay, so this lane ships the join surface the sweep must use:
regular file in `archive/usage_ledger/` that is not a byte-prefix of a
stamp-less live file is `generation newer` — a fresh ledger started
beside a surviving archive (a reset that deleted the ledger alone) leaves
every history question a PERMANENT corruption verdict for the life of the
install, and an operator's stray `notes.jsonl` there is skipped on a
every history question a `generation newer` corruption verdict until the
fresh ledger has compacted as many times as the old one had — after which
the surviving segments fall under the epoch floor and are silently ignored,
their ids absent from every history question — and an operator's stray `notes.jsonl` there is skipped on a
stamped ledger but is corruption on a stamp-less one; move or delete the
archive with the ledger, or keep both. Disclosed, lock-free readers: a
compaction that commits between a question's live-header read and its
@ -614,7 +630,8 @@ byte authority.
the archive — an entry that opens but is not a regular file — is no
segment and is skipped on both shapes of the scan (the FIFO cannot hang
the question; an entry the kernel refuses to open at all, a UNIX socket,
is corruption), and the chain union is built once per chain and cached
is corruption on the dir-fd shape and skipped by the path shape's
stat-before-open), and the chain union is built once per chain and cached
within a bound.
6. **Idempotency survives**: subscription/external replays after compaction
dedup (no double charge) and still conflict-check; legacy import stays

View file

@ -5614,3 +5614,13 @@ Cross-cutting disclosures:
surfaces are the two pinned suites and the lock/liveness consumers above,
all run), and the Windows LockFileEx mechanics remain owed to the 3-OS CI
matrix.
## From the C6 micro-round 5.4 close-out (operator, base b4938c31; owner batch №12 A)
Three read-only lenses on the 5.4 delta: NEEDS_FIXES × 3, no HIGH (3 MEDIUM, 7 LOW). The operator closed them without a further agent round (the owner bound was one micro-round):
1. **R1 blast radius (MEDIUM, two lenses) — fixed.** Round 5.4 had moved `ENOLCK` out of the unsupported set so the probe kept the enforced tier and EVERY live acquisition failed closed. The primitive is shared (state singletons, task results, custody, claims, …), so a lockd-less NFS `state/` would have refused every locked write in the product, where the name protocol had always worked. Now `ENOLCK` selects the name tier like a filesystem that cannot, the probe records the errno beside the verdict, and `acquire_exclusive_file_lock(refuse_name_tier_errnos=…)` lets a caller fail closed on it — only `usage_ledger._named_lock` names `ENOLCK`. Red-first: `platform_layer.py`/`usage_ledger.py` @ `b4938c31` → `assert True == (True, 5)`, `assert True is False`.
2. **R4 typed resolution (MEDIUM) — fixed.** `_segment_path` resolves with the non-strict `os.path.realpath` inside the typed `try` (`except (OSError, RuntimeError)`). Red-first: a self-loop segment link → `RuntimeError: Symlink loop` / `OSError [Errno 40]` escaped @ `b4938c31`.
3. **R3 stamp-less symlink levels (LOW) — fixed**, pin deferred (suite at its 1600-line cap; mutation-verified by hand). **R5 witness inode tie + `_Abort` on a vanished ledger (LOW) — fixed**, no pin (disclosed). **R6 heartbeat-ownership pin (LOW) — disclosed, not fixed.** **R1 uncached unprobeable directory, R7 shared liveness primitive, R8d epoch-floor duration, R8e socket shape (LOW) — docs corrected** in DESIGN §8/§10/§12.5, packet §5.9/§5.10/§9/§10, ARCHITECTURE row.
Gates at the close-out tip: `tests/test_lockfile_helpers.py` + `tests/test_usage_compaction.py` + `tests/test_usage_*` + `tests/test_persistence_inventory.py` rc 0 (107 passed, 1 skipped Windows-only); `ruff --select F` rc 0; `scripts/check_domains.py` rc 0; `scripts/regenerate_size_ratchet.py --check` rc 0 (`platform_layer.py` 1500, `tests/test_usage_compaction.py` 1600 — both AT their ceilings); `git diff --check` rc 0. The CI-shape battery and `-m serial` run on the integration tree after the lane merges.