Uh oh!
There was an error while loading. Please reload this page.
feat(spec): refuse an auto-launched flow whose stack omits requires: ['triggers'] - #14330
Conversation
…['triggers'] (#14153) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
…utomation exports; declare triggers on the lint tier fixture (#14153) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
📓 Docs Drift CheckThis PR changes 2 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
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 3b3c9e059159c1ea382daa3f44ee0f2f220f0ac5 && git checkout 3b3c9e059159c1ea382daa3f44ee0f2f220f0ac5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f645d6f8879f5e868b8c0aac978dc0db92552739 24aec0a0a2ffe42e339eded184f60ffff85569ac && git checkout -B drift-repro f645d6f8879f5e868b8c0aac978dc0db92552739 && git merge --no-ff 24aec0a0a2ffe42e339eded184f60ffff85569ac
node scripts/docs-audit/affected-docs.mjs --json f645d6f8879f5e868b8c0aac978dc0db92552739
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14153
What
defineStacknow refuses a stack that declares an auto-launched flow (record_change/schedule/time_relative/api) whilerequiresomits'triggers'— a sibling ofvalidateHierarchyScopeCapability, in the same throw-site family:The wording is the automation engine's boot-audit line (
getTriggerBindingAudit), moved from post-deploy to author time — no second vocabulary.triggersis one membership test (one token installs all four kinds), no per-kind table.Ruling premise — verified (in-tree corpus census at
a39b02a6)requiresexamples/app-crm['ui', 'automation']convert-lead.flow.tsistype: 'screen'defineStackimport through the built dist,flows=1 types=["screen"])examples/app-showcase'triggers'(objectstack.config.ts:107)schedulepackages/create-objectstackblank template['automation']src/objectsonly)examples/app-todoschedule('0 8 * * *'), 1 ×record_change, 1 ×screenpackages/**/objectstack.config.ts(5)No in-tree stack declares a
record_change/schedule/time_relative/apiflow and launches it only by hand (notype: 'flow'action targeting such a flow, no docs saying so). The premise holds; the reroute condition is not met — the remedy is indefineStack, nothing was built inpackages/lintbeyond the declared import-site edit.Absent
requires— measured Outcome Bpackages/cli/src/commands/serve.ts: an absentrequiresreads as[](:2295), the CLI appends only its convenience defaults —emailiffauth,mcp,pinyin-search, the always-on slate (PLATFORM_ALWAYS_ON_CAPABILITIES=queue, job, cache, settings, email, storage, sms, sharing, messaging, analytics— neitherautomationnortriggers),queue/job— and the capability resolver loads a provider only for tokens in that list (for (const cap of requires):3985;automation:1460,triggers:1506 are entries ofServe.CAPABILITY_PROVIDERS; no other block imports@objectstack/service-automationor@objectstack/trigger-*).os devforwards to aservechild. So a stack that declares nothing gets no trigger — and no engine. ⇒ the validator fires on absentrequirestoo, andexamples/app-todo(whosetask-completion-trigger.test.tshand-assemblesRecordChangeTriggerPluginprecisely because itsrecord_changeflow is meant to fire) now declaresrequires: ['automation', 'triggers']. Not booted live: booting app-todo needs the whole CLI closure built in this worktree; the reading above is from the resolver's own source, line-cited.Predicate — hoisted into spec (single source)
resolveFlowTriggerKind(packages/spec/src/automation/flow-trigger-kind.ts, exported from@objectstack/spec/automation) mirrors the engine'sresolveTriggerBindingchain, kind only, in its precedence (timeRelativedescriptor outranks a siblingschedulecadence).@objectstack/lint'svalidate-flow-trigger-readiness.tsnow readsisAutoTriggered = resolveFlowTriggerKind(flow) !== undefined— byte-identical to the six-term disjunction it replaces (the resolver answers a kind exactly when one of those terms held). The one documented divergence from the engine is the array-formtriggerType(unsupported, #3457): the engine routes it to the record-change trigger only so the trigger can refuse it loudly at bind time, lint 1d already reports it as anerror, and lint's predicate never counted it — so the resolver answers no kind for it, which is what keeps the lint edit identical by construction rather than by test coverage. Lint's suite (92 files / 2685 tests) is the proof; see Tests.Fixture triage
packages/lint/src/authoring-rule-input-tier.test.ts— itsflowStackuses ascheduleflow only as a vehicle for a parse-time default; the new refusal tripped it. Declaredrequires: ['triggers']on the fixture (the flow's auto-trigger is incidental; the test's meaning is unchanged).defineStacktest consumer with flows (git grep -ln defineStack -- '**/*.test.ts'→ 85 files, 20 withflows) either declares no auto-triggered flow, declarestriggers, or does not go throughdefineStackfor it — the spec (451 files) and lint (92 files) suites ran green after the two repairs above.Downstream
Out of scope, filed as #14328 (
finding, open): the automation engine'sresolveTriggerBindingkeeps a private copy of the trigger-kind chain the spec resolver now mirrors — hoisting the kind ontoresolveFlowTriggerKindis aservice-automationchange, not this card's.objectstack-ai/duly'stest/trigger-capability.test.tsstopgap can be deleted in that repo once this lands and the pin moves — a follow-up there, not here.Files
packages/spec/src/automation/flow-trigger-kind.ts(+ test, + barrel export, + regeneratedapi-surface/automation.jsonandexport-origins/automation.json)packages/spec/src/stack.zod.ts—validateTriggerCapability+ wiring after the hierarchy siblingpackages/spec/src/stack-requires.test.ts— thedefineStack trigger capability validationsuitepackages/lint/src/validate-flow-trigger-readiness.ts— import-site edit only (+ one comment that named the old predicate)packages/lint/src/authoring-rule-input-tier.test.ts— fixture declares the tokenexamples/app-todo/objectstack.config.ts— corpus repaircontent/docs/permissions/capabilities.mdx,content/docs/automation/flows.mdx— one table row, one paragraph.changeset/define-stack-trigger-capability-refusal.md—@objectstack/specminor (BREAKING accept-set narrowing under the launch-window convention; ADR-0087not-required (no-migration-prescription)— the refusal names the one-line fix, nothing is renamed or removed),@objectstack/lintpatchTests
All runs through
scripts/pm/os-verify-lock.sh; verdicts quoted from each runner's own summary line, exit codes captured before any pipe. Head for every re-run:24aec0a0.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2(whole suite, at66d5d580):Test Files 2 failed | 449 passed (451)·Tests 3 failed | 12084 passed (12087)— all three failures were the staleexport-origins/automation.json(the module exports these at runtime and export-origins does not record them), regenerated withcheck:generated --fixtogether withapi-surface/automation.json(the only two artifacts it proved stale). Targeted re-run at24aec0a0ofscripts/export-origins.test.ts src/automation/state-machine.test.ts src/stack-requires.test.ts src/automation/flow-trigger-kind.test.ts:Test Files 4 passed (4)·Tests 64 passed (64).pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2(whole suite):Test Files 1 failed | 91 passed (92)·Tests 1 failed | 2684 passed (2685)— the one failure was the tier fixture (see Fixture triage). Targeted re-run at24aec0a0ofsrc/authoring-rule-input-tier.test.ts src/validate-flow-trigger-readiness.test.ts:Test Files 2 passed (2)·Tests 80 passed (80)— the readiness suite (60 cases, untouched) is the byte-identical proof for the import-site edit.pnpm --filter @objectstack/spec typecheckexit 0 (check:test-typecheck: OK — … 54 file(s) / 262 error(s) / 146 pinned signature(s), ledger unchanged);pnpm --filter @objectstack/lint typecheckexit 0.const triggerErrors = validateTriggerCapability(data);replaced byconst triggerErrors: string[] = [];— on-disk proofinjected=1 removed-anchor-left=0; run:Tests 6 failed | 18 passed (24)— exactly the six refusal cases red, every pass-through case and the whole resolver suite green; restoregit checkout HEAD -- ABSOLUTE_PATHproven byhash d8411f45… == HEAD blob; git diff HEAD empty(trap-guarded script). Spec's own tests import./stack.zodfromsrc, so no dist was involved in this leg; spec was rebuilt afterwards regardless (the restore's touch had turned the mtime-based dist-freshness guards red) and the dist-reading gates re-run on that rebuilt tree.tsximport ofexamples/app-todo/objectstack.config.ts→requires=["automation","triggers"] flows=4 types=["schedule","record_change","screen"];examples/app-crm→requires=["ui","automation"] flows=1 types=["screen"].examples/app-showcaseis not importable in this worktree (its@objectstack/connector-mcpdist is unbuilt) and passes on the static membership test ('triggers'atobjectstack.config.ts:107).pnpm validateper example NOT run (it needs the CLI closure built); the import exercises the exactdefineStackrefusal, and the lint half ofos validateis unchanged (readiness suite green).packages/clisuites (theirdefineStackconsumers declare no auto-triggered flow — census in Fixture triage); fullpnpm test— CI's.Gates
node scripts/pm/dispatch-gates.mjs --commandswith no paths at24aec0a0(answer stampedobjectstack-ai/objectstackat24aec0a0): 71 commands (the PM's three-path reading gave 40; the same three paths gave 46 here — 41 by path + 7 by change KIND; the real change set adds the changeset, docs, example and artifact families). Every command run with its exit code captured by redirection; 67 green, including the ones this card names:check:stack-collection-maps,check:api-surface(public API surface + factory signatures unchanged ✓— the two new exports are in the regenerated shard),check:docs,check:llms-txt,check:liveness,check:where-matcher,check:objectql-double-limit,check-adr-0087-registration,check:export-origins,check:dual-source-exports(0 accepted dual-source),check:exported-any,check:entry-nameability,check:skill-examples(259 prose examples type-check across 3 surface(s)— after building@objectstack/client-react...),check:cross-package-test-inputs,check:test-source-alias,check:doc-authoring,check-affected-docs,check:changeset-gate-self-tests,check-empty-changeset,check-changeset-no-major.4 NOT MEASURED locally — each exits with its own prerequisite code, nothing measured, CI owns them:
check-dev-prereqs(exit 1: 63 of 67 workspace packages unbuilt here),check-test-completeness(exit 3: needs a savedturbo run testlog),check:dual-build-cjs-loads(exit 3:PREREQUISITE NOT MET, unbuilt dists),check:type-check-debt(exit 3: needs the whole closure built).Also run, not in the derived list:
pnpm check:nul-bytes(OK … 7815 text file(s) … no raw ASCII control bytes) plus a control-character self-scan over every edited file (clean).skip-changeset: not applicable — this PR ships a changeset.needs:contract-reviewhung on this PR and on #14153 (Clause ②:defineStackgains a refusal).Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21
🤖 Generated with Claude Code
Generated by Claude Code
Generated by Claude Code