fix(plugin-calendar,plugin-map): honour filter, sort and the row ceiling on inline value data - #9138
Conversation
…ing on inline `value` data Ports objectui#8769's repair off `ObjectGantt` to the two siblings that carry a hand copy of the same short-circuit. Their fetch effect exited on `provider: 'value'` with `setData(dataItems)` BEFORE the adapter query, which is the one site in each file that lowers `schema.filter` onto `$filter`, `schema.sort` onto `$orderby` and the objectui#7210 ceiling onto `$top`. An authored `filter` therefore reached nothing and every authored row was drawn — the fail-OPEN direction, because the key that was dropped is the key that NARROWS. Not a literal transplant: `ObjectGantt` resolves ONE adapter for every provider and could delete the branch and fall through, while these two call `find` inside their `dataProvider === 'object'` arm, behind an `$expand` projection an inline set has no metadata to build. So each resolves `ValueDataSource` for the inline provider only, leaving the `api` arm exactly as it was, and no dependency array moves. Part of objectui#9061.⚠️ BLOCKED on a decision — see the PR body: routing the inline rows through `ValueDataSource` makes them pass through that adapter's `JSON.parse(JSON.stringify(...))` constructor clone, which retires objectui#6018's pinned guarantee that a map's inline rows never have to be serializable. Three standing assertions are left RED and untouched on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…ange which query keys apply The changeset and both docs sets for objectui#9061: an authored `filter` / `sort` narrows and orders inline rows on every provider, the objectui#7210 ceiling applies to inline rows and is applied to the FILTERED set, and the two consequences of routing inline rows through the in-memory adapter — deep-copied row identity, and the JSON round-trip that makes inline rows have to be serializable. Part of objectui#9061. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ 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
|
|
Director seat pointer (director seat, summon #22, Generated by Claude Code |
Bring the branch up to date with `main` (64 commits behind) so CI runs against the current tree. No source change of this branch's own is touched. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ 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
|
…he arm its published `data` row declares The nine reds on this branch were fixture-side, not a production defect, and eight of them were one cause seen at two altitudes. Rung 1 of the record-source ladder is judged against the BLOCK's own published `data` row (objectui#8348, decision batch #83 — "the row decides"): - `object-calendar`.data is `z.array(z.unknown()).optional()` -> arm 'array', so a `{ provider, items }` CONFIG OBJECT is refused by kind. - `object-map`.data is `ViewData` -> arm 'view-data', so a BARE ARRAY is refused by kind, and the objectui#5305 normalizing head is gone. Both new pin files used the other block's spelling. Refused by kind, the ladder fell through `staticData` and `objectName` to null, so the view had no record source at all and hit `DataSource required for object/api providers`. The calendar helper reads that through `getByTestId` ("unable to find calendar-view"); the map helper reads it through `queryAllByTestId`, which counts an error screen as '0' — the same failure, two altitudes. - calendar: the six inline rows now use `staticData`, the one rung that wraps into `{ provider: 'value', items }` on this block. Every assertion is unchanged. - map: `arrayShorthandSpelling` asserted the retired bare-array rung. Replaced by `offArmDataSpelling`, the mirror image of the calendar's new row of the same name; each carries a lit control, so the absence half is a reading about the SPELLING and not about the harness. `ObjectMap.filterConfig.test.tsx` is a second, genuinely different cause: it grades map CONFIG resolution and authored `filter` values purely as a vehicle. Those filters were inert on an inline set before this branch — the fail-OPEN bug #9061 repairs — and now correctly select nothing. - describe (d)'s ordinary `[['owner','=','me']]`: the row now satisfies it, so the case reads BOTH that the filter is honoured and that it did not eat `schema.map`. - describe (a)'s legacy `filter.map` stash is a query filter that no row can satisfy, so its two marker-counting rows take the exempt host `data` prop. This also repairs a phantom green: the `toHaveLength(0)` row had started passing because the set was empty, and would have gone on passing with config resolution completely broken. No production byte is changed by this commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ 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
|
✅ 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
|
⛔ DO NOT MERGE AS-IS — this pull request is a DECISION REQUEST. It is complete, measured and green on everything it set out to do, and it leaves three standing assertions RED on purpose. The red is the decision, not an oversight. See "The decision" below.
Part of objectui#9061. Deliberately not a closing keyword: the card must stay open until the decision below is taken.
What this does
ObjectCalendarandObjectMapeach held a hand copy of theprovider: 'value'short-circuit objectui#8769 removed fromObjectGantt. Inside the record-fetch effect, the inline branch calledsetData(dataItems)plussetRowCeilingwith truncated false and RETURNED — before thefinda few dozen lines below, which is the ONE site in each file that lowersschema.filteronto$filter,schema.sortonto$orderbyand the objectui#7210 ceiling onto$top.Consequence, identical in both: an authored
filterreached nothing and every authored row was drawn. The fail-OPEN direction — the key that was dropped is the key that NARROWS, so the view answered a wider question than the author asked. Nothing was exposed that was not already in the authored schema.Not a literal transplant, and the difference is structural.
ObjectGanttresolves ONEeffectiveDataSourcefor every provider, so its repair was to delete the branch and let the inline case fall through. These two callfindINSIDE theirdataProvider === 'object'arm, behind an$expandprojection an inline set has no metadata to build (and, on the calendar, behind anobjectSchemaReadygate scoped to that same arm). Falling through would throwDataSource required for object/api providerson a view that needs no DataSource at all. So each resolves aValueDataSourcefor the inline provider only; theapiarm keeps exactly the behaviour it had, and no dependency array moves — nothing about WHEN either effect re-runs changes.The decision this is blocked on
Routing inline rows through
ValueDataSourcepasses them through that adapter's constructor, which deep-clones withJSON.parse(JSON.stringify(...)). That imposes a requirement these two renderers do not have today: inline rows must be JSON-serializable.Measured, three surfaces, same input (a record carrying a back-reference to its own graph — what an expanded lookup looks like once a host hands the resolved rows to the block as inline data):
ObjectGantt(untouched here)TypeError: Converting circular structure to JSON, from the render body, via its ownJSON.stringifymemo keyObjectMapObjectCalendarSo the platform is already inconsistent about this, and this PR makes the two siblings match the gantt — by retiring a pinned guarantee on the map. That is a ruling, not an implementation detail, and it is not this PR's subject, so it is left for a maintainer:
ValueDataSourceclones "to prevent external mutation"; on a read-only query the clone is pure cost. Replacing the JSON round-trip in@object-ui/core(for example withstructuredClone, which handles cycles andBigInt) makes every assertion below green with no change to this diff.ValueDataSourceconsumer (aDatevalue would survive as aDateinstead of becoming a string), so it needs its own card and its own measurement. Recommended.ViewData.itemsis declared serializable metadata in@objectstack/spec, a circular record graph is off-contract, and the failure is LOUD (an error panel naming the cause) rather than silent. Then objectui#6018's third case is re-based, the twoObjectMap.filterConfigfixtures below are re-based, and this lands as written.⛔ An agent seat should not pick between A and B on its own: option B deletes a guarantee a previous card chose deliberately and recorded, and it does so as a side effect of a PR about something else.
The three RED assertions, named
All three are untouched in this diff — no fixture was edited to make anything green.
packages/plugin-map/src/ObjectMap.dataConfigMemo.test.tsx— "renders inline data the serializer cannot handle — identity needs no round-trip" (objectui#6018). This one IS the decision.packages/plugin-map/src/ObjectMap.filterConfig.test.tsx— "really is the DEFAULT config that applies, not 'no config at all'". Collateral: it authors a legacyfilter.mapstash alongside inline rows and relied on the filter being DROPPED; honoured as an object-shaped$filter, it now narrows to zero markers.packages/plugin-map/src/ObjectMap.filterConfig.test.tsx— "appliesschema.mapwhen an array filter is authored alongside it". Same collateral: it authorsfilter: [['owner', '=', 'me']]against a row that has noowner.2 and 3 are ordinary fixture triage under option B (give the row the field the filter asks for, or move the case onto an adapter that ignores
$filter) — they are NOT a second decision, and they were left red only so that the diff shows exactly which standing assertions the port disturbs.⭐ The row-ceiling measurement the brief asked for
An inline list that exceeds the ceiling now renders FEWER rows than the author supplied. Measured: 2,501 inline rows in, 2,000 drawn, on both renderers. Reported rather than buried, because "inline data is authored, not fetched" is a real objection.
It is shipped anyway, and the reason is that the cut is loud: both renderers already mount
NonGridRowCeilingNoteunconditionally at their top level, so the footnote naming BOTH numbers (2,000 of 2,501) renders on the inline path with no extra wiring.ceilingNotepins that in each file. objectui#7210 ruling a-prime's budget is measured in DOM elements per record, its own measurement table was taken over the inlinevalueprovider, and its text carves out no provider — an inline row costs the browser what a fetched row costs. The exemption that REMAINS is the hostdataReact prop, and only it: those rows are not ours to cap and we issued no query whose total a footnote could name.ceilingOrderpins the other half from the observable side: the ceiling is applied to the filtered set (2,400 rows, 800 of them matching, 800 drawn, no footnote), matching the fetching path where$filterand$toptravel in one query.Acceptance, and how each row is met
New:
packages/plugin-calendar/src/ObjectCalendar.inlineQueryKeys-9061.test.tsx(8 cases) andpackages/plugin-map/src/ObjectMap.inlineQueryKeys-9061.test.tsx(9 cases). 17 passed / 17 on the head of this branch.twoSidedFilterrenders the SAME rows and the SAME filter twice — once inline, once through a context adapter that is itself aValueDataSourceover those rows — and reads the disagreement, so "the filter was ignored" cannot be confused with "the filter matched everything".inlineSortreads the drawn id order off the view (the calendar stub carriesdata-event-ids; the map stub carries each marker's longitude, which the marker array takes straight off the record).staticDataSpellingon both;arrayShorthandSpellingon the map only — see the divergences below.ceilingCap,ceilingNote,ceilingOrder, per renderer.providerBackedControlrenders a NON-inline view with the same filter, the same sort and the same rows through the context adapter, and asserts the rows and their order are unchanged. It is green before this repair, after it, and on both ablation legs — that is what bounds the change to the inline path.controlrenders an inline view authoring NONE of the three and asserts the authored rows, in the authored order, with no footnote.Ablation — each renderer's fix removed SEPARATELY
Each leg restored the ONE file from the branch point, proved the mutation reached the disk before reading anything (repair marker
1 -> 0, pre-repair line1 -> 2, blob hash changed), then restored withgit checkout HEAD -- pathand proved the tree clean by state, never by an exit code.ObjectCalendar.tsxrevertedtwoSidedFilter,inlineSort,staticDataSpelling,ceilingCap,ceilingNote,ceilingOrder;controlandproviderBackedControlgreenObjectMap.tsxrevertedarrayShorthandSpelling;controlandproviderBackedControlgreenNeither renderer's repair covers the other. Both needed porting.
Restore proof, both legs:
blob 460e680d… == HEAD blob 460e680d…; git diff HEAD emptyandblob 42d8b312… == HEAD blob 42d8b312…; git diff HEAD empty.Where the two files DIVERGE — verified per file, not assumed
The brief asked for this explicitly. The short-circuit itself is byte-identical in shape; four things around it are not.
ObjectMap.getDataConfignormalizes a bare array underdatainto{ provider: 'value', items }before delegating to the shared ladder (objectui#5305).ObjectCalendarhas no such head — three spellings reach the repair on the map, two on the calendar. Hence the extra map-only case.ObjectMap'srowCeilingstate said "A hostdataprop and an inlinevalueset are never truncated by us". Corrected here.ObjectCalendar's equivalent docblock never made that claim and is untouched.ObjectCalendar's effect guards every state write withisMounted;ObjectMap's does not. Each new branch follows its own file.ObjectGantt's "an inline set is never capped and never footnoted" case. Neither sibling has one —ObjectMap.rowCeiling-7210andObjectCalendar.rowCeiling-7210both grade theobjectprovider only — so the ceiling cases here are NEW coverage, not an inversion. Read both files' case lists before writing.They do not need different repairs: the same shape lands on both, with the same three keys on the same query, and both pay the same serializability cost. The divergences above are about their surroundings.
The fourth surface — reported, not fixed
Triage asked whether any further surface carries this, because a short-circuit found on three components is one that was copied. It does:⚠️ With one measured difference —
packages/plugin-tree/src/ObjectTree.tsxholds the same inline exit before the same$filter/$topquery.ObjectTreenever readsschema.sortat all (zero hits forconvertSortToQueryParamsand forschema.sortin the whole file), so two keys are dropped there, not three. Filed as objectui#9136 and left alone, per this card's fence. It inherits the decision above unchanged.Two further inline branches were looked at and are deliberately NOT graded:
ObjectGrid(a grid, which pages its own rows and sits outside objectui#7210's non-grid ceiling) andListView(an inline branch that applies its own search term and delegates to a child view). Each needs its own reading.Clause-2 declaration holds
Declared no, and the diff keeps it: no declared type, registration input, exported symbol, authorable key or accept-set moves.
OBJECT_CALENDAR_DATA_SOURCEandOBJECT_MAP_DATA_SOURCEare byte-identical — neitherindex.tsxis in this diff at all.filterandsortwere already declared and already read on theobjectpath; this is only about whether they are read on one more path.Verification
pnpm exec vitest run packages/plugin-calendar/ packages/plugin-map/— 3 failed / 361 passed, and the 3 are exactly the named reds above. Every other case in both packages, including all 17 new ones, is green.pnpm exec vitest run packages/plugin-view/ packages/sdui-parser/ examples/schema-catalog/ apps/console/src/__tests__/public-block-binding-reach.test.tsx packages/react/src/__tests__/SchemaRendererProvider.smoke.test.tsx— 85 files / 2704 tests, all green.pnpm turbo run type-check --filter=@object-ui/plugin-calendar --filter=@object-ui/plugin-map— 17 tasks successful.eslint . --no-inline-config --format jsonover the whole repo at7bd112303: 4807 files judged, and all four files this PR touches underpackages/carry 0 errors (only pre-existingno-explicit-anywarnings shared with their neighbours). The 79 error-carrying files are all untouched by this PR.check:control-bytes,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:changeset-claims,check:new-line-citations,check:doc-fences,check:element-data-source-declaration,check:unreferenced-sources,check:shell-escape-residue,check-changeset-presence,check-changeset-no-major.check:readme-exportsneeds builtdist/for 36 packages and refused with "runpnpm buildfirst" over packages this PR does not touch — a prerequisite, not a red. Declared to CI.Acceptance notes
Out-of-scope observations, noted and not filed:
packages/plugin-map/src/index.tsxsays the map's fetch "issues no$top", andpackages/plugin-calendar/src/index.tsxsays the calendar "fetches the whole window rather than a capped page". Both stopped being true at objectui#7210, before this card. Not corrected here: both sentences sit directly above anElementDataSourceMappingdeclaration this PR's clause-2 declaration says it will not touch, and neither is made wrong by this change. The carrier would be whoever next edits those two registrations.ObjectMap.filterConfig.test.tsx's "ignores a MapConfig stashed underfilter.mapand falls to the default config" still passes, but after this change it would pass for two reasons at once (the default config finds no coordinates, AND the legacy stash now narrows to zero rows as an object-shaped$filter). Worth re-basing alongside the two red cases if option B is taken. Carrier: whoever re-bases those fixtures.Prepared by an automated developer seat, session
session_01UzHd6hDYatoDn17BuwKxnZ.🤖 Generated with Claude Code
https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Generated by Claude Code