Skip to content

feat(scripts): gate that kernel-reaching pages are declared in a discoverable shape - #12266

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-11576-bundle-pages-ground-truth
Aug 25, 2026
Merged

feat(scripts): gate that kernel-reaching pages are declared in a discoverable shape#12266
yinlianghui merged 2 commits into
mainfrom
claude/issue-11576-bundle-pages-ground-truth

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

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:

How many object literals sit inside an exported bundle's pages: array (or reach the kernel via
definePage) and are not reachable by the export-shape scan — i.e. lack the : Page
annotation — in packages that do not already carry a page gate?

Residual = 1. Greater than zero, so the card's own rule selects option A, and the census
below is its first census.

pages: entry classcountreachable by the export-shape scan?
identifier → export const X = definePage(…)28yes — the parsing door
identifier → export const X: Page = …5yes — and both owning packages already carry a gate
identifier → bare export const X = { … }1NO — the residual
inline object literal62n/a — every one a migration fixture (see below)
string page-name181n/a — book / doc-site nav, a reference not a declaration

The residual is CONNECT_AGENT_PAGE (packages/mcp/src/connect-ui.ts), authored as a plain
export const CONNECT_AGENT_PAGE = { … } with per-field as const and no annotation, reaching the
kernel through manifest.register(CONNECT_AGENT_UI_BUNDLE) (packages/mcp/src/plugin.ts:505), in a
package carrying no page gate. That is the card's class exactly, unfixed — the same shape
MarketplaceInstalledPage shipped 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 in
registerPlugin — funnel into one body, registerMetadataCollections
(packages/objectql/src/engine.ts), which reads source['pages'] for each key of
METADATA_ARRAY_KEYS and hands every entry to registerItem('page', …). There is no third entry
route.
definePage() is not one: it is PageSchema.parse(), an authoring-time door whose result
still has to land in a pages: array. So the card's ground-truth claim holds, with one refinement it
did not state — the nested-plugin seam is a second pages: carrier that need not be an exported
bundle
.

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*=/g over comment-stripped source).

The gate

scripts/check-page-declaration-shape.mjs (check:page-declaration-shape) walks every non-test
workspace TypeScript source and asserts that each identifier entry in a pages: array resolves
to export const X: Page = or export const X = definePage(…).

Two design decisions worth review:

  • Both doors are accepted.definePage() is the stronger door — it runs PageSchema.parse()
    at authoring time — and it is what all 29 examples/ pages use. A gate accepting only the
    annotation would have demanded 29 pages abandon a stronger door for a weaker one.
  • Only identifier entries are judged, which admits 100% of the real population and 0% of the
    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 private stripComments.

A blind spot the gate reports rather than hides

examples/app-crm/objectstack.config.ts:86 spells pages: Object.values(pages) — a computed
carrier 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) or count(config.pages) (a
numeric summary) — naming either would be a fabricated lead.

Ablation

Direction predicted in writing before running: removing the : Page annotation from the real
site turns the production run red with exactly one finding naming CONNECT_AGENT_PAGE; the
self-test stays green. Observed exactly that.

injected present (expect 1): 1 deleted absent (expect 0): 0
ABLATED_PROD_EXIT=1 → "check-page-declaration-shape: 1 problem(s)"
ABLATED_SELFTEST_EXIT=0

Restore proven byte-identical, via a trap … EXIT INT TERM in the ablation script:

HEAD blob: 42ada6a679623486368d7e86341c5e311a02ea91
working blob: 42ada6a679623486368d7e86341c5e311a02ea91 → identical
git status --porcelain: (empty)

No rebuild step is involved and none is owed: this gate reads .tssource text directly and
resolves nothing through a package's exportsdist/, so the mutation reaches the subject the
moment 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's TRIAGE
map, #12074) and then a second, in a different toolcheck:pm-dispatch-gates's
ESCAPABLE_LITERAL_LEDGER. #12074 documents only the first. That the class spans two tools with
two 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:

constPAGE_CARRIER_GLOBS=['packages/**','examples/**','apps/**'];

escapableLiteralRows skips any hint containing / (dispatch-gates.mjs:1901), and bareRootLiterals
skips any literal extractWatchHints can already see. The declaration is true — the gate really
does 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_HINTS
idiom discharged by construction, remedy (b)'s shape applied to remedy (a)'s situation.

Confirmed empirically, not assumed: check:page-declaration-shapenames itself in its own
dispatch union (matched via packages/mcp/src/connect-ui.ts ⇢ gate source 'packages/**'), so it did
not land invisible.

pnpm check:pm-dispatch-gates was 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.

⚠️ Expected: single-writer path conflict with #12225

This PR claims package.json and .github/workflows/lint.yml to 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 path reds 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.

⚠️ Deviation: this PR carries a changeset, and must NOT get skip-changeset

The brief anticipated a scripts/ + workflows-only diff. The measurement falsified that: closing the
residual required annotating a published package's source (@objectstack/mcp). #11574 shipped a
patch for @objectstack/cloud-connection for the byte-identical type-level-only repair, so this
follows that precedent instead. skip-changeset was deliberately not applied.

Verification

Union derived at the final commit 19c0248b3a with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — 36 families, fresh tree (no
staleness 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 was
applied and none is declared — the full union ran.

check:type-check-debt reded on first run for a reason that was not this change: it refuses to
--re-measure without a built dependency closure. After
turbo run build --filter='./packages/*' --filter='./packages/*/*' (70/70 successful) it is green,
quoting its own verdict line:

check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 289.3s,
1843 raw tsc error(s) total, none above its recorded number.

Gate's own verdict lines:

check-page-declaration-shape: OK — 34 page entries across 2171 sources under packages/**,
examples/**, apps/** all reach the kernel through a discoverable declaration (`: Page` or
`definePage()`).
not judged: 62 inline literal(s) in 1 file(s) …, 181 string page-name reference(s) …
blind spot: 1 computed carrier(s) no source scan can enumerate — examples/app-crm/objectstack.config.ts:86
check-page-declaration-shape --self-test: all passed (17 cases)

Package-level, for the one source file touched:

pnpm --filter @objectstack/mcp typecheck → exit 0 (tsc --noEmit, no diagnostics)
pnpm --filter @objectstack/mcp test → Test Files 22 passed (22) · Tests 242 passed (242)

Heavy runs went through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 · held the lock 483s · waited 56s for the closure build).

CI convergence is the PM's to read, per the dispatch contract.


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/l ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • 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 — 12 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 ce14581f071b01110f31691e03e86b84847aaf00packageMentionDocs.

Which tree this was computed on

This run read content/docs from dcfd8bba8f937570d82d05b5766537d92c3dbdcd — the merge of head 19c0248b3a481ec8cbf13f6cd6be81f44ea23151 into base ce14581f071b01110f31691e03e86b84847aaf00, 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 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

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 15:48
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 8649b39Aug 25, 2026
35 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-11576-bundle-pages-ground-truth branch August 25, 2026 16:36
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/ltooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export-shape page discovery cannot see an un-annotated raw-literal Page — bundle pages: arrays are the ground truth the scans approximate

2 participants

@yinlianghui@claude