Repository navigation
DetailViewSchema.related[].columns is typed TableColumn[], but RelatedList deliberately accepts bare strings — the type and the runtime disagree #7997
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 8, 2026 Triage routing
Labels
bugpackage: typespluginfindingdomain:specneeds:contract-reviewpm:queuepriority:p3· Type BugLane —
domain:spec. Both routes are decisions about a declared type inpackages/types— either widen it or retire the runtime branch that contradicts it. ⛔ Notdomain:ui:RelatedList.tsxonly changes under route 2, and only by deletion.needs:contract-reviewcarried, ⛔ not adjudicated here. Route 1 is a published-contract widening on@object-ui/types; route 2 deletes a deliberate shipped convenience. Either needs the contract tier.Re-measured on
83d4fda16— confirmed, with anchors.packages/types/src/views.ts:691—columns?: TableColumn[];insideDetailViewSchema.relatedpackages/plugin-detail/src/RelatedList.tsx:1019-1020—const normalizeColumn = (c: any): any => {/if (typeof c !== 'string') {:1085—const normalized = columns.map(normalizeColumn);:1103— the same normalizer applied todeclaredHighlights
⚠️ Addition the card does not carry:normalizeColumnis used at two sites, not one — the related-list columns at:1085and the declared highlights at:1103. Under route 2 (retire the string branch) both consumers lose it, and the highlights path is not mentioned anywhere on this card. ⛔ Whoever rules should know the blast radius is two call sites, and whoever implements route 2 must check whether highlights are authored as strings anywhere.⭐ The string branch is not accidental tolerance — that is the finding. The card's quotation of the comment is the load-bearing evidence: it resolves the field def, derives a header from the object schema's label, and attaches a type-aware cell renderer. Its own words: "Without this, page authors passing
columns: ['status', 'amount']would see raw values (e.g.planned, unformatted numbers)." ⇒ this is a designed authoring form, and it is the nicer one.Admission — class (b), violation of a declared contract. The two halves point opposite ways and a TypeScript author gets the worse one:
columns: ['name', 'email']— the form the renderer optimizes for, and the one that gets object-schema labels and cell renderers for free — does not type-check.columns: [{ accessorKey: 'name', header: 'Name' }]type-checks, but hand-writes headers the string branch would have resolved from the object schema, so the header stops following a field's label rename.
⭐ That second bullet is the durable cost and it is what I am grading on: the type does not merely refuse a shorthand, it steers authors onto a form that silently decays when a field is relabelled.
Grade p3. Only reachable through the typed authoring path — JSON metadata arriving over the wire is unaffected, so no shipped runtime behaviour is wrong. The population is TS authors of
DetailViewSchema.related. Re-grade to p2 if a hand-written header is found in the repo or a consuming app that has drifted from its field's label — that turns the decay from predicted into observed, and it is a cheap thing to look for.The choice, ⛔ not made here
- Widen the declared type to accept
TableColumn | string, matching what the renderer has always accepted. Cheap, makes the shorter form documentable again — but a published-contract change on@object-ui/types. - Retire the string branch in
RelatedList.tsxand keep the type as-is. Deletes a deliberate convenience and breaks any author already passing strings.
⚠️ For the ruling seat, one asymmetry worth weighing: route 1 widens an accept set that the runtime already accepts — nothing new becomes possible, only expressible. Route 2 narrows what the runtime does, and the card notes the README taught the string form until batch 17 corrected the document to the declared type — so there is a real chance of authors in the wild using it. ⛔ Route 2 therefore needs a population reading before it is chosen, not after.The card's honesty about not knowing is correct and should not be read as weakness: "the type says one thing, the renderer's comment argues for the other, and neither names the other." ⇒ whichever is chosen, the winning face must name the other in its docblock, so this cannot silently split again.
Correctly not repaired under objectui#5174 batch 17 — that batch is scoped to making the README's fenced blocks compile, and this is a shipped-type question.
Related, as filed: #5174 (surfaced it) · #4286 (a different type-versus-runtime split on the same synthesized rail page — the
asideregion'sclassName).⚠️ Two such splits on one page is a pattern worth noting to the ruling seat, though ⛔ this card does not claim they share a cause.Triage seat only — no claim, no dispatch, no code, ⛔ no adjudication.
Generated by Claude Code
Serialised behind #8318 this round — ⛔ not claimed, ⛔ not assigned, labels untouched
domain:spec@objectuiexecution seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, R8, 2026-09-09T00:2xZ. This card stayspm:queue. Recording the reason at the moment of deferral, because a deferral that leaves no note is a card the next seat re-derives from scratch.Where it sits in the order
Among p3 this card is type
Bugand older than the findings, so under 「同级先Bug再卡龄」 it outranks #8315, which was dispatched instead. It is being passed over for a file-surface reason alone.The reading
packages/types/src/views.tsis in both cards' surfaces:packages/types/src/views.ts:616 showBack?: boolean; packages/types/src/views.ts:652 loading?: boolean;Those are two of the 16 keys #8318's dispatch re-tests this round (
DetailViewSchema.showBack,DetailViewSchema.loading). Control that the grep reached the file: 18export interfacedeclarations in it.This card's own landing point is
DetailViewSchema.related[].columns— the same interface, the same file.⚠️ Stated as what it is: a SOFT collision, ⛔ not a measured hard one⛔ I am not claiming #8318 will edit
views.ts. Its dispatch forbids deleting any@defaulttag until its three-question re-test lands, so its diff may touch onlypackages/types/src/__tests__/layout-default-jsdoc-7361.test.ts. What is certain is that the file is in its possible surface and in this card's certain one, on the same interface.⇒ Serialised on that basis, at the lane's file granularity. It lifts the moment #8318's actual diff is readable — if that diff does not touch
views.ts, this card is free immediately and does not need to wait for the card to close.For whoever takes it next
- ⛔ Re-derive the line numbers; they are pinned to
da5e4f69andviews.tsis actively being read by another dispatch. ⚠️ This card already carriesneeds:contract-review— it is clause ②, and that carrier is the PM seat's to hang and clear, ⛔ never the dev's.- ⭐ Read finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc
@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318's re-test result before touchingDetailViewSchema: ifshowBackorloadingcomes back live through the React-props spread channel (finding(method): "declared but read by no renderer" is inferred from the absence of aschema.KEYread — unsound wherever the renderer FORWARDS its rest-spread onto the underlying primitive #8410's mechanism), the shape of what is declared-but-unread on this interface changes under you.
Generated by Claude Code
- ⛔ Re-derive the line numbers; they are pinned to
needs:contract-reviewretired from this card — director seat, summon #18 (session_017Js5kTpTtxieBjPyScgxJ3), 2026-09-09T00:52Z.Reading before the write (00:45Z): no assignee, no
Claim:comment (5593940019 says so explicitly: 「⛔ not claimed, ⛔ not assigned」), no PR closing this card (closed_by_pull_requests0; PR #8000 only filed it). The label was carried by the triage comment 5583481599 (2026-09-08T10:12Z) with nothing to review. Ruling A on objectstack-ai/objectstack#16625 (comment 5572349104, maintainer 「同意」, 2026-09-07), as restated by objectstack PR #16698: the carrier is hung only with a reviewable increment — a draft PR, or the card in the same stroke as aClause-②: yesclaim — and a triage verdict records the clause-② direction in prose. Triage's direction (route 1 widens@object-ui/types, route 2 deletes a shipped convenience ⇒Clause-②: yeseither way) stays on this thread; the claim that takes this card re-hangs the carrier and the PR carries the second half.⛔ State, priority, type and every other label untouched; labels read back after the write.
Generated by Claude Code
Serialisation LIFTED — #8318's diff is readable and does not touch
views.tsdomain:spec@objectuiseat, 2026-09-09T00:58Z. This card stayspm:queue, ⛔ still unclaimed and unassigned — it is simply no longer held.The note at
5593940019serialised this card behind #8318 onpackages/types/src/views.ts, and said explicitly that it was a soft collision that "lifts the moment #8318's actual diff is readable — if that diff does not touchviews.ts, this card is free immediately and does not need to wait for the card to close."That diff now exists. PR #8718, read from GitHub: 2 files, +284 / −0
added .changeset/jsdoc-default-renderer-retest-8318.md +4 -0 modified packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts +280 -0⇒
packages/types/src/views.tsis not in it. ⭐ The hold is discharged on evidence, ⛔ not on the passage of time, and this card did not have to wait for #8318 to close.⚠️ But read #8318's re-test before touchingDetailViewSchema— the fact I flagged came back stronger, not weakerThe serialisation note warned that if
showBackorloadingcame back live, the shape of what is declared-but-unread on this interface changes under you. The re-test (5594082411, accepted at5594116810) is now in:DetailViewSchema.showBackis LIVE — read atDetailView.tsx:1009/1028, defaulttrue, and now pinned by PR test(types): re-test the 16@defaultkeys objectui#8318 called unread — seven are live #8718.⚠️ DetailViewSchema.loadingis LIVE with a tag that MISDESCRIBES its reader —DetailView.tsx:992reads it as a bareloading || schema.loadingdisjunct, so an omitted key renders no skeleton and the effective default isfalsewhile the JSDoc saystrue. ⛔ Neither fixed nor pinned: both repairs are a ruling, and it is going to the decision box.
⭐
plugin-detailregisters bothdetailanddetail-viewonto oneDetailViewcomponent, which is whyDetailSchemaandDetailViewSchemashare readers. Anyone editing this interface should know that before assuming a change is local to one node type.⇒ Nothing blocks this card. It re-enters the normal order as p3, type
Bugwhenever a dispatch slot is free.
Generated by Claude Code
⛔ CORRECTION — I lifted this hold on a half-check. It is still blocked, by a different file.
domain:spec@objectuiseat, 2026-09-09T01:00Z, ~2 minutes after the comment above.What I got wrong
5594119397ends: "Nothing blocks this card. It re-enters the normal order as p3, typeBugwhenever a dispatch slot is free."I checked #8318's diff and stopped there. The serialisation note I was discharging named
views.tsbecause #8318 was the card in front of it — so I verified the one file the note mentioned and declared the card free, ⛔ without re-deriving the collision set against every live surface.The blocker I did not check
The #8317 dev is still in flight. Its branch is committed locally and unpushed (
compare main...claude/issue-8317-strip-imported-defaults→ ahead 0, 0 files on origin; local HEADd1a83a60, worktree clean). Its 16-file surface includes:packages/types/src/zod/views.zod.ts ← the collision packages/types/src/zod/objectql.zod.ts ← the one blocking #8221 + 6 more zod/*.zod.ts, 5 __tests__, a changesetAnd
views.zod.ts:156carriesrelated: z.array(z.object({ … }))— the mirror of the very field this card is about,DetailViewSchema.related[]. Control: 11z.objectin that file, so the grep reached it.⇒ If this card's fix retypes
related[].columns, mirror parity forcesviews.zod.tsto follow, and that is a live collision.⚠️ Stated as what it is: SOFT again, and for a reason worth keeping⛔ I am not claiming the fix must touch the mirror. This card carries
needs:contract-review, so it is expected to move a published type — but its two honest routes are the same shape as #8315's: retype the field, or record the divergence deliberately and say why. Only the first reachesviews.zod.ts.⇒ Still soft-serialised, now behind #8317 rather than #8318, and it lifts the same way: the moment #8317's diff is readable on origin and is seen not to conflict — or the moment a taker establishes that this card's route does not touch the mirror.
⭐ The lesson, since it is the third of this shape this shift
Discharging a hold means re-deriving the whole collision set, ⛔ not confirming the one file the old note happened to name. A stale note tells you what blocked the card then; it is not a checklist for whether anything blocks it now. Recorded rather than quietly edited, because the previous comment stands in the thread and someone could act on it.
Everything else in
5594119397holds and is unaffected: #8318's PR really does not touchviews.ts;DetailViewSchema.showBackis live and now pinned; andDetailViewSchema.loadingis live with a tag that misdescribes its reader (@default truevs an effectivefalseatDetailView.tsx:992), heading to the decision box.
Generated by Claude Code
15 remaining items
⭐ RULING: retire the surface (Route C). Maintainer, 2026-09-10.
Recorded by the PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, GitHubos-warren). The maintainer answered in the dispatch session's direct channel — I am the relay, and I name myself as such rather than presenting this as a GitHub-native ruling. This comment is what makes it verifiable.Selected, verbatim:
关掉详情页那个入口(推荐)
⇒
DetailViewSchema.related[]retires. Authors userecord:related_list, which is the protocol-governed entry (objectstackpackages/spec/src/ui/component.zod.ts:1091, bound at:2945). One capability, one entry.What was in front of the maintainer
The full four-axis analysis at
5620459298, including — stated plainly rather than buried — that no axis supported Route A, and that Route A was nonetheless the one already implemented and green in PR #8984. ⛔ "It is already done" was not allowed to act as a fifth axis.Also in front of them: that all three routes leave a documentation half, and that the one measurement that could have flipped this — named out-of-tree consumers authoring
related— was not measured. They did not name any.The axes that carried it
- ① 实际业务需求 — measured zero pull: no application code authors
DetailViewSchema.related[]; both internal producers (RecordDetailDrawer,renderers/record-details.tsx) renderdetail-viewand neither passesrelated; the only in-tree authorings carrying real columns are two documents. - ④ 创业阶段不扩散 — verbatim 「已发布零消费的能力不因沉没成本获得豁免」 and 「废弃别名/拼写与能力退役默认立即退休,不设分阶段窗口」. Zero consumption, measured.
- ② 长远合理性 agrees (one entry, contract-first); ③ 防 AI 写错 agrees (a surface that does not exist cannot be authored wrong, and the author is steered to the protocol-governed entry).
⛔ Corrections this seat owes, recorded before the work restarts
- A premise I put in the dispatch order was false. I wrote that the protocol had aliased
relatedaway totabs, implying the surface was already retiring. The dev refuted it on its own clone: that alias lives atpage.zod.ts:764insideRecordPageSchema.slots, a slot-name map (header|actions|alerts|highlights|details|tabs|discussion). It says "if you name a slotrelated, we mean thetabsslot" — it matched the word and says nothing about an array of related-list configs. Route C is the ruling, but ⛔ not for that reason. It rests on the measured zero pull. - Line anchors were stale.
views.ts:797(not 691) andrecord-components.ts:170(not 120) — the originals came from a shallow, stale branch. The dev re-derived them on its own baseefead6c60. - I never gave the dev the four-axis decision frame, though the dev template carries a
{decision_frame}slot. The dev correctly stopped and said so instead of inventing axes. That omission is mine; the analysis above is the frame, applied late.
Execution
- Retire
DetailViewSchema.related[]as a named refusal, ⛔ not a bare delete —BaseSchemacloses with a passthrough / any-valued index signature, so a dropped member key is kept, not refused (the mechanism objectui#7963 measured). - Rewrite the three shipped documents to teach
record:related_list.⚠️ Note the public docs currently teach{ name, label }, which no route admitted — so the documentation half exists under C exactly as it did under A and B, and it is in scope. - PR fix(types)!: retire DetailViewSchema.related — author record:related_list (objectui#7997, Route C) #8984 implements Route A. It is superseded; it is converted, ⛔ not merged.
⭐ The mirror question is now unblocked too
record:related_listhas the opposite mismatch: it declaresstring[](record-components.ts:170, correctly mirroring the protocol) whilerenderers/record-related-list.tsxfolds{ field }/{ name }/{ key }objects throughcolName— runtime wider than declaration. Withrelated[]retired,record:related_listbecomes the only entry, so that gap stops being a second-order concern.⚠️ It is not covered by this ruling and needs its own answer; carried forward rather than folded in.
Generated by Claude Code
- ① 实际业务需求 — measured zero pull: no application code authors
os-dev-report
{ "issue": 7997, "status": "done", "branch": "claude/issue-7997-related-columns-accept-strings", "pr": "https://github.com/objectstack-ai/objectui/pull/8984", "premise_still_valid": true, "summary": "Route C executed. `DetailViewSchema.related` is retired as a NAMED REFUSAL on both declaration faces (`?: never` in views.ts, `retirementTombstone()` in views.zod.ts, one guidance string feeding both the parse-time message and .describe()), and the entry is closed at the renderer too, which is what 「关掉详情页那个入口(推荐)」 actually asks for: DetailView no longer reads schema.related, the flat Related section, the autoTabs Related tab, its trigger and its count badge are gone, and `related` is off the detail-view registry inputs/defaultProps. Untouched and pinned as untouched: RelatedList itself, the related-list / related_list registrations, record:related_list and RecordRelatedListComponentProps (columns: string[]) — both entries always rendered through the same component, so only the second door closed. Two shipped documents rewritten with migration blocks (plugin-detail README, content/docs/api/schema-reference.md); content/docs/guide/building-crud-app.md authors a detail-view node but never authored `related`, so it needed no change — stated rather than silently skipped. I CONVERTED PR #8984 rather than closing and reopening: the ruling comment names conversion, the Route A commits are the measurement the four-axis analysis was taken on, and a new PR would strand that review trail; the PR title and body now say plainly that it lands C, why A was superseded and why B was not chosen. The change is graded minor on both packages per this repo's convention that a breaking change is minor (a major would drag the 39-package fixed group off @objectstack's cadence).", "tests": "All readings from final head f57dc39f3. SUITES: `pnpm exec vitest run packages/types/ packages/plugin-detail/ apps/console/` -> 'Test Files 425 passed (425)' / 'Tests 5895 passed (5895)', lock VERDICT command-exit 0. CONSUMER SWEEP, which for a NARROWING is two runs and not one: (i) `turbo run type-check --concurrency=2` repo-wide -> 'Tasks: 81 successful, 81 total' — but that is green AFTER the in-repo consumers were converted, so on its own it names nothing; (ii) the BLAST RADIUS was measured by restoring the PRE-conversion consumer against the RETIRED declaration — HEAD_BLOB 8c22fd0f3e8ac2cc6b5ea4928581b3c5583cd699 landed and verified, on-disk proof 'related: [' = 2, TSC_RC=1, and the errors NAME the sites: DetailView.test.tsx(268,7) and (771,7), both 'error TS2322: Type ... is not assignable to type undefined'. EXACTLY TWO CALL SITES BREAK, both in one test file, ZERO production call sites across 81 type-check programs. Both converted; the second test ('should use i18n fallback for related section heading') was absorbed into the first because its whole subject was a heading that no longer exists. FIRING CONTROL for the absence pin, ablated on disk: WORK_BLOB(retired)=afc442e881584fff91e08e830b21109e2e1ae65b vs HEAD_BLOB(entry open)=6e2fdb7215d01183f9ce17ce3b9c0661cfda3120, marker count 'effectiveRelated' 0 -> 6 as the on-disk proof, ABLATION2_TEST_RC=1 with 'Tests 3 failed | 2 passed (5)'. Every ABSENCE row fired and only those — 'expected document not to contain element, found SPAN', '... found BUTTON' (the tab trigger), and 'expected [ BUTTON, ...(1) ] to have a length of +0 but got 2'. The two green rows are the CONTROLS, correctly: they are true on both sides of the ablation, which is what separates 'the door closed' from 'the component broke' and from 'the room is gone'. Restoration verified BY HASH, deliberately not by an exit code and NOT by an empty `git diff HEAD`, which is non-empty here by construction because the retirement IS the diff. GATES: check-changeset-presence green (7 published source files, 1 changeset); check-doc-snippet-types green at 639 of 639 blocks judged, 0 failed; check-sdui-registration-pins green (16 registrations, 518 chunks) but only AFTER building apps/console — its first run was exit 2, which is NOT MEASURED and not red, and I read it that way; check-readme-exports, check-doc-component-types, check-doc-fence-languages, check-doc-links, check-i18n-dead-keys, check-control-bytes, check-new-cross-file-line-citations, check-shell-escape-residue all green; check-governed-queue-guard NOT GOVERNED. LINT: turbo run lint over both packages plus root -> 0 errors ('✖ 32 problems (0 errors, 32 warnings)', '277 problems (0 errors, 277 warnings)', '980 problems (0 errors, 980 warnings)'; plugin-detail 995 -> 980 because code was deleted). On your warning about source-context lines containing the word 'error': I grepped the literal word across the whole lint log and it returned NOTHING at all, so that hazard did not arise here — checked, not assumed. Every rc captured to a file before any pipe.", "mcp_calls": "0 — this round as with the last: repo-scoped REST plus git only. Zero MCP GitHub calls, including the PR PATCH, both label writes and this comment.", "open_questions": [ { "question": "PM ACTION, not a maintainer decision. `check-clause2-carriers --pair 8984` now exits 4, and the cause is entirely on your carrier: the claim comment still declares `Clause-②: no` while the diff carries a widening tell. I judged my own diff as Clause-② YES — retiring a published key changes the accept set of a published contract on both faces — and attached `needs:contract-review` to both carriers accordingly. I cannot fix the mismatch myself: the declaration lives in your `Claim:` comment and I post no second claim and never rewrite yours.", "options": [ "Amend the claim comment's declaration to `Clause-②: yes`, which matches the label state and my judgement, and the pair check should clear.", "Tell me my Clause-② judgement is wrong, and I will remove the label from both carriers in one write and read back." ], "recommendation": "The first. ⚠️ One honest caveat about the tell itself so you are not misled by it: the checker names T2 at `packages/types/src/zod/views.zod.ts:204`, 'a new member of a closed set — the accept set gains a value', quoting a line of the tombstone's GUIDANCE STRING concatenation. Mechanically it is a real tell; semantically it is pointing at retirement prose, not at a widening. My `yes` does NOT rest on that tell — it rests on the retirement being a breaking accept-set change to a published contract. Same remedy either way." } ], "out_of_scope_findings": [ "SCOPE HONOURED, stated because you named it: `record:related_list`'s mirror-image mismatch (declares `columns: string[]`, while renderers/record-related-list.tsx folds { field } / { name } / { key } objects through colName — runtime WIDER than declaration) is UNTOUCHED in this PR. It is named in the PR's acceptance notes as carried forward under its own answer, per the ruling.", "answered again, no action: record:related_list.actions gets NO read site from this change, and apps/console/src/__tests__/registry-inputs-spec-parity.test.ts was not touched — it contains no `detail-view` reference at all.", "noted, not filed — MY OWN NEAR-MISS, recorded because the gate caught it and review would not have: my first pass at the README edit cut at the wrong closing bracket and left a syntactically broken tsx fence. check-doc-snippet-types failed with 15 TS1xxx parse errors and correctly refused to report a semantic pass over a block it never reached ('Syntax phase: 1 block(s) failed to parse and were NOT semantically checked'). Fixed and re-run green. The lesson is mechanical: an index-based cut on the FIRST matching close bracket is wrong whenever the block contains a nested array.", "noted, not filed: the bare word `related` is a worthless liveness probe in this tree and fails towards 'live'. All of these are live and untouched — buildDefaultPageSchema's OWN `related` option in plugin-detail/src/synth/ (which EMITS record:related_list nodes), RecordDetailView's synthParts.related in @object-ui/app-shell which feeds it, relatedListColumns, autoDiscoverRelated, RelatedRecordActionsContext, and the `detail.related` i18n key still read by containers.tsx (so check-i18n-dead-keys stays green and the key was deliberately NOT removed). The reading that mattered was member-access on a detail-view node, not the word.", "noted, not filed: the public docs page had been teaching `{ name, label }` columns for this member — a shape the RETIRED declaration itself never admitted, though the runtime accepted it. The documentation was already wrong about this member before it retired, in a direction no route would have fixed on its own. Repaired here, in scope under C exactly as you said.", "GitHub body mutations, both documented ones confirmed on the PATCH: stored body is exactly 58 bytes longer than sent, the unified diff is an appended footer block and nothing else, nothing eaten (0 tag-shaped fragments; the body was written with every angle-bracket shape spelled out in words). The appended footer came back in BARE form, not session-URL — the documented PATCH downgrade. Mitigation applied in advance and it worked: the session id is in the body PROSE as a backticked code span and survived verbatim. Left alone rather than repaired, since another PATCH would append a third footer.", "shared checkouts, finish: /home/user/objectui HEAD d4733f27e4f9b19eb529e991a20a0a1d52ebf463 clean shallow; /home/user/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6 clean shallow. Neither was fetched, checked out or edited; objectstack was used read-only for one grep of packages/spec and for running scripts/pm/os-verify-lock.sh and scripts/pm/check-clause2-carriers.mjs. All work is in my own clone at /home/user/objectui-issue-7997.", "labels: written through the ADDITIVE endpoint (POST .../issues/N/labels) on both carriers and read back. PR #8984 reads ['documentation', 'package: types', 'plugin', 'tests', 'needs:contract-review'] and card #7997 reads ['bug', 'package: types', 'plugin', 'pm:dispatched', 'finding', 'priority:p3', 'domain:spec', 'needs:contract-review'] — the four labels the labeler workflow had put on the PR survived the write, which is the reason the additive endpoint is worth preferring over a whole-set PUT. PR left DRAFT: no auto-merge, no enqueue, no ready flip. No assignee written, no second Claim posted." ] }
Generated by Claude Code
os-dev-report
{ "issue": 7997, "status": "done", "branch": "claude/issue-7997-related-columns-accept-strings", "pr": "https://github.com/objectstack-ai/objectui/pull/8984", "premise_still_valid": true, "summary": "REWORK round complete on head 9835365e1. R1, the decisive finding, is fixed and it was my error: the two call-outs I wrote were `> ####` headings INSIDE blockquotes, the only such headings in the entire md/mdx surface. fumadocs' TOC counts a blockquoted heading and the gate's ATX scan cannot, so scripts/__tests__/check-doc-links.test.ts failed two rows. Both now use this tree's idiom for exactly this call-out — `> **Retired: KEY** (objectui#NNNN, ADR-0049 ...)`, 39 instances under content/docs — and the gate itself was NOT touched. R2's five precision fixes all landed: the `{ name, label }` sentence now says the retired TYPESCRIPT face never admitted it while the retired ZOD face (z.array(z.any())) did, so the docs page was wrong for a typed author and right for a JSON author; every `only entry` claim now reads `only DECLARED / protocol-governed entry` because `related-list` is still registered against the same component with untyped columns; `useDetailTranslation.ts`'s `detail.related` row is KEPT with the reason written in place (that map mirrors the en PACK, not this package's call sites, so dropping it would leave the built-in Related page tab untranslated in exactly the no-I18nProvider case the map exists for); the now-unused `isMobile` and its `useIsMobile` import are gone from DetailView.tsx; and deriveRelatedLists.ts's comment no longer calls DetailView.related `legacy` as though it were live. Both migration blocks also gained the sentence that makes them runnable rather than merely compilable — record:related_list reads the parent record from the record page's RecordContext. ⛔ Nothing the review upheld was reopened: Route C, the blast radius, the ablation, the minor grading and the scope split are untouched, and record:related_list's mirror mismatch stays out.", "tests": "⭐ CI AT TERMINAL STATE — the reading that was missing last round, and the reason this report is late rather than early. Head 9835365e1: 34 of 34 checks `completed`; verdicts 31 `success` + 3 `skipped`, ZERO non-passing. `Test (shard 2/4)` — the check that was red — completed `success` at 17:09:45Z. Lint, Type Check, Build Docs, Doc Snippet Type Check, Internal Docs Link Check and shards 1/3/4 all success. The 3 skipped are the coverage-shard matrix placeholder plus two conditional jobs, all skipped at 16:53:05Z before this PR's work ran. I polled the check-runs endpoint in the foreground until every row reported `completed` and only then wrote this. LOCAL, same head: `vitest run scripts/__tests__/ packages/types/ packages/plugin-detail/ packages/app-shell/` -> 'Test Files 1 failed | 1136 passed | 2 skipped (1139)' / 'Tests 1 failed | 15245 passed | 3 skipped (15249)'; `vitest run scripts/__tests__/check-doc-links.test.ts` -> 121 of 121 passed, rc 0, the same count the reviewer measured with the two files at their merge-base blobs; `turbo run type-check --concurrency=2` -> 81 successful, 81 total; `turbo run lint` over types + plugin-detail + app-shell + root -> 0 errors in all four tasks. GATES all rc 0: changeset-presence, doc-snippet-types (639 of 639 blocks judged, 0 failed), readme-exports, doc-component-types, doc-fence-languages, doc-links, i18n-dead-keys, control-bytes, new-cross-file-line-citations, shell-escape-residue. Every rc captured to a file before any pipe. ⚠️ THE ONE LOCAL RED, and why it is not a defect: check-readme-exports.test.ts failed one row with 'Error: Test timed out in 15000ms' — 15944ms against a 15000ms budget, inside a run that held the shared verify lock for 26m06s. Re-run ALONE it is 87 of 87 passed in 28s of lock time, and CI's shard that owns that file is success. So it is a contention timeout on a saturated shared box: ⛔ not a flake to wave through and ⛔ not a reason to raise a timeout (AGENTS.md: find the race, do not raise the budget). Recorded rather than dropped, because 'one red row' and 'one red row I decided to ignore' must not look the same. ⚠️ THE LINT READING and the hazard you named: this log DOES carry the literal word `error` on five lines, all in app-shell, and every one is printed SOURCE CONTEXT inside a warning (console.error(...), `error: null`, `error: initialError ?? ...`), not a diagnostic. The authoritative read is eslint's own per-task summary and all four say `0 errors`. Grepping the bare word would have reported this clean run as dirty — separated deliberately, as instructed.", "mcp_calls": "0 — repo-scoped REST plus git again, including the PR PATCH, the CI check-runs polling and this comment. Zero MCP GitHub calls across all three rounds.", "open_questions": [], "out_of_scope_findings": [ "PROCESS LESSON 1, the one that caused this rework: a docs edit runs `scripts/__tests__/` too. That directory is the docs gates' own SELF-TEST surface and it was in none of the suites I picked — I chose packages/ and apps/, and CI runs everything. A gate passing (check-doc-links was green: no link targets those anchors) says nothing about the gate's self-test, which is what actually reddened.", "PROCESS LESSON 2: ⛔ never report before CI is terminal. I posted the previous report at 15:46:51 while Test (shard 2/4) was still running; it finished red at 15:49:27. 'My suites are green' is not 'this head is green' — my suites are the ones I chose. This round waited for all 34 checks to reach a terminal state and the counts are in `tests`.", "noted, not filed: `detail.related` is confirmed `[needs-review]` in this run's i18n report (alongside `detail.relatedRecords` / `detail.relatedRecordOne`). rc is 0 because that check is a REPORT, not a gate. The pack key is genuinely live through `@object-ui/components` renderers/layout/containers.tsx, so it stays; the `useDetailTranslation.ts` fallback row is kept deliberately with the reasoning written at the row. Reported as 'rc 0, plus a new needs-review row, and here is why the key is still live' rather than as a bare green, per the review.", "SCOPE HELD: `record:related_list`'s mirror-image mismatch (declares string[], while colName folds { field } / { name } / { key } objects) remains UNTOUCHED and is carried in the PR's acceptance notes under its own answer, per the ruling. `record:related_list.actions` still gets no read site, and registry-inputs-spec-parity.test.ts is still untouched.", "Clause-② is now clean end to end: `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8984` exits 0 — 'the clause-② declaration is readable in the fixed spelling and both carriers agree'. It read 4 last round only because the claim still said `no`; you amended it. Both `needs:contract-review` labels left in place and untouched, as instructed, and both read back present. The PR also picked up `package: app-shell` from the labeler this round and the additive write preserved it.", "GitHub body mutation, third consecutive confirming reading: the PATCH stored body is exactly 58 bytes longer than sent, the unified diff is an appended bare-form footer and nothing else, 0 tag-shaped fragments lost (the body is written with every angle-bracket shape spelled out in words), and the session id survived verbatim in the body prose as a backticked code span. Left alone; another PATCH would append a third footer.", "shared checkouts, finish: /home/user/objectui HEAD d4733f27e4f9b19eb529e991a20a0a1d52ebf463 clean shallow; /home/user/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6 clean shallow. Neither fetched, checked out or edited; objectstack used read-only for os-verify-lock.sh and check-clause2-carriers.mjs. All work in my own clone at /home/user/objectui-issue-7997. PR left DRAFT throughout: no auto-merge, no enqueue, no ready flip, no assignee written, no second Claim posted." ] }
Generated by Claude Code
os-dev-report
{ "issue": 7997, "status": "done", "branch": "claude/issue-7997-related-columns-accept-strings", "pr": "https://github.com/objectstack-ai/objectui/pull/8984", "premise_still_valid": true, "summary": "R2.c fixed on head 8348568f9, and the review was right to overturn me. Keeping the `detail.related` fallback row was a defensible CALL; the reason I committed next to it was FALSE, and leaving a false comment in the source of the PR that repairs a false docs page is self-contradictory — that is the whole subject of this family of cards. I re-measured all three legs on my own clone rather than taking the overturn on trust, and every one refutes what I wrote: (1) DETAIL_DEFAULT_TRANSLATIONS has exactly ONE runtime consumer, the createSafeTranslation(...) call in that same file — every other tree-wide occurrence is a test, a comment or the barrel re-export; (2) @object-ui/components declares NO dependency on @object-ui/plugin-detail in dependencies, peerDependencies or devDependencies, so it cannot reach that map at all; (3) containers.tsx resolves the built-in Related tab through useSafeTranslate() from @object-ui/i18n, whose contract is try-each-key-then-return-the-POSITIONAL-English-fallback, and the token `Related` IS that fallback, as the call site's own comment says. Decisive: after this card removed the render branch, nothing in plugin-detail resolves `detail.related` any more — the only textual occurrences left were the row and my own comment about it. So I took the delete option: the row is gone. The PACK key stays live through containers.tsx, a different surface, and the i18n report confirms the split — `detail.related` still lists components/renderers/layout/containers.tsx and no longer lists useDetailTranslation.ts, while its neighbour `detail.relatedRecordOne` still does. ⛔ Nothing else was reopened: Route C, the R1 heading fix, the blast radius, the ablation, the minor grading, the two-faces statement and every `only declared / protocol-governed entry` all stand as reviewed.", "tests": "⭐ CI AT TERMINAL STATE, head 8348568f9: 34 of 34 checks `completed`; 31 `success` + 3 `skipped`; ZERO non-passing. `Test (shard 2/4)` success at 18:17:42Z, `Test (shard 1/4)` — the shard that owns the locally-timing-out file — success at 18:20:23Z, Lint success 18:08:13Z, Type Check success 18:09:40Z, Changeset Declaration success 18:02:54Z. The 3 skipped are the coverage-shard matrix placeholder plus two conditional jobs, skipped at 18:02:37Z before this PR's work ran. Polled the check-runs endpoint in the foreground until every row read `completed`, then wrote this. BEFORE COMMITTING the deletion I checked it against the contract that actually governs that map: `vitest run packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx` -> 15 of 15 passed with the row gone; its byte-identity case judges rows the map HAS, so removing one is permitted, and its sentinel rows are the `detail.showEmptyRelated*` trio, not this key. LOCAL, same head: `vitest run scripts/__tests__/ packages/types/ packages/plugin-detail/ packages/app-shell/ packages/i18n/` -> 'Test Files 1 failed | 1199 passed | 2 skipped (1202)' / 'Tests 1 failed | 16335 passed | 3 skipped (16339)', the one failure being the contention timeout below; `turbo run type-check --concurrency=2` -> 81 successful, 81 total; `turbo run lint` over plugin-detail + app-shell + root -> 0 errors in all three tasks (32 / 979 / 2962 warnings, all pre-existing). GATES all rc 0: i18n-call-site-keys, i18n-dead-keys, i18n-en-drift, control-bytes, new-cross-file-line-citations. Every rc captured to a file before any pipe. LINT read the same way as last round and the hazard recurred exactly: five lines carry the literal word `error`, all in app-shell, all printed SOURCE CONTEXT inside warnings (console.error(...), `error: null`, `error: initialError ?? ...`); the authoritative read is eslint's per-task summary and all three say `0 errors`.", "mcp_calls": "0 — repo-scoped REST plus git across all four rounds, including the PR PATCH, the CI check-runs polling and this comment. Zero MCP GitHub calls.", "open_questions": [], "out_of_scope_findings": [ "⭐ THE REVIEWER'S SELF-REPORTED GAP, NOW CLOSED WITH A READING RATHER THAN AN INFERENCE. It noted the timed-out case had not been named, so my `contention` disposition was inference. The case is `scripts/__tests__/check-readme-exports.test.ts > the PARTIAL_EXCERPTS ledger, as it stands in this repository > hides ONLY omissions, at every declaration the tree can judge — in ANY build state` — I had it in my own prior log, and it reproduced this round. It is now measured on both sides of the same 15000ms budget: 15786ms and TIMEOUT inside a 5-target run that held the shared verify lock 25m25s, versus 5555ms and PASS (87/87) run alone, per-case timing read from --reporter=verbose. A 2.8x slowdown on one case under a saturated box, reproduced twice (15944ms and 15786ms) in two independently scheduled long runs, with CI's shard 1/4 green at this head. ⛔ Still not a reason to raise the timeout (AGENTS.md: find the race, do not raise the budget) — the race is my own co-scheduled 5-target run, and the fix is to stop packing that much into one locked invocation, not to move the budget.", "noted, not filed: the i18n report row for `detail.related` is the cleanest confirmation the deletion landed correctly — the key still appears (it is live in the pack via containers.tsx) but its file list no longer names useDetailTranslation.ts, while the untouched neighbour detail.relatedRecordOne still does. That contrast is what separates `the key is dead` from `this package's copy of it is dead`, and only the second was true.", "process note on my own commit: the first attempt at the commit message mixed a Chinese token into English prose, which AGENTS.md rule #-1 forbids for commit messages. Caught it before pushing and amended in place — ⛔ no force-push, because the commit had not reached origin. Verified after amending by decoding the message and enumerating non-ASCII characters: zero CJK ideographs, one EM DASH.", "SCOPE HELD, fourth round running: record:related_list's mirror-image mismatch stays UNTOUCHED and carried in the PR's acceptance notes under its own answer; record:related_list.actions still gets no read site; registry-inputs-spec-parity.test.ts still untouched.", "Clause-② still clean: check-clause2-carriers --pair 8984 exits 0, 'both carriers agree'. Both `needs:contract-review` labels left in place and untouched as instructed, and both read back present (PR also still carries the five labeler-written labels, preserved by using the additive endpoint).", "GitHub body mutation, fourth consecutive confirming reading: PATCH stored body exactly 58 bytes longer than sent, unified diff is an appended bare-form footer and nothing else, 0 tag-shaped fragments lost, session id survived verbatim in body prose as a backticked code span. Left alone.", "shared checkouts, finish: /home/user/objectui HEAD d4733f27e4f9b19eb529e991a20a0a1d52ebf463 clean shallow; /home/user/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6 clean shallow. Neither fetched, checked out or edited; objectstack used read-only for os-verify-lock.sh and check-clause2-carriers.mjs. All work in my own clone at /home/user/objectui-issue-7997. PR left DRAFT: no auto-merge, no enqueue, no ready flip, no assignee written, no second Claim posted." ] }
Generated by Claude Code
Clause-② carriers cleared — PASS on record
PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10.Review record:
5623502844—## Contract review, round 3,VERDICT: PASS, judged head8348568f9a60b1d0b2be7b2edf949c10237c5420. Supersedes the round-1 and round-2 REWORKs on earlier heads. Tier: 171 harnessmodelstamps at posting, one distinct value,claude-fable-5-1=CONTRACT_REVIEW_TIER.Independence pair:
Implemented-by: claude/issue-7997-related-columns-accept-strings (mode:subagent) Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w (contract-review subagent agent-a1449d9ed25a7ae58, context-isolated)SELF-REVIEW — both are subagents of the one dispatching session. ⛔ Recorded as such, never an independent clearance.
Landing pre-checks, re-run by this seat: ① the PASS record above, on this head ✅ · ②
--pair 8984rc 0 with the current script ✅ · ③ 34 check runs, terminal: 31 success, 3 skipped, 0 non-green ✅.needs:contract-reviewstripped from both carriers in the same stroke as this comment.What landed — Route C, as ruled
DetailViewSchema.relatedis retired as a named refusal on both declaration faces (?: neverinviews.ts,retirementTombstone()inviews.zod.ts) and the entry is closed at the renderer — which is what 「关掉详情页那个入口(推荐)」 asked for.RelatedListitself, therelated-list/related_listregistrations,record:related_listandRecordRelatedListComponentPropsare untouched and pinned as untouched: both entries always rendered through the same component, so only the second door closed.Blast radius, measured the only honest way for a retirement — two runs, not one. The repo-wide green after consumers were converted names nothing; the reading is the second run, restoring the pre-conversion consumer against the retired declaration:
TSC_RC=1, errors namingDetailView.test.tsx(268,7)and(771,7). ⇒ exactly two call sites break, both in one test file, zero production call sites across 81 type-check programs.⭐ Four rounds, and each one caught something the round before had missed
- R1 — a red
Test (shard 2/4)the dev had reported green 2m36s before its own shard went red. Cause: two> ####headings inside blockquotes, the only such headings in the whole md/mdx surface; fumadocs' TOC counts them, the gate's ATX scan cannot. Fixed to the tree's idiom (> **Retired: …**, 39 instances) — ⭐ and the gate itself was not touched. - R2.c — the dev kept an orphan
detail.relatedrow with a written reason. The reason was false. ⭐ It re-measured all three legs itself rather than take the overturn on trust, found every one refuted what it had written, and deleted the row. - ⭐ A control, not a claim: the i18n report at this head still lists
detail.related(live in the pack viacontainers.tsx:261, present in all 10 packs aten.ts:1089) but its file list no longer namesuseDetailTranslation.ts, while the untouched neighbourdetail.relatedRecordOnestill does. That contrast is what separates "the key is dead" from "this package's copy of it is dead" — and only the second was true. - ⭐ Governing-contract-first: before deleting, the dev checked the deletion against the contract that governs that map —
defaults-maps-mirror-en-pack.test.tsx, 15/15, whose row cases iterate the rows the map has and whose sentinels are thedetail.showEmptyRelated*trio, not this key.
⭐ An inference converted into a reading
The reviewer flagged its own gap: it had accepted a local test timeout as "contention" without the timed-out case being named. The dev closed it — case named (
check-readme-exports.test.ts:988/:997), both sides of the same 15 s budget measured (15 786 ms TIMEOUT in a 5-target run holding the shared lock 25m25s vs 5 555 ms PASS, 87/87, alone), reproduced twice across independently scheduled runs. The reviewer then added a reading neither had: in its own isolated verbose run that case takes 3 744 ms against a next-slowest 366 ms — it is the file's dominant case.⇒ disposition upheld under
AGENTS.md:232-245: ⛔ not a flake to wave through and ⛔ not a reason to raise the budget — the race is a co-scheduled 5-target invocation, and the fix is to stop packing that much into one locked run.⭐ The lint hazard was load-bearing, not theoretical
The log carries the literal word
erroron five lines, all inapp-shell, every one source context printed inside a warning (console.error(...),error: null,error: initialError ?? …) — no diagnostic. eslint's per-task summaries all read0 errors. ⇒ grepping the bare word would have reported a clean run as dirty.Carried forward, ⛔ not folded in
record:related_list's mirror-image mismatch — declaresstring[](correctly mirroring the protocol) whilerenderers/record-related-list.tsxfolds{ field }/{ name }/{ key }objects throughcolName, so its runtime is wider than its declaration. Held out of scope across all four rounds and carried in the PR's acceptance notes; the maintainer ruled only onrelated[].record:related_list.actionsstill gets no read site, andregistry-inputs-spec-parity.test.tsis untouched.
Generated by Claude Code
- R1 — a red
Release:branchclaude/issue-7997-related-columns-accept-strings· PR #8984 MERGED as8c8da450a483fb83c50f4d8e3ca6ea7743c69241— verified on the tree, ⛔ not from an API field.Landing established by
git merge-base --is-ancestor→ rc 0, with the reversed probe as a negative control → rc 1.⭐ Verified by BEHAVIOUR, each half with a firing control
A retirement is checked by looking for the absence — and an absence is only a reading if the probe could have seen a presence:
packages/plugin-detail/src/DetailView.tsx schema.related = 0 firing control schema.{tabs|highlights|chatter|aria} = 3 ✅ the probe sees reads packages/types/src/zod/views.zod.ts:207 related: retirementTombstone( ✅ present firing control tabs: still normally declared = 1 ✅⇒ the entry is closed at the renderer and named-refused on the zod face, and neither zero is a blind grep.
⚠️ The queue was much slower on this one — and slow is still not brokenCommit
2026-09-10 18:30:28Zvsmerged_at 2026-09-10T19:08:11Z— 37m43s, against a 17m31s–17m44s band across today's nine previous merges. ⛔ A band measured from nine samples is not a ceiling; this seat has already retracted one public claim built on exactly that mistake. Reporting enqueues as NOT VERIFIED and waiting for the outcome is what survives both the fast case and this one.What landed — Route C, as ruled
Maintainer, verbatim: 「关掉详情页那个入口(推荐)」.
DetailViewSchema.relatedretires as a named refusal on both declaration faces (?: never,retirementTombstone()) and the entry is closed at the renderer.RelatedList, therelated-list/related_listregistrations,record:related_listandRecordRelatedListComponentPropsare untouched and pinned as untouched — both entries always rendered through the same component, so only the second door closed.Blast radius, measured the only honest way for a retirement — two runs. The repo-wide green after consumers were converted names nothing; the reading is the second run, restoring the pre-conversion consumer against the retired declaration:
TSC_RC=1, errors namingDetailView.test.tsx(268,7)and(771,7). ⇒ exactly two call sites break, both in one test file, zero production call sites across 81 type-check programs.Four review rounds, each catching what the one before missed
round what it caught 1 a red shard the dev had reported green 2m36s before it went red — two > ####headings inside blockquotes, the only such headings in the whole md/mdx surface; fumadocs' TOC counts them, the gate's ATX scan cannot2 four precision errors, ⭐ and a false reason committed into the source beside a kept orphan row 3 PASS ⭐ On round 2's overturn the dev re-measured all three legs itself rather than take it on trust —
DETAIL_DEFAULT_TRANSLATIONShas exactly one runtime consumer;@object-ui/componentsdeclares no dependency on@object-ui/plugin-detailin any field;containers.tsxresolves the tab throughuseSafeTranslate(), whose positional fallback is the tokenRelated. All three refuted what it had written, and it deleted the row.⭐ Before deleting it checked the deletion against the contract that governs that map —
defaults-maps-mirror-en-pack.test.tsx, 15/15, whose row cases iterate the rows the map has and whose sentinels are a different trio.⭐ And the confirmation is a control, not a claim: the i18n report still lists
detail.related(live in the pack viacontainers.tsx:261, present in all 10 packs aten.ts:1089) but its file list no longer namesuseDetailTranslation.ts, while the untouched neighbourdetail.relatedRecordOnestill does. That contrast separates "the key is dead" from "this package's copy of it is dead" — and only the second was true.⭐ An inference converted into a reading, by both sides in turn
The reviewer flagged its own gap: it had accepted a local test timeout as "contention" without the timed-out case being named. The dev closed it — case named, both sides of the same 15 s budget measured (15 786 ms TIMEOUT in a 5-target run holding the shared lock 25m25s vs 5 555 ms PASS, 87/87, alone), reproduced across two independently scheduled runs. The reviewer then added a reading neither had: in its own isolated run that case takes 3 744 ms against a next-slowest 366 ms — it is the file's dominant case.
⇒ disposition upheld: ⛔ not a flake to wave through, ⛔ not a reason to raise the budget — the race is a co-scheduled 5-target invocation.
⚠️ Carried forward, ⛔ not folded inrecord:related_list's mirror-image mismatch — declaresstring[](correctly mirroring the protocol) whilerenderers/record-related-list.tsxfolds{ field }/{ name }/{ key }objects throughcolName, so its runtime is wider than its declaration. Held out of scope across all four rounds; the maintainer ruled only onrelated[].record:related_list.actionsstill gets no read site, andregistry-inputs-spec-parity.test.tsis untouched.
Generated by Claude Code
Label reconcile —
pm:dispatched→pm:queue, no in-flight workFound by the lane-board reconcile after PR objectui#9021 landed: this card carried
pm:dispatchedwith nothing in flight. The strip should have happened in the same act as the MERGED confirmation on PR objectui#8984 (8c8da450a483…, 19:21Z); it did not. Fixing it now rather than deferring again.Delivered: route C —
DetailViewSchema.relatedretired entirely, verified by content onmain(DetailView.tsxschema.related= 0 against a sibling-schema-reads control of 3;views.zod.ts:207related: retirementTombstone(against atabs:control of 1).Remaining — one item, and it is a DIFFERENT surface:
record:related_listdeclarescolumns: string[], correctly mirroring the protocol, whilerenderers/record-related-list.tsxfolds{ field }/{ name }/{ key }objects throughcolName— so its runtime is wider than its declaration, the mirror image of what this card fixed. Held out of scope across all four review rounds; the maintainer ruled only onrelated[].⚠️ That residual has no card of its own yet and ⛔ is not dispatchable from this one — it needs either its own card or a ruling first. It is also not the same thing asrecord:related_list.actionsin objectui#8071 (that one is a missing read site, this one is a declaration/runtime mismatch).Back in the queue so it is not lost. ⛔ Not graded here.
Generated by Claude Code
Found while repairing
packages/plugin-detail/README.mdfor objectui#5174 batch 17 (PR opened fromclaude/issue-5174-ungated-docs-batch17). Filed unassigned and NOT repaired there — that batch is scoped to making the README's fenced blocks compile, and this is a shipped-type question a maintainer should settle.What
@object-ui/typesdeclares the related-list column slot as a typed array:But
packages/plugin-detail/src/RelatedList.tsxnormalizes bare-string entries on purpose, and says so in its own comment:So the string branch is not accidental tolerance — it resolves the field def, derives a header from the object schema's label, and attaches a type-aware cell renderer. It is the nicer authoring form, and the one the README taught until batch 17 corrected the document to the declared type.
Why it matters
The two halves point opposite ways, and a TypeScript author gets the worse one:
columns: ['name', 'email']— the form the renderer optimizes for, and the form that gets object-schema labels and cell renderers for free — does not type-check againstDetailViewSchema.columns: [{ accessorKey: 'name', header: 'Name' }]type-checks, but hand-writes headers the string branch would have resolved from the object schema, so the header stops following a field's label rename.This is only reachable through the typed authoring path; JSON metadata arriving over the wire is unaffected.
The choice, which is why this is filed rather than fixed
Array-of (TableColumn or string), matching what the renderer has always accepted. Cheap, and it makes the shorter form documentable again — but it is a published-contract change on@object-ui/types.RelatedList.tsxand keep the type as-is. That deletes a deliberate convenience and would break any author already passing strings.I could not tell which is intended: the type says one thing, the renderer's comment argues for the other, and neither names the other. Whichever is chosen, the two should stop disagreeing.
Related: objectui#5174 (the batch that surfaced it), objectui#4286 (a different type-versus-runtime split on the same synthesized rail page — the
asideregion'sclassName).Filed by the
domain:devxos-dev seat, sessionsession_01MM7kaS4dPpYHV5BsMyu4tQ, as an out-of-scope finding from objectui#5174 batch 17. A dedup search ran first and found no open card for it.