feat(plugin-detail,types): record:details honours hideEmpty again — the protocol's all-empty section contract - #9627
Conversation
… 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
|
changeset-claim-re-read
|
✅ 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
|
Contract reviewServed-tier: ① Derived judgmentsRuling executed: objectui#8603 comment 5689202410 (director seat batch #137 item 3, letter A, maintainer 「同意」 2026-09-15T22:57Z). Its words: restore the Accept-set and public-surface changes the diff implies, one by one:
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 Decision 2 — file surface. Decision 3 — where the default lives. RIGHT placement. Fallback body and 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 ② Semver levelChangeset ③ Boundary flags
Implemented-by: 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
✅ 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
|
Contract reviewServed-tier: ① Derived judgmentsRuling executed: objectui#8603 comment 5689202410 (director seat batch #137 item 3, letter A, maintainer 「同意」 2026-09-15T22:57Z). Its words: restore the Accept-set and public-surface changes the diff implies, one by one:
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 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 ② Semver levelChangeset ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
清标 provenance —— 条款②双载体已剥(第二轮复核,PASS)记录:PR 评论 ⭐ 这是本班唯一走完 FAIL → 补丁轮 → 重审 → PASS 全程的卡,值得把过程记清楚:
档位:本席独立量的同题发火对照(同一次扫描,最近 6 个子代理): ⭐ 二审不是复读一审。 简报里写死了「PR 上那条 FAIL 记录判的是已被取代的 head,⛔ 不许继承,要在新 head 上自己重推」。它照做了,并逐条复算:消融三腿 4/2/1 独立复现、 落地前检三条
受管面: 复核件点名的非阻断残留,⛔ 不改变 PASS,另行立卡
⛔ 一条不归本 PR、也不归 dev复核件两轮都点名:本裁决反转了 2026-08-31 维护者对 objectui#7064 的裁决而未点名它,#7064 欠一份 #7129 拿到过的那种带日期备注,归总监席。二审复核时在 #7064 上找不到这样的备注(唯一那条 2026-09-16 的评论是本席留的指路,它自己就写着备注仍欠)。⇒ 仍然欠着。 — PM Generated by Claude Code |
Fixes #8603
record:detailshonourshideEmptyon a section again. An all-empty section hides itself;hideEmpty: falsekeeps its heading and label skeleton. That is what@objectstack/specdeclares 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
8d8523e0dand 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?? trueplacement and its deliberate non-reach into the fallback body and thedetail-sectionnode, the!isEditinggate, both pins, the ablation, the ledger edit, the file surface and the semver level.1. The
hideEmptydocblock claimed a default that is false on the other consumer.@default trueand "Omitted behaves astrue" were written on a member ofDetailViewSection, and that type is read by TWO authorable renderers.record:detailsmaps every authored section withhideEmpty ?? true;detail-view— registered with its ownsectionsinput — hands each section toDetailSectionunchanged 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:truefalsedetail-viewrecord:detailsDetailViewhas nodataprop (the record arrives asschema.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 truetags in that file, on other members. objectui#7361 / objectui#7735 class.2. A sentence in the
RENDERER_ONLY_SECTION_KEYShunk asserted a falsehood. It restated "the spec's section object does not declare it" ofshowBorder, 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.mdpublished "Migration: deletehideEmptyfrom anyrecord:detailssection you author";7064-empty-section-default.mdpublished "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
7129paragraph 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 onmainand 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. The7064correction keeps what still holds (the fallback body and thedetail-viewnode 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.tsxcomment 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-writinghideEmpty: falseper 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.-7129pin 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: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 requiresfilledCountgreater than zero,hideEmptyapplies only where it is zero.Premises, measured against the installed artifact before writing
Read from the installed
@objectstack/specin this worktree, not transcribed from the card.Version.
node_modules/@objectstack/spec/package.jsonreads 17.4.0; the lockfile resolves the same.The key, and its
describe(). Read off the shippedjson-schema/ui/RecordDetailsProps.json, atproperties.sections.items.properties.hideEmpty, verbatim: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 carryhideEmptyat all.Runtime polarity probe, with controls, exit 0:
The minted control token was verified absent from the tracked corpus first (
git grep -lIreturns 0 files, exit 0), against a positive control on the same instrument (RecordDetailsPropsreturns 30 files). ⇒ the key parses because it is DECLARED, not because the object went permissive, and the value reachesparsed.data.What objectui#7129 actually deleted (premise 2, read from the retirement commit rather than assumed): from
DetailSection, the!section.hideEmptylimb ofshouldAutoHideEmpty, thesection.hideEmpty ||limb ofhideEmptyEffective, and the same limb inshowEmptyToggle; fromRecordDetailsRenderer, thehideEmpty: s.hideEmptyslot. The restoration below matches the DECLARED behaviour, ⛔ not that deleted code — the old read tested truthiness, so a writtenfalsewas indistinguishable from unauthored, which is the paradox #7129's Q2 measured.RENDERER_ONLY_SECTION_KEYS(premise 3) did listhideEmpty; verified before editing, and it no longer does.What changed, and the one design decision in it
Where the default lives is the decision.
RecordDetailsRendererresolves it on an AUTHORED section (hideEmpty: s.hideEmpty ?? true) andDetailSectiontestssection.hideEmpty === true.That placement is load-bearing.
DetailSectionalso renders sections nobody can author the key on: therecord:detailsdirect-fieldsfallback body and thedetail-sectionnode each synthesize one, and neither surface declareshideEmpty. A default read as!== falseinside 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 thatdescribe()means: the default of the renderer the key is declared on.!== falseinDetailSectionand 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 valuelesssignaturesample, and the unresolved-identity fail-closed pin, whose ghost row is a synthesized body). Moving the default to the mapping leaves all four untouched.recordDetailsInputs.spec-parity.test.ts; the four that reddened under the first implementation are not in the diff at all.?? trueis 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?: booleandeclared again, replacing the retirement tombstone. Mechanically required:DetailSectioncannot 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—DetailViewSectionSchemamirrors 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'ssectionsdescription teaches the key instead of warning that authoring it does nothing;hideEmptyleaves the never-teach note.packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts—RENDERER_ONLY_SECTION_KEYSdrops 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.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-presencenames 2 released packages;check-changeset-no-majorcaps 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/typesdeclares 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 wasCalendarConfigSchema.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.namefield, so the page-H1 dedupe ladder consumedindustry— 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 emptygit diff HEAD. Run on the committed tree.DetailSectionread fromhideEmptyEffective?? true), keep the pass-throughDetailSectiontest to!== falsefieldsfallback body loses its skeletonLeg 3 is what makes⚠️ 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.
=== truea measured choice rather than a stylistic one.Gates — hand-derived, since the derivation tool refuses cross-repo
scripts/pm/dispatch-gates.mjslives in objectstack and answers only about the tree it lives in, so there is no--ranreconciliation for this PR.check:*scripts and.github/workflows/, and a hand-derived list can be short. Final headf8388535d(the patch round; the first review judged8d8523e0d).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-checkfor both packages → Done,TYPECHECK_EXIT=0, afterBUILD_EXIT=0on the dependency closure — the first attempt in the recreated worktree reportedTS2307: Cannot find moduleacross 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 jsonjudged 5012 files, and the 9 files this branch touches carry 0 errors / 89 warnings, every warning a pre-existingno-explicit-anyorreact-refreshon 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 noprojectServiceand noparserOptions.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, andpnpm lintis 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-floorsandcheck:readme-exports(both reportno-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
sectionswill show fewer sections than before unless it writeshideEmpty: 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-
fieldsfallback body, and thedetail-sectionnode. Inline-edit mode also always renders.Rollback is one revert of this branch; nothing is generated, no baseline moves, no lockfile changes.
Acceptance notes
content/docs/guide/*.mdand no package README enumeratesrecord:detailssection member keys (grep forhideEmpty,showBorder,headerColoracrosscontent/docs/**,docs/**andpackages/*/README.mdreturns nothing for this surface), so the manifestinputsdescription 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.packages/plugin-detaildeclares@objectstack/specat^17.1.0while 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-floorsis the instrument that owns this question and could not judge it here without a workspace build.5689202985on 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