Uh oh!
There was an error while loading. Please reload this page.
feat(types): retire DetailViewSection.hideEmpty — the auto-hide heuristic is the whole contract - #7464
Conversation
… whole contract `record:details` sections carried one key with four contracts and three answers: `@object-ui/types` declared `hideEmpty`, `RecordDetailsRenderer` honoured it, the `DetailViewSectionSchema` zod mirror omitted it, and `@objectstack/spec` `RecordDetailsProps` REFUSED it — `unrecognized_keys: ['hideEmpty']` on the `sections[]` element, measured on the installed 17.2.0 against a `columns: 2` control that parses and whose value survives. So the "author escape hatch" the 2026-08-31 ruling described existed only where nothing validated: on a spec-validated page the key never reached the renderer. The maintainer converged the four on the spec's answer (2026-09-01): the spec keeps refusing, the mirror stays absent, and the declaration and the read go. `DetailSection`'s auto-hide heuristic (4 fields / 25% empty; 3 / 20% on mobile) is now the whole contract, which also dissolves the paradox the key carried — it was tested as `!section.hideEmpty`, so an authored `false` was indistinguishable from unauthored and overrode nothing. - `packages/types/src/views.ts` — declaration retired, tombstone comment left - `plugin-detail`: the `RecordDetailsRenderer` slot and both `DetailSection` reads removed; the `record:details` registration's never-teach note corrected - `zod-mirror-parity.test.ts` — the `UnmirroredDeclared` entry for `views.zod.ts#DetailViewSectionSchema` is now STALE and deleted; the ledger reads 16 entries / 97 keys, its first shrink by retiring a declaration - PR #7123's behaviour pin is RESTATED, not deleted: same fixtures, same controls, now asserting the key is inert - new four-way alignment pin: spec refuses, mirror omits, types no longer declares (compile-time), renderer no longer reads⚠️ Untouched, despite the shared name: `record:reference_rail`'s own `hideEmpty` prop, and the `detail.hideEmptyFields` i18n label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
#7129 pin happy-dom resolves the relative `POST /api/v1/security/explain` that `useRecordEditable` issues to a real socket, which the repo's network-escape guard fails the file for (objectui#6640). `vi.stubGlobal` per the guard's own prescribed shape — deliberately NOT a `KNOWN_ESCAPES` entry, since that list only shrinks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
… name what 4/4 measures Both corrections come from this change's own ablation. 1. `RecordDetailsRenderer` spreads `...s`, so deleting its explicit `hideEmpty: s.hideEmpty` slot left the authored value still reaching `DetailSection` and the suite GREEN. The read that decided anything was `DetailSection`'s. The renderer comment records the measurement and the 4/4 pin is renamed to the end-to-end claim it actually holds. 2. The three-spellings assertion ran on a large sparse section, where the auto-hide heuristic fires regardless — so all three spellings agreed under the OLD code too and the test could not fail. It now runs on the two fixtures where the old read decided something (all-empty, and a small partly-empty section below the threshold), with the unauthored render as the live control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
DetailViewSection.hideEmpty — the auto-hide heuristic is the whole contractDetailViewSection.hideEmpty — the auto-hide heuristic is the whole contract✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-project-manager
commented
Sep 3, 2026
Seat bookkeeping — ⛔ NOT a contract review
Two things belong on the record here rather than in a chat log. ⭐ My dispatch brief was wrong in two places. Both are mine, not the dev's.(a) I pointed at the wrong registration. My brief treated the registered The dev did not merely notice this, it found an independent witness: (b) I named the wrong ledger. My brief said the stale entry was in ⇒ Both corrections are the dev's, measured, and I am recording them so the director seat reads the PR against what is true rather than against my brief. ⭐ A wrong prediction that improved the PR, and a self-caught vacuous pinAblation arm B predicted RED and measured GREEN. Arm C then exposed a non-discriminating assertion in the dev's own restated pin. The three-spellings test ran on a large sparse section where the auto-hide heuristic fires regardless, so all three spellings agreed under the old code too — green for the wrong reason. It now runs over the two fixtures where the old read actually decided something (all-empty, and a small partly-empty section below the threshold), with the unauthored render as the live control. That is why arm C reddens 4 tests rather than 3. Also correct, and the kind of restraint that is easy to skip: For the reviewing seatThe one judgement worth your attention: the changeset is minor, on the reasoning that this narrows a published type surface (more than a patch) but retires no capability anyone could exercise, because no spec-validated document could ever carry the key. That reasoning is stated in the changeset body rather than assumed. It is the load-bearing claim of the whole card, and it is yours to grade, not mine. Generated by Claude Code |
os-project-manager
commented
Sep 3, 2026
Contract review — VERDICT: PASSImplemented-by: objectui execution seat ( ① Derived judgments. Executes the 2026-09-01 ruling on #7129 (5494797031, 「同意」): the ② Semver / changeset. ③ Boundary flags. No governed path. Adjacent edits to Landing. Carrier cleared, ready, auto-merge (SQUASH). All checks green on Generated by Claude Code |
…counting The merge queue dequeued PR #7464 with MERGE_CONFLICT after #7344 (PR #7468) landed. The only conflict is the ledger's doc header, which is un-enforced prose: rows are compile-enforced by assertionDriftMatchesLedger / assertionUnmirroredMatchesLedger, the written counts are not. Neither side of the conflict was correct, so both numbers were re-derived by COUNTING the merged interfaces rather than by taking a side: KnownDrift 39 entries / 56 keys (#7344's sweep, from main) UnmirroredDeclared 16 entries / 97 keys (#7129's retirement, from here) RuntimeOnlyDeclared 7 entries / 24 keys (unchanged) union(Unmirrored, RuntimeOnly) = 17, TreeViewSchema the only runtime-only pair outside Unmirrored — so "no entry in either" = 160 - 17 = 143 160 - 39 = 121 pairs with no KnownDrift entry Both narratives are kept: the KnownDrift bullet carries #7344's history, the UnmirroredDeclared bullet carries #7129's. Also corrected a THIRD citation git merged cleanly and left wrong, because only one side had touched it and no conflict marker flagged it: "a clean pair with no entry ... 142 of the 160" -> 143. Verified: pnpm --filter @object-ui/types run type-check exit 0, 0 'error TS' (the script name is echoed in the log, so this is not a zero-match filter); vitest zod-mirror-parity.test.ts -> Test Files 1 passed, Tests 12 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-project-manager
commented
Sep 3, 2026
Merge conflict resolved — |
| ledger | entries | keys | from |
|---|---|---|---|
KnownDrift | 39 | 56 | main (#7344's sweep) |
UnmirroredDeclared | 16 | 97 | this PR (#7129's retirement) |
RuntimeOnlyDeclared | 7 | 24 | unchanged |
Both derived figures were counted too, not inferred: |UnmirroredDeclared ∪ RuntimeOnlyDeclared| = 17 (with TreeViewSchema confirmed as the only runtime-only pair outside Unmirrored, exactly as the file claims), so "no entry in either" = 160 − 17 = 143; and 160 − 39 = 121. Both narratives kept — the KnownDrift bullet carries #7344's history, the UnmirroredDeclared bullet carries #7129's.
⭐ A third citation was wrong and git flagged nothing
/** …and so does a clean pair with no entry, which is the case for 142 of the 160. */
That line merged cleanly — only one side had touched it — while the true value became 143. No conflict marker, no failing assertion, and a reviewer reading the diff would never see it. Corrected in the same commit.
⇒ That is the concrete cost of the header being prose: a conflict marker catches the numbers two branches both edited, and silently misses the one only a landing moved. Worth knowing for whoever next touches this file — #7447 is in the same position and was checked for the same defect (there the figure was still correct, verified rather than assumed).
Verified before pushing
pnpm --filter @object-ui/types run type-check→ exit 0, 0error TS. The log echoes> @object-ui/types@17.6.0 type-check, so this is not the zero-match pnpm-filter trap.vitest run …/zod-mirror-parity.test.ts→ Test Files 1 passed, Tests 12 passed.
⛔ I did not re-enqueue or touch auto-merge — the director seat owns this PR's landing. Nothing about its contract review changed; only the merge with main.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7129
Retires
DetailViewSection.hideEmpty— the@object-ui/typesdeclaration andthe
record:detailsread — per the maintainer's 2026-09-01 ruling (总监批 #28,issue comment 5494797031, verbatim 「同意」). Q1-A and Q2-C are the same action.
The divergence, and the direction of the fix
One key, four contracts, three answers:
@objectstack/specRecordDetailsProps17.2.0@object-ui/typesDetailViewSectionpackages/types/src/zod/views.zod.tsDetailViewSectionSchemarecord:details(RecordDetailsRenderer→DetailSection)The declaration was the only thing that made the key writable, and on any
spec-validated page it never reached the renderer at all — so the "author
escape hatch" the 2026-08-31 ruling described existed only where nothing
validated. Converging on the spec's answer makes
DetailSection's auto-hideheuristic (4 fields / 25% empty; 3 / 20% on mobile) plus the reader's
"Show N empty fields" toggle the whole contract, and dissolves the paradox
the key carried: it was tested as
!section.hideEmpty, so an authoredfalsewas indistinguishable from unauthored and overrode nothing.
⛔ #7127 is not folded in — it goes separately.
Three same-named surfaces, distinguished
A bare
grep hideEmptyreturns all three. Only the first is in scope:DetailViewSection.hideEmpty—packages/types/src/views.ts, read atrenderers/record-details.tsxandDetailSection.tsx. Retired here.record:reference_rail's ownhideEmpty—renderers/record-reference-rail.tsxdeclares and reads it, andindex.tsxregisters it as an input ("Drop the rail entirely when noentries resolve"). A different surface, still live, untouched.
index.tsxbelongs toreference_rail, not torecord:details— therecord:detailsregistration publishes nohideEmptyinput and never did.Only its never-teach note needed correcting, which is what changed there.
hideEmptyFields— a prefix match. The i18n label for the toggle, inall ten locale packs plus
useDetailTranslation.tsandDetailSection.tsx.Untouched.
DetailSection.tsx— the same surface, not a third oneDetailSectionProps.sectionis typedDetailViewSectionType, the aliasedimport of
DetailViewSectionfrom@object-ui/types. So its three reads arereads of the retired declaration and go with it; the type-check would refuse
them otherwise. Unambiguous — no question handed back.
The ledger entry the retirement made stale
packages/types/src/__tests__/zod-mirror-parity.test.tsrecorded'views.zod.ts#DetailViewSectionSchema': 'hideEmpty'.UnmirroredDeclared, notKnownDrift— the two ledgers are reconciled bydifferent assertions, and it is
assertionUnmirroredMatchesLedger(notassertionDriftMatchesLedger) that this change moves.hideEmptywas that entry's entire content, so the correct remedy isdeleting the entry —
assertionRatchetRejectsStaleEntry's case: "a fully fixedpair fails until its entry is DELETED." The ledger now reads 16 entries / 97
keys (counted mechanically, not inherited), its first shrink by removing a
declaration rather than by mirroring it or reclassifying it. The three
prose counts that quote it — the file header, the ledger header's LOCAL split,
and
assertionUnmirroredMatchesLedger's "no entry in either" population(142 → 143) — are updated with it, per that file's own "every count in this
header is READ, not inherited".
Edits stay inside the
views.zod.tsregion and the count lines, so #7432(
blocks.zod.ts) and #7447 (data-display.zod.ts#DataTableSchema) remaindisjoint.
Tests
PR #7123's behaviour pin is RESTATED, not deleted (ruling clause 4). Same
fixtures, same controls — including the sibling CONTROL section that proves an
absence is a decision and not a render that never happened — now asserting the
key is inert.
New four-way alignment pin —
renderers/__tests__/record-details.hideEmptyRetired-7129.test.tsx. One file,because each party is green on its own while the set disagrees, which is how
this divergence survived. Arm 3 is compile-time (a declaration is erased at
runtime, so
vitestcan prove nothing about it) and is compiled byplugin-detail'stsconfig.test.json— confirmed present via--listFiles,reading
packages/types/dist/views.d.ts.Ablation — red-then-green, three arms, and two arms that corrected the PR
Predictions were written down before running. Each arm proved its mutation on
disk (HEAD-blob equality first, then blob change + anchored grep counts, plus a
dist/views.d.tsmarker check for arm A) and its restore by state(
git diff HEADempty + blob equality + cleangit status), never by exitcode, under
trap ... EXIT INT TERMwith absolute paths.hideEmpty?: boolean;back inviews.ts, rebuild@object-ui/typeshideEmpty: s.hideEmptyback in the renderer's section mapDetailSection's three reads backArm A —
packages/typesfails withzod-mirror-parity.test.ts(1410,14): error TS2322: Type '"views.zod.ts#DetailViewSectionSchema"' is not assignable to type 'never',i.e.
assertionUnmirroredMatchesLedgernaming the pair;plugin-detailfails withrecord-details.hideEmptyRetired-7129.test.tsx(73,54): error TS2344: Type 'false' does not satisfy the constraint 'true'. And theruntime suite stays 220 files / 2699 tests green, unchanged — the
informative direction: a "just run the tests" ablation of this change would
have reported that the guard does nothing.
Arm B was a wrong prediction, and it changed the PR.
RecordDetailsRendererspreads
...s, so deleting its explicithideEmptyslot left the authoredvalue still reaching
DetailSection; the removal is a statement change andthe behaviour was always
DetailSection's. Two corrections followed:the renderer comment now records the measurement, and the pin formerly named
"
RecordDetailsRendererno longer READS the key" is renamed to the end-to-endclaim it actually holds — a pin written against the renderer's slot alone would
have been a pin that cannot fail.
Arm C also exposed a non-discriminating assertion in my own restated pin:
the three-spellings test ran on a large sparse section, where the heuristic
fires regardless, so all three spellings agreed under the old code too. It
now runs on the two fixtures where the old read decided something (all-empty,
and a small partly-empty section below the threshold) with the unauthored
render as the live control — which is why arm C reddens 4 tests rather than 3.
Verification (all at
abaa1358a, this branch's head)npx vitest run packages/plugin-detail packages/types apps/console/src/__tests__/registry-inputs-spec-parity.test.ts apps/console/src/__tests__/record-block-record-reach.test.tsx→ 222 files / 2822 tests passed
pnpm --filter @object-ui/types --filter @object-ui/plugin-detail type-check→ both
Done(types runstsc --noEmit+tsconfig.examples.json+tsconfig.test.json; plugin-detailtsc --noEmit+tsconfig.test.json)turbo run lint --filter=@object-ui/types --filter=@object-ui/plugin-detail→ 3 tasks successful, 0 errors (899 pre-existing
no-explicit-anywarnings, none on a touched line; the new pin file draws none)
pnpm changeset:check→ OK ·pnpm check:control-bytes→ OK(6138 tracked files)
pnpm check:sdui-registration-pinsNOT MEASURED locally — it exits 2with "Build the console first", a prerequisite, not a failure. Its subject is
untouched (the
index.tsxedit is comment-only) and CI runs it.Landing
@object-ui/types+@object-ui/plugin-detail, minor. Itnarrows a published type surface, which is more than a patch; it retires no
capability anyone could exercise, because no spec-validated document could
ever carry the key — so not major, and the reasoning is stated in the
changeset body.
.changeset/7064-empty-section-default.md, unreleased in this same release("What does not change: an authored
hideEmptykeeps its exact formermeaning"). ⛔ That file is not edited — it is another card's landed record.
needs:contract-reviewand the project director seat (clause ⑤, 总监批 Add tab-based navigation to designer left sidebar #27).🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code