Skip to content

fix(spec): the object-grid arm takes the page-size accept set the view arm rules (#19046) - #19095

Merged
os-elon-musk merged 5 commits into
mainfrom
claude/issue-19046-grid-pagination-accept-set
Sep 18, 2026
Merged

os-elon-musk merged 5 commits into
mainfrom
claude/issue-19046-grid-pagination-accept-set

Conversation

@os-elon-musk

@os-elon-musk os-elon-musk commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #19046

Clause-②: yes

The object-grid page-component door declared pagination: z.unknown() and pageSize: z.number(), so the same authored member carried two accept sets and renderers read the looser one. This bounds the page-size members to the accept set the view arm has ruled all along, and deliberately leaves the pagination bag open.

The premise, re-derived by symbol at this branch's base (362035cc0)

⛔ No line number inherited from the card — triage warned about exactly that, and the card's own reading was taken on abb01f1.

arm symbol declaration at my base accepts 0?
view PaginationConfigSchema (packages/spec/src/ui/view.zod.ts:867-868) pageSize: z.number().int().positive().default(25) · pageSizeOptions: z.array(z.number().int().positive()).optional() no
grid component ObjectGridPropsSchema (packages/spec/src/ui/component.zod.ts:2632, :2634) pagination: z.unknown().optional() · pageSize: z.number().optional() yes — both

The view arm's refusals are pinned by name (view.test.tsshould reject negative pageSize, should reject zero pageSize, and the same pair for pageSizeOptions). The corpus corroboration also holds at my base — every other page-size declaration in the package is bounded:

packages/spec/src/ui/view.zod.ts:867           z.number().int().positive().default(25)
packages/spec/src/ui/view.zod.ts:868           z.array(z.number().int().positive())
packages/spec/src/ui/component.zod.ts:2634     z.number()                               ** the outlier
packages/spec/src/marketplace/marketplace.zod.ts:435   z.number().int().min(1).max(100).default(20)
packages/spec/src/marketplace/marketplace.zod.ts:456   z.number().int().min(1)
packages/spec/src/kernel/metadata-plugin.zod.ts:399    z.number().int().min(1).max(500).default(50)
packages/spec/src/kernel/metadata-plugin.zod.ts:429    z.number().int().min(1)

PR #18638, which held this file, is merged (2026-09-18T16:01:37Z) and did not tighten it in passing, so triage's downgrade clause does not apply.

The shape decision — a permissive object, and the evidence that chose it

The card's complaint is that the two arms disagree about a page size. It is ⛔ not that pagination should become a closed shape. Two shapes were plausible; the evidence is one-sided.

Chosen: z.looseObject({ pageSize, pageSizeOptions }) — validates the two declared members, passes every other key through.

Rejected: z.unknown() plus a refinement judging only pageSize. It looks more conservative and is measurably worse here:

  • z.toJSONSchema() has no arm for a custom check. A record, the same record with a .refine(), and the same record with an aborting .refine() all project byte-identically — the mechanism packages/spec/dropped-refinements.baseline.json exists to record. A refinement would have left the published JSON Schema still accepting pageSize: 0 while the parser refused it, and it would have needed a new row in that shrink-only ledger, which is a ratchet this dev may not raise.
  • The loose object is a type narrowing, so it projects. Measured on the built artifact:
packages/spec/json-schema/ui/ObjectGridProps.json
  pagination.properties.pageSize                 { "type": "integer", "exclusiveMinimum": 0 }
  pagination.properties.pageSizeOptions.items    { "type": "integer", "exclusiveMinimum": 0 }
  pagination.additionalProperties                {}          ** the bag stays OPEN
  pageSize                                       { "type": "integer", "exclusiveMinimum": 0 }

dropped-refinements.baseline.json is untouched by this PR: ui/ObjectGridProps keeps its single pre-existing filter.element site and gains none.

Read points, measured at objectui d18322415 (the sibling checkout in this container; the .objectui-sha pin is 53ded82bf): ObjectGrid.tsx:1209 and :1628 read (schema.pagination as any)?.pageSize ?? schema.pageSize, :4179 reads schema.pagination?.pageSize, :4359 reads schema.pagination?.pageSizeOptions. Across objectui's whole source, pageSize and pageSizeOptions are the only two members any pagination read point names (37 + 6 reads of .pageSize, 7 + 3 of .pageSizeOptions, zero of anything else). The objectui registry declares this input type: 'object' (plugin-grid/src/index.tsx:223).

What was NOT narrowed, and why

  • Sibling keys inside the bag. z.looseObject, not strictObject: a sibling key that parsed before still parses and still survives the parse byte-identically. Reusing PaginationConfigSchema here would have refused every one of them — the in this door's own describe says authors write them — which is a wider breaking change than the card's premise and a different decision. §3 of the new pin is what makes that auditable; §4 records the deliberate asymmetry (the view arm stays closed, this bag stays open), so a future author harmonising the two arms reds a case instead of discovering the consequence in a renderer.
  • No .default(25) added to the flat shorthand. The view arm has one; adding one here would change parsed output, not the accept set.
  • pageSizeOptions WAS bounded, and that is a judgement I am naming rather than burying. It is the same defect class by a second door: pageSizeOptions: [0, 25] puts a zero entry in the page-size selector, which sets the fetch window to zero rows — the card's exact failure. Its shape was already pinned by the view arm (z.array(z.number().int().positive())), whose zero/negative refusals are pinned by name, and its read point is measured above. Corpus cost: zero pageSizeOptions entries outside the spec's own refusal fixtures are non-positive.

One second axis, stated 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 on an object-grid node in either repository (the pagination: false hits in objectui are on data-table / object-data-table, whose props this schema does not declare, plus one internal per-group table the grid builds itself at ObjectGrid.tsx:4590);
  • the registry has published type: 'object' all along, so the html tier already answered type-mismatch on one while this schema accepted it — the same shape the sort docblock two members up already records;
  • ObjectGrid.tsx:4175 reads the key for presence (schema.pagination !== undefined ? true : …), 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.

Pins, each with its control

New file: packages/spec/src/ui/component-object-grid-pagination-accept-set.pin.test.ts — 19 cases, 4 sections.

section asserts control
§1 pagination.pageSize refuses zero / negative / non-integer, and pageSizeOptions entries refuse zero / negative — each asserting the issue code and path (too_small at pagination.pageSize), not a bare throw two LIT CONTROLS: a legal pageSize parses and is preserved; the whole ruled bag parses with its options
§2 the flat shorthand carries the same accept set, by name a LIT CONTROL: pageSize: 25 parses and keeps its value
§3 a sibling key in the bag parses with no unrecognized_keys issue, survives byte-identically (toStrictEqual), and a bag of only sibling keys parses this section IS the control for the trap above
§4 both arms refuse the same three non-page-sizes, and both accept 50 an unknown KEY is refused by the view arm (unrecognized_keys) and accepted by the component bag — the asymmetry, pinned

Defect reproduced in this tree, then the refusal proved able to fail. Ablation through scripts/ablation-replace.mjs, anchor const GridPageSizeSchema = z.number().int().positive(); replaced by const GridPageSizeSchema = z.number(); (the pre-PR accept set), from the committed state:

ablation-replace: ok mutation landed: anchor 1 -> 0, blob d9e4decd6443 -> 462c333a1bda
  Test Files  1 failed (1)
       Tests  11 failed | 8 passed (19)
  FAIL §1 ... > should reject zero pageSize
  AssertionError: expected true to be false     ** parse({ pagination: { pageSize: 0 } }) SUCCEEDS
ablation-replace: ok restored: blob == HEAD (d9e4decd6443) and `git diff HEAD` is empty

The 11 that reddened are exactly §1/§2/§4's refusals; the 8 that stayed green are the lit controls and §3's openness pins — the right partition, since the ablation removed only the value bound. Restored again through the explicit form: git checkout HEAD -- packages/spec/src/ui/component.zod.ts, then git hash-object equal to git rev-parse HEAD: that path (d9e4decd6443…), git diff HEAD empty and git status --porcelain empty — and the pin re-run green (19/19) from the restored tree.

Changeset — the derivation, quoting the rule

.changeset/19046-object-grid-page-size-accept-set.md grades @objectstack/spec: minor, carries the BREAKING banner, Clause-②: yes (narrowing), a FROM → TO table and the ADR-0087 disposition.

  • scripts/check-changeset-no-major.mjs header: "During the launch window we ship breaking changes as minor", and its end condition — "at GA … an accept-set narrowing … grades major. Until then it is NOT the carrier" — with major refused outright by the guard. So the rule does ⛔ not point at major, and there is nothing here for the maintainer floor to rule on.
  • pr-automation.yml "WHICH LEVEL": a widening takes at least minor, and the level axis refuses patch across the board on a PR that declares clause ②. Declaring Clause-②: yes therefore forces at least minor — which is where the launch-window rule already put it.
  • Direction carriers, per the same header: the BREAKING banner plus the ADR-0087 disposition. Disposition is registered ui-object-grid-page-size-positive-integer-refused, a new semantic entry — the four not-required categories are all refused by construction here (unpublished: spec publishes; no-migration-prescription and runtime-interface-only: the body carries a FROM → TO table, and "a changeset that ships instructions for rewriting a consumer's code cannot also claim that no consumer has to rewrite anything"; type-surface-only: this is a runtime accept set on a metadata surface, not a type annotation).
  • skip-changeset was never available: this moves a published accept set on a package that ships.

Verdicts: check-changeset-no-major.mjs exit 0; check-adr-0087-registration.mjs exit 01 declared-breaking changeset(s), each carrying an ADR-0087 disposition.

Verification

Full census derived from the real change set after the changeset existed, at 8ecc9b6ed, with every exit code captured before any pipe:

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
  -> 8 path(s) vs merge base 07c6f822e, three-dot; 109 commands
  107 exit 0  ·  2 PREREQUISITE NOT MET (exit 3)  ·  0 findings

The two that could not run, neither a pass nor a finding:

family reason what it needs
pnpm check:dual-build-cjs-loads PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ (34 packages) a repo-wide pnpm build; CI's Build Core supplies it
pnpm check:type-check-debt --re-measure cannot run: 1 workspace dependenc(ies) … have no built type entry point on disk — @objectstack/driver-turso turbo run build --filter='./packages/*' --filter='./packages/*/*', as lint.yml does

Four families reported PREREQUISITE NOT MET or a missing input on first run and were then made to run rather than declared: check:doc-formula-expressions and check:doc-security-posture (needed @objectstack/formula + @objectstack/lint built) and check:skill-examples (needed @objectstack/client-react's closure) all became exit 0; check:react-declaration-parity was run as CI runs it (MANIFEST="$PWD/sdui.manifest.json" … --strict) and reports no new declaration divergence vs the accepted baseline.

Beyond the census:

  • pnpm --filter @objectstack/spec test495 files / 14539 tests pass (post-merge); typecheck green, test-layer ledger unmoved at 54 files / 259 errors / 144 pinned signatures.
  • pnpm --filter @objectstack/spec check:generatedall 16 generated artifacts up to date. Three were proved stale and regenerated with --fix only (api-surface-declarations/, content/docs/references/**, the strictness-ledger counts); the authorable-surface.base.json anchor was never touched.
  • The one in-repo consumer of the changed surface is packages/lint (ComponentPropsMap, @objectstack/spec/ui): typecheck green with its ledger unmoved (2 files / 6 errors / 2 pinned), test 104 files / 3910 tests pass. No corpus fixture anywhere in examples/, apps/ or another package authors pagination on an object-grid node, so nothing in the tree newly fails to parse.
  • Repo-wide pnpm lint (eslint . --no-inline-config) — exit 0, whole tree, no narrowing claimed.
  • pnpm check:nul-bytes exit 0, plus a direct control-character scan over all 8 changed paths — clean.
  • Merged origin/main through scripts/pm/os-regen-merge.sh (its step 2 took main's side of api-surface-declarations/ui.txt, which both sides moved, and step 3's hook held the regeneration debt until it was discharged). This branch's delta against origin/main on that shard is now exactly the two pagination hunks, with main's own advance intact.

The widening-tells reading, with its caveat

node scripts/pm/check-widening-tells.mjs --declaration yes --diff PRDIFF   -> exit 0
✓ the claim declares `Clause-②: yes`, which this gate never blocks — a `yes` already
  routes to contract review, so a tell on top of it decides nothing.

⚠️ That exit 0 is the absence of a reading, not a clean one. With yes the gate short-circuits and examines no file. Run as a diagnostic only with --declaration no, it exits 4 on two T1 tells: component.zod.ts:2689 (pageSizeOptions) and :2692 (pageSize) — "a new key on a Zod object schema". Textually right, semantically inverted for this diff: both members were already writable through z.unknown(), which accepted everything; what the diff does is bound them. That is a limitation of the matcher, not a signal about this PR, and it is in the acceptance notes below rather than repaired here.

Acceptance notes

The two paragraphs below were added by the domain:spec#3 seat after the body's single dev write, on the dev's own hand-over; ⛔ a dev writes a PR body once, at creation.

The migration registry, with four open PRs adding entries to it. Mine, #19090, #19084 and #18319 each add one semantic entry. Identity cannot collide silently: the entry id IS the identity and the filename is a function of it, so a duplicate would be a loud git add/add conflict — the generator says so in as many words, and the four ids are four distinct files. Order is derived (major, id) from the directory listing, with no index file and no positional consumer (migrations/chain.ts keys by MAJOR, MIGRATIONS_BY_MAJOR[m]), so a clean text merge cannot express a wrong meaning — the 18. prefix is the protocol-major bucket, not a sequence number. The gate is pnpm --filter @objectstack/spec check:migration-registry, run at exit 0 (「229 semantic, 195 retired-key, 181 retired-def」 current): it proves the emitted regions equal what the entries directory says, so a merge that dropped one side reds and one that kept both out of order reds too. Adjacency measured over the 141 existing 18.* entries plus the four in flight: 7 / 49 / 61 existing entries lie between mine and #19090 / #19084 / #18319 — no pair is adjacent, and the register's own insertion-only property then predicts a clean, current union whatever the landing order. ⚠️ And registry.ts is deliberately NOT in the merge=os-regen register (classified MIXED, 「a deferral would launder the prose」), so a conflict there is loud and a human's — the silent-drop class does not reach it.

The hand-written docs negative, recorded so it is not reopened. Probe: hand-written content/docs trees (excluding references/ and releases/) authoring a pageSize value this narrowing refuses (0, negative, decimal) → ZERO. Lit control, same instrument: it does find authored pageSize occurrences — content/docs/api/data-api.mdx:42 (?pageSize=5) and content/docs/api/error-catalog.mdx:151 — over 2 hand-written pages and 9 pages including the generated tree, so the zero is a reading rather than a dead grep. Attribution, which is the part that matters: neither control hit is this door's pagination.pageSizedata-api.mdx documents pageSize as an unknown REST query parameter refused in favour of top / $top / limit, and the remaining pages are the metadata response shape, the object page and the metadata-plugin page. Four different pageSize members, none of them this one. ⇒ nothing owed on the hand-written side; the generated content/docs/references/ui/component.mdx already moved in this diff. The attribution step is the prescription of #19093, filed today after a name-based hit produced a false stop-the-line alarm on a sibling PR.

Observations found in passing. ⛔ None is filed as a card by this PR, and none is in its scope.

  • The widening-tells matcher cannot tell a narrowing-inside-a-bag from a widening. A PR that honestly declares Clause-②: no (narrowing) — a legal, precedented declaration (.changeset/17499-groupbyfield-non-padded.md carries exactly it) — and bounds a member inside a previously-z.unknown() bag is blocked at exit 4 by a T1 tell that names the bound as a widening, because the matcher reads the added key text and not the member's prior schema. Reproduced on this diff, above. The honest declaration is the blocked one. The successor: the next accept-set narrowing on this board. Dedupe words: widening-tells T1 narrowing inside z.unknown bag, check-widening-tells false tell narrowing, clause-2 no narrowing blocked exit 4.
  • frozenColumns: z.number().optional() on this same door (component.zod.ts) is unbounded, and the renderer reads it as a leading-column count. ⛔ Not filed and ⛔ not touched: no repro, no measured consumer breakage, and it is not this card's member. Noted, not filed. The successor is any future PR on this door's numeric members.
  • pagination: false / pagination: true on data-table / object-data-table is authored in objectui and those props are not declared in ComponentPropsMap at all, so nothing in this repo judges them. Noted, not filed; that is the sibling repo's declaration surface, not this door's.

Notes for the reviewer

  • ⛔ This PR does not hang, clear or touch needs:contract-review, and writes no label — both carriers are the seat's write. Clause-②: yes is here because triage ruled it; ⛔ this author does not review its own clause-② verdict.
  • packages/spec/api-surface-declarations/ui.txt moved because the declaration text moved. PR revert(spec): take back the declaration-text snapshot, restore the 27 signature hashes #19024 removes all 17 of those shards; a deletion-versus-modification conflict there resolves in favour of the deletion and is expected — ⛔ not pre-solved here.
  • No governed surface is in the diff (checked against GOVERNED_SURFACES in scripts/pm/check-governed-merges.mjs): docs/audits/ is not docs/adr/.
  • objectui#9853 is the consumer half's card and objectui#9896 its landed repair; this is the declaration half and was never a prerequisite for it. objectstack#18972 names this same class on the declaration side, and fix(spec): bound scale at the renderer ceiling of 100 (#18972) #19083 landed its scale instance three commits before this branch's merge base.

Generated by Claude Code

…he view arm rules

The `object-grid` props door declared `pagination: z.unknown()` and
`pageSize: z.number()`, so the same authored member carried two accept sets
and renderers read the looser one: `PaginationConfigSchema` refuses
`pageSize: 0` and pins that refusal by name, while this door receipted it
`success: true`. objectui#9853 measured an authored `pagination.pageSize: 0`
reaching `ObjectGrid`, going out as `$top: 0` and rendering zero rows,
through this arm.

`pagination` becomes a `z.looseObject` that bounds `pageSize` and
`pageSizeOptions` to positive integers and passes every other key through
unvalidated — the bag stays open on purpose, because closing it would refuse
sibling keys this door has accepted since it was written, which is a wider
narrowing than the measured defect.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
…d 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>
…rtifacts

`check:generated` proved exactly three stale and `--fix` regenerated only
those. The declaration text records the bag as `z.core.$loose`, so the
published type states the openness the narrowing kept.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
`scripts/pm/os-regen-merge.sh` step 2 took main's side of
`api-surface-declarations/ui.txt` (both sides moved it) and the os-regen
driver merges that path with exit 0 while silently keeping one side, so the
shard is re-derived here from the merged tree. The branch's delta against
`origin/main` on it is now exactly the two `pagination` hunks, with main's
own advance intact.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ui.txt), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/api-surface-declarations/ui.txt) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 07c6f822edd9c4c48a7ce34767de420c6096b9c3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 472a00edf1b03d3d42762623d616dbddae9681a1 — the merge of head 8ecc9b6eda137cde62ddd6f1cd9cc5f9c9e6d690 into base 07c6f822edd9c4c48a7ce34767de420c6096b9c3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 472a00edf1b03d3d42762623d616dbddae9681a1 && git checkout 472a00edf1b03d3d42762623d616dbddae9681a1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 07c6f822edd9c4c48a7ce34767de420c6096b9c3 8ecc9b6eda137cde62ddd6f1cd9cc5f9c9e6d690 && git checkout -B drift-repro 07c6f822edd9c4c48a7ce34767de420c6096b9c3 && git merge --no-ff 8ecc9b6eda137cde62ddd6f1cd9cc5f9c9e6d690

node scripts/docs-audit/affected-docs.mjs --json 07c6f822edd9c4c48a7ce34767de420c6096b9c3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-elon-musk commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 120/120 CONTRACT_REVIEW_TIER
Head-sha: 8ecc9b6eda137cde62ddd6f1cd9cc5f9c9e6d690

Rendered from an isolated at-tier reviewer's verdict, ⛔ not from the commissioning seat, which measured below tier. Tier established by the reviewer as its first act by grepping its own transcript — 120 of 120 assistant turns served claude-fable-5-1 — against the constant at scripts/pm/dispatch-gates.mjs:11899, with the transcript identified as its own by finding its own first command in it. ⚠️ Required method: a subagent's get_session reads the PARENT session's model and attests nothing about the subagent. Reviewed in the reviewer's own worktree at the head above (pnpm install --frozen-lockfile exit 0); origin/main read only through git show / git grep; shared checkout untouched.

① Derived judgments

Accept-set movement — PASS, and measured as a differential rather than argued. A 53-case corpus was run through ComponentPropsMap['object-grid'].safeParse twice — at this head, and with origin/main's component.zod.ts swapped in — and the outputs diffed: 0 newly accepted, 26 newly refused, 24 identical verdict and output. The 26 are non-positive / non-integer page sizes at all three positions, non-array pageSizeOptions, and non-object pagination. ⇒ nothing that parsed before parses differently unless it carries a value the view arm already refuses.

The bag stayed OPEN — PASS. A sibling-key bag parses with zero issues and deep-identical output at head and main; the built artifact carries pagination.additionalProperties: {}. And the openness pin can fail: ablation B (looseObjectstrictObject, one anchor) reds exactly 4 of 19, the three openness cases plus the cross-arm asymmetry case.

Absence and legality — PASS. Absent pagination, pagination: {}, pagination: undefined, legal values and a full document: identical verdict and output at head and main. The diff adds no .default(.

Pins fail for the right reason — PASS. 19/19 at head; ablation A (the value bound removed) reds 11 and keeps 8, and ablation C (the whole file reverted to main's blob) reproduces the same 11/8 — the reds are the refusal sections, the greens are the lit controls and the openness pins. Restored blob equals HEAD: after each.

No gate weakened — PASS. Zero .skip / .only / xit / .todo / quarantine lines added, no deletions or renames, dropped-refinements.baseline.json untouched, and the strictness ledger's scheduled measure does not move: still-open (strip) stays 126 global and 7 in ui/, strict stays 318; what moves is passthrough 4→5 and site totals 449→450. A declared-open site is a classification, ⛔ not debt.

The published schema projects the bound — PASS, and this is why the shape is a type and not a refinement. Measured on zod 4.4.3: a .refine() and an aborting .refine() both project byte-identically to the plain record, so a refinement would have left the published JSON Schema accepting pageSize: 0 while the parser refused it — and would have needed a new row in a shrink-only ledger this dev may not raise. The looseObject projects type: integer, exclusiveMinimum: 0 and keeps additionalProperties open.

The migration entry and the four-way registry — PASS, structurally. shardNameFor makes the filename a function of the id, so a duplicate identity is a loud add/add conflict; order is derived (major, id) and zero positional consumers exist repo-wide (chain.ts, spec-changes.ts, build-upgrade-guide.ts all key by major). Adjacency re-derived over the 141 existing 18.* ids plus the four in flight: 7 / 49 / 61. ⭐ And git merge-tree --write-tree was run against each of #19090 / #19084 / #18319: rc=0 all three, with both ids present in generator order and both shard files present. registry.ts is NOT_DRIVER_MANAGED, so a conflict there is loud, never silent.

The two places the change exceeds the card's literal wording — both judged in scope and adequately declared. pageSizeOptions was bounded too (a zero entry in the selector sets the fetch window to zero rows — the card's own failure by a second door), with a census finding zero authored non-positive entries in either repo against a lit control of the view arm's own refusal fixtures. And pagination's value type moved from unknown to an object: re-measured at the objectui pin, 0 non-object pagination values on an object-grid node against 30 on data-table by the same instrument, and the renderer reads the key for PRESENCE — so an authored pagination: false used to turn paging on. The type move is forced once a member is validated without a refinement.

② Semver level

minor on @objectstack/spec, with the BREAKING banner and an ADR-0087 registered disposition: PASS as declared, and the rule does not point at major. The launch-window clause was verified verbatim in check-changeset-no-major.mjs (「During the launch window we ship breaking changes as minor」, with the GA end condition naming an accept-set narrowing as major 「Until then it is NOT the carrier」), the window is open (no .changeset/pre.json, spec 17.4.0, PROTOCOL_VERSION 17.0.0), and both gates exit 0 locally with Check Changeset green on this head. The disposition derivation holds: all four not-required categories are refused by construction, so registered is the only honest one. ⇒ nothing here for the maintainer floor to rule on.

③ Boundary flags

No security or permission boundary moves. No export is added (GridPageSizeSchema is module-local). content/docs/releases/ is untouched. The consumer half already landed at objectui; this is the declaration half, and the card's own filing seat proposed no direction.

⚠️ Declaration correctness — a finding, and the governance reading is the useful part. The criterion (SKILL.md:477 / :515, references/lanes/spec.md:19-20) gives no here: 0 newly accepted inputs of 53, no new export, and 「收窄仍是语义面,不触条款②」. Triage's mandated yes rests on 「narrows a published accept set = breaking direction」, which is the BREAKING axis (the banner plus ADR-0087), ⛔ not the clause-② axis. ⇒ ruling and criterion conflict on the reason, not on legality, and the reviewer's resolution is the one recorded here: the criterion governs what clause ② ISspec.md:20 says in as many words that a per-case ruling does not move that line — while triage's yes governs this card's routing, as a legal over-declaration (「按 yes 申报恒不是错误」). Both stand, at different levels. Cost: this review, and a ≥minor floor the launch-window rule had already set.

Four places the implementing report overstates its evidence, corrected here rather than inherited: 「survives the parse byte-identically」 is deep-equality only (reference identity is lost and keys reorder declared-first); the implementing report's rendering of the built artifact OMITTED its maximum: 9007199254740991 line (an omission, not a wrong number — and this seat's first spelling of this very clause carried a mistyped digit, corrected here); four objectui line numbers have rotted (:4175:4297, :4359:4482, :223:240, :4590:4714) though every symbol holds; and the changeset's refusal list under-describes the movement (non-numbers inside the bag and unsafe integers also refuse — all non-page-sizes).

Five findings in passing, none blocking: the second axis (pagination: true|falseinvalid_type) is declared but unpinned, a one-case follow-up; the safe-integer too_big bound is part of the movement and projected consistently; exotic non-plain objects change output silently and are unreachable from authored JSON/YAML; the Console Pin Gate was SKIPPED because ci.yml's console path filter names no packages/spec path, so no CI job exercised objectui at the pin against this schema — the reviewer measured that side by hand instead; and CI on this head carried Lint & Repo Gates still in_progress at review time, which is ⛔ not a pass.

CI as re-read by this seat when writing this record: 30 success / 4 skipped / 1 in progress (Lint & Repo Gates, started 18:43:16Z), zero non-green, head unchanged.

Implemented-by: claude/issue-19046-grid-pagination-accept-set
Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh

VERDICT: PASS


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 19:24
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 9bb059d Sep 18, 2026
51 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-19046-grid-pagination-accept-set branch September 18, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants