Skip to content

Commit b922105

Browse files
committed
fix(spec): the changeset still asserted what round 4 retracted — and three wordings overstated
Round 4 touched four files and not the changeset, so the tree carried both the retracted claim and the corrected one at once. The changeset is the published CHANGELOG entry for @objectstack/spec, an author-facing surface, which makes that round 2's lesson repeated inside the tree instead of in the PR body. 1. Changeset 'What the published text said' rewritten to the round-4 claim: 'only when unset' is exactly right for this face; the accept set is a SUBSET of isUsableRowLimit so 'set but not usable' is empty; the view half is pagination.pageSize ALONE, with savedViewLimit's flat view.limit named as the saved-view RECORD face it belongs to. Grade unchanged at patch -- this is text, not level. 2. 'the SAME set this key declares' overstated and is now the derivable claim, accept-set SUBSET-OF usable-set, with the separating witness: 2**53+2 is refused here (zod 4 .int() is safe-integer, too_big) and Number.isInteger calls it usable. Verified by parse, not asserted. The test title moves the same way and the witness is pinned as a case. 3. The test comment claimed 'either side widening reds this'. Half wrong, and now stated asymmetrically: the SPEC side reds (nullable, a 0 sentinel, dropping .int(), adding a .default() each fail a specific expect), but the GATE side CANNOT -- usableToTheGate is a transcription of isUsableRowLimit dated to pin 87af769e9, not an import, so a rewrite at objectui HEAD leaves this green. It refreshes only on a pin bump, by hand. 4. The instrument partition was off by one label: ListView.kanbanOptionsBag Canonical-8193.test.tsx:99 is an it() title string, not a comment. Now 2 comments, 1 test-title string, 2 quoted-pin lines -> 8 executable of 13. The spread predicate's exclusion list also gains ListView.tsx:3044-3046 (mergedGallery, nested) and app-shell ObjectView.tsx:1284 (a write back into a view document's own block, not a node build). No describe changed, so no .mdx regenerates; check:generated is green without --fix, which is the prediction this round was able to state in advance. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx
1 parent 01a7ce4 commit b922105

4 files changed

Lines changed: 70 additions & 27 deletions

File tree

‎.changeset/19228-view-row-limit-route-record.md‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,29 @@ 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-
## What the published text said, and what the consumer does
13-
14-
`ObjectKanbanPropsSchema.limit` told authors that a bound view's `pagination.pageSize`
15-
「fills it only when unset」. Measured first-hand at the objectui pin this repo builds against
16-
(`.objectui-sha` = `87af769e9`), the branch is `if (!fromView || !isUsableRowLimit(authored))`
17-
(`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`): the view's cap ALSO lands
18-
when the key is set to a cap the contract refuses — zero, negative or fractional — with
19-
`describeDisplacedRowLimit` telling the author. Unset is one arm of that guard, not the whole of
20-
it. The view half is likewise not `pagination.pageSize` alone: `savedViewLimit` reads
21-
`pagination.pageSize`, else that view's flat `limit`
22-
(`core/src/data-scope/element-data-source.ts:237-241`). The describe now states the guard as
23-
implemented.
12+
## What the published text said, and what an author can actually reach
13+
14+
`ObjectKanbanPropsSchema.limit` tells authors that a bound view's `pagination.pageSize` fills it
15+
「only when unset」. Measured first-hand at the objectui pin this repo builds against
16+
(`.objectui-sha` = `87af769e9`), that sentence is exactly right for this face, and the describe
17+
now says WHY rather than leaving it to look narrower than the mechanism.
18+
19+
The gate's branch is `if (!fromView || !isUsableRowLimit(authored))`
20+
(`react/src/element-data-source/ElementDataSourceGate.tsx:316-331`), and `isUsableRowLimit` is
21+
`typeof v === 'number' && Number.isInteger(v) && v > 0` (`:192-194`). Every cap this key ACCEPTS
22+
is one that predicate already calls usable — the accept set is a subset of the usable set — so
23+
across the whole accept set the guard has exactly two outcomes and 「set but not usable」 is
24+
empty. The extra arm, a cap displaced and reported because it is zero, negative or fractional,
25+
is reachable only for a node this contract refuses, so it is recorded in the docblock rather
26+
than in an author-facing sentence.
27+
28+
The view half is `pagination.pageSize` ALONE on this face. `savedViewLimit` does fall back to a
29+
flat `view.limit` (`core/src/data-scope/element-data-source.ts:237-241`), but that names a
30+
saved-view RECORD as the adapter's `listViews()` returns it — a third face, not an authored view
31+
document. Measured on this tree: `ListViewSchema` REFUSES a flat `limit` with
32+
`unrecognized_keys: ["limit"]`, the verdict a bogus key gets, while the same minimal document
33+
parses with `pagination.pageSize: 50` and with a per-kind `kanban.limit: 50`. No view document
34+
declares a flat `limit` and none carries a tombstone for one.
2435

