fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext - #14285

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation
Sep 2, 2026
Merged

fix(lint): derive the runtime gate's context-collection set from RuntimeStackContext#14285
baozhoutao merged 1 commit into
mainfrom
claude/issue-13977-context-stack-keys-derivation

Conversation

@baozhoutao

@baozhoutaobaozhoutao commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13977

CONTEXT_STACK_KEYS in packages/lint/src/runtime-gate.ts is now derived from RuntimeStackContext instead of hand-listed, so the completeness the docblock has been claiming since #8309 is held by the compiler.

Premise re-check on fresh origin/main

All three anchors are still exactly where the card puts them, at the same line numbers, on origin/main at aca23aba4:

  • :116 — the docblock claim, verbatim: "see CONTEXT_STACK_KEYS, which is derived from this shape and keeps the two from drifting".
  • :293 — the hand-written literal with as const satisfies readonly (keyof RuntimeStackContext)[] — validity, never completeness.
  • :393buildRuntimeWriteSnapshots iterating 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[] to RuntimeStackContext on the pre-fix file:

derivation present? [0] hand-written literal present? [1]
probe key on disk: 1 occurrence(s)
PRE-FIX build exit: 0
check-dts-emitted: @objectstack/lint - 4/4 declared declaration file(s) present.

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_KEYS encodes ("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 grep over .ts/.mts/.mjs, node_modules and dist excluded) — two consumers, plus their transitive readers:

  1. The loop at :393, whose insertion order becomes the snapshot's key order. runtime-gate.derived-name-keys.test.ts reads that back as a value (Object.keys(baseline)) and asserts an orderedtoEqual.
  2. deriveNameKeyedStackKeys, which filters in context order by documented contract, feeding buildTopLevelIndexPattern and thence TOP_LEVEL_INDEX — an alternation whose sourceruntime 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 empty git status --porcelain):

legon-disk orderpre-existing pinnew pin
0 — unmutatedobjects permissions books datasets pagesexit 0exit 0
A — swap objects / permissionspermissions objects books datasets pagesexit 1exit 1
B — swap datasets / pagesobjects permissions books pages datasetsexit 0exit 1

Leg A's failure, quoted:

AssertionError: expected [ 'permissions', 'objects', …(2) ] to deeply equal [ 'objects', 'permissions', …(2) ]
Tests 1 failed | 14 passed (15)

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 to main directly.

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 datasets out (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:

constCONTEXT_STACK_KEY_ORDER={objects: true,permissions: true,books: true,datasets: true,pages: true,}asconstsatisfies{[KinkeyofRuntimeStackContext]-?: true};constCONTEXT_STACK_KEYS=Object.keys(CONTEXT_STACK_KEY_ORDER)asreadonly(keyofRuntimeStackContext)[];

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.keys returns 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:

POST-FIX typecheck exit: 2
src/runtime-gate.ts(379,12): error TS1360: Type '{ readonly objects: true; readonly permissions: true; readonly books: true; readonly datasets: true; readonly pages: true; }' does not satisfy the expected type '{ objects: true; permissions: true; books: true; datasets: true; pages: true; widgets: true; }'.
POST-FIX build exit: 1
Property 'widgets' is missing in type '{ readonly objects: true; ... }' but required in type '{ ...; widgets: true; }'.
DTS Build error

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 --noEmit and the DTS build carry it, so the required TypeScript Type Check and Build Core jobs 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.json excludes its test files by glob and the package has no sibling test tsconfig, so no tsc program compiles that file — a @ts-expect-error written there would evaluate never and delete clean, the phantom-check shape AGENTS.md warns about. Completeness is enforced in src, 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 RuntimeStackContext docblock (the derivation is now the mechanism, not only the intent), the constant's own, and NAME_KEYED_STACK_KEYS's, which read "CONTEXT_STACK_KEYS carries a satisfies clause, 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 the skip-changeset route 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 seat session_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.
  • Dependency closure built first; @objectstack/metadata-protocol rebuilt through turbo (14 tasks) — the cross-package assertion that reads RuntimeStackContext through this package's dist/runtime.d.ts still compiles.
  • pnpm lint — full repo ESLint scan, clean (not narrowed).
  • check:ratchet-remedy-authority and check: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-completeness grades a saved turbo run test log that CI tees; it does not run tests and cannot produce one locally.
  • check:dual-build-cjs-loads reads built output for every package and refuses on a worktree without a full workspace dist/.
  • check:type-check-debt re-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/lint reports 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 confirms packages/lint is 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.

…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.
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json aca23aba40883b2c181053229fc6748d6f7ad0d5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5525164b3635fd4e7da9275e40405fd4043c9f8f — the merge of head 85ce871b8c279f9a6253f7f78d68d3cd46b38c41 into base aca23aba40883b2c181053229fc6748d6f7ad0d5, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 00:23
@baozhoutao
baozhoutao added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit 8e03393Sep 2, 2026
38 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-13977-context-stack-keys-derivation branch September 2, 2026 00:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime publish gate: CONTEXT_STACK_KEYS is a hand-written literal, not the derivation its docblock claims -- completeness is still forgettable

2 participants

@baozhoutao@os-try2026