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
components: the filter-builder renderer calls onChange with a { target: { name, value } } event, but FilterBuilderSchema.onChange declares (filter: FilterGroup) => void #26382
Filed by the domain:ui seat 1 from an out-of-scope finding in the #26348 os-dev-report (comment 6431261263, PR #26380). The #26348 claim kept the signature out of bounds. ⛔ Not a claim.
ui/packages/types/src/complex.ts:641: FilterBuilderSchema.onChange?: (filter: FilterGroup) => void. The docblock at :638 says the filter-builder renderer calls it as props.onChange.
ui/packages/components/src/renderers/complex/filter-builder.tsx:23-30: the renderer types its own onChange as (event: any) => void and calls it with { target: { name, value } }.
Effect: a host that writes a handler typed from the schema reads .logic / .conditions off an event object and gets undefined. The compiler can't catch it, because the renderer's any hides the mismatch. Class b (a named producer), as the dev measured it on #26380's head.
Open choice for triage: which side is the contract? Either the renderer passes the FilterGroup it already holds, or the schema declares the event shape it actually gets. Search the in-repo hosts of a filter-builder node's onChange before choosing; the choice sets Clause-②.
p3, as filed: the declared FilterBuilderSchema.onChange(filter: FilterGroup) and the renderer's { target: { name, value } } call disagree, and an any hides the gap.
The dev measures before choosing a side: the { target: { name, value } } shape is how a form field's change event looks, so check whether a form host binds a filter-builder node through it. A search on main found no in-repo filter-builder node outside docs and types. If nothing binds the event shape, the declared schema is the contract and the renderer passes the FilterGroup (Clause-②: no). If a binding needs the event shape, the type changes instead, which is a published-type change on @object-ui/types (Clause-②: yes). Say which in the PR.
Claim: PM loop round 1
Session: session_017iXjUm1rujwDk7f3KT1Pkk (seat); implementing card thread session_01ADHkmNhEw4jVVQ4UTxrxMZ
Account: os-steve
Branch: claude/project-thread-eq0t7c (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: the card thread's own container
Domain: domain:ui
Seat: domain:ui#1
File surface: the filter-builder renderer in ui/packages/components/src/renderers/**, FilterBuilderSchema in ui/packages/types/src/** (only if the measurement says the type changes), their tests, one root changeset (stop on breach; explain in the report). ⛔ ui/.github/** and the root .github/workflows/ci.yml are out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root .changeset/.
Container & model: S, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: no (the expected side: the renderer adopts the declared FilterGroup; if the measurement shows a binding needs the event shape, the seat amends this claim to yes with a BREAKING note)
Metadata-protocol: none
Responsibility: n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public Console
Thread-read: 6431774981
Premise: holds (read-only premise check at origin/main, 2026-10-27T08:18Z; spot-checked by the seat)
Serial constraints cleared: none
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
{
"issue": 26382,
"status": "done",
"branch": "claude/project-thread-eq0t7c",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26590",
"session": "session_01ADHkmNhEw4jVVQ4UTxrxMZ",
"premise_still_valid": true,
"summary": "Step 1 found no in-repo host that binds a filter-builder node's onChange through the event shape: form fields resolve only field:-namespaced widgets, no SchemaRenderer host passing onChange renders this node, the metadata-admin 'filter-builder' widget is FilterBuilderWidget over FilterRuleLite[] and never this node, and the docs/examples declare or carry the FilterGroup shape. So the declared schema is the contract (Clause-②: no). The renderer's onChange is now typed FilterBuilderSchema['onChange'] and called with the group; handleChange is typed FilterBuilderRowGroup, and no any or cast is left on the path. FilterBuilderRowGroup is not assignable to FilterGroup (TS2345 on the opt-in exists/notExists operators), so the renderer narrows with a type predicate against VIEW_FILTER_OPERATORS and throws, naming the operator, when a row reached it off contract. FilterBuilderSchema in @object-ui/types is unchanged; one @object-ui/components patch changeset.",
"tests": "At head 070b433f. New ui/packages/components/src/__tests__/filter-builder-renderer-onchange-26382.test.tsx: 4/4 (host onChange gets the group with no target; node-authored handler gets the same; off-contract exists row throws on window and onChange is not called; expectTypeOf parameter(0) EQUAL FilterGroup plus @ts-expect-error for the old event handler). components type-check (tsc --noEmit && tsc -p tsconfig.test.json): VERDICT command-exit 0. Full components suite under the lock: Test Files 438 passed | 1 skipped (439), Tests 4273 passed | 24 skipped (4297), VERDICT command-exit 0. Ablation 1 via scripts/ablation-replace.mjs (anchor onChange(group); x1->x0, blob 96162878fc0f->ece34562e098, replaced with the old target-event call): Tests 2 failed | 2 passed ('expected { target: { name: filters, ...(1) } } to not have property target'; 'Target cannot be null or undefined.'), restored blob == HEAD, git diff HEAD empty. Ablation 2 (guard replaced by false as boolean): tsc TS2345 'FilterBuilderRowGroup is not assignable to parameter of type FilterGroup' down to 'Type exists is not assignable', restored blob == HEAD. No vitest alias question: the test imports the renderer relatively, so no build sits between mutation and run.",
"gates": "dispatch-gates --commands at 070b433f (after merging origin/main; the first derivation flagged a stale tree) listed 34 commands; all 34 exit 0, each captured before any pipe (check-adr-0087-registration: no declared-breaking changeset; check-empty-changeset: 1 declaring changeset added; pnpm --dir ui exec vitest run scripts/: Test Files 178 passed | 2 skipped (180)). dispatch-gates --ran: 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED (a DERIVED zero, all 34 recorded an exit code). Also pnpm --dir ui run check:new-line-citations: 0 new citation(s), exit 0. CI families: in_progress at report time (draft just opened); not re-run locally.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 relay strokes as objectstack-fleet[bot]: (1) pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/26590/assignees os-steve), read back 10242 bytes sent = stored; (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/26382/comments. git push is not a REST write. Card reads were single-card REST GETs.",
"open_questions": [
{
"question": "What should the renderer do when the builder hands it a row whose operator a FilterGroup cannot carry (the opt-in exists/notExists, reachable only off contract: a host forwarding extraOperators through the undeclared prop spread, or a stored value the FilterGroup zod mirror refuses)?",
"options": [
"A throw an error naming the operator (shipped): loud, onChange not called, the declared type stays true",
"B skip the emit and log: quiet, the builder shows the edit and the host never hears it",
"C also stop forwarding extraOperators to FilterBuilder, so only refused stored input reaches the branch: a behaviour change to an undeclared host prop, beyond this card"
],
"recommendation": "A, because handing the row on would make the declared FilterGroup false and dropping it loses the edit in silence; no in-repo host reaches the branch."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: the renderer spreads undeclared host props (extraOperators included) into FilterBuilder, so a host can grant an opt-in operator that FilterBuilderSchema does not declare; no in-repo host does. Recorded in the PR's Acceptance notes.",
"carrier: none · noted: the objectstack-ai/cloud and hotcrm repositories were not searchable from this container for an event-shape binding."
]
}
Round 2 (FAIL round on PR #26590), superseding comment 6438144275.
{
"issue": 26382,
"status": "done",
"branch": "claude/project-thread-eq0t7c",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26590",
"session": "session_01ADHkmNhEw4jVVQ4UTxrxMZ",
"premise_still_valid": true,
"summary": "FAIL round 2 (seat ruling on the open question): the renderer now takes extraOperators out of the host spread and never forwards it to FilterBuilder, so the node offers only protocol operators and the off-contract branch is unreachable from the node. On the branch left (a stored value that skipped the FilterGroup zod mirror), the renderer skips onChange and logs one console.error naming the operator; chosen over keeping a throw because that path is still reachable, so a throw would still crash a click. isAuthoredFilterGroup stays as the type guard; no any or cast on the path. Round 1 unchanged otherwise: renderer onChange typed FilterBuilderSchema['onChange'] and called with the group; Clause-② no; FilterBuilderSchema unchanged. Changeset paragraph and the PR body's What changes / Tests / Acceptance notes / Verification updated to match.",
"tests": "At head 904e58cf (39ae55ec + origin/main merge; main touched none of these files). New test file 5/5: host onChange gets the group; node-authored handler same; host-forwarded extraOperators ['exists','notExists'] gives a dropdown without 'Is set' / 'Is not set' (and with 'Is empty'); stored exists row skips onChange with one console.error; type-level expectTypeOf + @ts-expect-error. Narrowed run (declared): all 22 filter-builder* files in components __tests__ plus inline-locale-label-read-sites: Test Files 23 passed (23), Tests 392 passed (392). components type-check: VERDICT command-exit 0. Full components suite was green at round 1 (070b433f: 438 files / 4273 tests); full run at this head left to CI Test UI. Ablation 3 (extraOperators destructure removed, blob 5645f25cbc23->57047d781a2f): 'expected [ Equals, Does not equal, ...(11) ] to not include Is set', Tests 1 failed | 4 passed, restored blob == HEAD. Ablation 4 (skip-emit return replaced by a no-op marker, blob 5645f25cbc23->8a937573edee): 'expected vi.fn() to not be called at all, but actually been called 1 times', restored blob == HEAD; a first attempt was refused by ablation-replace before running (replacement was a substring of the anchor), a no-op, not a reading. Round-1 ablations 1 and 2 stand.",
"gates": "Re-derived at 904e58cf after merging origin/main (stale-tree warning cleared; same 34 commands). All 34 exit 0, captured before any pipe; pnpm --dir ui exec vitest run scripts/: Test Files 178 passed | 2 skipped (180). dispatch-gates --ran: 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED (a DERIVED zero). CI families: not re-run locally; in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "Round 2: 2 relay strokes as objectstack-fleet[bot]: (1) issue_patch PATCH /repos/objectstack-ai/objectstack/issues/26590 body (seat-ordered update of the throw/spread bullets), read back 12540 bytes sent = stored; (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/26382/comments. Round 1: pr_create (+ assignee) and report comment 6438144275. git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted: the objectstack-ai/cloud and hotcrm repositories were not searchable from this container for an event-shape binding or an extraOperators forwarder on a filter-builder node."
],
"deviations": "none"
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
bug·priority:p3·domain:ui·area:records·pm:queue(grade as filed) · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T01:52Z⛔ Not a claim, ⛔ not a dispatch.
FilterBuilderSchema.onChange(filter: FilterGroup)and the renderer's{ target: { name, value } }call disagree, and ananyhides the gap.{ target: { name, value } }shape is how a form field's change event looks, so check whether a form host binds afilter-buildernode through it. A search onmainfound no in-repofilter-buildernode outside docs and types. If nothing binds the event shape, the declared schema is the contract and the renderer passes theFilterGroup(Clause-②: no). If a binding needs the event shape, the type changes instead, which is a published-type change on@object-ui/types(Clause-②: yes). Say which in the PR.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_017iXjUm1rujwDk7f3KT1Pkk(seat); implementing card threadsession_01ADHkmNhEw4jVVQ4UTxrxMZAccount:
os-steveBranch:
claude/project-thread-eq0t7c(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree: the card thread's own container
Domain:
domain:uiSeat:
domain:ui#1File surface: the
filter-builderrenderer inui/packages/components/src/renderers/**,FilterBuilderSchemainui/packages/types/src/**(only if the measurement says the type changes), their tests, one root changeset (stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root.changeset/.Container & model:
S,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②: no (the expected side: the renderer adopts the declared
FilterGroup; if the measurement shows a binding needs the event shape, the seat amends this claim to yes with a BREAKING note)Metadata-protocol: none
Responsibility:
n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public ConsoleThread-read: 6431774981
Premise: holds (read-only premise check at
origin/main, 2026-10-27T08:18Z; spot-checked by the seat)Serial constraints cleared: none
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 26382, "status": "done", "branch": "claude/project-thread-eq0t7c", "pr": "https://github.com/objectstack-ai/objectstack/pull/26590", "session": "session_01ADHkmNhEw4jVVQ4UTxrxMZ", "premise_still_valid": true, "summary": "Step 1 found no in-repo host that binds a filter-builder node's onChange through the event shape: form fields resolve only field:-namespaced widgets, no SchemaRenderer host passing onChange renders this node, the metadata-admin 'filter-builder' widget is FilterBuilderWidget over FilterRuleLite[] and never this node, and the docs/examples declare or carry the FilterGroup shape. So the declared schema is the contract (Clause-②: no). The renderer's onChange is now typed FilterBuilderSchema['onChange'] and called with the group; handleChange is typed FilterBuilderRowGroup, and no any or cast is left on the path. FilterBuilderRowGroup is not assignable to FilterGroup (TS2345 on the opt-in exists/notExists operators), so the renderer narrows with a type predicate against VIEW_FILTER_OPERATORS and throws, naming the operator, when a row reached it off contract. FilterBuilderSchema in @object-ui/types is unchanged; one @object-ui/components patch changeset.", "tests": "At head 070b433f. New ui/packages/components/src/__tests__/filter-builder-renderer-onchange-26382.test.tsx: 4/4 (host onChange gets the group with no target; node-authored handler gets the same; off-contract exists row throws on window and onChange is not called; expectTypeOf parameter(0) EQUAL FilterGroup plus @ts-expect-error for the old event handler). components type-check (tsc --noEmit && tsc -p tsconfig.test.json): VERDICT command-exit 0. Full components suite under the lock: Test Files 438 passed | 1 skipped (439), Tests 4273 passed | 24 skipped (4297), VERDICT command-exit 0. Ablation 1 via scripts/ablation-replace.mjs (anchor onChange(group); x1->x0, blob 96162878fc0f->ece34562e098, replaced with the old target-event call): Tests 2 failed | 2 passed ('expected { target: { name: filters, ...(1) } } to not have property target'; 'Target cannot be null or undefined.'), restored blob == HEAD, git diff HEAD empty. Ablation 2 (guard replaced by false as boolean): tsc TS2345 'FilterBuilderRowGroup is not assignable to parameter of type FilterGroup' down to 'Type exists is not assignable', restored blob == HEAD. No vitest alias question: the test imports the renderer relatively, so no build sits between mutation and run.", "gates": "dispatch-gates --commands at 070b433f (after merging origin/main; the first derivation flagged a stale tree) listed 34 commands; all 34 exit 0, each captured before any pipe (check-adr-0087-registration: no declared-breaking changeset; check-empty-changeset: 1 declaring changeset added; pnpm --dir ui exec vitest run scripts/: Test Files 178 passed | 2 skipped (180)). dispatch-gates --ran: 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED (a DERIVED zero, all 34 recorded an exit code). Also pnpm --dir ui run check:new-line-citations: 0 new citation(s), exit 0. CI families: in_progress at report time (draft just opened); not re-run locally.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 relay strokes as objectstack-fleet[bot]: (1) pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST /repos/objectstack-ai/objectstack/issues/26590/assignees os-steve), read back 10242 bytes sent = stored; (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/26382/comments. git push is not a REST write. Card reads were single-card REST GETs.", "open_questions": [ { "question": "What should the renderer do when the builder hands it a row whose operator a FilterGroup cannot carry (the opt-in exists/notExists, reachable only off contract: a host forwarding extraOperators through the undeclared prop spread, or a stored value the FilterGroup zod mirror refuses)?", "options": [ "A throw an error naming the operator (shipped): loud, onChange not called, the declared type stays true", "B skip the emit and log: quiet, the builder shows the edit and the host never hears it", "C also stop forwarding extraOperators to FilterBuilder, so only refused stored input reaches the branch: a behaviour change to an undeclared host prop, beyond this card" ], "recommendation": "A, because handing the row on would make the declared FilterGroup false and dropping it loses the edit in silence; no in-repo host reaches the branch." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed: the renderer spreads undeclared host props (extraOperators included) into FilterBuilder, so a host can grant an opt-in operator that FilterBuilderSchema does not declare; no in-repo host does. Recorded in the PR's Acceptance notes.", "carrier: none · noted: the objectstack-ai/cloud and hotcrm repositories were not searchable from this container for an event-shape binding." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
Round 2 (FAIL round on PR #26590), superseding comment 6438144275.
{ "issue": 26382, "status": "done", "branch": "claude/project-thread-eq0t7c", "pr": "https://github.com/objectstack-ai/objectstack/pull/26590", "session": "session_01ADHkmNhEw4jVVQ4UTxrxMZ", "premise_still_valid": true, "summary": "FAIL round 2 (seat ruling on the open question): the renderer now takes extraOperators out of the host spread and never forwards it to FilterBuilder, so the node offers only protocol operators and the off-contract branch is unreachable from the node. On the branch left (a stored value that skipped the FilterGroup zod mirror), the renderer skips onChange and logs one console.error naming the operator; chosen over keeping a throw because that path is still reachable, so a throw would still crash a click. isAuthoredFilterGroup stays as the type guard; no any or cast on the path. Round 1 unchanged otherwise: renderer onChange typed FilterBuilderSchema['onChange'] and called with the group; Clause-② no; FilterBuilderSchema unchanged. Changeset paragraph and the PR body's What changes / Tests / Acceptance notes / Verification updated to match.", "tests": "At head 904e58cf (39ae55ec + origin/main merge; main touched none of these files). New test file 5/5: host onChange gets the group; node-authored handler same; host-forwarded extraOperators ['exists','notExists'] gives a dropdown without 'Is set' / 'Is not set' (and with 'Is empty'); stored exists row skips onChange with one console.error; type-level expectTypeOf + @ts-expect-error. Narrowed run (declared): all 22 filter-builder* files in components __tests__ plus inline-locale-label-read-sites: Test Files 23 passed (23), Tests 392 passed (392). components type-check: VERDICT command-exit 0. Full components suite was green at round 1 (070b433f: 438 files / 4273 tests); full run at this head left to CI Test UI. Ablation 3 (extraOperators destructure removed, blob 5645f25cbc23->57047d781a2f): 'expected [ Equals, Does not equal, ...(11) ] to not include Is set', Tests 1 failed | 4 passed, restored blob == HEAD. Ablation 4 (skip-emit return replaced by a no-op marker, blob 5645f25cbc23->8a937573edee): 'expected vi.fn() to not be called at all, but actually been called 1 times', restored blob == HEAD; a first attempt was refused by ablation-replace before running (replacement was a substring of the anchor), a no-op, not a reading. Round-1 ablations 1 and 2 stand.", "gates": "Re-derived at 904e58cf after merging origin/main (stale-tree warning cleared; same 34 commands). All 34 exit 0, captured before any pipe; pnpm --dir ui exec vitest run scripts/: Test Files 178 passed | 2 skipped (180). dispatch-gates --ran: 34 derived famil(ies) accounted for, 34 run, 0 NOT-MEASURED (a DERIVED zero). CI families: not re-run locally; in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "Round 2: 2 relay strokes as objectstack-fleet[bot]: (1) issue_patch PATCH /repos/objectstack-ai/objectstack/issues/26590 body (seat-ordered update of the throw/spread bullets), read back 12540 bytes sent = stored; (2) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/26382/comments. Round 1: pr_create (+ assignee) and report comment 6438144275. git push is not a REST write.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted: the objectstack-ai/cloud and hotcrm repositories were not searchable from this container for an event-shape binding or an extraOperators forwarder on a filter-builder node." ], "deviations": "none" }Generated by Claude Code