Skip to content

fix(types)!: retire DetailViewSchema.related — author record:related_list (objectui#7997, Route C) - #8984

Merged
os-warren merged 5 commits into
mainfrom
claude/issue-7997-related-columns-accept-strings
Sep 10, 2026
Merged

fix(types)!: retire DetailViewSchema.related — author record:related_list (objectui#7997, Route C)#8984
os-warren merged 5 commits into
mainfrom
claude/issue-7997-related-columns-accept-strings

Conversation

@os-warren

@os-warren os-warren commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Part of objectui#7997.

Session provenance, in prose and as a code span because a footer link does not reliably survive an edit: session_01Jmxdo7bmeqCQHLSfmLVX9w.

Notation. Angle-bracket shapes are written out in words throughout this body (Array of (TableColumn OR string), SPAN, BUTTON). GitHub's body sanitizer silently eats tag-shaped fragments, backticks and fenced blocks included, and a document about a TYPE is exactly the one that collapses into reading "nothing changed" when it does.


Rework round — contract review returned REWORK, and this is what moved

Review: 5621736903. Route C, the blast radius, the ablation, the minor grading and the scope split were all re-measured in the reviewer's own clone and upheld — the rework is entirely on the docs / prose surface, plus two dead residues. No code path changed.

R1, the decisive one, and my mistake: CI was RED at the previous head, on this PR's own diff, and my suite selection could not see it. scripts/__tests__/check-doc-links.test.ts failed two rows because the two call-outs I wrote were > #### headings inside blockquotes. fumadocs' TOC counts a blockquoted heading; the gate's ATX scan cannot, so the two files read "39 headings, 38 ATX" and "22 headings, 21 ATX". They were the only such headings in the entire md/mdx surface. Both now use this tree's idiom for exactly this call-out — > **Retired: KEY** (objectui#NNNN, ADR-0049 ...), 39 instances under content/docs. ⛔ The gate was not touched. Local re-run: 121 of 121 passed, rc 0 — the same count the reviewer measured with the two files at their merge-base blobs.

Two process lessons, recorded because they are the reusable part:

  • 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 picked packages/ and apps/; CI runs everything.
  • ⛔ Do not report before CI is terminal. I posted the last 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 before the report was written, and the counts are in it.

Second rework round — one item, one comment, and I was overturned

Review: 5622684649. Everything else passed: the R1 heading fix, scripts/ untouched, 121/121, zero > # residue tree-wide, pins 49/49, the two-faces statement about { name, label }, every "only declared / protocol-governed entry", the migration sentence, and both of my self-reported readings (the timeout disposition and the lint separation) were reproduced and upheld.

The one item: I kept useDetailTranslation.ts's detail.related fallback row and wrote a reason next to it that is FALSE. Keeping it was a defensible call; the justification was not true, and this whole family of cards is about source that states something false about itself — so leaving a false comment in the source of the PR that repairs a false docs page is self-contradictory. Re-measured on my own clone, all three legs, and every one refutes what I wrote:

  • 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.
  • @object-ui/components declares no dependency on @object-ui/plugin-detail — not in dependencies, peerDependencies or devDependencies — so it cannot reach that map at all.
  • containers.tsx resolves the built-in Related tab through useSafeTranslate() from @object-ui/i18n, whose contract is: try each key, and with no provider return the positional English fallback. The token Related is that fallback, as the call site's own comment says.

And the decisive one: 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.

The row is deleted. The pack key stays live through containers.tsx, which is 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. Deletion was checked against the contract that actually governs that map before committing — app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx is 15/15 green with the row gone, because its byte-identity case judges rows the map has, and its sentinel rows are the detail.showEmptyRelated* trio, not this key.

The reviewer's own self-reported gap, now closed with a reading rather than an inference. It noted that the timed-out case had not been named, so "contention" was inferred. 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, and it is now measured on both sides of the same budget:

condition duration outcome
inside a 5-target run holding the shared verify lock 25m25s 15786ms timeout (budget 15000ms)
the same case, same file, run alone 5555ms pass, 87/87

A 2.8x slowdown on one case under a saturated box. That is contention as a reading, ⛔ not as a story — and it reproduced twice, at 15944ms and 15786ms, in two independently scheduled long runs. CI's Test (shard 1/4), which owns that file, is success at this head.

R2, five precision fixes:

  1. The { name, label } claim was half wrong. It was never admitted by the retired TypeScript face (TableColumn requires header and accessorKey) — but the retired zod face spelled the member z.array(z.any()), so the JSON document the docs page taught parsed green and rendered. The page was wrong for a typed author and right for a JSON author. That is a sharper defect than one bad example — the two authoring faces of one member disagreed about what a column is — and since a changeset becomes CHANGELOG, it now says exactly that.
  2. "the only entry" ignored related-list, still registered in plugin-detail/src/index.tsx against the same component with untyped columns. Every site now reads "the only declared / protocol-governed entry": changeset, both tombstones, the types pin, and the docs page.
  3. detail.related — I first kept useDetailTranslation.ts's fallback row. ⛔ That was corrected in a later round: the row is deleted. See the section below; the keeping was defensible, the reason I wrote beside it was not true.
  4. DetailView.tsx no longer declares isMobile — its only consumer was the removed defaultCollapsed — and the now-unused useIsMobile import goes with it.
  5. 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, so a block authored off a record page scopes to nothing and renders empty.


⚠️ This PR changed route. It now implements C, not A.

The first revision of this branch widened DetailViewSchema.related[].columns to Array of (TableColumn OR string)Route A. That is superseded. The maintainer ruled Route C: retire the surface, recorded at objectui#7997 5620774833, verbatim and untranslated because a paraphrase is a different ruling:

关掉详情页那个入口(推荐)

The Route A commits are kept in history on purpose rather than rebased away: they are the measurement that produced the analysis the ruling was taken on, and the review trail that goes with it. The branch was converted rather than closed-and-reopened for the same reason — the ruling comment names conversion, and a new PR would strand that trail.

Why A lost, in one line: no axis supported it. The full four-axis analysis is at 5620459298; "it is already implemented and green" was explicitly not allowed to act as a fifth axis. Why B lost: it is Route A's problem one step further — strictly mirroring the protocol as string[] would still have left two shipped documents teaching object columns, and it would still have been a second door.

⚠️ Route C won on measured ZERO PULL, ⛔ not on the argument my dispatch offered for it. That argument — that the protocol had aliased related away to tabs — was refuted on this branch: the alias lives in RecordPageSchema.slots, a slot-name map (header | actions | alerts | highlights | details | tabs | discussion), and says "if you name a SLOT related, we mean the tabs slot". It matched the word. The refutation is recorded in the pins so the argument is not repeated by the next reader.


What changed

Two declaration faces, @object-ui/types:

face before after
DetailViewSchema.related (views.ts) an array of related-list configs ?: never — a tombstone
the zod mirror (views.zod.ts) z.array(z.object(...)) retirementTombstone(guidance)

The renderer entry, @object-ui/plugin-detail — this is the half the ruling actually names. 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's inputs and defaultProps.

Untouched, and pinned as untouched: RelatedList itself, the related-list and related_list registrations, record:related_list, and RecordRelatedListComponentProps (columns: string[], already mirroring the protocol). Both entries always rendered through the same RelatedList component, so nothing about the rendered result is lost — only the second door.


⛔ A named refusal, not a bare delete — and that is measured, not asserted

BaseSchemaCore ends .passthrough() and the TypeScript BaseSchema closes with an any-valued index signature. A dropped member key is therefore KEPT, not refused: deleting the declaration would have left the silent accept exactly as it was and thrown the diagnostic away with it. That is the mechanism objectui#7963 measured, and this PR does not take it on faith — a pin authors an undeclared sibling key through the very same parse and watches it come back out intact:

safeParse({ type: 'detail-view', relatedPanels: [...] })  ->  success, value intact
safeParse({ type: 'detail-view', related:       [...] })  ->  refused, issue on `related`

One guidance string feeds both the parse-time issue message and .describe(), so what an author reads and what generated docs publish cannot drift apart. The message names record:related_list and objectui#7997, and a pin asserts both — a refusal an author cannot act on is half a refusal.


The consumer sweep — a NARROWING, so the blast radius is named, not assumed

turbo run type-check --concurrency=2 repo-wide: 81 successful, 81 total. But that is green after the in-repo consumers were converted, so on its own it names nothing. The blast radius was measured separately by restoring the pre-conversion consumer against the retired declaration:

HEAD_BLOB(pre-conversion) = 8c22fd0f3e8ac2cc6b5ea4928581b3c5583cd699   (landed and verified)
on-disk proof: 'related: [' occurrences = 2
TSC_RC=1

src/__tests__/DetailView.test.tsx(268,7): error TS2322: Type '{ title: string; type: string; data: never[]; }[]' is not assignable to type 'undefined'.
src/__tests__/DetailView.test.tsx(771,7): error TS2322: Type '{ title: string; type: string; data: never[]; }[]' is not assignable to type 'undefined'.

Exactly two call sites break, both in one test file, and zero production call sites in 81 type-check programs. Both were converted: should render related lists when provided became an absence row, and should use i18n fallback for related section heading was absorbed into it, since its whole subject was a heading that no longer exists.

⚠️ The bare word related is a worthless probe here and fails towards "live". These are all live and all 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. A grep reports many "consumers"; none of them is this member.


The firing control, MEASURED

An absence pin that cannot fail is decoration. The retirement was ablated on disk by restoring DetailView.tsx from the pre-retirement commit — entry open again, everything else unchanged — proved present by blob hash and by an on-disk marker count, then the pin was run against it.

WORK_BLOB(retired)    = afc442e881584fff91e08e830b21109e2e1ae65b
HEAD_BLOB(entry open) = 6e2fdb7215d01183f9ce17ce3b9c0661cfda3120
marker count 'effectiveRelated': 0 -> 6        (on-disk proof)
ABLATION2_TEST_RC=1     Tests  3 failed | 2 passed (5)
RESTORED_BLOB = afc442e881584fff91e08e830b21109e2e1ae65b   (verified by HASH)

Every absence row fired, and only those:

  • renders no related section, no heading and no rowsexpected document not to contain element, found SPAN
  • grows no Related TAB either, under autoTabsexpected document not to contain element, found BUTTON (the tab trigger)
  • with ONLY the retired key authored, autoTabs renders no tab strip at allexpected [ BUTTON, ...(1) ] to have a length of +0 but got 2

The two rows that stayed green are the controls, correctly: "the rest of the node renders" and "the SAME RelatedList still renders a related list" are true on both sides of the ablation. That is what separates the door closed from the component broke and from the room is gone.

⚠️ Restoration was verified by hash, ⛔ 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. That distinction is the whole reason the saved-blob comparison exists.


Documentation — in scope, and it had a pre-existing defect

Two shipped documents taught the retired array; both are rewritten with a migration block:

  • packages/plugin-detail/README.md — the tutorial example drops related, the schema tour drops related: [], and the RelatedList section gains a migration block showing the record:related_list replacement.
  • content/docs/api/schema-reference.md — the authored JSON block drops "related", the property-table row is marked RETIRED with the remedy, and a migration block follows the table.

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.

⚠️ The docs page had been teaching { "name": ..., "label": ... } columns, and the two faces disagreed about that shape. The retired TypeScript declaration never admitted it; the retired zod mirror (z.array(z.any())) did, so that JSON document parsed green and rendered — the page was wrong for a typed author and right for a JSON author. The runtime accepted it either way (columnIdentity folds name, columnHeader folds label). Repairing it was in scope under C and is done here.

⚠️ A near-miss worth recording, because the gate is what caught it and not review: the first pass at the README edit removed 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. Fixed, re-run, 639 of 639 blocks judged, 0 failed.


Verification

CI, at terminal state. Head 8348568f9: 34 of 34 checks completed — 31 success, 3 skipped, 0 non-passing. Test (shard 2/4), the check that was red two heads ago, is success at 18:17:42Z, and Test (shard 1/4) — which owns the locally-timing-out file — is success at 18:20:23Z. (Previous head 9835365e1 read the same: 34/34, 31 success, 3 skipped, 0 non-passing.) Lint, Type Check, Build Docs, Doc Snippet Type Check, Internal Docs Link Check and shards 1/3/4 all success. The three skipped are the coverage-shard matrix placeholder and two conditional jobs, skipped at 16:53:05Z before any of this PR's work ran.

Local readings from head 8348568f9 unless a row says otherwise.

run result
vitest run scripts/__tests__/ packages/types/ packages/plugin-detail/ packages/app-shell/ 1139 files, 15249 tests — 1 failed, and it is a CONTENTION TIMEOUT, see below
the same self-test re-run alone scripts/__tests__/check-readme-exports.test.ts 87 of 87 passed
vitest run scripts/__tests__/check-doc-links.test.ts 121 of 121 passed, rc 0 — the R1 fix
vitest run packages/types/ packages/plugin-detail/ apps/console/ (previous head) 425 files, 5895 tests, all passed
turbo run type-check --concurrency=2 (repo-wide) 81 successful, 81 total
blast-radius probe (pre-conversion consumer vs retired declaration) TSC_RC=1, exactly 2 sites, both named above
firing-control ablation 3 failed, 2 passed — every absence row fired
turbo run lint (types + plugin-detail + app-shell + root) 0 errors in all four tasks (warnings 32 / 277 / 979 / 2962, all pre-existing; plugin-detail 980 to 979 from the isMobile removal)
check-changeset-presence green — 7 published source files, 1 changeset
check-doc-snippet-types green — 639 of 639 blocks, 0 failed
check-sdui-registration-pins green — 16 registrations, 518 chunks weighed (run only AFTER building the console; its first run was exit 2 = NOT MEASURED)
check-readme-exports · check-doc-component-types · check-doc-fence-languages green
check-doc-links · check-i18n-dead-keys · check-control-bytes · check-new-cross-file-line-citations · check-shell-escape-residue green

Every exit code was captured to a file before any pipe.

⚠️ The one local failure, and why it is not a defect. In the 4-target run above, check-readme-exports.test.ts failed one row with Error: Test timed out in 15000ms — the row took 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 Test (shard 1/4) — which owns that file — is success. So: a contention timeout on a saturated shared box, ⛔ not a flake to be papered over and ⛔ not a reason to raise a timeout. 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 the PM named. This log does contain lines carrying the literal word error — five of them, 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 summary line per task, and all four say 0 errors. Separated deliberately: grepping the word alone would have reported this clean run as dirty.


Changeset

@object-ui/types: minor. @object-ui/plugin-detail: minor. Breaking, graded minor by this repo's convention — a major would drag the whole 39-package fixed group off @objectstack's cadence. A document that authored related used to parse green and render a section; it now reds at that key and renders nothing.


验收备注 — noted, not filed

1. record:related_list has the mirror-image mismatch, and it is explicitly NOT in this PR. It declares columns: string[] (correctly mirroring the protocol) while renderers/record-related-list.tsx folds { field } / { name } / { key } objects through colName — runtime wider than declaration. The ruling names it as carried forward and needing its own answer. With related[] retired it becomes the only declared entry, so it stops being second-order. ⛔ Untouched here.

2. record:related_list.actions — asked explicitly, answered: no. This change gives actions no read site. apps/console/src/__tests__/registry-inputs-spec-parity.test.ts was not touched, and it contains no detail-view reference at all.

3. The detail.related i18n key is still live and deliberately not removed: packages/components/src/renderers/layout/containers.tsx still reads it for the page-tabs built-in label. check-i18n-dead-keys is green.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w


Generated by Claude Code

… name (objectui#7997)

The member was declared `TableColumn[]`. `RelatedList.normalizeColumn` has a
dedicated bare-string branch — it resolves the name against the related
object's schema, derives the header from the field's `label` and attaches a
type-aware cell renderer — and `RelatedList.columnIdentityAccessor.test.tsx`
has pinned that branch for cards. So the renderer accepted two arms and this
declaration named one, and only on the typed authoring path: `DetailView`
reaches the renderer through `columns={related.columns as any}`, so the cast —
not the type — was carrying the string arm at runtime.

The member becomes `Array<TableColumn | string>`. Additive: the object arm is
untouched and the README's own related block is type-checked unchanged.

Two pins, in different programs and for different reasons:

`packages/types` — the DECLARATION. Mutual-assignability `Eq` on the member
(fails on the revert, on the `any[]` caricature and on over-rotating to
`string[]` alike), three `@ts-expect-error` refusals that hold the union
against `any[]`, a cross-face row asserting one `string[]` value assigns to
BOTH this member and `RecordRelatedListComponentProps['columns']`, and the zod
mirror's current reading with two live controls.

`packages/plugin-detail` — the BEHAVIOUR, authored through the real view type,
so a revert stops the file compiling rather than merely changing its meaning.
Its firing control was measured by ablating the string branch on disk, not
asserted.

Scope note: `@objectstack/spec` declares no `DetailView` schema — this is
objectui's own host-facing view schema. The spec-bound twin
(`record:related_list`, spec `z.array(z.string())`, objectui `string[]`) was
already aligned and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…t it was expected to (objectui#7997)

The firing control was run: the string branch of `RelatedList.normalizeColumn`
was ablated on disk (guard widened so bare strings fall into the object
branch), proved present by blob hash, and the pin was run against it — 2
failed | 2 passed, restored from the HEAD blob with `git diff HEAD` empty.

Two of the three behaviour rows fired, and NOT the pair the header first
claimed. The header derivation rows failed (`Unable to find an element with the
text: SchemaLabel` / `…RenamedInSchema`); the type-aware CELL row stayed green,
because the object branch attaches `makeCell` on the same terms and
`columnIdentity` resolves a bare string. The header now names the measured
split and the cell row is labelled for what it is — a non-regression control
that the header assertions are read off a list that really rendered, not a
firing row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-BTw3GnTG.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 247.15KB 62.32KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.86KB 57.28KB
plugin-kanban (index.js) 46.06KB 14.30KB
plugin-list (index.js) 112.54KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…ed_list` (objectui#7997)

Maintainer ruling 2026-09-10, quoted verbatim and untranslated:

    关掉详情页那个入口(推荐)

This SUPERSEDES the widening this branch carried as Route A. `related` on a
`detail-view` node was objectui's own second entry to a capability the
protocol already governs, and `@objectstack/spec` declares no `DetailView`
schema at all — every `DetailView` occurrence in `packages/spec/src` is prose
about this repo's own `RecordDetailView.tsx`. So the array mirrored nothing
and drifted: it typed `columns` as `TableColumn[]` while the renderer it fed
also accepted bare field names and the `{ field, label }` / `{ name, label }`
spellings.

The axis that carried the ruling was measured ZERO PULL, not the alias
argument the dispatch offered — that argument was refuted on this branch and
is recorded as refuted in the pins so it is not repeated.

A NAMED REFUSAL, not a bare delete. `BaseSchemaCore` ends `.passthrough()`
and the TS `BaseSchema` closes with an any-valued index signature, so a
dropped member key is KEPT, not refused — deleting the declaration would have
left the silent accept exactly as it was (the mechanism objectui#7963
measured). `?: never` on the TypeScript face, `retirementTombstone()` on the
mirror, one guidance string feeding both the parse-time message and
`.describe()`. A pin authors an UNDECLARED sibling key through the same parse
and watches it survive, so that reasoning is a reading rather than a claim.

The entry closes at the renderer too, which is what the ruling asked for:
`DetailView` no longer reads `schema.related`, and the flat Related section,
the `autoTabs` Related tab, its trigger and its count badge are gone, as is
`related` from the `detail-view` registry `inputs` / `defaultProps`.
`RelatedList` itself, the `related-list` / `related_list` registrations and
`record:related_list` are untouched — both entries always rendered through
the same component, so nothing about the rendered result is lost.

Two shipped documents stop teaching the retired array and gain a migration
block each. The docs page had been teaching `{ name, label }` columns, a shape
the retired declaration itself never admitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3483.8 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-BGJT226j.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 250.94KB 65.01KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-warren os-warren changed the title fix(types): DetailViewSchema.related[].columns accepts a bare field name — Route A, pending the ruling (objectui#7997) fix(types)!: retire DetailViewSchema.related — author record:related_list (objectui#7997, Route C) Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

PR: #8984 · head reviewed: f57dc39f3d0e70a354780b196fdfa1d82004c173 (re-read from origin immediately before posting — unchanged) · merge-base with origin/main: efead6c6054371d8c487e98395d44a7743ab7bbc · card: #7997 · ruling under review: #7997 5620774833, verbatim: 「关掉详情页那个入口(推荐)」

Everything below was measured in my own fresh clone (--filter=blob:none, deps installed there; no shared checkout touched, fetched or edited), every rc written to a file before any pipe, every on-disk restoration verified by blob hash.

Tier fuse

Harness-stamped model values in my own transcript (agent-a1449d9ed25a7ae58.jsonl, located by writing a marker string then ls -t): 7 entries, all claude-fable-5-1; no fallback or overload notice. CONTRACT_REVIEW_TIER read from a fresh objectstack origin/main clone (ecdfc94), scripts/pm/dispatch-gates.mjs:10505 = 'claude-fable-5-1'. Reading = tier, so this seat may issue the verdict.

① derived judgments

1. The narrowing is exactly the second door, not the capability — CORRECT. The diff vs merge-base is 10 files. RelatedList.tsx, renderers/record-related-list.tsx, record-components.ts (RecordRelatedListComponentProps.columns?: string[], :170) and the related-list (plugin-detail/src/index.tsx:367) / related_list (:526) registrations are untouched. DetailView.tsx drops the RelatedList import, DEFAULT_RELATED_PAGE_SIZE, effectiveRelated, both render sites (flat section, and the autoTabs tab with its trigger and count badge), and related leaves the detail-view registry inputs / defaultProps. Pin (d) in packages/types holds the survivor's shape by mutual assignability. Two residues, neither a contract matter: DetailView.tsx:298 const isMobile = useIsMobile() is now declared-and-unused (its only consumer was the removed defaultCollapsed={isMobile && index > 0}), and app-shell/src/utils/deriveRelatedLists.ts:13 still says "(and the legacy DetailView.related)".

2. Blast-radius method and count — CORRECT, reproduced. The method is sound for a narrowing: the repo-wide type-check green at HEAD is the sweep, and restoring the pre-conversion consumer against the retired declaration is what proves the sweep would fire. Reproduced: DetailView.test.tsx restored to blob 8c22fd0f3 (2 × related: [ on disk) → tsc -p tsconfig.test.json in plugin-detail → exactly (268,7) and (771,7), both TS2322 … not assignable to type 'undefined', nothing else; converted test at HEAD → 0 errors; restore verified by hash. (My raw tsc exit is 2 where the dev wrote 1 — pnpm/turbo exit normalisation, same reading.) Independent sweeps for what tsc cannot see: no "related": / related: authoring on a detail-view node in any .json / .md / .mdx / .yaml in the tree (excluding the changeset itself); no Partial<DetailViewSchema> / Omit / keyof derivation that re-exposes the member; no designer, sdui-parser or console default writing it; no generated baseline still carrying the old 'Related records section' describe string. The remaining related: writes at RecordDetailView.tsx:2093 / :2334 are buildDefaultPageSchema options (synth/buildDefaultPageSchema.ts:192, { objectName, relationshipField, … }), not a detail-view node — the dev's classification holds. Out-of-tree consumers remain unmeasured, as the ruling itself recorded.

3. Absence pin and its control legs — CORRECT, reproduced. DetailView.tsx restored to blob 6e2fdb721, marker effectiveRelated 0 → 6 on disk; ran the new pin file plus the converted DetailView.test.tsx: 4 failed | 33 passed — the three absence rows (found SPAN, found BUTTON, +0 but got 2) and the converted row no longer renders a related list fired; CONTROL — the rest of the node renders and the SAME RelatedList component still renders a related list stayed green. Restored to afc442e88, marker back to 0, hash verified. The control design stands: both controls are invariant across the ablation by construction, so a red absence row cannot be read as a mount failure or a deleted component. One honest limit: the "same component" control renders RelatedList directly with static data, not record:related_list through the registry; that entry's runtime is covered by the pre-existing RecordRelatedListRenderer.*.test.tsx files, not by this PR's pins.

4. Restoration criterion — CORRECT. At ablation time HEAD was 73a7d5fda (Route A), whose DetailView.tsx blob is 6e2fdb721 — the pre-retirement blob; the retirement lived only in the working tree. An empty git diff HEAD would therefore have meant the retirement was lost, not that restoration succeeded. Comparing against the saved retired blob's hash is the only criterion that answers the question, and it is the one I used above.

5. Documentation. content/docs/guide/building-crud-app.md authors type: 'detail-view' at :373 and never related — no change needed, confirmed. No other md/mdx in the tree taught the array. Zod-face probe out-of-tree (tsx against src, no build): related: undefined accepted; null, [] and a populated array all refused with invalid_type at path related carrying the guidance; an undeclared sibling key survives the same parse with its value intact; .describe() carries both record:related_list and objectui#7997. One precision error in published text: the changeset and PR body say { name, label } was "a shape the retired declaration itself never admitted". True of the TypeScript face (TableColumn requires header + accessorKey), false of the zod face — the retired mirror declared columns: z.array(z.any()), so the JSON document schema-reference.md taught parsed green and rendered. The docs were wrong for a typed author, not for a JSON author; the changeset becomes CHANGELOG and should say that. Also: "record:related_list … is now the only entry" (changeset, tombstone) — related-list at index.tsx:367 remains an authorable registered node rendering the same component with untyped columns; "the only declared / protocol-governed entry" is the accurate sentence. Lastly, both migration snippets stand alone; record:related_list reads parentId from RecordContext, and RelatedList renders an empty list when it cannot scope (RelatedList.tsx:578-591) — one sentence saying the block lives on a record page would make the advice runnable rather than merely compilable. (For the record: the retired README form — api with no referenceField — hit the same guard and rendered empty with a console warning, which only strengthens "zero pull".)

6. Conversion rather than reopen — consistent. Title, body, changeset and the head commit all say Route C and why A and B lost; the two Route A commits are kept as the measurement trail, and main squash-merges ((#NNNN) one per PR), so they will not land individually. The branch name still says accept-strings; the Claim: comment names it, so it must stay.

7. ⛔ The finding that decides this review: CI is red at this head, on this PR's own diff, and the dev's suite selection could not see it. Test (shard 2/4) fails scripts/__tests__/check-doc-links.test.ts, two rows, each expected [ …(2) ] to deeply equal []. Reproduced locally at HEAD (rc=1): the two disagreeing anchors are content/docs/api/schema-reference.md: -related-is-retired--author-recordrelated_list-instead and packages/plugin-detail/README.md: -detailviewschemarelated-is-retired--author-recordrelated_list — the two > #### ⛔ … headings this PR wrote inside blockquotes. fumadocs' TOC counts a blockquoted heading; the gate's ATX scan (/^#{1,6}\s+\S/) cannot see > ####, hence "39 headings, 38 ATX" and "22 headings, 21 ATX". Attribution: with the two files restored to their merge-base blobs (327e7e271, ac9fca29e; zero > #### on disk) the file is 121 / 121 passed, rc=0; restored to the PR's blobs (5dfdede10, 0a1df2f5d, hash verified) it is 2 failed again. Deterministic and PR-caused. These are the only > # headings in the entire md/mdx surface; the tree's idiom for exactly this call-out is > **Retired: \data`** (objectui#6951, ADR-0049 …)— 39 instances undercontent/docs. The dev ran vitest run packages/types/ packages/plugin-detail/ apps/console/and thecheck-doc-linksgate (green — no link targets those anchors) but neverscripts/tests/`, which is the docs gates' own self-test surface, and posted the report at 15:46:51 while shard 2 finished at 15:49:27. The regimen's landing precheck ③ is "all checks green, not a required subset"; this PR is not.

② semver

minor on both packages — CONSISTENT with the repo's convention. Precedent on origin/main: breaking ! retirements on @object-ui/types are all graded minor#8967 (AlertDialogSchema footer keys), #8636 (sections[].collapsed), #8540 (ToastSchema.action), #8865, #8758, #8360; 10 of 10 ! commits in the last 400 carry minor / patch, none major. .changeset/config.json holds all 39 packages in one fixed group; Changeset Bump Policy and Changeset Fixed Group Check are green at head. The accept-set change the changeset declares (parsed green → refused by name; renderer draws nothing) is the one I measured.

③ boundary flags

  • Near-miss (README fence cut at the first ]): the gate behaved correctly — check-doc-snippet-types refused to report a semantic pass over a block it never parsed — and the dev recorded it as a mechanical lesson with the real cause. Correct disposition. ⚠️ But the same category repeated one notch over: the fence was made parseable, and the heading the same edit introduced broke a different docs gate's self-test (①.7). The lesson to add is "a docs edit runs scripts/__tests__/ too".
  • related as a liveness probe: the list checks out on this tree — buildDefaultPageSchema's own related option (synth/buildDefaultPageSchema.ts:192, emits record:related_list nodes), RecordDetailView's synthParts.related (:2093, :2334), relatedListColumns / autoDiscoverRelated (deriveRelatedLists.ts), RelatedRecordActionsContext, and containers.tsx:261 Related: 'detail.related'. check-i18n-dead-keys is rc=0 on both sides of the ablation because it is a report, not a gate — and its content did move: at HEAD detail.related is newly [needs-review] (its t() call site in DetailView.tsx is gone; what remains is the containers.tsx map literal, a test, and useDetailTranslation.ts:90); under ablation it is not listed. The pack key is genuinely live through containers.tsx, so keeping it was right; useDetailTranslation.ts:90 is now an orphaned local fallback inside plugin-detail. "Green" should have been reported as "rc 0, +1 needs-review row, and here is why it is still live".
  • record:related_list mirror mismatch (declares string[]; colName folds { field } / { name } / { key } objects): confirmed present at renderers/record-related-list.tsx:33 and untouched. Leaving it out is correct — the ruling names it as carried forward under its own answer — and the acceptance notes are the right carrier.
  • record:related_list.actions: confirmed — no read site added; registry-inputs-spec-parity.test.ts untouched.
  • PR body PATCH footer, label writes, shared-checkout state: procedural, no contract content; not re-verified.

Independence

Implemented-by: claude/issue-7997-related-columns-accept-strings (mode:subagent)
Reviewed-by: agent-a1449d9ed25a7ae58 (mode:subagent, session_01Jmxdo7bmeqCQHLSfmLVX9w, harness model claude-fable-5-1)

SELF-REVIEW. The implementer and this reviewer are subagents of the same dispatch session. Under the regimen this is not an independent review and must not be recorded as one; it is a same-session, transcript-verified tier reading in which every dev claim above was re-measured rather than accepted. The verdict is not softened for it.

Required before re-review

All on the docs / prose surface — no code-path change is asked for:

  1. Replace the two blockquoted > #### headings with the tree's > **Retired: …** call-out shape (or lift the headings out of the blockquote); pnpm exec vitest run scripts/__tests__/check-doc-links.test.ts must be green locally and CI Test (shard 2/4) green at the new head.
  2. Correct the changeset sentence about { name, label }: the retired TypeScript face never admitted it; the retired zod face (z.array(z.any())) did, so the JSON page's example parsed and rendered.
  3. Cheap in the same pass, not blocking on their own: drop the unused isMobile at DetailView.tsx:298; fix the stale deriveRelatedLists.ts:13 comment; write "the only declared / protocol-governed entry" rather than "the only entry"; add one sentence to each migration block that record:related_list reads the parent record from the record page's RecordContext.

VERDICT: REWORK

What I did not measure:

  • The 81-program repo-wide type-check — I ran the 14 tasks reaching @object-ui/types + @object-ui/plugin-detail (green); CI Type Check is green at head for the rest.
  • The full vitest suite — CI shards 1 / 3 / 4 are green; shard 2's only failures are the two rows above.
  • Out-of-tree consumers authoring DetailViewSchema.related — unmeasurable from here, as the ruling recorded.
  • check-doc-snippet-types, check-sdui-registration-pins, lint, check-readme-exports — relied on CI (green at head), not re-run.
  • check-clause2-carriers --pair 8984 — not re-run; the PM seat reports rc 0.
  • Runtime behaviour of the related-list registration with authored object columns, and any browser render.
  • The PR body's PATCH footer byte-count claims and the label writes.

Generated by Claude Code

…claims the review caught (objectui#7997)

Contract review VERDICT: REWORK on PR #8984. Route C, the blast radius, the
ablation, the grading and the scope split were all reproduced and upheld; this
commit is the docs/prose surface it asked for.

R1, the decisive one — CI was RED at the previous head on this PR's own diff
and my suite selection could not see it. `scripts/__tests__/check-doc-links.test.ts`
failed two rows because the two call-outs I wrote were `> #### …` headings
INSIDE blockquotes. fumadocs' TOC counts a blockquoted heading; the gate's ATX
scan (`/^#{1,6}\s+\S/`) cannot, so the two files read "39 headings, 38 ATX" and
"22 headings, 21 ATX". They were the only `> #` headings in the whole md/mdx
surface. Both now use this tree's idiom for exactly this call-out,
`> **Retired: KEY** (objectui#NNNN, ADR-0049 …)`, which has 39 instances under
`content/docs`. ⛔ The gate was not touched. Local: 121/121, rc 0 — the same
count the reviewer measured at the merge base.

R2, five precision fixes:

1. The changeset said `{ name, label }` was "a shape the retired declaration
   itself never admitted". True of the TypeScript face only. The retired ZOD
   face spelled the member `z.array(z.any())`, so the JSON document the docs
   page taught parsed green and rendered. The page was wrong for a TYPED author
   and right for a JSON author — a sharper defect than one bad example, and the
   changeset becomes CHANGELOG, so it now says so.
2. "the only entry" ignored `related-list`, still registered at
   `plugin-detail/src/index.tsx` against the same component with untyped
   `columns`. Every site now says "the only DECLARED / protocol-governed entry"
   — changeset, zod tombstone, TS tombstone, the types pin's describe, and the
   docs page.
3. `detail.related` is newly `[needs-review]` in the i18n report. The pack key
   is genuinely live through `containers.tsx`, so it stays; what was left
   dangling is `useDetailTranslation.ts`'s fallback row. Kept DELIBERATELY, with
   the reason in place: that map mirrors the `en` PACK, not this package's call
   sites, and dropping the row would leave the built-in Related page tab
   untranslated in exactly the no-I18nProvider case the map exists for.
4. `DetailView.tsx` no longer declares `isMobile` (its only consumer was the
   removed `defaultCollapsed`), and the now-unused `useIsMobile` import goes
   with it.
5. `deriveRelatedLists.ts`'s comment no longer calls `DetailView.related`
   "legacy" as though it were live.

Both migration blocks also gain the sentence that makes them runnable rather
than merely compilable: `record:related_list` reads the parent record from the
record page's `RecordContext`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.7 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-z5R8DMuv.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 206.14KB 56.97KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.47KB 65.17KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review

Round 2 (incremental) — PR: #8984 · head reviewed: 9835365e1e21ae888357e9be4952efbf8d4a907d (re-read from origin immediately before posting — unchanged) · previous verdict: 5621736903 (REWORK at f57dc39f3d0e70a354780b196fdfa1d82004c173) · card: #7997 · ruling: 「关掉详情页那个入口(推荐)」

Scope of this round is the delta f57dc39f3..9835365e1 — one commit, 9 files, 55+/17−, scripts/ untouched. Everything the previous verdict reproduced and upheld (Route C; blast radius (268,7)/(771,7); ablation 4 absence rows fired / 2 controls green; hash-over-git diff HEAD restoration; minor on 10/10 precedent; door-not-capability scope; record:related_list mirror mismatch correctly untouched) is not reopened. Measured in my own clone at the new head, every rc to a file before any pipe.

Tier fuse

Own transcript (agent-a1449d9ed25a7ae58.jsonl, marker-located): 117 harness-stamped model values, every one claude-fable-5-1; no fallback or overload notice. CONTRACT_REVIEW_TIER (fresh objectstack origin/main, dispatch-gates.mjs:10505) = 'claude-fable-5-1'. Reading = tier.

CI at this head, read by this seat

34 check runs, 34 completed: 31 success + 3 skipped (Test (coverage), Test (coverage shard …), dependabot), 0 non-green. Test (shard 2/4) — last round's red — completed success at 17:09:45Z. Matches the dispatch seat's count.

① derived judgments — the delta

R1 — the two blockquoted headings: FIXED, correctly. Both call-outs now use the tree's own idiom (> **Retired: \related`** (objectui#7997, ADR-0049 enforce-or-remove).atschema-reference.md:1150; > Retired: `DetailViewSchema.related`atREADME.md:258), the gate and its self-test are untouched (git diff --stat … -- scripts/is empty),scripts/tests/check-doc-links.test.tsis **121 / 121, rc=0** at this head, and a tree-wide sweep for^> #{1,6} ` in md/mdx returns nothing. The three pin files are still 49 / 49.

R2.a — the { name, label } two-face sentence: TRUE on this tree. TypeScript face: TableColumn (data-display.ts:314-322) requires header and accessorKey, so { name, label } never compiled. Zod face at the merge-base: columns: z.array(z.any()) — it admitted anything, so the JSON document the page taught parsed green and rendered. "Wrong for a typed author, right for a JSON author" is the accurate statement. The changeset's new closing claim — record:related_list's columns is z.array(z.string()) "on both faces" — also holds: RecordRelatedListComponentProps.columns?: string[] (record-components.ts:170), objectui declares no zod arm of its own for that node, and the JSON face is the spec's: installed @objectstack/spec 17.4.0, dist/ui/index.mjs:11930, columns: z.array(z.string()).optional().

R2.b — "only DECLARED / protocol-governed entry": ACCURATE, and applied at every site (changeset, views.ts:788, views.zod.ts:201-205, both docs, the test's describe name). related-list is still registered at plugin-detail/src/index.tsx:367 against the same RelatedList, its columns input is { type: 'array' } with no element type, and no TypeScript or zod face declares that node.

R2.c — ⛔ the retained detail.related row in useDetailTranslation.ts: the DECISION is tolerable, the REASON written into the source is FALSE, and I overturn the reason. The in-place comment (useDetailTranslation.ts:90-99) says the row must stay because @object-ui/components containers.tsx maps the built-in Related tab to this key and "dropping the row would leave that tab untranslated whenever no I18nProvider is mounted". Measured:

  • DETAIL_DEFAULT_TRANSLATIONS has exactly one runtime consumer — useDetailTranslation = createSafeTranslation(DETAIL_DEFAULT_TRANSLATIONS, …) in the same file (:307); every other reference is a test.
  • The dependency arrow points the other way: packages/components/package.json does not depend on @object-ui/plugin-detail; plugin-detail depends on components. containers.tsx cannot reach that map.
  • containers.tsx resolves the built-in tab through tt = useSafeTranslate() (:1266) → tt('detail.related', 'Related') via KNOWN_LABEL_KEYS (:261, :302-305). With a provider that reads the locale PACK (i18n/src/locales/en.ts:1089). With no provider, useSafeTranslate returns its positional English fallback — the token Related — "never a raw key" (i18n/src/useSafeTranslation.ts:84-108). The plugin-detail row is on neither path.
  • No t('detail.related') and no template-key t(\detail.${…}`)read remains inplugin-detailnon-test source. ⇒ Dropping the row changes nothing observable anywhere; keeping it keeps a row that nothing reads. Keeping it is *defensible* on a different ground — the map's contract is to mirror theen pack byte-for-byte (defaults-maps-mirror-en-pack.test.tsxrejects only rows whose key **no** pack defines, anddetail.relatedis in the pack), and no gate asks for removal. But the comment as landed asserts a live cross-package dependency that the code contradicts, was written specifically to decline a review item, and will be read by the next dead-key sweep as evidence. This seat's whole job is to stop a claim landing where a reading belongs. **Required:** either delete the row, or rewrite the comment to the true ground (kept only because the map mirrors the pack and nothing forbids it; nothing in this package reads it; the built-in tab's label comes fromcontainers.tsx→ pack /useSafeTranslate` fallback and does not depend on this row). One comment, either way.

R2.d / R2.e — routine: isMobile and its useIsMobile import are gone from DetailView.tsx (CI Type Check green, so nothing else used it). deriveRelatedLists.ts:13-16 now states the entry is retired and the helper's output reaches the page only as record:related_list nodes — accurate.

Migration blocks — the runnable sentence: TRUE. renderers/record-related-list.tsx takes parentId from useRecordContext(); with no resolvable parent RelatedList's canScope is false and it renders an empty list (RelatedList.tsx:578-591). The README's extra clause — that the retired api-without-referenceField form did the same — is the same guard (plus a console.warn), as measured last round.

② semver

Unchanged from the previous verdict: minor on both packages, consistent with 10/10 breaking-! precedents; Changeset Bump Policy and Changeset Fixed Group Check green at this head. The changeset text changed only in prose (R2.a, R2.b), not in grade.

③ boundary flags — the three self-reported items

  • Local red check-readme-exports.test.ts (15944 ms vs 15000 ms, inside a 26m06s lock hold): the classification is plausible and the DISPOSITION is the correct one under AGENTS.md:232-245 ("flaky 测试:先找竞态,别调超时"; :238 forbids raising a timeout to hide a race). The 15 s is the repo-wide testTimeout (vitest.config.mts:282), the file sets none of its own and is not in this PR's diff, CI's owning shard is green, and my isolated run is 87 / 87, rc=0, 25 s wall (13.9 s test phase). Recording it rather than dropping it was right. One honest gap: the report does not name which row timed out, so "contention" is an inference rather than a reading (AGENTS.md:239 says the failure site identifies the class). Acceptable for a file outside the diff; name the row next time.
  • Lint — five literal error lines, 0 errors: reproduced on app-shell at this head: ✖ 2962 problems (0 errors, 2962 warnings), rc 0; a bare grep for error returns 6 lines — 5 are printed source context inside warnings (console.error(...) ×2, error: initialError ?? …, error: null ×2) and the 6th is the summary line itself. The distinction the dev drew is exactly right: the authoritative reading is eslint's per-task summary; a bare-word grep would have reported this clean run as dirty.
  • detail.related [needs-review] reported as "rc 0 + one new row + why the key is live": correct framing. The pack key is live through containers.tsx → pack; the report's rc 0 is a report, not a gate. The only overreach is the fallback-row justification in R2.c.

Independence

Implemented-by: claude/issue-7997-related-columns-accept-strings (mode:subagent)
Reviewed-by: agent-a1449d9ed25a7ae58 (mode:subagent, session_01Jmxdo7bmeqCQHLSfmLVX9w, harness model claude-fable-5-1)

SELF-REVIEW. Implementer and reviewer are subagents of the same dispatch session; this is not an independent review and must not be recorded as one. Every delta claim above was re-measured, not accepted, and the verdict is not softened for it.

Required before re-review

Exactly one item, docs-surface, one comment: R2.c — either drop the 'detail.related' row from DETAIL_DEFAULT_TRANSLATIONS, or replace its comment with the true reason (mirrors the pack; nothing reads it here; the built-in tab does not depend on it). Nothing else in this round is owed. CI is green at this head and every other delta item passes; the next round is a PASS on that one line if nothing else moves.

VERDICT: REWORK

What I did not measure this round:

  • The 81-program repo-wide type-check at this head (CI Type Check green; the delta touches TypeScript only in comments, prose strings and one import removal).
  • The full vitest suite (CI shards 1–4 green; locally I ran check-doc-links, check-readme-exports and the three pin files).
  • The dev's 26-minute contended run — its box, not reproducible here.
  • check-clause2-carriers --pair 8984 (dev reports rc 0; not re-run).
  • Lint on types / plugin-detail / root (CI Lint green; I re-ran app-shell only, the package the five lines were reported in).
  • The check-doc-snippet-types / check-readme-exports gate scripts themselves (CI green).
  • Out-of-tree consumers; any browser render.

Generated by Claude Code

… reason I wrote for keeping it was false (objectui#7997)

Incremental contract review returned REWORK on one item, and it overturned me
correctly. Keeping the row was a defensible CALL; the justification I committed
next to it was not TRUE, and this whole family of cards is about source that states
something false about itself.

What I had written: that `@object-ui/components` `containers.tsx` resolves the
built-in Related page tab through this map, so dropping the row would leave it
untranslated with no I18nProvider mounted. Re-measured on my own clone, all
three legs, and every one refutes it:

  - `DETAIL_DEFAULT_TRANSLATIONS` has exactly ONE runtime consumer, the
    `createSafeTranslation(...)` call in this same file. Every other tree-wide
    occurrence is a test, a comment, or the barrel re-export.
  - `@object-ui/components` declares NO dependency on `@object-ui/plugin-detail`
    — not in `dependencies`, `peerDependencies` or `devDependencies` — so it
    could not reach this map even if it wanted to.
  - `containers.tsx:491` resolves that label with `useSafeTranslate()` from
    `@object-ui/i18n`, whose contract (`useSafeTranslation.ts`) is: try each
    key, and with no provider return the POSITIONAL English fallback. The token
    `Related` is itself that fallback, stated in the call site's own comment.

And the decisive one: 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 the row is genuinely unreachable from its only consumer, and it goes. The
PACK key stays live through `containers.tsx`, which is a different surface; 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.

Deletion was checked against the contract that actually governs this map before
committing: `app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx` is
15/15 green 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3489.8 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-B2r4RIaV.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 7.95KB 3.19KB
create-plugin (index.js) 28.21KB 9.54KB
data-objectstack (index.js) 207.32KB 57.37KB
fields (index.js) 247.01KB 62.29KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 130.98KB 34.54KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 251.44KB 65.17KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.03KB 14.30KB
plugin-list (index.js) 112.52KB 27.64KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 83.34KB 27.61KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Contract review

Round 3 (incremental, R2.c only) — PR: #8984 · head reviewed: 8348568f9a60b1d0b2be7b2edf949c10237c5420 (re-read from origin immediately before posting — unchanged) · previous verdicts: 5621736903 (REWORK, f57dc39f3) → 5622684649 (REWORK, 9835365e1, one item) · card: #7997 · ruling: 「关掉详情页那个入口(推荐)」

Scope: the delta 9835365e1..8348568f9 — one commit, one file, 11 deletions (packages/plugin-detail/src/useDetailTranslation.ts: the 'detail.related' row and the ten-line comment that justified it). Everything upheld in the two previous verdicts is not reopened. Measured in my own clone at this head, every rc to a file before any pipe.

Tier fuse

Own transcript (agent-a1449d9ed25a7ae58.jsonl, marker-located): 158 harness-stamped model values at the start of this round (171 at posting), every one claude-fable-5-1; no fallback or overload notice. CONTRACT_REVIEW_TIER (fresh objectstack origin/main, dispatch-gates.mjs:10505) = 'claude-fable-5-1'. Reading = tier.

CI at this head, read by this seat

34 check runs, 34 completed: 31 success + 3 skipped (Test (coverage), Test (coverage shard …), dependabot), 0 non-green. 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. Matches the dispatch seat's count.

① derived judgments — the delta

1. The deletion is correct and complete. In plugin-detail non-test source: detail.related' = 0, with the untouched sibling row detail.relatedRecordOne' = 1 as the probe's own control (the probe sees rows that exist, so the zero is a reading); template-key reads t(\detail.${…}`)= 0; no test in the package asserts the deleted row (and CI is green). The key itself did not die: all **10** locale packs still carryrelated: (i18n/src/locales/en.ts:1089), and @object-ui/components containers.tsx:261still maps the built-inRelatedtab to'detail.related'— with a provider that reads the pack, without oneuseSafeTranslatereturns the positional token. The dev re-measured the three legs it had asserted wrongly and each reading matches mine: one runtime consumer of the map (same file), nocomponentsplugin-detaildependency in any of the three dependency fields,useSafeTranslate's positional fallback **is** the Relatedtoken. Taking the delete option was the cleaner of the two I offered. No accept-set or public-surface change is involved:DETAIL_DEFAULT_TRANSLATIONS` is a provider-less fallback table, and this row was unreachable.

2. The i18n-report contrast IS a real control. Re-run at this head (rc=0): detail.related is still listed [needs-review] and its file list is now exactly components/src/renderers/layout/containers.tsx + its page-tabs-builtin-label-i18n-4645.test.tsxuseDetailTranslation.ts is gone from it — while the neighbour detail.relatedRecordOne, still [needs-review], still lists useDetailTranslation.ts. That pairing separates the two propositions the way the dev says it does: "the key is dead" is refuted by the row still appearing with a live source in another package; "this package's copy is dead" is confirmed by its file dropping off the list; and the neighbour proves the report can see a useDetailTranslation.ts row when one exists. One precision note, in the dev's favour: the report's status did not move ([needs-review] before and after, because containers.tsx's occurrence is a map literal rather than a t() call) — only the file list did, which is exactly the claim.

3. "Check the governing contract before deleting" — the reasoning is right, and it is the test's own shape. defaults-maps-mirror-en-pack.test.tsx at this head: 15 / 15, rc=0. Its byte-identity case iterates Object.entries(map) (:242) and its key-existence cases iterate Object.keys(map) (:228, :344) — all judge rows the map has, so removing a row cannot fail them; the only "do not empty" guard is Object.keys(map).length > 0 (:216); and the sentinel family the test pins by name is detail.showEmptyRelated / _one / _other (:290-300), not this key. Deleting a row whose key the pack defines is therefore permitted by the contract that governs the table, and the dev read that from the test rather than assuming it.

② semver

Unchanged: minor on both packages; Changeset Bump Policy and Changeset Fixed Group Check green at this head. The delta touches no declared face, no changeset text and no published document.

③ boundary flags

  • ⭐ The gap I reported last round is closed with a reading. The case is now named and exists (scripts/__tests__/check-readme-exports.test.ts:988 / :997the PARTIAL_EXCERPTS ledger, as it stands in this repository > hides ONLY omissions, at every declaration the tree can judge — in ANY build state), and it is measured on both sides of the same 15 000 ms budget: 15 786 ms → TIMEOUT inside a 5-target run holding the shared verify lock 25m25s, 5 555 ms → PASS run alone, per-case from --reporter=verbose; two independently scheduled long runs breached it (15 944 / 15 786 ms). My own isolated verbose run at this head: 87 / 87, rc=0, that case 3 744 ms, the file's dominant case by an order of magnitude (next slowest 366 ms). That is a reading of contention on a named case — a ~3–4× slowdown on the one case that already spends a quarter to a third of the budget alone — not an inference. The attribution ("the race is my own co-scheduled 5-target invocation") is the inference consistent with it, and the remedy (stop packing five targets into one locked run; ⛔ do not raise the budget) is the AGENTS.md:232-245 / :238 one. CI's owning shard 1/4 is green at this head. Closed. Out-of-scope observation, not a defect of this PR: a single ~4–6 s case is intrinsically the canary of any saturated run; it belongs to a different card if anyone wants it cheaper.
  • Lint, five literal error lines, 0 errors: same reading as last round, reproduced then; not re-run this round (delta is 11 deleted lines in one file; CI Lint green at 18:08:13Z).
  • Scope held: record:related_list mirror mismatch still untouched and carried in the acceptance notes; record:related_list.actions still has no read site; registry-inputs-spec-parity.test.ts untouched. Correct.
  • Commit-message amend before push (no force-push): procedural, not contract content; noted, not verified.

Independence

Implemented-by: claude/issue-7997-related-columns-accept-strings (mode:subagent)
Reviewed-by: agent-a1449d9ed25a7ae58 (mode:subagent, session_01Jmxdo7bmeqCQHLSfmLVX9w, harness model claude-fable-5-1)

SELF-REVIEW. Implementer and reviewer are subagents of the same dispatch session; this is not an independent review and must not be recorded as one. The delta was re-measured, not accepted, and the verdict is not softened for it.

VERDICT: PASS

What I did not measure this round:

  • The 81-program repo-wide type-check and the full vitest suite at this head (CI Type Check and all four test shards green; the delta is 11 deleted lines in one fallback table; locally I ran the mirror test, the dead-key report and check-readme-exports).
  • The dev's 25m25s contended run — its box, not reproducible here; I measured only the isolated side.
  • Lint at this head (CI green; reproduced last round).
  • check-clause2-carriers --pair 8984 (dev reports rc 0; not re-run).
  • Out-of-tree consumers; any browser render.

Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 10, 2026 18:30
@os-warren
os-warren added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 8c8da45 Sep 10, 2026
36 checks passed
@os-warren
os-warren deleted the claude/issue-7997-related-columns-accept-strings branch September 10, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants