fix(core): drop a saved view row cap the contract refuses at the lowering layer, and say so on the path with no renderer (objectui#9928) - #10014
Merged
Conversation
`savedViewLimit` admitted its carrier on `typeof … === 'number'` alone, so a saved view's `pagination.pageSize` — or its legacy flat `limit` — of `0`, `-10` or `25.5` lowered unchecked into the composed `limit`. Both ends of that journey are declared positive; the lowering layer in between asked nothing. Measured on both consumers of the composed key, which behave differently: through a renderer the refused value met a block with its own guard, so the named view's cap went missing and the read went WIDER than the view asked for; through `ViewDataProvider.resolveElementDataSource`, which forwards the key straight to `DataFetcher.fetchRecords` with no guard of its own, `0`, `-10` and `25.5` reached the fetcher verbatim. The cap is now dropped. Clamping is refused because this layer has no default to clamp to — every consuming block owns its own and `ViewDataProvider` owns none. Throwing is refused because the composer is pure and sits under every bindable block. Dropping alone would be silent and would remove the only place the author was being told, so `elementDataSourceRefusedLimitMessage` is the loud half: a pure builder, following the shape `elementDataSourceViewNotFoundMessage` already established here, reported by `ViewDataProvider` on the `console.warn` channel the repaired read points use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
The reverse verification caught it: one assertion named CONTROL also required the cap to be dropped, so it went red with the guard ablated. Split into a real control (the other composed keys survive a USABLE cap) and an unlabelled pin for the drop, so that every name carrying CONTROL or SILENCE passes on both sides of the guard — which is the property that makes them worth having. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Brings in objectui#10009 (the shared gate's row-cap authorship rule), the immediate consumer of the key this branch changes.
Contributor
✅ 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
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #9928
savedViewLimitadmitted its carrier ontypeof … === 'number'alone, so a saved view'spagination.pageSize— or its legacy flatlimit— of0,-10or25.5lowered uncheckedinto
composed.limit. Both ends of that journey are declared positive; the lowering layer inbetween asked nothing.
FIRST DELIVERABLE — the population measurement, with its control
Taken on the branch base
9f7e8463c, overgit ls-files(8158 files): every strict-JSON fileparsed and walked as a schema tree (548 files), every JSON fence in
.md/.mdxparsed the sameway (255 fences), and a literal scan across
.ts/.tsx/.js/.jsx/.mjs/.cjs(5161 files). Usabilitypredicate, restating the one the repaired read points spell: a number, an integer, greater than
zero.
A. The card's own population — carriers reached through a saved view
pagination.pageSizelimit⛔ The control is DARK here, so this zero is NOT a reading. The tree contains exactly two
authored saved views, both in
examples/schema-catalog/src/schemas/plugin-view/object-view-named-views.json,and neither carries a row cap in any spelling. There is no denominator, so "no saved view carries a
refused cap" is a statement about an instrument that found nothing at all — exactly the dead
instrument the dispatch warned about. Reported as dark rather than dressed up as a zero.
B. The nearest LIT instrument — the same carriers in any metadata position
pagination.pageSizepageSizelimitThe control is lit in every row. These three numbers were re-taken independently here and land on
the same values PR #10009 reported for its own run, which is corroboration rather than a relay.
C. Source literals
pageSize: 197 found, 176 legitimate, 21 refused — 18 in test files, and the 3 outside them areguard internals in
RelatedList,ObjectGridandListView(the literal0inside apositivity check), not authored metadata.
limit: 149 found, 122 legitimate, 27 refused — 14 intests, the other 13 the same kind of guard internal plus the ESLint rule corpus.
⇒ zero refused authored metadata, zero published metadata a user could have copied. The
re-grade switch in the dispatch (non-zero AND published) does not fire, so the rule lands.
What actually happens — measured on BOTH consumers, which do not agree
composed.limithas two consumers, and the family has so far only looked at one of them.composed.limitpagination.pageSize: 00limit: 0ViewDataProvider, onceViewDataProvider.resolveElementDataSourceforwards this key straight toDataFetcher.fetchRecordsand has no guard of its own. Measured on the unmodified tree,0,-10and25.5all reached the fetcher verbatim. Through a renderer the story is theother one the dispatch carries — a block with its own guard drops the value and draws its own
default, so the named view's cap goes missing and the read goes wider.
⭐ So both directions in this family are real, on different paths. The corrected direction the
dispatch carries ("the cap is DROPPED and the read goes WIDER") is the renderer path. On the
ViewDataProviderpath there is no renderer, nothing guards, and the original direction holdsliterally: a starved read. Neither reading is wrong; they describe different consumers.
Drop, clamp or refuse — and the evidence
DROP.
⛔ Clamp is refused because this layer has no default to clamp to. Every consuming block owns
its own default and
ViewDataProviderowns none, so a number invented here would override adefault the author never asked it to — and quietly substituting a number the author never wrote
is the quieter half of this same defect.
⛔ Throwing is refused because
composeElementDataSourceis pure and sits under every blockthat can be bound to a view; a throw takes out the page over one declaration, which is worse than
the defect.
⇒ an absent
limitis the honest statement that the view supplied no usable cap, and it is theone thing every consumer already handles.
Does PR #10009's argument transfer to this layer? Partly — and the part that does not is mechanical
The conclusion transfers. #10009's reasoning was: after the repair the renderer that used to
report the refused value receives something it accepts, goes correctly silent, and the repair
therefore removes the only place the author was being told — so the layer that makes the decision
is the layer that reports it. The same thing happens one layer down. Measured at the gate, on both
sides of this branch's guard:
pagination.pageSizebefore0, component declares nothing00, component declares5050507, component0(the case #10009 repaired)777, component declares nothing77⭐ Two things that reading settles, and neither was assumed:
component's refused cap being displaced by a usable one, and it returns "nothing to say" when
the component authored nothing. So this branch does not silence a gate message — there was none.
0to absent, and the renderer's owndiagnostic treats an absent key as "not a mistake". That is fix(react): a row cap the contract refuses is not "authored" in the shared gate (objectui#9899) #10009's structure exactly, one layer
down: the repair removes the only place the author was being told.
The mechanism does NOT transfer, and this is the part where "I did what the neighbour did"
would have been wrong. #10009 emits from a React effect because the gate is a component.
composeElementDataSourceis a pure function and the render path calls it from auseMemo, soa warning emitted inside it would fire during render and fire again on every re-render — the very
thing #10009 avoided.
⇒ this layer follows the shape its own module already established for its other refusal:
elementDataSourceViewNotFoundMessageis a pure builder, exported here so that every callerreports the same defect the same way. New export
elementDataSourceRefusedLimitMessageis the sameshape, and
ViewDataProvider— the consumer with no guard, the one with a real victim — reports iton the same
console.warnchannel the repaired read points use. It is fail-soft: records stillload, so an
errorwould be worse than the defect.On the renderer path the author now hears nothing about a refused view cap. The builder exists
and is wired on the path inside this card's fence; the gate would have to adopt it for the other
path, and the gate is
packages/react— explicitly out of this card's fence and #10009's own file,landed hours ago. ⛔ Not silently absorbed: it is called out here and reported for filing.
Tests
Reverse verification is a real pair, not a re-run. The guard's positivity half was ablated on disk
(the
Number.isInteger(value) && value > 0conjunction removed, leaving every export in place, sothe red is about the rule and not about a missing import), the injected and removed text each
counted on disk before the run, and the restore proven by blob hash against
HEADrather than byan exit code.
pnpm exec vitest run packages/core/src/data-scope/— 5 files, 135 tests passed.With the guard ablated: 19 failed, 12 passed. Every name carrying
CONTROLorSILENCEis among the 12 — checked mechanically, not by eye.
named a control while also asserting the drop, so it went red with the guard removed. Split in
a follow-up commit on this branch rather than relabelled.
pnpm exec vitest run packages/core/ packages/react/— 248 files, 4477 tests passed (on themerge with
mainthat brings in fix(react): a row cap the contract refuses is not "authored" in the shared gate (objectui#9899) #10009).limitmapping:pnpm exec vitest run packages/plugin-list/ packages/plugin-grid/ packages/plugin-kanban/ packages/plugin-timeline/ packages/plugin-detail/ packages/plugin-form/—604 files, 5966 passed, 1 skipped.
elementDataSourceconsumer suite pluspackages/react/— 104 files, 1216 tests passed.pnpm --filter @object-ui/core type-checkandpnpm --filter @object-ui/react type-check— bothclean, after
pnpm --filter '@object-ui/react^...' buildsupplied the dependency dists.pnpm exec eslint .— repo-wide, not narrowed: 5161 files, 0 errors, 13256 warnings, allpre-existing. The four changed files add zero warnings (the 13 on
ViewDataProviderarepre-existing
no-explicit-any, none on an added line). Run on the final head5e65b4e85.check:element-data-source-declaration,check:control-bytes,check:new-line-citations,check-changeset-presence,check-changeset-no-major— all exit 0 on5e65b4e85.Acceptance notes
To file (reproducible defect). The BINDING's own
limit— the other operand ofconfig.limit ?? savedViewLimit(view)— is still admitted unchecked, sodataSource: { object: 'account', limit: 0 }reachesfetchRecordsas0on the same unguardedpath this PR repairs for the view's half. ⛔ Deliberately not fixed here: it raises a PRECEDENCE
question this card does not own — does a refused binding cap suppress the view's legitimate one? —
which is the same question #10009 answered for the component/view pair one layer up. Nothing in the
new test file asserts the current answer, so whoever settles it will not have to edit a pin that
endorsed it. Dedupe words:
element data source binding limit non-positive,config.limit zero lowered,dataSource limit positivity.To file (author feedback regression, introduced here). See the open item above: the renderer
path loses its one diagnostic for a refused VIEW cap. The fix is the gate adopting
elementDataSourceRefusedLimitMessage; that file is out of this fence. Dedupe words:gate refused view cap silent,ElementDataSourceGate view pageSize diagnostic,refused view cap no reporter.noted, not filed: the card body and the dispatch both cite the spec's positive
limitdeclaration at
packages/spec/src/ui/page.zod.ts:234. That address is wrong twice over — there isno
packages/specin this repo (the spec arrives as the dependency@objectstack/spec), and inthat file line 234 is a docblock about retired component types. The declaration itself is real and
was verified by content, at
limit: z.number().int().positive().optional(). Carrier: the nextperson to re-take that citation; this is the second time the family has shipped a
file:linewritten from memory, which is the thing AGENTS.md commandment #11 rules against.
noted, not filed: the vitest invocation guard refused a run here that appended
packages/react/src/element-data-source/__tests__/ElementDataSourceGate.test.tsxafterpackages/react/— positional filters take a union, so the narrower path added nothing. The guardwas right and its message named the fix. Carrier: nobody — recorded because the run it refused was
one this seat would otherwise have reported as a narrowed run.
🤖 Generated with Claude Code
https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Generated by Claude Code