Skip to content

Commit 0dcc574

Browse files
committed
chore(spec): changeset and ADR-0087 semantic entry for the object-grid page-size narrowing
The narrowing refuses an authored value that parsed before, so it declares the `narrowing` arm and registers the migration prescription in the ADR-0087 ledger rather than claiming a not-required category: the body carries a FROM -> TO table, which closes `no-migration-prescription` by construction. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
1 parent b755269 commit 0dcc574

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING for authored metadata** — the `object-grid` page-component door now refuses a page size of `0`, a negative page size and a non-integer page size, at all three of its spellings: `pagination.pageSize`, every `pagination.pageSizeOptions[]` entry, and the flat `pageSize` shorthand (#19046).
6+
7+
Clause-②: yes (narrowing)
8+
9+
The accept set shrinks to the one the VIEW arm has ruled all along. `PaginationConfigSchema` (`view.zod.ts`) declares `pageSize: z.number().int().positive()` and pins its refusals by name; `MetadataQuery` and the two marketplace request schemas say `z.number().int().min(1)`, each with its own throwing pin. The `object-grid` door said `pagination: z.unknown()` and `pageSize: z.number()` — the only page-size declaration in the package that accepted `0`, and the one renderers read.
10+
11+
**It was not theoretical.** Measured at objectui#9853: an authored `pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` and rendered ZERO ROWS, with no grouping needed to trigger it — through this arm, with a `success: true` receipt from this schema. The view arm would have refused the same value. objectui#9896 repaired the consumer half (a resolver at every read point, fail-soft, one loud diagnostic); this is the declaration half and is not a prerequisite for it.
12+
13+
```
14+
✗ pagination.pageSize: Too small: expected number to be greater than 0
15+
✗ pageSize: Invalid input: expected int, received number
16+
```
17+
18+
### Migration — FROM → TO
19+
20+
| You wrote | Write instead |
21+
| --- | --- |
22+
| `pagination: { pageSize: 0 }` | `showPagination: false` and no `pagination` bag — the bag's PRESENCE is what enables paging, so `pageSize: 0` never meant "no paging" |
23+
| `pagination: { pageSize: 0 }` (meaning "all rows on one page") | the page size you actually want (`{ pageSize: 100 }`); `0` reached the wire as `$top: 0` and returned nothing |
24+
| `pagination: { pageSizeOptions: [0, 25, 50] }` | `{ pageSizeOptions: [25, 50] }` — drop the `0` entry; selecting it set the fetch window to zero rows |
25+
| `pageSize: 25.5` | `pageSize: 25` — a fractional page size was truncated or forwarded verbatim, depending on the read point |
26+
27+
The one-line fix is always the same: **write a positive integer, or delete the key and take the renderer's default.**
28+
29+
<!-- adr-0087: registered ui-object-grid-page-size-positive-integer-refused -->
30+
31+
**⛔ What this deliberately does NOT narrow: the `pagination` bag stays OPEN.** The card's defect is that the two arms disagreed about a page SIZE — not that the bag should become a closed shape. `pagination` is now a `z.looseObject` that validates the two members whose value is a page size and passes every other key through unvalidated, so a sibling key that parsed before still parses and still survives the parse byte-identically (pinned in `component-object-grid-pagination-accept-set.pin.test.ts` §3). Reusing the view arm's `PaginationConfigSchema` here would have refused every sibling key this door has accepted since it was written — the `…` in its own describe says authors write them — which is a wider narrowing than the measured defect and a different decision. `PaginationConfigSchema` itself is unchanged and stays closed; §4 of that pin states both the agreement and the deliberate asymmetry.
32+
33+
**One second axis, named rather than left to be discovered.** `pagination` moves from `z.unknown()` to an object type, so a non-object value (`pagination: true`) is refused where it used to parse. Measured before narrowing: zero non-object `pagination` values exist on an `object-grid` node in either repository's corpus, the objectui registry has published this input as `type: 'object'` all along (`plugin-grid/src/index.tsx`), so the html tier already answered `type-mismatch` on one, and the renderer reads the key for PRESENCE (`schema.pagination !== undefined`) — which means an authored `pagination: false` used to turn paging ON. That value now gets a located refusal instead of the opposite of what it says.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'ui-object-grid-page-size-positive-integer-refused',
7+
surface: '`object-grid` page-component page sizes '
8+
+ "(`ComponentPropsMap['object-grid']` — `pagination.pageSize`, each "
9+
+ '`pagination.pageSizeOptions[]` entry, and the flat `pageSize` shorthand) — '
10+
+ 'zero, negative and non-integer values (`pagination: { pageSize: 0 }`, '
11+
+ '`pageSize: 25.5`)',
12+
replacement: 'a positive integer, or no declaration at all. A page size of `0` has no '
13+
+ 'defined meaning on this surface and never had one: delete the key to take the '
14+
+ "renderer's own default, or write the page size that was meant (`pageSize: 0` "
15+
+ 'authored to mean "no paging" is `showPagination: false` with no `pagination` bag, '
16+
+ "since the bag's PRESENCE is what enables paging)",
17+
reason:
18+
'#19046: this door carried the pre-#7751 read-point shape — `pagination: z.unknown()` '
19+
+ 'and `pageSize: z.number()` — after the view arm converged on '
20+
+ '`z.number().int().positive()`. So the SAME authored member carried two accept sets '
21+
+ 'and renderers read the looser one: `PaginationConfigSchema` (`view.zod.ts`) refuses '
22+
+ '`pageSize: 0` and pins that refusal by name, and every other `pageSize` the package '
23+
+ 'declares is bounded with its own throwing pin (`kernel/metadata-plugin.zod.ts`, '
24+
+ '`marketplace/marketplace.zod.ts`) — the component arm was the only one that '
25+
+ 'accepted `0`. The value is LIVE: measured at objectui#9853, an authored '
26+
+ '`pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` '
27+
+ 'and rendered ZERO ROWS, with no grouping needed to trigger it, and it reached the '
28+
+ 'renderer through this arm. objectui#9896 repaired the consumer half (a resolver at '
29+
+ 'every read point, fail-soft, one loud diagnostic); this is the declaration half, '
30+
+ 'and it is not a prerequisite for that repair. '
31+
+ '⚠️ The `pagination` bag itself stays OPEN (`z.looseObject`): only the two members '
32+
+ 'whose value is a page size are bounded, and sibling keys parse and pass through '
33+
+ 'exactly as before. `PaginationConfigSchema` on the view arm is a closed shape and '
34+
+ 'is unchanged by this entry.',
35+
acceptanceCriteria:
36+
'Every `object-grid` node declaring a page size — inside `pagination` or through the '
37+
+ 'flat shorthand — carries a positive integer. Well-formed values (`10`, `25`, `50`) '
38+
+ 'parse byte-identically to before, a `pagination` bag carrying sibling keys parses '
39+
+ 'and keeps them, and absence stays absence. A stored page whose `object-grid` node '
40+
+ 'carries `pageSize: 0` is refused on its next authoring-path save with a per-key '
41+
+ 'issue at `pagination.pageSize`; the author deletes the key or writes the page size '
42+
+ 'they meant.',
43+
};

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12274,6 +12274,45 @@ const step18: MigrationStep = {
1227412274
+ 'anything), and behaviour that seems to need one is a renderer capability request '
1227512275
+ 'against objectui, not a metadata key.',
1227612276
},
12277+
{
12278+
id: 'ui-object-grid-page-size-positive-integer-refused',
12279+
surface: '`object-grid` page-component page sizes '
12280+
+ "(`ComponentPropsMap['object-grid']` — `pagination.pageSize`, each "
12281+
+ '`pagination.pageSizeOptions[]` entry, and the flat `pageSize` shorthand) — '
12282+
+ 'zero, negative and non-integer values (`pagination: { pageSize: 0 }`, '
12283+
+ '`pageSize: 25.5`)',
12284+
replacement: 'a positive integer, or no declaration at all. A page size of `0` has no '
12285+
+ 'defined meaning on this surface and never had one: delete the key to take the '
12286+
+ "renderer's own default, or write the page size that was meant (`pageSize: 0` "
12287+
+ 'authored to mean "no paging" is `showPagination: false` with no `pagination` bag, '
12288+
+ "since the bag's PRESENCE is what enables paging)",
12289+
reason:
12290+
'#19046: this door carried the pre-#7751 read-point shape — `pagination: z.unknown()` '
12291+
+ 'and `pageSize: z.number()` — after the view arm converged on '
12292+
+ '`z.number().int().positive()`. So the SAME authored member carried two accept sets '
12293+
+ 'and renderers read the looser one: `PaginationConfigSchema` (`view.zod.ts`) refuses '
12294+
+ '`pageSize: 0` and pins that refusal by name, and every other `pageSize` the package '
12295+
+ 'declares is bounded with its own throwing pin (`kernel/metadata-plugin.zod.ts`, '
12296+
+ '`marketplace/marketplace.zod.ts`) — the component arm was the only one that '
12297+
+ 'accepted `0`. The value is LIVE: measured at objectui#9853, an authored '
12298+
+ '`pagination.pageSize: 0` reached `ObjectGrid`, went out on the wire as `$top: 0` '
12299+
+ 'and rendered ZERO ROWS, with no grouping needed to trigger it, and it reached the '
12300+
+ 'renderer through this arm. objectui#9896 repaired the consumer half (a resolver at '
12301+
+ 'every read point, fail-soft, one loud diagnostic); this is the declaration half, '
12302+
+ 'and it is not a prerequisite for that repair. '
12303+
+ '⚠️ The `pagination` bag itself stays OPEN (`z.looseObject`): only the two members '
12304+
+ 'whose value is a page size are bounded, and sibling keys parse and pass through '
12305+
+ 'exactly as before. `PaginationConfigSchema` on the view arm is a closed shape and '
12306+
+ 'is unchanged by this entry.',
12307+
acceptanceCriteria:
12308+
'Every `object-grid` node declaring a page size — inside `pagination` or through the '
12309+
+ 'flat shorthand — carries a positive integer. Well-formed values (`10`, `25`, `50`) '
12310+
+ 'parse byte-identically to before, a `pagination` bag carrying sibling keys parses '
12311+
+ 'and keeps them, and absence stays absence. A stored page whose `object-grid` node '
12312+
+ 'carries `pageSize: 0` is refused on its next authoring-path save with a per-key '
12313+
+ 'issue at `pagination.pageSize`; the author deletes the key or writes the page size '
12314+
+ 'they meant.',
12315+
},
1227712316
{
1227812317
id: 'ui-react-list-view-binding-aliases-retired',
1227912318
surface: '`kind:\'react\'` page source — `<ListView objectName="…">` and `<ListView viewType="…">` '

0 commit comments

Comments
 (0)