Skip to content

feat(plugin-detail,types): record:details honours hideEmpty again — the protocol's all-empty section contract - #9627

Merged
hotlong merged 4 commits into
mainfrom
claude/issue-8603-record-details-hideempty-restore
Sep 16, 2026
Merged

hotlong merged 4 commits into
mainfrom
claude/issue-8603-record-details-hideempty-restore

Conversation

@hotlong

@hotlong hotlong commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #8603

record:details honours hideEmpty on a section again. An all-empty section hides itself; hideEmpty: false keeps its heading and label skeleton. That is what @objectstack/spec declares and describes, and what this renderer had stopped delivering.

Patch round — the four corrections an at-tier contract review required

The first at-tier contract review of this branch returned FAIL on head 8d8523e0d and named four corrections. All four are prose, release notes and one docblock; ⛔ no behavioural line moved, and everything that review ruled right is untouched — the renderer read, the ?? true placement and its deliberate non-reach into the fallback body and the detail-section node, the !isEditing gate, both pins, the ablation, the ledger edit, the file surface and the semver level.

1. The hideEmpty docblock claimed a default that is false on the other consumer. @default true and "Omitted behaves as true" were written on a member of DetailViewSection, and that type is read by TWO authorable renderers. record:details maps every authored section with hideEmpty ?? true; detail-view — registered with its own sections input — hands each section to DetailSection unchanged and applies no default. Re-measured here rather than taken from the review, with a sibling control section that must render present in every case, and it rendered in all six:

consumer omitted true false
detail-view heading + 4 rows hidden heading + 4 rows
record:details hidden hidden heading + 4 rows

