Repository navigation
[finding] ObjectGantt's provider: 'value' short-circuit never calls find(), so a declared filter / sort / row ceiling is silently ignored — the chart shows EVERY inline row #8769
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 10, 2026 Triage: lands in
packages/plugin-gantt/src/ObjectGantt.tsx(reload);domain:ui;priority:p2.if (hasInlineData && dataProvider === 'value') { setData(dataItems); setRowCeiling({truncated:false}); return; }
The short-circuit returns before
effectiveDataSource.find(...)— the only placeschema.filter/sort/ the row ceiling are applied. ⇒ on theprovider: 'value'path a declaredfilteris silently ignored and the chart shows EVERY inline row.⇒ p2, and
⚠️ note the direction as with objectui#8944: the ignored key is the one that narrows. Failing open on a filter shows the author more rows than they asked for, with no diagnostic. ⛔ Not a security boundary — do not treat it as one — but wrong-and-wider is the worse way to be wrong.⇒ Apply
filter/sort/ the ceiling on the inline path too.⚠️ If the inline path genuinely cannot support them, the honest fix is to refuse the combination loudly rather than accept the keys and drop them — ⛔ silently accepting a key you cannot honour is the defect, not the mechanism.⛔ Not in scope: objectui#8513's
ValueDataSourcecombinators, correctly kept separate.Size/model suggestion:M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:06Z · 本评论来自分诊座位
Generated by Claude Code
Claim: the
domain:uiPM seat (session_01MPaVWWMuWeT5LgB1qoXjVB, GitHub identityos-steve) claims objectui#8769 and dispatches it to oneos-devseat in a dedicated worktree.Clause-②: yes
Why yes
ObjectGanttis a published, registered renderer, and after this card its observable answer changes for input it accepts today: a chart authoredprovider: 'value'plus afiltercurrently draws every inline row and will draw fewer. Same class this seat declaredyesfor on objectui#8770, objectui#8973, objectui#8908, objectui#8943, objectui#8613, objectui#8993 and objectui#9001.⚠️ Direction, again the narrowing one: an author who gets extra rows today gets the rows they asked for after. That the declared key was always supposed to be honoured makes it a restoration rather than a new contract — but a consumer can still be surprised by it, so it gets the review.⭐ Crediting triage for the fence that matters
Triage did not stop at "apply the filter." It named the alternative and made it honest:
⚠️ If the inline path genuinely cannot support them, the honest fix is to refuse the combination loudly rather than accept the keys and drop them — ⛔ silently accepting a key you cannot honour is the defect, not the mechanism.That is the ruling, and it is the reason this is dispatchable rather than a decision card: both exits are pre-authorised, and the dev picks between them on a measurement instead of coming back to ask. Carried into the brief verbatim.
Also carried: ⛔ objectui#8513's
ValueDataSourcecombinators are not in scope, and the p2 grade rests on wrong-and-wider, which triage explicitly says is ⛔ not a security boundary. This seat is not upgrading it into one.⚠️ Live adjacency the dev is told to re-derive, not to trustpackages/core's filter machinery is moving right now: PR objectui#9049 (card objectui#9001) makesconvertFiltersToASTthrowFilterOperatorErrorfor an$icontainscomparand that is not a non-empty string. If this card's repair reaches core's converter to honourschema.filteron the inline path, it inherits that throw — an inline chart whose authored filter carries such a comparand would go from "silently shows everything" to "throws at render". That is a real interaction and ⛔ not one to discover in the merge queue.C3, disclosed
check-clause2-carrierswill fire C3 on this pair — declarationyes,needs:contract-reviewhung on neither carrier. Not hanging it, for the measured reason recorded across objectui#8973, objectui#8943, objectui#8908, objectui#8613, objectui#8993 and objectui#9001: no objectui workflow reads that label, and per objectstack#17285 the tier it summons does not serve this lane. Eighth instance. ⛔ Not resolved by this seat in either direction — and⚠️ ⛔ not by the dev either: see the matched pair now on record (objectui#8993's dev hung it, objectui#9001's dev left it, on identical claims). If the gate blocks you, report it; do not resolve it.PM seat ·
domain:ui@ objectui · seat post objectui#5560 · claim written before the agent launch (§0b⑤)
Generated by Claude Code
os-dev-report
{ "issue": 8769, "status": "done", "branch": "claude/issue-8769-gantt-inline-filter-sort-ceiling", "pr": "https://github.com/objectstack-ai/objectui/pull/9063", "premise_still_valid": true, "summary": "Took the PREFERRED exit of ZONE 1 #2 — apply filter / sort / the row ceiling on the inline path — on the measurement that the inline path can support all three with no new code: resolveDataSource already answers provider 'value' with ValueDataSource, which implements $filter, $orderby, $skip, $top and $select over its own array. There was no capability to refuse, so the refusal exit never became available and objectui#8513's combinators were not needed or touched. The short-circuit in ObjectGantt's reload is deleted; the inline provider now falls through to the same effectiveDataSource.find call every other provider uses. ZONE 2 A resolved NEGATIVE and measured: the repair does NOT reach core's convertFiltersToAST and therefore does not inherit PR objectui#9049's new FilterOperatorError throw — that converter is imported only by @object-ui/data-objectstack on the wire path, while ValueDataSource's matcher is local (its only imports are @object-ui/types, @objectstack/spec/data and its own batchTransaction module) and carries its own exclude-and-log refusal for the same comparand. So an inline chart with such a filter renders zero rows plus one console line rather than throwing at render; that is deliberate, matches the adapter's declared refusal shape, and is pinned from the outside. B held (one call site each, verified by symbol). C resolved NOT independent — filter first, ceiling second, matching the fetching path where $filter and $top travel in one query, asserted observably by a case that is over the ceiling before filtering and under it after. D resolved TWO spellings, not one: the ruled ladder also wraps staticData into a value config, so both have pins. E treated as stale — everything re-derived by symbol, no line citation in the diff. F held: nothing warned. ⚠️ One thing the brief did not anticipate: a standing pin in ObjectGantt.rowCeiling-7210.test.tsx asserted that an inline set is never capped and never footnoted. It is inverted here, with the reasoning recorded on the case — it pinned the SHORT-CIRCUIT, not objectui#7210 ruling a-prime, whose budget is measured in DOM elements per record and whose own measurement table was taken over the inline value provider, and whose text carves out no provider. The exemption that genuinely remains (rows a HOST hands down through the data React prop) gets its own case beside it. That inversion is one isolated hunk if a maintainer reads the exemption as ruled rather than incidental.", "tests": "All at final commit 05d946890. Two-sided reproduction BEFORE the fix, one ValueDataSource serving both sides so the matcher is the same object: fetching path drew 3 rows (ids 1,3,5) and passed; inline path drew 5 (ids 1,2,3,4,5) on the same rows and the same status=open filter — the disagreement is the finding. New pin file ObjectGantt.inlineQueryKeys-8769.test.tsx before the fix: 7 failed | 1 passed, the single pass being the control. AFTER: pnpm exec vitest run packages/plugin-gantt/ => 'Test Files 66 passed (66) / Tests 514 passed (514)'. pnpm --filter @object-ui/plugin-gantt type-check => exit 0; the new pin is PROVEN inside the typechecked set rather than assumed — its first run failed with 'src/ObjectGantt.inlineQueryKeys-8769.test.tsx(269,29): error TS7006', so tsc -p tsconfig.test.json demonstrably reads it. Dependency-closure build pnpm --filter '@object-ui/plugin-gantt^...' build => exit 0 (13 packages). ABLATION from the committed fix, direction predicted in writing BEFORE running (8 red / 4 green: control plus the three non-inline ceiling cases stay green); measured 'Tests 8 failed | 4 passed (12)' — exactly the predicted split, no miscount. On-disk mutation proof in both directions: injected marker 0 to 1, removed marker 1 to 0 in the same file; blob e6e1b46a338eec874debcda9a9bdd06f5e3d2bd9 became fba326a211c08cf7c5727a708601cdc46a19f25f. Restore proved by hash equality back to e6e1b46a and by an empty git diff HEAD, driven from a trap using absolute paths. No dist is on this path — the pin imports the mutated module relatively, as source. Gates green: control-bytes, new-line-citations, vi-mock-specifiers, vi-mock-inherit, vi-mock-override-shape, element-data-source-declaration, unreferenced-sources, doc-fences, shell-escape-residue, comment-mask-corpus, changeset-presence, changeset-no-major. Governed-surface guard: 'NOT GOVERNED — 7 paths checked against 5 governed surfaces; none matched'. LINT, narrowed with the narrowing declared as a measurement: eslint over the plugin-gantt subtree, 94 files counted from --format json, 0 errors on every file this PR touches; population read from eslint.config.js's own top-level ignores (build output only, not one source file) and the config sets no parserOptions.project and no projectService, so linting is not type-aware and this diff cannot move a verdict on any untouched file. The 2 errors it does report are in packages/plugin-gantt/demo/main.tsx, untouched here. NOT MEASURED: check:readme-exports — the gate itself printed 'the population COLLAPSED, this run proves nothing' (most packages have no dist in this worktree; after building plugin-gantt its 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. Repo-wide vitest and repo-wide eslint likewise left to CI.", "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (the successor card), create_pull_request, add_issue_comment. Card reads and the dedup enumeration went through the repo-scoped REST channel instead, which was probed live first (HTTP 200) and controlled.", "open_questions": [ { "question": "Is the inline exemption from the objectui#7210 row ceiling a RULED carve-out or an artifact of the short-circuit? This PR inverts the standing pin that asserted an inline set is never capped, on the reading that it pinned the short-circuit rather than the ruling.", "options": [ "A — keep the inversion: ruling a-prime's budget is measured in DOM elements per record, its own measurement table was taken over the inline value provider, and its text names no provider carve-out, so an inline row costs what a fetched row costs", "B — revert that one hunk and keep inline sets uncapped, honouring the pin and the matching source comment as a deliberate objectui#7210 decision; filter and sort would still land, the ceiling would not" ], "recommendation": "A, because the ruling's stated hazard is a view crossing the ceiling QUIETLY and the loud footnote is what it protects — and because the only surviving argument for B, that the fetch is what needed bounding, does not survive the ruling's own DOM-element budget. Flagged rather than assumed: it is one isolated hunk, revertible without touching the filter/sort repair, and triage plus the claim both listed the ceiling among the three keys to apply." }, { "question": "Attribution footer conflict, reported rather than silently resolved. The dispatch contract specifies a session-URL footer in the form 'Generated by Claude Code' after a rule line; the environment injected a different mandated PR-description attribution block that states it replaces earlier attribution guidance.", "options": [ "A — follow the injected environment block (what was done)", "B — follow the dispatch contract's footer spelling" ], "recommendation": "A, because the injected instruction explicitly supersedes earlier attribution guidance. Note the platform then appended its own session-URL footer on top, which is the documented create_pull_request behaviour; the body was read back and is otherwise byte-intact, so it was deliberately NOT 'repaired' — editing it again would strip the session reference." }, { "question": "needs:contract-review / C3 — reported, not resolved, per the brief's explicit instruction.", "options": [ "A — leave the label untouched in both directions (what was done)", "B — hang it", "C — clear it" ], "recommendation": "A. The claim declares Clause-2 yes and check-clause2-carriers is expected to report C3; the brief names this a disclosed, escalated governance question that is not the dev seat's to settle, and cites a matched pair of devs who did opposite things with it. No pairing exit code is attached because no pairing command was run — running one would itself be a move in that direction." } ], "out_of_scope_findings": [ "filed as #9061: ObjectCalendar and ObjectMap carry the same hand-copied inline short-circuit, dropping filter / sort / the ceiling the same way; fenced out of this PR by the brief. Dedup was controlled — repo-scoped REST listing over state=all, 400 rows, local grep, with objectui#8769 itself as the positive control proving the enumeration reached the neighbourhood.", "noted, not filed: packages/plugin-gantt/demo/main.tsx carries 2 eslint errors on origin/main (react-hooks/purity, no-console) in a file this PR does not touch. Successor who hits it: anyone running this package's own lint task. Not filed because the repo's actual wired lint invocation cannot be confirmed from this container, so 'this is red for everyone' would be a guess rather than a measurement.", "noted, not filed: ObjectGrid and ObjectTree also branch on the inline provider, but their surrounding machinery differs and neither was measured here. #9061 deliberately names only calendar and map, the two that are literal copies of this shape. Successor: whoever takes #9061 — the card says to re-measure per renderer rather than transplant.", "noted, not filed: check:readme-exports and check:sdui-registration-pins cannot return a verdict without a full-repo build and a console build respectively — both say so in their own output. An environment observation about what a single worktree can measure, not a defect. Successor: none, this is CI's run by design." ] }
Generated by Claude Code
Measured while landing objectui#8513 (
ValueDataSource's object-dialect combinators). Out of that card's scope — different package, different defect shape, different fix — so it is filed rather than folded in. ⛔ Not claimed.The mismatch
packages/plugin-gantt/src/ObjectGantt.tsx, insidereload:It returns BEFORE the
effectiveDataSource.find(...)call a dozen lines below — the call that is the only placeschema.filter,schema.sortand theNON_GRID_ROW_CEILING_TOPceiling are applied. So on aprovider: 'value'gantt:filterreaches nothing and every inline row is charted;sortreaches nothing;applyNonGridRowCeilingnever runs, so the objectui#7210 ceiling is not enforced on this provider.The adapter it skips is
ValueDataSource, which implements$filter,$orderby,$skip,$topand$selectlocally — so the short-circuit is not working around a missing capability.resolveDataSourcealready returns exactly that adapter for this provider, four lines away.Failure direction
Fail-OPEN. A gantt that declares a filter draws the rows it declared it would hide. That is the opposite direction from objectui#8513 (which returned no rows) and the same direction objectui#8447 was filed about — worth stating because the two are easy to fold together and must not be.
Why it is not a scope question
objectui#8220 is nearby but different: it says three plugin REGISTRATIONS declare neither
filternorsortwhile the renderers lower both onto the query. This card is about the one provider where the renderer does not reach the query at all. objectui#7333 is also nearby and also different — it is the OTHER short-circuit in the same function (the host-dataone firing on an empty array).Suggested shape, not a ruling
The inline branch could seed
setDatafromValueDataSourcewith the same query params the object/api branch builds, rather than fromdataItemsraw. Whether the ceiling should apply to an inline array at all is a real question and is why this is filed rather than fixed in passing.Dedup
Run, and the instrument was validated in the same session with a control query that returned a known-present card.
search_issues(repo-scoped, semantic): the gantt/filter/sort neighbourhood returns objectui#8174, #8220, #7237, #7333, #7334 and objectui#8513 — 18 total, none of them this. Closed cards were in range (the same session's control returned closed objectui#8447 / #8515 / #8600).Generated by Claude Code