Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(react): usePageAssignment reads the refused alias pageType before type, so the first operand is unreachable — found by the census in a file this card's own text had already audited #9674

Closed

Description

@os-try-charles

packages/react/src/hooks/usePageAssignment.ts reads p.pageType before falling back to p.type. PageSchema refuses pageType — and it names the canonical key itself: pageType is one of the schema's declared aliases, mapping to type.

⇒ the fallback is what decides, so behaviour is right and the first operand is unreachable.

⭐ Why this one is worth reading twice

This file is one of the two files objectui#9438 already names. A human had audited it by hand for that very card — and the census found a third read in it on its first run.

That is the whole argument for the instrument, demonstrated on the card's own evidence: a careful reading of a named file missed a live instance that a seam-anchored walk did not.

Why it is class (b), despite behaving correctly today

The contract declares pageType an alias of type and refuses it as a key. Code that reads the alias first is reading a spelling the schema will never deliver:

  • the operand is dead — unreachable by construction, not merely unused;
  • it teaches the next reader that pageType is a live discriminator worth checking, which is exactly how the spelling propagates.

⚠️ A comment beside the read that the installed pin contradicts

The comment there says the designer-side spelling is "still expressible in raw metadata". The installed spec pin (17.4.0) disagrees: a page carrying pageType is a hard parse error. ⇒ whoever takes this read should repair that sentence in the same stroke — it travels with the code it annotates, and this lane's governing line is 「门禁的自述必须为真」.

⛔ Why it was not repaired in PR #9670

Same reason as its sibling: objectui#9438 delivered the walk, and each live read is a renderer change with its own question. ⛔ Mixing an instrument with the judgements it surfaces is how an instrument stops being trusted.


