@@ -127,6 +127,69 @@ const REPO = path.resolve(SPEC, '../..');
127127const SRC = path . join ( SPEC , 'src' ) ;
128128const PIN_FILE = path . join ( REPO , '.objectui-sha' ) ;
129129
130+ /**
131+ * ## The dispatch-gates declaration — the root-FILE idiom
132+ *
133+ * `scripts/pm/dispatch-gates.mjs` derives the gate families a card must run
134+ * from the paths it touches, and it learns a gate's population by scanning the
135+ * gate's own SOURCE TEXT for path-shaped literals (`extractWatchHints`). Every
136+ * path this gate reads is COMPUTED — `PIN_FILE` from `REPO`, `SRC` from
137+ * `SPEC` — so the extractor found nothing here and this gate had no population
138+ * at all. Measured on `7bf96cfd0`, before this declaration existed:
139+ *
140+ * node scripts/pm/dispatch-gates.mjs --commands .objectui-sha --repo objectstack-ai/objectstack
141+ * -> 12 commands (5 matched by path), 0 of them naming check:objectui-pin-citations
142+ *
143+ * and on the same specimen with it, 13 commands (6 matched by path), the new
144+ * one being `pnpm --filter @objectstack/spec run check:objectui-pin-citations`.
145+ *
146+ * A change set consisting only of the pin file is EXACTLY the class this gate
147+ * exists for — a pin bump is the one event that invalidates an asserting
148+ * citation — and it was the class that never derived it. The measured cost was
149+ * a full CI cycle: a pin bump ran 47 derived commands green and then redded
150+ * here on 8 stale citations, discovered a cycle late.
151+ *
152+ * ⛔ The declaration is the POPULATION, never a hand list of the citing files.
153+ * They are discovered by regex on purpose (`sourceFiles(SRC)` + `scanFile`), so
154+ * a list of today's citers is stale the moment a record is written or moved —
155+ * the same reason `check:merge-driver` refuses a hand list of generator names.
156+ *
157+ * A bare `.objectui-sha` carries no path separator and reaches the extractor as
158+ * a bare word; the trailing `/` + `**` suffix is the sanctioned escape for a
159+ * repo-ROOT file — `collapseHint` reduces it back to the literal filename, so
160+ * it claims the root file and no same-named file inside a directory. Nothing in
161+ * this tree lives under `.objectui-sha/`, so it claims no directory either.
162+ * Measured through `hintCovers`, the sole predicate:
163+ *
164+ * hintCovers('.objectui-sha/**', '.objectui-sha') -> true
165+ * collapseHint('.objectui-sha/**') -> '.objectui-sha'
166+ *
167+ * `check-doc-anchors.mjs` declares `README.md`/`ARCHITECTURE.md` this way and
168+ * `git-merge-regen.mjs` declares `package.json` this way; the sibling package-
169+ * scoped gate `check-llms-txt.ts` proves the idiom reaches a gate invoked
170+ * through a pnpm filter — dispatch-gates resolves the `--filter` script back to
171+ * this file and reads it as `gate source`, exactly as it does for that one.
172+ *
173+ * ⚠️ Provenance, NOT a lookup key. Nothing here is joined with `REPO` and
174+ * stat'd: `PIN_FILE` and `SRC` remain the paths this gate opens. The glob form
175+ * used as a path would resolve to nothing and `existsSync` would drop it
176+ * SILENTLY — the "checked nothing, reported green" disease this gate's own
177+ * vacuous-green guard exists to refuse. `selfTest` pins both halves.
178+ */
179+ const ROOT_FILE_WATCH_HINTS = [ '.objectui-sha/**' ] ;
180+
181+ /**
182+ * The other half of the population: the SUBTREE this gate walks for citations.
183+ *
184+ * A pin bump is not the only way a citation goes stale — writing a new record,
185+ * or moving one, changes what this gate has to say about the SAME pin. The
186+ * glob sits in the FINAL segment, so `hintCovers` reaches every source beneath
187+ * `packages/spec/src` and nothing outside it, which is precisely the scope
188+ * `sourceFiles(SRC)` walks (the header's "everything outside `packages/spec/src`"
189+ * exclusion is the same boundary, stated from the other side).
190+ */
191+ const ROOT_DIR_WATCH_HINTS = [ 'packages/spec/src/**' ] ;
192+
130193const LIST = process . argv . includes ( '--list' ) ;
131194
132195/** How far past a `.objectui-sha` mention a citation may reach. */
@@ -379,6 +442,51 @@ function selfTest(): never {
379442 check ( found > 0 , 'discovery: found zero citations in the real tree — the matcher or the scope has moved' ) ;
380443 }
381444
445+ // ── the dispatch-gates declaration ──────────────────────────────────────
446+ // Enforcement cannot hold any of this: the declaration is read by ANOTHER
447+ // tool entirely (`extractWatchHints` in scripts/pm/dispatch-gates.mjs, off
448+ // source text), so a wrong, reworded or deleted entry runs perfectly green
449+ // here and shows up only as a dev dispatched on a `.objectui-sha` pin bump
450+ // who is never told this gate reads it — which is the failure that bought
451+ // this declaration, measured as a full CI cycle after 47 green commands.
452+ {
453+ const declared = [ ...ROOT_FILE_WATCH_HINTS , ...ROOT_DIR_WATCH_HINTS ] ;
454+ const collapse = ( h : string ) : string => h . replace ( / \/ \* + $ / , '' ) ;
455+
456+ // The population the declaration CLAIMS is the population this gate READS.
457+ check (
458+ ROOT_FILE_WATCH_HINTS . length === 1 && collapse ( ROOT_FILE_WATCH_HINTS [ 0 ] ! ) === path . relative ( REPO , PIN_FILE ) ,
459+ `the declared root file is the pin file this gate opens: ${ ROOT_FILE_WATCH_HINTS . join ( ', ' ) } vs ${ path . relative ( REPO , PIN_FILE ) } ` ,
460+ ) ;
461+ check (
462+ ROOT_DIR_WATCH_HINTS . length === 1 && collapse ( ROOT_DIR_WATCH_HINTS [ 0 ] ! ) === path . relative ( REPO , SRC ) ,
463+ `the declared subtree is the tree this gate walks: ${ ROOT_DIR_WATCH_HINTS . join ( ', ' ) } vs ${ path . relative ( REPO , SRC ) } ` ,
464+ ) ;
465+
466+ // The separator is what makes `hintCovers` admit the entry at all — a bare
467+ // filename reaches it as a bare word. A reword back to `.objectui-sha`
468+ // leaves every other signal this gate emits green.
469+ check (
470+ declared . every ( ( h ) => h . includes ( '/' ) ) ,
471+ `every declared entry carries a path separator: ${ declared . join ( ', ' ) } ` ,
472+ ) ;
473+
474+ // Provenance, never a lookup key: joined with REPO these resolve to
475+ // nothing, and `existsSync` would drop them SILENTLY.
476+ check (
477+ declared . every ( ( h ) => ! existsSync ( path . join ( REPO , h ) ) ) ,
478+ `the declared spellings are provenance, not paths: ${ declared . join ( ', ' ) } ` ,
479+ ) ;
480+
481+ // …and the declared subtree really covers the live population, so a
482+ // narrowing of either side cannot pass unnoticed.
483+ const declaredSrc = path . join ( REPO , collapse ( ROOT_DIR_WATCH_HINTS [ 0 ] ?? '' ) ) ;
484+ check (
485+ sourceFiles ( SRC ) . every ( ( f ) => f . startsWith ( declaredSrc + path . sep ) ) ,
486+ 'every source this gate scans lies under the declared subtree' ,
487+ ) ;
488+ }
489+
382490 if ( failures . length ) {
383491 for ( const f of failures ) console . error ( `✗ self-test: ${ f } ` ) ;
384492 console . error ( `\ncheck-objectui-pin-citations --self-test: ${ failures . length } failure(s).\n` ) ;
@@ -388,7 +496,9 @@ function selfTest(): never {
388496 '✅ self-test: asserting citations are checked against the pin and historical ones are not;\n' +
389497 ' a citation wrapped across comment lines is still found (both wrap positions); a sha in\n' +
390498 ' neither spelling and a sha missing its backticks both FAIL; a mention naming no sha is\n' +
391- ' skipped; hex-looking prose without a digit is not a sha; a missing pin file throws.' ,
499+ ' skipped; hex-looking prose without a digit is not a sha; a missing pin file throws; and\n' +
500+ ' the dispatch-gates watch-hint declaration names the pin file and the subtree this gate\n' +
501+ ' really reads, in the separator-carrying spelling `hintCovers` admits.' ,
392502 ) ;
393503 process . exit ( 0 ) ;
394504}
0 commit comments