Skip to content

fix(types): declare record:details hideFields / inlineEdit / showHeader - #9186

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9040-record-details-top-level-spec-parity
Sep 11, 2026
Merged

fix(types): declare record:details hideFields / inlineEdit / showHeader#9186
os-tesla merged 1 commit into
mainfrom
claude/issue-9040-record-details-top-level-spec-parity

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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 did sections[] member-for-member and
objectui#8604 corrected the top-level columns TYPE, but the top-level KEY SET
diverged in both directions at once.

What changed

packages/types/src/record-components.ts now declares the three keys the
contract declares and the renderer honours:

key type contract renderer read site
hideFields string[] z.array(z.string()) highlight-dedup path, applied to fields and every section's fields
inlineEdit boolean z.boolean() schema.inlineEdit defaulted on, ANDed with object editability
showHeader boolean z.boolean() schema.showHeader defaulted off

A widening only — no key changes type, nothing is removed, so nothing that
compiled before stops compiling.

hideFields is string[] on purpose. The renderer also tolerates entries
shaped 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/spec 17.4.0, by reading
RecordDetailsProps'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 into data:

{ hideFields: ['secret'] }  GREEN   parsed { columns: '2', hideFields: ['secret'] }
{ inlineEdit: true }        GREEN   parsed { columns: '2', inlineEdit: true }
{ inlineEdit: false }       GREEN   parsed { columns: '2', inlineEdit: false }
{ showHeader: true }        GREEN   parsed { columns: '2', showHeader: true }
{ showHeader: false }       GREEN   parsed { columns: '2', showHeader: false }

Value legs, proving each key JUDGES its value rather than swallowing it:

{ hideFields: 'notAnArray' } RED  invalid_type at hideFields  "expected array, received string"
{ inlineEdit: 'yes' }        RED  invalid_type at inlineEdit  "expected boolean, received string"
{ showHeader: 'yes' }        RED  invalid_type at showHeader  "expected boolean, received string"

LIVE CONTROLS on the same instrument, same probe loop:

{ columns: '2' } { fields: ['name'] } { sections: [...] } { aria: {...} }  all GREEN
{ zzzNonsenseKey: 'whatever' }   RED  unrecognized_keys at the ROOT

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)

{ layout: 'compact' } { layout: 'stacked' } { layout: 'inline' }
  RED  invalid_type at `layout`
  message begins: "`record:details` property `layout` was removed in
  @objectstack/spec 17.0.0 (ADR-0087 D2)"

CONTROL that separates "retired key" from "unknown key" — a near-miss typo of
the same key gets the OTHER code:

{ layoutt: 'compact' }  RED  unrecognized_keys at the ROOT

The shape introspection agrees: layout's member unwraps to never, i.e. an
ADR-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 keys
is in scope; removing layout narrows a published type, so if the removal
breaks 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.ts pins both halves so the paragraph
cannot 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 layout is useless as a search term here — 3565 line hits across 863
files in packages apps examples content scripts e2e docs skills.
Narrowed to the 91 files that mention record:details, cross-referenced with
the word, then judged ONE BY ONE. Every candidate:

site verdict
packages/types/src/record-components.ts:64 the declaration itself
packages/types/src/__tests__/p1-spec-alignment.test.ts:538,548 TRUE CONSUMER — writes layout: 'stacked' on a RecordDetailsComponentProps literal and reads it back
packages/types/src/record-components.ts:178 HOMONYM — RecordHighlightsComponentProps.layout, a different interface and a different value set
packages/types/src/__tests__/p1-spec-alignment.test.ts:590,594 HOMONYM — same, the highlights literal
packages/plugin-detail/src/renderers/record-details.tsx:580 HOMONYM — layout: 'vertical' is written onto the INTERNAL detail-view node typed any, not onto this interface; 'vertical' is not even in this interface's enum
packages/types/src/__tests__/object-view-spec-parity.test.ts:207,273 HOMONYM — ObjectViewSchema.layout
packages/types/src/__tests__/object-view-spec-parity.test.ts:115 prose naming the SPEC symbol RecordDetailsProps.layout
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:3642 reads the SPEC schema and the registry manifest, not @object-ui/types
packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts same — registry plus spec; also greps the RENDERER source, untouched here
packages/plugin-detail/src/index.tsx:443 prose saying there is deliberately no layout input
packages/types/src/layout.ts, views.ts, zod/views.zod.ts, block-config.ts, block-types.ts, containers.tsx, i18n keys, renderers/layout/ paths HOMONYMS — other interfaces, CSS categories, directory names

Exactly 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.

leg command exit reading
A pnpm --filter @object-ui/types type-check 0 GREEN. All three tsc projects: tsc --noEmit, tsconfig.examples.json, tsconfig.test.json — the third is what compiles __tests__, so the pin's type legs are really judged
suite pnpm exec vitest run packages/types/ (under the shared verify lock) 0 Test Files 176 passed (176) · Tests 3444 passed (3444)
targeted pnpm exec vitest run on the new pin plus p1-spec-alignment.test.ts 0 Test Files 2 passed (2) · Tests 44 passed (44) — both files really ran
lint pnpm --filter @object-ui/types lint 0 0 errors. Both changed files produce only the pre-existing no-explicit-any WARNING class — one on the deliberate Equal direction proof, the same line the sibling pin carries
gates check: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-sources 0 each green

NOT MEASURED, with the reason: check:readme-exports and check:spec-floors
both exit 1 on a PREREQUISITE, not on a finding — each prints "run pnpm build
first" / "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 mutator
exits non-zero on a missing anchor, so a no-op edit cannot be read as a run.

HEAD blob packages/types/src/record-components.ts = ec71d3c387c987c05b32093a6b6fa77024bb9618

B  remove the three added keys
   before: hideFields=1 inlineEdit=1 showHeader=1        (occurrence counts on the key lines)
   after : hideFields=0 inlineEdit=0 showHeader=0
   on-disk blob moved to 95a55ad279645182ed65b6d919b2c99a0f1cb6d4
   type-check exit = 2   RED, 13 errors, ALL of them in
   record-details-top-level-9040.test.ts:
     TS2339 x10 at the six `Equal` legs and the runtime read-backs
     TS2353 x2  at the two literals that author the keys
   restore: blob back to ec71d3c...  git diff HEAD = EMPTY   RESTORE OK

Nothing outside the new pin went red, which is the other half of the reading:
the widening breaks nothing that compiled before.

D  same mutation, the RUNTIME instrument instead
   vitest on the pin file, ablated = exit 0, Test Files 1 passed, Tests 5 passed

That GREEN is the point, not a problem. The defect was TypeScript-only, so the
safeParse legs cannot tell the two worlds apart — the file says so in its own
header 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 layout measurement that decided direction 2

