Skip to content

Commit 99527f4

Browse files
committed
feat(plugin-list)!: the renderers stop reading the list-view keys every door refuses (objectui#6152 round 14)
Retires classes A, B, C, E and F of the round-13 census on every route that read them: ListView, app-shell's object-page relays, plugin-view's generateViewSchema and ObjectTimeline's nested date rung. - A: the pre-#2231 alias rungs (kanban groupField / cardFields, gallery imageField, timeline dateField, and calendar dateField read as a timeline axis) in ListView's branches and capability gate, the relays, the object-view element and ObjectTimeline. - B: calendar.defaultView's lift and the ...restKanban / ...restCalendar spreads; the declared summarizeField, colorField and allDayField are read by name instead. - C: the tree titleField label rung and the tree, gantt and options.grid spreads; gantt (and plugin-view's timeline) go through typed total tables of the spec block's keys. A block key named like a node key no longer overrides the node. - E: the projection collectors list the blocks' spec keys only. - F: the types prose, the refusal messages and two pending changesets' dated notes say the readers moved. The core fold (normalize-list-view.ts) is parked until PR objectui#12036 lands. Class D (the legacy chart binding) is the next round. Claude-Session: https://claude.ai/code/session_01CijGnfWLxTLUFcJkY2ouUw Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8de8ba2 commit 99527f4

32 files changed

Lines changed: 1216 additions & 456 deletions

