Skip to content

Commit ff3d631

Browse files
committed
fix(spec): name the FACE before every statement, and take the changeset to patch
At-tier FAIL, grounds 1-3. Grounds 1 and 2 were one error: the previous round fixed a face-swap by swapping the other way. A view-face fact had sat on the element-face key; the rewrite put an element-face key under a view-face fact. Re-measured first-hand at pin 87af769: - ELEMENT FACE, a node's own nested timeline.limit: read on NO route. ObjectTimeline binds timelineConfig = schema.timeline (:262) and reads exactly eight members off it -- startDateField, dateField, titleField, endDateField, groupByField, colorField, metaFields (:520, behind an `as any` cast, which is why a timelineConfig?.x sweep under-counts) and scale. No spread of a node's own timeline block exists anywhere (0 hits; lit control = the view-block spreads, which fire). The gate mapping is limit: 'limit' (plugin-timeline/src/index.tsx:333) -- flat. - VIEW FACE, a ListViewSchema document's timeline.limit: that is the route-dependent one. ObjectView.tsx:1725 flattens it onto the node's flat limit (and that node then carries no timeline block at all); ListView.tsx:3084 keeps it nested and unread. So every sentence now names its face first, in both docblocks, the published describe, the test comment and the changeset -- the distinction is stated rather than navigated, because three rounds have failed on it. Also flagged, not fixed: a third route neither the card nor the first two reviews described -- a hand-authored object-timeline node reaching ObjectTimeline through SchemaRenderer with no adapter. Its nested timeline.limit is unread there too, and that is the route an author face is written for. The second instrument is now recorded by its PREDICATE (a spread whose target is the object literal an adapter returns as the node) rather than by its count, so the reading can be re-taken without re-deriving the rule. Changeset level to patch per the coordinator's correction: the gate's two remedies are not interchangeable and this is the second one -- the level was right and the declaration was wrong. The "Why minor" paragraph is dropped. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
1 parent 99fd9f9 commit ff3d631

5 files changed

Lines changed: 127 additions & 75 deletions

File tree

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"@objectstack/spec": minor
2+
"@objectstack/spec": patch
33
---
44

55
fix(spec): state the row-cap guard `ElementDataSourceGate` implements, and record where the per-kind view `limit` actually lands (#19228)
@@ -9,13 +9,6 @@ to the same values before and after. ⛔ No `.default()` moves, ⛔ no precedenc
99
of the per-kind view `limit`, a view's `pagination.pageSize` and a component's flat `limit`
1010
should win is the open half of #19228 and is not answered here.
1111

12-
⚠️ **Why `minor` when nothing behavioural moved.** `check-changeset-no-major`'s LEVEL axis
13-
(#16055 / #16776) requires a PR that declares `Clause-②: yes` to grade at least one package
14-
whose published source it moves `minor` or above, and this PR carries that declaration. The bump
15-
is therefore the gate's floor, ⛔ not a description of a behaviour change: no key is added,
16-
removed or renamed, no export moves, and every document that parsed before parses to the same
17-
value after. Upgrading gains corrected published prose and nothing else.
18-
1912
## What the published text said, and what the consumer does
2013

2114
`ObjectKanbanPropsSchema.limit` told authors that a bound view's `pagination.pageSize`
@@ -29,20 +22,28 @@ it. The view half is likewise not `pagination.pageSize` alone: `savedViewLimit`
2922
(`core/src/data-scope/element-data-source.ts:237-241`). The describe now states the guard as
3023
implemented.
3124

32-
## Where the per-kind view `limit` lands
25+
## Where the per-kind VIEW `limit` lands
26+
27+
⚠️ Two different keys are easy to confuse here, so each statement names its face. The **view
28+
face** is a `ListViewSchema` document's `kanban` / `gallery` / `timeline` block — that is where
29+
this key lives. The **element face** is a page component node's own flat `limit`, declared in
30+
`component.zod.ts`, and that is the key every renderer actually reads. An adapter turns the
31+
first into the second.
3332

3433
The adapters spread a view's per-kind block FLAT onto the node they generate — `...restKanban`
3534
(`plugin-list/src/ListView.tsx:2979`, `plugin-view/src/ObjectView.tsx:1638`; neither destructure
36-
strips `limit`) and `...(viewOptions.timeline || {})` / `...(viewOptions.gallery || {})`
37-
(`ObjectView.tsx:1725` / `:1697`). So a view's `kanban.limit` or `timeline.limit` — including the
38-
100 the applied default materializes — becomes the flat `limit` that `ObjectKanban.tsx:553` and
39-
`ObjectTimeline.tsx:279` read. The `$top` it would govern is not issued on either adapter route
40-
today, because both hosts hand rows down as a React `data` prop and both children short-circuit
41-
their own fetch; ⛔ that is a statement about the query, not about the key being unread.
42-
43-
**Gallery is the exception**, and only gallery: the block is spread flat by `ObjectView.tsx:1697`
44-
and `ObjectGallery.tsx` contains no `limit` at all.
45-
46-
⚠️ For authors: `timeline.limit` inside an `object-timeline` node's `timeline` block reaches the
47-
rail on one adapter route and not on the other. Write the flat `limit` beside it when you mean
48-
the row cap.
35+
strips `limit`) and `...(viewOptions.gallery || {})` / `...(viewOptions.timeline || {})`
36+
(`ObjectView.tsx:1697` / `:1725`). So a view's `kanban.limit` — including the 100 the applied
37+
default materializes — becomes the node's flat `limit`, which `ObjectKanban.tsx:553` reads. A
38+
view's `timeline.limit` is route-dependent: `plugin-view` flattens it and `ObjectTimeline.tsx:279`
39+
reads it, while `plugin-list` forwards the block nested, where nothing does. A view's
40+
`gallery.limit` is flattened too and read by nobody — `ObjectGallery.tsx` contains no `limit` at
41+
all.
42+
43+
Where it is read, the `$top` it would govern is still not issued on either adapter route today,
44+
because both hosts hand rows down as a React `data` prop and both children short-circuit their
45+
own fetch; ⛔ that is a statement about the query, not about the key being unread.
46+
47+
⚠️ For authors of an `object-timeline` NODE: a `limit` written inside that node's own `timeline`
48+
block is read by no renderer on any route — the rail is capped by the flat `limit` beside it,
49+
which is also the only one a bound `dataSource` lowers into. Write the flat one.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ View filter rule
803803
| Property | Type | Required | Description |
804804
| :--- | :--- | :--- | :--- |
805805
| **objectName** | `string` | optional | Object this timeline binds to. Optional because the component-level `dataSource` binding can supply the object instead — this block registers through `ElementDataSourceGate`, which lowers the binding onto this key before the renderer sees the node |
806-
| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: `{ startDateField, endDateField, titleField, groupByField, colorField, scale, limit }`. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member whose effect depends on which adapter rendered the node: one spreads this block flat, so it becomes the flat `limit` beside this key, and one forwards it nested, where nothing reads it. Write the flat `limit` when you mean the row cap for the rail |
806+
| **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: `{ startDateField, endDateField, titleField, groupByField, colorField, scale, limit }`. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member of this block NO renderer reads on any route: the rail is capped by the FLAT `limit` beside this key, which is also the only one a bound `dataSource` lowers into. A `limit` written inside this block is accepted, defaulted to 100, and never read |
807807
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` |
808808
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order for the fetched entries — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` |
809809
| **limit** | `integer` | optional | Maximum number of records loaded onto the rail (row cap); lowered to the query's top-level `$top` (renderer default 100). A timeline renders one rail with no pagination control, so this is the author's window rather than a page size |

‎packages/spec/src/ui/component.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3833,12 +3833,13 @@ describe('row caps on the object-bound blocks — what #19228 recorded', () => {
38333833

38343834
it('materializes the NESTED `timeline.limit` on a node whose flat `limit` stays absent', () => {
38353835
// The shape the record is about: one strictObject, two authorable row
3836-
// caps, and an applied default on the nested one. ⚠️ Where that 100 then
3837-
// GOES is route-dependent and is deliberately not asserted here: at the
3838-
// pin `ObjectView.tsx:1725` spreads the block FLAT (so it becomes the flat
3839-
// `limit` the rail reads) while `ListView.tsx:3084` forwards it nested (so
3840-
// nothing reads it). This pin is about the PARSE, which is the only half a
3841-
// schema owns.
3836+
// caps, and an applied default on the nested one. ⚠️ Faces, because this
3837+
// card keeps confusing them: the NESTED key asserted below is the ELEMENT
3838+
// face, and at the pin no renderer reads it on any route. The
3839+
// route-dependent one is a VIEW document's `timeline.limit`, a different
3840+
// key on a different document, which `ObjectView.tsx:1725` flattens onto
3841+
// a generated node's FLAT `limit`. Neither is asserted here: this pin is
3842+
// about the PARSE, which is the only half a schema owns.
38423843
const result = timeline.safeParse({
38433844
objectName: 'task',
38443845
timeline: { startDateField: 'start_date', titleField: 'name' },

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3113,8 +3113,12 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
31133113
* ⛔ This note reports the guard; it picks no precedence.
31143114
*
31153115
* ⚠️ It takes a THIRD door, and missing it is what the at-tier review of
3116-
* #19533 caught. The ADAPTERS spread the view's kanban block FLAT onto the
3117-
* generated node — `plugin-list/src/ListView.tsx:2979` and
3116+
* #19533 caught. Stated with its faces named, because that is where this
3117+
* card keeps going wrong: `kanban.limit` is a VIEW-FACE key (a member of a
3118+
* `ListViewSchema` document's `kanban` block) and THIS `limit` is the
3119+
* ELEMENT-FACE key on the node; the third door is the one that turns the
3120+
* first into the second. The ADAPTERS spread the view's kanban block FLAT
3121+
* onto the generated node — `plugin-list/src/ListView.tsx:2979` and
31183122
* `plugin-view/src/ObjectView.tsx:1638`, both `...restKanban`, and neither
31193123
* destructure (`ListView.tsx:2952`, `ObjectView.tsx:1579`) strips `limit`.
31203124
* So a view's `kanban.limit`, INCLUDING the 100 its applied default
@@ -3983,10 +3987,10 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [
39833987
* the header names: `:407`, the fetch's one top-level `$top`, through
39843988
* `resolveRowLimit(schema.limit, DEFAULT_TIMELINE_LIMIT)` with the default
39853989
* `100` at `:29` and the refused-cap diagnostic at `:279`. ⚠️ It is this FLAT
3986-
* key that is read, and the nested `timeline` block REACHES it: the
3987-
* `plugin-view` adapter spreads that block flat at `ObjectView.tsx:1725`
3988-
* (`plugin-list` forwards it nested instead). See the `timeline` door below),
3989-
* `items` (`:170`, `:247`, `:299`,
3990+
* key that is read — this node's own nested `timeline.limit` is not, on any
3991+
* route. What reaches this flat key is a VIEW document's `timeline.limit`,
3992+
* flattened onto the generated node by `ObjectView.tsx:1725`; see the
3993+
* `timeline` door below), `items` (`:170`, `:247`, `:299`,
39903994
* `:480` — the authored pass-through that short-circuits the object query),
39913995
* `data` (`:171`, `:247`, `:254`, `:256` — the pre-fetched record source,
39923996
* read off REACT PROPS rather than `schema`; the door's own docblock carries
@@ -4016,18 +4020,41 @@ const OBJECT_TIMELINE_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [
40164020
* strictObject, beside the flat `limit` below — two authorable row caps on one
40174021
* node, and the nested one carries an APPLIED default, so every parsed node
40184022
* with a `timeline` block materializes `timeline.limit: 100` (#19228).
4019-
* Measured at the pin `87af769e9`, 2026-09-21T08:05Z, the two adapter routes
4020-
* DISAGREE about that key and both readings are needed:
4021-
* - `plugin-view/src/ObjectView.tsx:1725` spreads the whole block FLAT
4022-
* (`...(viewOptions.timeline || {})`), so `timeline.limit` becomes the flat
4023-
* `limit` that `ObjectTimeline.tsx:279` reads and `:407` would lower.
4024-
* - `plugin-list/src/ListView.tsx:3062-3117` forwards it NESTED (`:3084`) and
4025-
* hoists only `startDateField` / `endDateField` / `titleField` /
4026-
* `groupByField` / `colorField` / `scale` — `limit` is not among them, so
4027-
* on that route it stays nested and nothing reads it.
4028-
* ⛔ So 「inert」 is route-dependent and was stated flatly here before the
4029-
* at-tier review of #19533 measured the spread. Recorded, not repaired: which
4030-
* key should carry a timeline's row cap is the open half of #19228.
4023+
*
4024+
* ⛔ THE TWO ARE ON DIFFERENT FACES, and #19228 has now gone wrong on that
4025+
* boundary in both directions — once putting a view-face fact on the element
4026+
* key, once the reverse. So each statement names its face first. Measured at
4027+
* the pin `87af769e9`, 2026-09-21T09:15Z:
4028+
* - **ELEMENT FACE — THIS node's own nested `timeline.limit`: read on NO
4029+
* route.** `ObjectTimeline` binds `timelineConfig = schema.timeline`
4030+
* (`ObjectTimeline.tsx:262`) and reads exactly eight members off it —
4031+
* `startDateField`, `dateField`, `titleField`, `endDateField`,
4032+
* `groupByField`, `colorField`, `metaFields` (`:520`, through an `as any`
4033+
* cast, which is why a `timelineConfig?.x` sweep alone under-counts) and
4034+
* `scale`. `limit` is not among them, and NO spread of a node's own
4035+
* `timeline` block exists anywhere in objectui (0 hits; lit control, same
4036+
* shape, the view-block spreads `...mergedTimeline` /
4037+
* `...(viewOptions.timeline || {})`, which do fire). The
4038+
* `ElementDataSourceGate` mapping is `limit: 'limit'`
4039+
* (`plugin-timeline/src/index.tsx:333`) — FLAT, so it never touches the
4040+
* nested key either.
4041+
* - **VIEW FACE — a `ListViewSchema` document's `timeline.limit`: that is
4042+
* the route-dependent one**, and it is a different key on a different
4043+
* document. `ObjectView.tsx:1725` flattens the view block onto the node it
4044+
* returns (which then carries no `timeline` block at all), so the value
4045+
* arrives as this node's FLAT `limit` and is read; `ListView.tsx:3084`
4046+
* forwards it nested instead, where nothing reads it. Recorded on
4047+
* `rowLimitKey` in `view.zod.ts`, which is where that key lives.
4048+
*
4049+
* ⚠️ Flagged, not fixed — a THIRD route neither the card nor the first two
4050+
* reviews described: a hand-authored `object-timeline` node reaching
4051+
* `ObjectTimeline` through `SchemaRenderer` with no adapter in between. Its
4052+
* nested `timeline.limit` is unread there too (it is the same element-face
4053+
* key as the first bullet), and that route is the one an AUTHOR face is
4054+
* written for — so it is the route the open question most concerns.
4055+
*
4056+
* ⛔ Recorded, not repaired: which key should carry a timeline's row cap is
4057+
* the open half of #19228.
40314058
* `mapping` stays `z.unknown()`: its contract
40324059
* (`TimelineMappingSchema`) still lives in objectui, which is the
40334060
* `object-calendar.calendar` posture this section's header prescribes for
@@ -4051,7 +4078,7 @@ export const ObjectTimelinePropsSchema = lazySchema(() => strictObject({
40514078
objectName: z.string().optional()
40524079
.describe('Object this timeline binds to. Optional because the component-level `dataSource` binding can supply the object instead — this block registers through `ElementDataSourceGate`, which lowers the binding onto this key before the renderer sees the node'),
40534080
timeline: TimelineConfigSchema.optional()
4054-
.describe('Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: { startDateField, endDateField, titleField, groupByField, colorField, scale, limit }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member whose effect depends on which adapter rendered the node: one spreads this block flat, so it becomes the flat `limit` beside this key, and one forwards it nested, where nothing reads it. Write the flat `limit` when you mean the row cap for the rail'),
4081+
.describe('Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: { startDateField, endDateField, titleField, groupByField, colorField, scale, limit }. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling. ⚠️ `limit` is the one member of this block NO renderer reads on any route: the rail is capped by the FLAT `limit` beside this key, which is also the only one a bound `dataSource` lowers into. A `limit` written inside this block is accepted, defaulted to 100, and never read'),
40554082
/** Base query filter — the family's one `ViewFilterRule` array orthography (#15449). */
40564083
filter: z.array(ViewFilterRuleSchema, {
40574084
error: ruleArrayFilterError({

0 commit comments

Comments
 (0)