2536
## Where the per-kind VIEW `limit` lands
2637

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

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3857,16 +3857,26 @@ 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('admits exactly the caps the binding gate calls usable — which is WHY 「unset」 is the whole rule', () => {
3860+
it('admits only caps the binding gate calls usable — the SUBSET that makes 「unset」 the whole rule', () => {
38613861
// ⛔ Not a prose pin. The published sentence says a bound view's
38623862
// `pagination.pageSize` fills this key only when it is UNSET, and this is
38633863
// the structural fact that makes that true rather than narrow:
38643864
// `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.
3865+
// `isUsableRowLimit = typeof v === 'number' && Number.isInteger(v) && v > 0`.
3866+
// This key's accept set is a SUBSET of that predicate — ⛔ NOT the same
3867+
// set; `2 ** 53 + 2` separates them, and the case below pins it. Subset is
3868+
// the direction the sentence needs: it makes 「set but not usable」 empty
3869+
// across the whole accept set, so the guard has exactly two outcomes.
3870+
//
3871+
// ⚠️ What this pin can and cannot catch, because the two sides are not
3872+
// symmetric here:
3873+
// · SPEC side — reds. A `.nullable()`, a `0` sentinel, dropping `.int()`
3874+
// or adding a `.default()` each fail a specific expect below.
3875+
// · GATE side — ⛔ CANNOT red. `usableToTheGate` is a TRANSCRIPTION of
3876+
// `isUsableRowLimit` as it read at objectui pin `87af769e9`, not an
3877+
// import — nothing here resolves into objectui. A rewrite of that
3878+
// predicate at objectui HEAD leaves this test green. It is re-read on
3879+
// a PIN BUMP, by hand, and that is the only thing that refreshes it.
38703880
const usableToTheGate = (v: unknown): boolean =>
38713881
typeof v === 'number' && Number.isInteger(v) && v > 0;
38723882

@@ -3884,6 +3894,15 @@ describe('row caps on the object-bound blocks — what #19228 recorded', () => {
38843894
expect(usableToTheGate(cap), `gate also rejects ${JSON.stringify(cap)}`).toBe(false);
38853895
}
38863896

3897+
// ⛔ The sets are NOT equal, and this is the witness. `2 ** 53 + 2` is
3898+
// refused here (zod 4's `.int()` enforces SAFE integers, `too_big`) while
3899+
// `Number.isInteger` calls it usable. Subset, not coincidence — if this
3900+
// case ever flips, the docblock sentence built on the subset direction
3901+
// has to be re-derived rather than reworded.
3902+
const beyondSafe = 2 ** 53 + 2;
3903+
expect(kanban.safeParse({ objectName: 'x', limit: beyondSafe }).success).toBe(false);
3904+
expect(usableToTheGate(beyondSafe)).toBe(true);
3905+
38873906
// UNSET — accepted, and the one state the gate treats as unauthored.
38883907
const unset = kanban.safeParse({ objectName: 'x' });
38893908
expect(unset.success).toBe(true);

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3104,10 +3104,14 @@ export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({
31043104
* 2026-09-21T10:20Z. The branch is
31053105
* `if (!fromView || !isUsableRowLimit(authored))`, and
31063106
* `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.
3107+
* (`ElementDataSourceGate.tsx:192-194`), and this key's accept set
3108+
* (`z.number().int().positive()`) is a SUBSET of it — ⛔ not the same set,
3109+
* and the difference is reachable: `2^53 + 2` is refused here (zod 4's
3110+
* `.int()` is safe-integer, `too_big`) and `Number.isInteger` calls it
3111+
* usable. Subset is the direction that matters, and it is the whole
3112+
* argument: for every node this schema ACCEPTS, `authored` is either absent
3113+
* (not usable ⇒ the view's cap lands) or a cap the gate already treats as
3114+
* authored (⇒ it does not). So unset is the only reachable arm.
31113115
* The extra arm — a cap displaced and reported because it is `0`, negative
31123116
* or fractional — is reachable ONLY for a node this contract refuses, so
31133117
* ⛔ it does not belong in an author-facing describe. Pinned structurally

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,10 +1248,14 @@ type RowLimitView = keyof typeof ROW_LIMIT_SUBJECT;
12481248
* `plugin-list/src/__tests__/ListView.kanbanOptionsBagCanonical-8193.test.tsx:42`,
12491249
* `:99`, `plugin-view/src/ObjectView.tsx:1695`, and
12501250
* `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.
1251+
* ⚠️ Filtering changes that number and the filter must be stated with it.
1252+
* Of the 13: **2 are COMMENTS** (`ObjectView.galleryBinding-7547.test.tsx:41`,
1253+
* `ListView.kanbanOptionsBagCanonical-8193.test.tsx:42`), **1 is an
1254+
* `it()` TITLE string** (same file, `:99` — ⛔ not a comment), and **2 are
1255+
* lines inside a QUOTED source-text pin**
1256+
* (`object-kanban-group-by-limit-7322.test.ts:146`, `:148`). So a reader
1257+
* counting executable reads only gets **8**. All three readings are of one
1258+
* hit set. A live instrument — and a WRONG answer.
12551259
* 2. ⭐ SPREADS — a spread carries a key without ever spelling it, so it is
12561260
* the hole instrument 1 cannot see by construction. ⛔ Re-take it by its
12571261
* PREDICATE, not by its count: **a spread whose target is the object
@@ -1266,7 +1270,12 @@ type RowLimitView = keyof typeof ROW_LIMIT_SUBJECT;
12661270
* `galleryViewOptions` (`:334`). They build an OPTIONS BAG that feeds
12671271
* `ListView`'s nested forward, not the object literal an adapter returns
12681272
* as the node, so the predicate excludes them — deliberately, not by
1269-
* oversight.
1273+
* oversight. Two more the predicate excludes for their own reasons:
1274+
* `plugin-list/src/ListView.tsx:3044-3046` (`mergedGallery`) builds a
1275+
* NESTED gallery prop, the `...mergedTimeline` family; and
1276+
* `app-shell/src/views/ObjectView.tsx:1284`
1277+
* (`spec.kanban = { ...(spec.kanban || {}), columns }`) writes back into a
1278+
* VIEW document's own block — a metadata write, not a node build.
12701279
* Under that predicate, at that pin, the VIEW-face per-kind blocks give:
12711280
* `plugin-list/src/ListView.tsx:2979` `...restKanban`
12721281
* `plugin-view/src/ObjectView.tsx:1638` `...restKanban`

0 commit comments

Comments
 (0)