fix(plugin-kanban,plugin-timeline,plugin-detail): refuse a non-positive row cap before it reaches $top (objectui#9925) - #10001
Conversation
…ve row cap before it reaches `$top` Three read points spelled their row cap `?? DEFAULT`. `??` rejects only `null` and `undefined`, so a value `@objectstack/spec` declares refused (every one of these members is `z.number().int().positive()`) was not nullish and survived as a real fetch window: `$top: 0` left for the adapter, the block asked the server for nothing, and the empty board / rail / card named no cause. All three now go through one resolver per site, mirroring the shape objectui#9853 landed on `ObjectGrid` and objectui#9897 repeated on `ListView` — one resolver at every entry is what keeps the answer single. Fail-soft: the value is dropped, the site's own default is used, and one conditional, deduped `console.warn` names the block, the object and the value. The repair is at the READ POINT because each block has two entrances into one key: a `dataSource` binding lowers a named view's `pagination.pageSize` into `schema.limit`, and a block with no binding at all carries the authored `limit` straight through. A repair at the lowering layer closes only the first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
…a neighbour arriving The dedupe claim is not the effect's dependency key: adding a second entry changes the declaration, the effect re-runs and walks the list again, and without a per-(object, value) memory the untouched entry is named twice. The row ablates against removing that memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ing this PR ships objectui#9925 moved this board's row cap from `$top: schema.limit ?? DEFAULT_KANBAN_LIMIT` to `$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT)`, and the source-text pin in `object-kanban-group-by-limit-7322.test.ts` still held the retired spelling. That is the one red check on this branch, and the pin is doing exactly the job it was built to do. The pin states its own contract: line numbers live in the docblocks' prose and the READ is the fact. The read survived — same file, same named `query` object, same `schema.limit`, still lowered into the query's top-level `$top` — and only the spelling moved, because `??` rejects `null` and `undefined` and nothing else, so an authored `limit: 0` rode through as a real fetch window on a key `@objectstack/spec` declares a positive integer. So the constant moves to the new spelling at unchanged strength: one `toContain`, same reader, same key, and a string the retired `??` form cannot satisfy. The two declaration docblocks the failing assertion names — "as the exact text the docblocks cite" — move with it. Leaving them behind would make the assertion's own stated semantic false with nothing checking the disagreement. Both of those also carried a cross-file `ObjectKanban.tsx:264` address, already measured as drifted twice. Per commandment #11 a cross-file `path:line` is the banned form, so these are REMOVED and the citation left to the quoted expression beside them — the same repair objectui#8478 recorded for this exact address ("rewritten to cite the same fact by identifier/behavior instead of by file:line, which survives a line renumbering that a bare address would not"). The differential gate reads zero new citations before and after. The renderer's own cross-reference is restated true: what keeping the read inline preserves is the READ the pin reads out of the named `query` object, not the retired spelling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Part of #9925
Part of, not a closing keyword — deliberately. The card names three blocks. Two of themare repaired here. The third,
record:line_items, does not live where the card says it does,lies outside this PR's declared file face, and sits inside a package another seat has claimed.
It is reported below and left untouched. A third block in the face, which the card did not name,
is repaired here. The dispatch asked for a closing keyword; the standard clause says a merge
that leaves half the card undone must not close it, and this body follows the clause. Named in the
seat report as a conflict rather than resolved silently.
1. The derived site list — the population, by CONCEPT, and the reading that produced it
The card's list of three was not taken on trust. Derived on this branch's base
1ed2e69fabysweeping the concept (a row cap spent into
$topthrough a nullish fallback), not one identifier:object-kanban—packages/plugin-kanban/src/ObjectKanban.tsx$top: schema.limit ?? DEFAULT_KANBAN_LIMITobject-timeline—packages/plugin-timeline/src/ObjectTimeline.tsx$top: schema.limit ?? DEFAULT_TIMELINE_LIMITrecord:reference_rail—packages/plugin-detail/src/renderers/record-reference-rail.tsx$top: entry.limit ?? 3ReferenceRailEntrySchema.limitis authorable and declared positiverecord:related_list—packages/plugin-detail/src/renderers/record-related-list.tsxtypeof schema.limit === 'number' && schema.limit > 0record:history—packages/plugin-detail/src/renderers/record-history.tsxNumber(... ?? 50) || 50, then$top: Math.max(1, limit)record:activity—packages/plugin-detail/src/renderers/record-activity.tsx$top: Math.max(1, pageSize) + 1record:line_items—packages/plugin-form/src/LineItemsPanel.tsx$top: schema.limit ?? DEFAULT_LINE_ITEMS_LIMIT⭐ The card's third site is real, and it is not where anyone would look
The claim's grep of
packages/plugin-detail/srcwas correct to come back empty.record:line_itemsis registered and implemented in
packages/plugin-form, notplugin-detail:ComponentRegistry.register('line_items', LineItemsPanelRenderer, ...)inpackages/plugin-form/src/index.tsx, and the renderer's own header says"LineItemsPanel — the
record:line_itemscomponent". Its cap read is$top: schema.limit ?? DEFAULT_LINE_ITEMS_LIMIT, identical in shape to the two the card measured.So the site is unread, not absent — and it is outside this PR's declared file face
(
packages/plugin-kanban/src/**,packages/plugin-timeline/src/**,packages/plugin-detail/src/**,.changeset/*).packages/plugin-formis also the face objectui#9884 is claimed against, sotouching it here would collide with in-flight work. ⛔ Not repaired here; reported.
⭐ The face did contain a site the card did not name
record:reference_railspends each entry's preview-row cap through the same?? DEFAULTshape, andits
limitis authorable metadata the spec declares positive(
ReferenceRailEntrySchema.limit = z.number().int().positive().optional(), described there as "the$topof the one query this entry issues"). The card asked for the population rather than the firstsite; this is what the population turned out to be inside the fence.
2. What objectui#9853 and objectui#9897 ACTUALLY landed — ⛔ there is no shared helper
Read from the commits themselves before anything was copied.
31d34cb44packages/plugin-grid/src/ObjectGrid.tsx+ one pin + one changesetf5e2fcb4apackages/plugin-list/src/ListView.tsx+ one pin + one changeset⇒ Neither landed an importable helper. Each landed a file-local trio inside its own
component file: a predicate (
isUsablePageSize), a resolver (resolvePageSize) and a conditionaldiagnostic (
describeNonPositivePageSize/describeRefusedPageSize), plus oneuseEffectkeyedon the declaration that emits the diagnostic through
console.warn.ListViewre-declared theshape rather than importing
ObjectGrid's.savedViewLimitin@object-ui/core'selement-data-source.tsis the lowering layer — objectui#9928's subject — and is not aread-point resolver.
This PR therefore repeats the same file-local trio once per package, which is the family's actual
precedent and the only option inside the declared face (a shared home would be
@object-ui/core,which is out of face here). The duplication is called out under "Acceptance notes".
3. drop / clamp / refuse — the answer taken, and why, from the code
Three different answers exist in this tree, and the card's own sibling warns they are not
interchangeable. Established from the code rather than from the card:
record:historyandrecord:activityalready do (Math.max(1, limit)). Itkeeps the query alive but silently turns an authored
0into a window of 1 — a number the authornever wrote, with nothing said.
words: throwing "would take out the whole subtree for a declaration the flat path already
tolerated, which is a worse outcome than the defect."
All three sites here are renderers inside a page subtree, and one of them (
record:reference_rail)is documented as degrading silently by design — a failed entry renders an em dash rather than
blanking the rail. So a throw is worse here than at either sibling, and a silent substitution is
least observable here. ⇒ fail-soft refusal plus one loud, conditional, deduped diagnostic,
the sibling answer, adopted because this code says it carries, not because the card says so.
⛔ Not a spec change from this side — the declaration half is objectstack#19046.
4. Both entrances, which is why the repair is at the read point
Each block reads one key. Two authoring shapes fill it:
dataSourcebinding lowers a named view'spagination.pageSizeintoschema.limit(
OBJECT_KANBAN_DATA_SOURCE = { filter: true, limit: 'limit' },OBJECT_TIMELINE_DATA_SOURCE = { filter: true, sort: true, limit: 'limit' });limitstraight through.A repair at the lowering layer closes only the first. Every refusal assertion in the two block pins
is therefore run twice, once per entrance, and the two are asserted in separate
describeblocks so a failure says which entrance broke.
5. The pins, and the ablation — red leg and green leg
Three pin files, 56 assertions. Each refusal row is the relation (a refused value does not reach
$top; the site's own default does), never a literal, and each is paired with a control that mustNOT fire: a legitimate
7reaches$topunchanged at every site, an absentlimitstays silent,and the rest of the binding still arrives when its page size is refused.
RED leg — ablated from the committed fix, through
ablation-replace.mjsEach leg asserts on disk that the anchor hit exactly once, that the blob hash moved, and after the
run that the blob equals HEAD's and
git diff HEADis empty. ⛔ No exit code was read as evidenceof restore.
isUsableRowLimit(authored) ? authored : fallbackrestored toauthored ?? fallbackdescribeRefusedRowLimitforced to always returnnull[schema.limit, schema.objectName]widened to[schema]Setcheck removedBlob evidence, one example:
124648549d91 -> 315011d04520on mutation,-> 124648549d91onrestore, equal to HEAD,
git diff HEADempty.root vitest config aliases every
@object-ui/*specifier to that package'ssrc, and each pinimports its subject by a relative path inside its own package. Nothing resolves through
dist,so a stale build cannot make an ablation falsely green here.
GREEN leg
At
cebc39270, tree clean: 3 files passed, 56 tests passed.6. Verification
pnpm exec vitest run packages/plugin-kanban/ packages/plugin-timeline/ packages/plugin-detail/type-checkover the three (tsc --noEmit && tsc -p tsconfig.test.json)pnpm --workspace-concurrency=2 --filter 'PKG^...' run buildover the threenode scripts/check-changeset-presence.mjspnpm check:control-bytespnpm check:new-line-citationsnode scripts/check-governed-queue-guard.mjs --test ...Lint is a declared narrowing, not the repo-wide run.
eslintover the six changed files:0 errors, and on the one file re-measured with inline directives honoured, 10 warnings — all
pre-existing rule classes (
no-explicit-any, onereact-hooks/exhaustive-depson a fetch effectthis PR did not touch). The three pieces of evidence:
ESLint#isPathIgnoredovergit ls-files, not guessed: 5156 files of 8150 tracked;--format json: 6;parserOptions.projectand noprojectServiceanywhere ineslint.config.js— so no edit inthis diff can move the verdict on a file the diff does not contain.
⛔ The repo-wide
pnpm lint(turbo run lint) is CI's run and was not taken here.The objectui#7322 pin, re-pointed at unchanged strength (commit
28a24f50e)packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.tsholds a source-textpin that reads
ObjectKanban.tsxoff disk andtoContains the exact$topexpression. ThisPR's repair retires that spelling, so the pin went red on
Test (shard 4/4)— correctly: it wasdoing the job it was built for. The pin's own docblock says what it holds ("Line numbers drift
and live in the docblocks' prose only; the READ is the fact"), and the fact —
schema.limitlowered into the query's top-level
$top— survives this change intact. SoREAD_TEXT.limitisre-pointed, ⛔ not deleted, skipped, loosened or weakened: same file, same reader, same key,
one
toContain,DECLAREDstilltoHaveLength(2), and a string the retired??form cannotsatisfy. Two ablation legs were run and both go red (the retired spelling restored; a deliberate
typo in the new constant), so the re-pointed pin is not vacuous.
The two declaration docblocks the failing assertion names — "as the exact text the docblocks
cite", i.e.
packages/types/src/objectql.tsandpackages/types/src/zod/objectql.zod.ts— carrythe same re-spelling, because leaving them behind would make that sentence false with nothing
checking the disagreement.
⭐ Their stale cross-file address
ObjectKanban.tsx:264was removed, not re-pointed.AGENTS.mdcommandment #11 bans a cross-filepath:lineoutright and directs exactly thisopportunistic repair ("when you touch a file anyway, fix the cross-file line addresses inside
it"); re-pointing would have minted a fresh instance of the banned form, and
check:new-line-citationsreads 0 citations added by this branch. The claiming seat'sdispatch had asked for a re-point and is withdrawn on this point — see the ratification on
objectui#9925.
Seat content-verification on
28a24f50evsorigin/main, taken 2026-09-19T08:38Z: new expression in thereader 0 → 1; retired expression 1 → 0; the three
groupByREAD_TEXTcontrols andDEFAULT_LIMIT_TEXTunmoved at 1 / 1 / 2 / 1 on both refs; cross-file:264addresses2 → 0.
Acceptance notes
⛔ Nothing below is filed as an issue by this PR; it is recorded for the seat to route.
record:line_items— a real, reachable instance of this exact defect, left untouched.packages/plugin-form/src/LineItemsPanel.tsxspells$top: schema.limit ?? DEFAULT_LINE_ITEMS_LIMITwith
DEFAULT_LINE_ITEMS_LIMIT = 500. Out of this PR's face, andplugin-formis objectui#9884'sclaimed face. This is the half objectui#9925 keeps, which is why this body says
Part of.Dedupe words:
record-line-items limit zero $top·LineItemsPanel nullish limit·plugin-form row cap non-positive.(
ObjectGrid,ListView, and the three here). Noted, not filed: a shared home would be@object-ui/core, which is out of face for this card and is objectui#9928's package; the rightmoment to extract it is when that card lands and the family stops moving. Whoever picks that up
reads all five call sites.
(100 / 100 / 3). Whether those belong next to
ObjectGrid's three andListView's one is thesame product question those two cards handed back, and it is not answered here.
record:historyandrecord:activityanswer this class by clamping, not refusing … an authored0there stillbecomes a window of 1 in silence." Both halves are wrong, measured on
origin/mainat 2026-09-19T08:17Z:record:activitydoes not clamp at all —normalizeLimit(
packages/plugin-detail/src/renderers/recordActivityFeed.ts:565-:567) already refuses everynon-positive and non-finite value and falls back to
DEFAULT_ACTIVITY_LIMIT = 20, which is thisPR's own shape minus the warning, and it is pinned at
renderers/__tests__/recordActivityFeed.test.ts:431-:436. And an authored0does not become1 in either renderer:
record:history'sNumber(…) || 50(
renderers/record-history.tsx:76) catches0as falsy and yields 50.The defect is real but narrower and in
record:historyalone: a negative is truthy, survivesthe
||, andMath.max(1, limit)at:85repairs it into$top: 1; a non-integer survivesboth and reaches the adapter as a fractional
$top(2.5→$top: 2.5). A usable cap (10)passes through unchanged on both, which is the control that makes those readings readings.
Filed as objectui#10005 rather than left here, because "which answer does the family keep" is a
decision and this note's stated carrier — "the next PR in this family that touches
plugin-detail" — names no actual PR or person.them. After this PR,
$top: schema.limit ?? DEFAULT_KANBAN_LIMITexists nowhere inpackages/plugin-kanban/src/ObjectKanban.tsx, but it is still quoted as the renderer's read atpackages/plugin-kanban/src/index.tsx:426and:570and atpackages/app-shell/src/views/metadata-admin/previews/block-config.ts:237(readings taken onorigin/mainat 2026-09-19T08:17Z). They are prose, not pins, so nothing turns red. They are fencedout of this PR deliberately:
block-config.ts:232names its own source-text pin(
__tests__/block-config.test.ts), so editing that comment carries a redness risk this PR hasno reason to take, and none of the three is one of the three filing classes. The two declaration
docblocks the objectui#7322 pin actually names —
packages/types/src/objectql.ts:3717andpackages/types/src/zod/objectql.zod.ts:2096— ARE in this PR, because the pin's assertionmessage is "as the exact text the docblocks cite" and leaving them behind would make that
sentence false with nothing checking it. Carrier for the remaining three: none named.
.changeset/7322-object-kanban-group-by-limit.md:15and.changeset/7772-page-block-kanban-group-by-control.md:28quote it too and are ⛔ out of reach —a changeset records what its own change did at the time and is not retroactively rewritten.
🤖 Generated with Claude Code
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Generated by Claude Code
Generated by Claude Code