Skip to content

test(mcp): gate CONNECT_AGENT_PAGE on canonical expression envelopes - #12345

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-12269-mcp-canonical-envelope-gate
Aug 25, 2026
Merged

test(mcp): gate CONNECT_AGENT_PAGE on canonical expression envelopes#12345
yinlianghui merged 2 commits into
mainfrom
claude/issue-12269-mcp-canonical-envelope-gate

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12269

packages/mcp was the third package shipping a raw-literal Page that reaches the kernel through its own manifest bundle, and the only one of the three without the canonical-expression-envelope gate. This adds the thin gate, taking closure A from the card.

CONNECT_AGENT_PAGE authors zero expression keys today. That is the argument for the gate rather than against it, and #11480's own header states it:

They author ZERO expression keys today, which is exactly why the gate is worth having: the hazard is the NEXT predicate added to one of them, which would ship bare with every authoring-time signal green.

The page body is one mcp:connect-agent widget plus a page:header, both live SDUI surfaces that can grow a visibleWhen at any time.

Third hand-copy, recorded as such

This is the third per-package copy of the same shape (platform-objects#11255, cloud-connection#11480, this one). That count is live evidence for hoisting the population discovery into one shared helper instead of repeating it — #11576's option C, and the subject of #12307, which stays that card's question rather than this one's. Recorded here so the count is inherited rather than the habit.

The copy was held behind #12317 deliberately: until that merged, both sibling gates carried the naive two-regex comment stripper that #9367 retired from six gates, and a third hand-copy would have carried the defect forward a third time. Both siblings were read on this branch's base before anything was copied — both now import maskComments from the shared mask, and the private stripper is gone from both.

Which sibling was copied, and why

cloud-connection's. platform-objects discovers its audited pages through a page barrel (import * as pageExports from './index.js'), which packages/mcp does not have — its page lives in a *-ui.ts file. cloud-connection's shape (explicit audited list, checked against the source scan in both directions) is also strictly the better one: it reds both when a declared page is unaudited and when an audited page falls out of the : Page scan. Its import.meta.url seed is kept rather than platform-objects' __dirname, which that file uses only to dodge a TS1470 its own package config forces.

The population is discovered by export shape over src/, never by a hard-coded name.

The page population was swept, not assumed

The card names one page. Swept packages/mcp/src for every export const … : Page =, every regions: and pages: literal, every *_UI_BUNDLE, and every .register( call site. Result: exactly one page (CONNECT_AGENT_PAGE), one bundle (CONNECT_AGENT_UI_BUNDLE), one register call (plugin.ts:505). A gate that audits one page in a package shipping two would be worse than no gate, so this was checked rather than taken from the card.

Door 3 carries one recorded exemption

page:header has a ComponentPropsMap row; mcp:connect-agent does not — it is a console-registered widget, so door 3 has no schema to read its properties with. This is the same standing-exemption state cloud-connection's two widgets were in between #11480 and #11575, and giving this type its row is that same piece of work, outside this card's declared surface. Filed as #12344.

Contained rather than covered, three pins together:

  • the unmapped set is asserted EXACTLY, so any NEW unmapped type reds;
  • the exempted widget's props bag is pinned EMPTY — nothing authored is nothing to serve bare;
  • a non-vacuity pin reds if the exempted type stops appearing on the page, so the exemption cannot rot into one that covers nothing.

The gate is proven to fail — both ways

A gate whose population scan silently returns zero pages passes exactly like a gate over a clean page, and those two greens mean opposite things. Both were ablated, each mutation confirmed on disk by counting the injected text and the anchor text (an editor's exit code is not evidence), each restored under a trap … EXIT INT TERM and proved byte-identical with git hash-object.

Leg A — a bare predicate planted in the shipped page literal. Anchor count 1 to 0, injected count 0 to 1. Result: 3 failed | 10 passed (13), the verdict test among them —

FAIL 'CONNECT_AGENT_PAGE' authors NO bare expression string
FAIL flags CONNECT_AGENT_PAGE the moment a bare predicate lands on the connect widget
FAIL flags the header region too — the other live SDUI surface on this page

(the two downgrade controls red on their pristine re-audits, correctly: the export is no longer pristine.) Restored: 42ada6a679623486368d7e86341c5e311a02ea91, identical to before.

Leg B — the population emptied. Scan regex made to match nothing and the audited list emptied, i.e. the gate finds no pages at all. Result: 4 failed | 2 passed (6), the floor assert firing by name:

FAIL covers every `Page` declared in this package — and audits nothing undeclared
AssertionError: expected 0 to be greater than or equal to 1

Worth reading the counts: 6 tests instead of 13, because an empty population registers no it.each cases. Without the floor assert an empty scan would silently drop 7 tests and report green — which is exactly the reading the floor exists to separate.

Rebuild leg — does one apply here? Not to the mutations. Both ablated files are read from source: vitest resolves ./connect-ui.js inside the package to src/, and @objectstack/lint through the new anchored alias to lint/src/index.ts, so neither mutation passes through a dist/. One thing in the measured graph does: the detector imports ComponentPropsMap / PageComponentSchema / PageSchema from @objectstack/spec/ui, which resolves through exports to packages/spec/dist. That closure was built in this worktree before any measurement (pnpm --workspace-concurrency=2 --filter '@objectstack/mcp^...' build, and again on the merged head), and this diff touches no packages/spec source, so that dist is current for every number above.

Cross-package inputs declared, not worked around

The maskComments import escapes the package, so scripts/cross-package-test-inputs.mjs gains an @objectstack/mcp entry and turbo.json a matching @objectstack/mcp#test task hashing the same two paths — the .d.mts alongside the .mjs, because it is what gives maskComments its type and so is an input to this package's typecheck verdict too. Same declaration both siblings carry.

The vitest alias is appended to this package's existing alias array rather than replacing it — packages/mcp already aliased @objectstack/metadata-core, and both entries stay anchored so neither swallows a subpath.

No changeset — reasoning, not assumption

The diff touches no published packages/mcp source. packages/mcp publishes dist only (files: ["dist", "README.md", "CHANGELOG.md"]), and the change is a test file, a devDependencies line, a vitest config, a lint-roster entry and a turbo task. Nothing a consumer can observe changes; no dist output moves. #12317 — the same kind of diff over these same gates — shipped no changeset either. skip-changeset label applied.

Verification

Gate union derived at the final commit 0efd5f8bce with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack. The first derivation printed STALE TREE (6 commits behind, 6 files it derives from changed, lint.yml and the script itself among them), so origin/main was merged at c48d46d70a and the union re-derived clean — which added check:bash32-floor to the matched list.

All run on 0efd5f8bce, exit codes captured before any pipe:

checkverdict line
@objectstack/mcp suiteTest Files 23 passed (23) · Tests 255 passed (255)
@objectstack/mcp typecheckexit 0, no diagnostics
check:cross-package-test-inputsOK: 18 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check:type-check-coveragecheck-type-check-coverage: OK — 65/78 workspace packages type-checked (plus the root), 13 in the DEBT ledger …
check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 45 published subpath(s) resolved through every alias table.
check:page-declaration-shapeOK — 34 page entries across 2188 sources … all reach the kernel through a discoverable declaration
check:published-files69 publishable package(s) of 78 workspace member(s) declare a files whitelist …
check:engine-double-contractexit 0
check:where-matcherOK self-test: separates conjoining, early-returning, combinator-blind and refusing
check:query-options-erasureratchet holds: 67 unswept non-test site(s) in 17 file(s), none new
check:type-source-resolutionOK — 93 tsc program(s) across 77 packages scanned
check:slot-lookupexit 0
check:parse-guard167 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.
check:entry-guard168 scripts/ file(s) — every entry guard goes through invoked-as.mjs
check:agent-test-spelling0 violations — 384 file(s) · 3934 bare separator token(s) · 1167 launcher-rooted run(s)
check:pnpm-filter-targets136/173 --filter occurrence(s) across 28 file(s) resolve against 78 workspace package(s)
check:cli-command-ids276 command-id literal(s) across 99 file(s) … all resolve to a real command path
check:bash32-floor20 tracked shell file(s) … name no bash 4+ construct
check:nul-bytesOK (scanned 6834 text file(s) … no raw ASCII control bytes)
pnpm lint (whole repo, not narrowed)exit 0, no findings

Not run locally, left to CI: check:type-check-debt --re-measure (needs the full workspace closure built), and the docs-audit, osv-exemptions and changeset-fixed families that need CI context. Every heavy run went through scripts/pm/os-verify-lock.sh.

Ⓘ One derivation note worth carrying: two ablation legs were first run with a repo-relative path handed to a vitest invoked with the package as cwd. Both exited 1 on No test files found — a false red that reads exactly like a successful ablation. They were re-run with the package-relative filter, and only the re-run is reported above.


Generated by Claude Code

packages/mcp is the third package shipping a raw-literal Page that reaches the
kernel through its own manifest bundle, and the only one without the
canonical-expression-envelope gate the other two carry. CONNECT_AGENT_PAGE
authors zero expression keys today, which is the argument for the gate rather
than against it: the hazard is the NEXT predicate added to it, which would ship
bare with every authoring-time signal green.
Thin gate copying the cloud-connection shape: the shared detector from
@objectstack/lint, the @objectstack/lint devDependency, and an anchored vitest
source alias appended to this package's existing alias array so the gate judges
lint's SOURCE rather than a possibly-stale dist. Population is discovered by
export shape over src/ through the shared maskComments, never by a hard-coded
name, with a floor so a scan that reads nothing cannot pass as a clean page, and
a phantom-comment pin so comment-shaped text cannot delete a page from it.
Door 3 has one recorded exemption: mcp:connect-agent is a console-registered
widget with no ComponentPropsMap row, the same standing-exemption shape
cloud-connection's two widgets were in before #11575 gave them rows. The
exemption is asserted exactly, its bag is pinned empty, and a non-vacuity pin
reds if the type ever stops appearing on the page.
The cross-package maskComments import is declared in
scripts/cross-package-test-inputs.mjs and hashed by a matching
@objectstack/mcp#test entry in turbo.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️1 changed file(s) yielded no anchor (packages/mcp/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/vitest.config.ts) — pages documenting those are invisible to this run
  • 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 cf99875ea88a6a611353442e76c93de70727b74dpackageMentionDocs.

@github-actionsgithub-actionsBot added size/m dependencies Pull requests that update a dependency file tests labels Aug 25, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM review of #12269.

The claim I asked you to falsify, re-measured here

Zone 2b said the card names one page and told you a gate auditing one page in a package shipping two is worse than no gate. Checked independently on origin/main:

export const … : Page =
packages/mcp/src/connect-ui.ts:23 export const CONNECT_AGENT_PAGE: Page = {
bundle connect-ui.ts:55 export const CONNECT_AGENT_UI_BUNDLE = {
register plugin.ts:505 manifest?.register?.(CONNECT_AGENT_UI_BUNDLE);
pages: connect-ui.ts:63 pages: [CONNECT_AGENT_PAGE],

Exactly one page, one bundle, one register call — your sweep holds.

Positive control, because a single hit could just as easily be a broken pattern:

cloud-connection/src/cloud-connection-ui.ts : 1
cloud-connection/src/marketplace-ui.ts : 2
platform-objects/src/pages/sys-*.page.ts : 1 each

The same pattern finds pages where they exist, so the single hit in packages/mcp is a fact about the package, not about my grep.

⭐ And the control says something your prose only implies: cloud-connection ships three pages, not one. That is a harder reason for copying its shape than the ones you gave — its explicit-audited-list-checked-both-ways design was built against a genuinely plural population, so it is the sibling whose shape has actually been exercised. Copying the platform-objects barrel into a package with no barrel would have been the mistake; you avoided it for the right reason and it turns out to have a second one.

Leg B is the best thing in this PR

4 failed | 2 passed (6) … 6 tests instead of 13, because an empty population registers no it.each cases. Without the floor assert an empty scan would silently drop 7 tests and report green.

That is precisely the trap Zone 2d named — a gate whose population scan returns zero passes exactly like a gate over a clean page, and both greens are green. You did not just add the floor assert, you measured what it buys: seven tests that would have vanished without a single red anywhere. The test-count delta is the evidence; the assertion message alone would not have been.

⭐ The Ⓘ note — a false RED, and the second self-caught instrument failure today

two ablation legs were first run with a repo-relative path handed to a vitest invoked with the package as cwd. Both exited 1 on No test files found — a false red that reads exactly like a successful ablation.

This is the mirror of the failure this lane keeps hitting from the other side. An ablation is supposed to produce a red, so a red from the harness being wrong is indistinguishable from success — and unlike a false green, nothing about it feels wrong to the author. Catching it required reading why it was red rather than that it was red. Reporting only the re-run is right.

(For the record: #11363's dev caught the same class in the opposite direction the same hour — a grep bracket expression that could not match multibyte marks, reporting "no cases went red" for two legs that did. Two devs, two self-caught instrument failures, opposite signs.)

The rest

Landing conditions

  1. Lint & Repo Gates reports completed + success, read by name.
  2. Nothing else. skip-changeset is already on the PR.

Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] packages/mcp ships a kernel-reaching Page with no canonical-envelope gate — now discoverable, still unaudited

2 participants

@yinlianghui@claude