Skip to content

Commit 01a7ce4

Browse files
committed
fix(spec): the repaired describe named a third face and an unreachable arm — both removed
Round 4 leads, each verified first-hand before a line moved. Both halves of lead 1 reproduce; the repair shipped in round 1 was wrong in its own way. 1. `ListViewSchema` REFUSES a flat `limit`. Measured on this tree with four lit controls: the minimal view document parses; + `pagination.pageSize: 50` parses; + per-kind `kanban.limit: 50` parses; + flat `limit: 50` is REFUSED with unrecognized_keys: ["limit"], the same verdict a bogus key gets. Only three `limit` members exist on any view document and all three are rowLimitKey(...) per-kind blocks; no retiredKey() tombstone for a flat one. So `savedViewLimit`'s fallback to `view.limit` names a THIRD face -- a saved-view RECORD from the adapter's listViews() -- and stating it on the author face with no qualifier is the same face-merge this card has failed on three times. Dropped from the describe, recorded in the docblock. 2. The displaced-and-reported arm is unreachable from inside the accept set. isUsableRowLimit is `typeof v === 'number' && Number.isInteger(v) && v > 0` -- the same set this key declares as z.number().int().positive(). Measured per value: 1/25/100/5000 accepted and usable; 0/-1/2.5/'100'/null refused by both; unset accepted and not usable. So across the whole accept set "set but not usable" is EMPTY and the guard is exactly "only when unset" -- the wording round 1 retired as too narrow. Restored, with the reason. The prose pin that guarded the retired sentence is replaced by the structural fact underneath it: the schema's accept set and isUsableRowLimit coincide, so the sentence is derivable rather than pinned, and either side widening reds it. Lead 3: the property-access control is now published as its EXPRESSION plus every hit -- 13 lines across 6 files -- with the filter that yields 8 stated beside it (3 comments, 2 inside a quoted source-text pin; one hit set, three readings). The spread predicate now also names what it EXCLUDES: app-shell timelineViewOptions:206 and galleryViewOptions:342, options-bag builders feeding ListView's nested forward. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
1 parent ff3d631 commit 01a7ce4

4 files changed