⚠️ The first attempt at that measurement read all zeros, including for the control — DetailView has no data prop (the record arrives as schema.data), so the component took its record-not-found branch and rendered nothing. A vacuous zero, caught by the control and discarded rather than read. The tag and the sentence are gone and the prose now names both consumers and which one resolves the default; the removal is proven by extracting the JSDoc block that precedes this member specifically and finding zero default tags in it, against a lit control (showBorder's own block, same file, same idiom, still carrying exactly one). A file-wide count could not have answered it — there are other @default true tags in that file, on other members. objectui#7361 / objectui#7735 class.

2. A sentence in the RENDERER_ONLY_SECTION_KEYS hunk asserted a falsehood. It restated "the spec's section object does not declare it" of showBorder, which the installed 17.4.0 does declare among the section entry's 12 keys. The previous revision of this PR noted that and deferred it; this PR authored the sentence, so it is corrected here. The list is now described as what it is — a hand-kept CANDIDATE set of keys this renderer honours on a section, whose forbidden members are derived per run by filtering it through the installed schema — so neither name's status is written down as a verdict anywhere in the file. The membership itself is unchanged. Commandment #9 class.

3. Two pending changesets contradicted this release, and they are corrected in place rather than rebutted. Both go verbatim into the same CHANGELOG as this one. 7129-retire-detailviewsection-hideempty.md published "Migration: delete hideEmpty from any record:details section you author"; 7064-empty-section-default.md published "a sparse record keeps its section skeleton instead of collapsing".

⭐ Route taken: correct the two bodies. Prose only, frontmatter and declared packages untouched. Why this route and not naming the superseded paragraphs from the 8603 entry: the 7129 paragraph is a MIGRATION STEP, an instruction a reader performs, and performing it now deletes the very spelling that keeps an all-empty section's skeleton. A rebuttal elsewhere in the same file does not stop someone acting on it. The route is also the one this repository's own history sanctions: check-changeset-overwrite's header measured every modification of a pre-existing changeset on main and found 19 for 19 legitimate — bump corrections and "factual corrections to prose" — with the narrower signal being whether declared package NAMES survive. They do here: the gate reports this change under its own case 2 and prints "declared at base" identical to "declares now" for both files. The 7064 correction keeps what still holds (the fallback body and the detail-view node keep the skeleton with zero authoring; the label-graveyard guard is intact) and marks what does not. The 8603 entry additionally states the net delta against the last PUBLISHED release, which is what a consumer upgrading actually sees.

4. The record-details.tsx comment rewrote a maintainer ruling's ground. It said objectui#7064's objection was that an all-empty section vanished "with no way for a spec-validated page to ask it back, because the spec refused the key". Read first-hand from that card: the ruling of 2026-08-31 held that an empty detail body is a PLATFORM concern and that a metadata application should not have to author its way out of one, the deliverable being a sparse record keeping a full structural skeleton with zero app-side authoring. The objection was to the AUTHORING SHAPE — every application hand-writing hideEmpty: false per section as per-app tax — and it was made KNOWING the key was declared upstream by objectstack PR #11662. The spec-refusal reading came out of that card's own execution afterwards and was routed onward. The comment now records that ground, records that this change reverses the ruling's behavioural half, and leaves the standing of that reversal to the review that ruled it. ⛔ It does not argue the reversal.

⚠️ Also in this round, and not one of the four: the CJK this branch had added to the -7129 pin header is removed, per AGENTS.md commandment #-1 (English-only codebase). Measured across the branch's whole diff with a lit control: net CJK introduced is zero. Pre-existing non-English strings in these files are left alone.

⛔ Two further items from that review are NOT in this diff and are not the implementer's: the dated note owed on objectui#7064, and naming the hotcrm#1247 consequence on the card. Both belong to the director seat.

The ruling this executes

Director seat, batch #137 item 3, maintainer 「同意」 2026-09-15 — objectui#8603 comment 5689202410, letter A, quoted:

The protocol is correct. @objectstack/spec RecordDetailsProps.sections[].hideEmpty was declared on #11289 (maintainer ruling 2026-08-23, direction 1) from a measured symptom and with 「the renderer is unchanged」 written into the declaration; an author-declarable section-level empty-hide is an ordinary capability of record layouts. objectui#7129's Q1-A (2026-09-01, batch #28) retired the read on the premise 「spec refuses it」, which was already false upstream (the pin sat at 17.2.0). ⇒ A: objectui aligns to the protocol and restores the record:details read of hideEmpty with the behaviour the describe() promises (true / renderer default: an all-empty section renders nothing; false: heading and skeleton stay).

objectui#7129 Q1-A is thereby superseded for this key; a dated note is added on that card, ⛔ its text is not rewritten. Q2-C of that ruling is untouched.

⚠️ Q2-C is untouched, and that is a scope line, not a footnote. The empty ROWS of a section that still has a filled row stay with DetailSection's auto-hide heuristic and the reader's show-empty toggle, with no authored override in either polarity. This change does not touch that branch. The two domains are disjoint by construction: the heuristic requires filledCount greater than zero, hideEmpty applies only where it is zero.

Premises, measured against the installed artifact before writing

Read from the installed @objectstack/spec in this worktree, not transcribed from the card.

Version. node_modules/@objectstack/spec/package.json reads 17.4.0; the lockfile resolves the same.

The key, and its describe(). Read off the shipped json-schema/ui/RecordDetailsProps.json, at properties.sections.items.properties.hideEmpty, verbatim:

Hide this section's empty fields (renderer default: on — and a section whose fields are ALL empty then renders nothing at all: no heading, no skeleton). Set false to render empty rows, keeping the section's label skeleton on an all-empty record (e.g. a brand-new one).

The entry declares 12 member keys and additionalProperties: false. ⇒ premise 1 CONFIRMED, and the ruling's parenthetical is the describe's own words.

Distinct from the reference rail's same-named key, which is component-level and says something else entirely ("Fold entries whose related count is 0 into a single + N empty expander chip…"); RecordReferenceRailProps.entries[] does not carry hideEmpty at all.

Runtime polarity probe, with controls, exit 0:

SUBJECT hideEmpty:true            success=true   sections[0] = {"label":"Contact","fields":["phone"],"hideEmpty":true}
SUBJECT hideEmpty:false           success=true   sections[0] = {"label":"Contact","fields":["phone"],"hideEmpty":false}
CONTROL+ declared sibling columns success=true   value survives
CONTROL- minted absent key        success=false  codes=["unrecognized_keys"] keys=["os8603_absent_probe_qhx"]

The minted control token was verified absent from the tracked corpus first (git grep -lI returns 0 files, exit 0), against a positive control on the same instrument (RecordDetailsProps returns 30 files). ⇒ the key parses because it is DECLARED, not because the object went permissive, and the value reaches parsed.data.

What objectui#7129 actually deleted (premise 2, read from the retirement commit rather than assumed): from DetailSection, the !section.hideEmpty limb of shouldAutoHideEmpty, the section.hideEmpty || limb of hideEmptyEffective, and the same limb in showEmptyToggle; from RecordDetailsRenderer, the hideEmpty: s.hideEmpty slot. The restoration below matches the DECLARED behaviour, ⛔ not that deleted code — the old read tested truthiness, so a written false was indistinguishable from unauthored, which is the paradox #7129's Q2 measured.

RENDERER_ONLY_SECTION_KEYS (premise 3) did list hideEmpty; verified before editing, and it no longer does.

What changed, and the one design decision in it

Where the default lives is the decision. RecordDetailsRenderer resolves it on an AUTHORED section (hideEmpty: s.hideEmpty ?? true) and DetailSection tests section.hideEmpty === true.

That placement is load-bearing. DetailSection also renders sections nobody can author the key on: the record:details direct-fields fallback body and the detail-section node each synthesize one, and neither surface declares hideEmpty. A default read as !== false inside the component reaches those too — and a hide there is a hide with no declarable spelling to ask the skeleton back, which is the exact defect upstream declared this key to fix, reintroduced one surface over. It is also what "the renderer default" in that describe() means: the default of the renderer the key is declared on.

⚠️ This was measured, not reasoned about after the fact. The first implementation put !== false in DetailSection and it reddened four pins across the package — every one of them a section the author never wrote (two emptiness-predicate amplifications, the credential-gate blast radius on a deliberately valueless signature sample, and the unresolved-identity fail-closed pin, whose ghost row is a synthesized body). Moving the default to the mapping leaves all four untouched. ⚠️ Precisely: outside the two pins the ruling names, the only other test file this diff touches is the ruling-named recordDetailsInputs.spec-parity.test.ts; the four that reddened under the first implementation are not in the diff at all.

?? true is the spelling objectui#7064 removed (maintainer ruling 2026-08-31) and it is back deliberately. That ruling's objection was to the BEHAVIOUR — an all-empty section vanishing while a spec-validated page had no way to ask it back, because the spec refused the key. This ruling reverses the behaviour and the way back now exists and parses; the spelling is what CONFINES the default to the authored surface.

One boundary this renderer draws, stated rather than buried: the hide is gated on !isEditing. Inline-edit mode is where those empty rows are the INPUTS, so hiding an all-empty section there would put its fields out of reach entirely — a new defect rather than a restored behaviour. The contract governs what a reader sees.

Files:

  • packages/types/src/views.ts — hideEmpty?: boolean declared again, replacing the retirement tombstone. Mechanically required: DetailSection cannot read a key the interface does not declare, and the four-party pin's compile-time assertion is precisely about this declaration.
  • packages/types/src/zod/views.zod.ts — DetailViewSectionSchema mirrors it, so the zod-mirror parity ledger neither grows nor gains an entry.
  • packages/plugin-detail/src/renderers/record-details.tsx — the slot, and the default.
  • packages/plugin-detail/src/DetailSection.tsx — the read.
  • packages/plugin-detail/src/index.tsx — the manifest's sections description teaches the key instead of warning that authoring it does nothing; hideEmpty leaves the never-teach note.
  • packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts — RENDERER_ONLY_SECTION_KEYS drops it (the criterion is "keys the spec REFUSES", and it no longer is), plus a live control asserting it is on the declared side of that line.
  • the two pins the ruling names.
  • scripts/check-installed-spec-pin-claims.mjs — the ledger entry for the rewritten paragraph goes 2 sites to 1. That gate named the mismatch itself; the count moving DOWN is its ratchet.
  • .changeset/8603-record-details-hide-empty-restored.md — minor, taken from the gate's verdict (check-changeset-presence names 2 released packages; check-changeset-no-major caps at minor).

Pins, in both directions, with non-vacuity

record-details.hideEmptyRetired-7129.test.tsx — the four-party alignment pin. All four now declare and honour the key: the spec declares it, @object-ui/types declares it (compile-time, with a minted absent probe as the non-vacuity leg), the zod mirror carries it, and 4/4 reads it end to end in BOTH directions. ⭐ The filename is deliberately kept: it is this key's lineage pin, and its routing target moves to this card, away from objectui#7122 — a card that is no longer open, and whose subject was CalendarConfigSchema.titleField, unrelated to this key.

record-details.emptySectionDefault.test.tsx — the two domains, stated separately: the empty-row default (unchanged, #7064 / Q2-C), the all-empty default (moved), and a third block pinning that the default reaches only the surface that declares the key.

⚠️ Non-vacuity. Every case whose verdict is "nothing rendered" carries a sibling CONTROL section that MUST render, so an absence assertion cannot pass by the tree having rendered nothing at all. That control needed a fixture repair to work: the pin's object schema had no name field, so the page-H1 dedupe ladder consumed industry — the record's one filled value — and the control section rendered no fields and took the all-fields-hidden exit. Measured, and the reason is written where the fixture is.

Ablation — three legs, each mutated on disk, proven, run, and restored

Each leg proves the injected text landed and the removed text is gone before reading any result, restores with git checkout HEAD -- path, and verifies the restore by blob hash against the HEAD blob plus an empty git diff HEAD. Run on the committed tree.

leg mutation result
CONTROL none 2 files / 11 tests pass
1 delete the DetailSection read from hideEmptyEffective 4 tests red — 4/4 both directions, both all-empty cases, the authored-section case
2 delete the renderer default (?? true), keep the pass-through 2 tests red — the UNAUTHORED default and the authored-vs-fallback pair
3 widen the DetailSection test to !== false 1 test red — the direct-fields fallback body loses its skeleton

Leg 3 is what makes === true a measured choice rather than a stylistic one. ⚠️ Leg 2's first attempt did not land on disk (a bad substitution wrote a placeholder instead); the on-disk proof caught it, the run was declared void rather than read, and the leg was redone — the table is the redone reading.

Gates — hand-derived, since the derivation tool refuses cross-repo

scripts/pm/dispatch-gates.mjs lives in objectstack and answers only about the tree it lives in, so there is no --ran reconciliation for this PR. ⚠️ Stated as a gap, not as coverage: the list below was hand-derived from this repo's own check:* scripts and .github/workflows/, and a hand-derived list can be short. Final head f8388535d (the patch round; the first review judged 8d8523e0d).

Green (exit 0) at f8388535d, the changeset gates re-run after the two corrections: check-changeset-presence · check-changeset-no-major · check-changeset-overwrite (report-only; it reports this change under its own case 2 and confirms both corrected declarations keep every package name they had at base) · check:changeset-claims · check:control-bytes · check:new-line-citations · check:component-surface-parity · check:spec-symbols · check:handler-key-reads · check:vi-mock-override-shape · check:vi-mock-specifiers · check:test-path-roots · check:registry-bare-names · check:prompt-keys · check:unreferenced-sources · check:i18n-dead-keys · check:doc-types · check:designer-field-key-parity · check:element-data-source-declaration · check:comment-mask-corpus · check:metadata-write-doors · check:upstream-port-parity · check:installed-pin-claims (red first, on a mismatch this branch caused; repaired, re-run green).

Tests and types, re-run at f8388535d: pnpm exec vitest run packages/plugin-detail/ packages/types/ scripts/__tests__/check-installed-spec-pin-claims.test.ts → 373 files / 6309 tests passed; the three pin and parity files on their own → 3 files / 27 tests passed. type-check for both packages → Done, TYPECHECK_EXIT=0, after BUILD_EXIT=0 on the dependency closure — the first attempt in the recreated worktree reported TS2307: Cannot find module across the package, which is an unbuilt-closure prerequisite and ⛔ not a reading about this diff. The gate script's own suite is included above, because this branch edits a gate script.

ESLint, re-run at f8388535d: the FULL population, not a narrowing — eslint . --no-inline-config --format json judged 5012 files, and the 9 files this branch touches carry 0 errors / 89 warnings, every warning a pre-existing no-explicit-any or react-refresh on a line this branch does not touch. Of the 79 files that do carry an error, 0 are files this branch changed — computed by intersecting the error set with this branch's own diff, not by reading the package name. The config declares no projectService and no parserOptions.project, so linting is not type-aware and this diff cannot move the verdict on a file it does not contain. (The repo-wide run reports 95 errors across 79 files, none of them in this diff; that population is CI's, and pnpm lint is a different, per-package command.)

NOT MEASURED, with the reason: check:sdui-registration-pins (exit 2, its own "no console build to weigh" prerequisite), check:spec-floors and check:readme-exports (both report no-artifact / a collapsed population without a full workspace build). All three are prerequisite readings, ⛔ not failures of this diff; none reads a file this branch changes except through built output.

Risk, and how to roll back

The user-visible change is the unauthored default: an all-empty AUTHORED section used to render its skeleton and now renders nothing. On a brand-new or very sparse record, a page that authors sections will show fewer sections than before unless it writes hideEmpty: false. That spelling parses green on the strict section object at spec 17.3.0 and later, which is exactly what it could not do when the read was retired — the escape hatch is real this time.

Two surfaces deliberately keep the old rendering, because neither gives the author that spelling: the direct-fields fallback body, and the detail-section node. Inline-edit mode also always renders.

Rollback is one revert of this branch; nothing is generated, no baseline moves, no lockfile changes.

Acceptance notes

  • Docs. No content/docs/guide/*.md and no package README enumerates record:details section member keys (grep for hideEmpty, showBorder, headerColor across content/docs/**, docs/** and packages/*/README.md returns nothing for this surface), so the manifest inputs description is the author-facing surface that moved, and it did. Measured, ⛔ not assumed.
  • showBorder — RESOLVED in the patch round, no longer deferred. The earlier revision of this body deferred it to "the next PR that touches this file"; the contract review escalated that, correctly, because this PR touches the file and authored the sentence asserting it. The list's words now describe a candidate set whose forbidden members are derived per run, so no name's status is asserted in prose. See patch-round item 2.
  • Noted, ⛔ not filed — packages/plugin-detail declares @objectstack/spec at ^17.1.0 while its pin asserts a key the spec declares only from 17.3.0. That assertion has been in the tree since 2026-09-05 and predates this branch, which neither widens nor narrows a range; the lockfile resolves 17.4.0. check:spec-floors is the instrument that owns this question and could not judge it here without a workspace build.
  • objectui#7129's dated note already exists — the director seat posted it at ruling time (comment 5689202985 on that card), so this branch adds no second note and ⛔ does not rewrite the ruling's text.

