…`value` data
`ObjectGantt`'s `reload` short-circuited the `provider: 'value'` path with
`setData(dataItems); return;` BEFORE `effectiveDataSource.find(...)` — the one
site that lowers `schema.filter` to `$filter`, `schema.sort` to `$orderby` and
the objectui#7210 ceiling to `$top`. An authored `filter` therefore reached
nothing and the chart drew every inline row: the fail-OPEN direction, since the
dropped key is the one that narrows.
Measured two-sided before the repair, with one `ValueDataSource` serving both
sides so the matcher was literally the same object: the same five rows and the
same `status = open` filter drew 3 through the fetching path and 5 inline.
The inline branch is removed; `provider: 'value'` now goes through the adapter
`resolveDataSource` already returns for it, which implements `$filter`,
`$orderby`, `$skip`, `$top` and `$select` over its own array. No combinator was
written here. The dead `dataItems` binding goes with it — `reload` no longer
reads the payload, and authored items still reach it through the deep-compared
`dataConfig` that builds the adapter.
The ceiling now applies to inline rows, and the `never capped by us` case in
`ObjectGantt.rowCeiling-7210.test.tsx` is inverted with the reason recorded on
it: that case pinned the short-circuit, not ruling a′, whose budget is measured
in DOM elements per record over its own inline-provider measurement table and
which carves out no provider. Rows a HOST hands down through the `data` prop
stay exempt and get their own case.
The repair does not reach `convertFiltersToAST`, so it does not inherit the
throw objectui#9001 is adding there; the inline matcher is local and excludes
the row with a log instead.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Fixes #8769
Exit taken: the preferred one. Both exits were pre-authorised — apply the keys, or refuse the combination loudly — and the measurement chose the first:
resolveDataSourcealready answers this provider withValueDataSource, which implements$filter,$orderby,$skip,$topand$selectover its own array, so there was no capability the inline path could not support and nothing to refuse. No filter combinator was written here — objectui#8513's work stays where it is.What was wrong
ObjectGantt'sreloadshort-circuited the inline provider: it set the authored rows and returned beforeeffectiveDataSource.find(...), which is the one site that lowersschema.filteronto$filter,schema.sortonto$orderbyand the objectui#7210 ceiling onto$top. An authoredfiltertherefore reached nothing and the chart drew every inline row.The direction is fail-open: the dropped key is the key that NARROWS, so the author was shown more rows than declared, with no diagnostic. ⛔ Nothing was exposed — the rows are already in the authored schema. What was wrong is that the chart answered a wider question than the one asked.
The two-sided reading is the finding
A one-sided reproduction cannot separate "the filter was ignored" from "the filter matched everything", so the same five rows and the same
status = openfilter were rendered twice, with oneValueDataSourceserving both sides — the matcher is literally the same object, and the only remaining variable is which branch ofreloadran.objectName+ context adapter)1,3,5provider: 'value') — before1,2,3,4,5provider: 'value') — after1,3,5What changed
The inline branch is deleted;
provider: 'value'falls through to the same adapter query as every other provider. The now-deaddataItemsbinding goes with it —reloadno longer reads the payload, and authored items still reach it through the deep-compareddataConfigthat builds the adapter, so the objectui#6592 memo-identity reasoning is preserved and re-stated where the binding used to be.The assumptions, and how each resolved
A — does the repair inherit objectui#9001 / PR objectui#9049's new
FilterOperatorErrorthrow? NO, measured.convertFiltersToASTis reached only by@object-ui/data-objectstackon the wire path.ValueDataSourcenever imports it — its imports are@object-ui/types,@objectstack/spec/dataand its ownbatchTransactionmodule — and it carries its own local refusal for that comparand, which excludes the row and logs once. So an inline chart whose filter carries such a comparand renders with zero rows and a console line, it does not throw at render. This was a deliberate decision, not an accident of routing: an in-memory matcher decides about one row at a time, while the converter is deciding whether to send a query at all, and that asymmetry is the adapter's own stated refusal shape. Pinned from the outside byrefusedComparandDoesNotThrow.B — is
find(...)really the only site applying the three? HELD. Verified by symbol across the package:applyNonGridRowCeilingappears at exactly one call site, and$filter/$orderbyat exactly one each, all insidereload.C — are the ceiling and filter/sort independent? NO, and the order is now asserted. A ceiling computed before filtering is a different number from one computed after. Filter first, ceiling second, matching the fetching path exactly: there
$filterand$toptravel in one query and every backend filters before it limits, so any other order would have made the two paths disagree again in a subtler way.ceilingOrderpins it from the observable side — 2,400 inline rows of which 800 match draw all 800 with no footnote, which a ceiling-then-filter implementation could not produce.D — is
data: { provider: 'value' }the only spelling? NO — there are TWO. The ruled three-rung record-source ladder also wrapsstaticDatainto aprovider: 'value'config, so an author lands on this path either way.staticDataSpellingcovers the second one. (A baredata: []array does not reach here on the gantt — this renderer returns it verbatim rather than normalising it, so it falls through to the fallback adapter; and the hostdataReact prop is a different branch, objectui#7333's.)E — are the card's
path:lineanchors stale? ASSUMED SO and not used. Everything was re-derived by symbol, and there is no line citation anywhere in this diff.F — is today's behaviour silently wrong? HELD. Nothing warned. The short-circuit set state and returned with no console line, so the card's "no diagnostic" framing is correct.
A standing pin is INVERTED here, deliberately
ObjectGantt.rowCeiling-7210.test.tsxasserted "an inlinevaluedata set is never capped by us, and never footnoted", andObjectGantt.tsxcarried the matching comment. That case pinned the short-circuit, not objectui#7210 ruling a-prime:valueprovider" — an inline row costs the browser exactly what a fetched row costs;So the case is inverted in place with that reasoning recorded on it, and the exemption that genuinely remains — rows a host hands down through the
dataReact prop, which we did not query and have no total for — gets its own case beside it. If a maintainer reads the exemption as ruled rather than incidental, this is the one hunk to revert; it is isolated.Behaviour a consumer can notice
filter/sortnow narrows and orders inline rows.ViewData.itemsis serializable metadata, so every value the contract admits survives that copy unchanged; only reference comparison against the authored array changes, andidequality is the replacement.Ablation — predicted before running, then measured
Prediction written down first: restoring the short-circuit turns 8 cases red and leaves 4 green (
control, plus the above-ceiling, below-ceiling and host-data-prop ceiling cases).Measured: 8 failed, 4 passed — exactly the predicted split, no miscount.
On-disk proof, both directions: the injected marker went 0 to 1 and the removed marker 1 to 0 in the same file; blob
e6e1b46abecamefba326a2. Restore proved by hash equality back toe6e1b46aand by an emptygit diff HEAD, from a trap with absolute paths. Nodistis involved on this path — the pin imports the mutated module relatively, as source.The
controlcase is the one that makes the rest readable: an inline chart with no filter, no sort and under the ceiling draws the same five rows in the same order on both legs, which is what separates this repair from "the inline path now drops rows".Verification
All at
05d946890, the final commit.pnpm exec vitest run packages/plugin-gantt/— 66 files, 514 tests passed.pnpm --filter @object-ui/plugin-gantt type-check— clean. The new pin is proven inside the typechecked set rather than assumed into it: its first run failed withTS7006on line 269 of the new file, sotsc -p tsconfig.test.jsondemonstrably reads it.pnpm --filter '@object-ui/plugin-gantt^...' build— green (13 packages).plugin-ganttsubtree: 94 files counted from--format json, 0 errors on every file this PR touches. The narrowing is a measurement, not a skip: the linted population is read fromeslint.config.js's own top-levelignores, which excludes only build output (dist,.next,node_modules,public,.sourceand three named artefacts) and not one source file; and the config sets noparserOptions.projectand noprojectService, so linting is not type-aware and this diff cannot move the verdict on any file it did not touch. The 2 errors the run does report are inpackages/plugin-gantt/demo/main.tsx, which this PR does not touch — pre-existing.check:readme-exports— the gate itself printed "the population COLLAPSED, this run proves nothing" because most packages have nodistin this worktree; after buildingplugin-ganttits own README is judged and reports zero complaints.check:sdui-registration-pins— exit 2, its own text says a run with no console build "has measured nothing". Both need a full-repo build, which is CI's run.Acceptance notes
ObjectCalendarandObjectMapcarry the same inline short-circuit, hand-copied. Fenced out of this PR by the brief and filed rather than fixed. The dedup behind it was controlled: a repo-scoped REST listing overstate=all, 400 rows, grepped locally, with objectui#8769 itself as the positive control proving the enumeration reached this neighbourhood.packages/plugin-gantt/demo/main.tsxcarries 2 eslint errors onmain(react-hooks/purity,no-console). Whoever runs this package's own lint task hits them; not filed because the repo's actual wired lint invocation cannot be confirmed from here, so "this is red for everyone" would be a guess.ObjectGridandObjectTreealso branch on the inline provider, but their surrounding machinery is different and neither was measured here. objectui#9061 deliberately names only calendar and map, the two that are literal copies of this shape.🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Generated by Claude Code