Lines changed: 88 additions & 32 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,7 @@ Sort field and direction pair
617617
| **groupBy** | `string` | optional | Field whose values become the board columns |
618618
| **columns** | `any[]` | optional | Swimlane definitions (`{ id, title }` per `groupBy` value, or bare value strings) — NOT a field projection |
619619
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter, handed to the wire `$filter` — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` |
620-
| **limit** | `integer` | optional | Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's row cap (`pagination.pageSize`, else that view's flat `limit`) fills this key unless it already carries a USABLE cap — a cap the contract refuses (zero, negative, fractional) is displaced by the view's and reported, not honoured |
620+
| **limit** | `integer` | optional | Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's `pagination.pageSize` fills this key only when it is unset — and on this face unset is the whole rule, because every cap this key accepts is one the binding gate already treats as authored |
621621
| **data** | `any[]` | optional | Static inline cards — bypasses the object query |
622622
| **cardTitle** | `string` | optional | Field rendered as each card title |
623623
| **titleField** | `string` | optional | Legacy fallback for `cardTitle` (the board reads `cardTitle \|\| titleField`). Prefer `cardTitle` |

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

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3857,17 +3857,37 @@ describe('row caps on the object-bound blocks — what #19228 recorded', () => {
38573857
expect(JSON.stringify(control.error?.issues)).toContain('unrecognized_keys');
38583858
});
38593859

3860-
it('states the gate guard the gate actually implements, not the narrower 「unset」 arm', () => {
3861-
// ⛔ The one prose assertion here, and it is negative on purpose: this
3862-
// card's whole repair IS the published sentence, so without a pin the
3863-
// change has no falsifier. The retired wording claimed the view's cap
3864-
// lands ONLY on an unset key; measured, it also lands on a key set to a
3865-
// cap the contract refuses, with `describeDisplacedRowLimit` reporting it.
3866-
const shape = (ObjectKanbanPropsSchema as unknown as {
3867-
def: { shape: Record<string, { description?: string }> };
3868-
}).def.shape;
3869-
const description = shape.limit?.description ?? '';
3870-
expect(description).not.toContain('only when unset');
3871-
expect(description).toMatch(/usable cap/i);
3860+
it('admits exactly the caps the binding gate calls usable — which is WHY 「unset」 is the whole rule', () => {
3861+
// ⛔ Not a prose pin. The published sentence says a bound view's
3862+
// `pagination.pageSize` fills this key only when it is UNSET, and this is
3863+
// the structural fact that makes that true rather than narrow:
3864+
// `ElementDataSourceGate`'s guard is `!isUsableRowLimit(authored)` with
3865+
// `isUsableRowLimit = typeof v === 'number' && Number.isInteger(v) && v > 0`
3866+
// — the same set this key declares. So across the whole accept set the
3867+
// guard has exactly two outcomes, and 「set but not usable」 is empty.
3868+
// If either side ever widens (a `.nullable()`, a `0` sentinel, a float),
3869+
// this reds and the sentence has to be rewritten with it.
3870+
const usableToTheGate = (v: unknown): boolean =>
3871+
typeof v === 'number' && Number.isInteger(v) && v > 0;
3872+
3873+
// ACCEPTED by the schema ⇒ usable to the gate ⇒ the view's cap does NOT land.
3874+
for (const cap of [1, 25, 100, 5000]) {
3875+
const r = kanban.safeParse({ objectName: 'x', limit: cap });
3876+
expect(r.success, `accept ${cap}`).toBe(true);
3877+
expect(usableToTheGate((r.success ? r.data : {} as never).limit), `usable ${cap}`).toBe(true);
3878+
}
3879+
3880+
// REFUSED by the schema ⇒ never reaches the gate from a valid document,
3881+
// which is why the displaced-and-reported arm is not in the describe.
3882+
for (const cap of [0, -1, 2.5, '100', null]) {
3883+
expect(kanban.safeParse({ objectName: 'x', limit: cap }).success, `refuse ${JSON.stringify(cap)}`).toBe(false);
3884+
expect(usableToTheGate(cap), `gate also rejects ${JSON.stringify(cap)}`).toBe(false);
3885+
}
3886+
3887+
// UNSET — accepted, and the one state the gate treats as unauthored.
3888+
const unset = kanban.safeParse({ objectName: 'x' });
3889+
expect(unset.success).toBe(true);
3890+
expect(Object.prototype.hasOwnProperty.call(unset.success ? unset.data : {}, 'limit')).toBe(false);
3891+
expect(usableToTheGate(undefined)).toBe(false);
38723892
});
38733893
});

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3092,24 +3092,39 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
30923092
* Why the carrier is `limit` and not the bound view's `pagination.pageSize`
30933093
* (the alternative the card opened): precedence is the `ElementDataSourceGate`
30943094
* table, not this key's. The component-level `dataSource.limit` overrides
3095-
* this key unconditionally; a bound named view's row cap is LOWERED INTO it
3096-
* through the `limit: 'limit'` mapping only when this key does not already
3097-
* carry a USABLE cap (`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`,
3095+
* this key unconditionally; a bound named view's `pagination.pageSize` is
3096+
* LOWERED INTO it through the `limit: 'limit'` mapping only when this key is
3097+
* unset (`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`,
30983098
* `readLimit`/`writeLimit` keyed by `ElementDataSourceLimitKey`; the branch
30993099
* gained objectui#9899's presence-is-not-authorship test and a
31003100
* `describeDisplacedRowLimit` report on this hop).
31013101
*
3102-
* ⚠️ 「only when UNSET」 is what this docblock and the describe beside it
3103-
* used to say, and it is narrower than the guard — re-READ first-hand at the
3104-
* pin `87af769e9` on 2026-09-21T06:35Z. The branch is
3105-
* `if (!fromView || !isUsableRowLimit(authored))`, so the view's cap also
3106-
* lands when this key IS set to a value the contract refuses (`0`, negative,
3107-
* fractional, non-number), with `describeDisplacedRowLimit` telling the
3108-
* author. Unset is one arm of that guard, not the whole of it. The view half
3109-
* is likewise not `pagination.pageSize` alone: `savedViewLimit` reads
3110-
* `pagination.pageSize`, else that view's FLAT `limit`
3111-
* (`core/src/data-scope/element-data-source.ts:237-241`); the per-kind
3112-
* `kanban.limit` #19226 declared takes neither of those two doors.
3102+
* ⚠️ 「only when UNSET」 reads narrower than the guard and is nonetheless
3103+
* EXACTLY right on this face — a correction to a correction, measured
3104+
* 2026-09-21T10:20Z. The branch is
3105+
* `if (!fromView || !isUsableRowLimit(authored))`, and
3106+
* `isUsableRowLimit` is `typeof v === 'number' && Number.isInteger(v) && v > 0`
3107+
* (`ElementDataSourceGate.tsx:192-194`) — the SAME set this key declares,
3108+
* `z.number().int().positive()`. So for every node this schema ACCEPTS,
3109+
* `authored` is either absent (not usable ⇒ the view's cap lands) or a
3110+
* positive integer (usable ⇒ it does not): unset is the only reachable arm.
3111+
* The extra arm — a cap displaced and reported because it is `0`, negative
3112+
* or fractional — is reachable ONLY for a node this contract refuses, so
3113+
* ⛔ it does not belong in an author-facing describe. Pinned structurally
3114+
* beside the parse pins in `component.test.ts` rather than as prose.
3115+
*
3116+
* ⚠️ And the view half is `pagination.pageSize` ALONE on this face.
3117+
* `savedViewLimit` does fall back to a flat `view.limit`
3118+
* (`core/src/data-scope/element-data-source.ts:237-241`), but that names a
3119+
* THIRD face — a saved-view RECORD as the adapter's `listViews()` returns it
3120+
* — not an authored view document. Measured on this tree: `ListViewSchema`
3121+
* REFUSES a flat `limit` with `unrecognized_keys: ["limit"]`, the same
3122+
* verdict a bogus key gets, while the same minimal document parses with
3123+
* `pagination.pageSize: 50` and with a per-kind `kanban.limit: 50`. There is
3124+
* no flat `limit` member on any view document and no `retiredKey()`
3125+
* tombstone for one. ⛔ So naming that arm here would put a runtime-record
3126+
* shape on the author face with no qualifier — the face-merge this card has
3127+
* now failed on three times.
31133128
* ⛔ This note reports the guard; it picks no precedence.
31143129
*
31153130
* ⚠️ It takes a THIRD door, and missing it is what the at-tier review of
@@ -3146,7 +3161,7 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
31463161
* a schema default would materialize `limit: 100` on every parsed board.
31473162
*/
31483163
limit: z.number().int().positive().optional()
3149-
.describe("Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's row cap (`pagination.pageSize`, else that view's flat `limit`) fills this key unless it already carries a USABLE cap — a cap the contract refuses (zero, negative, fractional) is displaced by the view's and reported, not honoured"),
3164+
.describe("Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's `pagination.pageSize` fills this key only when it is unset — and on this face unset is the whole rule, because every cap this key accepts is one the binding gate already treats as authored"),
31503165
data: z.array(z.unknown()).optional().describe('Static inline cards — bypasses the object query'),
31513166
cardTitle: z.string().optional().describe('Field rendered as each card title'),
31523167
titleField: z.string().optional().describe('Legacy fallback for `cardTitle` (the board reads `cardTitle || titleField`). Prefer `cardTitle`'),

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,10 +1234,24 @@ type RowLimitView = keyof typeof ROW_LIMIT_SUBJECT;
12341234
* `87af769e9`), 2026-09-21T09:15Z, with TWO instruments, because one was not
12351235
* enough and the first one's answer was wrong:
12361236
*
1237-
* 1. PROPERTY-ACCESS spellings — `.kanban.limit` / `.gallery.limit` /
1238-
* `.timeline.limit` and the receiver alternation: **0** hits. Lit control,
1239-
* identical shape, `.kanban.groupByField` / `.gallery.coverField` /
1240-
* `.timeline.scale`: **13** lines. A live instrument, and a WRONG answer.
1237+
* 1. PROPERTY-ACCESS spellings. ⛔ Published as its EXPRESSION, not as a
1238+
* number — this card exists because a confident count was wrong once, so
1239+
* a control nobody can re-derive is not a control. Run at the pin, from
1240+
* an objectui checkout, over every tracked file:
1241+
* probe: git grep -nIE '\.(kanban|gallery|timeline)(\?)?\.limit\b'
1242+
* control: git grep -nIE '\.(kanban|gallery|timeline)(\?)?\.(groupByField|scale|coverField)\b'
1243+
* Probe: **0** lines, 0 files. Control: **13** lines across **6** files —
1244+
* `app-shell/src/views/ObjectView.galleryBinding-7547.test.tsx:41`,
1245+
* `app-shell/src/views/ObjectView.tsx:450`,
1246+
* `plugin-list/src/ListView.tsx:2538`, `:2540`, `:2547`, `:3057`, `:3114`,
1247+
* `:3116`,
1248+
* `plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:42`,
1249+
* `:99`, `plugin-view/src/ObjectView.tsx:1695`, and
1250+
* `types/src/__tests__/object-kanban-group-by-limit-7322.test.ts:146`, `:148`.
1251+
* ⚠️ Filtering changes that number and the filter must be stated with it:
1252+
* 3 of the 13 are in COMMENTS and 2 more sit inside a quoted source-text
1253+
* pin, so a reader counting executable reads only gets **8**. All three
1254+
* readings are of one hit set. A live instrument — and a WRONG answer.
12411255
* 2. ⭐ SPREADS — a spread carries a key without ever spelling it, so it is
12421256
* the hole instrument 1 cannot see by construction. ⛔ Re-take it by its
12431257
* PREDICATE, not by its count: **a spread whose target is the object
@@ -1246,6 +1260,13 @@ type RowLimitView = keyof typeof ROW_LIMIT_SUBJECT;
12461260
* the lit control for the instrument AND the example of what the predicate
12471261
* excludes). A grep broad enough to find these also returns the nested
12481262
* merges, so the rule, not the number, is what makes it reproducible.
1263+
* ⛔ And name what the predicate EXCLUDES, or the next reader re-finds
1264+
* it and wonders: `app-shell/src/views/ObjectView.tsx:206` and `:342`
1265+
* ARE spreads of a view block, inside `timelineViewOptions` (`:201`) and
1266+
* `galleryViewOptions` (`:334`). They build an OPTIONS BAG that feeds
1267+
* `ListView`'s nested forward, not the object literal an adapter returns
1268+
* as the node, so the predicate excludes them — deliberately, not by
1269+
* oversight.
12491270
* Under that predicate, at that pin, the VIEW-face per-kind blocks give:
12501271
* `plugin-list/src/ListView.tsx:2979` `...restKanban`
12511272
* `plugin-view/src/ObjectView.tsx:1638` `...restKanban`

0 commit comments

Comments
 (0)