Generated by Claude Code


Generated by Claude Code

… the protocol's all-empty section contract

`@objectstack/spec` declares `hideEmpty` on `RecordDetailsProps.sections[]` and
its `describe()` promises: hiding is the renderer default, a section whose
fields are ALL empty then renders nothing at all — no heading, no skeleton —
and `false` keeps the heading and the label skeleton on an all-empty record.
This renderer read nothing, so the contract itself misled the author: the key
parsed green at publish and did nothing.

The retirement that removed the read rested on the premise that the spec
REFUSED the key, which was true at the 17.2.0 pin this repo held and already
false upstream. The director seat ruled the protocol correct and restored the
read; that ruling supersedes the retirement's first clause for this key only.

- `DetailSection` owns the ALL-EMPTY decision: `section.hideEmpty !== false`,
  read as an explicit polarity test so a written `false` is distinguishable
  from unauthored — the defect the pre-retirement `!section.hideEmpty` carried.
  Gated on `!isEditing`, so inline-edit mode never puts a section's fields out
  of reach; the contract governs what a reader sees.
- The auto-hide heuristic keeps the empty ROWS of a partly-filled section, with
  no authored override in either polarity. The two domains are disjoint by
  construction: the heuristic requires a filled row, this key requires none.
- `RecordDetailsRenderer` restores the explicit slot, deliberately undefaulted —
  `?? true` there would erase the distinction before the one read that resolves
  it.
- `@object-ui/types` declares the key again and the `DetailViewSectionSchema`
  zod mirror carries it, so the parity ledger neither grows nor gains an entry.
- The manifest's `sections` description teaches the key instead of warning that
  it does nothing, and it leaves the never-teach set, which the spec-derived
  filter had already stopped selecting it for.

Both pins move by the ruling and in both directions: the four-party alignment
pin now reads the key end to end with a sibling control section that must
render, so "nothing rendered" cannot pass by the tree having rendered nothing
at all, and the empty-section default pin states the two domains separately.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE
…ng, so it reaches only the declared surface

`DetailSection` also renders sections nobody can author the key on — the
`record:details` direct-`fields` fallback body and the `detail-section` node
each synthesize one, and neither surface declares `hideEmpty`. A default read
as `!== false` inside the component reached those too, so an all-empty
fallback body hid itself with no declarable spelling to ask the skeleton back:
the exact defect upstream declared this key to fix, one surface over. Measured
as four failing pins across the package, every one of them a section the author
never wrote.

The default therefore lives where the contract does. `RecordDetailsRenderer`
applies `?? true` to an AUTHORED section and `DetailSection` tests `=== true`,
which is also what "the renderer default" in the spec's own `describe()` means:
the default of the renderer the key is declared on. The collateral disappears
with it — the four pins are untouched by this change.

