Skip to content

Commit 8649b39

Browse files
yinlianghuiclaude
andauthored
feat(scripts): gate that kernel-reaching pages are declared in a discoverable shape (#12266)
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. Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 071ec4e commit 8649b39

5 files changed

Lines changed: 537 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/mcp': patch
3+
---
4+
5+
`CONNECT_AGENT_PAGE` is declared `: Page` (type-level only; no runtime change) so export-shape page discovery can see it. It reaches the kernel through `CONNECT_AGENT_UI_BUNDLE.pages`, but was authored as a bare `export const CONNECT_AGENT_PAGE = { … }` with per-field `as const` — the same shape `MarketplaceInstalledPage` shipped in before #11574, and invisible to the export-shape scan the canonical-envelope gates (#11255, #11480) discover their population with. A new repo-wide gate, `check:page-declaration-shape`, now closes the class: every identifier in a bundle's `pages:` array must be declared `export const X: Page =` or through `definePage()` (#11576).

.github/workflows/lint.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,17 @@ jobs:
11711171
- name: React pages honour the useAdapter() query and result contracts
11721172
run: pnpm check:react-page-adapter-contract
11731173

1174+
# #11576 page discovery vs ground truth: what makes something a SERVED
1175+
# page is membership in a bundle's `pages:` array (the kernel reads it in
1176+
# `registerMetadataCollections`), not its type annotation — while the
1177+
# canonical-envelope gates (#11255, #11480) discover their population by
1178+
# export shape. #11480 measured that gap on a live page. This gate makes
1179+
# the approximation exact by construction: every identifier in a `pages:`
1180+
# array must be declared `export const X: Page =` or via `definePage()`,
1181+
# so a page can never again be served-but-uncovered.
1182+
- name: Kernel-reaching pages are declared in a discoverable shape
1183+
run: pnpm check:page-declaration-shape
1184+
11741185
# #10830 form-section `name` in docs examples: the docs pages that TEACH
11751186
# the i18n-anchor convention were the one surface where nothing enforced
11761187
# it. `packages/lint`'s `translation-section-name-missing` walks app

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"check:docs-image-tag": "node scripts/check-docs-image-tag.mjs --self-test && node scripts/check-docs-image-tag.mjs",
4646
"check:docs-image-tag-sync": "node scripts/sync-docs-image-tags.mjs --self-test",
4747
"check:react-page-adapter-contract": "node scripts/check-react-page-adapter-contract.mjs --self-test && node scripts/check-react-page-adapter-contract.mjs",
48+
"check:page-declaration-shape": "node scripts/check-page-declaration-shape.mjs --self-test && node scripts/check-page-declaration-shape.mjs",
4849
"check:template-version-sync": "node scripts/sync-template-versions.mjs --self-test",
4950
"check:role-word": "node scripts/check-role-word.mjs --self-test && node scripts/check-role-word.mjs",
5051
"check:quick-reference-counts": "node scripts/check-quick-reference-counts.mjs --self-test && node scripts/check-quick-reference-counts.mjs",

packages/mcp/src/connect-ui.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
* (`OS_MCP_SERVER_ENABLED=false`) gets no page and no nav entry.
1919
*/
2020

21-
export const CONNECT_AGENT_PAGE = {
21+
import type { Page } from '@objectstack/spec/ui';
22+
23+
export const CONNECT_AGENT_PAGE: Page = {
2224
name: 'connect_agent',
2325
label: 'Connect an Agent',
2426
type: 'app' as const,

0 commit comments

Comments
 (0)