Uh oh!
There was an error while loading. Please reload this page.
Assert every context collection is routed by CLOSURE_CONTEXT_KEY_BY_TYPE - #13976
Conversation
…CLOSURE_CONTEXT_KEY_BY_TYPE `CLOSURE_CONTEXT_KEY_BY_TYPE`'s `satisfies` clause pins validity — every key it names is a real `RuntimeStackContext` key — but never completeness: a context collection with no row routed nothing and nothing went red. That was the last of the five hand-kept spellings of this set still able to be forgotten. Adds a compile-time completeness assertion at the declaration site. The type crossing the package wall (`RuntimeStackContext`) is already imported here, so the guard needs no cross-package data movement and no widening of the deliberately narrow `@objectstack/lint/runtime` entry. Type-only: no runtime code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift Check4 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 — 8 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 f28bedf5474ef98cd9a19c10fdc128cc7334dcc3 && git checkout f28bedf5474ef98cd9a19c10fdc128cc7334dcc3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb649cb8bc3e17ac0302384eff90067f0f51b838 e3a70257affe647bb00de2efac9219f42e7bbaa0 && git checkout -B drift-repro eb649cb8bc3e17ac0302384eff90067f0f51b838 && git merge --no-ff e3a70257affe647bb00de2efac9219f42e7bbaa0
node scripts/docs-audit/affected-docs.mjs --json eb649cb8bc3e17ac0302384eff90067f0f51b838 |
zhuangjianguo
commented
Aug 31, 2026
PM review — ACCEPT on substance. ⛔ Not released: 11 checks still in flight.
Everything below is measured against the diff, not the seat's report. The report is a claim; the diff is the artifact. 1. ⭐⭐ The STOP I armed did not fire, because the seat found the route that avoids itMy dispatch said: ⛔ if this widens
Verified in the diff: two files, no package-entry change, no 2. ⭐⭐ The ablation is the strongest one this lane has seen todayThe part that matters: the type crosses a package wall through ⭐ That is precisely the naive-probe failure that produced five misreads in this lane today — a secondary text standing in for the primary artifact — caught by the seat itself, before it became a reading. And the probe discriminates, with a firing positive control:
Mode 2 was the right ablation to choose: it also satisfies 3. All four Zone-2 verdicts stated — and A2.4 falsified, as invited
The card measured none of these itself. ⭐ A2.4 was flagged in the order as falsifiable and came back falsified with the mechanism named — that is the outcome I wanted, not the one I predicted. 4. The card's own framing is corrected — filed, not folded inThe card called this the last validity-only spelling. It was not: 5. Clause ② — |
zhuangjianguo
commented
Aug 31, 2026
Released — draft cleared, auto-merge armed 22:00:11Following up the hold in my review above: all 32 checks are now completed — 30 success, 2 skipped (
⇒ No textual conflict is possible. The control is there because a zero from a query that could never match is not a reading — I produced exactly that error twice today, and the third one is not free.
On landing I will verify by content on Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13768
CLOSURE_CONTEXT_KEY_BY_TYPEwas the last of the five hand-kept spellings of theruntime publish gate's snapshot collection set that could still be forgotten
silently. It now carries a compile-time completeness assertion.
Nothing was broken: the table is correct as it stands, and this ships no
behaviour change of any kind — one exported type alias, two internal type
aliases, no runtime code. What changes is what happens the next time the set
widens.
All verification below was run on
e3a70257af, the head of this branch.The four measurements the card asked for and did not make
The card was explicit that it measured none of these. Each is stated with what
it was measured against.
A2.1 — Is
CLOSURE_CONTEXT_KEY_BY_TYPEderivable from inputsmetadata-protocolalready holds? NO, as a value. YES, as a type — which is all the completeness question needs.A value-level derivation needs the context-collection set as a runtime list.
That list is
CONTEXT_STACK_KEYSinpackages/lint/src/runtime-gate.ts:293,and it is module-private — no
exportkeyword at all, so it is absent fromthe root barrel as well as from
@objectstack/lint/runtime.TYPE_TO_STACK_KEY(line 75) is module-private for the same reason.
What the narrow entry does export, measured by reading
packages/lint/src/runtime.ts:six values (
buildRuntimeWriteSnapshots,narrowObjectsToPackageClosure,runRuntimeAuthoringRules,runtimeAuthoringRulesFor,runtimeGatedTypes,stackKeyForType) and five types (RuntimeGateResult,RuntimePackageScope,RuntimeStackContext,AuthoringFinding,AuthoringSeverity).stackKeyForTypeandruntimeGatedTypesare on that entry, sotype -> stackKeyis reachable — but filtering it down to the context collections still needs
CONTEXT_STACK_KEYS, which is not. So a value derivation reaches STOPcondition 1 and was not attempted.
What pins the narrowness:
runtime.ts's own header ("so the kernel boot path canname only the five gate functions below, and so a test can prove it named nothing
else"), and
authoring-rule-wiring.test.ts:423, which fails if this package everimports from the root barrel instead.
The type is a different answer.
RuntimeStackContextis already importedinto
runtime-authoring-gate.ts(line 53) — it has to be, becauseRuntimePendingDeclarationsis an alias of it. Sokeyof RuntimeStackContextisin scope at the declaration site, and the completeness question is answerable
there with no cross-package data movement at all.
A2.2 — Is a completeness assertion cheaper than moving the data? YES, decisively.
Moving the data costs a package-boundary change (widening a deliberately pinned
entry, a maintainer decision) and buys exactly the same red. The assertion costs
nothing: the type it needs is already imported. This PR is the assertion.
A2.3 — Is
protocol.ts's accumulator the right seam instead? NO — it is already guarded, and it does not cover the gap.Measured at
packages/metadata-protocol/src/protocol.ts:15983: the accumulatoris typed
{ [K in keyof RuntimePendingDeclarations]-?: unknown[] }. The-?already makes a new context collection a compile error at that literal. It is
the spelling the card correctly identifies as "the one the compiler could
already see" — it needs nothing.
And it does not close this gap, because the two guards answer different
questions. Two distinct forgetting modes:
RuntimeStackContext, forget everything else.The accumulator goes red already. No new coverage needed.
theNewKey: []there, and forget the routing row. Everything compiledclean before this PR. The collection is accumulated and never populated,
because no draft type routes into it — findings that look correct against a
universe that is silently empty.
Mode 2 is the realistic path precisely because the accumulator is guarded:
the compiler leads you to that one line and then stops. That is the gap this PR
closes, and it is the mode the ablation below reproduces.
protocol.tsis untouched by this PR — the diff is two files, neither ofthem that one — so the region-disjointness measurement against the two held PRs
is moot rather than favourable. For completeness: the accumulator sits at
~15972-15995, disjoint from #13870's regions (~1370-1454, ~10126-10290) and from
#13929's (~6495-6530, ~12830-12995). The ablation mutated that line temporarily
and restored it; restore is proved below by an empty
git diff HEADplus ablob-hash match, and it appears in no commit on this branch.
A2.4 — Does the #13390 template transfer? NO. Falsified, as the card invited.
Read at the source:
NAME_KEYED_STACK_KEYS(runtime-gate.ts:530) isderiveNameKeyedStackKeys(CONTEXT_STACK_KEYS, WRITTEN_STACK_KEYS)andTOP_LEVEL_INDEX(line 543) isbuildTopLevelIndexPattern(NAME_KEYED_STACK_KEYS).Both derive from module-private constants in their own file. Neither input
crosses even the package barrel, let alone the narrow entry.
So the template rests entirely on same-file visibility, and it does not transfer
to a constant one package away. That is the same fact A2.1 reports from the
other side, and it is why the answer here is an assertion rather than a
derivation.
The shape, and why it satisfies #13390's ruling
The ruling asks that forgetting the surface turn something red. Added directly
below the table, in the file that owns it:
RoutedContextCollections— the union of context keys the table routes into,read off the table rather than restated. The existing
satisfiesclausealready pins that each is a real
RuntimeStackContextkey, so this unioncannot name a collection the context does not have.
UnroutedContextCollections—Excludeof that union fromkeyof RuntimeStackContext. Must be empty.NoUnroutedContextCollection— a helper whose parameter is constrainedextends never, so a non-empty remainder cannot satisfy it.ClosureRoutingCoversEveryContextCollection— the assertion.Exported because
noUnusedLocalsis on in the root tsconfig: measured, anunused local type alias is a hard
TS6196here, so an unexported guard wouldnot compile at all. The name is not on the package's published entry
(
runtime-authoring-gate.tsis not re-exported fromsrc/index.ts).Ablation — the deliverable is the red, so here is the red
Mode 2, the realistic forgetting:
RuntimeStackContextgains awidgetscollection AND
protocol.ts's accumulator is updated to satisfy its own guard.@objectstack/lintwas rebuilt on both legs — the type crosses the package wallthrough
dist/, so an unrebuilt ablation would have gone falsely green.Mutation proved on disk before anything was measured:
Anchored greps in both directions (injected text present, replaced text gone)
plus changed blob hashes. The Python replacement asserted an anchor count of
exactly 1 on each file and would have aborted on 0 — the empty-edit trap.
Mutation proved to reach
dist/(a first attempt greppedpackages/lint/dist/runtime.d.ts, which is only a re-export line; the typelands in the content-hashed chunk. The preflight caught it and aborted rather
than measuring the wrong tree):
Leg 1 — guard present:
The error names the unrouted collection, and it is the only non-test error:
the accumulator stayed green because Mode 2 satisfied it.
Leg 2 — same mutated tree, guard reverted to
origin/main:Green. That is the defect, reproduced: before this PR, an unrouted context
collection compiles clean. The pair is the non-vacuity proof — same mutation,
guard present goes red, guard absent goes green. (Both legs exit 2 on the raw
tscbecause this package carries 159 pre-existing*.test.tserrors, measuredas a baseline first; every non-test source is clean, which is why the filter is
non-test files.)
Restore proved, both directions:
Restore used
git checkout HEAD -- ABSOLUTE_PATHunder atrap ... EXIT INT TERM, proved by emptygit diff HEADplus a blob-hash matchagainst the HEAD blob for each file, and by rebuilding
@objectstack/lintandconfirming the marker is absent from
dist/— so no later reading was takenon a mutated tree. The trap fired for real once, on the aborted first attempt.
Clause 2 — self-declared from the actual diff
Path limb: NO. The diff is two files:
.changeset/closure-context-key-completeness.mdandpackages/metadata-protocol/src/runtime-authoring-gate.ts. Nothing underpackages/spec/src/**.Content limb: NO. Measured against the built artifacts rather than asserted:
dist/index.jsanddist/index.cjs— they are type declarations and erasecompletely. No accept/reject behaviour can change when no runtime code does.
dist/index.d.ts.runtime-authoring-gate.tsis not re-exported fromsrc/index.ts, so the export is internal to the package.@objectstack/lint/runtime's entry is not widened — that was the one paththat would have fired this limb, and A2.1 is the measurement that avoided it.
Both limbs read NO, so no
needs:contract-reviewcarrier is attached, per the不预挂 rule.
Verification
Green, on
e3a70257af:pnpm --filter @objectstack/metadata-protocol test—Test Files 147 passed | 2 skipped (149),Tests 2048 passed | 10 skipped (2058), exit 0.pnpm --filter @objectstack/metadata-protocol build— exit 0,check-dts-emitted: 2/2 declared declaration file(s) present.pnpm lint(eslint . --no-inline-config, the whole repo, not a narrowing) — exit 0, clean, 60s.pnpm check:nul-bytes—OK (scanned 7662 text file(s) ... no raw ASCII control bytes).node scripts/check-system-context-census.mjs—OK — 145 anchors resolve. This PR shifts lines in a file the census does not cite, and the gate confirms every anchor still resolves.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 29 commands, 27 exit 0.NOT MEASURED — 2 of 29, both exit 3 = PREREQUISITE NOT MET, neither read as a pass:
node scripts/check-test-completeness.mjs— "PREREQUISITE NOT MET — this gate grades a savedturbo run testlog, and no log was named."pnpm check:dual-build-cjs-loads— "Runpnpm buildfirst. This is NOT a pass: nothing was measured." (51 packages without adist.)Both need a whole-workspace build/test that CI runs anyway. Every gate exit code
was captured before any pipe.
Generated by Claude Code
Generated by Claude Code