Two cases pin the placement itself: the fallback body keeps its skeleton, and
an authored section in the same document does not.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE
…etails` never-teach paragraph down to one site

objectui#8603 rewrote that paragraph — the `hideEmpty` half of it now points at
the instrument that re-derives the never-teach set instead of stamping a
17.2.0 measurement — so the entry's site count is one. The gate ratchets in
both directions and named the mismatch itself; the count moving DOWN is the
ledger shrinking, not a hole.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 12 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6594-headercolor-mirror-enum.md

  • names packages/types/src/views.ts → packages/types/src/views.ts — edited by this change

    The nearest precedent is objectui#5853 (.changeset/5853-tablecolumn-type-canonical-union.md), which narrowed TableColumn.type on the same three-ends pattern and exported a TABLE_COLUMN_TYPES tuple for the zod mirror to build its enum from. That shape is not available here and the difference is structural, not a preference: packages/types/src/views.ts is a type-only module, so a tuple there would add a runtime export to the package barrel (a value export cannot ride the barrel's export type block) and a runtime import edge from the zod entry into views.js. finding(types): TableColumn.type disagrees three ways — interface declares 8 literals, zod mirror accepts any string, renderer's live read set handles values outside the union via an as any cast #5853 had a second reason to export — producers needed its normalizeTableColumnType() at their emit seam — and headerColor has no producer that needs a runtime value. The literals are therefore written on each half and the anti-drift guarantee is carried by the pin above, which also covers the third end a shared tuple could not reach: the renderer, in a package @object-ui/types must not depend on.

.changeset/7287-resolve-title-field-shared-ladder.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    def.primaryField — a DetailViewSchema key (@object-ui/types views.ts), read off an OBJECT def and ranked ABOVE the canonical nameField ADR-0079 Phase 2 made the pointer (AGENTS.md Commandment #0.1). No producer can put it there: @objectstack/spec's object schema is a strictObject that answers unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws — which is why objectstack#6326 deleted the identical read from two lint rules. A census across both repos found zero object payloads carrying it (the only writers are three test fixtures), and primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist against 68 for nameField. Same shape as the undeclared objectDef.titleField read objectui#6531 measured and Five more consumer-side reads of the undeclared object-level titleField survive #6531 — in ObjectView view-config fallbacks and a useRecordSearch memo key #6557 removed. DetailViewSchema.primaryField is untouched and still honoured by DetailView's own header — it is a view key, and on a view it is legitimate.

.changeset/7344-handler-string-any-mirrors.md

.changeset/7586-primary-field-consumer-reads.md

  • names views.ts → packages/types/src/views.ts — edited by this change

    primaryField is a DetailViewSchema key (@object-ui/types views.ts) — a view key, which DetailView.resolveDisplayTitle reads off schema and is welcome to. Read off an object def it is undeclared: @objectstack/spec's object schema is a strictObject answering unrecognized_keys: ['primaryField'], and ObjectSchema.create() throws. primaryField appears in zero files of the shipped @objectstack/spec@17.2.0 dist, against 68 for the canonical nameField. objectstack#6326 removed the identical read from two lint rules; objectui#7287 / PR fix(plugin-detail): resolveTitleField delegates to the shared ADR-0079 ladder #7585 removed it from resolveTitleField. These two survived it — and three of this repo's own changelogs already called the probe "not a spec property — always undefined" while the code kept honouring it.

.changeset/7635-spec-173-comment-repairs.md

  • names scripts/check-installed-spec-pin-claims.mjs → scripts/check-installed-spec-pin-claims.mjs — edited by this change

    Two of the five sites are now watched. They are written as single-line claims about the installed pin, which moves them out of scripts/check-installed-spec-pin-claims.mjs's ledger of known-stale debt and into the population that gate re-derives at every bump; their ledger entries are deleted in the same change, as that gate's both-direction ratchet requires. The remaining three sit on facts no instrument reads, and say so rather than reading as live.

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names views.zod.ts → packages/types/src/zod/views.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face objectui#8229 found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/7997-detail-view-related-retired.md

  • names renderers/record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Why it retired. @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 this array mirrored no protocol schema and drifted freely: it declared columns as TableColumn[] while the renderer it fed also accepted bare field names, { field, label } and legacy { name, label } spellings. The axis that carried the ruling was measured zero pull: no application code authored the member, both internal producers of a detail-view node (RecordDetailDrawer, renderers/record-details.tsx) synthesize it without related, and the only in-tree authorings carrying real columns were two documents — both rewritten here.

.changeset/8394-record-page-emptiness-authority.md

  • names DetailSection.tsx → packages/plugin-detail/src/DetailSection.tsx — edited by this change

    The change. hasCellValue — the predicate objectui#8376 measured into existence — moves out of DetailSection.tsx into a small shared module, and every band above reads it. Its scalar answer is @object-ui/core's recordDisplayValueAt, the same authority the H1 uses, rather than a fifth hand-written test.

.changeset/8400-kanban-name-field-skip-set.md

  • names record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    Deliberately one rung, unlike the same dedupe in record-details.tsx, which also carries deriveTitleField: that ladder filters a synthesized field list, whereas this one filters an author-declared cardFields, where dropping a field the author asked for is a worse failure than a repeated title. A regression test pins both directions, including an object whose declared and derived pointers disagree.

.changeset/8649-detail-renderer-undeclared-keys.md

  • names record-details.tsx → packages/plugin-detail/src/renderers/record-details.tsx — edited by this change

    @object-ui/plugin-detail — the annotation-erasing destructure default is gone from three renderers. record-details.tsx, record-highlights.tsx and record-related-list.tsx each annotated schema correctly and then wrote schema = {} as any. A destructuring default's type joins the annotated property type at the binding, so any erased the annotation for every read site in the file — declared keys and undeclared ones alike read any. No published surface moves: the exported annotations were always correct.

.changeset/9280-record-highlights-entry-icon-retired.md

  • names packages/plugin-detail/src/index.tsx → packages/plugin-detail/src/index.tsx — edited by this change

    • packages/types/src/record-components.ts — RecordHighlightsComponentProps.fields[]'s object arm: { name; label?; icon?; type?; readonly? } → { name; label?; type?; readonly? }. The key is removed, not tombstoned: the contract's arm is $strict, so the refusal an author needs already exists upstream and arrives named (invalid_union at the entry). A ?: never tombstone buys nothing here — it is the remedy for a non-strict mirror that would otherwise strip in silence, which is not this arm. - packages/plugin-detail/src/renderers/record-highlights.tsx — the entry normalizer stops copying icon: f?.icon into the normalized entry. That read was unreachable, not merely unused: no author could feed it past the $strict arm, and HeaderHighlight renders no .icon on the far side either, so the copy had no consumer in either direction. - packages/plugin-detail/src/index.tsx — the registry manifest's fields input description sketched the entry as {name,label?,icon?,type?,readonly?} → {name,label?,type?,readonly?}. The inputs ARE the published contract (gen-manifest.ts serializes them into sdui.manifest.json and sdui-intrinsics.d.ts), so leaving the sketch standing would have gone on teaching AI and human authors a key that gets the whole document refused at publish.

.changeset/record-alert-cta-label-i18n-4998.md

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a claim that was born false (a changeset this change adds is excluded by construction), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with ff29450a9 (merge-base with origin/main): 9 file(s) changed outside .changeset/, read against 1104 pending declaration(s) that publish a body (1608 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3064.6 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DDcQLWdx.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) 544.93KB 130.50KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.35KB 62.93KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
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) 104.82KB 34.67KB
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.04KB 5.36KB
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

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8d8523e0d681fbd6d211a4e2a0a1f7ba1bdda009

① Derived judgments

Ruling executed: objectui#8603 comment 5689202410 (director seat batch #137 item 3, letter A, maintainer 「同意」 2026-09-15T22:57Z). Its words: restore the record:details read of hideEmpty with the behaviour the describe() promises — "true / renderer default: an all-empty section renders nothing; false: heading and skeleton stay"; #7129 Q1-A superseded for this key, Q2-C untouched; dispatch scope names record-details.tsx / DetailSection.tsx, the two pins (the -7129 pin re-routed from closed #7122 to #8603), the manifest text, RENDERER_ONLY_SECTION_KEYS, Clause-② yes, changeset minor. Premise re-derived from the published artifact, not the PR: @objectstack/spec@17.4.0 tarball (shasum 6d6d8dd0…) declares RecordDetailsProps.sections[].hideEmpty among 12 member keys with additionalProperties: false, describe text verbatim as the PR quotes; top-level RecordDetailsProps has no hideEmpty; the reference rail's same-named key is component-level and entries[] does not carry it. Base tree RENDERER_ONLY_SECTION_KEYS did list hideEmpty. CI at this head: all 36 check runs green (Type Check, Lint, all four test shards, Build and E2E, README Export Check, changeset gates). My own clean run from the repo root: 4 files / 56 tests pass; the pin-claims gate exits 0.

Accept-set and public-surface changes the diff implies, one by one:

  1. RecordDetailsRenderer resolves hideEmpty: s.hideEmpty ?? true on each AUTHORED section; DetailSection reads section.hideEmpty === true and, when every field is empty and not editing, filters to zero fields and takes the existing all-fields-hidden early return. Unauthored all-empty authored section: BEFORE (base ff46613, since fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 landed 2026-09-01) renders heading, every label and one placeholder per field; AFTER renders nothing. RIGHT, and within the ruling's words ("renderer default: an all-empty section renders nothing"). Decision 1 ruled below.

  2. hideEmpty: true hides an all-empty section; hideEmpty: false keeps heading and skeleton; neither polarity moves an empty ROW of a partly-filled section (heuristic requires a filled row; the key applies only with none). RIGHT — Q2-C honoured. Residual, the ruling's own carve-out and not the PR's: the describe() also says "Set false to render empty rows", which on a partly-filled section above threshold is still not honoured; the ruling left that branch to the heuristic.

  3. Default placement at the mapping, === true in the component: the direct-fields fallback body and the detail-section node (eight flat inputs, no hideEmpty) keep their skeleton. RIGHT — no declarable spelling exists on either (top-level RecordDetailsProps carries no hideEmpty; DetailSectionNode folds only the eight), and the pre-fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 renderer measured by the spec also left those bodies unforced. Decision 3 ruled below.

  4. !isEditing gate on the all-empty hide. Pre-fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 code: hideEmptyEffective = !showEmptyOverride && (section.hideEmpty || shouldAutoHideEmpty) — the authored branch hid in edit mode too, so this gate is NEW relative to the behaviour the describe() was measured from. RIGHT. Decision 4 ruled below.

  5. packages/types/src/views.ts — DetailViewSection.hideEmpty?: boolean restored. Declaration RIGHT (decision 2). Its docblock WRONG: "Omitted behaves as true" and @default true are false on the detail-view node, the other documented, authorable consumer of this same type (registered with a sections input; README examples author sections on it; DetailView hands each section to DetailSection unchanged, no ?? true on that path). Measured, non-vacuously, at this head: detail-view with an all-empty authored section — unauthored keeps heading and labels, hideEmpty: true hides them, false keeps them, control section rendered in all three. This is the shared-member conditionally-true default tag that objectui#7361 / finding(types): the zod layout mirror substitutes runtime .default() values the renderers never apply — a parsed container renders a different width than an unparsed one #7735 ruled a defect (maintainer 2026-09-09: a docs-vs-implementation mismatch is a docs fix), and it contradicts the PR's own principle that the default belongs to the renderer the key is declared on — the declaration sits on a type two renderers consume and only one applies the default. Fix in scope: drop the tag and the sentence; prose names both consumers.

  6. packages/types/src/zod/views.zod.ts — DetailViewSectionSchema.hideEmpty mirrored, no default. RIGHT (decision 2).

  7. Manifest sections description teaches the key with its all-empty-only scope, the edit-mode exception and Q2-C; never-teach set shrinks to title. RIGHT, ruling-named.

  8. RENDERER_ONLY_SECTION_KEYS drops hideEmpty, live control expect(refused).not.toContain('hideEmpty'). RIGHT, ruling-named. WRONG inside the same hunk: the rewritten comment restates "the spec's section object does not declare it" of showBorder, false on the installed 17.4.0 (the 12 keys include showBorder); the PR body admits showBorder fails the list's criterion and defers to "the next PR that touches this file" — this PR touches it and authored the sentence. Commandment 完善设计器的每一个细节 #9 class; fix in the hunk.

  9. Pins: record-details.emptySectionDefault.test.tsx and record-details.hideEmptyRetired-7129.test.tsx restated with sibling controls, a minted absent probe and a fixture repair (name field added so the H1 ladder does not eat the control's one filled value). RIGHT. Ablation re-run by me on the committed tree, each leg proven on disk and restored by blob equality: delete ?? true → 2 red (UNAUTHORED default; authored-vs-fallback pair); widen to !== false → 1 red (fallback body); delete the DetailSection read → 4 red (4/4 both directions, true hides, unauthored, authored pair). The PR's table is reproduced exactly.

  10. scripts/check-installed-spec-pin-claims.mjs ledger entry 2 → 1 sites for plugin-detail/src/index.tsx @ 17.2.0. RIGHT and mechanically required: with the entry back at 2 the gate exits 1 — "LEDGER count is wrong … entry says 2 site(s), the tree has 1".

  11. record-details.tsx comment WRONG on the record: it says Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064's objection was "no way for a spec-validated page to ask it back, because the spec refused the key". Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 (maintainer 2026-08-31, 「元数据应用不应该考虑这些细节」) objected to the authoring shape itself — "every app hand-writing hideEmpty: false per section … is per-app tax … the shape the maintainer has now ruled against" — knowing objectstack PR #11662 had declared the key. A source comment must not rewrite a maintainer ruling's ground.

Decision 1 — behavioural default. Before: skeleton. After: nothing. AUTHORISED by the ruling's own words (the parenthetical states the renderer default). It REVERSES the maintainer's 2026-08-31 ruling on objectui#7064 (hotcrm#1247 adjudication; deliverable "a sparse record renders a full structural skeleton with ZERO app-side authoring"; landed eeb6c2f), which the #8603 ruling does not name and its prior-rulings sweep did not surface. Standing: a director-seat batch item with maintainer 「同意」 carries the same authority as the 2026-08-31 live ruling, so the reversal stands; it was undisclosed, so the dated-note treatment #7129 received (comment 5689202985) is owed on #7064 by the director seat, not by the dev. Correct on the merits: under the standing 「以 objectstack 协议为准」 line and the ruling that the protocol is correct, the describe()'s stated default is the contract. Consequence the maintainer was not shown: hotcrm#1247's symptom (hand-created records losing all-empty authored sections) returns unless hotcrm authors hideEmpty: false per section.

Decision 2 — file surface. views.ts: MECHANICALLY REQUIRED — DetailSectionProps.section is typed DetailViewSection from @object-ui/types, the ruling-named four-party pin asserts the declaration at compile time (3/4), and superseded Q1-A was "retire the @object-ui/types declaration AND the renderer read" as one act. Alternative: a plugin-detail-local intersection type; worse because the published .d.ts that generators read would then disagree with the spec — the three-answers shape #7129 was filed over — and the four-party pin could not converge. views.zod.ts: REQUIRED — zod-mirror-parity.test.ts fails at compile time on a declared-unmirrored key (#7129's own ablation measured TS2322 there), so the only alternative is a ledger entry recording deliberate drift on a spec-declared key, worse, and the pin's 2/4 asserts the mirror. check-installed-spec-pin-claims.mjs: REQUIRED — the ledger ratchets both ways and the ruling-named manifest rewrite removed one stamped site; proven by the gate going red at 2. The other files are named by the ruling. The changeset is required by the ruling and by check-changeset-presence.

Decision 3 — where the default lives. RIGHT placement. Fallback body and detail-section node: correctly excluded (no spelling to ask the skeleton back; pre-#7123 behaviour identical). detail-view node: behaviour correctly left alone — the ruling does not reach it and the record:details/detail-view split predates #7123 — but the PR's documentation of the shared type is silently inconsistent with it (item 5). The fix is the docblock, not the placement; moving the default into DetailView would be an unauthorised behaviour change on a surface the ruling never named.

Decision 4 — edit-mode gate. NOT in the ruling's text nor in the describe(), and new relative to the measured pre-#7123 behaviour. Authorised as renderer latitude, not by the ruling: the describe() governs the rendered reader view, the same file already applies the identical edit-mode exception to the sibling heuristic, no accept set or authored semantics move, and it is disclosed in the manifest text and changeset. Correct: hiding an all-empty section while its rows are the inputs would put the fields out of reach.

Fifth — the -7129 pin. Editing and re-routing: AUTHORISED explicitly ("its routing target moves from the closed objectui#7122 to this card"). Keeping the filename: within the order's letter (the ruling names the file by this name and says it changes, not that it is renamed) — so authorised. Not correct: the name now asserts the inverted verdict, two pending changesets (7129's and 8603's) cite the same filename for opposite claims, and git log --follow keeps lineage without the name carrying it. Recommendation, not verdict-carrying.

② Semver level

Changeset 8603-record-details-hide-empty-restored.md: @object-ui/plugin-detail: minor, @object-ui/types: minor. Consistent with the diff — both packages' src/ change (renderer, component, manifest, type, mirror); scripts/ and tests are not released surface; Changeset Declaration and Changeset Bump Policy checks green — with AGENTS.md's no-major rule (a breaking behaviour is declared minor with the semantics in the body), and with the ruling's "changeset minor". Body: names the user-visible default move, Q2-C untouched, the edit-mode exception and the untouched reference-rail key — consistent with the diff. One framing to know: "Until this change an all-empty section always rendered its skeleton" is true of main since 2026-09-01, not of the last published release (2026-08-20, before #7123); .changeset/7064-empty-section-default.md and .changeset/7129-retire-detailviewsection-hideempty.md are still pending and publish in the same release, so the net released delta is "key declared upstream and honoured, false now works, default unchanged from the previous release". The "Why it moved twice" paragraph lets a reader reconstruct that; the two older bodies do not (see ③).

③ Boundary flags

  1. Q2-C is a scope line — ANSWERED: verified untouched; the "explicit false is not an override" paradox survives for the partly-filled case by the ruling's own carve-out, and the manifest text says so.
  2. Where the default lives — ANSWERED: placement right; the shared-type docblock wrong (① item 5), correction required.
  3. !isEditing — ANSWERED: renderer latitude, correct, disclosed.
  4. Filename deliberately kept — ANSWERED: authorised, not correct; rename recommended.
  5. Gates hand-derived; check:sdui-registration-pins, check:spec-floors, check:readme-exports not measured locally — ANSWERED by CI at this head: Build and E2E, README Export Check and Bundle Analysis green; nothing red.
  6. Docs claim (no guide or README enumerates section keys) — ANSWERED: git grep hideEmpty over content/docs, docs, package READMEs at this head returns nothing.
  7. showBorder stale in RENDERER_ONLY_SECTION_KEYS, noted not filed — ESCALATED to a required correction in this PR: the PR rewrote the sentence that asserts the falsehood (① item 8).
  8. plugin-detail declares @objectstack/spec ^17.1.0 while the pin asserts 17.3.0+ — ANSWERED as pre-existing: a consumer resolving below 17.3.0 gets the key refused by the spec and honoured by the renderer, the pre-[Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129 divergence one consumer over; check:spec-floors owns it; recommend a follow-up raising the floor to ^17.3.0 (packages/types already declares ^17.4.0).
  9. [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129's dated note exists — ANSWERED: verified, comment 5689202985.
  10. Open, raised by the changeset-claim-re-read gate and unanswered by the PR — ESCALATED, required before landing: pending 7129-retire-detailviewsection-hideempty.md publishes "the spec keeps refusing, the mirror stays absent, and the declaration and the read are retired" and "Migration: delete hideEmpty from any record:details section you author"; pending 7064-empty-section-default.md publishes "a sparse record keeps its section skeleton instead of collapsing". All three bodies go verbatim into one CHANGELOG release. Either correct those bodies (the gate's stated remedy, prose-only, frontmatter untouched) or name the superseded paragraphs explicitly in the 8603 changeset as [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129's did for Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064.
  11. Open, not raised — ESCALATED to the director seat: the ruling reverses Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 without naming it; a dated note on Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 is owed, and the hotcrm#1247 consequence (decision 1) belongs on the card.
  12. Open, not raised — the record-details.tsx account of Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 (① item 11) — required correction.
  13. PR body says "the diff changes no test outside the two pins the ruling names" — inaccurate (the parity test also changes) but that file is ruling-named; noted only.

Implemented-by: claude/issue-8603-record-details-hideempty-restore
Reviewed-by: session_01VCpmqvacV4BypY48QdoxcE

VERDICT: FAIL


Generated by Claude Code

Prose and release notes only — no behavioural line moves, and the review's
ruled-right findings (the read, the `?? true` placement, the `!isEditing` gate,
both pins, the ablation, the ledger edit, the file surface, the semver level)
are untouched.

1. `DetailViewSection.hideEmpty`'s docblock dropped `@default true` and
   "Omitted behaves as `true`". Both are false on the `detail-view` node, the
   other authorable consumer of this same type, which hands each section to
   `DetailSection` with no default applied. Measured here, non-vacuously — a
   sibling control section rendered in all six cases: `detail-view` unauthored
   keeps heading and rows, `true` hides, `false` keeps; `record:details`
   unauthored hides, `true` hides, `false` keeps. The prose now names both
   consumers and which one resolves the default. objectui#7361 / #7735 class.

2. The `RENDERER_ONLY_SECTION_KEYS` hunk no longer says the spec "does not
   declare" `showBorder` — false on the installed 17.4.0, whose section entry
   declares it. The list is restated as what it is, a hand-kept CANDIDATE set
   whose forbidden members are derived per run from the installed schema, so
   neither name's status is written down as a verdict anywhere. Commandment #9.

3. Two pending changesets that contradicted this release are corrected in
   place, prose only, frontmatter and declared packages untouched — the
   correction route, not the rebuttal route. `7129`'s migration step told
   authors to delete `hideEmpty`, an instruction a reader acts on and one that
   would now delete the spelling that keeps a skeleton; `7064`'s "a sparse
   record keeps its section skeleton" survives for the fallback body and the
   `detail-view` node but not for an authored section. Naming them from the
   8603 entry instead would have left that migration step live in the same
   CHANGELOG. `check-changeset-overwrite` reports this as its own case 2 and
   confirms both declarations keep every package name they had at base.

4. The `record-details.tsx` account of objectui#7064 is corrected. Its ruling's
   ground was the AUTHORING SHAPE — an empty detail body is a platform concern
   and an application should not have to author its way out of one, with every
   app hand-writing `hideEmpty: false` per section as per-app tax — made
   knowing the key was declared upstream. The spec-refusal reading came out of
   that card's own execution and was routed onward; it was not the ground. The
   comment now says so, records that this change reverses that ruling's
   behavioural half, and leaves the standing to the review that ruled it.

Also removes the CJK this branch had added to the -7129 pin header, per
AGENTS.md commandment #-1 (English-only codebase). Pre-existing non-English
strings elsewhere in these files are left alone.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3064.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-Cr8GPiz2.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) 544.93KB 130.50KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.35KB 62.93KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.29KB 65.88KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.64KB 57.91KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.98KB 20.99KB
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) 104.82KB 34.67KB
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) 4.11KB 2.06KB
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.04KB 5.36KB
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

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f8388535d91253b5674e8bb946ac5854bd6ec44d

① Derived judgments

Ruling executed: objectui#8603 comment 5689202410 (director seat batch #137 item 3, letter A, maintainer 「同意」 2026-09-15T22:57Z). Its words: restore the record:details read of hideEmpty with the behaviour the describe() promises — "true / renderer default: an all-empty section renders nothing; false: heading and skeleton stay"; #7129 Q1-A superseded for this key, its text not rewritten, Q2-C untouched; dispatch scope names record-details.tsx / DetailSection.tsx, the two pins (the -7129 pin re-routed from closed #7122 to #8603), the manifest text, RENDERER_ONLY_SECTION_KEYS, Clause-② yes, changeset minor. Premises re-derived from the published artifact, not from the PR or the prior record: the @objectstack/spec@17.4.0 tarball (shasum 6d6d8dd0994a6a02ebac6665779341c5a9517719) declares RecordDetailsProps.sections[].hideEmpty among 12 member keys with additionalProperties: false and no schema default, describe text verbatim as the PR quotes; showBorder is among the 12, title is not; top-level RecordDetailsProps carries no hideEmpty; the reference rail's same-named key is component-level and its entries[] carries none. The lockfile resolves 17.4.0. The merge-base is 3ecc369bf (the API's base sha ff46613f6 is main's tip, not the merge-base); the diff against it is exactly the 12 files the PR lists. Run at this head in a clean clone with a fresh install: the two pins and the parity test 3 files / 27 tests green; check-changeset-overwrite, check-installed-spec-pin-claims, check-changeset-presence and check-changeset-no-major all exit 0; ESLint on the nine touched source and test files 0 errors / 89 warnings. CI at this head: all 36 check runs complete, every non-skipped run successful including Type Check, Lint, all four test shards, Build and E2E, README Export Check and the changeset gates (the three skipped are the two coverage variants and dependabot).

Accept-set and public-surface changes the diff implies, one by one:

  1. RecordDetailsRenderer maps each AUTHORED section with hideEmpty: s.hideEmpty ?? true. An unauthored all-empty authored section: BEFORE (merge-base, since fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 landed 2026-09-01) renders heading, labels and one placeholder per field; AFTER renders nothing. RIGHT — the ruling's own parenthetical states the renderer default. Measured with a sibling control that rendered in every case: record:details omitted hides, true hides, false keeps heading, 4 rows, 4 placeholders. Ablation leg 2 (delete ?? true, keep the pass-through) reddens exactly 2 pins.

  2. DetailSection reads section.hideEmpty === true && allFieldsEmpty && !isEditing into hideEmptyEffective, reaching the pre-existing all-fields-hidden early return. true hides an all-empty section; false keeps heading and skeleton; neither polarity moves an empty ROW of a partly-filled section (the heuristic requires a filled row, the key applies only with none; pinned by the three-spellings case on a 2-field fixture). RIGHT — Q2-C honoured, ruling words met. Ablation leg 1 (delete the read) reddens exactly 4 pins. The residual the prior record named stands and is the ruling's own carve-out, not the PR's: the describe()'s "Set false to render empty rows" is not honoured on a partly-filled section above the heuristic threshold.

  3. Default placement at the mapping with === true in the component. The direct-fields fallback body (DetailView synthesizes { fields, columns }) and the detail-section node (DetailSectionNode folds eight inputs — title, description, fields, collapsible, defaultCollapsed, columns, showBorder, headerColor — and no hideEmpty) keep their skeleton. RIGHT — no declarable spelling exists on either surface. Measured: the fallback body over an all-empty record renders 4 rows / 4 placeholders; ablation leg 3 (widen to !== false) reddens exactly that one pin. The prior record's ablation table is reproduced exactly: control 2 files / 11 tests, then 4 / 2 / 1 red, each leg proven on disk before reading, restored by blob equality, final working-tree diff empty.

  4. !isEditing gate on the all-empty hide. Not in the ruling's words nor in the describe(); renderer latitude, correct — hiding an all-empty section while its rows are the inputs would put the fields out of reach; the same file already applies the identical edit-mode exception to the sibling heuristic; no accept set or authored semantics move; disclosed in manifest text and changeset. RIGHT.

  5. packages/types/src/views.ts — DetailViewSection.hideEmpty?: boolean declared: the published .d.ts widens by one optional member. RIGHT and mechanically required (DetailSectionProps.section is typed DetailViewSection; the four-party pin's compile-time leg asserts the declaration). Correction 1 verified on the diff: the JSDoc block immediately preceding this member carries zero @default tags and no "Omitted behaves as true" sentence, against a lit control — showBorder's own block in the same file still carries exactly one @default — with three @default true tags elsewhere in the file, so a file-wide count could not have answered it. The replacement prose's two-consumer account is TRUE, re-derived by rendering rather than read from the PR: detail-view (registered with a sections input; DetailView spreads ...s in its permission filter and hands each section to DetailSection unchanged, no default on that path) with an all-empty authored section and a sibling control that rendered in all three cases — omitted keeps the heading, true removes it, false keeps it (placeholder counts 5 / 1 / 5); record:details — omitted hides, true hides, false keeps. A default tag would have been true of one consumer and false of the other. The compile-time assertion re-derived directly against the source interface: Declares of hideEmpty resolves true, the minted probe key resolves false, and a negative control asserting the probe true fails on TS2344.

  6. packages/types/src/zod/views.zod.ts — DetailViewSectionSchema.hideEmpty: z.boolean().optional(), no default. RIGHT; the mirror pin's 2/4 carries a minted absent control.

  7. Manifest sections description (index.tsx, published into sdui.manifest.json) teaches hideEmpty with its all-empty-only scope, the Q2-C boundary and the edit-mode note; the never-teach set shrinks to title. RIGHT, ruling-named ("the manifest text … follow"). Docs surface measured: zero hits for hideEmpty / showBorder / headerColor under content/docs, docs and package READMEs, against a control of 3 files naming record:details, so the manifest description is the author-facing surface and it moved.

  8. RENDERER_ONLY_SECTION_KEYS drops hideEmpty and the live control expect(refused).not.toContain('hideEmpty') is added. RIGHT, ruling-named. Correction 2 verified on the diff: the sentence asserting "the spec's section object does not declare it" of showBorder is gone; the list is now described as a hand-kept candidate set whose forbidden members are derived per run by filtering through the installed schema. The membership is unchanged (title, showBorder). One imprecision the round introduced, not verdict-carrying: the header still writes both statuses down ("title is refused by the installed spec, showBorder is declared by it", hedged "today") and the body comment repeats them, and then the header says "Neither fact is written down as a verdict anywhere in this file" — the PR body's prediction that "neither name's status is written down as a verdict" is not what the diff does. Both statements are true at 17.4.0, derived by stripped each run, and point at the instrument; the sentence claiming they are not written down is the only false one, and it is a claim about the file, not about the contract.

  9. Pins. record-details.emptySectionDefault.test.tsx and record-details.hideEmptyRetired-7129.test.tsx restated with sibling controls, the minted absent probe and the fixture repair (name declared and unset so the H1 ladder does not consume the control's one filled value). RIGHT, both ruling-named. Filename kept: authorised (the ruling names the file by this name and says it changes); the rename recommendation from the prior record stands as a recommendation only. Routing moved to finding(plugin-detail): #7129 retired hideEmpty on the premise that the spec refuses it — the pin moved to spec 17.3.0 six days later and 17.3.0 DECLARES it, with a describe() promising the behaviour this repo removed #8603: When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122 verified closed completed 2026-09-07 on CalendarConfig titleField. Patch round: the five CJK spellings the first head had added to the -7129 pin (「同意」, 「the renderer is unchanged」, 总监批) are removed; net CJK across the whole branch against the merge-base is 0 lines added / 2 removed, control 293 CJK lines in AGENTS.md. Those 2 removed lines were pre-existing "总监批 Implement UI components from @object-ui/types #28" spellings in that pin converted to English, so the PR body's "pre-existing non-English strings in these files are left alone" is inaccurate for that one file — the conversion is commandment #-1 compliance, not a defect; the CJK in index.tsx (1), record-details.tsx (1) and views.ts (3) is untouched.

  10. scripts/check-installed-spec-pin-claims.mjs — ledger entry for plugin-detail/src/index.tsx at 17.2.0 goes 2 sites to 1. RIGHT and mechanically required: measured red with the entry set back to 2 ("entry says 2 site(s), the tree has 1"), green at 1, restored.

  11. record-details.tsx comment — correction 4 verified against objectui#7064 read first-hand: the card is filed on the maintainer's 2026-08-31 ruling (quoted verbatim there, a platform-concern statement), its body says objectstack PR #11662 "then made hideEmpty an AUTHORABLE opt-out — which is the shape the maintainer has now ruled against", its deliverable 2 keeps "authored hideEmpty: true (declared by objectstack PR #11662)" honoured, and the spec-refusal reading appears only in that card's LANDED comment as a measurement on 17.2.0 "routed on [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129". objectstack #11289 is the upstream issue and #11662 the PR that declared the key, both closed 2026-08-24. The comment's account — objection to the authoring shape, made knowing the key was declared upstream, spec-refusal reading arriving later from execution — is RIGHT. The comment also records that the reversal's standing "was ruled on this change's isolated at-tier contract review (Decision 1)": an accurate account of what happened on this PR's thread, and it expressly leaves whether the reversal should stand to that review rather than arguing it. The ruling of record for that reversal is still the dated note owed on Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 by the director seat, which does not yet exist (see ③).

  12. Two pending changesets — correction 3, judged on the diff. 7129-retire-detailviewsection-hideempty.md: the migration step "delete hideEmpty from any record:details section you author" is replaced wholesale by "Migration: none — superseded inside this same release. Do NOT delete hideEmpty from your sections", and a supersession paragraph is inserted after the four-contracts paragraph. 7064-empty-section-default.md: a header paragraph states what survives (fallback body and detail-view node skeleton, label-graveyard guard) and what does not (the authored record:details default), and the all-empty bullet is annotated. Frontmatter byte-identical base vs head in both files; check-changeset-overwrite reports both under its case 2 with "declared at base" equal to "declares now" (@object-ui/plugin-detail: minor for 7064; @object-ui/types: minor, @object-ui/plugin-detail: minor for 7129). Route: correcting the bodies is RIGHT — the 7129 paragraph was an instruction a reader performs, which a rebuttal in a sibling entry does not stop, and it is the remedy the repo's own changeset-claim-re-read gate names ("correct the body … prose-only, frontmatter untouched"). Authority: the finding(plugin-detail): #7129 retired hideEmpty on the premise that the spec refuses it — the pin moved to spec 17.3.0 six days later and 17.3.0 DECLARES it, with a describe() promising the behaviour this repo removed #8603 ruling does not reach these two files; the edits are authorised by the repo's standing changeset-claim rule and commandment 完善设计器的每一个细节 #9, the same footing as the ledger and mirror edits, and they are pending changesets rather than a published CHANGELOG, so commandment [WIP] Update documentation for project #11's historical-record prohibition does not apply; the [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129 ruling text is not rewritten. Residual, noted not verdict-carrying: in 7129 the paragraph below the inserted supersession note still reads "an authored hideEmpty of either polarity is now inert, and the release notes should read that way" — the note says "the paragraphs above" and so does not reach it, and the corrected Migration paragraph two below contradicts it; in 7064 the closing "No application metadata needs editing — that is the point of the change" is likewise only framed by the new header. Neither is an instruction a consumer performs, and both entries now state what ships; a one-line touch on each would close the contradiction.

  13. .changeset/8603-record-details-hide-empty-restored.md — patched to state the net delta against the last PUBLISHED release and to name the two corrections. The claim is TRUE: the last release is 17.6.0 dated 2026-08-20; Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 landed 2026-09-01 and its changeset is pending at the merge-base, so the pre-Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 forced default is what the last release shipped and the authored-section default is unchanged release-to-release.

Decision 1 (behavioural default) — authorised by the ruling's own words; reverses the 2026-08-31 ruling on #7064 without naming it; standing as the prior record derived it. Decision 2 (file surface) — every file either ruling-named or mechanically required, re-derived above. Decision 3 (placement) — RIGHT, measured. Decision 4 (edit-mode gate) — renderer latitude, RIGHT.

Fifth — nothing the earlier record ruled right was disturbed. Checked by reading the patch-round diff 8d8523e0d..f8388535d (7 files, 294 lines) and mechanically: zero non-comment lines added or removed in any .ts / .tsx file. The renderer read hideEmpty: s.hideEmpty ?? true is present; === true and !isEditing are present in DetailSection; the default's placement is unchanged (ablation 4 / 2 / 1 reproduced); both pins carry the same assertions and pass; the ledger edit is unchanged and live; the file surface is the first head's 10 files plus exactly the two pending changesets the prior record required; the semver level is unchanged.

Sixth — what this round introduced, each judged: the two changeset body corrections (item 12); the docblock rewrite (item 5, measured true); the parity-test comment rewrite (item 8, with the self-referential sentence noted); the record-details.tsx comment rewrite (item 11); the 8603 changeset's release-delta paragraph (item 13, measured true); the CJK removal (item 9). Nothing unauthorised, nothing behavioural.

② Semver level

Changeset 8603-record-details-hide-empty-restored.md declares @object-ui/plugin-detail: minor and @object-ui/types: minor. Consistent with the diff: check-changeset-presence counts 8 published source files across exactly those 2 released packages; scripts/ and tests are not released surface; the two corrected pending changesets keep their declared packages and levels. Consistent with AGENTS.md's fixed-group rule (breaking behaviour declared minor with the semantics in the body; check-changeset-no-major green) and with the ruling's "changeset minor". The body names the user-visible default move, the net release-to-release delta, Q2-C untouched, the edit-mode exception, the untouched reference-rail key and the two in-place corrections — every one of those matches the diff.

③ Boundary flags

  1. Q2-C as a scope line — ANSWERED: verified untouched; the heuristic's !isEditing and filled-row conditions are intact and the three-spellings pin holds on the partly-filled fixture; the partly-filled false case remains the ruling's carve-out.
  2. Where the default lives — ANSWERED: placement right, measured; the shared-type docblock now correct and re-derived on both consumers.
  3. !isEditing — ANSWERED: renderer latitude, correct, disclosed.
  4. Filename deliberately kept — ANSWERED: authorised; rename remains a recommendation.
  5. Gates hand-derived; check:sdui-registration-pins, check:spec-floors, check:readme-exports not measured locally — ANSWERED by CI at this head: Build and E2E, README Export Check and Bundle Analysis green; nothing red across all 36 runs.
  6. Docs claim — ANSWERED: measured zero hits with a live control.
  7. showBorder in RENDERER_ONLY_SECTION_KEYS — RESOLVED in this round; the residual self-referential sentence is noted in ① item 8 and is not verdict-carrying.
  8. plugin-detail declares @objectstack/spec ^17.1.0 while its pin asserts a 17.3.0+ key — ANSWERED as pre-existing and outside this diff (packages/types already declares ^17.4.0); a follow-up raising the floor is recommended, owner check:spec-floors.
  9. [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129 dated note — ANSWERED: comment 5689202985 by the director seat exists on [Decision] hideEmpty on record:details sections: the spec REFUSES the key, @object-ui/types declares it, the zod mirror omits it, and the renderer honours it — plus hideEmpty: false is not an override #7129 and does not rewrite the ruling text.
  10. Dated note owed on Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 and the hotcrm#1247 consequence on the card — ESCALATED to the director seat, not the implementer's: no director-seat note exists on Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064 at review time (the only 2026-09-16 comment there is a PM-seat pointer that itself says the note is owed).
  11. "No behavioural line moved in the patch round" — ANSWERED: verified mechanically.
  12. "Net CJK introduced is zero" — ANSWERED: verified, with the one PR-prose inaccuracy about pre-existing strings noted in ① item 9.
  13. Two pending changesets corrected in place — ANSWERED: route right, frontmatter and package names intact, overwrite gate case 2; residual contradictory sentences noted in ① item 12, recommended for a one-line touch, not blocking.
  14. PR body's "neither name's status is written down as a verdict anywhere in the file" — ANSWERED: the diff does write both down, hedged and pointed at the derivation; noted in ① item 8.

Implemented-by: claude/issue-8603-record-details-hideempty-restore
Reviewed-by: session_01VCpmqvacV4BypY48QdoxcE

VERDICT: PASS


Generated by Claude Code

hotlong commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

清标 provenance —— 条款②双载体已剥(第二轮复核,PASS)

记录:PR 评论 5700911252 · 所判 head f8388535d91253b5674e8bb946ac5854bd6ec44d · VERDICT: PASS · 本席逐字转贴,⛔ 未改一字。

⭐ 这是本班唯一走完 FAIL → 补丁轮 → 重审 → PASS 全程的卡,值得把过程记清楚:

轮次 head 裁决 处置
一审 8d8523e0d6 FAIL 四条欠改回 dev;按 contract-review.md:19 FAIL 同 PASS 剥双载体,而卡态与 assignee 不动
补丁轮 → f8388535d9 — head 一动,一审记录即作废 ⇒ 双载体重挂到新 head
二审 f8388535d9 PASS 本次清标

⚠️ 中间那一步是最容易漏的:一审记录点名的是 8d8523e0d6,补丁一推就不再适用。本席重挂载体后跑 --pair,输出里 C6-RECORD 行消失 —— 那是「这个 head 上没有复核记录」的机读确认,不是本席的断言。二审记录落上去之后它才重新出现。

档位:本席独立量的

agent-abc7e9cf94b8a938a.jsonl · type:"assistant" 的 message.model
→ claude-fable-5-1 ×87  (87/87,无杂值,16:09:10Z–16:26:43Z)

同题发火对照(同一次扫描,最近 6 个子代理):claude-opus-5 ×3 / claude-fable-5-1 ×3,分界与派发所传 model 完全重合。⇒ 仪器能分辨。

⭐ 二审不是复读一审。 简报里写死了「PR 上那条 FAIL 记录判的是已被取代的 head,⛔ 不许继承,要在新 head 上自己重推」。它照做了,并逐条复算:消融三腿 4/2/1 独立复现、detail-view 与 record:details 两个消费者各三种写法带对照渲染、编译期断言用自铸探针加反向对照(断言探针为真时必须 TS2344 失败)。

落地前检三条

闸门 读数
① 席内条款②复核 PASS 在案 上述记录
② 双载体已清 + 机读 剥标后取的落地读数:--pair 9627 exit 0(剥标前也 exit 0)
③ 全部 check head 上 36 个:33 success / 3 skipped / 0 非绿 / 0 在跑

受管面:check-governed-merges.mjs --test 对 PR 的 12 个真实路径 → 0 of 12,NOT governed。路径取自 /pulls/9627/files(GitHub 自己的三点语义),⛔ 不是本地二点差。

复核件点名的非阻断残留,⛔ 不改变 PASS,另行立卡

  1. 两份被更正的待发 changeset 里各留有一句与新结论相抵的旧话(7129 的「an authored hideEmpty of either polarity is now inert」、7064 的「No application metadata needs editing」)—— 新加的说明段没盖住它们。⚠️ 这两句会逐字进 CHANGELOG。
  2. 本轮改写的 parity 测试头里有一句自指的假话:它写「Neither fact is written down as a verdict anywhere in this file」,而同一段其实把两个状态都写下来了(带 today 的限定)。契约无碍,文件自述有误。
  3. PR 正文说「这些文件里既有的非英文串原样保留」对 -7129 pin 不准确 —— 那里有 2 行既有中文被转成英文了。那是 commandment #-1 合规,不是缺陷,但正文的说法与 diff 不符。
  4. plugin-detail 声明 @objectstack/spec ^17.1.0 而其 pin 断言 17.3.0+ 的键(packages/types 已声明 ^17.4.0)—— 既有问题,归 check:spec-floors,建议另起一卡把地板抬到 ^17.3.0。

⛔ 一条不归本 PR、也不归 dev

复核件两轮都点名:本裁决反转了 2026-08-31 维护者对 objectui#7064 的裁决而未点名它,#7064 欠一份 #7129 拿到过的那种带日期备注,归总监席。二审复核时在 #7064 上找不到这样的备注(唯一那条 2026-09-16 的评论是本席留的指路,它自己就写着备注仍欠)。⇒ 仍然欠着。

— PM domain:spec · session session_01VCpmqvacV4BypY48QdoxcE · 清标时刻 2026-09-16T16:29Z


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 16, 2026 16:29
@hotlong
hotlong added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 542718f Sep 16, 2026
38 checks passed
@hotlong
hotlong deleted the claude/issue-8603-record-details-hideempty-restore branch September 16, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants