Uh oh!
There was an error while loading. Please reload this page.
fix(spec): type ActionEngineFacade.find's second parameter as a FilterCondition, not an ObjectQL envelope (#14175) - #15118
Conversation
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 073756227d292a523b4c5547e38305b9b05562fa && git checkout 073756227d292a523b4c5547e38305b9b05562fa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 97a22639b44ae04693c9999f13d8a9aa985ed810 81e79ab343a6b3dbd78e14c483d929af8724564d && git checkout -B drift-repro 97a22639b44ae04693c9999f13d8a9aa985ed810 && git merge --no-ff 81e79ab343a6b3dbd78e14c483d929af8724564d
node scripts/docs-audit/affected-docs.mjs --json 97a22639b44ae04693c9999f13d8a9aa985ed810
|
Contract review (Clause ②) — PASS · ACCEPTReviewer of record: the Head and window (readings at 2026-09-04T01:32Z)
The narrowing, verified in the diff
Report checklist
Seat rulings on the report's open questions (open to the maintainer's veto)
Out-of-scope group (sweep criterion: neighbouring facade members whose declared type differs from what the runtime facade produces or accepts)
LandingAll-green on
Generated by Claude Code |
Landing provenance — Landing window readings, all on head
Sequence: ready → squash auto-merge → Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14175
Clause ②: yes — path limb:
packages/spec/src/**(ui/action-params.zod.tsis a Clause ② SUSPECT surface perdispatch-gates --tier, no path-derived mandate); content limb yes: a published type narrows —ActionEngineFacade.find's second parameter moves from an open string-keyed record toFilterCondition, so what a caller may pass changes at compile time.needs:contract-reviewhung on this PR and on #14175 in the same stroke. Draft; not to be flipped ready by this seat.Remedy 1 of the card only (type the parameter, document it). Remedy 2 (the runtime throwing on an envelope) is the
domain:clilane's and is not taken; remedy 3 (the facade as a shipped test double) is a card of its own and is not taken.What changed
packages/spec/src/ui/action-params.zod.ts—ActionEngineFacade.find(object, query)with an open record becomesfind(object, filter: FilterCondition), the publishedQueryAST.wheretype (data/filter.zod.ts), renamedquery→filter. The member's doc comment states: it is a FILTER (thewherehalf), not an ObjectQL envelope; the runtime —buildActionEngineFacade'sfindarm,packages/runtime/src/action-execution.ts:1183-1187on369da918, read-only — wraps a non-empty filter as{ where: filter }and passes an EMPTY filter ({}) through unwrapped (the unfiltered read); an envelope becomes{ where: { where: … } }, matches nothing and returns[]without error; and exactly what the type refuses and does not refuse. The facade docblock gains one sentence pointing at it.insert/update/deleteandActionHandlerContextuntouched.packages/spec/src/ui/action-params.test.ts— four pins, in the compiled test program (tsc -p tsconfig.test.json --listFileslists the file): the exported type-level pinFindFilterIsFilterCondition(strictEqbetween the declared second slot offind, read viaParameters, andFilterCondition; measured: the same assertion against the old open record is a TS2344); a positive control (implicit equality, operators,$and/$or/$not, and the empty filter); a compile-time refusal pin under@ts-expect-errorfor what the type refuses; and a MEASURED-GAP pin recording that the exact envelope mistake still compiles (below).content/docs/ui/actions.mdx— a callout under the registered-handler example carrying the filter-not-envelope sentence and both limbs. It is the only hand-written page showing the facade;git grep -n "engine.find" origin/main -- content/docshits are DataEngine / ObjectQLengine.find(envelope-taking), notctx.engine, and were left alone..changeset/action-engine-facade-find-filter.md—@objectstack/specpatch. Precedent: [finding] tsc does not police unknown keys on plugin action-param literals — the only enforcement is the ActionParamSchema strict parse at module load #12615 (.changeset/action-param-excess-keys-compile.md, a compile-layer-only narrowing "shipped as patch because no working code changes meaning"); theActionHandlerContext.sessionnarrowing in 17.0.0 rode a minor changeset only because it landed together with the newActionSessionSchema. Non-breaking, so no ADR-0087 marker is owed (check-adr-0087-registrationagrees). The contract review judges the level.Measurements the card asked for
FilterConditionIS the shape the runtime'swhereaccepts.QuerySchema.whereisFilterConditionSchema.optional()(data/query.zod.ts:522);EngineQueryOptionsSchema.where— whatql.find(object, options)takes — is a union of an open string-keyed record andFilterConditionSchema(data/data-engine.zod.ts:98), soFilterConditionis the named member of that union and the narrowest published filter type the slot admits; the facade's old open record was the other member. It admits every shape handlers legitimately pass (field-keyed equality, operators,$and/$or/$not,{}) — the positive control compiles.FilterConditionadmitswhereas a key. Measured with a scratch tsc program before the edit:FilterCondition's string index signature admitsanyper key, so{ where: { position_code: 'qa_lead' } }— the exact mistake — compiles, and so does the nested{ where: { where: … } }. What the type DOES refuse: a primitive (TS2322),$and/$orthat are not arrays ({ $and: 'active' },{ $or: { active: true } }), and$notthat is not a filter ({ $not: 'archived' }) — the three logical-operator cases and the primitive are the@ts-expect-errorpins (an array under$notis admitted and is not pinned). So the reporting app's mistake becomes a documented mistake, not a compile error; the doc comment is the contract of record, and the MEASURED-GAP pin records the admission so that a later narrowing updates the sentence together with the type. Not widened to force the bar (per the dispatch); thewhere-refusing intersection that would close it is raised below for the contract review.Before / after (angle brackets spelled as entities so the body sanitizer leaves them alone)
Before (
369da918):After (this PR; the member doc comment abbreviated to its load-bearing sentences — the file carries the full text):
Consumer direction (READ and typecheck only — no edits outside
packages/spec)ActionEngineFacade/ActionHandlerContext/ActionHandleroutsidepackages/spec(excludingdist, CHANGELOGs, docs): zero bygit grep.packages/runtimenames the facade only in comments, andbuildActionEngineFacadeis declared(…): anyaround an unannotated object literal, so the narrowed member cannot reach it;packages/objectqlmentionsActionHandlerin one comment.pnpm --filter @objectstack/spec build:pnpm turbo run typecheck --filter=@objectstack/runtime --filter=@objectstack/objectql --concurrency=2— the prefix direction, i.e. these two CONSUMERS — 31 tasks successful (29 cached closure builds + the 2 typechecks), exit 0. The reroute condition (the runtime's facade construction no longer compiling) did not fire.examples/app-todo/src/actions/task.handlers.tsdeclares its ownActionContextwith the oldqueryspelling and an array-takingdelete— is out of scope here and filed as ActionEngineFacade.delete declares id: string while the runtime facade accepts string | string[] and examples/app-todo relies on the array form through a hand-rolled context type #15117 (the neighbouringdeletemember's declared-vs-produced gap;find's copy is mentioned there).Reverse verification (one leg, from the committed state
81e79ab3)Prediction written before the run: the type-level pin turns red (TS2344); the three logical-operator refusal pins turn red (TS2578 — the directive goes unused because the open record admits them); the primitive refusal, the positive control and the measured-gap pin stay green. Mutation:
filter: FilterConditionreverted to the open record plus the then-deadimport typeremoved, inaction-params.zod.ts; confirmed on disk by anchored counts (removed text 0, injected text 1, import line 0) and blobf6cd8405vs HEADdd75f9a9.tsc -p tsconfig.test.json: 265 errors against the 261-error ledger baseline — exactlyaction-params.test.ts(416,51): TS2344andTS2578at446 / 448 / 450; nothing else moved. No build leg was owed: the test program compiles the pin againstsrcthrough a relative import, not throughdist. Restore:git checkout HEAD --the absolute path, under an EXIT/INT/TERM trap; proven bygit hash-objectequal to theHEAD:blobdd75f9a9…and by an emptygit diff HEAD/ emptygit status --porcelain. Side effect worth knowing: the restore refreshed the file's mtime, so the dist-reading spec gates reported "dist OLDER than src" until one morepnpm --filter @objectstack/spec build; the readings below are from after it.Verification record — head
81e79ab3(each exit captured before any pipe; verdict lines quoted from the gates)pnpm --filter @objectstack/spec buildthroughscripts/pm/os-verify-lock.sh—VERDICT command-exit 0,check-dts-emitted: 34/34 declared declaration file(s) present(run before the union and again after the reverse leg).pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/action-params.test.ts—Test Files 1 passed (1) · Tests 29 passed (29)(25 existing + 4 new), exit 0.pnpm --filter @objectstack/spec typecheck(tsc --noEmit+check:scripts-typecheck+check:test-typecheck) — exit 0;check:test-typecheck: OK — 54 file(s) / 261 error(s) / 145 pinned signature(s)— the shrink-only ledger is unchanged, so the new test contributes zero errors;--listFileslistssrc/ui/action-params.test.tsandsrc/ui/action-params.zod.ts.pnpm --filter @objectstack/spec check:generated—✓ All 15 generated artifacts are up to date.(nothing regenerated: the interface member's docblock does not render onreferences/ui/action-params.mdx, andapi-surface/records thatActionEngineFacadeexists, not its member signatures;check:react-declaration-parity"cannot run here" as always).check:api-surface—public API surface + factory signatures unchanged ✓;check:docs—229 generated files in sync with packages/spec;check:strictness-ledger—docs/audits/2026-07-unknown-key-strictness-ledger.counts.md is current — 439 site(s) measured(no new object site, nothing to update);check:export-origins—5242 exports across 17 entry points resolve exactly as recorded.--no-inline-configover the two edited TypeScript files — exit 0, no output.node scripts/check-system-context-census.mjs(+--self-test) —OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve; nothing inaction-params.zod.tsis anchored, so no--fixwas owed.node scripts/pm/dispatch-gates.mjs --commands(no paths; the change set derived from the merge base) at81e79ab3→ 89 commands, all run; every one exit 0 except the following, which are NOT MEASURED by the gates' own text (exit 3,PREREQUISITE NOT MET), not red:scripts/check-test-completeness.mjs(grades a savedturbo run testlog; none exists locally),pnpm check:dual-build-cjs-loads(38 packages have nodist/),pnpm check:type-check-debt(--re-measureneeds 4 unbuilt workspace deps of the ledgered packages; none of the ledgered packages imports the narrowed type).check:skill-examplesfirst reported the client-SDK surface unmeasured (noclient-reactdist); afterturbo run build --filter=@objectstack/client-react --filter=@objectstack/clientit reads✅ 257 prose examples type-check across 3 surface(s), exit 0.pnpm lint(repo-wide eslint) is CI's run; the local reading is the two-file eslint above.grep -naPover the four edited files) clean;pnpm check:nul-bytes—OK (scanned 8224 text file(s) … no raw ASCII control bytes).For the contract review
FilterConditionintersected with{ where?: never }would make the exact{ where: … }mistake a TS2322 at the call site (measured shape:anyintersected withneverisnever).whereis not a reserved field name anywhere inpackages/spectoday (grep), so this is a new vocabulary claim ("no object has a field namedwhere") rather than a re-statement of an existing one — which is why it is raised here and not shipped. Recommendation: land this PR as is (the dispatch's shape), and decide the intersection as its own small follow-up if the review wants the full bar.patchper the [finding] tsc does not police unknown keys on plugin action-param literals — the only enforcement is the ActionParamSchema strict parse at module load #12615 precedent;minorif the review reads a parameter-type narrowing on a published interface as a public-surface move regardless of behaviour.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i
Generated by Claude Code