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.

Flip the empty-section DEFAULT: sparse records keep the section skeleton — stop forcing hideEmpty ?? true over DetailSection's own stated heuristic #7064

Closed

Description

@huangyiirene

Filed on the maintainer's ruling (2026-08-31, hotcrm#1247 adjudication, verbatim: 「#1247 也是平台的问题吧。元数据应用不应该考虑这些细节。」).

The mechanism (measured across hotcrm#1247 / objectstack#11289 / shipped bundle)

record:details hides empty fields, then RecordDetailsRenderer (packages/plugin-detail/src/renderers/record-details.tsx) maps every section with hideEmpty: s.hideEmpty ?? true, and DetailSection returns null when all fields are empty. On a hand-created sparse record the body collapses to a degenerate two rows (hotcrm opportunity: 7 optional body fields, 5 empty; case page: case number + three empty rows) — while seeded demo data masks it.

⭐ The platform already knows the right behavior and says so in its own code — DetailSection's heuristic comment: "If a section is entirely empty (e.g., loading state, brand-new record), do NOT auto-hide — the labels themselves are useful as a structural skeleton." The renderer's forced ?? true overrides exactly that case. objectstack PR #11662 then made hideEmpty an AUTHORABLE opt-out — which is the shape the maintainer has now ruled against: every app hand-writing hideEmpty: false per section to avoid looking broken is per-app tax (objectstack#13848 principles).

Deliverable

  1. Flip the default: stop forcing hideEmpty ?? true in RecordDetailsRenderer; let DetailSection's stated heuristic own the all-empty case — an empty section keeps its heading and field labels (empty-value placeholders per the design system), so a sparse record renders a full structural skeleton with ZERO app-side authoring;
  2. authored hideEmpty: true (declared by objectstack PR fix(app-shell,plugin-chatbot,i18n): AI build surface UX — land on the running app, a task-named composer, one usage meter, a gauge that is not a spinner, a localized scope chip (objectui#11658) #11662) remains fully honoured as the explicit opt-in to hiding — the key's meaning is unchanged, only the unauthored default flips;
  3. changeset marks the behavior change (apps relying on auto-hide of unauthored sections will now show skeletons — the ruled loud-over-silent direction); name any reference-app hits in the PR.

Acceptance scenario

hotcrm opportunity_detail_page and case_detail_page on hand-created records with only required fields: full section skeletons render (no two-row body, no vanished sections), with zero hotcrm-side changes. The strip-vs-body field re-allocation hotcrm#1247 contemplated becomes unnecessary and is NOT performed.

Refs: hotcrm#1247 (closed with pointer here) · objectstack#11289 / PR #11662 (key declaration half) · objectstack#13848 (application-repo principles) · objectui#7063 (sibling ruling: dashboard widget default empty state — same principle, different surface).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 31, 2026
  2. self-assigned this
    on Sep 1, 2026
  3. os-warren commented on Sep 1, 2026

    @os-warren
    Collaborator

    🔒 Claimed — domain:ui execution seat (objectui)

    • Session: session_012wwHa4aaFybxXrfmfHioDM
    • Branch: claude/issue-7064-empty-section-default
    • Build tier: opus (⚠️ Fable 5 quota exhausted; standing maintainer exemption 2026-08-13 lowers the build tier only)

    Not clause ②, which is why it can be dispatched while the quota is out. This flips an unauthored default in a renderer. hideEmpty stays authorable and its meaning is unchanged — nothing enters or leaves the accepted schema, and no published surface widens. It is a behaviour change (correctly requiring a changeset that says so), not a contract change, so no CONTRACT_REVIEW_TIER is owed.

    File is free. packages/plugin-detail/** was held by #6868 through R1–R4; that landed as 03380aa14 and the hold is released.

    ⭐ Why this card is unusually well-founded

    The platform already states the correct behaviour in its own code — DetailSection's heuristic comment: "If a section is entirely empty (e.g., loading state, brand-new record), do NOT auto-hide — the labels themselves are useful as a structural skeleton." RecordDetailsRenderer's forced hideEmpty: s.hideEmpty ?? true overrides exactly the case that comment reserves.

    ⇒ This is the same class as #7092 and #7070 in this round: a comment that describes behaviour the adjacent code does not implement. Three instances on this lane today. The difference here is that the comment is right and the code is wrong, so the fix restores the stated intent rather than correcting the prose.

    Fences carried into the dispatch


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 7064,
      "status": "done",
      "branch": "claude/issue-7064-empty-section-default",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7123",
      "premise_still_valid": true,
      "summary": "Removed the forced `hideEmpty: s.hideEmpty ?? true` in RecordDetailsRenderer so an unauthored section reaches DetailSection as undefined and its own stated heuristic owns the all-empty case; the authored value now passes through untouched. One-line behaviour change plus a 7-case pin file and a changeset naming the behaviour change. The issue's premise held exactly: the force site was where the card said, and DetailSection's heuristic really does produce the skeleton. Two dispatch assumptions were falsified (no existing test encoded the old default; and authored `hideEmpty` is REFUSED, not merely undeclared, by @objectstack/spec 17.2.0 on a record:details section). Issue assignee field read as null in the card payload while an AssignedEvent exists in its timeline - PM-side half state, reported and worked through.",
      "force_sites_found": [
        "packages/plugin-detail/src/renderers/record-details.tsx:210 - `hideEmpty: s.hideEmpty ?? true` - THE ruled site, FIXED",
        "packages/plugin-detail/src/renderers/record-reference-rail.tsx:232 - `schema.hideEmpty !== false` - LEFT ALONE: different surface (record:reference_rail), component-level key folding rail entries whose related count is 0, default is spec-documented as `renderer default: on`. Outside the ruled class; same family as sibling card objectui#7063.",
        "packages/plugin-detail/src/index.tsx:731 - registered input for the rail's hideEmpty (defaultValue: true) - LEFT ALONE, belongs to the rail row above",
        "packages/types/src/views.ts:230 - the DetailViewSection.hideEmpty type declaration - unchanged",
        "packages/plugin-detail/src/DetailSection.tsx - the heuristic itself plus hideEmptyEffective and the toggle - LEFT ALONE, this is what the ruling hands the case to",
        "NO independent empty-section dropping anywhere else: DetailView renders schema.sections.map(...) unconditionally; the single section-hiding decision is DetailSection's one early return at DetailSection.tsx:476"
      ],
      "heuristic_verified_by_rendering": true,
      "heuristic_actual_output": "Rendered RecordDetailsRenderer inside a real RecordContextProvider over a sparse record with an all-empty 4-field section and dumped the DOM. container.textContent = 'Deal Termsstage—amount—close_date—next_step—'. Structure: Card + CardHeader carrying the heading 'Deal Terms', then a 2-column grid with one row per field; each row is a label div plus a span with aria-label=\"No value\" title=\"No value\" rendering an em dash. The heuristic is fully implemented - the forced default was the only thing suppressing it.",
      "precedence_chain": "hideEmpty is PER SECTION only - no page-level or view-level hideEmpty exists with competing precedence (grep across the repo found the key in 5 source files; the only other one is record:reference_rail's own component-level key). Effective chain inside DetailSection: (1) the user's per-session showEmptyOverride toggle wins over everything; (2) otherwise empties hide when section.hideEmpty is truthy OR shouldAutoHideEmpty fires; (3) shouldAutoHideEmpty requires !section.hideEmpty AND !isEditing AND at least 4 fields (3 mobile) AND at least 25% empty (20% mobile) AND at least one filled row - that last clause is what reserves the all-empty case; (4) the whole section returns null only when every field is empty and all were filtered out. MEASURED CONSEQUENCE: `hideEmpty: false` is NOT an override - step 3 tests `!section.hideEmpty`, so an authored false is indistinguishable from unauthored and auto-hide still fires above the thresholds. Pre-existing and UNCHANGED by this PR (`?? true` preserved an authored false too, so the same fixture took the same path before). Raised as an open question rather than changed.",
      "files_changed": [
        "packages/plugin-detail/src/renderers/record-details.tsx (the one-line default removal; the replacement comment also drops a non-English string, AGENTS.md commandment #-1)",
        "packages/plugin-detail/src/renderers/__tests__/record-details.emptySectionDefault.test.tsx (new, 7 pins)",
        ".changeset/7064-empty-section-default.md (new)"
      ],
      "tests_that_went_red": [],
      "pins_added": [
        "an ALL-empty section renders its skeleton: heading, every field label, an empty placeholder each",
        "a SMALL partly-empty section (below the auto-hide threshold) now shows its empty row - the second half of the behaviour delta, not limited to all-empty sections",
        "the label-graveyard guard is INTACT: a large mostly-empty section still auto-hides, and still offers the 'Show N empty fields' toggle",
        "`hideEmpty: true` still hides an all-empty section entirely - with a sibling section as the render CONTROL so the absences are a decision and not a render that never happened",
        "`hideEmpty: true` still hides the empty rows of a partly-filled section",
        "`hideEmpty: false` shows the empty rows the heuristic would not have hidden anyway",
        "MEASURED, not endorsed: `hideEmpty: false` is 'not true', NOT an override of the auto-hide heuristic - pinned as pre-existing and explicitly framed as a measurement, not an endorsement"
      ],
      "pin_ablation": {
        "predicted": "Stated before running: restoring `?? true` turns exactly 2 of the 7 pins RED - the all-empty skeleton and the below-threshold empty row - and leaves the other 5 GREEN, because `?? true` is a no-op on an authored value and produces an identical DOM on the auto-hide path.",
        "observed": "Exactly as predicted: 'Tests 2 failed | 5 passed (7)', and the two failures are the two predicted tests by name. The mutated failure output is the issue's own failure mode: 'TestingLibraryElementError: Unable to find an element with the text: Deal Terms' with the printed body being a div.space-y-6 wrapping a single self-closed div.space-y-3 sm:space-y-4 - the section list element with no section inside it.",
        "blob_before": "bee9ec88115d566c98ba314ca665ddd0f426f5c1 (equal to `git rev-parse HEAD:packages/plugin-detail/src/renderers/record-details.tsx`, asserted non-empty and asserted equal before mutating)",
        "blob_after": "4b5d8813361ca109018c6ab312d6a37942d7270b",
        "restore_proven_how": "By STATE, never by an exit code. Restore was `git checkout HEAD -- ABSOLUTE_PATH` (HEAD named explicitly so a polluted index cannot serve the mutation back), with REPO_ROOT taken from `git rev-parse --show-toplevel` and an `EXIT INT TERM` trap as the crash-path backstop only. Proof after restore: blob hash back to bee9ec88115d566c98ba314ca665ddd0f426f5c1 AND `git diff HEAD --name-only` empty. Mutation itself was proven on disk before the run, not from the editor's exit code: injected-line count 1, removed-line count 0, control line `showBorder: s.showBorder` count 1, and a changed blob hash. NO REBUILD LEG IS OWED: vitest.config.mts aliases every @object-ui/* specifier to that package's src (lines 275-310) and the pin file imports '../record-details' relatively, so the mutation runs from source with no dist in the path.",
        "final_state": "The final union suite run was made on the restored tree at commit 1895c0965."
      },
      "changeset_grade_and_gate_verdict": {
        "file": ".changeset/7064-empty-section-default.md",
        "grade": "minor for @object-ui/plugin-detail - the breaking behaviour is named in the changeset BODY, per AGENTS.md (objectui's major is a cross-repo pin, not a severity dial)",
        "gate_verdicts_quoted": [
          "check-changeset-presence: 'Compared the working tree with 71d83a6b1 (merge-base with origin/main): 1 file(s) changed, 1 of them published source of a package the release covers, 0 of them a manifest whose published contract moved, 0 under a package changesets ignores, 1 changeset(s) added.' then 'OK - 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7064-empty-section-default.md.' exit 0",
          "check-changeset-no-major: 'No changeset declares a `major` bump.' exit 0"
        ]
      },
      "gates": [
        { "name": "vitest packages/plugin-detail/ (FINAL, at 1895c0965)", "exit": 0, "verdict": "green", "raw": "Test Files  120 passed (120) / Tests  1107 passed (1107); lock 'VERDICT command-exit 0 - held the lock 239s'" },
        { "name": "vitest packages/plugin-detail/ (BASELINE, at 71d83a6b1, before any edit)", "exit": 0, "verdict": "green", "raw": "Test Files  119 passed (119) / Tests  1100 passed (1100)" },
        { "name": "vitest - 15 consumer test files referencing record:details (apps/console, packages/app-shell, packages/core, packages/types)", "exit": 0, "verdict": "green", "raw": "Test Files  15 passed (15) / Tests  329 passed (329)" },
        { "name": "pnpm --filter @object-ui/plugin-detail type-check", "exit": 0, "verdict": "green", "raw": "script echoed: 'tsc --noEmit && tsc -p tsconfig.test.json'; lock 'VERDICT command-exit 0'. Coverage PROVEN not assumed: `tsc -p tsconfig.test.json --listFiles` lists the new pin file (1 hit) beside the existing renderers/__tests__/record-details.test.tsx control (1 hit) out of 1712 files." },
        { "name": "pnpm --filter @object-ui/plugin-detail lint (eslint .)", "exit": 0, "verdict": "green", "raw": "894 problems (0 errors, 894 warnings) - all pre-existing families. --format json over just the two changed files: 2 files, 0 errors, 32 warnings (31 pre-existing in record-details.tsx, 1 no-explicit-any in the new test matching the sibling harness idiom)." },
        { "name": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "green", "raw": "OK - 1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)" },
        { "name": "node scripts/check-changeset-no-major.mjs", "exit": 0, "verdict": "green", "raw": "No changeset declares a `major` bump." },
        { "name": "pnpm check:control-bytes", "exit": 0, "verdict": "green", "raw": "check-control-bytes: OK (scanned 5895 tracked text file(s); skipped 85 binary)." },
        { "name": "pnpm check:vi-mock-specifiers", "exit": 0, "verdict": "green", "raw": "check-vi-mock-specifiers: OK (4074 tracked source file(s), 2349 test-named; 515 carry a mock; ...)" },
        { "name": "pnpm check:vi-mock-inherit", "exit": 0, "verdict": "green", "raw": "check-vi-mock-inherit: OK (114 call site(s) on @object-ui/react judged (114 inherit, 0 auto-mocked))" },
        { "name": "pnpm check:i18n-keys", "exit": 0, "verdict": "green", "raw": "Every in-scope call-site key resolves against the en pack (2842 keys), every literal inline defaultValue matches the value the pack serves ..." },
        { "name": "pnpm check:spec-symbols", "exit": 0, "verdict": "green", "raw": "spec symbol derivation: 1329 files scanned against 4959 spec export names; 16 declared dialects, 0 untriaged collisions in 0 packages." },
        { "name": "pnpm check:sdui-registration-pins (FIRST attempt)", "exit": 2, "verdict": "NOT_MEASURED", "raw": "This is exit 2, not a pass: the registrations this gate pins are dropped by a WRONG ARRAY at BUNDLE time, so a run with nothing to read has measured nothing. Build the console first: pnpm --filter @object-ui/console build" },
        { "name": "pnpm --filter @object-ui/console build (FIRST attempt, to satisfy that precondition)", "exit": 2, "verdict": "NOT_MEASURED", "raw": "TS2307: Cannot find module '@object-ui/auth' / '@object-ui/plugin-form' - unbuilt dependency closure, reported as NOT MEASURED, not as a failure. Resolved by building the closure: pnpm --workspace-concurrency=2 --filter '@object-ui/console^...' build (exit 0), then the console itself (exit 0)." },
        { "name": "pnpm check:sdui-registration-pins (RE-RUN, precondition met)", "exit": 0, "verdict": "green", "raw": "All 16 registration(s) a `sideEffects` array promises are present in the built console (517 chunks weighed; the 3 ruled control(s) are in the derived set)." }
      ],
      "controls_used": [
        "grep for hideEmpty in packages/types/src/zod/views.zod.ts returned 0 - CONTROLS in the same file: headerColor = 2 hits, DetailViewSectionSchema = 3 hits. So the zero is a real absence (the zod mirror does not carry the key), not a broken query.",
        "spec parse probe: the FAILING case (a section carrying hideEmpty) is paired in the same script with a MUST-HIT control (a section carrying columns: 2), which parsed successfully with the value surviving.",
        "mutation proof paired an injected-line count and a removed-line count with a CONTROL line (`showBorder: s.showBorder`, must be 1) so a zero-hit anchor could not read as a successful mutation.",
        "the `hideEmpty: true` pin renders a SECOND, filled section as an in-test control, so the assertions about the hidden section are a decision by hideEmpty and not a render that never happened.",
        "control-byte scan of the changed files used redirect-then-read exit capture (exit 1 = no matches = clean), never a pipe, and was paired with a printable-anchor count on the same file.",
        "consumer-test enumeration: the grep that found 15 consumer files outside plugin-detail was paired with the same query inside plugin-detail (8 hits) as the must-hit control.",
        "eslint narrowing is itself a measurement: eslint.config.js declares no project / projectService / parserOptions (control: 'rules' = 10 hits in the same file), so type-aware linting is off and this diff cannot move a verdict on an untouched file; file count read from --format json (2 files), not guessed.",
        "FAILED CONTROL, reported: mcp search_issues returned total_count 0 for a query that must hit #7064 itself. Per the zeroed-session rule I did not retry and switched off that channel."
      ],
      "assumptions_falsified": [
        "PM assumption 'existing tests encode the OLD default and will go red' is FALSE. Baseline was 119 files / 1100 tests green; after the change 120 files / 1107 tests green - the whole delta is this PR's new file. Nothing in the repo pinned `?? true`. tests_that_went_red is empty for that reason, not because I skipped looking.",
        "ISSUE/DISPATCH assumption that authored hideEmpty is a declared opt-out is FALSE at the installed @objectstack/spec 17.2.0. Measured: RecordDetailsProps.safeParse over a 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. Consequence: for any spec-validated page the key never reaches this renderer, so the old `?? true` was an unconditional platform policy with NO author escape hatch, and the flip is what actually gives spec-validated pages the ruled behaviour with zero authoring. ZONE 1 rule 2 is still satisfied in code - the renderer honours an authored value verbatim for the schemas that carry one (@object-ui/types declares DetailViewSection.hideEmpty) - but the ruling's second clause reads differently once this is known.",
        "PM assumption 'hideEmpty may exist at page or view level with different precedence' is FALSE - it is per-section only. But a precedence surprise does exist one level down and is reported in precedence_chain: `hideEmpty: false` is not an override of the auto-hide heuristic.",
        "The issue's own file/line pointer was CORRECT and DetailSection's heuristic is fully implemented - premise_still_valid stays true."
      ],
      "hotcrm_acceptance_unverified_reason": "hotcrm is not in this session's repo scope; the dispatch forbade reaching it and I did not. I claim nothing about hotcrm opportunity_detail_page or case_detail_page. All evidence here is objectui-side: component-level renders of RecordDetailsRenderer through the real DetailView and DetailSection with sparse fixtures, plus the DOM dump quoted in heuristic_actual_output. The hotcrm acceptance scenario still needs a run by a seat that has that repo. This is stated plainly in the PR body under its own heading.",
      "mcp_calls": "3 - create_pull_request, search_issues (returned a failed control, see controls_used), add_issue_comment. All issue and PR reads went through the zero-quota public-repo page payload channel instead; repo-scoped REST is 403 for this session ('GitHub access is not enabled for this session').",
      "open_questions": [
        {
          "question": "Should `hideEmpty: false` become a hard override of DetailSection's auto-hide heuristic? Today it means only 'not true': shouldAutoHideEmpty tests `!section.hideEmpty`, so an authored false is indistinguishable from an unauthored section and empties are still auto-hidden above the 4-field / 25% thresholds. An author who writes false on a large sparse section sees no change at all.",
          "options": [
            "A - leave as is (what this PR does): `false` stays 'not true'. Zero risk, but the key silently does nothing in exactly the case an author would reach for it.",
            "B - make `false` a hard override: hideEmptyEffective becomes false whenever the author wrote false, regardless of thresholds. Restores the key's plain-reading meaning; costs one more authorable lever, which cuts against the ruling's zero-authoring direction.",
            "C - retire the authored key on this surface entirely and let the heuristic be the whole contract, matching the fact that @objectstack/spec already refuses it on record:details sections."
          ],
          "recommendation": "A for this PR (changing it exceeds the ruling and this card's fence), then a separate decision between B and C. On the four axes: real business need favours C - the spec already refuses the key, so no spec-validated app can be pulling on it today, and the measured pull for B is zero; long-term soundness favours C, since a key declared in @object-ui/types, honoured by the renderer, and refused by the spec is a two-contract divergence that the enforce-or-remove principle already answers; making AI-authored metadata hard to get wrong favours C strongly - B adds a lever whose correct use requires knowing a threshold formula, while C makes the platform's behaviour unconditional; startup scope discipline favours C over B for the same reason (B is capability expansion with no pull). B's only real merit is honesty toward whoever already writes false in a non-spec-validated schema. Maintainer call."
        },
        {
          "question": "Does @objectstack/spec need to declare hideEmpty on the record:details section object, now that the renderer honours it but the spec refuses it? This is an objectstack-side contract decision, outside this repo.",
          "options": [
            "A - leave the spec refusing it: the ruling says apps should not author this at all, so a loud refusal is the aligned answer, and @object-ui/types' declaration becomes the divergence to close instead.",
            "B - declare it in the spec so the renderer's honoured key is authorable and the two contracts agree."
          ],
          "recommendation": "A. The ruling's whole direction is that the platform decides this and the app does not author it, so the spec refusing the key is consistent with the ruling rather than a gap - and the thing to fix is then the objectui-side declaration in packages/types/src/views.ts, not the spec. Flagging rather than acting because it is a cross-repo contract decision and objectstack is outside this dispatch."
        }
      ],
      "out_of_scope_findings": [
        "COULD NOT FILE - handed to the PM to file, with the reason. Both channels for filing are down for this session: repo-scoped REST returns 403 ('GitHub access is not enabled for this session. An org admin must connect the Claude GitHub App for this organization'), and MCP search_issues is SILENTLY ZEROED - it returned total_count 0 for a query that must hit #7064 itself ('record:details section hideEmpty default empty section skeleton'). Per the zeroed-session rule a zero without a passing control is not a reading, so I did not open a card blind and did not retry.",
        "FINDING 1 (objectui, would be a new card): packages/plugin-detail/src/index.tsx lines 426-431 carry a stale factual claim - they say the spec's record:details section object 'STRIPS' undeclared keys such as title / showBorder / hideEmpty on parse. Measured against the installed @objectstack/spec 17.2.0 it REFUSES them instead: safeParse returns success:false with unrecognized_keys and the message 'Unrecognized key(s) on this `record:details` section. Until #4001 batch A an undeclared prop was dropped in silence ...'. Same defect class the PM is tracking this round (a comment describing behaviour the code no longer has). Not fixed here: different file, different subject from this card, and the bounded in-place exemption needs same-defect-class, which does not hold.",
        "FINDING 2 (objectstack / @objectstack/spec, cross-repo, decision-shaped): the record:details section schema is a strictObject over exactly { name?, label?, columns?, fields } and refuses hideEmpty, while @object-ui/types declares DetailViewSection.hideEmpty and this renderer honours it. Two contracts disagree about one key. Captured as open question 2 rather than as a card because the resolution is a maintainer contract decision, and objectstack is outside this dispatch's repo scope.",
        "OBSERVATION (no card proposed): the pre-existing `hideEmpty: false` precedence surprise is already captured as open question 1 and pinned in the new test file, so it is visible in code rather than only in this report."
      ]
    }

    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    Collaborator

    🟢 LANDED — closed by PR #7123

    Merged to main as eeb6c2f2b: "fix(plugin-detail): let DetailSection's heuristic own the empty-section default".

    Landing verified with a control, in renderers/record-details.tsx on main

    probe expected measured
    hideEmpty: s.hideEmpty ?? true — the forced default 0 0
    hideEmpty: s.hideEmpty, — the pass-through 1 1
    showBorder ⚠️ control — untouched, must still hit >0 2

    The control fires, so the zero is a real absence.

    ⇒ An unauthored section now reaches DetailSection as undefined and its own stated heuristic owns the all-empty case: heading, field labels and empty-value placeholders, with zero app-side authoring — the ruling's whole point.

    ⭐ The heuristic was verified by rendering, not by trusting the comment

    The card's argument rests on DetailSection's own comment ("do NOT auto-hide — the labels themselves are useful as a structural skeleton"). The implementer rendered it instead and dumped the DOM: Deal Termsstage—amount—close_date—next_step—, heading plus one labelled row per field with aria-label="No value".

    ⚠️ Worth doing, because this round produced five separate cases of comments describing behaviour the code no longer has (#7092, #7070, #7008, #7127, and the interfaces message on #6795). Trusting this one would have been the wrong instinct even though it turned out true.

    The ablation was exact and quantified

    Predicted 2 of 7 pins red by name, 5 green with the reason (?? true is a no-op on an authored value and produces an identical DOM on the auto-hide path). Observed exactly that — and the mutated failure output is the issue's own failure mode: the section list element rendering with no section inside it.

    ⚠️ My assumption about the test suite was wrong, and the disproof is the interesting part

    I predicted existing tests encoded the old default and would go red. None did — baseline 119 files / 1100 tests, after 120 / 1107, the entire delta being the new pin file. ⇒ A platform-wide rendering default that no test pinned. That is a finding in its own right, and it is only visible because a baseline run was taken before the edit.

    ⭐ The finding that outranks the fix — routed, not absorbed

    Measured against installed @objectstack/spec 17.2.0: a record:details section carrying hideEmpty is refused (unrecognized_keys), against a control (columns: 2) that parses and whose value survives.

    ⇒ On any spec-validated page the key never reaches this renderer at all, so the old ?? true was not a default over an authorable key — it was an unconditional platform policy with no author escape hatch. The ruling's second clause preserved an opt-in that, where the spec is enforced, does not exist.

    The PR is correct as built (the renderer honours an authored value for schemas that carry one), but the divergence is real and is four-party: the spec refuses the key, @object-ui/types views.ts:230 declares it, the zod mirror omits it (0 hits, control headerColor = 2), and the renderer honours it. Routed on #7129 — together with the second measured surprise, that hideEmpty: false is not an override of the auto-hide heuristic (pre-existing, unchanged here, pinned as a measurement explicitly framed as not an endorsement).

    Also filed

    #7127 — the record:details registration comment says the spec "STRIPS" undeclared section keys; measured, it refuses them. ⚠️ And the same comment block says "rejects" two sentences earlier, so it contradicts itself.

    hotcrm

    ⛔ Unverified by this seat and correctly claimed as such — hotcrm is outside this session's repo scope. The acceptance scenario still needs a run by someone with that repo.


    Generated by Claude Code

  6. os-litant commented on Sep 2, 2026

    @os-litant
    Contributor

    Reconciliation by the domain:ui seat (session session_01NRRumy89BYdW9ogbcdHTho): this card is closed (completed, PR #7123 merged 2026-09-01) but still carried pm:dispatched. Stripped — a closed card carries no PM state. No other change.


    Generated by Claude Code

  7. hotlong commented on Sep 16, 2026

    @hotlong
    Contributor

    指路:本卡当初的维护者裁决,已被一条没有点名它的后续裁决反转

    domain:spec @ objectui 执行席。⛔ 本席不裁、不改本卡任何状态,只是把一条读数放在它该在的地方 —— 因为没有它,本卡的读者会以为这里的结论仍然生效。

    反转它的是:objectui#8603 上的总监席裁决(批次 #137 第 3 项 · letter A · 维护者「同意」2026-09-15T22:57Z)。它恢复了 record:details 对 hideEmpty 的读取,并把未编写时的默认值定为「全空的 section 什么都不渲染」。

    ⇒ 本卡当初的交付物 —— 原话「a sparse record renders a full structural skeleton with ZERO app-side authoring」(hotcrm#1247 的判例,落地于 eeb6c2f2b)—— 在那条裁决落地后不再成立:稀疏记录要保住骨架,需要逐 section 写 hideEmpty: false,也就是本卡当初明确否掉的那个形状。

    出处,⛔ 不是本席的判断:PR objectui#9627 的隔离达档契约复核记录(5700105332,VERDICT FAIL)的 Decision 1。它同时认定反转本身成立 —— 带维护者「同意」的批次件与当日现行裁决同权 —— 但它是未披露的:#8603 的裁决没有点名本卡,其前序裁决普查也没扫出本卡。

    欠本卡一份带日期的备注,⛔ 不是本席写

    #7129 在同一批次里拿到了这样一份(5689202985,写明其 Q1-A 被取代、且裁决原文不重写)。本卡欠一份对等的,归总监席。同样欠的是一句维护者当时没有被展示的后果:hotcrm#1247 的症状(新建记录丢掉全空的已写 section)会回来,除非 hotcrm 逐 section 编写。

    ⚠️ 另有一条待发 changeset .changeset/7064-empty-section-default.md 仍会逐字发布「稀疏记录保留其 section 骨架而不是塌掉」,而它与 #8603 的 changeset 同一次进 CHANGELOG。这一条已作为欠改项记在 objectui#8603 的交接评论上,由该卡的补丁轮处理。

    — PM domain:spec · session session_01VCpmqvacV4BypY48QdoxcE · 记录时刻 2026-09-16T15:34Z

    ⛔ 本条已编辑一次:原标题把本卡裁决的日期写成了 2026-08-31T00:00Z。本席只知道日期,不知道时刻 —— 补一个 00:00 就是把不知道的东西写成了读数。删掉,不替换。日期本身在正文里,出处是那条复核记录。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions