fix(spec): the liveness gate stops trusting the direction it never checked - #4094
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 31, 2026 03:47
…ecked The gate walked the schema and looked each property up in the ledger. That direction was well defended — a property with no row is UNCLASSIFIED and fails CI. The reverse had no check at all, and the two removal routes make that gap load-bearing: - a `retiredKey()` tombstone leaves `z.never()` in the walked shape, so the key is still a property and its row must STAY (deleting it fails as UNCLASSIFIED — 14 at once in the #3896 close-out sweep); - a strict removal takes the key OUT of the shape, so its row must GO. Nothing asked. The forward pass stops enquiring about a key it can no longer see, and a stale `dead`/`live` claim about a nonexistent property rots in place — still read by anyone treating the ledger as the capability catalogue it doubles as. Not hypothetical: the report `aria`/`performance` rows outlived their keys by a full release and were deleted by hand in the #3896 sweep, noticed only because someone happened to be reading the file. One direction failed loudly; the other never failed at all — the same silent-no-op shape this whole ledger exists to catch, one layer up. `scripts/liveness/orphans.mts` closes it, as a FAILURE rather than a warning. The tree is orphan-free across all sixteen governed types, so there is no debt to amortise and nothing to soften: a warning here would have re-created the original defect (the README's own verdict is that a permanently-noisy check is a check nobody reads, the same way a stale row is a claim nobody re-tests). The error text names BOTH causes, because which one applies is the author's judgement: delete the row, or — if the property really is still authorable — fix the WALK, since a property the walk cannot see is one the ratchet cannot govern. Deliberately silent in one case: a row declaring `children` on a non-container. The forward pass already reports that as UNCLASSIFIED with a more precise message, and two headings for one fix reads as two problems. Confirmed against `tool.parameters` (a `z.record`, undrillable) — the forward pass claimed it and the orphan scan stayed quiet. Because the tree is clean, a green gate proves nothing about whether the scan can fire, so the proof lives in `orphans.test.ts` (9 cases: both orphan levels, the tombstone that must NOT flag, the deferred non-container, entry fields that are not child props, absent/empty props). Both legs were also driven end-to-end by injecting a real orphan into a real ledger: `tool/category` (top level) and `view/list.ghostListKey` (drilled child) each failed the gate with exit 1 and the full prescription; the ledgers were restored and verified byte-identical. The skill and the ledger README carried the now-false claim that this direction never fails; both corrected, with the orphan leg documented as the fix. Gate-only change under `packages/spec/scripts/`, which is not published — empty changeset. Verification: all ten spec gates pass (`check:api-surface` needed a rebuild first — the documented stale-dist false alarm, on a dist predating 2af1988); liveness script suite 98/98; spec `tsc` clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX
xuyushun441-sys
force-pushed
the
claude/sharing-rules-schema-bypass-h4c7xr
branch
from
July 31, 2026 03:52
caed06d to
ff08dfd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #4080, which documented this hole. Closing it turned out to be ~130 lines.
The hole
The liveness gate walks the schema and looks each property up in the ledger. That direction is well defended — a property with no row is
UNCLASSIFIEDand fails CI (the ratchet). The reverse had no check at all, and the two removal routes make that gap load-bearing:retiredKey()tombstonez.never()is a property)UNCLASSIFIED, fails CI (14 at once in the #3896 sweep)The forward pass stops enquiring about a key it can no longer see, so a stale
dead/liveclaim about a property that does not exist rots in place — still read by anyone treating the ledger as the capability catalogue it doubles as.Not hypothetical: the report
aria/performancerows outlived their schema keys by a full release and were deleted by hand in the #3896 sweep, noticed only because someone happened to be reading the file. One direction failed loudly; the other never failed at all — the same silent-no-op shape this whole ledger exists to catch, one layer up.The fix, and why it fails rather than warns
scripts/liveness/orphans.mts— a pure scan run off the same walk the forward pass classifies against, so the two directions can never disagree about what the schema contains.It's a hard failure. The tree is orphan-free across all sixteen governed types, so there's no debt to amortise and nothing to soften; a warning would have re-created the original defect one layer up — the ledger README's own verdict is that a permanently-noisy check is a check nobody reads, the same way a stale row is a claim nobody re-tests.
The error text names both causes, because which one applies is the author's judgement:
shapeOfskips).It also restates the asymmetry inline, so the tempting wrong fix — deleting a tombstoned key's row — is warned against at the point of failure rather than in a doc.
Deliberately silent in one case: a row declaring
childrenon a non-container. The forward pass already reports that asUNCLASSIFIEDwith a more precise message, and two headings for one fix reads as two problems. Confirmed againsttool.parameters(az.record, undrillable): the forward pass claimed it, the orphan scan stayed quiet.How it's proven, given the tree is clean
A green gate proves nothing about whether the scan can fire, so:
orphans.test.ts(9 cases) — both orphan levels, the tombstone that must NOT flag, the deferred non-container, entry fields (status/evidence/verifiedAt/authorWarn) that are not child props, and absent/emptyprops. The logic module + colocated test matches theevidence.mts/verification.mts/empty-state.mtspattern already in that directory.tool/category(top level) andview/list.ghostListKey(drilled child). Each failed the gate with exit 1 and printed the full prescription. Ledgers restored and verified byte-identical (git diffonliveness/*.jsonempty).Docs corrected
Both the skill (#4080) and the ledger README carried the now-false claim that this direction never fails. Both updated to describe the orphan leg as the fix, with the asymmetry table intact.
Verification
check:api-surfacefailed first on a dist predating2af1988— the stale-dist false alarm the skill documents; rebuilt, then green. Reporting it because the trap is the point.tscclean.packages/spec/scripts/, which is not in the package's publishedfiles— empty changeset.Generated by Claude Code