Uh oh!
There was an error while loading. Please reload this page.
feat(scripts): gate that kernel-reaching pages are declared in a discoverable shape - #12266
Conversation
…overable shape
What makes something a served page is membership in a manifest bundle's `pages:`
array -- verified against the kernel's own read path, not the card's prose: both
seams that admit a page (registerApp at the manifest, the nested-plugin seam in
registerPlugin) funnel into registerMetadataCollections, which reads
`source['pages']` and hands each entry to registerItem('page', ...). There is no
third entry route; definePage() is PageSchema.parse(), an authoring door whose
result still has to land in a `pages:` array.
The canonical-envelope gates (#11255 -> #11480) discover their population by
export shape instead (`export const X: Page =`), an approximation #11480
measured failing on a live page. Measuring the residual across the tree at
f11fc61 found the class still open in exactly one place: CONNECT_AGENT_PAGE
(packages/mcp), a bare `export const ... = { ... }` reaching the kernel through
CONNECT_AGENT_UI_BUNDLE.pages, in a package carrying no page gate.
check:page-declaration-shape makes the approximation exact by construction: every
IDENTIFIER entry in a `pages:` array must resolve to `export const X: Page =` or
`export const X = definePage(...)`. Judging identifier entries catches 100% of
the real population (34 entries) and admits 0% of the noise -- the 62 inline
literals are all migration fixtures in spec/conversions/registry.ts, and the 181
string entries are book/doc page-NAME lists. Comment masking is the shared
js-comment-mask.mjs, never a private stripComments.
CONNECT_AGENT_PAGE is annotated, the same type-level-only repair #11574 applied
to MarketplaceInstalledPage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6…ndle-pages-ground-truth
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dcfd8bba8f937570d82d05b5766537d92c3dbdcd && git checkout dcfd8bba8f937570d82d05b5766537d92c3dbdcd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce14581f071b01110f31691e03e86b84847aaf00 19c0248b3a481ec8cbf13f6cd6be81f44ea23151 && git checkout -B drift-repro ce14581f071b01110f31691e03e86b84847aaf00 && git merge --no-ff 19c0248b3a481ec8cbf13f6cd6be81f44ea23151
node scripts/docs-audit/affected-docs.mjs --json ce14581f071b01110f31691e03e86b84847aaf00 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11576
The option choice was made by measurement, not by argument
The card records options A / B / C and says plainly "No recommendation recorded". The deciding
number, measured across the tree rather than reasoned about:
Residual = 1. Greater than zero, so the card's own rule selects option A, and the census
below is its first census.
pages:entry classexport const X = definePage(…)export const X: Page = …export const X = { … }The residual is
CONNECT_AGENT_PAGE(packages/mcp/src/connect-ui.ts), authored as a plainexport const CONNECT_AGENT_PAGE = { … }with per-fieldas constand no annotation, reaching thekernel through
manifest.register(CONNECT_AGENT_UI_BUNDLE)(packages/mcp/src/plugin.ts:505), in apackage carrying no page gate. That is the card's class exactly, unfixed — the same shape
MarketplaceInstalledPageshipped in before #11574.The 5 already-annotated pages are platform-objects ×3, cloud-connection ×2, confirming the
card's list is still accurate on today's ref.
Ground truth, verified against the kernel rather than the card's prose
Both seams that admit a page —
registerApp(manifest) and the nested-plugin seam inregisterPlugin— funnel into one body,registerMetadataCollections(
packages/objectql/src/engine.ts), which readssource['pages']for each key ofMETADATA_ARRAY_KEYSand hands every entry toregisterItem('page', …). There is no third entryroute.
definePage()is not one: it isPageSchema.parse(), an authoring-time door whose resultstill has to land in a
pages:array. So the card's ground-truth claim holds, with one refinement itdid not state — the nested-plugin seam is a second
pages:carrier that need not be an exportedbundle.
I also confirmed the third PM assumption directly: both existing gates really do discover by export
shape (
/export\s+const\s+(\w+)\s*:\s*Page\s*=/gover comment-stripped source).The gate
scripts/check-page-declaration-shape.mjs(check:page-declaration-shape) walks every non-testworkspace TypeScript source and asserts that each identifier entry in a
pages:array resolvesto
export const X: Page =orexport const X = definePage(…).Two design decisions worth review:
definePage()is the stronger door — it runsPageSchema.parse()at authoring time — and it is what all 29
examples/pages use. A gate accepting only theannotation would have demanded 29 pages abandon a stronger door for a weaker one.
noise by construction rather than by an exclusion list that would rot: all 62 inline literals are
migration fixtures in
packages/spec/src/conversions/registry.ts(fixture: { before/after }),and an anonymous literal has no declaration to be discoverable at.
Comment masking is the shared
scripts/js-comment-mask.mjs, never a privatestripComments.A blind spot the gate reports rather than hides
examples/app-crm/objectstack.config.ts:86spellspages: Object.values(pages)— a computedcarrier that names no entry in source, so no source scan can enumerate it, including the one the
card proposes. Its single page happens to use
definePage(), but that is a fact about today's tree,not a property this gate holds. It is counted in the success summary and pinned in
--self-test,narrowed so it does not also report
z.array(PageSchema)(a schema) orcount(config.pages)(anumeric summary) — naming either would be a fabricated lead.
Ablation
Direction predicted in writing before running: removing the
: Pageannotation from the realsite turns the production run red with exactly one finding naming
CONNECT_AGENT_PAGE; theself-test stays green. Observed exactly that.
Restore proven byte-identical, via a
trap … EXIT INT TERMin the ablation script:No rebuild step is involved and none is owed: this gate reads
.tssource text directly andresolves nothing through a package's
exports→dist/, so the mutation reaches the subject themoment it lands on disk.
⭐ The invisible-landing-obligation class is BIGGER than #12074 measured
I was handed one undiscoverable obligation up front (
scripts/pm/bare-root-worklist.mjs'sTRIAGEmap, #12074) and then a second, in a different tool —
check:pm-dispatch-gates'sESCAPABLE_LITERAL_LEDGER. #12074 documents only the first. That the class spans two tools withtwo separate shrink-only ledgers is a genuine finding, not a chore.
Neither obligation is owed by this PR, and neither was discharged by buying a ledger row. Both
species require a population literal that builds no watch hint — a bare single-segment word.
Spelling the roots as subtree globs instead makes both unreachable by construction:
escapableLiteralRowsskips any hint containing/(dispatch-gates.mjs:1901), andbareRootLiteralsskips any literal
extractWatchHintscan already see. The declaration is true — the gate reallydoes walk those roots — and the walk roots are derived from the globs, so the declaration cannot
drift from the scan the way a hand-maintained sibling list can. This is the
ROOT_DIR_WATCH_HINTSidiom discharged by construction, remedy (b)'s shape applied to remedy (a)'s situation.
Confirmed empirically, not assumed:
check:page-declaration-shapenames itself in its owndispatch union (
matched via packages/mcp/src/connect-ui.ts ⇢ gate source 'packages/**'), so it didnot land invisible.
pnpm check:pm-dispatch-gateswas run deliberately as asked: exit 0. Two notes for the PM —it is not in this card's derived union of 36 families, contrary to the expectation sent to me, so
running it was a deliberate extra rather than a union item; and no fresh ledger row appeared.
This PR claims
package.jsonand.github/workflows/lint.ymlto wire the gate — the normal wiring,not restructured to dodge anything. PR #12225 (card #12016) already claims both. If
No other open PR may claim the same single-writer pathreds here, that is an ordering conflict,not a defect in this change; #12225 is the other claimant and should land first. Left as draft for
that reason. Nothing was disabled, skipped, or rebased onto #12225.
skip-changesetThe brief anticipated a
scripts/+ workflows-only diff. The measurement falsified that: closing theresidual required annotating a published package's source (
@objectstack/mcp). #11574 shipped apatchfor@objectstack/cloud-connectionfor the byte-identical type-level-only repair, so thisfollows that precedent instead.
skip-changesetwas deliberately not applied.Verification
Union derived at the final commit
19c0248b3awithnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack— 36 families, fresh tree (nostaleness warning). Every exit code captured before any pipe.
36 of 36 green, plus
check:pm-dispatch-gates(exit 0), run at the same commit. No narrowing wasapplied and none is declared — the full union ran.
check:type-check-debtreded on first run for a reason that was not this change: it refuses to--re-measurewithout a built dependency closure. Afterturbo run build --filter='./packages/*' --filter='./packages/*/*'(70/70 successful) it is green,quoting its own verdict line:
Gate's own verdict lines:
Package-level, for the one source file touched:
Heavy runs went through
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0 · held the lock 483s · waited 56sfor the closure build).CI convergence is the PM's to read, per the dispatch contract.
Generated by Claude Code