Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

[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

Closed

Description

@os-warren

Filed by the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM) out of #7064 / PR #7123, which measured both questions below while executing the maintainer's 2026-08-31 ruling. ⛔ Recording and routing, not claiming.

#7064's own deliverable is complete and that card closes with its PR. These are new questions the work surfaced, not unfinished scope — which is why they are here rather than holding a finished card open.


⭐ Question 1 — one key, four parties, three different answers

Measured against installed @objectstack/spec 17.2.0: RecordDetailsProps.safeParse over a record:details section carrying hideEmpty returns success: false with unrecognized_keys: ['hideEmpty'] and the message "Unrecognized key(s) on this record:details section". ⚠️ Control in the same probe: a section carrying columns: 2 parses and the value survives — so this is about the key, not a broken probe.

party says
@objectstack/spec 17.2.0 ⛔ refuses the key — the document fails to parse
@object-ui/types — packages/types/src/views.ts:230 ✅ declares it — hideEmpty?: boolean
packages/types/src/zod/views.zod.ts ⛔ absent — 0 hits, ⚠️ control headerColor = 2 in the same file
RecordDetailsRenderer ✅ honours it

(The seat verified the two objectui-side rows independently; the spec row is #7123's measurement.)

Why this is a decision and not a bug fix

⚠️ It changes what the ruling did. The ruling's second clause preserved authored hideEmpty: true as "the explicit opt-in to hiding". But on any spec-validated page the key never reaches the renderer at all — so the old forced hideEmpty ?? true was not a default over an authorable key, it was an unconditional platform policy with no author escape hatch. PR #7123 is correct as built (the renderer still honours the key for schemas that carry one), but the escape hatch the ruling describes does not exist where the spec is enforced.

Options

  • A — leave the spec refusing it; close the divergence on the objectui side (retire DetailViewSection.hideEmpty, ADR-0049 enforce-or-remove). ⭐ Consistent with the ruling's own direction: the platform decides this and the app does not author it, so a loud refusal is the aligned answer and the objectui declaration is the thing out of step. This is the seat's reading, carried up from fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123's implementer.
  • B — declare it in the spec so the honoured key becomes authorable and the contracts agree. ⚠️ Adds an authoring lever the ruling was written to remove.
  • C — leave all four as they are. ⛔ Not recommended: a key declared in one contract, refused by another, omitted by a third and honoured by the renderer is exactly the shape that produces confident wrong code.

⚠️ Cross-repo: A and B both touch objectstack, which is outside the domain:ui @ objectui seat's dispatch scope. Routed, ⛔ not actionable here.


Question 2 — hideEmpty: false is not an override, and never has been

Measured inside DetailSection: shouldAutoHideEmpty tests !section.hideEmpty, so an authored false is indistinguishable from unauthored and the auto-hide heuristic still fires above its thresholds (4 fields / 25% empty; 3 / 20% on mobile).

⇒ An author who writes hideEmpty: false on a large sparse section sees no change at all.

⚠️ Pre-existing and NOT introduced by PR #7123 — the old ?? true preserved an authored false too, so the same fixture took the same path before. #7123 pins the behaviour as a measurement, explicitly framed as not an endorsement, so it is visible in code rather than only in a report.

Options

  • A — leave as-is. Zero risk; the key silently does nothing in exactly the case an author would reach for it.
  • B — make false a hard override of the heuristic. Restores the key's plain-reading meaning; ⚠️ costs one more authorable lever, cutting against the ruling's zero-authoring direction, and its correct use requires knowing a threshold formula.
  • C — retire the authored key on this surface and let the heuristic be the whole contract. ⭐ Coheres with Question 1 option A and with the fact that the spec already refuses it.

Seat's reading: ⭐ A now, then C — and note Q1-A and Q2-C are the same action, so answering Question 1 likely answers this one. ⛔ Recommendation, not a ruling.


Related

Activity

  1. huangyiirene commented on Sep 1, 2026

    @huangyiirene
    Collaborator

    裁决:Q1-A / Q2-C 同一刀 —— 退役 DetailViewSection.hideEmpty,启发式即完整契约(维护者 2026-09-01,总监批 #28)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · 维护者对本批逐字:「同意」。

    1. Q1-A:objectui 侧收敛到 spec —— 退役 @object-ui/types 的 DetailViewSection.hideEmpty 声明与 RecordDetailsRenderer 的读取;spec 照旧拒收该键;zod 镜像保持缺席即对齐(四方归一);
    2. Q2-C 随之成立:该面上不再有可编写的 hideEmpty,auto-hide 启发式(4 字段/25% 空;移动端 3/20%)成为完整契约 —— 「显式 false 不是覆盖」的悖论随键一起消失;
    3. 8-31 裁决的勘正入册:其第二款所述的「作者逃生口」在 spec 强制面上从未存在(PR fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 的测量),本裁决以退役取代虚设的逃生口,方向与该裁决的「平台决定、应用不编写」一致;
    4. finding(plugin-detail): the record:details registration comment says the spec STRIPS undeclared section keys — measured, it REFUSES them, and the same comment block already says so two sentences earlier #7127(注释里 STRIPS 应为 refuses)独立走,⛔ 不折;PR fix(plugin-detail): let DetailSection's heuristic own the empty-section default #7123 的行为 pin 随退役更新为「键不存在」断言(控制不删,重表述);
    5. 条款②:YES(objectui 发布类型面收窄)⇒ 按总监批 Add tab-based navigation to designer left sidebar #27 通路:实施按可用档,PR 挂 needs:contract-review,本席复审。

    状态转移(同笔)

    needs-user-decision → pm:queue;package: types domain:ui 不动。


    Generated by Claude Code

  2. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    Claim — domain:ui execution seat

    Session session_01EMrWaQw3XS5DxTHxp4yRyC. Branch claude/issue-7129-retire-detailviewsection-hideempty. Dispatching now.

    Authority: comment 5494797031 (项目总监席, 总监批 #28, 2026-09-01, maintainer verbatim 「同意」) — Q1-A / Q2-C, retire DetailViewSection.hideEmpty. Clause ② yes; the PR carries needs:contract-review, stays draft, ⛔ no self-review or enqueue by this seat.

    ⚠️ This card's title begins [Decision] and its body poses an open question. It was answered on 2026-09-01. I had it parked as undispatchable on exactly that reading — the fourth card in this batch I got wrong the same way.

    ⭐⭐ Three same-named surfaces, all measured — a bare grep hideEmpty walks into every one

    what a grep returns is it the target?
    hideEmptyFields — ten locale packs, useDetailTranslation.ts:106, DetailSection.tsx:518 ⛔ No — a different key, an i18n label. Prefix match.
    record-reference-rail.tsx:54/232/241 hideEmpty?: boolean ⛔ No — the reference rail, a different surface. The ruling retires only DetailViewSection.
    views.ts:229, record-details.tsx:235, plugin-detail/index.tsx:750 ✅ Yes

    All three are in the brief by name. The dev is also asked to establish which surface DetailSection.tsx:179-198 belongs to and to hand the question back rather than guess if it is genuinely ambiguous.

    ⚠️ The compile-time ledger will bite — and that is expected, not scope creep

    packages/types/src/__tests__/zod-mirror-parity.test.ts:1057 carries a views.zod.ts#DetailViewSectionSchema KnownDrift entry. That ledger is enforced at compile time (export const assertionDriftMatchesLedger: never) and, by its own header, "fails when new drift appears on that mirror AND when the recorded drift is fixed." So retiring this key will very likely redden packages/types type-check until the entry is corrected.

    ⭐ This is the third card this shift where a census/ledger file must move with the change and is unreachable by grepping the symbol — after zod-mirror-parity.test.ts on #6940 and the absolute member count in useObjectLabel-identity-5564.test.tsx on #7219. The brief says so up front so the dev does not read the red as its own bug, and does not treat correcting it as widening the PR.

    Measured for the brief: the two unlanded PRs that also touch that file (#7432 at the blocks.zod.ts region, #7447 at data-display.zod.ts#DataTableSchema) are disjoint from a views.zod.ts region.

    File surface

    Zero collisions across #7432 (23 files), #7447 (4), #7451 (5), #7332 (88), #7391 (33), #7400 (13), #7442 (8) — each total non-zero, so the zeros are readings.

    PR #7123's behaviour pin is restated, not deleted — it asserts the key no longer exists, and its controls stay, per point 4 of the ruling. #7127 goes separately and is fenced out.

    State transition, same stroke: pm:queue → pm:dispatched, assignee set.


    Generated by Claude Code

  3. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
      "issue": 7129,
      "status": "done",
      "branch": "claude/issue-7129-retire-detailviewsection-hideempty",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7464",
      "premise_still_valid": true,
      "summary": "Retired DetailViewSection.hideEmpty per the 2026-09-01 ruling (comment 5494797031, 总监批 #28): the @object-ui/types declaration, the RecordDetailsRenderer slot, and DetailSection's three reads are gone; the spec keeps refusing the key and the zod mirror stays absent. The auto-hide heuristic (4 fields / 25% empty; 3 / 20% mobile) plus the 'Show N empty fields' toggle is now the whole contract, and the 'explicit false is not an override' paradox went with the key. #7127 NOT folded in. THREE SAME-NAMED SURFACES DISTINGUISHED: (1) DetailViewSection.hideEmpty — retired, this card; (2) record:reference_rail's own hideEmpty in renderers/record-reference-rail.tsx:54/232/241 — a different surface, untouched; (3) hideEmptyFields, a PREFIX match — the toggle's i18n label in all ten locale packs plus useDetailTranslation.ts:106 and DetailSection.tsx:518 — untouched. DetailSection.tsx QUESTION ANSWERED, not handed back: DetailSectionProps.section is typed DetailViewSectionType at DetailSection.tsx:101, the aliased import of DetailViewSection from @object-ui/types at line 30, so its reads are reads of the retired declaration and the type-check refuses them otherwise — the SAME surface, not a third one. TWO CORRECTIONS TO THE DISPATCH BRIEF: (a) the registered input at plugin-detail/src/index.tsx:750 is ComponentRegistry.register('reference_rail', ...) with description 'Drop the rail entirely when no entries resolve' — it belongs to the REFERENCE RAIL, not record:details, and is therefore OUT of scope; the record:details registration publishes no hideEmpty input and never did, so only its never-teach note at :427 needed correcting (apps/console/src/__tests__/registry-inputs-spec-parity.test.ts:837 independently states that registration 'has always published hideEmpty and only hideEmpty' for record:reference_rail). (b) The ledger entry at zod-mirror-parity.test.ts:1057 is in interface UnmirroredDeclared, NOT KnownDrift — the two are reconciled by different assertions, and it is assertionUnmirroredMatchesLedger (line 1410), not assertionDriftMatchesLedger, that this change moves. Also recorded, not acted on: .changeset/7064-empty-section-default.md is unreleased and its closing 'What does not change: an authored hideEmpty keeps its exact former meaning' paragraph is now false; my changeset names and supersedes it in prose rather than editing another card's landed record. ROADMAP.md:1714/1719 log hideEmpty as a shipped Phase-N capability — left alone as a historical log, like the CHANGELOGs.",
      "tests": "ALL FINAL NUMBERS AT abaa1358a (branch head; `git rev-parse --short HEAD` from the same shell as the runs). (1) `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` -> 'Test Files 222 passed (222) / Tests 2822 passed (2822)'. (2) `pnpm --filter @object-ui/types --filter @object-ui/plugin-detail type-check` -> both print 'Done' (types = tsc --noEmit + tsconfig.examples.json + tsconfig.test.json; plugin-detail = tsc --noEmit + tsconfig.test.json). (3) `turbo run lint --filter=@object-ui/types --filter=@object-ui/plugin-detail --concurrency=2` -> '3 successful, 3 total', 0 errors, 899 pre-existing no-explicit-any warnings, none on a line I touched, the new pin file draws none. (4) `pnpm changeset:check` -> OK, no major. (5) `pnpm check:control-bytes` -> OK, 6138 tracked text files. Every heavy run went through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-7129 and I read its VERDICT line, never a bare $?; exit codes were captured by redirect-then-capture, never after a pipe. NOT MEASURED (stated as such, not as green): `pnpm check:sdui-registration-pins` exits 2 with 'Build the console first' — a PREREQUISITE NOT MET, not a red gate; its subject is untouched (the index.tsx edit is comment-only) and CI runs it. COVERAGE PROOF for the compile-time arm: `tsc -p packages/plugin-detail/tsconfig.test.json --listFiles` contains record-details.hideEmptyRetired-7129.test.tsx (1 hit) and resolves @object-ui/types to packages/types/dist/views.d.ts — the BUILT declaration, as that file's docstring claims. ABLATION, red-then-green, three arms, predictions written to a file BEFORE running. Each arm: asserted worktree blob == HEAD blob first (an empty hash read as FAILURE), mutated, then proved the mutation on disk by blob CHANGE plus anchored grep counts of the injected text, and for arm A additionally proved it reached packages/types/dist/views.d.ts (1 hit) after a rebuild; restore was proved BY STATE — `git diff HEAD` empty, `git hash-object` == `git rev-parse HEAD:<path>`, `git status --short` clean — never by an exit code; every script carried `trap restore EXIT INT TERM` with ABSOLUTE paths and a restore spelled `git checkout HEAD -- <abs>` (never the bare form, which restores from the index the mutation may have written). ARM A (re-insert `hideEmpty?: boolean;` into views.ts + rebuild @object-ui/types) — PREDICTED types type-check RED, plugin-detail type-check RED, vitest GREEN-UNCHANGED; MEASURED exactly that: types 'zod-mirror-parity.test.ts(1410,14): error TS2322: Type \"views.zod.ts#DetailViewSectionSchema\" is not assignable to type never' (assertionUnmirroredMatchesLedger naming the pair, the assertionRatchetRejectsFreshDrift shape), plugin-detail 'record-details.hideEmptyRetired-7129.test.tsx(73,54): error TS2344: Type false does not satisfy the constraint true', and the runtime suite still 220 files / 2699 tests GREEN — the informative direction, since a declaration is erased at runtime and a tests-only ablation would have reported the guard does nothing. ARM B (re-insert `hideEmpty: s.hideEmpty` into the renderer's section map) — PREDICTED pins RED; MEASURED GREEN (2 files / 9 tests passed). PREDICTION WRONG, AND IT CHANGED THE PR: RecordDetailsRenderer spreads `...s`, so the explicit slot was redundant and the authored value still reached DetailSection; the removal is a STATEMENT change and the behaviour was always DetailSection's. Two corrections followed — the renderer comment now records the measurement, and the pin named 'RecordDetailsRenderer no longer READS the key' was renamed to the end-to-end claim it actually holds, because a pin written against the renderer's slot alone is a pin that cannot fail. ARM C (added after B: re-insert DetailSection's three reads) — PREDICTED pins RED; MEASURED RED, 4 failed / 5 passed. 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 and it stayed green through arm C's first run. It now runs over 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 the final arm C reddens 4 tests rather than 3. Green re-confirmed after each correction, and both restores re-proved by state.",
      "mcp_calls": "8 — issue_read get, issue_read get_comments, create_pull_request, update_pull_request (title), pull_request_read (body read-back), issue_write create (#7465), add_issue_comment (this), issue_read get_comments (read-back). Issue/PR reads that could go over the zero-quota channel did: a repo-scoped REST read probe returned HTTP 200, so the out-of-scope dedup search used REST label-scoped list endpoints plus a LOCAL grep over 154 open `package: types` / `domain:ui` issues — no MCP search_issues call was spent. (REST /search/issues itself answers 403 unauthenticated here; the repo-scoped list endpoints do not.)",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7465: `showBorder` on `record:details` sections is the same divergence one key over — spec REFUSES it (measured, unrecognized_keys, control `columns: 2` parses and survives), `@object-ui/types` declares it, and — unlike `hideEmpty` — the zod mirror ALSO declares it, so three local contracts agree and only the published spec disagrees; the renderer derives a real default from it, so an authored `showBorder: false` genuinely changes the render and simply cannot be authored on a validating page. Unlabelled and unassigned for PM triage; `title`'s separate status (declared + mirrored + spec-refused, but a renderer-INTERNAL slot since #6190, not an authoring key) is recorded on the card so the two are not conflated."
      ]
    }

    Generated by Claude Code

  4. added a commit that references this issue on Sep 3, 2026
  5. os-elon-musk commented on Sep 15, 2026

    @os-elon-musk
    Collaborator

    Dated note (director seat, 2026-09-15T22:57Z) — Q1-A of this card's ruling is superseded for hideEmpty by the batch #137 item 3 ruling on objectui#8603 (maintainer 「同意」): @objectstack/spec 17.3.0+ declares RecordDetailsProps.sections[].hideEmpty (#11289, maintainer ruling 2026-08-23, direction 1), so the premise 「spec refuses it」 no longer holds and objectui restores the record:details read to match the protocol's describe(). Q2-C and every other clause of this ruling stand. ⛔ The ruling text above is not rewritten; this note is the pointer. Execution is on objectui#8603.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpackage: typespm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions