From 4983cb98971292c82d3d3f864544b84f4feba747 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 18:48:38 +0000 Subject: [PATCH 1/2] feat(types)!: retire `sections[].collapsed` from RecordDetailsComponentProps (objectui#8583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@objectstack/spec` `RecordDetailsProps.sections[]` refuses `collapsed` by name (`unrecognized_keys`) and declares `defaultCollapsed` for that state — the spelling `DetailSection` has always read. So this type declared the one collapse key an author could write in TypeScript and the contract rejects, while the key the contract accepts did not compile until item 1 landed it. Retired outright, no transition window: director seat decision batch #101 applies standing ruling batch #87 (this repo does not declare a key the spec refuses by name) and the 2026-08-27 retirement-pacing ruling. Zero readers and zero writers of this member in the repository, re-measured on this head with controls, so the removal is rename-free rather than a migration. The pin is reached by the leg that TYPE-CHECKS. `vitest` strips types and this package's build project excludes tests, so `tsc -p tsconfig.test.json` — the third leg of `type-check` — is the only invocation that reads the pin file. Its `@ts-expect-error` directives are the assertion: re-declare the member and they go unused (TS2578). The one in-repo writer the ruling's census reported as zero was real — `p1-spec-alignment.test.ts` wrote `collapsed: true` on a `RecordDetailsComponentProps` literal — and is renamed to the surviving spelling. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w --- ...retire-record-details-section-collapsed.md | 31 +++++++ .../src/__tests__/p1-spec-alignment.test.ts | 2 +- ...ecord-details-section-members-8583.test.ts | 87 +++++++++++++++++-- packages/types/src/record-components.ts | 1 - 4 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 .changeset/retire-record-details-section-collapsed.md diff --git a/.changeset/retire-record-details-section-collapsed.md b/.changeset/retire-record-details-section-collapsed.md new file mode 100644 index 0000000000..bf9f569b55 --- /dev/null +++ b/.changeset/retire-record-details-section-collapsed.md @@ -0,0 +1,31 @@ +--- +'@object-ui/types': minor +--- + +**BREAKING** — `RecordDetailsComponentProps.sections[]` no longer declares `collapsed`. + +**FROM** `collapsed` **TO** `defaultCollapsed`. + +```ts +// before +sections: [{ label: 'Address', fields: ['street', 'city'], collapsed: true }] +// after +sections: [{ label: 'Address', fields: ['street', 'city'], defaultCollapsed: true }] +``` + +`@objectstack/spec` `RecordDetailsProps.sections[]` refuses `collapsed` by name +(`unrecognized_keys`), and `defaultCollapsed` is the spelling it declares for that +state — the spelling `DetailSection` has always read. So the retired key never +reached the renderer through a spec parse, and nothing in this repository read it: +this is a type-face narrowing, not a data or runtime change. There is nothing to +migrate — rename the key at the authoring site. + +`collapsible` is unaffected; it stays. + +⚠️ The census behind this removal covers this repository only. A TypeScript +consumer of `@object-ui/types` outside it that wrote `collapsed` is not +observable from here, and gets a compile error (TS2353) naming the key — which +is why the FROM/TO is spelled out above. + +Retires the last member of the divergence objectui#8583 measured; the six keys +this type used to omit landed in the previous release. diff --git a/packages/types/src/__tests__/p1-spec-alignment.test.ts b/packages/types/src/__tests__/p1-spec-alignment.test.ts index e036072e39..f2e3c7b727 100644 --- a/packages/types/src/__tests__/p1-spec-alignment.test.ts +++ b/packages/types/src/__tests__/p1-spec-alignment.test.ts @@ -498,7 +498,7 @@ describe('P1.5 Record Components', () => { layout: 'stacked', sections: [ { label: 'Basic Info', fields: ['name', 'email', 'phone'], collapsible: true }, - { label: 'Address', fields: ['street', 'city', 'state'], collapsed: true }, + { label: 'Address', fields: ['street', 'city', 'state'], defaultCollapsed: true }, ], fields: ['name', 'email'], aria: { ariaLabel: 'Account Details' }, diff --git a/packages/types/src/__tests__/record-details-section-members-8583.test.ts b/packages/types/src/__tests__/record-details-section-members-8583.test.ts index 0d0f147423..243342fcd4 100644 --- a/packages/types/src/__tests__/record-details-section-members-8583.test.ts +++ b/packages/types/src/__tests__/record-details-section-members-8583.test.ts @@ -7,10 +7,18 @@ */ /** - * objectui#8583 (item 1) — `RecordDetailsComponentProps.sections[]` declares - * the six member keys `@objectstack/spec` `RecordDetailsProps.sections[]` - * declares and this type used to omit: `columns`, `icon`, `description`, - * `showBorder`, `defaultCollapsed`, `headerColor`. + * objectui#8583 — `RecordDetailsComponentProps.sections[]` and the + * `@objectstack/spec` `RecordDetailsProps.sections[]` member set, both + * directions of the divergence the card measured. + * + * - **item 1** — this type declares the six keys the spec declares and it used + * to omit: `columns`, `icon`, `description`, `showBorder`, + * `defaultCollapsed`, `headerColor`. + * - **item 2** — this type no longer declares `collapsed`, the one key it + * declared that the spec REFUSES by name. Retired outright, no transition + * window (director seat, decision batch #101, applying batch #87 — this repo + * does not declare a key the spec refuses by name — and the 2026-08-27 + * retirement-pacing ruling). `defaultCollapsed` is the surviving spelling. * * ## Why this pin is compile-time, and why a runtime pin alone measures nothing * @@ -143,6 +151,53 @@ const refusedByTsc: RecordDetailsComponentProps = { ], }; +/* ── item 2: `collapsed` is RETIRED, `defaultCollapsed` is the spelling ───── */ + +/** + * The retired key must NOT be a member. The directive is the assertion: re-add + * `collapsed?: boolean` to the section entry and `Expect` compiles, the + * directive goes UNUSED, and TypeScript reports TS2578 here. That is the + * ablation this pin is built to fail on. + */ +// @ts-expect-error objectui#8583 (item 2) — `collapsed` is retired; it is not a member of the section entry. +type _CollapsedIsRetired = Expect>; + +/** + * The surviving spelling, asserted with NO directive so it carries the other + * half: were `defaultCollapsed` ever dropped too, this line goes red on its own + * terms rather than leaving the pin above to pass over an entry that declares + * neither collapse key. + */ +type _DefaultCollapsedSurvives = Expect>; + +/** + * FROM `collapsed` TO `defaultCollapsed`, at the authoring site an external + * consumer actually writes. Excess-property checking on the section literal is + * what turns the retirement into a compile error (TS2353 naming the key) rather + * than a value that is silently carried and never read. + */ +const retiredSpellingRefusedByTsc: RecordDetailsComponentProps = { + sections: [ + { + fields: ['phone'], + collapsible: true, + // @ts-expect-error objectui#8583 (item 2) — the retired spelling does not compile; write `defaultCollapsed`. + collapsed: true, + }, + ], +}; + +/** The same literal in the surviving spelling — no directive, so it MUST compile. */ +const survivingSpellingAccepted: RecordDetailsComponentProps = { + sections: [ + { + fields: ['phone'], + collapsible: true, + defaultCollapsed: true, + }, + ], +}; + /** Member keys of one `sections[]` entry, read off the INSTALLED spec. */ const specSectionKeys = (): string[] => listedShapeKeys(arrayElementSchema(resolvePropsShape(RecordDetailsProps)?.sections)); @@ -155,7 +210,7 @@ const specSectionKeys = (): string[] => const refusedKeys = (issues: ReadonlyArray | undefined): string[] => (issues ?? []).flatMap((i) => (i as { keys?: string[] }).keys ?? []); -describe('record:details `sections[]` declares the six spec members (objectui#8583, item 1)', () => { +describe('record:details `sections[]` mirrors the spec member set, both directions (objectui#8583)', () => { it('the installed spec still declares all six on the section object', () => { // The premise the compile-time pins rest on: were the spec to drop one, the // `Equal` above would go red for the right reason, and this names it first. @@ -215,10 +270,32 @@ describe('record:details `sections[]` declares the six spec members (objectui#85 ]); }); + it('item 2 — the installed spec does not declare `collapsed`, and refuses it BY NAME', () => { + // The premise batch #87 rests on, measured rather than recalled, and with + // its own control in the same read: a zero here is a reading only because + // the surviving spelling is present in the same key list. + const keys = specSectionKeys(); + expect(keys).toContain('defaultCollapsed'); + expect(keys).not.toContain('collapsed'); + + // And the refusal is attributable to the NAME, not to a parser that refuses + // everything: the same canonical section parsed clean in control A above. + const parsed = RecordDetailsProps.safeParse({ + sections: [{ ...canonicalSection, collapsed: true }], + }); + expect(parsed.success).toBe(false); + expect(parsed.error?.issues.map((i) => i.code)).toContain('unrecognized_keys'); + expect(refusedKeys(parsed.error?.issues)).toEqual(['collapsed']); + }); + it('keeps the compile-time literals alive at runtime', () => { // Runtime shape is unaffected by the type-level assertions above; these // exist so the file also fails visibly if a literal is ever emptied. expect(canonicalSection.headerColor).toBe('muted'); expect(refusedByTsc.sections).toHaveLength(1); + // item 2's pair. `collapsed` is read through `Object.keys` because the type + // no longer declares it — which is the whole point of the pin above. + expect(Object.keys(retiredSpellingRefusedByTsc.sections?.[0] ?? {})).toContain('collapsed'); + expect(survivingSpellingAccepted.sections?.[0]?.defaultCollapsed).toBe(true); }); }); diff --git a/packages/types/src/record-components.ts b/packages/types/src/record-components.ts index 6a2617028b..a3c22d5e10 100644 --- a/packages/types/src/record-components.ts +++ b/packages/types/src/record-components.ts @@ -117,7 +117,6 @@ export interface RecordDetailsComponentProps { | 'secondary/10' | 'destructive/10'; collapsible?: boolean; - collapsed?: boolean; }>; /** Specific fields to display (overrides auto-detection from object) */ fields?: string[]; From 55c135d573a9be8dc8d47b8af4cf7571517638f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 19:15:04 +0000 Subject: [PATCH 2/2] docs(changeset): correct a false release claim in the 8583 item-2 changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changeset closed with "the six keys this type used to omit landed in the previous release." Item 1's changeset, `.changeset/8583-record-details-section-members.md`, is still PENDING on this PR's own base `c4326fe` (`git show` on it exits 0), and the `17.6.0` section of `packages/types/CHANGELOG.md` carries zero occurrences of `8583` — control: 17 occurrences of `objectui#` in that same section, so the zero is a reading. Both items ship in the SAME release. A changeset is this repository's input to release notes, so a wrong sentence there is wrong in the release notes, read by people who were never in the thread. Prose only: the `**BREAKING**` banner and the FROM/TO stay byte-identical, and no other file moves. Found by the ceiling-tier contract review of PR #8636 (defect 2), verified here rather than taken on report. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w --- .changeset/retire-record-details-section-collapsed.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/retire-record-details-section-collapsed.md b/.changeset/retire-record-details-section-collapsed.md index bf9f569b55..7399c135b4 100644 --- a/.changeset/retire-record-details-section-collapsed.md +++ b/.changeset/retire-record-details-section-collapsed.md @@ -27,5 +27,6 @@ consumer of `@object-ui/types` outside it that wrote `collapsed` is not observable from here, and gets a compile error (TS2353) naming the key — which is why the FROM/TO is spelled out above. -Retires the last member of the divergence objectui#8583 measured; the six keys -this type used to omit landed in the previous release. +Retires the last member of the divergence objectui#8583 measured. The six keys +this type used to omit are item 1 of the same card; its changeset is still +pending, so both halves ship in this release.