You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(react,fields,app-shell): render-time filter mergers throw converter refusals out of render or a click handler — ElementDataSourceGate, PeoplePicker, RecordPickerDialog, composeDrillFilter #10789
Filing-gate category: ① a product defect class with named sites, read and not measured. Reader: triage first (route and grade), then the domain:ui execution seat that claims it.
Filed by the domain:ui seat #2 (session_014mXUNuFomfj24w7s1pZzhN) from the contract review of PR objectui#10784 (objectui#9164) and that card's dev report. Both name this class with "carrier: none".
The class
convertFiltersToAST / toFilterNode REFUSE malformed filters with FilterOperatorError. There are twelve refusal shapes as of objectui#9164, including the empty operator map. Callers split three ways:
Safe:toFilterNodeSafely, where the refusal becomes a logged exclusion or a malformed-filter notice. This covers ObjectGrid schema.filter, RelatedList, LineItemsPanel and ValueDataSource.
Inside a load try or an async path, where it surfaces as a load error. This covers the ObjectGrid defaultFilters leg, ListView buildEffectiveFilter, ObjectView, LookupField recent rows, related-count-store, and the data-objectstack find / export paths.
From render or a click handler, uncaught. These are the sites below.
Sites (read on objectui origin/main; ⛔ not reproduced)
packages/react/src/element-data-source/ElementDataSourceGate.tsx:397: useElementDataSourceSchema, inside a render useMemo. The gate wraps the form, detail, related-list, calendar, charts and dashboard blocks. When a block carries an element dataSource binding, the gate's merge throws BEFORE LineItemsPanel / RelatedList reach their own toFilterNodeSafely.
composeElementDataSource (the useElementDataSourceuseMemo).
PeoplePicker recentFilter.
RecordPickerDialog mergedFilter.
composeDrillFilter (a click handler).
Each merges through mergeFilterNodes / the throwing converter form. A malformed authored filter therefore throws out of render, into the nearest error boundary, instead of drawing the malformed-filter notice its siblings draw.
Related, pre-existing: lowerLogicalGroup returns early on a TRUE disjunct, so { $or: [{}, { a: {} }] } answers TRUE rather than a refusal, depending on member order.
Direction (for triage)
Route each site through toFilterNodeSafely, or catch the refusal at the merge and surface it the way its siblings do. ⛔ Do not swallow it silently. Census every mergeFilterNodes caller first. Pin one malformed filter per site: it renders the notice and does not throw.
⇒ domain:ui. Rationale: a malformed authored filter throws a FilterOperatorError out of render, into the nearest error boundary, instead of drawing the malformed-filter notice its siblings draw through toFilterNodeSafely. It was read from source, not reproduced, with no producer named ⇒ p3. One card covers every site, with an enumerating pin.
Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T15:18Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), and objectui#9164 / PR objectui#10784 (both closed).
Execution notes.
Census first: every mergeFilterNodes / throwing-converter caller, classed as safe, inside a load try, or render/click-uncaught. Record the table.
Route each render or click site through toFilterNodeSafely, or catch at the merge and surface the notice the way its siblings do. ⛔ Never swallow the refusal silently.
Pin one malformed filter per site: it renders the notice and does not throw.
The lowerLogicalGroup early return on a TRUE disjunct ({ $or: [{}, { a: {} }] } → TRUE, depending on member order) is the same family. Include it, and pin both member orders.
Claim: PM loop round 1
Session: session_01DuWo5bdP9SdVebamn99GGk
Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-10789-render-time-filter-mergers
Worktree: objectui-issue-10789
Domain: domain:ui
Seat: domain:ui#1
File surface: the render-time and click-time filter mergers the triage names — packages/react/src/element-data-source/ElementDataSourceGate.tsx (useElementDataSourceSchema), packages/core/src/data-scope/element-data-source.ts (composeElementDataSource), packages/fields/src/widgets/PeoplePicker.tsx (recentFilter), packages/fields/src/widgets/RecordPickerDialog.tsx (mergedFilter only), packages/core/src/utils/drill-down.ts (composeDrillFilter) — plus any further render/click-uncaught site the census finds, tests beside them, and patch changesets for the touched packages (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: opus (TIER_DEFAULT, objectstack scripts/pm/dispatch-gates.mjs:12534 at 0d3ec47137)
Clause-②: no
Thread-read: 5857118114
Serial constraints cleared: read 2026-09-27T16:58Z at objectui origin/main. None of the 14 open PRs touches these files. One live claim shares a file: this seat's objectui#10594 edits RecordPickerDialog.tsx's withoutDeniedFields (:451); this card edits mergedFilter (:693) ⇒ region-disjoint; merge main if it lands first.
Why Clause-②: no: a malformed filter stays refused — it is surfaced as the malformed-filter notice its siblings already draw through toFilterNodeSafely instead of throwing out of render. No filter shape becomes accepted and no public face grows.
os-dev-report
{
"issue": 10789,
"status": "done",
"branch": "claude/issue-10789-render-time-filter-mergers",
"pr": "#10830",
"session": "session_01DuWo5bdP9SdVebamn99GGk — the dispatching seat's session (mode:subagent; this run's commits carry the same Claude-Session line)",
"premise_still_valid": true,
"summary": "Premise measured, not assumed: on base a14fb23 all 18 site pins go red with the FilterOperatorError each site threw. Census (table in the PR body) found 9 render/click-uncaught sites, 4 more than the brief listed: ObjectPivotTable, DatasetWidget and DatasetReportRenderer (composeDrillFilter callers besides ObjectChart), plus the ListView server export click. R1/R3/R4 lower each source through toFilterNodeSafely, then merge. R2 and R5-R9 catch only FilterOperatorError around the core utility, which keeps throwing, so there is no new export. The notice matches each nearest sibling: the gate draws the blocks' malformed-filter notice (view.malformedFilter); record_picker shows its missing panel; the pickers use their error slot; the drills log via console.warn and open or emit nothing, never a filter-less drill; the export names the refusal in its menu. lowerLogicalGroup now reads every $or member before a TRUE disjunct absorbs, so both member orders refuse. origin/main ff94a12 is merged, including objectui#10594: one import-line conflict in ObjectPivotTable, resolved as the union.",
"tests": "8 new pin files, one malformed filter per site plus a CONTROL each; the assertions check the notice content (the refused operator) or the envelope (code INVALID_FILTER, httpStatus 400) plus the absence of an unfiltered read, never a bare toThrow. RED LEG: fix committed at 9f1d071; the 10 source files were checked out at BASE a14fb23, with an on-disk grep -c of objectui#10789 = 0 in each. Result: 'Test Files 8 failed (8) / Tests 18 failed | 12 passed (30)', and every red is the site's FilterOperatorError or an absent notice. Restored with git checkout HEAD --, proven by 'RESTORED: git diff HEAD empty, 10 blobs match HEAD' (a trap covers EXIT INT TERM); the subjects resolve through the vitest src alias, so no dist was involved. GREEN at 664305c: core+react+all pins 'Test Files 285 passed (285) / Tests 4931 passed (4931)'. fields+plugin-charts+plugin-report 'Test Files 321 passed | 1 skipped (322) / Tests 4722 passed | 7 skipped (4729)'. plugin-list+plugin-dashboard+gate-consumer sweep (13 elementDataSource suites across 10 packages, the console injection probe, 3 record_picker suites) 'Test Files 258 passed (258) / Tests 2565 passed (2565)'. turbo type-check over the 8 packages 'Tasks: 22 successful, 22 total', all 8 a cache miss. An earlier type-check at cae4c5e caught TS7006 in a pin (an untyped warn-call mapper); f2bc82a fixed it.",
"gates": [
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ + 6 pin files' @664305cb3",
"verdict": "VERDICT batch-last-exit 0 (last part = vitest); Test Files 285 passed (285)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/fields/ packages/plugin-charts/ packages/plugin-report/' @664305cb3",
"verdict": "VERDICT command-exit 0; Test Files 321 passed | 1 skipped (322)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ packages/plugin-dashboard/ + 17 consumer suites' @664305cb3",
"verdict": "VERDICT command-exit 0; Test Files 258 passed (258)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec turbo run type-check --concurrency=2 --continue --filter=(core,react,fields,components,plugin-charts,plugin-dashboard,plugin-report,plugin-list)' @664305cb3",
"verdict": "VERDICT command-exit 0; Tasks: 22 successful, 22 total",
"exit": 0
},
{
"command": "red leg: redleg.sh (BASE sources + new pins) under os-verify-lock",
"verdict": "VITEST-EXIT=1, Tests 18 failed | 12 passed (30); RESTORED: git diff HEAD empty, 10 blobs match HEAD; VERDICT command-exit 0",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs",
"verdict": "18 source file(s) of 8 released package(s) changed, and this change declares 1 changeset(s)",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"verdict": "No changeset declares a major bump",
"exit": 0
},
{
"command": "pnpm check:element-data-source-declaration",
"verdict": "OK — 13 gate-consuming file(s) checked",
"exit": 0
},
{
"command": "pnpm check:i18n-keys",
"verdict": "Every in-scope call-site key resolves against the en pack",
"exit": 0
},
{
"command": "pnpm check:new-line-citations",
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)",
"exit": 0
},
{
"command": "pnpm check:phantom-deps / check:self-import / check:esm-specifiers (specifier leg) / check:test-path-roots",
"verdict": "each OK",
"exit": 0
},
{
"command": "node scripts/check-control-bytes.mjs / check-vi-mock-specifiers.mjs / check-vi-mock-inherit.mjs / check-vi-mock-override-shape.mjs",
"verdict": "each OK",
"exit": 0
},
{
"command": "pnpm exec eslint --format json (the 18 changed files, git diff --name-only HEAD^2 HEAD)",
"verdict": "declared narrowing: 18 files, 0 errors; per-rule warning delta vs base versions = 0 on every source file; config has no parserOptions/projectService (no type-aware lint) so untouched files cannot move",
"exit": 0
},
{
"command": "repo-wide pnpm test, pnpm lint, check:esm-specifiers load leg, remaining check:* families",
"verdict": "NOT MEASURED locally: CI-owned",
"exit": null
}
],
"line_budget": "n/a — no skills/** or ratcheted ledger touched",
"files_changed": [
".changeset/10789-render-time-filter-refusals.md",
"packages/core/src/utils/filter-converter.ts",
"packages/core/src/utils/tests/filter-or-absorb-order-10789.test.ts",
"packages/react/src/hooks/useElementDataSource.ts",
"packages/react/src/element-data-source/ElementDataSourceGate.tsx",
"packages/react/src/element-data-source/tests/ElementDataSourceGate.malformedFilter-10789.test.tsx",
"packages/components/src/tests/record-picker-malformed-filter-10789.test.tsx",
"packages/fields/src/widgets/PeoplePicker.tsx",
"packages/fields/src/widgets/RecordPickerDialog.tsx",
"packages/fields/src/widgets/pickerMalformedFilter-10789.test.tsx",
"packages/plugin-charts/src/ObjectChart.tsx",
"packages/plugin-charts/src/tests/ObjectChart.drillRefusal-10789.test.tsx",
"packages/plugin-dashboard/src/ObjectPivotTable.tsx",
"packages/plugin-dashboard/src/DatasetWidget.tsx",
"packages/plugin-dashboard/src/tests/drillRefusal-10789.test.tsx",
"packages/plugin-report/src/DatasetReportRenderer.tsx",
"packages/plugin-report/src/tests/DatasetReportRenderer.drillRefusal-10789.test.tsx",
"packages/plugin-list/src/ListView.tsx",
"packages/plugin-list/src/tests/ListView.exportRefusal-10789.test.tsx"
],
"deviations": [
"File surface grew beyond the claim's five named sites, under its 'plus any further render/click-uncaught site the census finds' clause: plugin-charts, plugin-dashboard (ObjectPivotTable, DatasetWidget), plugin-report (DatasetReportRenderer) and plugin-list (ListView export), plus one components test for element:record_picker (no components source changed, so no components changeset).",
"Brief zone 2 item 1 falsified in part: composeDrillFilter is not a click handler. It is a core utility with five callers: two recompose during render after a click (ObjectChart, ObjectPivotTable) and three are click handlers (DatasetWidget openDrill, DatasetReportRenderer drill and drillCell). DrillDownDrawer's mergedFilter makes no converter call (a literal $and): classified, untouched.",
"Brief zone 2 item 2 decided at the callers, not the utilities: composeElementDataSource and composeDrillFilter keep throwing because their async callers (ViewDataProvider) and contracts are unchanged, and a caller-side FilterOperatorError-only catch needs no new core export.",
"Brief zone 2 item 4: the drill surfaces have no rendered malformed-filter state, so none was invented. They log via console.warn (ReportView's drill failure and ValueDataSource's refused filter use the same channel), and the drill does not open. See open_questions.",
"Public face: the UseElementDataSourceResult and UseElementDataSourceSchemaResult interfaces (@object-ui/react) gain an optional filterRefusal field, and the 'missing' status docs now include a refused filter. The claim's Clause-② reason says 'no public face grows'. The PR body carries the claim's 'Clause-②: no' verbatim; see open_questions.",
"The changeset declares patch for all seven packages, as the brief asked; the react interfaces gain the additive optional field noted above.",
"Merged origin/main ff94a12 before opening the PR, as the brief asked (objectui#10594 had landed). One conflict (ObjectPivotTable import lines) was resolved as the union; RecordPickerDialog.tsx merged without conflict.",
"PeoplePicker's recentFilter threw on EVERY render with a malformed declared filter, not only when recents exist: the useMemo merged even with an empty id list."
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay writes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write). pr_create → POST /repos/objectstack-ai/objectui/pulls (draft #10830). label-write assign → POST /repos//issues/10830/assignees (os-steve, read back). os-dev-report comment → POST /repos//issues/10789/comments. git push to the branch is not counted (not REST). Card assignee and labels: 0 writes.",
"open_questions": [
{
"question": "Drill sites (ObjectChart, ObjectPivotTable, DatasetWidget, DatasetReportRenderer) now refuse with a console.warn and open or emit no drill. Should a drill refusal also be drawn on screen?",
"options": [
"A — keep the logged refusal (this PR): no new UI, and nothing opens without the filter",
"B — add a rendered malformed-filter state to the drill drawers (DrillDownDrawer and ObjectChart's drawer) and a host-visible signal for DatasetReportRenderer's onDrill: new UI and a new onDrill contract"
],
"recommendation": "A. Real need: reaching it takes a widget filter this layer refuses that the widget's own read still accepted (aggregate route, inline or bound rows), with no named producer. Long-term: B widens the onDrill contract before any demand. AI-error axis: both refuse loudly and neither widens. Startup focus: no new surface without pull."
},
{
"question": "Does the new optional filterRefusal field on the two exported @object-ui/react hook-result interfaces flip the claim's 'Clause-②: no'?",
"options": [
"A — stays no: an output-only optional field on a hook result, no new export, no new authorable key, and the accept set only narrows (lowerLogicalGroup)",
"B — yes: any exported TypeScript interface growth counts as public face; flip the line and raise the changeset to minor"
],
"recommendation": "A, but the seat owns the reading; the PR body carries 'Clause-②: no' verbatim from the claim."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — LookupField's recents effect catches every error, a refusal included, into an empty rail. Read on main; the main popover query still surfaces the refusal. Class 2 (async), outside this card. dedupe: LookupField recents catch swallow, recentRows empty refusal",
"carrier: 承接者:无 · noted, not filed — composeDrillFilter's final parseFilterAST can raise the spec's own INVALID_FILTER, not a FilterOperatorError, for a malformed AST widget filter, and these sites do not catch it. Read only, not measured through a public door. dedupe: composeDrillFilter parseFilterAST INVALID_FILTER, drill AST widget filter",
"carrier: 承接者:无 · noted, not filed — DrillDownDrawer's report drill composes report.filter with the drill filter as a literal $and rather than through mergeFilterNodes. Read only, no reach measured. dedupe: DrillDownDrawer report drill mergedFilter $and, report drill filter compose"
]
}
Claim amended: Clause-② flips to yes, and the drill question is decided · domain:ui seat #1 · 2026-09-27T18:37Z
session_01DuWo5bdP9SdVebamn99GGk (os-steve). The dev report on this card (PR objectui#10830, head 664305cb3) raised two open questions. The seat re-read the one that bears on the claim against the PR head itself.
Clause-②: yes
Why the claim's no was false. The claim rested on 「no public face grows」. At the head:
UseElementDataSourceResult (packages/react/src/hooks/useElementDataSource.ts:59) and UseElementDataSourceSchemaResult (packages/react/src/element-data-source/ElementDataSourceGate.tsx:168) each gain an optional filterRefusal.
Both are reachable from @object-ui/react's only exports entry .: src/index.ts re-exports ./hooks/index.js and ./element-data-source/ElementDataSourceGate.js with export *, and hooks/index.ts re-exports ./useElementDataSource.js the same way.
⇒ A published type gains a member, which is the public-face limb of the criterion. The acceptance set itself only narrows: lowerLogicalGroup now reads every $or member before a TRUE disjunct absorbs it, and no refused filter becomes accepted. The seat's claim was wrong, and this line replaces it. A contract review is owed on the PR regardless (the diff also carries a changeset). The review judges whether the declared patch level fits an additive member on a published interface.
Dev question 1 (should a drill refusal also be drawn on screen?) — decided by this seat: A, ⛔ not escalated. The four drill sites (ObjectChart, ObjectPivotTable, DatasetWidget, DatasetReportRenderer) keep a logged refusal: nothing opens without the filter. This is an execution parameter inside the claim, since both options refuse loudly and neither accepts a refused filter. B would add new drawer UI and widen onDrill's host contract with no named producer, which is outside this card's surface. The decision can be overturned by a maintainer ruling; if one asks for B, it is a new card.
Dev question 2 is answered by the line above.
The PR body's second line is rewritten to match, with a seat note, since the dev writes a body once.
os-dev-report
{
"issue": 10789,
"status": "done",
"branch": "claude/issue-10789-render-time-filter-mergers",
"pr": "#10830",
"head": "6f4bee846",
"round": "patch round 1 — CI shard 2/8 (check-i18n-dead-keys) + contract review 5858758764 (VERDICT FAIL, two defects)",
"session": "session_01DuWo5bdP9SdVebamn99GGk — the dispatching seat's session (mode:subagent; commits carry the same Claude-Session line)",
"premise_still_valid": true,
"summary": "Both review defects fixed in one round, at head 6f4bee8 (previous head 664305c). DEFECT 1 took option (b): the gate's malformed-filter panel no longer imports the en pack. Its provider-less default is a literal inline defaultValue, the way the siblings carry the sentence: they keep a literal copy in a module-scope createSafeTranslation table, which @object-ui/react cannot have because its entry re-exports this module. check:i18n-keys holds the literal byte-identical to the pack, and an ablation proves it: one changed character gives EXIT=1 with [default-value-drift] view.malformedFilter. The file left the pack-object importer population (dead-keys CLI: 0 UNANALYSED), so no bullet or ledger entry was needed. The gate pin now asserts the sentence text, with the operator filled and no raw key, both without and with an I18nProvider. DEFECT 2 closed the seam rather than narrowing the sentence. composeDrillFilter now re-raises the spec parseFilterAST refusal (plain Error, code INVALID_FILTER, status 400) as a FilterOperatorError with the message verbatim; any other error passes through untouched. All four drill sites (ObjectChart, ObjectPivotTable, DatasetWidget, DatasetReportRenderer) therefore handle it through the catch they already had: nothing opens, no onDrill, never a filter-less drill, and a console.warn names the operator. No site code changed and @object-ui/core gains no export. The changeset's opening sentence was rewritten to be true at every site and the drill bullet now names both refusal steps.",
"tests": "NEW PINS: core drill-filter-spec-refusal-10789 checks that array and object dialects, a scalar on $in, come out as a FilterOperatorError (code INVALID_FILTER, httpStatus 400, message naming $in); that a non-INVALID_FILTER error passes through by identity (vi.resetModules + doMock, undone in finally, because the unit project is isolate:false); plus a CONTROL. A spec-refusal case was added to each drill site pin: ObjectChart (the reviewer's inline-data [['stage','in','won']] probe, drawer + navigate), ObjectPivotTable (drawer + navigate), DatasetWidget, and DatasetReportRenderer (row + matrix). RED LEG: drill-down.ts checked out at the previous head 664305c, on-disk marker count 0, pins kept. Result 'Test Files 4 failed (4) / Tests 9 failed | 13 passed (22)': the 9 reds are exactly the 9 spec-refusal assertions, and the 13 greens are the converter cases, CONTROLs and the passthrough guard. Restored, 'RESTORED: git diff HEAD empty, blob matches HEAD', with a trap on EXIT INT TERM. GREEN at 6f4bee8: core+react+ALL scripts/tests 'Test Files 457 passed | 2 skipped (459) / Tests 10231 passed | 2 skipped (10233)'. Explicitly, 'Test Files 9 passed (9) / Tests 467 passed (467)' for check-i18n-dead-keys (134 tests, including '✓ … EVERY non-test importer in the live population is analysed in the header'), check-i18n-call-site-keys, check-i18n-en-drift, check-i18n-designer-table-parity, check-hand-rolled-comment-mask, check-comment-mask-corpus, check-new-cross-file-line-citations, check-unreferenced-sources and the console element-data-source-input-injection probe. plugin-charts+plugin-dashboard+plugin-report 'Test Files 244 passed (244) / Tests 2535 passed (2535)'. type-check core/react/plugin-charts/plugin-dashboard/plugin-report 'Tasks: 18 successful, 18 total', all 5 a cache miss. Order-independence of the passthrough pin: it ran green in one worker (--maxWorkers=1) beside two drill-importing core files, 'Test Files 4 passed (4)'. A first full core run at c5cee60 had failed it under isolate:false (the module-scope vi.mock did not apply); 6f4bee8 fixed that.",
"gates": [
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ scripts/tests/' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 457 passed | 2 skipped (459)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --reporter=verbose scripts/tests/check-i18n-dead-keys.test.ts + check-i18n-call-site-keys + check-i18n-en-drift + check-i18n-designer-table-parity + check-hand-rolled-comment-mask + check-comment-mask-corpus + check-new-cross-file-line-citations + check-unreferenced-sources + apps/console element-data-source-input-injection' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 9 passed (9), Tests 467 passed (467)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/plugin-charts/ packages/plugin-dashboard/ packages/plugin-report/' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 244 passed (244)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec turbo run type-check --concurrency=2 --continue --filter=(core,react,plugin-charts,plugin-dashboard,plugin-report)' @6f4bee846",
"verdict": "VERDICT command-exit 0; Tasks: 18 successful, 18 total",
"exit": 0
},
{
"command": "red leg: redleg2.sh (drill-down.ts at 664305c + new pins) under os-verify-lock",
"verdict": "VITEST-EXIT=1, Tests 9 failed | 13 passed (22); RESTORED: git diff HEAD empty, blob matches HEAD; VERDICT command-exit 0",
"exit": 0
},
{
"command": "ablation: literal defaultValue mutated one char, pnpm check:i18n-keys",
"verdict": "MUTATED EXIT=1 [default-value-drift] view.malformedFilter; RESTORED git diff HEAD empty; RESTORED check:i18n-keys EXIT=0",
"exit": 0
},
{
"command": "pnpm check:i18n-keys @6f4bee846",
"verdict": "every literal inline defaultValue matches the value the pack serves",
"exit": 0
},
{
"command": "pnpm check:i18n-dead-keys @6f4bee846",
"verdict": "report; 0 UNANALYSED importers, ElementDataSourceGate.tsx absent from the importer population",
"exit": 0
},
{
"command": "pnpm check:element-data-source-declaration @6f4bee846",
"verdict": "OK — 13 gate-consuming file(s) checked",
"exit": 0
},
{
"command": "node scripts/check-hand-rolled-comment-mask.mjs @6f4bee846",
"verdict": "OK every carrier is a DEBT entry, and every DEBT entry still carries one",
"exit": 0
},
{
"command": "pnpm check:new-line-citations @6f4bee846",
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs @6f4bee846",
"verdict": "20 source file(s) of 8 released package(s) changed, 1 changeset declared / No changeset declares a major bump",
"exit": 0
},
{
"command": "pnpm check:phantom-deps / check:self-import; node scripts/check-control-bytes.mjs / check-vi-mock-specifiers.mjs / check-vi-mock-inherit.mjs / check-vi-mock-override-shape.mjs",
"verdict": "each OK",
"exit": 0
},
{
"command": "pnpm exec eslint --format json (this round's 7 changed ts/tsx files)",
"verdict": "0 errors; drill-down.ts and ElementDataSourceGate.tsx per-rule warnings identical to 664305c",
"exit": 0
},
{
"command": "fields, plugin-list suites and their type-check this round",
"verdict": "NOT RE-RUN: this round changed no file in either package; last measured green at 664305c",
"exit": null
}
],
"line_budget": "n/a — no skills/** or ratcheted ledger touched",
"files_changed": [
".changeset/10789-render-time-filter-refusals.md",
"packages/core/src/utils/drill-down.ts",
"packages/core/src/utils/tests/drill-filter-spec-refusal-10789.test.ts",
"packages/react/src/element-data-source/ElementDataSourceGate.tsx",
"packages/react/src/element-data-source/tests/ElementDataSourceGate.malformedFilter-10789.test.tsx",
"packages/plugin-charts/src/tests/ObjectChart.drillRefusal-10789.test.tsx",
"packages/plugin-dashboard/src/tests/drillRefusal-10789.test.tsx",
"packages/plugin-report/src/tests/DatasetReportRenderer.drillRefusal-10789.test.tsx"
],
"deviations": [
"Defect 2 is closed at the SEAM, not by a four-site predicate. The seat asked for one small predicate over four copies. Instead, composeDrillFilter re-raises the spec INVALID_FILTER as a FilterOperatorError: it is the only caller of parseFilterAST on the drill path, all four sites already catch that type, and so no site code changed and @object-ui/core gains no export. The behaviour is the one the seat specified: only code === 'INVALID_FILTER' is converted, everything else passes through, and a pin checks it by identity.",
"Defect 1 went option (a) first, then (b). I committed the classification bullet plus the ANALYSED_PACK_OBJECT_IMPORTERS entry (cab1c7d) before the review arrived. A local hard reset to drop it was denied by the permission classifier, so I reverted it forward (3dbcaba); both commits sit on the pushed branch and the net diff in scripts/ is empty. Option (b) then removed the pack import, so the file is outside the population and any entry for it would fail the gate's reverse check, 'EVERY analysed importer still exists'.",
"The first full core run at c5cee60 failed one new pin, the passthrough guard: its module-scope vi.mock did not apply under the unit project's isolate:false. 6f4bee8 rewrote it with vi.resetModules + doMock + dynamic import, undone in finally, and it is green in a shared worker and in the full run.",
"PR body needs edits I may not make (the dev writes the body once, no PATCH), for the seat: (1) Evidence: the head is 6f4bee8 and red leg 2 is 9 failed | 13 passed. (2) Census rows R5–R8: composeDrillFilter now re-raises the spec INVALID_FILTER as a FilterOperatorError, so these sites also catch the parseFilterAST refusal. (3) Design-choice notice bullet for the gate: the default is now a literal inline defaultValue policed by check:i18n-keys, not a pack read. (4) Acceptance notes 'Observed, not in scope': drop the parseFilterAST sentence, which is now fixed. (5) The review cites a claim amendment with 'Clause-②: yes', while the body's second line still reads 'Clause-②: no'; reconcile it.",
"The fields and plugin-list packages are unchanged this round, so their suites and type-check were not re-run; the last green reading is at 664305c."
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "This round: 1 relay write as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches), which becomes the os-dev-report comment POST /repos//issues/10789/comments. Run total: 4 (pr_create #10830, PR assignee os-steve, the first os-dev-report comment, this one). git push is not REST. Reads: REST GET for review 5858758764 and the comment read-back.",
"open_questions": [
{
"question": "Drill sites refuse with console.warn and open nothing: should a drill refusal also be drawn on screen? (Carried over; the review flags it and calls it not a defect.)",
"options": [
"A — keep the logged refusal: no new UI and no new onDrill contract",
"B — add a rendered malformed-filter state to the drill drawers and a host-visible signal on onDrill"
],
"recommendation": "A. The four axes as in the first report: no named producer, B widens a host contract before any demand, both refuse loudly, and nothing new without pull."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — LookupField's recents effect catches every error, a refusal included, into an empty rail; the main popover query still surfaces the refusal. Class 2 (async). dedupe: LookupField recents catch swallow, recentRows empty refusal",
"carrier: 承接者:无 · noted, not filed — DrillDownDrawer's report drill composes report.filter with the drill filter as a literal $and rather than through mergeFilterNodes. Read only, no reach measured. dedupe: DrillDownDrawer report drill mergedFilter $and, report drill filter compose"
]
}
session_01DuWo5bdP9SdVebamn99GGk (os-steve), the dispatching seat and reviewer of record. Checked against GitHub and origin/main, ⛔ not against the reports.
Checklist
Shape. Draft → main. The body opens Fixes #10789, the only closing keyword, and Clause-②: yes is at line start (the claim amendment 5858635767). PR assignee os-steve.
Scope. 21 files, +1,497 / −79. Nine render- or click-time merge sites, R1–R9 in the PR's census table:
the element data-source gate and binding hook;
PeoplePicker and RecordPickerDialog;
the four drill sites;
the ListView server export.
Around them: lowerLogicalGroup's $or absorption order in packages/core, nine pin files, and one changeset, patch for seven packages.
Surface growth under the claim's census clause, accepted. plugin-charts, plugin-dashboard, plugin-report and plugin-list, each a genuine member of the class. @object-ui/react gains the optional filterRefusal on two published hook-result interfaces, and @object-ui/core gains no export.
Checks on head 6f4bee846. 43 check-runs: 40 success, 3 skipped, 0 failure.
Round 1, head 664305cb3: contract review 5858758764 FAILED with two defects:
The gate's new en pack import failed check-i18n-dead-keys (shard 2/8 red).
The changeset's opening sentence was false. An authored array-dialect drill filter was refused by the spec parseFilterAST as INVALID_FILTER, not a FilterOperatorError, so it still threw into the error boundary at the four drill sites.
Patch round 1 (dev report 5859112009) fixed both:
The en import is gone. The default is now a literal held byte-identical to the pack by check:i18n-keys, and the one-character ablation turns that check red.
composeDrillFilter re-raises only code === 'INVALID_FILTER' as a FilterOperatorError, and everything else passes through by identity. So the four sites handle it through the catch they already had, with no site code changed.
Delta record 5859131963 — PASS.Served-tier: CONTRACT_REVIEW_TIER, head 6f4bee84658bf84753acd58b78476e4ae994b442 (the current head), independence pair present.
Both closures were measured, including the reviewer's round-1 ObjectChart probe re-run in both dialects.
Red leg: 9 failed / 13 passed with the round-1 seam.
The changeset prose is now true sentence by sentence.
None of the 12 pending changesets the changeset-claim-re-read bot names is made false.
Semver.patch was judged consistent with this repo's practice for an additive optional member (react's own CHANGELOG ships them under both levels, and two pending changesets do so at patch). The fixed group makes the published version identical either way.
PR body. The reviewer named three stale sentences. The seat patched all three and read the body back identical before this ACCEPT.
Seat decisions recorded (execution parameters; a maintainer ruling can overturn them)
The drill sites refuse with a console.warn and open nothing. No new drawer UI, and no new onDrill host contract (amendment 5858635767).
The spec's INVALID_FILTER at the drill seam was closed rather than narrowed in prose, because it is this card's symptom at this card's sites, reachable through ObjectChartSchema.filter.
Out-of-scope findings, one line each (Acceptance notes, no card)
LookupField's recents effect catches every error into an empty rail; the main popover query still surfaces the refusal. It is async, class 2 and outside this card.
DrillDownDrawer's report drill composes with a literal $and rather than mergeFilterNodes. Read only; no reach measured.
The re-raised drill-seam error carries no operator / field subject, so filterRefusalSubject answers undefined for it. No drill site draws a headline from it.
Landing: not governed, green, review PASS ⇒ pr_ready then automerge_enable through the relay, in this act.
Activity
objectstack-fleet commentedon Sep 27, 2026
Path: business objects, records and views | 缺项 (no checklist item renders a malformed element
dataSourcefilter) | P2Triage: first grade —
bug·priority:p3·domain:ui·area:records·pm:queue(findingremoved — graded). The closure card for the classTriage: lands at the render-time and click-time filter mergers, which call the throwing converter form:
packages/react/src/element-data-source/ElementDataSourceGate.tsx:397(useElementDataSourceSchema'suseMemo);composeElementDataSource;recentFilter;mergedFilter;composeDrillFilter.⇒
domain:ui. Rationale: a malformed authored filter throws aFilterOperatorErrorout of render, into the nearest error boundary, instead of drawing the malformed-filter notice its siblings draw throughtoFilterNodeSafely. It was read from source, not reproduced, with no producer named ⇒ p3. One card covers every site, with an enumerating pin.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01W89enF2dYV7K4N2Fbfj33f· 2026-09-27T15:18Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments), and objectui#9164 / PR objectui#10784 (both closed).Execution notes.
mergeFilterNodes/ throwing-converter caller, classed as safe, inside a loadtry, or render/click-uncaught. Record the table.toFilterNodeSafely, or catch at the merge and surface the notice the way its siblings do. ⛔ Never swallow the refusal silently.lowerLogicalGroupearly return on a TRUE disjunct ({ $or: [{}, { a: {} }] }→ TRUE, depending on member order) is the same family. Include it, and pin both member orders.objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 1
Session:
session_01DuWo5bdP9SdVebamn99GGkAccount:
os-steve(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-10789-render-time-filter-mergersWorktree:
objectui-issue-10789Domain:
domain:uiSeat:
domain:ui#1File surface: the render-time and click-time filter mergers the triage names —
packages/react/src/element-data-source/ElementDataSourceGate.tsx(useElementDataSourceSchema),packages/core/src/data-scope/element-data-source.ts(composeElementDataSource),packages/fields/src/widgets/PeoplePicker.tsx(recentFilter),packages/fields/src/widgets/RecordPickerDialog.tsx(mergedFilteronly),packages/core/src/utils/drill-down.ts(composeDrillFilter) — plus any further render/click-uncaught site the census finds, tests beside them, andpatchchangesets for the touched packages (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT, objectstackscripts/pm/dispatch-gates.mjs:12534at0d3ec47137)Clause-②: no
Thread-read: 5857118114
Serial constraints cleared: read 2026-09-27T16:58Z at objectui
origin/main. None of the 14 open PRs touches these files. One live claim shares a file: this seat's objectui#10594 editsRecordPickerDialog.tsx'swithoutDeniedFields(:451); this card editsmergedFilter(:693) ⇒ region-disjoint; mergemainif it lands first.Why
Clause-②: no: a malformed filter stays refused — it is surfaced as the malformed-filter notice its siblings already draw throughtoFilterNodeSafelyinstead of throwing out of render. No filter shape becomes accepted and no public face grows.domain:uiseat #1 · claim · 2026-09-27T16:58ZGenerated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{
"issue": 10789,
"status": "done",
"branch": "claude/issue-10789-render-time-filter-mergers",
"pr": "#10830",
"session": "session_01DuWo5bdP9SdVebamn99GGk — the dispatching seat's session (mode:subagent; this run's commits carry the same Claude-Session line)",
"premise_still_valid": true,
"summary": "Premise measured, not assumed: on base a14fb23 all 18 site pins go red with the FilterOperatorError each site threw. Census (table in the PR body) found 9 render/click-uncaught sites, 4 more than the brief listed: ObjectPivotTable, DatasetWidget and DatasetReportRenderer (composeDrillFilter callers besides ObjectChart), plus the ListView server export click. R1/R3/R4 lower each source through toFilterNodeSafely, then merge. R2 and R5-R9 catch only FilterOperatorError around the core utility, which keeps throwing, so there is no new export. The notice matches each nearest sibling: the gate draws the blocks' malformed-filter notice (view.malformedFilter); record_picker shows its missing panel; the pickers use their error slot; the drills log via console.warn and open or emit nothing, never a filter-less drill; the export names the refusal in its menu. lowerLogicalGroup now reads every $or member before a TRUE disjunct absorbs, so both member orders refuse. origin/main ff94a12 is merged, including objectui#10594: one import-line conflict in ObjectPivotTable, resolved as the union.",
"tests": "8 new pin files, one malformed filter per site plus a CONTROL each; the assertions check the notice content (the refused operator) or the envelope (code INVALID_FILTER, httpStatus 400) plus the absence of an unfiltered read, never a bare toThrow. RED LEG: fix committed at 9f1d071; the 10 source files were checked out at BASE a14fb23, with an on-disk grep -c of objectui#10789 = 0 in each. Result: 'Test Files 8 failed (8) / Tests 18 failed | 12 passed (30)', and every red is the site's FilterOperatorError or an absent notice. Restored with git checkout HEAD --, proven by 'RESTORED: git diff HEAD empty, 10 blobs match HEAD' (a trap covers EXIT INT TERM); the subjects resolve through the vitest src alias, so no dist was involved. GREEN at 664305c: core+react+all pins 'Test Files 285 passed (285) / Tests 4931 passed (4931)'. fields+plugin-charts+plugin-report 'Test Files 321 passed | 1 skipped (322) / Tests 4722 passed | 7 skipped (4729)'. plugin-list+plugin-dashboard+gate-consumer sweep (13 elementDataSource suites across 10 packages, the console injection probe, 3 record_picker suites) 'Test Files 258 passed (258) / Tests 2565 passed (2565)'. turbo type-check over the 8 packages 'Tasks: 22 successful, 22 total', all 8 a cache miss. An earlier type-check at cae4c5e caught TS7006 in a pin (an untyped warn-call mapper); f2bc82a fixed it.",
"gates": [
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ + 6 pin files' @664305cb3",
"verdict": "VERDICT batch-last-exit 0 (last part = vitest); Test Files 285 passed (285)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/fields/ packages/plugin-charts/ packages/plugin-report/' @664305cb3",
"verdict": "VERDICT command-exit 0; Test Files 321 passed | 1 skipped (322)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/plugin-list/ packages/plugin-dashboard/ + 17 consumer suites' @664305cb3",
"verdict": "VERDICT command-exit 0; Test Files 258 passed (258)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec turbo run type-check --concurrency=2 --continue --filter=(core,react,fields,components,plugin-charts,plugin-dashboard,plugin-report,plugin-list)' @664305cb3",
"verdict": "VERDICT command-exit 0; Tasks: 22 successful, 22 total",
"exit": 0
},
{
"command": "red leg: redleg.sh (BASE sources + new pins) under os-verify-lock",
"verdict": "VITEST-EXIT=1, Tests 18 failed | 12 passed (30); RESTORED: git diff HEAD empty, 10 blobs match HEAD; VERDICT command-exit 0",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs",
"verdict": "18 source file(s) of 8 released package(s) changed, and this change declares 1 changeset(s)",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"verdict": "No changeset declares a major bump",
"exit": 0
},
{
"command": "pnpm check:element-data-source-declaration",
"verdict": "OK — 13 gate-consuming file(s) checked",
"exit": 0
},
{
"command": "pnpm check:i18n-keys",
"verdict": "Every in-scope call-site key resolves against the en pack",
"exit": 0
},
{
"command": "pnpm check:new-line-citations",
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)",
"exit": 0
},
{
"command": "pnpm check:phantom-deps / check:self-import / check:esm-specifiers (specifier leg) / check:test-path-roots",
"verdict": "each OK",
"exit": 0
},
{
"command": "node scripts/check-control-bytes.mjs / check-vi-mock-specifiers.mjs / check-vi-mock-inherit.mjs / check-vi-mock-override-shape.mjs",
"verdict": "each OK",
"exit": 0
},
{
"command": "pnpm exec eslint --format json (the 18 changed files, git diff --name-only HEAD^2 HEAD)",
"verdict": "declared narrowing: 18 files, 0 errors; per-rule warning delta vs base versions = 0 on every source file; config has no parserOptions/projectService (no type-aware lint) so untouched files cannot move",
"exit": 0
},
{
"command": "repo-wide pnpm test, pnpm lint, check:esm-specifiers load leg, remaining check:* families",
"verdict": "NOT MEASURED locally: CI-owned",
"exit": null
}
],
"line_budget": "n/a — no skills/** or ratcheted ledger touched",
"files_changed": [
".changeset/10789-render-time-filter-refusals.md",
"packages/core/src/utils/filter-converter.ts",
"packages/core/src/utils/tests/filter-or-absorb-order-10789.test.ts",
"packages/react/src/hooks/useElementDataSource.ts",
"packages/react/src/element-data-source/ElementDataSourceGate.tsx",
"packages/react/src/element-data-source/tests/ElementDataSourceGate.malformedFilter-10789.test.tsx",
"packages/components/src/tests/record-picker-malformed-filter-10789.test.tsx",
"packages/fields/src/widgets/PeoplePicker.tsx",
"packages/fields/src/widgets/RecordPickerDialog.tsx",
"packages/fields/src/widgets/pickerMalformedFilter-10789.test.tsx",
"packages/plugin-charts/src/ObjectChart.tsx",
"packages/plugin-charts/src/tests/ObjectChart.drillRefusal-10789.test.tsx",
"packages/plugin-dashboard/src/ObjectPivotTable.tsx",
"packages/plugin-dashboard/src/DatasetWidget.tsx",
"packages/plugin-dashboard/src/tests/drillRefusal-10789.test.tsx",
"packages/plugin-report/src/DatasetReportRenderer.tsx",
"packages/plugin-report/src/tests/DatasetReportRenderer.drillRefusal-10789.test.tsx",
"packages/plugin-list/src/ListView.tsx",
"packages/plugin-list/src/tests/ListView.exportRefusal-10789.test.tsx"
],
"deviations": [
"File surface grew beyond the claim's five named sites, under its 'plus any further render/click-uncaught site the census finds' clause: plugin-charts, plugin-dashboard (ObjectPivotTable, DatasetWidget), plugin-report (DatasetReportRenderer) and plugin-list (ListView export), plus one components test for element:record_picker (no components source changed, so no components changeset).",
"Brief zone 2 item 1 falsified in part: composeDrillFilter is not a click handler. It is a core utility with five callers: two recompose during render after a click (ObjectChart, ObjectPivotTable) and three are click handlers (DatasetWidget openDrill, DatasetReportRenderer drill and drillCell). DrillDownDrawer's mergedFilter makes no converter call (a literal $and): classified, untouched.",
"Brief zone 2 item 2 decided at the callers, not the utilities: composeElementDataSource and composeDrillFilter keep throwing because their async callers (ViewDataProvider) and contracts are unchanged, and a caller-side FilterOperatorError-only catch needs no new core export.",
"Brief zone 2 item 4: the drill surfaces have no rendered malformed-filter state, so none was invented. They log via console.warn (ReportView's drill failure and ValueDataSource's refused filter use the same channel), and the drill does not open. See open_questions.",
"Public face: the UseElementDataSourceResult and UseElementDataSourceSchemaResult interfaces (@object-ui/react) gain an optional filterRefusal field, and the 'missing' status docs now include a refused filter. The claim's Clause-② reason says 'no public face grows'. The PR body carries the claim's 'Clause-②: no' verbatim; see open_questions.",
"The changeset declares patch for all seven packages, as the brief asked; the react interfaces gain the additive optional field noted above.",
"Merged origin/main ff94a12 before opening the PR, as the brief asked (objectui#10594 had landed). One conflict (ObjectPivotTable import lines) was resolved as the union; RecordPickerDialog.tsx merged without conflict.",
"PeoplePicker's recentFilter threw on EVERY render with a malformed declared filter, not only when recents exist: the useMemo merged even with an empty id list."
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay writes as objectstack-fleet[bot], each one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write). pr_create → POST /repos/objectstack-ai/objectui/pulls (draft #10830). label-write assign → POST /repos//issues/10830/assignees (os-steve, read back). os-dev-report comment → POST /repos//issues/10789/comments. git push to the branch is not counted (not REST). Card assignee and labels: 0 writes.",
"open_questions": [
{
"question": "Drill sites (ObjectChart, ObjectPivotTable, DatasetWidget, DatasetReportRenderer) now refuse with a console.warn and open or emit no drill. Should a drill refusal also be drawn on screen?",
"options": [
"A — keep the logged refusal (this PR): no new UI, and nothing opens without the filter",
"B — add a rendered malformed-filter state to the drill drawers (DrillDownDrawer and ObjectChart's drawer) and a host-visible signal for DatasetReportRenderer's onDrill: new UI and a new onDrill contract"
],
"recommendation": "A. Real need: reaching it takes a widget filter this layer refuses that the widget's own read still accepted (aggregate route, inline or bound rows), with no named producer. Long-term: B widens the onDrill contract before any demand. AI-error axis: both refuse loudly and neither widens. Startup focus: no new surface without pull."
},
{
"question": "Does the new optional filterRefusal field on the two exported @object-ui/react hook-result interfaces flip the claim's 'Clause-②: no'?",
"options": [
"A — stays no: an output-only optional field on a hook result, no new export, no new authorable key, and the accept set only narrows (lowerLogicalGroup)",
"B — yes: any exported TypeScript interface growth counts as public face; flip the line and raise the changeset to minor"
],
"recommendation": "A, but the seat owns the reading; the PR body carries 'Clause-②: no' verbatim from the claim."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — LookupField's recents effect catches every error, a refusal included, into an empty rail. Read on main; the main popover query still surfaces the refusal. Class 2 (async), outside this card. dedupe: LookupField recents catch swallow, recentRows empty refusal",
"carrier: 承接者:无 · noted, not filed — composeDrillFilter's final parseFilterAST can raise the spec's own INVALID_FILTER, not a FilterOperatorError, for a malformed AST widget filter, and these sites do not catch it. Read only, not measured through a public door. dedupe: composeDrillFilter parseFilterAST INVALID_FILTER, drill AST widget filter",
"carrier: 承接者:无 · noted, not filed — DrillDownDrawer's report drill composes report.filter with the drill filter as a literal $and rather than through mergeFilterNodes. Read only, no reach measured. dedupe: DrillDownDrawer report drill mergedFilter $and, report drill filter compose"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Claim amended:
Clause-②flips to yes, and the drill question is decided ·domain:uiseat #1 · 2026-09-27T18:37Zsession_01DuWo5bdP9SdVebamn99GGk(os-steve). The dev report on this card (PR objectui#10830, head664305cb3) raised two open questions. The seat re-read the one that bears on the claim against the PR head itself.Clause-②: yes
Why the claim's
nowas false. The claim rested on 「no public face grows」. At the head:UseElementDataSourceResult(packages/react/src/hooks/useElementDataSource.ts:59) andUseElementDataSourceSchemaResult(packages/react/src/element-data-source/ElementDataSourceGate.tsx:168) each gain an optionalfilterRefusal.@object-ui/react's onlyexportsentry.:src/index.tsre-exports./hooks/index.jsand./element-data-source/ElementDataSourceGate.jswithexport *, andhooks/index.tsre-exports./useElementDataSource.jsthe same way.⇒ A published type gains a member, which is the public-face limb of the criterion. The acceptance set itself only narrows:
lowerLogicalGroupnow reads every$ormember before a TRUE disjunct absorbs it, and no refused filter becomes accepted. The seat's claim was wrong, and this line replaces it. A contract review is owed on the PR regardless (the diff also carries a changeset). The review judges whether the declaredpatchlevel fits an additive member on a published interface.Dev question 1 (should a drill refusal also be drawn on screen?) — decided by this seat: A, ⛔ not escalated. The four drill sites (
ObjectChart,ObjectPivotTable,DatasetWidget,DatasetReportRenderer) keep a logged refusal: nothing opens without the filter. This is an execution parameter inside the claim, since both options refuse loudly and neither accepts a refused filter. B would add new drawer UI and widenonDrill's host contract with no named producer, which is outside this card's surface. The decision can be overturned by a maintainer ruling; if one asks for B, it is a new card.Dev question 2 is answered by the line above.
The PR body's second line is rewritten to match, with a seat note, since the dev writes a body once.
domain:uiseat #1 · claim amendment · 2026-09-27T18:37ZGenerated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
os-dev-report
{
"issue": 10789,
"status": "done",
"branch": "claude/issue-10789-render-time-filter-mergers",
"pr": "#10830",
"head": "6f4bee846",
"round": "patch round 1 — CI shard 2/8 (check-i18n-dead-keys) + contract review 5858758764 (VERDICT FAIL, two defects)",
"session": "session_01DuWo5bdP9SdVebamn99GGk — the dispatching seat's session (mode:subagent; commits carry the same Claude-Session line)",
"premise_still_valid": true,
"summary": "Both review defects fixed in one round, at head 6f4bee8 (previous head 664305c). DEFECT 1 took option (b): the gate's malformed-filter panel no longer imports the
enpack. Its provider-less default is a literal inline defaultValue, the way the siblings carry the sentence: they keep a literal copy in a module-scope createSafeTranslation table, which @object-ui/react cannot have because its entry re-exports this module. check:i18n-keys holds the literal byte-identical to the pack, and an ablation proves it: one changed character gives EXIT=1 with [default-value-drift] view.malformedFilter. The file left the pack-object importer population (dead-keys CLI: 0 UNANALYSED), so no bullet or ledger entry was needed. The gate pin now asserts the sentence text, with the operator filled and no raw key, both without and with an I18nProvider. DEFECT 2 closed the seam rather than narrowing the sentence. composeDrillFilter now re-raises the spec parseFilterAST refusal (plain Error, code INVALID_FILTER, status 400) as a FilterOperatorError with the message verbatim; any other error passes through untouched. All four drill sites (ObjectChart, ObjectPivotTable, DatasetWidget, DatasetReportRenderer) therefore handle it through the catch they already had: nothing opens, no onDrill, never a filter-less drill, and a console.warn names the operator. No site code changed and @object-ui/core gains no export. The changeset's opening sentence was rewritten to be true at every site and the drill bullet now names both refusal steps.","tests": "NEW PINS: core drill-filter-spec-refusal-10789 checks that array and object dialects, a scalar on $in, come out as a FilterOperatorError (code INVALID_FILTER, httpStatus 400, message naming $in); that a non-INVALID_FILTER error passes through by identity (vi.resetModules + doMock, undone in finally, because the unit project is isolate:false); plus a CONTROL. A spec-refusal case was added to each drill site pin: ObjectChart (the reviewer's inline-data [['stage','in','won']] probe, drawer + navigate), ObjectPivotTable (drawer + navigate), DatasetWidget, and DatasetReportRenderer (row + matrix). RED LEG: drill-down.ts checked out at the previous head 664305c, on-disk marker count 0, pins kept. Result 'Test Files 4 failed (4) / Tests 9 failed | 13 passed (22)': the 9 reds are exactly the 9 spec-refusal assertions, and the 13 greens are the converter cases, CONTROLs and the passthrough guard. Restored, 'RESTORED: git diff HEAD empty, blob matches HEAD', with a trap on EXIT INT TERM. GREEN at 6f4bee8: core+react+ALL scripts/tests 'Test Files 457 passed | 2 skipped (459) / Tests 10231 passed | 2 skipped (10233)'. Explicitly, 'Test Files 9 passed (9) / Tests 467 passed (467)' for check-i18n-dead-keys (134 tests, including '✓ … EVERY non-test importer in the live population is analysed in the header'), check-i18n-call-site-keys, check-i18n-en-drift, check-i18n-designer-table-parity, check-hand-rolled-comment-mask, check-comment-mask-corpus, check-new-cross-file-line-citations, check-unreferenced-sources and the console element-data-source-input-injection probe. plugin-charts+plugin-dashboard+plugin-report 'Test Files 244 passed (244) / Tests 2535 passed (2535)'. type-check core/react/plugin-charts/plugin-dashboard/plugin-report 'Tasks: 18 successful, 18 total', all 5 a cache miss. Order-independence of the passthrough pin: it ran green in one worker (--maxWorkers=1) beside two drill-importing core files, 'Test Files 4 passed (4)'. A first full core run at c5cee60 had failed it under isolate:false (the module-scope vi.mock did not apply); 6f4bee8 fixed that.",
"gates": [
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/ scripts/tests/' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 457 passed | 2 skipped (459)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --reporter=verbose scripts/tests/check-i18n-dead-keys.test.ts + check-i18n-call-site-keys + check-i18n-en-drift + check-i18n-designer-table-parity + check-hand-rolled-comment-mask + check-comment-mask-corpus + check-new-cross-file-line-citations + check-unreferenced-sources + apps/console element-data-source-input-injection' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 9 passed (9), Tests 467 passed (467)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec vitest run --maxWorkers=2 packages/plugin-charts/ packages/plugin-dashboard/ packages/plugin-report/' @6f4bee846",
"verdict": "VERDICT command-exit 0; Test Files 244 passed (244)",
"exit": 0
},
{
"command": "bash os-verify-lock.sh -c 'pnpm exec turbo run type-check --concurrency=2 --continue --filter=(core,react,plugin-charts,plugin-dashboard,plugin-report)' @6f4bee846",
"verdict": "VERDICT command-exit 0; Tasks: 18 successful, 18 total",
"exit": 0
},
{
"command": "red leg: redleg2.sh (drill-down.ts at 664305c + new pins) under os-verify-lock",
"verdict": "VITEST-EXIT=1, Tests 9 failed | 13 passed (22); RESTORED: git diff HEAD empty, blob matches HEAD; VERDICT command-exit 0",
"exit": 0
},
{
"command": "ablation: literal defaultValue mutated one char, pnpm check:i18n-keys",
"verdict": "MUTATED EXIT=1 [default-value-drift] view.malformedFilter; RESTORED git diff HEAD empty; RESTORED check:i18n-keys EXIT=0",
"exit": 0
},
{
"command": "pnpm check:i18n-keys @6f4bee846",
"verdict": "every literal inline defaultValue matches the value the pack serves",
"exit": 0
},
{
"command": "pnpm check:i18n-dead-keys @6f4bee846",
"verdict": "report; 0 UNANALYSED importers, ElementDataSourceGate.tsx absent from the importer population",
"exit": 0
},
{
"command": "pnpm check:element-data-source-declaration @6f4bee846",
"verdict": "OK — 13 gate-consuming file(s) checked",
"exit": 0
},
{
"command": "node scripts/check-hand-rolled-comment-mask.mjs @6f4bee846",
"verdict": "OK every carrier is a DEBT entry, and every DEBT entry still carries one",
"exit": 0
},
{
"command": "pnpm check:new-line-citations @6f4bee846",
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)",
"exit": 0
},
{
"command": "node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs @6f4bee846",
"verdict": "20 source file(s) of 8 released package(s) changed, 1 changeset declared / No changeset declares a major bump",
"exit": 0
},
{
"command": "pnpm check:phantom-deps / check:self-import; node scripts/check-control-bytes.mjs / check-vi-mock-specifiers.mjs / check-vi-mock-inherit.mjs / check-vi-mock-override-shape.mjs",
"verdict": "each OK",
"exit": 0
},
{
"command": "pnpm exec eslint --format json (this round's 7 changed ts/tsx files)",
"verdict": "0 errors; drill-down.ts and ElementDataSourceGate.tsx per-rule warnings identical to 664305c",
"exit": 0
},
{
"command": "fields, plugin-list suites and their type-check this round",
"verdict": "NOT RE-RUN: this round changed no file in either package; last measured green at 664305c",
"exit": null
}
],
"line_budget": "n/a — no skills/** or ratcheted ledger touched",
"files_changed": [
".changeset/10789-render-time-filter-refusals.md",
"packages/core/src/utils/drill-down.ts",
"packages/core/src/utils/tests/drill-filter-spec-refusal-10789.test.ts",
"packages/react/src/element-data-source/ElementDataSourceGate.tsx",
"packages/react/src/element-data-source/tests/ElementDataSourceGate.malformedFilter-10789.test.tsx",
"packages/plugin-charts/src/tests/ObjectChart.drillRefusal-10789.test.tsx",
"packages/plugin-dashboard/src/tests/drillRefusal-10789.test.tsx",
"packages/plugin-report/src/tests/DatasetReportRenderer.drillRefusal-10789.test.tsx"
],
"deviations": [
"Defect 2 is closed at the SEAM, not by a four-site predicate. The seat asked for one small predicate over four copies. Instead, composeDrillFilter re-raises the spec INVALID_FILTER as a FilterOperatorError: it is the only caller of parseFilterAST on the drill path, all four sites already catch that type, and so no site code changed and @object-ui/core gains no export. The behaviour is the one the seat specified: only code === 'INVALID_FILTER' is converted, everything else passes through, and a pin checks it by identity.",
"Defect 1 went option (a) first, then (b). I committed the classification bullet plus the ANALYSED_PACK_OBJECT_IMPORTERS entry (cab1c7d) before the review arrived. A local hard reset to drop it was denied by the permission classifier, so I reverted it forward (3dbcaba); both commits sit on the pushed branch and the net diff in scripts/ is empty. Option (b) then removed the pack import, so the file is outside the population and any entry for it would fail the gate's reverse check, 'EVERY analysed importer still exists'.",
"The first full core run at c5cee60 failed one new pin, the passthrough guard: its module-scope vi.mock did not apply under the unit project's isolate:false. 6f4bee8 rewrote it with vi.resetModules + doMock + dynamic import, undone in finally, and it is green in a shared worker and in the full run.",
"PR body needs edits I may not make (the dev writes the body once, no PATCH), for the seat: (1) Evidence: the head is 6f4bee8 and red leg 2 is 9 failed | 13 passed. (2) Census rows R5–R8: composeDrillFilter now re-raises the spec INVALID_FILTER as a FilterOperatorError, so these sites also catch the parseFilterAST refusal. (3) Design-choice notice bullet for the gate: the default is now a literal inline defaultValue policed by check:i18n-keys, not a pack read. (4) Acceptance notes 'Observed, not in scope': drop the parseFilterAST sentence, which is now fixed. (5) The review cites a claim amendment with 'Clause-②: yes', while the body's second line still reads 'Clause-②: no'; reconcile it.",
"The fields and plugin-list packages are unchanged this round, so their suites and type-check were not re-run; the last green reading is at 664305c."
],
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "This round: 1 relay write as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches), which becomes the os-dev-report comment POST /repos//issues/10789/comments. Run total: 4 (pr_create #10830, PR assignee os-steve, the first os-dev-report comment, this one). git push is not REST. Reads: REST GET for review 5858758764 and the comment read-back.",
"open_questions": [
{
"question": "Drill sites refuse with console.warn and open nothing: should a drill refusal also be drawn on screen? (Carried over; the review flags it and calls it not a defect.)",
"options": [
"A — keep the logged refusal: no new UI and no new onDrill contract",
"B — add a rendered malformed-filter state to the drill drawers and a host-visible signal on onDrill"
],
"recommendation": "A. The four axes as in the first report: no named producer, B widens a host contract before any demand, both refuse loudly, and nothing new without pull."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — LookupField's recents effect catches every error, a refusal included, into an empty rail; the main popover query still surfaces the refusal. Class 2 (async). dedupe: LookupField recents catch swallow, recentRows empty refusal",
"carrier: 承接者:无 · noted, not filed — DrillDownDrawer's report drill composes report.filter with the drill filter as a literal $and rather than through mergeFilterNodes. Read only, no reach measured. dedupe: DrillDownDrawer report drill mergedFilter $and, report drill filter compose"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
ACCEPT — PR objectui#10830 ·
domain:uiseat #1 · 2026-09-27T19:35Zsession_01DuWo5bdP9SdVebamn99GGk(os-steve), the dispatching seat and reviewer of record. Checked against GitHub andorigin/main, ⛔ not against the reports.Checklist
Shape. Draft →
main. The body opensFixes #10789, the only closing keyword, andClause-②: yesis at line start (the claim amendment5858635767). PR assigneeos-steve.Scope. 21 files, +1,497 / −79. Nine render- or click-time merge sites, R1–R9 in the PR's census table:
PeoplePickerandRecordPickerDialog;ListViewserver export.Around them:
lowerLogicalGroup's$orabsorption order inpackages/core, nine pin files, and one changeset,patchfor seven packages.Surface growth under the claim's census clause, accepted. plugin-charts, plugin-dashboard, plugin-report and plugin-list, each a genuine member of the class.
@object-ui/reactgains the optionalfilterRefusalon two published hook-result interfaces, and@object-ui/coregains no export.Governed-surface predicate. 21 paths:
NOT governed(AGENTS.md lit control: exit 3).Checks on head
6f4bee846. 43 check-runs: 40 success, 3 skipped, 0 failure.Round 1, head
664305cb3: contract review5858758764FAILED with two defects:enpack import failedcheck-i18n-dead-keys(shard 2/8 red).parseFilterASTasINVALID_FILTER, not aFilterOperatorError, so it still threw into the error boundary at the four drill sites.Patch round 1 (dev report
5859112009) fixed both:enimport is gone. The default is now a literal held byte-identical to the pack bycheck:i18n-keys, and the one-character ablation turns that check red.composeDrillFilterre-raises onlycode === 'INVALID_FILTER'as aFilterOperatorError, and everything else passes through by identity. So the four sites handle it through the catch they already had, with no site code changed.Delta record
5859131963— PASS.Served-tier: CONTRACT_REVIEW_TIER, head6f4bee84658bf84753acd58b78476e4ae994b442(the current head), independence pair present.ObjectChartprobe re-run in both dialects.changeset-claim-re-readbot names is made false.Semver.
patchwas judged consistent with this repo's practice for an additive optional member (react's own CHANGELOG ships them under both levels, and two pending changesets do so atpatch). The fixed group makes the published version identical either way.PR body. The reviewer named three stale sentences. The seat patched all three and read the body back identical before this ACCEPT.
Seat decisions recorded (execution parameters; a maintainer ruling can overturn them)
console.warnand open nothing. No new drawer UI, and no newonDrillhost contract (amendment5858635767).INVALID_FILTERat the drill seam was closed rather than narrowed in prose, because it is this card's symptom at this card's sites, reachable throughObjectChartSchema.filter.Out-of-scope findings, one line each (Acceptance notes, no card)
LookupField's recents effect catches every error into an empty rail; the main popover query still surfaces the refusal. It is async, class 2 and outside this card.DrillDownDrawer's report drill composes with a literal$andrather thanmergeFilterNodes. Read only; no reach measured.operator/fieldsubject, sofilterRefusalSubjectanswersundefinedfor it. No drill site draws a headline from it.Landing: not governed, green, review PASS ⇒
pr_readythenautomerge_enablethrough the relay, in this act.domain:uiseat #1 · ACCEPT · 2026-09-27T19:35ZGenerated by Claude Code
fix(react,fields,charts,dashboard,report,list,core): surface filter r…