Repository navigation
feat(verify): the handle fronts the automation engine's condition evaluator - #22553
Conversation
…e handle (WIP) stack.automation.evaluateCondition(condition, variables) calls the booted kernel's own automation service's evaluateCondition with the condition as given and the variables as the Map the engine takes, and answers its boolean. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
…tion Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 3 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 4ceead9431e96d5f3d3f2b02c7dd82563c8d28d1 && git checkout 4ceead9431e96d5f3d3f2b02c7dd82563c8d28d1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b53b949a1518ccd5bbfb2f3b8a6fac4eaa9c2da2 15827331397c73330476967e876ed58e31a6f9eb && git checkout -B drift-repro b53b949a1518ccd5bbfb2f3b8a6fac4eaa9c2da2 && git merge --no-ff 15827331397c73330476967e876ed58e31a6f9eb
node scripts/docs-audit/affected-docs.mjs --json b53b949a1518ccd5bbfb2f3b8a6fac4eaa9c2da2
|
…ation member The hand-built VerifyStack literal in rls-runner.test.ts types every handle member it does not model as `never`; the handle's new `automation` member joins them, so the dogfood typecheck compiles again. Claude-Session: https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: PR #22553 ( Check-runs on the head, read at 2026-10-10T01:09Z: 42 of 42 completed, 37 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #22301
Clause-②: yes (widening)
Item 8 of the card ("
automation.evaluateConditionis not fronted"): the verify handle gains a door to the automation engine's own condition evaluator. Items 5, 6 and 7 and item 1's remaining composition gap stay open on the card; this PR touches none of them.Premise, read on
origin/main86bf9ed7d5before any editAutomationEngine.evaluateConditionexists atpackages/services/service-automation/src/engine.ts:12225. It takes a condition (a string, or an envelope{ dialect?, source?, ast? }) and aMapof variables, and returns a boolean.IAutomationService(packages/spec/src/contracts/automation-service.ts:739) has noevaluateConditionmember (grep: 0 hits in that file).automationservice is the engine instance itself.AutomationServicePluginregistersthis.engineunder'automation'(plugin.ts:975), and that is the only producer of the slot inpackages/(grep: 1 hit).contextFor,hooks.run,hooks.updateWhere,validate,flows.run,flows.resume,actions.run,seed,rows,metadataandtenancy. None reaches the evaluator (evaluateConditioninpackages/verify: 0 hits).f0afcbd:conditionHolds(test/helpers/verify-stack.ts:184-:188) callsstack.kernel.getService('automation').evaluateCondition(…)by hand. It wraps a string condition as{ dialect: 'cel', source }first. It has 28 call sites in 5 test files.What lands
stack.automation.evaluateCondition(condition, variables)resolves the booted kernel'sautomationservice at call time (kernel.getServiceAsync, the lookup the handle's engine doors use). It calls the service's ownevaluateConditionand resolves with its boolean, unchanged. There is no second evaluator and no re-derived dialect rule, and no caller is resolved, because the evaluator takes none.conditionhas the engine's own parameter type, read off the method (ParametersofAutomationEngine['evaluateCondition']), so the door accepts exactly the shapes the engine accepts. It is handed over as given. The engine's own flow sites hand the evaluator an envelope: the start gate wraps a stringconditionas{ dialect: 'cel', source }, thedecisionexecutor wraps each branchexpressionthe same way, and an edge's condition already is one. A bare string gets the engine's sniff (CEL unless it carries a{var}hole), the reading a screen field'svisibleWhengets. The JSDoc says which to pass.variablesis a plain object. Each own key becomes one entry of theMapthe engine takes.false. No catch is added.SERVICE_NOT_REGISTEREDerror, whoseserviceNameis'automation'(isServiceNotRegisteredErrorfrom@objectstack/core). No error code is minted in@objectstack/verify, so its error-code ledger row is unchanged and nothing inpackages/specchanges. The remedy (automation: true, or the app'srequires: ['automation']) is in the JSDoc and the README.The name.
automationis the kernel slot the door resolves, andevaluateConditionis the engine method it calls, so the door reads as the call it makes, the waytenancy()is named after its slot. Theflowsgroup was the other candidate, but its doors are dispatcher routes run as a caller, and this one is an engine call with no caller.The door is named in
handle.ts's door roster, inVerifyStack's member list (harness.ts, docblock only) and in the README's handle section, API list and theautomationboot option..changeset/22301-verify-evaluate-condition-door.md:@objectstack/verifyminor,Clause-②: yes (widening).packages/qa/dogfood/test/rls-runner.test.ts(+1, patch round at1582733139). Its hand-builtVerifyStackliteral now stubsautomation: undefined as neverbeside the other handle members it does not model. Without it,@objectstack/dogfood#typecheckfailed with TS2741 on79e9053e50. A typed grep acrosspackages/**,examples/**,apps/**,skills/**andcontent/docsfound no other hand-builtVerifyStack/VerifyHandleliteral.Evidence, at HEAD
79e9053e50packages/verify/src/handle.automation-door.test.ts, 6 / 6. Two boots: one whose app declaresrequires: ['automation'], and one without the service.Mapwhose entries equal the variables, and its return value is the door's answer.record,previous) is true once and false twice, equal to the engine called directly.{amount} > 100keeps the template dialect (true), and the spy sees the string as given. The same text in a CEL envelope is the brace trap: the door rejects with the same message the engine throws directly.VerifyRefusal, nocodeadded.SERVICE_NOT_REGISTEREDerror (code,serviceName: 'automation', the brand predicate), with the same message the kernel gives when asked directly.node scripts/ablation-replace.mjs, anchor hit 1 -> 0, blob changed,git diff HEADempty after restore). The subject resolves tosrc/(relative imports, no packageexports), so no rebuild is involved.condition failed to evaluate as CEL: Expected COLON, got RBRACE).42as the condition redstscwith TS2345, naming the engine's parameter type. The probe was removed and the tree is clean.@objectstack/verifyin full: 26 files / 209 tests pass, run at0aac167d63. The one later commit changes a docblock only. Typecheck exit 0, with the new test file in the test program (1 of 26 test files listed by--listFiles).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat79e9053e50derives 64 families. 63 ran, each exit 0.pnpm check:dual-build-cjs-loadsis NOT MEASURED: it needs a whole-workspace build, which this dispatch excludes, so CI runs it.--ranreconciles 64 of 64, with 0 UNRUN. From the artifact-roster block, 49 of its 52 commands ran, each exit 0. That includescheck:error-code-provenance: 337 stamp sites, each listed or waived, and nothing new under@objectstack/verify. The other three are the PR-context guards. They run against this PR once it exists, and their readings go in the card report.Patch round, at
1582733139:pnpm --filter @objectstack/dogfood typecheckandpnpm --filter @objectstack/verify typecheckexit 0; the door file is 6/6.dual-build-cjs-loads), 0 UNRUN; the rosters were 51; CI showed 32 success and 3 skipped.Acceptance notes
Noted here, not filed:
automationslot as the engine class, becauseevaluateConditionis not onIAutomationService. This is the same reasonenginetypes theobjectqlslot asObjectQL. Another provider under that slot would answer aTypeErrorat this door. There is one producer of the slot inpackages/today, so this is an observation with no reach.flows.*is a route door, and gets the route's envelope for an empty slot (the dispatcher's501, read frompackages/runtime/src/domains/automation.ts:2374, not measured on a boot here). This door is an engine door, and gets the kernel's in-process error. That is the split the handle already documents between its route doors and its engine doors.For the
repo:hotcrmseat ("done when")conditionHolds(stack, condition, vars)can move toawait stack.automation.evaluateCondition(condition, vars). Two porting differences apply:{ dialect: 'cel', source }), as the helper did;filtercall site ataccount-name-normalized-match.test.ts:600needsPromise.all.Generated by Claude Code