Skip to content

Commit 5d0876c

Browse files
claude[bot]claude
andauthored
feat(types): retire the inert dashboard-root title declaration (#7633)
`DashboardComponentSchema.title` was declared under the comment "Dashboard title displayed in the header" — a description of behaviour that had stopped existing: objectui#7509 retired all five dashboard-root `title` read arms under ADR-0049, leaving the key declared, documented as rendering, and inert. The header text is the spec-canonical `label` on `BaseSchema`. Follows the objectui#5830 removal shape, with the one half of it that transfers. `BaseSchema` carries an index signature, so an authored `title` still compiles and a `@ts-expect-error` pin cannot stick; the pinnable effect is that `title` stops being a DECLARED member, which `__tests__/dashboard-title-retired-declaration.test.ts` asserts while keeping `columns` / `widgets` / `header` as positive controls and widget-level `DashboardWidgetSchema.title` — a different, live receiver — as an explicit one. ⛔ No tombstone, unlike `aria`: `@objectstack/spec`'s strict `DashboardSchema` refuses a root `title` as an UNRECOGNIZED key rather than with a named removal message, so there is nothing to inherit by reference and inventing one would assert a spec behaviour that does not exist. objectui's Zod twin is `.passthrough()` and its verdict does not move. `zod-mirror-parity.test.ts`'s `UnmirroredDeclared` ledger shrinks by removing the DECLARATION — the objectui#7129 route, reaching the SPEC-DERIVED half for the first time: 16 entries / 97 keys to 15 / 96, spec-derived 3 / 13 to 2 / 12, and the "no entry in either unmirrored ledger" population 143 to 144. Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 46cde82 commit 5d0876c

4 files changed

Lines changed: 201 additions & 37 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
**Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the
6+
dashboard-root `title` member (objectui#7623).
7+
8+
Its doc comment said "Dashboard title displayed in the header", and that stopped being
9+
true one release earlier: objectui#7509 retired all five dashboard-root `title` read
10+
arms under ADR-0049, leaving the key declared, documented as rendering, and inert. The
11+
header text is the spec-canonical `label` on `BaseSchema`, resolved through
12+
`pickLocalized`.
13+
14+
What an author loses is the **type-level suggestion** only, and there is **no runtime
15+
and no validation behaviour change**. `BaseSchema`'s index signature means an existing
16+
`title:` line still compiles; objectui's Zod twin extends `.passthrough()` `BaseSchema`,
17+
so it parsed a root `title` before this release and still does; and a document validated
18+
against `@objectstack/spec`'s strict `DashboardSchema` was already refused there
19+
(`unrecognized_keys: ['title']`) long before it reached a renderer. Nothing that rendered
20+
stops rendering — the read arms were already gone.
21+
22+
**Unlike `aria` (objectui#5830), no tombstone comes with this.** The spec refuses a root
23+
`title` as an unrecognized key, not with a named removal message, so there is nothing to
24+
inherit by reference and none was invented; the pin
25+
(`packages/types/src/__tests__/dashboard-title-retired-declaration.test.ts`) asserts the
26+
key-set half only — `title` is out of the interface's declared members while `columns`,
27+
`widgets` and `header` stay in.
28+
29+
**Not affected, despite the shared name:** widget-level `DashboardWidget.title` — the
30+
spec's `I18nLabel` on a different receiver, live, declared and read by
31+
`DashboardRenderer`, `DashboardGridLayout` and `DashboardWithConfig`. The pin carries it
32+
as an explicit control.
33+
34+
**Migration:** delete a dashboard-root `title` from any authored dashboard and use
35+
`label` (it accepts a string or a per-locale map). Widget titles are unchanged.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* Retirement pin — dashboard-ROOT `DashboardComponentSchema.title` (objectui#7623).
11+
*
12+
* `title` was declared on objectui's own dashboard component type under the comment
13+
* "Dashboard title displayed in the header". objectui#7509 (PR #7622) retired all
14+
* five dashboard-root `title` read arms under ADR-0049, so the key became declared,
15+
* documented as rendering, and inert — the enforce-or-remove candidate this card
16+
* removes. The header text is the spec-canonical `label` on `BaseSchema`;
17+
* `plugin-dashboard` has no dashboard-root `title` read site left (pinned by that
18+
* package's `__tests__/dashboardAuthoredInputs.test.tsx`).
19+
*
20+
* ## Why this pin has ONE half, where objectui#5830's `aria` pin has two
21+
*
22+
* `dashboard-aria-retired-contract-twins.test.ts` is the model — same interface,
23+
* same removal shape — and its SECOND half asserts that the Zod twin refuses the
24+
* retired key BY NAME, because the spec turned `dashboard.aria` into a tombstone
25+
* that objectui inherits by reference through `SpecDashboardFields`.
26+
*
27+
* ⛔ That half does NOT transfer here, and writing it would be a fabrication.
28+
* `@objectstack/spec`'s strict `DashboardSchema` refuses a root `title` as an
29+
* UNRECOGNIZED key — there is no named removal message to inherit — and objectui's
30+
* twin (`zod/complex.zod.ts`) extends `.passthrough()` `BaseSchema`, so this
31+
* deletion changes no parse verdict at all. The spec's own refusal is already
32+
* measured where it belongs, against the spec schema:
33+
* `plugin-dashboard/src/__tests__/dashboardAuthoredInputs.test.tsx` asserts
34+
* `unrecognizedKeys(specVerdict({ title })).toContain('title')`. Ruled at dispatch
35+
* (2026-09-04): ship the key-set half only; do not invent a tombstone.
36+
*
37+
* ## The trap: why this is a key-set probe and NOT `@ts-expect-error`
38+
*
39+
* ⚠️ `BaseSchema` carries `[key: string]: any`, so an authored `title:` on a
40+
* dashboard literal STILL COMPILES after the member is gone — it falls to the index
41+
* signature. A `@ts-expect-error` pin on an authored literal therefore cannot stick:
42+
* it would pass for the wrong reason before the removal and fail after it. The
43+
* pinnable effect is that `title` stops being a DECLARED member. The probe below
44+
* extracts the interface's literal key set — the index signature is filtered out by
45+
* `string extends K` — and asserts `title` is out while its neighbours stay in.
46+
* Real enforcement because `packages/types/tsconfig.test.json` is chained from this
47+
* package's `type-check` script (objectui#3009).
48+
*/
49+
50+
import { describe, it, expect } from 'vitest';
51+
import type { DashboardComponentSchema, DashboardWidgetSchema } from '../complex';
52+
53+
// Literal (declared) keys of T: `string extends K` is true only for the index
54+
// signature's key, so mapping it to `never` leaves exactly the authored members.
55+
type DeclaredKeys<T> = { [K in keyof T as string extends K ? never : K]: T[K] };
56+
type Declared = keyof DeclaredKeys<DashboardComponentSchema>;
57+
type WidgetDeclared = keyof DeclaredKeys<DashboardWidgetSchema>;
58+
59+
describe('the TS interface no longer declares a dashboard-root `title` (objectui#7623)', () => {
60+
it('`title` is not a declared member; the neighbours it stood beside still are', () => {
61+
// Type-level pin, erased at runtime: if the member came back, the first
62+
// annotation would collapse to `false` and this file would fail `type-check`.
63+
// (Reverse-verified at the PR: with the member restored, `tsc -p
64+
// tsconfig.test.json` goes red on exactly this line.)
65+
const titleNotDeclared: 'title' extends Declared ? false : true = true;
66+
// Positive controls through the same extraction: a probe that saw no members at
67+
// all would also report `title` absent. `columns` and `widgets` are what the
68+
// member was declared between; `header` owns the header block whose rendering
69+
// the deleted doc comment claimed.
70+
const columnsDeclared: 'columns' extends Declared ? true : false = true;
71+
const widgetsDeclared: 'widgets' extends Declared ? true : false = true;
72+
const headerDeclared: 'header' extends Declared ? true : false = true;
73+
expect(titleNotDeclared && columnsDeclared && widgetsDeclared && headerDeclared).toBe(true);
74+
});
75+
76+
it('the WIDGET-level `title` is a different receiver and stays DECLARED', () => {
77+
// ⛔ Not the same key. `DashboardWidgetSchema.title` is the spec's `I18nLabel`,
78+
// live and read (`DashboardRenderer.tsx`, `DashboardGridLayout.tsx`,
79+
// `DashboardWithConfig.tsx`). Without this leg the pin above cannot tell the two
80+
// apart: a sweep that deleted BOTH spellings would still read green.
81+
const widgetTitleDeclared: 'title' extends WidgetDeclared ? true : false = true;
82+
const widgetIdDeclared: 'id' extends WidgetDeclared ? true : false = true;
83+
// Negative control on the widget extraction itself — it discriminates rather
84+
// than answering `true` for everything (which is what a key set degenerated to
85+
// bare `string` would do).
86+
const widgetNonKeyAbsent: 'objectui7623NotAKey' extends WidgetDeclared ? false : true = true;
87+
expect(widgetTitleDeclared && widgetIdDeclared && widgetNonKeyAbsent).toBe(true);
88+
});
89+
});

‎packages/types/src/__tests__/zod-mirror-parity.test.ts‎

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,31 @@
7171
* and 37 / 53 until objectui#7344 swept the string / `z.any()` handler mirrors:
7272
* `DetailSchema` and `DetailViewSchema` entered (one `onBack` each) and
7373
* `CalendarViewSchema` grew by `onEventClick`.
74-
* - **16 entries** in `UnmirroredDeclared`, **97 keys** across them. It read 17 / 98
74+
* - **15 entries** in `UnmirroredDeclared`, **96 keys** across them. It read 17 / 98
7575
* between objectui#6576, which SEEDED the new `ObjectDataTableSchema` pair with its
7676
* one measured key `drillDown` (a pair born ledgered, not growth on an existing
7777
* one), and objectui#7129, which RETIRED `DetailViewSectionSchema.hideEmpty` —
7878
* the ledger's first shrink by removing the DECLARATION rather than by mirroring
79-
* it, and the entry's whole content, so the entry went too. ⚠️ It was
79+
* it, and the entry's whole content, so the entry went too. objectui#7623 took
80+
* that route a second time for `DashboardComponentSchema.title` (16 / 97 →
81+
* 15 / 96), again the entry's whole content — and the first time it reached the
82+
* SPEC-DERIVED half. ⚠️ It was
8083
* **121** when objectui#6058 seeded it; objectui#6152 moved 23 callback-shaped
8184
* keys to the ledger below by RECLASSIFICATION, not by fixing them, and
8285
* objectui#6639 MIRRORED `ObjectGridSchema.title` — one key actually repaired.
8386
* Anything citing "121" as the mirroring debt is citing a number that changed
84-
* meaning — the comparable figure is 96 + 1 mirrored + 1 retired + 23
87+
* meaning — the comparable figure is 95 + 1 mirrored + 2 retired + 23
8588
* reclassified. The full statement is on that ledger.
8689
* - **7 entries** in `RuntimeOnlyDeclared`, **24 keys** across them. Six of the
87-
* seven are a subset of the 16 pairs above; `TreeViewSchema` is NOT — it is
90+
* seven are a subset of the 15 pairs above; `TreeViewSchema` is NOT — it is
8891
* the first pair whose ONLY ledger entry is a runtime-only one
8992
* (objectui#6150 declared `onNodeClick` on an otherwise clean pair), so the
9093
* "no entry in either" population dropped by one to 141 — went to 142 when
91-
* objectui#6576 added two pairs, one of them ledgered, and stands at **143**
92-
* since objectui#7129 retired `DetailViewSectionSchema`'s only ledgered key.
94+
* objectui#6576 added two pairs, one of them ledgered, went to 143 when
95+
* objectui#7129 retired `DetailViewSectionSchema`'s only ledgered key, and
96+
* stands at **144** since objectui#7623 retired `DashboardComponentSchema`'s
97+
* only UNMIRRORED one (that pair keeps its `KnownDrift` entry — this
98+
* population counts entries in the two unmirrored ledgers).
9399
* - 160 − 40 = **120**, the "pairs with no entry" `LedgerMismatch` speaks of.
94100
*
95101
* ## Two ratchets, because the forward comparison has two halves
@@ -904,20 +910,25 @@ interface KnownDrift {
904910
* invites an author to write and the published validator has never heard of.
905911
*
906912
* ## ⚠️ READ THIS BEFORE QUOTING THE NUMBER — 121 became 98 by RECLASSIFICATION,
907-
* ## then 97 by the first REPAIR
913+
* ## then 97 by the first REPAIR, then 96 by RETIREMENT
908914
*
909-
* objectui#6058 seeded this ledger at **121 keys**. It records **97**, and the two
915+
* objectui#6058 seeded this ledger at **121 keys**. It records **96**, and the
910916
* movements are different facts. objectui#6152 measured the 23 callback-shaped
911917
* (`on*`) keys and ruled that mirroring is the wrong remedy for every one of them;
912918
* they moved, intact and still pinned, to `RuntimeOnlyDeclared` below — ⛔ nothing
913919
* was mirrored, no declaration was removed, nothing was waived by that move. Then
914920
* objectui#6639 MIRRORED `ObjectGridSchema.title` (census-directed maintainer
915921
* ruling 2026-08-29, declare branch) — one defect actually repaired, the ledger's
916-
* first shrink by repair.
917-
*
918-
* So: **97 is the mirroring debt; 97 + 1 mirrored + 23 reclassified is what "121"
919-
* used to mean.** A card that cites 121 as the size of the mirroring problem, or
920-
* 98/97 as a shrink from it, is wrong in both directions. objectui#6141 is the
922+
* first shrink by repair. Then objectui#7129 and objectui#7623 each RETIRED a key by
923+
* deleting the DECLARATION (`DetailViewSectionSchema.hideEmpty`;
924+
* `DashboardComponentSchema.title`) — shrinks by removal, which is neither a repair
925+
* of the mirror nor a waiver: the key stops being offered to authors at all.
926+
*
927+
* So: **96 is the mirroring debt; 96 − 1 seeded + 1 mirrored + 2 retired + 23
928+
* reclassified is what "121" used to mean** (the seed is objectui#6576's
929+
* `ObjectDataTableSchema.drillDown`, which was never part of the 121). A card that
930+
* cites 121 as the size of the mirroring problem, or 98/97/96 as a shrink from it,
931+
* is wrong in both directions. objectui#6141 is the
921932
* standing example of what a silently moved count costs — it is why this paragraph
922933
* is in the ledger rather than in a commit message.
923934
*
@@ -945,18 +956,25 @@ interface KnownDrift {
945956
* explicit that forcing the 121 per-key decisions now would be wrong. Two splits
946957
* are recorded here so whoever works them off does not re-derive them:
947958
*
948-
* - **SPEC-DERIVED (3 entries, 13 keys)** — `DashboardComponentSchema`,
949-
* `DashboardWidgetSchema`, `ObjectViewSchema`. ⚠️ objectui#6705 invalidated the
950-
* evidence for ONE of the three: `ObjectViewSchema` is no longer in
959+
* - **SPEC-DERIVED (2 entries, 12 keys)** — `DashboardWidgetSchema`,
960+
* `ObjectViewSchema`. It was 3 / 13 until objectui#7623 RETIRED
961+
* `DashboardComponentSchema.title` by deleting the DECLARATION — the
962+
* objectui#7129 route, reaching this half for the first time. That key was never
963+
* going to be answered by #2231 unification: the spec's strict `DashboardSchema`
964+
* refuses a root `title` outright, and objectui#7509 had already retired every
965+
* read of it, so the local declaration was offering a member the spec models
966+
* nowhere and no renderer consumed. ⚠️ objectui#6705 invalidated the
967+
* evidence for ONE of the two that remain: `ObjectViewSchema` is no longer in
951968
* `SPEC_DERIVED_PAIRS` below, because it never referenced a spec schema — it is
952969
* `BaseSchema.extend({…})` of local literals, and the pre-#6705 text scanner
953-
* charged it a neighbouring private const's `Spec…` token. The split is left
954-
* STANDING as written, counts and all: re-routing those keys from #2231's
970+
* charged it a neighbouring private const's `Spec…` token. That misclassification
971+
* is left STANDING as #6705 found it — neither the entry's classification nor its
972+
* key count moved for it: re-routing those keys from #2231's
955973
* unification question to a local mirror edit is a remedy decision on the
956974
* `UnmirroredDeclared` ledger, which #6705 was fenced out of. Whoever works
957975
* this split off must re-derive `ObjectViewSchema`'s side first.
958-
* For the other two the reading is unchanged:
959-
* their mirror takes its shape BY REFERENCE from `@objectstack/spec`. An
976+
* For `DashboardWidgetSchema` the reading is unchanged:
977+
* its mirror takes its shape BY REFERENCE from `@objectstack/spec`. An
960978
* unmirrored declared key there means the LOCAL declaration carries members the
961979
* spec schema does not model, which is objectui#2231's unification question and
962980
* NOT a local mirror edit. They are marked, not exempted: exempting them in the
@@ -970,10 +988,11 @@ interface KnownDrift {
970988
* (107 → 85; 84 since objectui#6639 mirrored `ObjectGridSchema.title`, 83 since
971989
* objectui#7129 retired one). The pair COUNTS did not move under the
972990
* reclassification — every affected pair kept keys here — so the split read 16
973-
* entries / 97 keys after it. Two later changes moved the entry count itself:
991+
* entries / 97 keys after it. Three later changes moved the entry count itself:
974992
* objectui#6576 SEEDED `ObjectDataTableSchema` (a 17th entry, in neither half
975-
* above), and objectui#7129 RETIRED an entry from the LOCAL half. The ledger now
976-
* totals **16 entries / 97 keys** — 3 / 13 spec-derived, 12 / 83 local, plus the
993+
* above), objectui#7129 RETIRED an entry from the LOCAL half, and objectui#7623
994+
* RETIRED one from the SPEC-DERIVED half (13 → 12 keys there). The ledger now
995+
* totals **15 entries / 96 keys** — 2 / 12 spec-derived, 12 / 83 local, plus the
977996
* seeded pair.
978997
*
979998
* ## How this was measured, and the trap that makes the number hard to get
@@ -1008,14 +1027,17 @@ interface UnmirroredDeclared {
10081027
* heard of.
10091028
*/
10101029
'complex.zod.ts#ChatbotSchema': 'displayMode' | 'floatingConfig' | 'requestBody';
1030+
// `complex.zod.ts#DashboardComponentSchema` recorded `title` here (SPEC-DERIVED)
1031+
// until objectui#7623 RETIRED the declaration — the objectui#7129 route, not a
1032+
// mirror edit: the spec's strict `DashboardSchema` refuses a root `title` outright,
1033+
// and objectui#7509 had already retired every read of it. The pair keeps its
1034+
// `KnownDrift` entry above (three TYPE-drifted keys) and records nothing here.
10111035
/**
10121036
* SPEC-DERIVED → objectui#2231. The mirror takes its shape by reference from
1013-
* `@objectstack/spec`, so `title` is a member the LOCAL declaration carries and the
1014-
* spec schema does not model. Its three TYPE-drifted keys are in `KnownDrift` above
1015-
* and route the same way.
1037+
* `@objectstack/spec`, so these are members the LOCAL declaration carries and the
1038+
* spec schema does not model. Its one TYPE-drifted key is in `KnownDrift` above and
1039+
* routes the same way.
10161040
*/
1017-
'complex.zod.ts#DashboardComponentSchema': 'title';
1018-
/** SPEC-DERIVED → objectui#2231, same reading as `DashboardComponentSchema`. */
10191041
'complex.zod.ts#DashboardWidgetSchema': 'pagination' | 'searchable';
10201042
/** LOCAL. Declared in `../data-display.ts`, absent from the mirror. */
10211043
'data-display.zod.ts#ChartSchema': 'drillDown';
@@ -1399,14 +1421,16 @@ export const assertionDriftMatchesLedger: never = 0 as unknown as LedgerMismatch
13991421

14001422
/**
14011423
* The SECOND half of the forward comparison: every pair's declared-but-unmirrored
1402-
* key set equals what the two ledgers TOGETHER record for it — `never` for the 143
1403-
* pairs with no entry in either (160 − 17). Six of `RuntimeOnlyDeclared`'s seven
1404-
* pairs are a measured subset of `UnmirroredDeclared`'s 16, so objectui#6152's
1424+
* key set equals what the two ledgers TOGETHER record for it — `never` for the 144
1425+
* pairs with no entry in either (160 − 16). Six of `RuntimeOnlyDeclared`'s seven
1426+
* pairs are a measured subset of `UnmirroredDeclared`'s 15, so objectui#6152's
14051427
* reclassification left the clean population unchanged; objectui#6150 then added
1406-
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 17
1407-
* pairs and not 16. (objectui#6576 took the union to 18; objectui#7129 brought it
1408-
* back to 17 by retiring `DetailViewSectionSchema`'s only ledgered key, leaving
1409-
* that pair with no entry in either half.)
1428+
* `TreeViewSchema`, whose only entry is runtime-only, which is why the union is 16
1429+
* pairs and not 15. (objectui#6576 took the union to 18; objectui#7129 brought it
1430+
* back to 17 by retiring `DetailViewSectionSchema`'s only ledgered key, and
1431+
* objectui#7623 to 16 by retiring `DashboardComponentSchema`'s — each leaving its
1432+
* pair with no entry in either half. ⚠️ That pair still carries a `KnownDrift`
1433+
* entry: "no entry in either" is about the two UNMIRRORED ledgers.)
14101434
*
14111435
* ⚠️ **The discriminating signal is the PER-PAIR set, not this file's exit code.**
14121436
* The exit code is a whole-file verdict, so it moves only while the rest of the

0 commit comments

Comments
 (0)