Uh oh!
There was an error while loading. Please reload this page.
fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285
Conversation
…imeStackContext
CONTEXT_STACK_KEYS carried a satisfies clause -- validity, not completeness --
while the RuntimeStackContext docblock claimed it was derived from that shape.
A collection declared on the interface and missing from the list was silently
never carried into the per-write snapshot, so every rule resolving into it
judged an empty universe and emitted findings that look correct.
Derive it from a keyed record typed { [K in keyof RuntimeStackContext]-?: true }
-- the -? mechanism proven at metadata-protocol's protocol.ts -- so a new
context collection without its row is a type error naming that collection at
tsc --noEmit and at the DTS build. Declaration order is preserved, since it
feeds both the snapshot key order and the derived top-level-index alternation,
and is now pinned end to end.📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5525164b3635fd4e7da9275e40405fd4043c9f8f && git checkout 5525164b3635fd4e7da9275e40405fd4043c9f8f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aca23aba40883b2c181053229fc6748d6f7ad0d5 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 && git checkout -B drift-repro aca23aba40883b2c181053229fc6748d6f7ad0d5 && git merge --no-ff 85ce871b8c279f9a6253f7f78d68d3cd46b38c41
node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13977
CONTEXT_STACK_KEYSinpackages/lint/src/runtime-gate.tsis now derived fromRuntimeStackContextinstead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.Premise re-check on fresh
origin/mainAll three anchors are still exactly where the card puts them, at the same line numbers, on
origin/mainataca23aba4::116— the docblock claim, verbatim: "seeCONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".:293— the hand-written literal withas const satisfies readonly (keyof RuntimeStackContext)[]— validity, never completeness.:393—buildRuntimeWriteSnapshotsiterating that set to fill the per-write snapshot, which is what turns a forgotten entry into a verdict against an empty universe rather than a missing member.The card's own measurement reproduces exactly. Adding
widgets?: readonly unknown[]toRuntimeStackContexton the pre-fix file:Exit 0, nothing in the package red. Premise still valid.
The ordering measurement — the triage pre-answer, answered by measurement
Triage made this binding before any spelling could be chosen: is the order
CONTEXT_STACK_KEYSencodes ("in stack-key order") load-bearing anywhere? A mapped type does not guarantee declaration order, so a spelling that silently reordered would be a regression no gate here would catch.What was searched. Every reader of the constant, repo-wide (
git grepover.ts/.mts/.mjs,node_modulesanddistexcluded) — two consumers, plus their transitive readers::393, whose insertion order becomes the snapshot's key order.runtime-gate.derived-name-keys.test.tsreads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.deriveNameKeyedStackKeys, which filters in context order by documented contract, feedingbuildTopLevelIndexPatternand thenceTOP_LEVEL_INDEX— an alternation whosesourceruntime publish gate: the snapshot collection set is hand-listed in five places and only one of them can go red #13390 keeps byte-identical to the literal it replaced.What was measured, by mutating the order on disk and reading which pin moves (each leg proves the mutation landed by reading the key order back off the file, and restores via
git checkout HEAD --, proven by an emptygit status --porcelain):objects/permissionsdatasets/pagesLeg A's failure, quoted:
Verdict: the ordering IS load-bearing — leg A moves a pin that is byte-identical to the one on
origin/main, through consumers this PR does not change, so the reading transfers tomaindirectly.No fork to report, because the spelling shipped preserves it. Leg B is the second half of the answer and is why the measurement was worth doing rather than assuming either way: the pre-existing ordered pin filters
datasetsout (no write type maps into it), so it stayed green through a genuine reordering of the set the snapshot is built from. That gap is closed here by a pin over the whole set, in order.The spelling
The keyed record plus the
-?mapped type —protocol.ts's accumulator in@objectstack/metadata-protocol, the mechanism this repo already proves, adapted from a record of arrays to a record of order marks:An actual derivation, not the completeness assertion a package boundary forced on the sibling card — here both inputs are in one file, exactly as the triage and the engine seat's routing datum both said. A type's keys cannot be materialised as values, so the derivation needs one runtime spelling to derive from; the mapped type makes that spelling complete in both directions (
-?demands a row per collection; the object-literal excess check refuses a row for a collection the interface no longer has), and the array is then computed, so it cannot disagree with the record.Order survives because
Object.keysreturns own enumerable string keys in declaration order (OrdinaryOwnPropertyKeys) — stated in the docblock rather than assumed, together with the integer-like-key caveat that makes it a rule and not a coincidence.The probe after the fix
The same probe that measured exit 0 above, re-run on this branch:
Red in this package, naming the forgotten collection by name, in the file that owns the set — not the second-order red one package over that named this constant nowhere. Both
tsc --noEmitand the DTS build carry it, so the requiredTypeScript Type CheckandBuild Corejobs both hold it.Tests
packages/lint/src/runtime-gate.derived-context-keys.test.ts(new, 4 tests) pins the half a type cannot: the whole set in order, order-independence from the write type, presence-with-empty-value for a collection the host never passed, and validity against the context the gate accepts.It deliberately carries no type-level witness:
packages/lint/tsconfig.jsonexcludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a@ts-expect-errorwritten there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced insrc, where it is compiled, and its failure was measured by the probe above instead.The neighbouring membership pin (
runtime-gate.test.ts, "an absent context still yields empty collections") still holds unchanged — same five members, same emptiness.Three docblocks that asserted the old state were corrected in the same edit, since a stale claim about a guard is the exact defect this card is about: the
RuntimeStackContextdocblock (the derivation is now the mechanism, not only the intent), the constant's own, andNAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYScarries asatisfiesclause, which is validity, not completeness, and the compiler holds nothing else" — true when written, false as of this PR, and now marked as history.Changeset route
Route 1, per the Check Changeset step's own text ("pick by what the PR actually releases"): the diff edits
packages/lint/src, a published package, so it releases something and theskip-changesetroute does not apply.patch— internal, no behaviour change, no public surface change, the same five collections carried in the same order.Checks
Run on the final commit
85ce871b8(dev seatsession_01WLJQhde67SeTccsmnBVarV). All 33 gate families the derivation names for this diff by path and kind, harvested with--commands(never from the prose block), plus the two named in dispatch. Result: 30 pass, 3 NOT MEASURED, 0 red.pnpm --filter @objectstack/lint typecheck— clean;pnpm --filter @objectstack/lint test— 92 files, 2664 tests passed.@objectstack/metadata-protocolrebuilt through turbo (14 tasks) — the cross-package assertion that readsRuntimeStackContextthrough this package'sdist/runtime.d.tsstill compiles.pnpm lint— full repo ESLint scan, clean (not narrowed).check:ratchet-remedy-authorityandcheck:declared-population-live— both green.The three NOT MEASURED are exit 3 — each gate's own code for "prerequisite not met", distinct from a finding's 1, and each says in its own output that this is neither a red nor a pass. All three run with their prerequisite satisfied in CI:
check-test-completenessgrades a savedturbo run testlog that CI tees; it does not run tests and cannot produce one locally.check:dual-build-cjs-loadsreads built output for every package and refuses on a worktree without a full workspacedist/.check:type-check-debtre-measures per ledger entry and refuses without the whole workspace closure built, because measuring from here would measure a different world rather than fail — it prints the demonstration:packages/lintreports 19 errors with its closure built and 147 without, same tree, same commit.Note on that last one:
check:type-check-coverage, its structural half, did run and passed, and its output confirmspackages/lintis already one of the 16 packages whose tests sit outside every tsc program — so the new test file joins an accounted-for population rather than creating one.