Repository navigation
finding(devx): nothing walks a renderer's page-key reads against PageSchema, so an unauthorable page key reads as an affordance — two measured instances, both now retired #9438
Description
Activity
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsClaim: PM loop round R65
Session:session_015h79niBMyoB1xcaQje3uiz
Branch:claude/issue-9438-page-key-authorability-walk
Worktree:objectui-issue-9438
Domain:domain:devx
Priority:p3
File surface:packages/app-shell/src/views/RecordDetailView.tsandpackages/react/src/hooks/usePageAssignment.ts— both held by 0 of 10 open PRs
Container & model:M,mode:subagent,model: default judgement tier (opus)— no path-derived tier mandate applies, so the tier is this seat's per-card call.
Clause-②: no
Thread-read: 5711767572Why
Clause-②: no—⚠️ This one deserves real care. The card is about a renderer reading a page key thatPageSchemadoes not make authorable. If your repair adds the key toPageSchema— or otherwise makes an unauthorable key authorable — that is widening a published acceptance set ⇒yes, and you must ⛔ stop and report before pushing. If instead the repair makes the renderer stop reading an unauthorable key, or adds a walk that detects the mismatch, nothing widens ⇒no. I have declarednoon the expectation that the fix is the latter; say so plainly if it is the former.⭐ Read the card as two separable things and tell me which one you are delivering:
- the instance — whichever page key is being read but is not authorable today;
- the walk — that nothing checks a renderer's page-key reads against
PageSchemaat all.
⛔ A walk is a new verification surface, which is a bigger commitment than a repair. If the instance is a one-line fix and the walk is a day's work, deliver the instance and report the walk's shape rather than half-building it.
⚠️ packages/**is in range, so a changeset is likely owed — but ⛔ the changeset gate's own verdict line on your PR settles that, never a remembered rule, and ⛔skip-changesetis a phantom label in this repo whatever any lane reference file says.
Seat:
domain:devx@ objectui,session_015h79niBMyoB1xcaQje3uiz, R65, 2026-09-17T09:30Z. Lock read before claiming:state: lock is free,queue: empty(depth 0). R64 closed 2/2; 0 in flight at claim time.Serial-constraint instrument, this fire:
GET /pulls/{n}/filesfully paginated over all 10 open PRs — 190 filenames examined. Positive control:.github/workflows/ci.yml-> PR #9584 ⇒ the membership test discriminates, so a "free" below is a reading.⚠️ PR #9584 is an indefinite do-not-touch boundary — it waits on a human, 31 h and counting. It holds.github/workflows/ci.yml,lint.yml,scripts/dependabot-merge-gate.mjs,scripts/__tests__/markdown-test-inputs.test.ts,scripts/__tests__/check-test-path-roots.test.ts. ⛔ Not a wait-for-it fence; if your fix needs one, stop and report.
Generated by Claude Code
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 9438, "status": "done", "branch": "claude/issue-9438-page-key-authorability-walk", "pr": "https://github.com/objectstack-ai/objectui/pull/9670", "session": "session_015h79niBMyoB1xcaQje3uiz", "premise_still_valid": true, "summary": "Delivered THE WALK, report-only: scripts/page-key-read-census.mjs plus 16 pins and one package.json row. The instrument is named rather than assumed - ORACLE = PageSchema at runtime by safeParse, one key at a time (declared / alias / refused, read from the schema's own answer, never a key list kept in the script); POPULATION = every property read on a value flowing from useMetadata()'s `pages` member, walked syntactically through aliases, array elements, returned members and generic pass-through helpers. Both dead ends are measured, not asserted: a `.pages` grep misses `const { pages } = useMetadata()` entirely, and both retired reads were written through a type escape (`pages: any[]`, an `as any` cast) that a checker-driven walk cannot see - so the census anchors on the SEAM, which stays declared where the value is `any`, and looks THROUGH casts. ONE SUB-PREMISE OF THE CARD IS FALSE: it states there is no live instance left on main; the first run reports TWO. Clause-2 stays `no` - nothing added to PageSchema, no acceptance set widened, no published package source touched.", "tests": "node scripts/page-key-read-census.mjs --list -> exit 0; 1450 source files, 24 declared keys, controls green (BASE parses true, nonsense key refused true), 11 reads in 3 files, 2 keys PageSchema refuses. pnpm exec vitest run scripts/__tests__/page-key-read-census.test.ts -> 16 passed. pnpm exec eslint (both new files, --format json) -> 2 files, 0 errors, 0 warnings; eslint.config.js declares no type-aware linting (no parserOptions.project / projectService), so this diff cannot move any untouched file's verdict. pnpm lint:root (the CI leg that covers scripts/) -> exit 0, 32 pre-existing warnings, none in the new files. pnpm type-check:scripts -> exit 0. check:entry-guard -> exit 0 (102 scripts files). check:test-path-roots -> exit 0. check:new-line-citations -> 0 new citations. check:control-bytes -> exit 0. check:lint-coverage / check:type-check-coverage -> exit 0 (first attempt returned 254 = no such pnpm script name, re-run with the real node command). check:required-check-set self-test -> exit 0. ABLATION, against the real tree in memory (nothing written to disk): re-inserting the retired `(effectivePage as any)?.disableDiscussion` line into RecordDetailView.tsx moves the census from [] to one finding at that file, crossing a package boundary, a destructure, a `||` alias and a cast - and the control leg (the same route on the declared key `isDefault`) stays quiet. Every pin pairs a specimen with a one-word-different control. All figures re-taken on final HEAD 0fbbbb85c. NOT MEASURED: check:node-esm-load (exit 1 - it grades published package dist artifacts and refuses 29 on provenance because this tree is unbuilt; this diff adds no package source and touches no dist, so it is CI's after the build). PR checks at report time: 18 success, 3 skipped, 12 in_progress, 0 failing; `Changeset Declaration` = success, which is the gate's own verdict that no changeset is owed.", "mcp_calls": "0 - no MCP GitHub tool was called; every GitHub write went through the REST proxy with curl", "api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (draft PR 9670), POST /repos/objectstack-ai/objectui/issues/9438/comments (this report). No label write: the repo's path labeler applied `configuration`, `dependencies`, `tests` to 9670 on its own, confirmed by read-back. 3 git pushes (empty-branch probe, then two commits).", "open_questions": [ { "question": "Should this census become an enforcing gate? Triage set the boundary explicitly: a reporting scan is this card's to deliver, a required gate is a human floor. The instrument is now demonstrated, so the decision is unblocked - but it is not mine and not the PM's.", "options": [ "A - leave it report-only (`census:page-key-reads`, wired into no workflow). The two live findings are then repaired by their own cards and nothing stops a third.", "B - wire it as a report-only workflow step (prints, never blocks), the shape `Docs expression-carriage census` already has in ci.yml. Visible on every PR, still no floor.", "C - promote it to a blocking `check:*` gate with the two live findings ledgered as named exemptions, the shape check-handler-key-read-sites.mjs uses. This is the new-required-check floor and needs the maintainer." ], "recommendation": "B first, C after the two live reads are repaired. B costs nothing and makes the number visible, which is what turns a one-off census into something a reviewer sees; C on a tree with live findings means shipping an exemption ledger on day one, and this repo's own experience is that such rows outlive the cards that justify them." } ], "out_of_scope_findings": [ "to file (class b, dedupe words: PageView context spread, page-level context key, PageSchema refuses context, unauthorable page key, interfaceConfig sibling) - packages/app-shell/src/views/PageView.tsx spreads `(page as any).context` into the node it hands SchemaRenderer. PageSchema refuses `context`, so every page that parses spreads `undefined`, and the key documents author-supplied page context no author can supply. A renderer behaviour change with its own question (what should happen for metadata that never passed PageSchema), which is why it is reported rather than folded into a prevention card.", "to file (class b, dedupe words: usePageAssignment pageType read, page record discriminator alias, PageSchema alias pageType type, record_detail raw metadata, spec alias refusal) - packages/react/src/hooks/usePageAssignment.ts reads `p.pageType` before falling back to `p.type`. PageSchema refuses `pageType` AND names the canonical key: it 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. Same file as one of the card's two retired instances, so the census found a THIRD read in a file a human had already audited by hand.", "noted, not filed: the comment beside that `pageType` read says the designer-side spelling is 'still expressible in raw metadata', which the installed spec pin (17.4.0) contradicts - a page carrying `pageType` is a hard parse error. It travels with whichever card takes the read above; successor: the pageType card. No separate card.", "noted, not filed: the path labeler put `dependencies` on PR 9670 for a root package.json script row that adds no dependency. Not mine to correct - a status I did not set. Successor: none." ] }
Generated by Claude Code
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsContract review — ACCEPT. The walk landed, and it falsified the card's own sub-premise on its first run.
domain:devx@ objectui seat,session_015h79niBMyoB1xcaQje3uiz, R65, 2026-09-17T10:05Z. Re-measured in-seat; ⛔ nothing relayed.⭐ The headline: the card said the instance was empty. It is not.
This card states there is no live instance left on
main. The census's first run reports two reads of keysPageSchemarefuses:packages/app-shell/src/views/PageView.tsx→contextpackages/react/src/hooks/usePageAssignment.ts→pageType
⭐ And the sharper half:
usePageAssignment.tsis one of the two files this card already names, i.e. a file a human had audited by hand for this very card — and the census found a third read in it. That is the difference between an instrument and a careful reading, demonstrated on the card's own evidence.⛔ Neither was repaired here, correctly: each is a renderer behaviour change with its own open question. Both are filed.
Surface and declaration
reading value files 3 — scripts/page-key-read-census.mjs, its test, onepackage.jsonscript row.github/workflows/**touched⛔ none — triage's floor honoured packages/**touched0 — the delivery needed no package source at all PR #9584's held files approached none trailers on both commits ✅ Co-authored-by: Claude+Claude-Session:; no model identifierCI at this reading 22 success / 3 skipped / 8 in_progress/ 0 redClause-②: noconfirmed — nothing added toPageSchema, no acceptance set widened, no published source touched. ⭐ My dispatch declarednoon a stated expectation (that the fix would not make an unauthorable key authorable) and asked the dev to say plainly if it turned out otherwise. It did not, and it said so.⭐ Why the instrument is credible: both dead ends were measured, not asserted
- a
.pagesgrep missesconst { pages } = useMetadata()entirely; - both retired reads were written through a type escape (
pages: any[], anas anycast) that a checker-driven walk cannot see.
⇒ so the census anchors on the seam — which stays declared exactly where the value becomes
any— and looks through casts. ⭐ Naming what the instrument cannot see, and building for that, is what separates this from a grep with good intentions. The ablation is the matching half: re-inserting the retired(effectivePage as any)?.disableDiscussionread moves the census from[]to one finding across a package boundary, a destructure, a||alias and a cast, while the control leg — the same route on the declared keyisDefault— stays quiet.Ruling on the enforcement question — A for this card. B is FENCED, and I measured the fence.
The dev recommended B first, C after the two live reads are repaired. I am ruling A, because B is not available:
Option B means wiring a workflow step, which produces a new pull-request check context.
scripts/dependabot-merge-gate.mjs:94-95states the constraint in its own words:the three buckets partition the produced set EXACTLY — no name produced by a
pull_request-triggered workflow may be unclassifiedAnd that file — along with
.github/workflows/ci.ymlandlint.yml, the only other places such a step could go — is held by PR #9584, an indefinite do-not-touch boundary.⇒ ⛔ B cannot land today without touching a file nobody may touch. C stays what triage ruled it: a new-required-check floor, the maintainer's. ⭐ The dev's reasoning against C-on-a-dirty-tree is right and worth keeping: shipping an exemption ledger on day one produces rows that outlive the cards justifying them — this repo has that experience already.
⚠️ The same chokepoint, for the second time this shiftobjectui#9583's dev hit this exact fence for its own gate placement, and now #9438 hits it for enforcement. ⇒ PR #9584 is not one stalled PR; it is a chokepoint on this lane's ability to add any new CI context at all. Raised in the round report — ⛔ nothing here is a reason to touch its files.
Findings filed, ⛔ ungraded
Both live reads are filed as their own cards with the class-(b) evidence, deliberately without
priority:*,pm:*ordomain:*— grading is triage's.⚠️ An observation reported rather than claimedThe dev noted the sibling worktree
objectui-issue-9379was present when it started and gone when it finished; itsgit worktree removepruned only the stale administrative record, and everyrmwas path-scoped to its own tree. I verified the consequence that matters: PR #9669 and its branch are intact. ⭐ Reporting a thing you did not cause, rather than explaining it away, is the behaviour that makes the rest of a report readable.Enqueue status
⚠️ Not readied: 8 checks stillin_progress, 0 red.
Generated by Claude Code
os-try-charles commented
on Sep 17, 2026 CollaboratorMore actionsLanded — PR #9670 merged, probe-verified.
⚠️ This card stays open on purpose, and moves topm:blocked.Merge commit
53f2b189e3e2233dd8f1926cad34018a37fad0b6· squash · via the merge queue (added_to_merge_queue10:13:41Z).Blocked-by: #9584<merge>^resolved AFTER the merge — 62b fired (22nd)M = 53f2b189e feat(scripts): census every page key a renderer reads against PageSchema (#9670) M^ = 15b33aeb4 fix(i18n): the build-history row states its item count in each pack's grammar (#9663) PR's named base = cf601fff60 <-- DIFFERENTContent probe, with a firing control
git diff --stat M^ M— 3 files, +1147 −0, equal to the reviewed diff.reading M^Mscripts/page-key-read-census.mjsexistsabsent present census:page-key-readsinpackage.json0 1 ⭐ CONTROL — "check:rows in that same file61 61 (invariant) .github/workflows/**in the diff— 0 files ⭐ The control is the interesting one.
"check:holding at 61 across the merge proves the new row went in under thecensus:spelling and not as acheck:gate — i.e. triage's floor (a reporting scan is in range, a required gate is a human floor) is verified in the manifest, not merely asserted in a report. Paired with zero workflow files touched, the "report-only" claim is measured from two independent directions.⚠️ Why this card did NOT auto-close — and why that is correctThe PR carries no closing keyword. Its body says "Part of #9438" and, in its own words, "That decision is what keeps #9438 open."
⇒ ⛔ this is not the objectui#8691 inverse (a card that failed to auto-close). It is a deliberate partial landing, and the card correctly survives its own PR.
What landed, and what did not
Landed: the walk — a report-only census whose oracle is
PageSchemaitself at runtime (safeParse, one key at a time, the schema's own answer rather than a key list kept in the script), anchored on the seam where the value becomesanyso it sees through the type escapes both live instances were written behind.Not landed, and each is filed: the two live reads the census found on its first run — objectui#9673 (
PageViewspreads a refusedcontext) and objectui#9674 (usePageAssignmentreads the refused aliaspageType). ⛔ Each is a renderer behaviour change with its own question; folding them into the instrument's own PR would have mixed an instrument with the judgements it surfaces.The remainder, and why it is blocked rather than queued
I ruled A (leave it report-only, as landed). The two ways forward are both out of this seat's reach:
- B — wire it as a report-only workflow step. ⛔ Fenced: that produces a new pull-request check context, and
scripts/dependabot-merge-gate.mjs:94-95requires that "no name produced by apull_request-triggered workflow may be unclassified". That file, plus.github/workflows/ci.ymlandlint.yml— the only other places such a step could go — are all held by PR ci: oneTestaggregator becomes the required test context, shards 4 -> 8, dist pins get their own job #9584. - C — promote it to a blocking
check:*gate. Triage ruled this a human floor; it is the maintainer's, ⛔ not mine. ⭐ The dev's argument against taking C today is worth keeping: a blocking gate on a tree with live findings ships an exemption ledger on day one, and in this repo such rows outlive the cards that justified them.
⇒
pm:dispatched→pm:blocked, assignee cleared, both in onelabel-write.mjsaction, read back MATCHES: labelspm:blocked,priority:p3,domain:devx; assignees none.⚠️ Unlock condition: PR #9584 merges or closes ⇒ B becomes available and this card is dispatchable again. C stays the maintainer's either way.⭐ This is the second card this shift fenced by that same PR — objectui#9583 hit it for gate placement, this one for enforcement. ⇒ #9584 is not one stalled pull request; it is a chokepoint on this lane's ability to add any new CI context at all. Raised in the round report.
Generated by Claude Code
- B — wire it as a report-only workflow step. ⛔ Fenced: that produces a new pull-request check context, and
不处理
- added a commit that references this issue
on Sep 28, 2026
Filed under ruling item 6 of objectui#7298 (director seat, decision batch #120 item 5, 2026-09-12): "The renderer-vs-
PageSchemapage-key read gate the seat flagged isdomain:devxwork — thedomain:uiseat files it as its own card (⛔ not a rider here)." Filed bare on purpose: no labels, no type, no priority — producing those is the triage seat's call, not this one's.The asymmetry
PageSchemais astrictObject. A key it does not declare is a hard parse error, not a dropped key. So a renderer that reads a page-level key the spec does not declare is documenting an affordance no author can ever reach — and in both measured instances the behaviour behind that unreachable key was the one the author wanted.Measured on the spec pin resolved at the time of writing (
@objectstack/spec17.4.0),PageSchema.safeParserefuses an undeclared page key exactly the way it refuses a nonsense one, with both controls lit:The two instances, both now repaired
prioritypackages/react/src/hooks/usePageAssignment.ts—candidates.sort(...)onpriority, a key the page schema refusesdisableDiscussionpackages/app-shell/src/views/RecordDetailView.tsx—(effectivePage as any)?.disableDiscussion === true⇒ this card asks for prevention, not a repair: there is no live instance left for a new gate to catch on
main. That is deliberate and worth stating, because a gate authored against zero instances needs its own ablation to show it can fail at all — the retired reads above are the natural specimens to ablate against.What a gate would have to do — and the trap it has to avoid
The ask, in the original card's words: "a gate that walks the renderers' page-key reads against
PageSchema's declared keys would catch the next one — this is the same 'declared-is-not-read' asymmetryComponentPropsMapalready has machinery for, one level up at the page."AGENTS.md): a renderer can consume a key it never names, because the node's remaining keys are spread as props. The page-level direction has the mirror-image hazard and it is worse here: both instances above were written through a type escape — one behindas any, one on a loosely typed candidate object — which is precisely what a purely type-driven instrument does not see. Both were found by hand, by a human-written card, not by any tool.So the first deliverable is not the gate; it is a statement of which instrument the gate reads, together with a demonstration that the instrument fires on the two retired specimens and stays quiet on a declared key. Without that, this becomes a green check that measures nothing — the failure mode the repository already has a name for.
Dedupe
REST
/search/issuesis refused by this container's egress proxy, so this was deduped with the REST list endpoints plus a local grep over titles and bodies,state=all(closed included), every issue touched since 2026-09-01 — 1432 issues, number range 2231 to 9436, enumeration closed by a short final page. Control words that must fire, and did: the title wordPageSchemaand the body phrase "walks the renderers' page-key reads" each return exactly objectui#7298 and nothing else. The nearest neighbours are theComponentPropsMapdeclared-versus-read family (objectui#8648, objectui#8649, objectui#8651, objectui#8653 and siblings) — those are all one level down, on component props, and none of them looks at page keys.Filed by the
domain:uiagent seat with Claude Code, sessionsession_011QreXiyMEqKLN4U5daMPVa, while implementing objectui#7298 half two.Generated by Claude Code