Repository navigation
feat(spec): the liveness gate reads the retiredKey() tombstone marker — a tombstoned key may not be graded live - #19092
Conversation
The gate graded a property from the ledger row's `status` or, with no row, from a `[experimental` / `[planned` marker. It had no reading of the tombstone marker at all, so `retiredKey()` tombstone + `status: live` was a structurally permitted combination: the forward pass is satisfied (the tombstone keeps the key in the walked shape), the orphan pass is satisfied (the property is still there), and nothing constrained what the row claimed. Adds the join both directions lacked — walk the tombstones, ask each one's ledger row what it says — as a fourth direction beside the orphan scan, with its guidance next to the guidance that states the rule's other half. Census over all 39 governed types at this base: the walk reaches 79 `[REMOVED]` tombstones, 78 already `dead`, one outlier. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Unit cases for the pure scan (including the marker held equal to the real `retiredKey()` producer, so a drifted marker fails instead of reporting a clean tree), and the real-gate pair via `--ledger-root`: one sample, two copies differing in exactly one status value, red and green respectively. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…rker Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
…a row The first draft named two rows: it flipped `agent/tools` to `dead` to neutralise the then-offender, and `hook/timeout` to `live` as the sample. The first broke as soon as that row was repaired upstream — the fixture's own "this would be a no-op" guard fired, loudly, which is what a guard is for. What was wrong was pinning a fixture to one row's current VERDICT, a moving fact about the ledger rather than a property of the rule. So the carrier is derived per run from the gate's own enumeration, under filters that are properties of the shape (top-level · not a bound high-risk coordinate · currently `dead`), and the derivation asserts it found one and names the population it searched. The neutralisation half is gone: these cases now inherit this file's existing dependency on the shipped ledgers being green rather than carrying a private patch list. To make that possible the scan enumerates its population instead of totalling it (`report.tombstones`), on `report.authorable`'s reason. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
Seat review — re-measured against the tree, not the report
One observation, recorded rather than filed. Of the derivation's three filters only the first bites today: after the top-level filter takes 79 to 42, the Radius of that re-measurement. It is the gate's own walk, so it is silent about the 59 further Ready flipped; auto-merge mounted on SQUASH. Generated by Claude Code |
Fixes #19062
Clause-②: no
What this closes
The liveness gate graded a schema property from two sources: the ledger row's
status, or — with no row — a[experimental/[plannedmarker read bymarkerStatus(). It had no reading of theretiredKey()tombstone marker at all, sotombstone + status: livewas a structurally permitted combination rather than an oversight:This adds the join both directions lacked, as a fourth direction beside the orphan scan: walk the tombstones the gate already reaches, and ask each one's ledger row what it claims.
Ordering — discharged
This section used to read "this PR must not merge before #19059", and that constraint is now spent: #19059 landed as
a362e0e425,mainis merged into this branch, and the single offending row it named —agent/tools, #18304's — isdeadon the real ledgers. Re-measured by the seat on this PR's head3d49d7a199(2026-09-18T22:08Z):check:livenessagainst the shipped ledgers: exit 0, printing79 graded with a status the tombstone allows.scripts/liveness/suite: 333 passed (333) over 12 files, exit 0.⛔ No landing order remains on this PR. Read the population table below as the measurement that motivated the rule, not as a description of today's tree.
The population, measured — the card's first deliverable
The card states its own evidence limit: "the population is unmeasured, and measuring it is the first piece of the work — it needs exactly the join the gate lacks". Measured over all 39 governed types by the gate's own walk, at two commits — the tree the rule was written against, and this PR's head
3d49d7a199(the right-hand column re-measured by the seat, 2026-09-18T22:08Z):[REMOVED]tombstones the gate's walk reachesdeadlive(the defective state)agent/toolschildrenDefault, none UNCLASSIFIED)The full list of 79 is in the report on the card. Drift control on the instrument: the census walk reproduces the gate's own per-type verdict counts for all 39 types and 1092 verdict points exactly (compared against
check:liveness --json'sclassified + unclassified), so it is the gate's walk and not a copy of it.Reachable radius of that census — it is the gate's walk, so it is the ledger-driven drill, nothing wider. Known targets necessarily outside it: a full-depth walk of the same governed roots finds 59 further
[REMOVED]tombstones the gate never reaches —object/fields.conditionalRequired,object/listViews.performance,job/retryPolicy.retryDelayMsamong them — because they sit under containers carrying a blanket verdict (the 601 child keys the gate already reports as classified nowhere). No rule placed in this gate can grade a key the gate does not walk; closing that is the undrilled-container worklist, not this card.Why remedy (2), measured rather than argued
Triage named two shapes and refused to pick. Their side-effect surfaces were measured, each applied alone to the base gate with the mutation proven on disk before any verdict was read:
[REMOVED]arm inmarkerStatus()— on the acceptance sample this leaves the gate at exit 0.markerStatus()is consulted only whenled?.statusis falsy, and all 79 tombstones the walk reaches take their status from an explicit row, so this arm changes the verdict of 0 of the 79 today. What it does change is the no-row case, which the gate currently reports as UNCLASSIFIED and fails on, and whichORPHAN_GUIDANCEtells authors never to create — so (1) alone would silence an existing loud failure. It cannot satisfy the acceptance.live, which is what was ruled.planned/experimental/live-elsewhereon a tombstone are equally false, and their population today is zero, so widening the set is a separate measurement rather than a rider on this one.Acceptance
Every leg runs the real gate the way CI invokes it, via
--ledger-rootagainst a copied ledger.The carrier the legs flip is derived per run from the gate's own⚠️ Today only the top-level filter bites — the bound-path and
report.tombstonesenumeration, not named: top-level, not aBOUND_PROOF_PATHScoordinate, and currentlydead. A fixture pinned to one row's current verdict is pinned to a moving fact, which is how this block first broke — it namedagent/tools, that row wentdeadupstream, and the "this would be a no-op" guard took four cases red. The seat re-derived the same filter chain independently (2026-09-18T22:08Z): 79 enumerated → 42 top-level → 42 unbound → 42dead, carrieraction/bulkEnabled.deadfilters each drop 0 — so those two are shape guards holding a property, not measured reductions, and the block's own assertion is on eligible > 0, never on a filter biting.The red and green samples differ in exactly one status value (plus the arithmetic the generated count artifact requires, so a stale count cannot mask the verdict), and "one value apart" is now checked as bytes by its own case rather than asserted in prose.
status: live<derived carrier> -> "live", with exactly one✗block in the outputliverowsThe dark control is the strong form: a full diff of the gate's output on a legal ledger, before versus after, differs only by the new
tombstoned keys:census line and the new clause in the summary sentence. All 39 per-type verdict lines and every other check's output are byte-identical, so nothing else was tightened.The before-legs were taken by reverting both files to the base commit after the fix was committed, with the mutation proven on disk (blob hashes moved, the new symbols counted 0 occurrences) and the restore proven by
git diff HEADbeing empty and the blob hash matchingHEAD's.Non-vacuity
0 forbiddenreads identically whether every row is honest or the marker drifted and the scan reached nothing, so the gate prints the population it asked on every run, pass or fail. On this PR's head3d49d7a199against the shipped ledgers (2026-09-18T22:08Z) it prints the green form:and on the red leg the same line carries the finding:
and
orphans.test.tsholdsTOMBSTONE_MARKERequal to whatretiredKey()actually writes, read from the producer rather than restated.Changeset
skip-changeset, measured rather than assumed.@objectstack/spec'sfiles[]isdist · json-schema · liveness · prompts · llms.txt · README.md · src/**/*.zod.ts · CHANGELOG.md · api-surface · api-surface-declarations · spec-changes.json;scripts/is not in it, and the build's entries are src modules. Each of the three symbols this PR adds occurs in 0 files under those shipped paths; positive controls through the same instrument and the same path set:retiredKeyin 102,ObjectSchemain 30. Nothing this diff moves is shipped by any package.Acceptance notes
Noted, not filed — observations, with no reproducible defect, contract violation or authoring trap behind them:
scripts/liveness/tombstoned-row-status.test.tsasa362e0e425(2026-09-18T22:08Z), a contract test asserting the same invariant against the real ledgers at top-level granularity only. It is complementary rather than duplicative — that one pins today's ledgers inTest Core, this one is a gate rule over every depth the walk reaches and over any ledger root. Left untouched here, per triage's⛔ 不折叠. This PR is the second of the two, and both files were written without sight of the other.markerStatus()still has no[REMOVED]arm, deliberately: with a row present it is never consulted, and without one the UNCLASSIFIED failure is the louder answer. Recorded so the next reader does not read its absence as an oversight twice.Generated by Claude Code