fix(types): declare record:details hideFields / inlineEdit / showHeader - #9186
Conversation
`RecordDetailsComponentProps`'s top level had never been reconciled against
`@objectstack/spec` — objectui#8583 did `sections[]` member-for-member and
objectui#8604 fixed the top-level `columns` type, but the top-level KEY SET
diverged in both directions.
Direction 1 (fixed here): `hideFields`, `inlineEdit` and `showHeader` are
declared by the spec, read by `RecordDetailsRenderer`, and published as inputs
by `@object-ui/plugin-detail`'s registry manifest. Every layer declared them
except this published TypeScript face, so a spec-valid, renderer-honoured,
registry-published document was refused with TS2353. Each key takes the
contract's own authoring type — `hideFields` stays `string[]`, the bare-name
dialect the spec declares, rather than the `{name}` objects the renderer also
tolerates at its read site.
Direction 2 (ledgered, not fixed): the retired `layout` is still declared here
and the contract refuses it by name (ADR-0087 D2 tombstone). Removing it is a
published-surface retirement that breaks an in-repo consumer, which triage
scoped out of this card; the divergence is now signposted at the declaration
and pinned so it cannot rot into a stale comment.
`record-details-top-level-9040.test.ts` pins both directions on two
instruments. Only the `tsc` legs discriminate direction 1 — the defect was a
TypeScript-only refusal, so the `safeParse` legs are labelled PREMISE and are
green in both worlds.
Part of #9040
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
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
Size Limits
|
|
内容 PASS + 契约审查 PASS。 两个 open question 我在下面裁了,两个都判你对。⏸️ arm 等自身 test shard 转绿。 ⭐⭐⭐ OQ2 —— 你违背了我的派单字面,而你是对的我的派单写「body 里 裁决:A,保持 ⭐⭐ OQ1 —— 同样按 Aitem 1 是被裁决挡住的,不是被你的努力挡住的。 我独立复核了那条 load-bearing 的事实: ⇒ 删 裁决:A。 #9040 保持 open,item 1 作为一次独立的已发布面退役另行走:删键 + 挪那一个消费者 + 删本 PR 的台账腿 + 按 ⭐⭐⭐ 消融腿 D —— 本轮最锋利的一手你用同一个变异(删掉三个键)跑了两个仪器:
⇒ 因为这个缺陷只存在于 TypeScript 层。而这条腿把 pin 文件里自己那句「the safeParse legs are PREMISE not evidence」从一句声明变成了一次测量。 ⭐ 腿 C 防住了同形异义陷阱删 ⭐⭐
|
Part of #9040 — item 2 of that card only. Item 1 is deliberately left open; see
"What this PR does NOT do" below. A merge of this PR must leave that card open.
RecordDetailsComponentProps's TOP LEVEL had never been reconciled against@objectstack/spec. objectui#8583 didsections[]member-for-member andobjectui#8604 corrected the top-level
columnsTYPE, but the top-level KEY SETdiverged in both directions at once.
What changed
packages/types/src/record-components.tsnow declares the three keys thecontract declares and the renderer honours:
hideFieldsstring[]z.array(z.string())fieldsand every section'sfieldsinlineEditbooleanz.boolean()schema.inlineEditdefaulted on, ANDed with object editabilityshowHeaderbooleanz.boolean()schema.showHeaderdefaulted offA widening only — no key changes type, nothing is removed, so nothing that
compiled before stops compiling.
hideFieldsisstring[]on purpose. The renderer also tolerates entriesshaped like a one-key object naming the field, but the contract declares bare
names and refuses those values on parse; declaring them here would publish a
second dialect the contract rejects (Commandment #0.1). The registry manifest
holds the same fence.
Direction 1 — the omissions (spec probes, with live controls)
Measured on the INSTALLED pin,
@objectstack/spec17.4.0, by readingRecordDetailsProps's own shape and parsing against it.Declared top-level shape keys, read off the live schema object (8):
aria,columns,fields,hideFields,inlineEdit,layout,sections,showHeader.Accept legs — each a full green
safeParse, value surviving intodata:Value legs, proving each key JUDGES its value rather than swallowing it:
LIVE CONTROLS on the same instrument, same probe loop:
So the greens are about these three keys, not a schema that accepts anything;
and the reds are at the keys' own paths, not a schema that refuses everything.
Direction 2 — the retired
layout(refusal is BY NAME, measured)CONTROL that separates "retired key" from "unknown key" — a near-miss typo of
the same key gets the OTHER code:
The shape introspection agrees:
layout's member unwraps tonever, i.e. anADR-0087 D2 tombstone — a DECLARED key that accepts nothing. That is why a
parity check spelled "is this key present in the spec shape" reads it as live.
What this PR does NOT do, and why
⛔ It does not remove
layout. Triage on this card ruled: adding the three keysis in scope; removing
layoutnarrows a published type, so if the removalbreaks in-repo consumers, stop and report rather than deleting their usages
under this card. It does break one, measured below.
The divergence is therefore SIGNPOSTED rather than silently left: the
declaration now carries the contract's refusal and the retirement checklist,
and
record-details-top-level-9040.test.tspins both halves so the paragraphcannot rot into a stale comment. Removing the key makes those legs stop
compiling, which is how the next author finds the rest of the work.
The in-repo consumer survey that decided it
The word
layoutis useless as a search term here — 3565 line hits across 863files in
packagesappsexamplescontentscriptse2edocsskills.Narrowed to the 91 files that mention
record:details, cross-referenced withthe word, then judged ONE BY ONE. Every candidate:
packages/types/src/record-components.ts:64packages/types/src/__tests__/p1-spec-alignment.test.ts:538,548layout: 'stacked'on aRecordDetailsComponentPropsliteral and reads it backpackages/types/src/record-components.ts:178RecordHighlightsComponentProps.layout, a different interface and a different value setpackages/types/src/__tests__/p1-spec-alignment.test.ts:590,594packages/plugin-detail/src/renderers/record-details.tsx:580layout: 'vertical'is written onto the INTERNALdetail-viewnode typedany, not onto this interface;'vertical'is not even in this interface's enumpackages/types/src/__tests__/object-view-spec-parity.test.ts:207,273ObjectViewSchema.layoutpackages/types/src/__tests__/object-view-spec-parity.test.ts:115RecordDetailsProps.layoutapps/console/src/__tests__/registry-inputs-spec-parity.test.ts:3642@object-ui/typespackages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.tspackages/plugin-detail/src/index.tsx:443layoutinputpackages/types/src/layout.ts,views.ts,zod/views.zod.ts,block-config.ts,block-types.ts,containers.tsx, i18n keys,renderers/layout/pathsExactly one true consumer, and it is the one triage scoped out.
Verification
All exit codes captured by redirecting FIRST and reading
$?before any pipe.Measured on
156cf2ef2, the last commit of this branch.pnpm --filter @object-ui/types type-checktscprojects:tsc --noEmit,tsconfig.examples.json,tsconfig.test.json— the third is what compiles__tests__, so the pin's type legs are really judgedpnpm exec vitest run packages/types/(under the shared verify lock)Test Files 176 passed (176)·Tests 3444 passed (3444)pnpm exec vitest runon the new pin plusp1-spec-alignment.test.tsTest Files 2 passed (2)·Tests 44 passed (44)— both files really ranpnpm --filter @object-ui/types lintno-explicit-anyWARNING class — one on the deliberateEqualdirection proof, the same line the sibling pin carriescheck:changeset-presence·check:changeset-no-major·check:control-bytes·check:installed-pin-claims·check:spec-symbols·check:new-line-citations·check:changeset-claims·check:unreferenced-sourcesNOT MEASURED, with the reason:
check:readme-exportsandcheck:spec-floorsboth exit 1 on a PREREQUISITE, not on a finding — each prints "run
pnpm buildfirst" / "Build the workspace before running this gate", and this worktree has no
dist/. Neither reads anything this diff touches. Left to CI, which builds.Ablation — the widening, both legs, proved on disk
Each leg: mutate, PROVE the mutation reached the file, run, restore, PROVE the
restore by STATE. Every mutation ran under
trap ... EXIT INT TERM; the mutatorexits non-zero on a missing anchor, so a no-op edit cannot be read as a run.
Nothing outside the new pin went red, which is the other half of the reading:
the widening breaks nothing that compiled before.
That GREEN is the point, not a problem. The defect was TypeScript-only, so the
safeParselegs cannot tell the two worlds apart — the file says so in its ownheader and labels them PREMISE. This leg is what turns that sentence from a
claim into a measurement, so nobody later counts a green vitest run as evidence
the widening landed.
The
layoutmeasurement that decided direction 2⇒ The removal DOES break an in-repo consumer, so the triage ruling applies and
layoutstays. Note also what leg C proves about the vitest half: that consumerwrites
layout: 'stacked'and asserts it back, and vitest strips types — so theremoval is invisible to the test runner and visible only to
type-check.Boundary, stated rather than assumed: leg C compiled
packages/types' own threeprojects. The cross-package half is answered by the survey above — every file in
the repository that names
RecordDetailsComponentPropsis listed, and the onlyone outside this package (
plugin-detail/src/renderers/record-details.tsx) hasno
schema.layoutread, whichrecordDetailsInputs.spec-parity.test.tsalreadypins by grepping that source. CI type-checks every package regardless.
Out-of-scope finding, filed
The
layoutsurvey turned up a second, unrelated trap on the OTHER interface inthe same file:
RecordHighlightsComponentProps.layoutdeclares three valueswhile the contract's enum has two, so
layout: 'grid'compiles and is refusedat publish with
invalid_value. Measured with controls and filed asobjectui#9187. Deliberately NOT touched here — different interface, different
key, and narrowing it carries its own consumer survey.
Contract review
Clause-② is
yeson the claim, by construction: this moves a publisheddeclaration.
needs:contract-reviewis applied; the PR stays DRAFT, is notflipped ready, and is not enqueued.
Generated by Claude Code, session
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ(written as prose as well as in the footer, because the footer does not reliably
survive a body edit).
🤖 Generated with Claude Code
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Generated by Claude Code