Filed by the domain:spec PM seat (session session_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at 2026-09-04T02:00Z out of the #14175 contract review (PR #15118, merged f794e4e5) — the report's open question 1, ruled "land as is, file for triage". Observation, not a defect repro. finding: awaiting first-touch grading (a vocabulary claim ⇒ likely a decision, not a queue card).
Measured
ActionEngineFacade.find(object, filter: FilterCondition) (packages/spec/src/ui/action-params.zod.ts, since #14175) types the second parameter as the published where half. FilterCondition (packages/spec/src/data/filter.zod.ts, the export type around :1362) carries a string index signature so that any field name may be a key — and where is a string. So the exact mistake #14175 documents, passing the ObjectQL envelope { where: { position_code: 'qa_lead' } }, still compiles; the runtime wrap (buildActionEngineFacade's find arm, packages/runtime/src/action-execution.ts) then produces { where: { where: … } }, which matches no row and resolves to [] with no error. PR #15118 records this as the MEASURED-GAP pin in packages/spec/src/ui/action-params.test.ts and states it in the doc comment, the changeset and content/docs/ui/actions.mdx: the type refuses a primitive and a mistyped $and / $or / $not; the doc comment is the contract of record for the envelope case.
What would close the bar, and why it was not done in #14175
Intersecting the slot with an object type whose where key is optional and typed never (FilterCondition & { where?: never }) turns the envelope into a TS2322 at the call site and flips the MEASURED-GAP pin into a refusal pin — a one-line type change. It was not taken in PR #15118 because it asserts a vocabulary fact the spec declares nowhere: that no object may carry a field named where. Today where is not a reserved field name anywhere in packages/spec (measured by the dev: no reservation, no refusal), so the intersection would be a NEW claim on the field-name vocabulary, not a restatement — a contract decision (Clause ② narrowing of what a handler may type), not a rider on a typing fix.
Options for the grader
- A — declare
where (and by the same argument fields / orderBy / limit, the envelope's other keys) as names a facade filter may not carry at the top level, and land the intersection with a refusal pin. Narrows the facade's compile-time accept set; changes nothing at runtime. - B — leave as is: the doc comment + the docs callout are the contract; the runtime keeps wrapping. Zero cost; the silent-
[] class stays writable by an untyped caller and by a typed caller who ignores the doc. - C — a runtime guard in the wrap (refuse a filter whose top-level keys are exactly the envelope's) —
domain:cli territory, not this lane's; listed for completeness, not proposed here.
Consumers: zero importers of ActionEngineFacade outside packages/spec on origin/main (a5485500); examples/app-todo passes bare filters; a hand-rolled ActionContext copy in that example is #15117's subject.
Dedup: REST search for open issues naming ActionEngineFacade at 2026-09-04T02:00Z — see the seat's landing note on #14175 for the reading; #15117 (the delete member's id: string vs string | string[]) is the neighbouring finding and is not this one.
Generated by Claude Code
Filed by the
domain:specPM seat (sessionsession_0174WZTU6XcFcS7g2kykC53i, seat post #6017) at 2026-09-04T02:00Z out of the #14175 contract review (PR #15118, mergedf794e4e5) — the report's open question 1, ruled "land as is, file for triage". Observation, not a defect repro.finding: awaiting first-touch grading (a vocabulary claim ⇒ likely a decision, not a queue card).Measured
ActionEngineFacade.find(object, filter: FilterCondition)(packages/spec/src/ui/action-params.zod.ts, since #14175) types the second parameter as the publishedwherehalf.FilterCondition(packages/spec/src/data/filter.zod.ts, theexport typearound:1362) carries a string index signature so that any field name may be a key — andwhereis a string. So the exact mistake #14175 documents, passing the ObjectQL envelope{ where: { position_code: 'qa_lead' } }, still compiles; the runtime wrap (buildActionEngineFacade'sfindarm,packages/runtime/src/action-execution.ts) then produces{ where: { where: … } }, which matches no row and resolves to[]with no error. PR #15118 records this as the MEASURED-GAP pin inpackages/spec/src/ui/action-params.test.tsand states it in the doc comment, the changeset andcontent/docs/ui/actions.mdx: the type refuses a primitive and a mistyped$and/$or/$not; the doc comment is the contract of record for the envelope case.What would close the bar, and why it was not done in #14175
Intersecting the slot with an object type whose
wherekey is optional and typednever(FilterCondition & { where?: never }) turns the envelope into a TS2322 at the call site and flips the MEASURED-GAP pin into a refusal pin — a one-line type change. It was not taken in PR #15118 because it asserts a vocabulary fact the spec declares nowhere: that no object may carry a field namedwhere. Todaywhereis not a reserved field name anywhere inpackages/spec(measured by the dev: no reservation, no refusal), so the intersection would be a NEW claim on the field-name vocabulary, not a restatement — a contract decision (Clause ② narrowing of what a handler may type), not a rider on a typing fix.Options for the grader
where(and by the same argumentfields/orderBy/limit, the envelope's other keys) as names a facade filter may not carry at the top level, and land the intersection with a refusal pin. Narrows the facade's compile-time accept set; changes nothing at runtime.[]class stays writable by an untyped caller and by a typed caller who ignores the doc.domain:cliterritory, not this lane's; listed for completeness, not proposed here.Consumers: zero importers of
ActionEngineFacadeoutsidepackages/speconorigin/main(a5485500);examples/app-todopasses bare filters; a hand-rolledActionContextcopy in that example is #15117's subject.Dedup: REST search for open issues naming
ActionEngineFacadeat 2026-09-04T02:00Z — see the seat's landing note on #14175 for the reading; #15117 (thedeletemember'sid: stringvsstring | string[]) is the neighbouring finding and is not this one.Generated by Claude Code