Skip to content

finding(spec): ActionEngineFacade.find's FilterCondition slot still admits the ObjectQL envelope { where: … } at compile time — closing the bar is a vocabulary claim (no field named where) the spec does not declare #15124

Description

@claude

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions