…channel
A stored page document's `type` is the spec page KIND, and PageView writes it
verbatim into the SchemaNode discriminator ComponentRegistry dispatches on. The
PageRenderer registrations exist because of that line, but nothing said so at
either place an auditor arrives, so two cards read them as "registered but never
declared" and one reached a draft PR that would have stopped every stored
kind-`app` page rendering.
Declared at both reading ends (the SchemaRegistry map's `page` entry, and the
registrations themselves), at the page-kind vocabulary's own face
(SpecPageFields' `type` omission bullet), and at the writing end. Pinned by a
new test that names the channel when it breaks, including that the registry key
`app` serves the spec page kind and not AppComponentSchema's app-level document
-- two vocabularies sharing one token.
No registration is added, removed or renamed; no accept set moves; no name joins
a published barrel; the upstream enum is read and never edited.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
Fixes #9642
Clause-②: no — re-derived, not inherited. This branch adds prose and one test. No
ComponentRegistry.registercall is added, removed or renamed; no Zod arm gains or loses a key;no accept set moves; nothing joins a published barrel (the new symbols are a test file's locals,
and
packages/types'SchemaRegistrymap is untouched — adding a page-kind key there would widenkeyof SchemaRegistry= the publishedComponentTypeunion, which the map's own docblock calls aruling, so it is declared in a comment instead). The upstream enum lives in the sibling repository
and was read there on
origin/main, never edited. ⇒ recording a coupling that already existsneither relaxes an accept set nor enlarges a public surface.
What the coupling actually is — and one place the card's own framing is wrong
Measured on objectui
92fba11eband objectstacka7e9a66, 2026-09-17T16:30Z UTC (clock read, not typed).@objectstack/spec'sPageTypeSchema.optionsreadsrecord, home, app, utility, list— five kinds,and
pageis refused by it. ThePageRendererregistrations readpage, app, utility, home, record.⇒ They are not "the five page kinds." Four kinds are registered;
listis not; andpage—which is not a kind at all — is the fallback the same two-line mapping writes for a stored document
carrying no
type.listis absent because it is ADR-0047 interface mode:PageViewbranches oninterfaceConfig?.sourceand rendersInterfaceListPagedirectly, so those documents never reachSchemaRenderer. Every storedlistpage in the showcase carries that key (5/5 files).The registry resolution was read directly rather than inferred (probe run, then deleted):
pageapphomerecordutilityPageRendererlistzzz(absent-token control)undefinedTwo controls on one instrument:
listreading defined-but-not-PageRenderer separates "servedelsewhere" from "unregistered", and the absent token proves a zero out of this probe is absence
rather than a dead query.
Where the declaration went, and why an auditor from each direction meets it
Two seats reasoned from a comment that was already there — the one at the WRITING end, in
PageView.tsx— and both got it wrong, because neither was reading that end. So the declaration isplaced at the READING ends, one per direction, plus one thing that cannot silently stay true.
packages/types/src/registry.tsappa declared node type?" is answered here, withno. The note sits at thepageentry and says why the gap is not the "registered but never declared" defect, and why adding keys is ⛔ not the remedy. This is the exact question both mis-framed cards asked.packages/types/src/zod/layout.zod.tsSpecPageFields'typeomission bullet already said spec'stypeis the kind and objectui's is the discriminator — and stopped there, reading as though the collision were resolved. It is not: the node that reaches the registry carriestype: 'app', whichPageNodeSchema'sz.literal('page')would refuse. The bullet now states that.packages/components/src/renderers/layout/page.tsxpackages/app-shell/src/views/PageView.tsxpackages/components/src/__tests__/page-kind-node-type-channel-9642.test.tsxpackages/components/src/__tests__/page-single-h1.test.tsxtype: 'app'— the edit to'page'was never merged, so its signal is intact. The note says the value is evidence about a real stored document and points at the pin..changeset/page-kind-node-type-channel-9642.md//line comments do notsurvive into the published
.d.ts(0//hits indist/registry.d.tsagainst 3 survivingJSDoc blocks in the same file as the lit control), and the
SpecPageFieldsdocblock never projectedeither — its own pre-existing phrase "genuinely collide" reads 0 in
dist/zod/layout.zod.d.tsagainst 29 surviving JSDoc blocks there. ⇒ these declarations reach a contributor reading source,
⛔ not a consumer reading the published types, and published bytes do not move.
The scope note's sentence, and where it landed
The director's scope note adds one sentence:
AppComponentSchema'sappand the spec page kindappare two vocabularies sharing one token. It landed in three places, primary first:packages/types/src/registry.ts— primary, because that is where the token's absence from thedeclared key set is read, and
AppComponentSchemalives in the same package.packages/components/src/renderers/layout/page.tsx— beside theappregistration itself.registryAppKeyServesThePageKindcase — the mechanical half of the sentence, assertingthat the registry key
appanswers withPageRenderer(the page kind) and carrying the sentencein its failure message.
Reverse verification of the pin
Committed first, then ablated, per the repo's rule. Mutation: the
appregistration replaced with amarker line in
page.tsx.appregistration count1 -> 0, injected marker count0 -> 1, blob48fb716 -> 78f2858Tests 3 failed | 2 passed (5), failing with the channel's own words: "page kind 'app' ... ComponentRegistry must answer it with PageRenderer. It does not. Either the registration was removed — every stored 'app' page then renders OBJUI-001 instead of the page ..."trap '... checkout HEAD -- PATH' EXIT INT TERM; afterwards blob back to48fb716, marker count 0,git diff HEADempty../renderersand../renderers/layout/pageas relative source, so nodistsits between the mutation and the reading⛔ Not exercised, stated rather than implied: the "upstream enum grows a kind nothing serves" leg
would need the installed
@objectstack/specmutated, which was not done.Gates — every exit code
Run at
5b293107funless noted. Heavy legs went through../objectstack/scripts/pm/os-verify-lock.sh.pnpm exec vitest run packages/components/ packages/types/ packages/app-shell/(locked)Test Files 1190 passed (1190),Tests 14405 passed / 1 skippedturbo run type-check --filter=@object-ui/types --filter=@object-ui/components --filter=@object-ui/app-shell(locked)turbo run build --filter=@object-ui/console... --concurrency=2(locked)pnpm exec eslintover the 6 touched files (the repo's ownlintform)no-explicit-anycheck-changeset-presencecheck-control-bytescheck-new-cross-file-line-citationscheck-registry-bare-name-collisionscheck-sdui-registration-pinscheck-comment-mask-corpuscheck-changeset-claimscheck-pending-changeset-literalscheck-test-path-rootscheck-component-surface-paritycheck-spec-symbol-derivationcheck-doc-component-typescheck-unreferenced-sourcescheck-vi-mock-specifierscheck-handler-key-read-sitescheck-governed-queue-guard --test(7 paths)Narrowing declared: the test leg was scoped to the three packages this diff touches rather than
pnpm testrepo-wide, and the gate list was derived from this repository's ownpackage.jsonand.github/workflows/—scripts/pm/dispatch-gates.mjslives in the sibling repository and refusesobjectui paths by design, so there is no
--commandsoutput to quote here. It is stated, ⛔ notsubstituted.
pnpm installWAS run in the worktree, so no reading was lost to a missing install.A repo-wide
eslint .was not run: the repo'slintis per-package via turbo, and the six touchedfiles were linted directly.
--no-inline-configis not this repository's lint form and reported one spurious error(
react-hooks/static-componentsat a pre-existing line that carries its owneslint-disable-next-line);re-run in the repo's own form it is 0. Recorded because the wrong form reads as a real finding.
check:changeset-claims— ten verdicts, read by paragraphThe gate is report-only and exits 0. 10 pending entries name a file this diff touches. Verdict
per entry, each with the reading behind it. ⛔ Nothing was corrected: this diff falsifies none of them.
The decisive instrument, covering the eight "claims about code"
Every one of the five modified files is byte-identical to its base after comment stripping —
stripped through the TypeScript scanner (
SingleLineCommentTrivia/MultiLineCommentTriviadropped), sha256 compared:
packages/app-shell/src/views/PageView.tsx3e8a4778f747packages/components/src/__tests__/page-single-h1.test.tsx77005bcb825epackages/components/src/renderers/layout/page.tsxa5af90104700packages/types/src/registry.ts7c86c6a7aa64packages/types/src/zod/layout.zod.ts45dff251cb6aPositive control: the stripper genuinely removes comments (
page.tsx32152 raw -> 20919 stripped).Negative control: the same instrument reports DIFFERS when handed two genuinely different files.
⇒ ⛔ No claim about a
.default()population, an accept set, a schema arm, a key, a registration,a
.describe()string or any symbol count can be moved by this diff.These are the ones the strip proof does not cover, so each was probed directly.
.changeset/8871-page-node-refuses-breadcrumbs.md— claims\.breadcrumbsis "exit 0 over4 files — every hit one of this branch's own four files (this changeset, the refusal pin,
layout.ts,zod/layout.zod.ts)". Probed tree-wide at base92fba11eband at head5b293107f: 4 files at both, and they are exactly the four named. Lit control on the sameinstrument,
\.breadcrumb\b: 16 files at both — which is also the number that paragraphstates for head. Absent-token control exits 1. And
git diff base..head | grep -ci breadcrumbis 0 — this diff adds no mention of the token in any spelling. STILL TRUE; ⛔ nothing corrected.
.changeset/8478-describe-line-addresses.md— claims certain line addresses inlayout.zod.tsare out of scope.layout.zod.tscarries exactly 1 line-address citation atbase and 1 at head:
`page.tsx:521`. My insertion intopage.tsxbegins at line 708,so 521 is not shifted — verified by content, not arithmetic: lines 519-523 of
page.tsxarebyte-identical at base and head. ⭐ And that address sits in a JSDoc block, not a
.describe()string, so the entry's claim (which is about published.describe()descriptions)does not reach it at all. Two independent reasons. STILL TRUE of this diff; ⛔ nothing corrected.
The other eight
Each is covered by the strip proof; two also carry a probe over a file this diff touches, and both
probes were re-run rather than reasoned about.
7926-page-node-refuses-actions.md— "git grep -ni actiononpage.tsxreturns only thePageVariableActionBridgeimport and its render". Re-run at base and at head: byte-identicalhit sets, 4 lines each, same line numbers. Its second claim (3
actionsauthor sites, all incontent/docs/guide/layout.md) is over a file this diff does not touch. STILL TRUE.page-source-tailwind-prose-retraction-5469.md— claims a specifickind === 'html'dispatch-arm comment in
page.tsxdoes not project intodist. Mine is a different commentand likewise does not project (0 hits in
dist/renderers/layout/page.d.tsafter a real build).STILL TRUE.
6951-text-value-retired.md(TextSchema.valuetombstone; "thevalue?: stringmembers ofTextSpanSchemaandTabsSchemain the same file are other schemas' contracts and areunchanged") ·
7735-zod-mirrors-stop-authoring-defaults.md(41.default()call sitesremoved,
layout.zod.ts22) ·8478-zod-pins-form-layout.md·8499-node-slot-registered-arms.md(four named arms) ·
8505-grid-columns-breakpoint-narrowing.md(GridSchema.columns) ·8516-8556-mirror-partial-record-narrowing.md(the mirror-narrowing table) — every claim isabout a schema member, an arm, a call-site population or an accept set, and the stripped-source
identity above says none of those can have moved. None names a comment. ALL STILL TRUE.
⛔ Already false before this branch existed — reported, ⛔ not corrected
8478-describe-line-addresses.mdsays the remaining addresses are "held by in-flight PRobjectui#8763". That PR is merged (read from the API), so the present-tense clause is stale;
its companion
8478-zod-pins-form-layout.mdcarries the same shape — but for PRobjectui#8799 (merged), not objectui#8766.
objectui#8766 appears in
8478-describe-line-addresses.md, already in the past tense andaccurate there ("cleared when that PR merged during this round"), so it needs no repair.
Both stale present-tense claims are now carded as objectui#9706. Its cardinal has moved too: the three files it names carry 2 / 1 / 7 line-address
citations today (
form.zod.ts/layout.zod.ts/complex.zod.ts), identical at base and head.⇒ falsified by other branches' merges, ⛔ not by this diff, and ⛔ not this card's to repair.
packages/types/src/registry.tsshifts a pre-existing citation (`renderer.tsx:256`,the chatbot note) from host line 264 to 304. The cited line is in
packages/plugin-chatbot/src/renderer.tsx, which this diff does not touch, so the address stillpoints where it did. No changeset names
registry.ts.Acceptance notes
ruling both describe the registrations as "the five page kinds". They are four kinds plus the
untyped-document fallback
page; the fifth kind,list, is served by the interface-mode branchand is registered elsewhere, to a different renderer. Nothing in the ruling turns on this — E
stands either way — but a declaration that repeated the card's wording would have been wrong on
its face, so the wording above is the measured shape instead.
SchemaRenderer." Read narrowly aboutPageView's own test that is right; read as written it isnot.
page-single-h1.test.tsxrenders a stored-page-shaped document withtype: 'app'throughthe real
SchemaRenderer— which is why removing theappregistration reddens it. What wasmissing is not coverage but attribution: it goes red for a reason that reads as a heading bug.
The new pin supplies that, ⛔ and no rendering suite was built as this card's deliverable.
PageNodeSchemapinstypetoz.literal('page')while the nodePageViewactually emitscarries the kind. Noted, ⛔ not filed and ⛔ not repaired: it is the same undeclared channel this
card exists to declare, and reconciling it is the render-path change the card fences off.
home8,app6,list5,record3,utility0) on the same instrument shape — but that instrument undercounts: onefile defines seven pages sharing one spread
type: 'list' as constbase, so 22 page-leveltypehits stand against 28
definePage(calls. Theutilityzero stands beside ahome8 firingcontrol and an absent-token zero on the same instrument. Reported, ⛔ not filed: no disposition
here depends on the cardinal.
PageTypeSchemauntouched, therender path untouched, no name added to a published barrel, no label or ready flip, no
auto-merge, no labels touched.
维护者速读
改了什么 — 六个源文件加了注释、一个新的 pin 测试、一个空 frontmatter 的 changeset。零行为改动:没有增删改任何
ComponentRegistry.register,没有动任何 Zod arm,没有动上游枚举(它在兄弟仓,只读)。为什么改 — 存储的页面文档的
type就是 spec 的页面 KIND,PageView把它原样写进ComponentRegistry用来分发的节点判别符。于是PageRenderer上那几个注册项是因为这一行才存在的。过去没有任何地方声明这件事,所以审计注册表的人看到「注册了但没有 schema 声明」——这在本仓是一个真实的缺陷
形态。这个误读已经产出两张方向错误的卡,其中一张走到了 draft PR,若落地会让六个已发布的 showcase 页面停止渲染
(页面位置画出 OBJUI-001)。现在这条通道在两个审计入口各写了一份声明,并由一个失败时会指名这条通道的测试钉住。
风险与代价(含回滚) — 风险极低:注释不参与编译语义,且实测不进入已发布的
.d.ts;唯一的可执行新增是一个只读注册表、不渲染任何东西的测试。代价是这个 pin 会在有人改动这条通道时变红——这正是它存在的意义,⛔ 不要靠改
fixture 把它弄绿。回滚:
git revert这一个 commit 即可,没有迁移、没有数据形态变化、没有发布面变化。你要做的 — 判两件事。① 声明放的位置对不对:
packages/types/src/registry.ts的page条目(节点类型词汇表的审计入口)与
packages/types/src/zod/layout.zod.ts的SpecPageFields注释(页面 KIND 词汇表的审计入口),是否真的是两个方向的审计者各自会撞上的地方。② 卡面「五个注册项就是五个页面 KIND」这个说法实测不成立(是四个
KIND + 一个无
type文档的兜底page,第五个 KINDlist走 interface 模式、由别的渲染器注册),本 PR 按实测措辞而非按卡面措辞落笔——请确认这个取舍。
🤖 Generated with Claude Code
https://claude.ai/code/session_01UanLVj6xvbS6puBCewLr8L
Generated by Claude Code
Generated by Claude Code