From e3a70257affe647bb00de2efac9219f42e7bbaa0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 21:19:49 +0000 Subject: [PATCH] fix(metadata-protocol): assert every context collection is routed by CLOSURE_CONTEXT_KEY_BY_TYPE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CLOSURE_CONTEXT_KEY_BY_TYPE`'s `satisfies` clause pins validity — every key it names is a real `RuntimeStackContext` key — but never completeness: a context collection with no row routed nothing and nothing went red. That was the last of the five hand-kept spellings of this set still able to be forgotten. Adds a compile-time completeness assertion at the declaration site. The type crossing the package wall (`RuntimeStackContext`) is already imported here, so the guard needs no cross-package data movement and no widening of the deliberately narrow `@objectstack/lint/runtime` entry. Type-only: no runtime code changes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L --- .../closure-context-key-completeness.md | 27 +++++++ .../src/runtime-authoring-gate.ts | 75 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 .changeset/closure-context-key-completeness.md diff --git a/.changeset/closure-context-key-completeness.md b/.changeset/closure-context-key-completeness.md new file mode 100644 index 0000000000..9bc2bfa911 --- /dev/null +++ b/.changeset/closure-context-key-completeness.md @@ -0,0 +1,27 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +runtime publish gate: assert that every context collection is routed by `CLOSURE_CONTEXT_KEY_BY_TYPE` (#13768) + +The last hand-kept spelling of the runtime publish gate's snapshot collection +set now has a completeness guard. `CLOSURE_CONTEXT_KEY_BY_TYPE`'s `satisfies` +clause asks that every key it NAMES is a real `RuntimeStackContext` key — +validity. It never asked that every collection needing a row HAS one, which is +the asymmetry #13390 removed from the four sibling spellings in +`packages/lint/src/runtime-gate.ts` and explicitly left standing here. + +Nothing was broken: the table is correct as it stands, and this ships no +behaviour change of any kind — it adds one exported type alias and no runtime +code. What changes is what happens NEXT time the set widens. Adding a key to +`RuntimeStackContext` without the row that routes a metadata type into it now +fails this package's build (`TS2344`, naming the unrouted collection), where +before it compiled clean and the collection silently stayed empty for every +batch — the shape #10377 was filed for. + +The guard is an assertion rather than a derivation because the derivation is +not available: the context-collection set exists as a VALUE only in +`CONTEXT_STACK_KEYS`, which is module-private in `@objectstack/lint` and on +neither of that package's entries. Reaching it would mean widening the +deliberately narrow `@objectstack/lint/runtime` entry to buy a red the +already-imported TYPE gives for free. diff --git a/packages/metadata-protocol/src/runtime-authoring-gate.ts b/packages/metadata-protocol/src/runtime-authoring-gate.ts index 5572c88aad..e1af707e46 100644 --- a/packages/metadata-protocol/src/runtime-authoring-gate.ts +++ b/packages/metadata-protocol/src/runtime-authoring-gate.ts @@ -401,6 +401,81 @@ export const CLOSURE_CONTEXT_KEY_BY_TYPE = { page: 'pages', } as const satisfies Readonly>; +/** + * Every context collection some row above routes into. + * + * Read off the table rather than restated: the `as const` keeps the values a + * union of literal keys, and the `satisfies` clause above has already pinned + * that each one is a real {@link RuntimeStackContext} key. So this union + * cannot name a collection the context does not have, and the only remaining + * question is the one below. + */ +type RoutedContextCollections = + (typeof CLOSURE_CONTEXT_KEY_BY_TYPE)[keyof typeof CLOSURE_CONTEXT_KEY_BY_TYPE]; + +/** Context collections with NO row above. Must be empty — see the assertion. */ +type UnroutedContextCollections = Exclude; + +/** + * `never`, or a compile error naming the collection nobody routes into. + * + * The constraint is the whole mechanism: a non-empty + * {@link UnroutedContextCollections} cannot satisfy `never`, so `tsc` reports + * `Type '""' does not satisfy the constraint 'never'` at the + * assertion below — the missing key, by name, at the file that owns the table. + */ +type NoUnroutedContextCollection = Unrouted; + +/** + * COMPLETENESS — the half {@link CLOSURE_CONTEXT_KEY_BY_TYPE}'s `satisfies` + * clause cannot state, and the last one of this set that was still missing. + * + * ## What the `satisfies` above does NOT ask + * + * It asks that every key the table NAMES is a real `RuntimeStackContext` key. + * It does not ask that every collection needing a row HAS one — validity, not + * completeness. That is exactly the asymmetry `NAME_KEYED_STACK_KEYS` carried + * in `@objectstack/lint` before #13390 derived it, one package over. + * + * ## Why it is worth an assertion when nothing is broken + * + * The set is correct as it stands. #13390's ruling is about what "correct + * today" costs: adding the `pages` collection had to touch FIVE spellings of + * this one set and only ONE announced itself, and the unguarded spelling + * produced correct-LOOKING findings whose `path` the caller could not resolve, + * with no test and no gate going red. Four of the five can no longer be + * forgotten. This was the fifth. + * + * ## What goes red, and when + * + * Add a key to `RuntimeStackContext` in `@objectstack/lint` without adding the + * row that routes a metadata type into it, and this package stops building: + * the dts build reports `TS2344` here. Measured, not assumed — a type error + * confined to this file fails `pnpm --filter @objectstack/metadata-protocol + * build` with `DTS Build error`, which is what CI's workspace build runs. + * + * The red arrives after `@objectstack/lint` is REBUILT, because the type + * crosses the package wall through `dist/runtime.d.ts`. That is inherent to + * the boundary and is the same latency the `satisfies` clause above and + * `protocol.ts`'s `-?` accumulator already have; turbo's dependency order + * makes it unconditional in CI. + * + * ## Why an assertion rather than a derivation + * + * A derivation would have to read the context-collection set as a VALUE, and + * `metadata-protocol` cannot: `CONTEXT_STACK_KEYS` is module-private in + * `runtime-gate.ts` and appears on neither of `@objectstack/lint`'s entries. + * Reaching it would mean widening the deliberately narrow + * `@objectstack/lint/runtime` entry — a package-boundary change — to buy the + * same red this costs nothing to get. The TYPE is already here; only the + * completeness question needed asking. + * + * Exported because `noUnusedLocals` is on: a local alias nothing reads is a + * hard `TS6196` here, so an unexported guard would not compile at all. + */ +export type ClosureRoutingCoversEveryContextCollection = + NoUnroutedContextCollection; + /** * The live collection with this batch's pending drafts folded in — REPLACING * by name, never appended beside.