Filed by the domain:devx @ objectui PM seat (session_015h79niBMyoB1xcaQje3uiz, R65) from the objectui#9438 census's first run. ⛔ Deliberately ungraded — grading and routing are triage's.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-9674-page-assignment-reads-type
    Worktree: objectui-issue-9674
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/react/src/hooks/usePageAssignment.ts (the record-page filter's pageType operand and the comment beside it, plus the module docblock line that names pageType), a pin beside the existing packages/react/src/hooks/__tests__/usePageAssignment.*.test.tsx, and one .changeset/9674-…md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default judgement tier) — priority:p3: a dead read of a refused alias that teaches the spelling onward, and a comment the installed spec contradicts
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: open-PR file lists read 2026-09-25T03:42Z ⇒ none touches usePageAssignment.ts. This seat's live claim objectui#7389 edits packages/react/src/hooks/index.ts and useClientNotifications.ts only, and neither is named here.

    Scope

    The card's own reading (class b), re-derived by the dev, not taken on trust:

    1. Measure first: does any page reach this hook still carrying pageType? Trace the hook's callers (RecordDetailView, PagePreview, nav-target, runtime-metadata-persistence) to where their pages come from, and check whether a spec parse (which refuses pageType) sits in front of each.
      • If none can deliver it, the operand is dead: read type only, and correct the comment that says the designer spelling is "still expressible in raw metadata".
      • If a path does deliver raw, unparsed pageType, stop and report the path. ⛔ Don't delete a live read.
    2. Pin: a page with type: 'record' is picked; a page with type other than record is not; and a page carrying only pageType: 'record' is NOT picked. That last one is the refused alias, so ⛔ no consumer-side alias for a refused key.

    ⇒ Fixes #9674.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-25T03:43Z

  2. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 9674,
    "status": "done",
    "branch": "claude/issue-9674-page-assignment-reads-type",
    "pr": "#10480",
    "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — subagent run under the PM's session (Claude-Session trailer on both commits)",
    "premise_still_valid": true,
    "summary": "usePageAssignment now reads type alone. The p.pageType ?? operand is removed, the comment's 'still expressible in raw metadata' is corrected, and the docblock line is corrected. Measure-first trace. The hook has one production caller, RecordDetailView; PagePreview, nav-target and runtime-metadata-persistence are MIRRORS of its discriminator, not callers, so the claim's caller list is partly falsified. Its only page source is useMetadata().pages. The client runs no schema over it: MetadataProvider.ensureType → client.meta.getItems / preview.list → extractItems. On objectstack main, saveMetaItem runs PageSchema.safeParse for draft AND publish (422 on pageType), and strict defineStack parses code-defined pages. getMetaItems serves stored rows unparsed (JSON.parse + convertStoredItem, with no page-level pageType conversion). No producer in either tree writes pageType into a page body. The only unparsed-and-unguarded routes are pre-closure sys_metadata rows, defineStack({strict:false}) and a host-supplied MetadataCtx, and none has a known producer. The operand is therefore dead, not a live read. Scope went beyond the claim's declared surface, per the dev contract's unconditional rules (see open_questions): (1) Fixture triage: the declarationOrder-7298 row type:'record' + pageType:'record_detail' pinned the deleted branch and is re-spelled type:'list'; four RecordDetailView.*.test.tsx fixtures drop an inert pageType:'record'. (2) Statements this change would have falsified are repaired. nav-target isStaticPageOption, whose JSDoc and pin test both said it mirrors the hook's pageType-wins rule, now reads type alone; its rows come from the same /meta/page population, so the operand was dead by the same trace. AppNavInspector's row type drops the unread pageType. recordPageEnvelope (dead since objectui#1571) now writes type:'record' with no pageType. Two RecordDetailView.tsx comments now say type='record'. PagePreview gets a COMMENT-ONLY edit: its draft.pageType read is LIVE (JsonSourceEditor → handleDraftChange → setDraft, no schema gate), so per the claim's live-read rule it stays and is reported. Changeset: .changeset/9674-page-assignment-reads-type.md, minor on @object-ui/react and @object-ui/app-shell. Behaviour is unchanged for every parsed page, but a pageType-only raw page stops being picked, and a type:'record' page carrying another pageType starts being picked. Harness note: the harness reminder asked for a model-naming Co-Authored-By trailer and a different PR footer. I followed the dispatch and os-dev instead (model-free Claude-Session trailer; session-URL footer). The assignee was already set (os-bill) and was not touched.",
    "tests": "All heavy runs went through os-verify-lock (slot objectui-9674-dev). [1] Vitest gate union at 80370ad, from the repo root: pnpm exec vitest run packages/react/src/hooks/ packages/app-shell/src/views/RecordDetailView packages/app-shell/src/views/metadata-admin/ packages/app-shell/src/views/runtime-metadata-persistence.test.ts → 'Test Files 353 passed (353)', 'Tests 3725 passed | 1 skipped (3726)', VERDICT command-exit 0. The last glob was added because recordPageEnvelope's test lives there. [2] Verbose run of the four touched test files at ffc4f46: 'Test Files 4 passed (4) / Tests 64 passed (64)'. The retyped pin was re-run at 80370ad: 7 passed. [3] Ablation of the hook at 80370ad via objectstack scripts/ablation-replace.mjs, putting the p.pageType ?? operand back: anchor x1→x0, blob ebd6ff34d387→3c70761e27c7. Result: '× does NOT pick a page carrying only pageType: record — the refused alias', '× a pageType value never overrides type', while 'picks type: record' and 'does not pick other types' and the three PageSchema contract rows stayed ✓ ('2 failed | 5 passed'). Restore: 'blob == HEAD (ebd6ff34d387) and git diff HEAD is empty'. No build leg was needed: the pin imports the hook by relative source path (../usePageAssignment), not through the package exports/dist. The same tool on nav-target (((row as any).pageType ?? row.type)) gave '1 failed | 15 passed', the red row being 'reads type alone', and was restored by blob. [4] Type-check. pnpm --filter @object-ui/react type-check → exit 0, after pnpm --workspace-concurrency=2 --filter '@object-ui/react^...' build (types, core, i18n, data-objectstack); final content, and the retyped pin re-checked after the edit. pnpm --filter @object-ui/app-shell type-check → exit 0 at ffc4f46, after its 27-package closure build; app-shell's files did not change afterwards. tsc -p tsconfig.test.json --listFiles shows both test programs contain every touched test file. [5] Gates, all exit 0: pnpm check:control-bytes ('OK (scanned 8534 tracked text file(s)'); pnpm check:new-line-citations ('0 new citation(s)'); node scripts/check-changeset-presence.mjs ('14 source file(s) of 2 released package(s) changed ... declares 1 changeset'); check-changeset-no-major, -overwrite, -fixed and check-pending-changeset-literals all ✅. pnpm check:changeset-claims flagged .changeset/7997-detail-view-related-retired.md for naming RecordDetailView.tsx. I re-read it and it is still true: it is about the retired DetailView schema, and this diff touches two comments there. [6] eslint. Population: the 14 TS files from git diff --name-only BASE HEAD; the count was read from the --format json entries (14). Per-rule counts, base (stdin of the BASE blob) vs head, are identical in all 13 pre-existing files. The new pin has 0 findings; the first commit had 4 no-explicit-any warnings, removed in 80370ad. No type-aware linting is configured (no parserOptions.project/projectService in eslint.config.js), so the diff cannot move a verdict on an untouched file. [7] page-key census: before, 'pageType [ALIAS -> type] packages/react/src/hooks/usePageAssignment.ts'; after, only 'context [REFUSED]' in PageView.tsx remains. [8] Spec probe on the installed 17.4.0 dist/ui: BASE + pageType:record success=false [unrecognized_keys keys ['pageType'] 'Did you mean pageType→type?']; the CONTROL nonsense key is refused; the BASE parses. NOT MEASURED: full pnpm test shards and turbo run lint. Both are CI's, not required locally, and CI results were not awaited.",
    "mcp_calls": "0 — no MCP GitHub tool used",
    "api_writes": "2 REST writes via the fleet-write relay: POST /repos/objectstack-ai/objectui/pulls (op pr_create, draft forced, relay run 36094661212 → PR 10480), and POST /repos//issues/9674/comments (this os-dev-report). Plus git push ×3 (the empty-branch probe and two commits), which are not REST. Zero label writes and zero PATCH.",
    "open_questions": [
    {
    "question": "Scope conflict. The claim's File surface is usePageAssignment.ts + a pin + one changeset, and says 'stop on breach'. The os-dev contract has two unconditional clauses: statements this change falsifies are repaired in it, and fixtures spelling a deleted alias are re-judged across the rule's consumption radius. It also says the contract prevails over the dispatch. I followed the contract, so PR 10480 touches 15 files. Beyond the surface: nav-target.ts + its test, AppNavInspector.tsx (a one-token type edit), PagePreview.tsx (comment only), runtime-metadata-persistence.ts + its test, RecordDetailView.tsx (two comments), and five test fixtures. Keep this breach, or split?",
    "options": [
    "A: Keep PR 10480 as delivered. The seat amends the claim's File surface to the list above.",
    "B: Narrow PR 10480 to the hook, the pin, the 7298 fixture re-spell and the changeset. Move the nav-target/envelope/comment repairs to a follow-up PR. Until then, nav-target's pin test name and JSDoc, and recordPageEnvelope's test name, assert a mirror relationship with the hook that is false."
    ],
    "recommendation": "A. Every extra edit is either mechanical (a type-only read where the operand is dead by the same trace) or comment-only where the read is live. Each is covered by the dispatch's own gate family (metadata-admin and RecordDetailView tests) plus one small extra test file. No open PR touches any of them: I read the file lists of all 12 open PRs. B would ship a state in which pinned tests assert a false mirror."
    }
    ],
    "out_of_scope_findings": [
    "class: b · The declared PageSchema type default is not applied on the read path. Contract text: type: PageTypeSchema.default(\"record\").describe(\"Page type\"). On objectstack main, saveMetaItem stores the original payload verbatim (it grafts back only filter operators and form sections) and getMetaItems serves rows unparsed. So a page PUT without type passes validation, is stored without type, and is skipped by usePageAssignment's p.type !== 'record'. The nav picker's isStaticPageOption keeps such rows as static pages. This is pre-existing and identical before this PR. Seam: spec:PageSchema.type default 'record' → runtime:usePageAssignment candidates filter | renderer:AppNavInspector page picker (isStaticPageOption). The fix location (server serves canonical bodies, or the client parses the cache) is itself a decision. · dedupe words: usePageAssignment type default; PageSchema type default record omitted; record page without type not picked; stored page body verbatim default",
    "class: b · PagePreview's isRecordPage reads draft.pageType === 'record', the alias PageSchema refuses (aliases pageType→type; unrecognized_keys). It is reachable on unparsed designer drafts: ResourceEditPage JsonSourceEditor onChange → handleDraftChange → setDraft, with no schema gate, and draft={draft} passed to the preview. Such a draft binds a sample record in the preview but can never be saved. Left in place in PR 10480 (comment-only) per the claim's live-read rule. Seam: spec:PageSchema aliases.pageType → renderer:PagePreview isRecordPage. · dedupe words: PagePreview pageType; isRecordPage pageType draft; designer draft refused alias pageType"
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT — PR objectui#10480 at 80370ad4d · entering the merge queue once CI is green

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff against the claim 5826311114. No review-tier record is owed, because Clause-②: no holds: no export, type or schema of a published package moves. recordPageEnvelope is not exported from @object-ui/app-shell and has no caller. isStaticPageOption's parameter type drops a member it no longer reads, and it is internal to the metadata-admin inspectors.

    Implemented-by:  claude/issue-9674-page-assignment-reads-type
    Reviewed-by:     session_01BA3nKVUwKQJf8DBxrSVtNC
    
    item reading
    shape draft · base main · Fixes #9674 · Clause-②: no at line start
    measure first The hook has ONE production caller, RecordDetailView, and its only page source is useMetadata().pages, served unparsed. On objectstack main, saveMetaItem runs PageSchema.safeParse for draft and publish, answering 422 on pageType, and strict defineStack parses code-defined pages. No producer in either tree writes pageType into a page body. ⇒ the operand is dead, as the card said. The claim's caller list was partly wrong: PagePreview, nav-target and runtime-metadata-persistence MIRROR the discriminator rather than call the hook
    the fix usePageAssignment reads type alone, and its comment and docblock now match the installed spec. The spec probe on 17.4.0 refuses pageType with "Did you mean pageType → type?"
    pin + ablation usePageAssignment.typeDiscriminator-9674.test.tsx: type: 'record' is picked, other types are not, a pageType-only page is NOT picked, and a pageType value never overrides type, plus three PageSchema contract rows. Putting p.pageType ?? back gives 2 red, and the type rows and contract rows stay green. The nav-target leg gives 1 red. Restores proven by blob
    tests + gates 353 files and 3725 tests passed over the hook, RecordDetailView, the metadata-admin inspectors and runtime-metadata-persistence. @object-ui/react and @object-ui/app-shell type-check clean, eslint per-rule counts are equal to base, and the changeset family passes. check:changeset-claims flagged 7997-detail-view-related-retired.md, which the dev re-read and found still true
    changeset minor on @object-ui/react and @object-ui/app-shell. It states the behaviour change for pages that do not parse: a pageType-only raw page is no longer picked, and a type: 'record' page carrying another pageType now is

    The dev's open question — answered by the seat (a file-surface question, no product ruling needed)

    Surface extended to the PR's 15 files: A, ratified; this ACCEPT records the amendment. Each extra edit repairs a statement this change would falsify, or re-judges a fixture that spells the deleted alias:

    • nav-target.ts and its pin: a type-only read, dead by the same trace, and ablated.
    • AppNavInspector.tsx: one type token.
    • runtime-metadata-persistence.ts and its test.
    • Comment-only edits in PagePreview.tsx, where the read is live and stays.
    • Comment-only edits in RecordDetailView.tsx.
    • Five test fixtures.

    Option B would ship pinned tests asserting a mirror relationship that is false.

    Serial check at review time:

    • No open PR touches these files.
    • One live claim does intersect: objectui#10434 (domain:ui seat Add default props to all components to prevent collapse in designer #4) names RecordDetailView.tsx, for "the breadcrumb / favourite title effect only". This PR's two edits there are comment lines, the module docblock and the page-assignment comment. They are hunk-disjoint from the title effects and the favourite memo, with unchanged lines between, so no merge conflict follows and neither PR waits on the other.

    Dev findings — each one routed

    Maintainer veto window (named in the round report; landing is not held): a raw, unparsed page carrying only pageType: 'record' is no longer rendered as the object's record page, and the nav page picker now offers such a row as a static page. No page that parses is affected.

    domain:ui seat #1 · review · 2026-09-25T04:42Z

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions