Skip to content

Commit 8ecfc3d

Browse files
claude[bot]claude
andauthored
fix(metadata-protocol): assert every context collection is routed by CLOSURE_CONTEXT_KEY_BY_TYPE (#13976)
`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. Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L Co-authored-by: Claude <noreply@anthropic.com>
1 parent 47389b3 commit 8ecfc3d

2 files changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
runtime publish gate: assert that every context collection is routed by `CLOSURE_CONTEXT_KEY_BY_TYPE` (#13768)
6+
7+
The last hand-kept spelling of the runtime publish gate's snapshot collection
8+
set now has a completeness guard. `CLOSURE_CONTEXT_KEY_BY_TYPE`'s `satisfies`
9+
clause asks that every key it NAMES is a real `RuntimeStackContext` key —
10+
validity. It never asked that every collection needing a row HAS one, which is
11+
the asymmetry #13390 removed from the four sibling spellings in
12+
`packages/lint/src/runtime-gate.ts` and explicitly left standing here.
13+
14+
Nothing was broken: the table is correct as it stands, and this ships no
15+
behaviour change of any kind — it adds one exported type alias and no runtime
16+
code. What changes is what happens NEXT time the set widens. Adding a key to
17+
`RuntimeStackContext` without the row that routes a metadata type into it now
18+
fails this package's build (`TS2344`, naming the unrouted collection), where
19+
before it compiled clean and the collection silently stayed empty for every
20+
batch — the shape #10377 was filed for.
21+
22+
The guard is an assertion rather than a derivation because the derivation is
23+
not available: the context-collection set exists as a VALUE only in
24+
`CONTEXT_STACK_KEYS`, which is module-private in `@objectstack/lint` and on
25+
neither of that package's entries. Reaching it would mean widening the
26+
deliberately narrow `@objectstack/lint/runtime` entry to buy a red the
27+
already-imported TYPE gives for free.

packages/metadata-protocol/src/runtime-authoring-gate.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,81 @@ export const CLOSURE_CONTEXT_KEY_BY_TYPE = {
401401
page: 'pages',
402402
} as const satisfies Readonly<Record<string, keyof RuntimeStackContext>>;
403403

404+
/**
405+
* Every context collection some row above routes into.
406+
*
407+
* Read off the table rather than restated: the `as const` keeps the values a
408+
* union of literal keys, and the `satisfies` clause above has already pinned
409+
* that each one is a real {@link RuntimeStackContext} key. So this union
410+
* cannot name a collection the context does not have, and the only remaining
411+
* question is the one below.
412+
*/
413+
type RoutedContextCollections =
414+
(typeof CLOSURE_CONTEXT_KEY_BY_TYPE)[keyof typeof CLOSURE_CONTEXT_KEY_BY_TYPE];
415+
416+
/** Context collections with NO row above. Must be empty — see the assertion. */
417+
type UnroutedContextCollections = Exclude<keyof RuntimeStackContext, RoutedContextCollections>;
418+
419+
/**
420+
* `never`, or a compile error naming the collection nobody routes into.
421+
*
422+
* The constraint is the whole mechanism: a non-empty
423+
* {@link UnroutedContextCollections} cannot satisfy `never`, so `tsc` reports
424+
* `Type '"<collection>"' does not satisfy the constraint 'never'` at the
425+
* assertion below — the missing key, by name, at the file that owns the table.
426+
*/
427+
type NoUnroutedContextCollection<Unrouted extends never> = Unrouted;
428+
429+
/**
430+
* COMPLETENESS — the half {@link CLOSURE_CONTEXT_KEY_BY_TYPE}'s `satisfies`
431+
* clause cannot state, and the last one of this set that was still missing.
432+
*
433+
* ## What the `satisfies` above does NOT ask
434+
*
435+
* It asks that every key the table NAMES is a real `RuntimeStackContext` key.
436+
* It does not ask that every collection needing a row HAS one — validity, not
437+
* completeness. That is exactly the asymmetry `NAME_KEYED_STACK_KEYS` carried
438+
* in `@objectstack/lint` before #13390 derived it, one package over.
439+
*
440+
* ## Why it is worth an assertion when nothing is broken
441+
*
442+
* The set is correct as it stands. #13390's ruling is about what "correct
443+
* today" costs: adding the `pages` collection had to touch FIVE spellings of
444+
* this one set and only ONE announced itself, and the unguarded spelling
445+
* produced correct-LOOKING findings whose `path` the caller could not resolve,
446+
* with no test and no gate going red. Four of the five can no longer be
447+
* forgotten. This was the fifth.
448+
*
449+
* ## What goes red, and when
450+
*
451+
* Add a key to `RuntimeStackContext` in `@objectstack/lint` without adding the
452+
* row that routes a metadata type into it, and this package stops building:
453+
* the dts build reports `TS2344` here. Measured, not assumed — a type error
454+
* confined to this file fails `pnpm --filter @objectstack/metadata-protocol
455+
* build` with `DTS Build error`, which is what CI's workspace build runs.
456+
*
457+
* The red arrives after `@objectstack/lint` is REBUILT, because the type
458+
* crosses the package wall through `dist/runtime.d.ts`. That is inherent to
459+
* the boundary and is the same latency the `satisfies` clause above and
460+
* `protocol.ts`'s `-?` accumulator already have; turbo's dependency order
461+
* makes it unconditional in CI.
462+
*
463+
* ## Why an assertion rather than a derivation
464+
*
465+
* A derivation would have to read the context-collection set as a VALUE, and
466+
* `metadata-protocol` cannot: `CONTEXT_STACK_KEYS` is module-private in
467+
* `runtime-gate.ts` and appears on neither of `@objectstack/lint`'s entries.
468+
* Reaching it would mean widening the deliberately narrow
469+
* `@objectstack/lint/runtime` entry — a package-boundary change — to buy the
470+
* same red this costs nothing to get. The TYPE is already here; only the
471+
* completeness question needed asking.
472+
*
473+
* Exported because `noUnusedLocals` is on: a local alias nothing reads is a
474+
* hard `TS6196` here, so an unexported guard would not compile at all.
475+
*/
476+
export type ClosureRoutingCoversEveryContextCollection =
477+
NoUnroutedContextCollection<UnroutedContextCollections>;
478+
404479
/**
405480
* The live collection with this batch's pending drafts folded in — REPLACING
406481
* by name, never appended beside.

0 commit comments

Comments
 (0)