‎.changeset/6152-list-view-blocks-strict.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ before; only authored metadata meets the refusal. The legacy `options.KIND` bag
4343
unchanged. It is now the spec's list-overlay bag by reference, with these four blocks in it, so each
4444
refusal above also applies under `options.KIND`, with the same message; see
4545
`.changeset/6152-list-view-options-bag.md`. The renderers' reads still have not moved.
46+
47+
**Note added 2026-10-09 (objectui#6152 round 14):** the renderers' reads have moved. No renderer reads
48+
the four aliases, `calendar.defaultView` or an undeclared key of these blocks any longer, so a view
49+
stored with one of them renders without what that key used to bind; see
50+
`.changeset/6152-list-view-readers-retired.md` for what changes on screen and what to write instead.

‎.changeset/6152-list-view-options-bag.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,10 @@ chart, and no longer show the bag.
7070
What did not move: the renderers' reads. `ListView` still merges each `options.KIND` under the
7171
top-level block and still reads the legacy spellings, so a view stored before these doors closed
7272
renders as before; only authored metadata meets the refusal.
73+
74+
**Note added 2026-10-09 (objectui#6152 round 14):** the renderers' reads have moved. `ListView`
75+
still merges each `options.KIND` under the top-level block, but it reads each kind's declared keys
76+
only: the legacy spellings, `options.grid` and the undeclared keys a bag carries are no longer read,
77+
so a view stored with one of them renders without what that key used to bind; see
78+
`.changeset/6152-list-view-readers-retired.md`. The legacy chart axes (`options.chart.xAxisField`
79+
and the rest) are still read until the next round on objectui#6152.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'@object-ui/plugin-list': minor
3+
'@object-ui/plugin-view': minor
4+
'@object-ui/app-shell': minor
5+
'@object-ui/plugin-timeline': minor
6+
'@object-ui/types': patch
7+
---
8+
9+
feat(plugin-list)!: the renderers stop reading the list-view keys every door refuses: the pre-#2231 aliases, `calendar.defaultView`, and the undeclared keys a per-kind block carries (objectui#6152, round 14)
10+
11+
Clause-②: no
12+
13+
**Changed (breaking for a stored view that carries a refused key).** Rounds 11 and 12 of
14+
objectui#6152 closed the doors: `@object-ui/types`, `objectui validate` and `@objectstack/spec`'s
15+
view write door refuse these keys on a list view's per-kind blocks, top-level and under the legacy
16+
`options` bag. The renderers kept reading them, so a view stored before the doors closed still
17+
rendered as written. They no longer read them, on every route that did: `ListView`, the object
18+
page's relay (`@object-ui/app-shell`), `ObjectView`'s own views (`@object-ui/plugin-view`) and
19+
`ObjectTimeline`. A stored view that carries one of these keys still renders, without what the key
20+
used to bind. Write the spec's key instead:
21+
22+
| stored key (either nesting) | what renders now | write instead |
23+
| :--- | :--- | :--- |
24+
| `kanban.groupField` | lanes from the object's declared lifecycle field, as for a view that names none | `kanban.groupByField` |
25+
| `kanban.cardFields` | cards show `kanban.columns`, or the view's own fields | `kanban.columns` |
26+
| `gallery.imageField` | no cover binding: `ObjectGallery` tries `image`, and the Gallery view is not offered | `gallery.coverField` |
27+
| `timeline.dateField`, and `calendar.dateField` read as a timeline axis | no timeline axis: the timeline's refusal names the keys to write, and the Timeline view is not offered | `timeline.startDateField` (or `calendar.startDateField`) |
28+
| `calendar.defaultView` | the calendar opens on its own default view | nothing on a list view: the initial mode is the `object-calendar` element's flat `defaultView` |
29+
| `kanban.swimlaneField` and any other undeclared kanban key | not drawn | nothing: the spec's kanban block has no swimlane |
30+
| `tree.titleField` | the tree labels by `name` | `tree.labelField` |
31+
| an undeclared key under `calendar`, `tree` or `gantt`, or anything under `options.grid` | not forwarded to the view | the block's declared key, or a top-level key of the view for a grid |
32+
33+
The declared keys keep their route: `kanban.summarizeField`, `calendar.colorField` and
34+
`calendar.allDayField` are now read by name where the removed spreads used to carry them, and every
35+
`gantt` key the spec declares reaches the gantt through a typed table that `tsc` keeps total. A
36+
block key named like a node key (`objectName`, `filter`) can no longer override the node's own
37+
value. `ListView`'s projection collectors stop asking the server for the fields these keys named.
38+
39+
**Not measured: production.** The census before this change (objectui#6152 round 13) found no
40+
writer and no stored row carrying these keys in any repository corpus a seat can reach, with
41+
positive controls; stored view and page metadata in deployments was not measured. A row that
42+
carries one of these keys renders as the table says until it is re-saved with the key on the
43+
right.
44+
45+
Not in this change: the legacy chart axes (`chart.xAxisField`, `yAxisFields`, `categoryField`,
46+
`valueField`, `aggregation`), which `ListView` still reads until the next round on objectui#6152.
47+
48+
- `@object-ui/types`: the refusal messages of these keys no longer say a stored view still renders
49+
through the alias, and the kanban `groupBy` refusal no longer suggests the refused `groupField`.

‎apps/console/src/__tests__/timelineAxisRefusalReach-7070.test.tsx‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,17 @@ describe('ListView → ObjectTimeline: an undeclared axis reaches the refusal (o
159159
expect(refusal()!.textContent ?? '').toContain('timeline.startDateField');
160160
});
161161

162-
it('CONTROL: the LEGACY `timeline.dateField` alias still renders', async () => {
163-
// The alias is resolved by the face, not by a floor. If step ③ had taken it
164-
// with the fabrication, a pre-#2231 view would start refusing — a regression
165-
// the ruling did not order.
166-
await mountListView({ ...LIST_BASE, timeline: { dateField: 'start_date' } });
167-
await waitFor(() => expect(canvas()).not.toBeNull());
168-
expect(refusal()).toBeNull();
162+
it('a stored LEGACY `timeline.dateField` reaches the refusal too (objectui#6152 round 14)', async () => {
163+
// Step ③ kept the alias (the ruling ordered no regression for a pre-#2231
164+
// view), and this arm pinned that it still rendered. objectui#6152 round 14
165+
// retired the alias reads on their own ruling: every list-view door refuses
166+
// the key by name. A stored one now binds no axis, so it gets the refusal,
167+
// which names the key to write. Written in the legacy `options` bag, which no
168+
// fold reaches; the TOP-LEVEL spelling joins it with that round's core leg.
169+
await mountListView({ ...LIST_BASE, options: { timeline: { dateField: 'start_date' } } });
170+
await waitFor(() => expect(refusal()).not.toBeNull());
171+
expect(canvas()).toBeNull();
172+
expect(refusal()!.textContent ?? '').toContain('timeline.startDateField');
169173
});
170174

171175
it('CONTROL: a CALENDAR-bound view still renders its timeline', async () => {
@@ -195,9 +199,10 @@ describe('ObjectView → ObjectTimeline: the second face reaches it too (objectu
195199
expect(screen.queryByText('Spring Launch')).toBeNull();
196200
});
197201

198-
it('CONTROL: the LEGACY `timeline.dateField` alias still renders here too', async () => {
202+
it('a stored LEGACY `timeline.dateField` reaches the refusal here too (objectui#6152 round 14)', async () => {
199203
await mountObjectView({ timeline: { dateField: 'start_date' } });
200-
await waitFor(() => expect(canvas()).not.toBeNull());
201-
expect(refusal()).toBeNull();
204+
await waitFor(() => expect(refusal()).not.toBeNull());
205+
expect(canvas()).toBeNull();
206+
expect(screen.queryByText('Spring Launch')).toBeNull();
202207
});
203208
});

‎packages/app-shell/src/views/ObjectView.galleryBinding-7547.test.tsx‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,19 +102,20 @@ describe('galleryViewOptions — the object page forwards, it does not invent (o
102102
expect(out).not.toHaveProperty('imageField');
103103
});
104104

105-
it('CONTROL: a stored legacy `imageField` still answers `coverField`', () => {
106-
// The legacy READ is not this round's to retire: a row stored before the
107-
// view write door judged it can still carry `imageField`, and its cover
108-
// still binds. The block spread forwards the stored key as it was stored;
109-
// nothing here SYNTHESIZES it.
105+
it('a stored legacy `imageField` answers nothing (objectui#6152 round 14)', () => {
106+
// Round 12 left the legacy READ in place for rows stored before the view
107+
// write door judged the bag; round 14 retired it. The cover binding is read
108+
// from `coverField` alone, so no `coverField` is synthesized from the alias.
109+
// The block spread still forwards the stored key as it was stored (a
110+
// carrier, not a reader): nothing downstream reads it any more.
110111
const out = galleryViewOptions({ gallery: { imageField: 'logo' } });
111-
expect(out.coverField).toBe('logo');
112+
expect(out).not.toHaveProperty('coverField');
112113
expect(out.imageField).toBe('logo');
113114
});
114115

115116
it('CONTROL: both spellings stored — `coverField` keeps its own value', () => {
116-
// `coverField` prefers itself, then the legacy spelling; the stored legacy
117-
// key rides the spread unchanged.
117+
// `coverField` is read from itself alone; the stored legacy key rides the
118+
// spread unchanged.
118119
const out = galleryViewOptions({ gallery: { imageField: 'logo', coverField: 'photo' } });
119120
expect(out.coverField).toBe('photo');
120121
expect(out.imageField).toBe('logo');

‎packages/app-shell/src/views/ObjectView.kanbanGroupByRetired-8213.test.tsx‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,13 @@ describe('the object page emits ONE lane spelling — the spec\'s (objectui#8213
148148
expect(refusedKeys(emitted)).not.toContain('groupBy');
149149
});
150150

151-
it('for a view that declared the LEGACY alias', () => {
152-
// The alias READ is untouched by this card: the value arrives legacy and
153-
// leaves canonical, and leaves under one key only.
154-
const emitted = kanbanViewOptions({ kanban: { groupField: 'stage' } }, OBJECT_WITH_STAGE);
151+
it('for a view that stored the retired LEGACY alias', () => {
152+
// The alias READ was untouched by this card; objectui#6152 round 14 retired
153+
// it. The lane is the detector's (a value the alias does not name), and it
154+
// still leaves under one key only.
155+
const emitted = kanbanViewOptions({ kanban: { groupField: 'name' } }, OBJECT_WITH_STAGE);
155156
expect(laneKeysOf(emitted)).toEqual(['groupByField']);
157+
expect(emitted.groupByField).toBe('stage');
156158
});
157159

158160
it('for a lane the ADR-0085 detector supplied', () => {
@@ -172,9 +174,10 @@ describe('the object page emits ONE lane spelling — the spec\'s (objectui#8213
172174
describe('the one reader of `groupBy` still gets the same value (objectui#8213)', () => {
173175
it('carries the lane under `groupByField`, the rung the collectors read first', () => {
174176
// The census question, stated as a property rather than as a claim about
175-
// call sites. `ListView`'s two projection/expand collectors list
177+
// call sites. `ListView`'s two projection/expand collectors listed
176178
// `v.groupByField, v.groupField, v.groupBy` as candidates for the SAME lane
177-
// value; deleting the third is only safe because the first carries it. If a
179+
// value; deleting the third was only safe because the first carries it, and
180+
// since objectui#6152 round 14 the first is the only one they list. If a
178181
// future edit moved the value onto `groupBy` alone, this reddens.
179182
expect(kanbanViewOptions({}, OBJECT_WITH_STAGE).groupByField).toBe('stage');
180183
});

‎packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
* side of that boundary, so nothing folded it and the alias read was
3232
* load-bearing purely because of where this producer wrote.
3333
*
34-
* ⚠️ THE ALIAS IS NOT RETIRED AND NO ALIAS READ WAS TOUCHED. Stored metadata
35-
* still authors `groupField`; `ListView` resolving `groupByField || groupField`
36-
* is exactly why the sibling could drop the alias write. The arms below assert
37-
* what this face WRITES, never what any face reads.
34+
* ⚠️ THIS CARD RETIRED NO ALIAS READ: it changed what this face WRITES. The
35+
* reads went later, on their own ruling (objectui#6152 round 14): this face,
36+
* `ListView` and `ObjectView` read the lane from `groupByField` alone now. The
37+
* legacy-alias arm below was re-judged then: it pins that the alias names no
38+
* lane, with a value the detector would never pick.
3839
*
3940
* ⚠️ `groupBy` WAS STILL WRITTEN WHEN THIS FILE LANDED, and is not any more:
4041
* objectui#8213 ran the producer census this file deferred and retired the
@@ -80,11 +81,12 @@ describe('the object page writes the SPEC lane key (objectui#8193)', () => {
8081
expect(out).not.toHaveProperty('groupField');
8182
});
8283

83-
it('emits `groupByField` for a view that declared the LEGACY alias', () => {
84-
// Reading the alias is untouched — a view that authored `groupField` still
85-
// resolves its lane. What changed is that the alias stops being propagated:
86-
// the value comes in legacy and leaves canonical.
87-
const out = kanbanViewOptions({ kanban: { groupField: 'stage' } }, OBJECT_WITH_STAGE);
84+
it('a view that stored the retired LEGACY alias gets the detector\'s lane, not the alias\'s (objectui#6152 round 14)', () => {
85+
// When this card landed the alias was still READ (the value came in legacy
86+
// and left canonical), and this arm wrote `stage`, the very field the
87+
// detector picks, so it could not tell a read alias from an ignored one.
88+
// Re-judged with a value the detector never picks: the alias names no lane.
89+
const out = kanbanViewOptions({ kanban: { groupField: 'name' } }, OBJECT_WITH_STAGE);
8890
expect(out.groupByField).toBe('stage');
8991
expect(out).not.toHaveProperty('groupField');
9092
});

‎packages/app-shell/src/views/ObjectView.timelineBinding.test.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,12 @@ describe('timelineViewOptions — the object page forwards, it does not resolve
5151
expect(out.scale).toBe('month');
5252
});
5353

54-
it('promotes the legacy `dateField` alias onto the spec key', () => {
54+
it('does NOT promote the retired `dateField` alias onto the spec key (objectui#6152 round 14)', () => {
55+
// It used to: this face resolved `startDateField || dateField`. Every
56+
// list-view door refuses the alias by name and `ListView` no longer reads
57+
// it either, so the relay restates the spec key alone.
5558
expect(timelineViewOptions({ timeline: { dateField: 'start_date' } }).startDateField)
56-
.toBe('start_date');
59+
.toBeUndefined();
5760
});
5861

5962
it('invents NO date field when the view declares none', () => {

‎packages/app-shell/src/views/ObjectView.titleFieldConvergence.test.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,17 @@ describe('ObjectView view-config `titleField` — the middle rung is gone (objec
297297
});
298298
});
299299

300-
it("CONTROL: the tree's second view-declared rung (`tree.titleField`) still answers", async () => {
301-
// The tree seam is the only one with TWO view-declared rungs; dropping the
302-
// object rung must not have collapsed them into one.
300+
it("the tree's retired `tree.titleField` rung answers nothing (objectui#6152 round 14)", async () => {
301+
// The tree seam used to be the only one with TWO view-declared rungs, and
302+
// this case pinned that dropping the object rung had not collapsed them. The
303+
// second rung read a key the spec refuses by name on the tree block
304+
// (objectui#8841), and objectui#6152 round 14 retired it, so a stored
305+
// `tree.titleField` now floors at 'name' like an undeclared label. The
306+
// declared `labelField` still wins (the CONTROL above).
303307
const seams = await resolveSeams(
304308
objectsWith({ titleField: REJECTED }, { tree: { titleField: 'v_tree_title' } }),
305309
);
306-
expect(seams.tree).toBe('v_tree_title');
310+
expect(seams.tree).toBe('name');
307311
});
308312
});
309313

0 commit comments

Comments
 (0)