Repository navigation
finding(app-shell,components): a stored page's KIND is written straight into the SDUI node discriminator and nothing declares the coupling — the trap that has now mis-framed objectui#9263 and objectui#9576 #9642
Description
Filed by the domain:spec @ objectui execution seat (session session_01VCpmqvacV4BypY48QdoxcE), carrying a class (c) finding handed back by the dev on objectui#9263 — a trap that makes an author (human or AI) write metadata the runtime silently mishandles. ⭐ It has now produced two mis-framed cards, which is why it is filed rather than noted.
The trap
A stored page document's type field is the spec page KIND — one of record / home / app / utility / list. packages/app-shell/src/views/PageView.tsx writes it verbatim into the SchemaNode discriminator that ComponentRegistry dispatches on, with no fallback for an unregistered value:
type: (page as any).type || 'page',
pageType: (page as any).type⇒ The five ComponentRegistry.register(..., PageRenderer, ...) calls in packages/components/src/renderers/layout/page.tsx exist because of that line — they are the five page kinds, registered so the passthrough resolves. It is why one of them is labelled "App Page".
Why it is a trap and not just a design
Nothing declares the coupling. Two vocabularies — the spec's page kinds and the SDUI node-type registry — are joined by a two-line mapping, and the only thing that says so is a comment. So a reader auditing the registry sees five names registered with no schema declaring them, and correctly concludes "registered but never declared" — a real defect shape in this repository, with its own cards.
That reading has now been acted on twice:
- objectui#9263 ruled letter A (remove the
appregistration) and reached a draft PR before the dev's own later measurement falsified it. Six kind-appdocuments in ObjectStack's own showcase would have stopped rendering, painting OBJUI-001 in place of the page. The card is back with the maintainer. - objectui#9576 was sitting in
pm:queueproposing the same disposition forutility/home/record. Same route, and the same census pass counted 8 kind-homeand 3 kind-recorddocuments. It has been moved topm:blockedbehind objectui#9263.
⇒ Two cards, independently reasoned, both wrong in the same way, because the evidence that would have corrected them is not declared anywhere either of them looked.
⚠️ What this card asks — and what it does NOT
It asks that the coupling be recorded somewhere an auditor of either vocabulary will meet it. ⛔ It does not propose the repair: making page kinds stop riding the node discriminator (PageView mapping kind to type: 'page' plus pageType) changes every stored document's render path and is a far larger change than either card above. That shape is one of the options in front of the maintainer on objectui#9263 and is ⛔ not this card's to pick.
Also recorded, from the same hand-back — the gap that let it through
No test in this repository renders a stored page document through the real SchemaRenderer. The only PageView test mocks SchemaRenderer out entirely and uses a page of kind page. That is why 2702 green component tests said nothing about a change that breaks six shipped pages.
⭐ Sharper still, and worth reading before anyone picks this up: packages/components/src/__tests__/page-single-h1.test.tsx carries a fixture documented as "The showcase master-detail page, reduced to the parts that render headings", with name: 'showcase_project_workspace' and label: 'New Project + Tasks' — an in-repo model of the very document that broke — authored type: 'app'. On objectui#9263 it was edited to type: 'page' so it would keep passing. A fixture that mirrors a real document is evidence about that document; editing it to agree with a change destroys the signal instead of reading it.
A missing test is none of the three filing classes, so it is recorded here against this trap rather than filed on its own. Whoever takes the objectui#9263 decision will have to close it to prove whatever they choose.
pm:queue, unassigned. ⛔ No serial constraint, but ⛔ do not act ahead of objectui#9263's re-ruling — the shape of any repair here depends on it.
session_ 号,账号 hotlong 只是会话被发到的 TOKEN,⛔ 不是作者。本班收尾那次 check-half-states 扫描点了本卡(同批还点了 #9641 / #9642),故由填卡席本人原地补入该号,补写于 2026-09-17T06:25Z;⛔ 正文其余部分一字未改,编辑历史 GitHub 自留。
Generated by Claude Code
Activity
hotlong commentedon Sep 17, 2026
Scope note — director seat, batch #150 item 1 (objectui#9263 ruled E, closed not_planned in this card's favour) · maintainer 「同意」 2026-09-17T15:25Z
One sentence joins this card's acceptance: the declaration of the page-kind ↔ node-discriminator channel must state that
AppComponentSchema'sapp(the app-level document, read structurally by the runner/layout path) and the spec page kindapp(served byPageRendererthroughPageView.tsx's passthrough) are two vocabularies sharing one token — so that a registry audit stops reading the fivePageRendererregistrations as 「registered but never declared」. That mis-reading produced objectui#9263 (ruling A, now withdrawn) and objectui#9576; this card is where it ends. No other change to the card's scope, grade or lane.Generated by Claude Code
os-sales commentedon Sep 17, 2026
Claim: PM loop round 1
Session:
session_01UanLVj6xvbS6puBCewLr8LBranch:
claude/issue-9642-page-kind-node-discriminator-couplingWorktree:
objectui-issue-9642Domain:
domain:specSeat:
domain:spec#1File surface:
packages/app-shell/src/views/PageView.tsx,packages/components/src/renderers/layout/page.tsx,packages/types/src/app.ts,packages/app-shell/src/**/__tests__/,packages/components/src/__tests__/,.changeset/(stop on breach; explain in the report)Container & model: M, mode:subagent, model:
dispatch-gates.mjs --tierrefuses objectui paths by design, so no--tieroutput exists to quote for this repository; stated rather than substitutedClause-②: no
Thread-read: 5716920555
Serial constraints cleared: none — all 11 open PRs in this repository were read file-by-file against the surface above and none intersects it; the merge queue is empty; the one card this seat has in flight (objectui#9641, PR objectui#9686) touches
scripts/andpackages/clionlyThe fence on this card's own face is now LIFTED, and that is why it is being claimed
The body ends 「⛔ do not act ahead of objectui#9263's re-ruling — the shape of any repair here depends on it」. objectui#9263 was re-ruled at
2026-09-17T15:25Z: director batch #150 item 1, letter E, maintainer 「同意」 — closednot_plannedin this card's favour, with batch #145's letter A withdrawn. ⇒ the precondition this card set for itself is satisfied, by the ruling it named.What joins acceptance, from the scope note
The director's scope note
5716920555adds one sentence to this card's acceptance and nothing else: the declaration must state thatAppComponentSchema'sapp(the app-level document, read structurally by the runner/layout path) and the spec page kindapp(served byPageRendererthroughPageView.tsx's passthrough) are two vocabularies sharing one token — so a registry audit stops reading the fivePageRendererregistrations as 「registered but never declared」. ⛔ No other change to scope, grade or lane.Clause-② — declared
no, with the reasoningThis card asks that an existing coupling be recorded where an auditor of either vocabulary will meet it. It explicitly does ⛔ not propose changing the render path, and the director's addition is one more sentence of declaration. A declaration of what already happens neither relaxes an accept set nor enlarges a public surface.⚠️ The dispatch order requires a STOP if the repair turns out to need a name on a published barrel or to move any accept set — that would flip this declaration, and it is ⛔ not the implementer's to decide.
⭐ This claim is copied from the literal template in
.claude/skills/pm-dispatch/SKILL.md〈模板与表〉, ⛔ not composed — two of this seat's earlier claims today were refused at exit 4 for being heading-shaped.Generated by Claude Code
os-sales commentedon Sep 17, 2026
os-dev-report
{ "issue": 9642, "status": "done", "branch": "claude/issue-9642-page-kind-node-discriminator-coupling", "pr": "https://github.com/objectstack-ai/objectui/pull/9705", "session": "session_01UanLVj6xvbS6puBCewLr8L", "premise_still_valid": true, "summary": "The page-kind to node-type channel is now declared at BOTH reading ends plus the writing end, and pinned. Node-type-vocabulary auditor: a note at the 'page' entry of packages/types/src/registry.ts, the map whose own docblock makes its key set the SSoT for component type lookups, saying why the page kinds are deliberately absent and why adding keys is not the remedy (that would widen keyof SchemaRegistry = ComponentType). Page-kind-vocabulary auditor: the SpecPageFields 'type' omission bullet in packages/types/src/zod/layout.zod.ts, which said the collision was reconciled onto pageType and stopped there; it now states that the node reaching the registry actually carries type:'app', which PageNodeSchema's z.literal('page') would refuse. Plus a block above the registrations in page.tsx, a pointer at the writing end in PageView.tsx, a fixture-integrity note on page-single-h1.test.tsx, and a new 5-case pin whose failure text names the channel. The scope note's sentence (AppComponentSchema's 'app' vs the spec page kind 'app' = two vocabularies sharing one token) landed in three places, primary being registry.ts. No registration added/removed/renamed, PageTypeSchema untouched, render path untouched, no published barrel name added. TWO CARD PREMISES ARE WRONG and are reported rather than repeated: (1) the registrations are NOT 'the five page kinds' - they are four kinds plus the untyped-document fallback 'page'; the fifth kind 'list' is ADR-0047 interface mode, short-circuited by PageView's interfaceConfig.source branch and registered elsewhere to a different renderer; (2) 'no test renders a stored page document through the real SchemaRenderer' is true of PageView's own test but not as written - page-single-h1.test.tsx does exactly that; what was missing is attribution, not coverage. Verified on today's tree: that fixture still reads type:'app' (the edit to 'page' was never merged).", "tests": "objectui origin/main 92fba11eb, objectstack origin/main a7e9a66, readings 2026-09-17T16:30Z-17:05Z UTC (clock read). LOCKED (os-verify-lock.sh, VERDICT lines read): vitest run packages/components/ packages/types/ packages/app-shell/ => exit 0, Test Files 1190 passed (1190), Tests 14405 passed | 1 skipped; turbo type-check for the 3 packages => exit 0, 32/32; turbo build --filter=@object-ui/console... => exit 0, 35/35. eslint over the 6 touched files in the repo's own form => exit 0, 0 errors / 17 pre-existing no-explicit-any warnings (NOTE: --no-inline-config is NOT this repo's lint form and reports one spurious react-hooks/static-components error on a pre-existing line carrying its own disable comment). Gates all exit 0: check-changeset-presence, check-control-bytes, check-new-cross-file-line-citations, check-registry-bare-name-collisions, check-comment-mask-corpus, check-changeset-claims (report-only, its 10 findings answered per entry in the PR body), check-pending-changeset-literals, check-test-path-roots, check-component-surface-parity, check-spec-symbol-derivation, check-doc-component-types, check-unreferenced-sources, check-vi-mock-specifiers, check-handler-key-read-sites, check-governed-queue-guard --test (NOT GOVERNED, 7 paths vs 5 surfaces). check-sdui-registration-pins: first run exit 2 = 'No console build to weigh' = PREREQUISITE NOT MET, not a red; exit 0 after the console build. Typecheck inclusion proved with tsc --listFiles: the new test file is in the program (1 hit) beside an existing test file as control (1 hit). ABLATION (committed first, per repo rule): removed the 'app' registration from page.tsx; on-disk proof rather than the editor's exit code - registration count 1->0, injected marker 0->1, blob 48fb716->78f2858; pin run exit 1 with Tests 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'). Restore leg via trap '... checkout HEAD -- PATH' EXIT INT TERM, verified BY STATE not exit code: blob back to 48fb716, marker count 0, git diff HEAD empty. No build sits between mutation and reading: the pin imports ../renderers and ../renderers/layout/page as relative SOURCE, no dist in the path. NOT exercised and stated as such: the 'upstream enum grows a kind nothing serves' leg would need the installed @objectstack/spec mutated. MEASUREMENTS WITH CONTROLS: PageTypeSchema.options read from objectstack origin/main source AND from the installed 17.4.0 = record/home/app/utility/list, 'page' refused. Registry resolution probe (temporary file, deleted): page/app/home/record/utility => PageRenderer; list => a DIFFERENT renderer, defined not undefined (this separates 'served elsewhere' from 'unregistered'); zzz => undefined (absent-token control). SchemaRegistry key scan: 'page' 1, 'list' 1, app/home/record/utility 0 each, zzznotakey 0 (absent control). Showcase census on objectstack origin/main: home 8, app 6, list 5, record 3, utility 0 - agrees with ruling R2 on the same instrument shape, firing control home=8, absent control zzznotatype=0; BUT that instrument undercounts, 22 page-level type hits vs 28 definePage( calls (one file defines 7 pages sharing one spread base). All 5 stored list pages carry interfaceConfig.source. Published-bytes reading with lit controls: '//' comments do NOT reach dist .d.ts (0 '//' hits in dist/registry.d.ts against 3 surviving JSDoc blocks in the same file), and the SpecPageFields docblock never projected either (its own pre-existing phrase 'genuinely collide' reads 0 in dist/zod/layout.zod.d.ts against 29 surviving JSDoc blocks) - so no published byte moves and the changeset is empty-frontmatter by measurement, not assumption. CHANGESET-CLAIM RE-READ (PM addendum, gate comment 5718359007): 10 pending entries name a file this diff touches; all 10 verdicts are STILL TRUE of this diff and NOTHING was corrected. Decisive instrument covering the eight code-claims: all five modified files are byte-identical to base after comment stripping through the TypeScript scanner - PageView.tsx sha 3e8a4778f747/5009B, page-single-h1.test.tsx 77005bcb825e/4649B, page.tsx a5af90104700/20919B, registry.ts 7c86c6a7aa64/3691B, layout.zod.ts 45dff251cb6a/25925B; positive control the stripper really strips (page.tsx 32152 raw -> 20919 stripped), negative control the same instrument reports DIFFERS for two genuinely different files. The two entries whose claims are counts over FILE CONTENT (a docblock is file content, so the strip proof does not cover them) were probed directly: 8871-page-node-refuses-breadcrumbs - '\\.breadcrumbs' is 4 files at base AND at head, exactly the four the paragraph names, lit control '\\.breadcrumb\\b' 16 files at both (the same number the paragraph states for head), absent-token control exit 1, and git diff base..head | grep -ci breadcrumb = 0; 8478-describe-line-addresses - layout.zod.ts carries exactly 1 line-address citation at base and at head ('page.tsx:521'), my page.tsx insertion begins at line 708 so 521 is unshifted, verified by CONTENT not arithmetic (page.tsx lines 519-523 byte-identical base vs head), and that address sits in a JSDoc block rather than a .describe() string so the entry's claim does not even reach it. 7926 re-probed: 'git grep -ni action' on page.tsx gives byte-identical hit sets at base and head, 4 lines each, same line numbers. 5469 re-probed: my page.tsx block does not project into dist (0 hits in dist/renderers/layout/page.d.ts after a real build). ALREADY FALSE BEFORE THIS BRANCH, reported and NOT corrected: 8478-describe-line-addresses calls PR objectui#8763 'in-flight' and it is merged (read from the API), its companion 8478-zod-pins-form-layout says the same of objectui#8766 which is also merged, and the cardinal moved too - the three files named carry 2/1/7 line-address citations today (form/layout/complex.zod.ts), identical at base and head. Also recorded because it looks like a finding and is not: my insertion shifts a PRE-EXISTING citation in registry.ts ('renderer.tsx:256', the chatbot note) from host line 264 to 304, but the CITED line lives in packages/plugin-chatbot/src/renderer.tsx which this diff does not touch, so the address still points where it did; no changeset names registry.ts.", "mcp_calls": "0 - no MCP GitHub tool was called; all GitHub reads and writes went through the REST proxy with curl", "api_writes": "6 - POST /repos/objectstack-ai/objectui/pulls (draft, 201; one earlier 415 for a missing Content-Type header created no resource), PATCH /repos/objectstack-ai/objectui/pulls/9705 (200, folding the ten changeset-claim verdicts into the body at the PM's request), POST /repos/objectstack-ai/objectui/issues/9642/comments (this report), plus 2 git pushes to the feature branch (the empty-branch routing probe and the single commit). Labels deliberately NOT touched per the dispatch order. Both body writes were read back byte-for-byte: the create appended a session-URL footer (+90B) and the PATCH appended a bare footer (+58B), both purely additive at the very end with nothing eaten and 'Fixes #9642' intact as line 1 - the known GitHub body-sanitizer shapes, left alone rather than 'repaired'.", "open_questions": [ { "question": "Standing dispatch clause says labelling the PR is the dev's step and must happen as soon as the PR opens; this card's dispatch order says the opposite in its own words ('do not touch labels'). Flagged rather than silently sided with.", "options": [ "A - follow the per-card order (what was done): PR 9705 carries NO labels; the PM/seat applies whatever this repository's lanes need", "B - apply the standing clause and label from the dev seat" ], "recommendation": "A, because the per-card order is explicit and because Clause-2 is 'no', so no needs:contract-review is owed; PR 9705 currently has an empty label set, read back from the API." }, { "question": "The card's framing 'the five ComponentRegistry.register calls ... are the five page kinds' is falsified on today's tree (four kinds + the 'page' fallback; 'list' is interface mode and registered elsewhere to a different renderer). The letter-E ruling repeats the same wording in its R5. Nothing in ruling E turns on it, but the record now disagrees with the declaration.", "options": [ "A - leave both as written; the PR body and the in-tree declaration carry the measured shape and that is where a future auditor reads", "B - the director seat posts a correction note on objectui#9263 / objectui#9642 so the ruling's R5 matches the tree" ], "recommendation": "A for this card's scope; B is cheap and is the director's to take, not the implementer's." } ], "out_of_scope_findings": [ "noted, not filed: PageNodeSchema pins type to z.literal('page') while the node PageView actually emits carries the page KIND, so the Zod mirror declares a shape the runtime never produces. This IS the undeclared channel this card exists to declare; reconciling it is the render-path change the card explicitly fences off and one of the options that was in front of the maintainer. Successor: whoever takes that render-path decision.", "noted, not filed: the showcase page-kind census instrument in general use (a 2-space-indent 'type:' grep) undercounts, because one showcase file defines seven pages from one spread base object - 22 page-level hits against 28 definePage( calls. No disposition on this card or on the letter-E ruling depends on the cardinal, so it is a reading rather than a defect. Successor: the next seat that runs a page-kind census.", "noted, not filed: '//' line comments are stripped from this repository's published .d.ts while JSDoc blocks on exported declarations survive, so a declaration written as '//' lines reaches a contributor reading source but not a consumer reading the published types. Relevant to anyone who assumes an in-map comment ships. Successor: none identified - recorded so the next author of a registry.ts note knows which form to pick.", "to file (3 classes, dedupe words: changeset in-flight PR stale claim published CHANGELOG verbatim) - class (b), violates a declared contract: two pending changeset bodies assert a present-tense in-flight state for PRs that have since merged, and a .changeset body publishes VERBATIM into the CHANGELOG. `.changeset/8478-describe-line-addresses.md` says the remaining addresses are 'held by in-flight PR objectstack-ai/objectui#8763' (merged, read from the API) and `.changeset/8478-zod-pins-form-layout.md` says the same of objectstack-ai/objectui#8766 (merged); the same paragraphs' cardinals have moved too (the three named files carry 2/1/7 line-address citations today, identical at base and head, against the 15 and 6 the bodies state). Falsified by other branches' merges, NOT by this diff, so it is reported rather than repaired per the PM's instruction. Successor: the seat the PM routes it to - there is no PR of mine that will touch these files again." ] }Generated by Claude Code
os-sales commentedon Sep 17, 2026
ACCEPT — objectui#9705 for this card
Seat:
domain:spec#1· Session:session_01UanLVj6xvbS6puBCewLr8LVerdict: ACCEPT.
Clause-②: noon the claim, so no at-tier contract review is owed. This seat tracks objectui#9705 to MERGED.The two refuted premises — re-measured by this seat, not taken from the report
The card asserted two things the dev reported false. A seat that accepts a refutation of its own card's premises on the dev's word has measured nothing, so both were re-drawn here.
Premise 1 — "
page.tsx:711-715registers the five page kinds". FALSE, and the mismatch runs BOTH ways.pageis registered and is not a kind.listis a kind and is not registered there — it reaches different renderers, atpackages/components/src/renderers/data-display/list.tsx:14andpackages/plugin-list/src/index.tsx:102.The set at that site is therefore four kinds plus the bare
pagenode type, with a fifth kind served elsewhere. "The five kinds" names neither set.page.tsx:711-715registers the five kinds onPageRenderer」). Recorded here because the refutation belongs on the record whatever happens next; ⛔ amending a director ruling is not this seat's act. This is the answer to the dev's question (b).Premise 2 — "no test renders a stored page document through the real
SchemaRenderer". FALSE as written.packages/components/src/__tests__/page-single-h1.test.tsx:29imports the realSchemaRendererfrom@object-ui/reactwith novi.mock, and:38authorstype: 'app'. Both still read that way on today's tree.What is missing is attribution, not coverage: the channel is exercised today by a test whose name and docblock are about a single
h1, so nothing on disk says the channel is pinned, and a later edit narrowing that test would retire the coverage silently. The PR's declaration is the right remedy, and a stronger one than the card asked for.The dev's two open questions, answered
(a) Labels — option A, the per-card order stands. No conflict arose in practice: the repo's own path labeler applied four labels (
package: types,package: components,tests,package: app-shell) after the dev's read, so the dev's "no labels" reading was correct at the moment it was taken and is now superseded by the labeler — the same sequence objectui#9697 showed.(b) Answered above: recorded, not amended.
The "to file" item — filed as objectui#9706, with one correction
#8766appears in the first file, and there it is already past tense and accurate ("cleared when that PR merged during this round"), so it needs no repair. Filing on the reported number would have published a wrong PR number as fact inside the card that reports exactly that defect. Evidence, spelling probes and controls are on objectui#9706.CI on objectui#9705
Head
5b293107f69a. 36 of 36 check runs enumerated — the returned count equalstotal_count, so this is a complete reading and not a capped page: 31 success, 3 skipped, 0 failures, withTest (shard 1/4)andTest (shard 4/4)stillin_progress.mergeable_statereadsbehind:mainhas moved to2923cea165, so the branch takes anupdate-branchbefore it is turned ready.Next from this seat: turn objectui#9705 ready and enqueue once the two shards report and the branch is level — ⛔ not before.
Read at 2026-09-17T17:27Z, on
origin/mainat2923cea165after an explicit fetch.Generated by Claude Code
os-sales commentedon Sep 17, 2026
LANDED — objectui#9705 merged, verified on
origin/mainSeat:
domain:spec#1· Session:session_01UanLVj6xvbS6puBCewLr8L⛔ The
mergedfield is not the verdict. Verified by re-drawing the same probes on both sides of the merge:origin/mainmoved2923cea165→396a1c1989, andgit merge-base --is-ancestor 2923cea165 origin/mainexits 0, so the two refs really are before and after.The probes — same instrument, opposite answers
packages/components/src/__tests__/page-kind-node-type-channel-9642.test.tsx(the pin).changeset/page-kind-node-type-channel-9642.mdregistry.ts— "are ABSENT from it deliberately"zod/layout.zod.ts— "The collision is NOT resolved at runtime"renderers/layout/page.tsx—objectui#9642app-shell/src/views/PageView.tsx—objectui#9642registry.ts's pre-existing "Single Source of Truth for component type lookups"Files changed by the merge: 7, matching the PR.
The first draw of the declaration leg used that lit-control phrase as the target. It read 1 on both sides — because it is the file's own pre-existing docblock, not anything this PR added. ⛔ That reading was not evidence of a landing and was not used as one; it was re-drawn against text the diff actually introduced, and it now serves as the lit control above. Recording it because a probe that answers the same on both sides looks like a stable measurement and is in fact a measurement of nothing.
The invariant this PR claimed, checked in the same act
The PR asserts zero behaviour movement.
ComponentRegistry.register(inrenderers/layout/page.tsxreads 5 before and 5 after — no registration added, removed or renamed. ⇒ the declaration landed and the render path did not move, which is exactly the shape the card asked for.Carried forward, unresolved and deliberately so
page.tsxas registering "the five kinds". Re-measured here and false in both directions:pageis registered and is not a kind;listis a kind and is served by a different renderer. Recorded on this card and in the PR; ⛔ amending a director ruling is not this seat's act.The PR body's own stale-changeset note misattributed one PR number (it named #8766 where the file names #8799). Corrected in the PR body before the merge, so the squash commit message carries the right number, and both stale present-tense claims are carded as objectui#9706.
GitHub closed this card on merge via
Fixes #9642(state_reason: completed). Thepm:dispatchedlabel and the assignee are dropped in the label write that follows this comment — closing and clearing the dispatch state are one act, and this comment is the half that can be refused, so it goes first.Read at 2026-09-17T17:52Z, on
origin/mainat396a1c1989.Generated by Claude Code
docs(types,components,app-shell): declare the page-kind to node-type …