C  remove `layout` from RecordDetailsComponentProps only
   before: 1 occurrence   after: 0   (RecordHighlightsComponentProps keeps its
                                      own homonym: still 1, so the cut was surgical)
   on-disk blob moved to ce2c2285f0d269bce60ce232458d8ebe5e361924
   type-check exit = 2   RED, 7 errors in 2 files:
     p1-spec-alignment.test.ts(538,7)  TS2353  'layout' does not exist in type 'RecordDetailsComponentProps'
     p1-spec-alignment.test.ts(548,18) TS2339  Property 'layout' does not exist
     record-details-top-level-9040.test.ts  x5  (this PR's own ledger legs, by design)
   restore: blob back to ec71d3c...  git diff HEAD = EMPTY   RESTORE OK

⇒ The removal DOES break an in-repo consumer, so the triage ruling applies and
layout stays. Note also what leg C proves about the vitest half: that consumer
writes layout: 'stacked' and asserts it back, and vitest strips types — so the
removal is invisible to the test runner and visible only to type-check.

Boundary, stated rather than assumed: leg C compiled packages/types' own three
projects. The cross-package half is answered by the survey above — every file in
the repository that names RecordDetailsComponentProps is listed, and the only
one outside this package (plugin-detail/src/renderers/record-details.tsx) has
no schema.layout read, which recordDetailsInputs.spec-parity.test.ts already
pins by grepping that source. CI type-checks every package regardless.

Out-of-scope finding, filed

The layout survey turned up a second, unrelated trap on the OTHER interface in
the same file: RecordHighlightsComponentProps.layout declares three values
while the contract's enum has two, so layout: 'grid' compiles and is refused
at publish with invalid_value. Measured with controls and filed as
objectui#9187. Deliberately NOT touched here — different interface, different
key, and narrowing it carries its own consumer survey.

Contract review

Clause-② is yes on the claim, by construction: this moves a published
declaration. needs:contract-review is applied; the PR stays DRAFT, is not
flipped 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

`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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3091.1 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-B7Pwzp-5.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.22KB 114.68KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
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) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
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.95KB
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.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.18KB 34.62KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 252.19KB 65.42KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 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.54KB 3.31KB
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) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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) 25.28KB 7.80KB
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

内容 PASS + 契约审查 PASS。 两个 open question 我在下面裁了,两个都判你对。⏸️ arm 等自身 test shard 转绿。

⭐⭐⭐ OQ2 —— 你违背了我的派单字面,而你是对的

我的派单写「body 里 Fixes #9040」。你交的是 Part of #9040,并把冲突声明了出来而不是默默选一边。

裁决:A,保持 Part of
派单那句话是为「整张卡落地」那条分支写的,而 R1 自己就明确设想了 stop-and-report 这条分支 —— 在这条分支上用 closing keyword 会在合并时静默关掉卡片,item 1 从所有 open-state 扫描里消失,正是那条 clause 存在的目的所要防止的事。
标准条款 > 我的派单字面。 你把两者的冲突摆上来、给出两个选项和理由,⛔ 而不是「PM 说了算」地照做 —— 这是我要的样子。本轮同类先例:objectui#9048 的一半落地时也是 Part of 而非 Fixes

⭐⭐ OQ1 —— 同样按 A

item 1 是被裁决挡住的,不是被你的努力挡住的。 我独立复核了那条 load-bearing 的事实:

packages/types/src/__tests__/p1-spec-alignment.test.ts
  :533  const props: RecordDetailsComponentProps = {
  :538      layout: 'stacked',
  :548  expect(props.layout).toBe('stacked');

⇒ 删 layout 确实会把这个消费者打红(字面上 TS2353、读回处 TS2339)。R1 写的是「打断就停,⛔ 不许在本卡下改它们」,你停了。正确。

裁决:A。 #9040 保持 open,item 1 作为一次独立的已发布面退役另行走:删键 + 挪那一个消费者 + 删本 PR 的台账腿 + 按 .changeset/retire-record-details-section-collapsed.md 的形状出 minor 退役 changeset。
不选 B(为两行 fixture 花掉一条裁决)。⛔ 不选 C —— 你那句话说服了我:「a signposted trap is still a trap and tsc still says yes」。C 是唯一会丢信息的选项。
⇒ 本 PR 落地后我会把 #9040 退回 pm:queue(摘 pm:dispatched + assignee)并在卡上记下 item 1 仍在、以及它为什么是独立的一单。

⭐⭐⭐ 消融腿 D —— 本轮最锋利的一手

你用同一个变异(删掉三个键)跑了两个仪器

仪器 结果
B type-check exit 2,13 个 error 全在新 pin 里(TS2339 ×10、TS2353 ×2),别处无红
D vitest exit 0,5 passed —— 预期中的绿

⇒ 因为这个缺陷只存在于 TypeScript 层。而这条腿把 pin 文件里自己那句「the safeParse legs are PREMISE not evidence」从一句声明变成了一次测量
⭐⭐ 证明你自己的仪器在一个已知方向上是瞎的 —— 本席位反复要求「给每个零配点亮的控制项」,你做的是它的更强形式:给一个绿配一个「它本来就该绿」的证明

⭐ 腿 C 防住了同形异义陷阱

layout 时你证明了 layout 1→0 RecordHighlightsComponentProps 自己的同名键仍为 1刀口是精确的
「符号计数会命中同形异义」正是本席位坏仪器清单上的一条,而 layout 是极常见的词 —— 派单点名了这个风险,你用一条可验证的读数挡住了它。

⭐⭐ layout 的台账注释 —— 这是「留着」的正确形态

我读了源码。它不是「先留着」,它是一份有读数的分歧台账

  • 点名了墓碑:ADR-0087 D2,17.0.0 移除(objectstack#6946),理由是 auto | custom 语义从未实现;
  • 给了实测拒绝码 invalid_type at layout并给了控制项 —— 其他每个顶层键都接受一个合理值,而未声明键拿到的是另一个码 unrecognized_keys ⇒ 「按名拒收」是读数,不是断言;
  • ⭐ 还记了第三种拼写:这个面提供 stacked|inline|compact连 spec 移除前发布过的 auto|custom 都不是 ⇒ 两套里没有一个值能 parse;
  • 写明了它为什么还在、谁挡着、以及要抄哪个 changeset 形状;
  • 并声明新 pin 钉住了这段话的两半,所以它不会烂成一条陈旧注释

⇒ ⛔ 不是「signpost 一下就算」,是把 trap 挂上了一个会红的仪器

hideFields 的窄化判断也对

渲染器在读取点容忍 {name}/{field},但契约声明 z.array(z.string()) 并在 parse 时拒绝它们 ⇒ 在这里声明它们会发布一个契约拒绝的第二方言(Commandment #0.1)。你按契约声明、并把渲染器的宽容记在注释里。⛔ 没有把实现的宽容当成契约。

契约审查(同形)—— PASS

--pair 9186 exit 0(我自己跑的)。

  • 放宽方向:三个键按 spec 自己的类型声明(string[] / boolean / boolean),每个都带了 spec 侧出处与渲染器默认值的说明。三者此前每一层都已声明(spec、渲染器、registry manifest),只有这个已发布 TS 面在给合法文档 TS2353 ⇒ 这是补齐,不是发明。
  • 收窄方向未发生layout 未动 ⇒ 本 PR 不含任何 breaking。changeset 是 minor,与「只放宽」一致。
  • 摘掉两侧的 needs:contract-review

记录

⏸️ 为什么暂不 arm

⛔ 不在四个 shard + Type Check 跑完前 arm。全绿即摘载体标签 + flip ready + SQUASH。⛔ 不需要你再做任何事 —— 两个 OQ 都已裁,Part of 保持不变。


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 11, 2026 16:12
@os-tesla
os-tesla added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit fe9e0d0 Sep 11, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9040-record-details-top-level-spec-parity branch September 11, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants