6.6.1 Schema + TOTAL trigger/action registries + `automationRulesService` CRUD (admin-gated, enable/disable, caps) + routes
Estimate: 32m · Depends on: 6.1.1
The rule model and the authoring surface's backend. Pure backend — no UI, no engine yet.
Schema (FKs as Prisma relations, both sides): automation_rule — projectId, name, enabled, triggerType + typed triggerConfig JSON, conditionAst JSON (the 6.1 FilterAST shape, row-capped 20), actions JSON (ordered, max 10), ownerId (the rule actor — the recorded deviation), consecutiveFailureCount, timestamps; automation_rule_execution — ruleId, status enum (success / failure / no_actions), triggering workItemId (nullable — the item may be deleted later), error text, durationMs, createdAt; indexed [ruleId, createdAt] for the paged log + the retention sweep.
lib/automation/registry.ts — TWO TOTAL registries (mistake #29). Triggers: created / transitioned (optional from/to status-id narrowing) / field-changed (built-in-field picker config; assignee is the "assigned" preset) / commented — each entry = config schema + validate + the event name it consumes + the UI editor kind. Actions (this subtask: the shipped-substrate entries): transition (target status id) / set-field (built-ins the shipped workItemsService.update accepts — assignee, priority, due date, story points) — each entry = config schema + validate + execute fn signature + editor kind. Unknown trigger/action/field ids → typed 422; the enumeration test fails on any registry gap. Epic-5 entries (watcher / comment / label / custom-field) land in 6.6.3 as registry EXTENSIONS — same pattern as 6.1.1→6.1.2.
automationRulesService + routes (4-layer): create / update / enable / disable / delete / list / get, gated by the shipped 6.4 manage-project permission (viewers get 403/404 per the 6.4 read rules); validation composes the trigger/action registries + the 6.1.1 condition-AST validation (built-in fields here); caps — 100 rules/project, 10 actions/rule, 20 condition rows — as typed 422s; enable resets consecutiveFailureCount; delete cascades the execution log (Prisma onDelete).
Acceptance criteria
- Migration applies cleanly (re-run: no drift); both tables modelled with relations on both sides per the CLAUDE.md FK rule.
- Both registries are TOTAL with enumeration tests (every entry's config-schema × validate × editor triple); malformed/unknown configs → 422 (fuzzed, incl. smuggled field ids through the condition AST — the 6.1 injection posture extends here).
- CRUD + enable/disable + caps behave per the description; admin-gating asserted per 6.4 role (admin yes, member no, viewer no); a rule referencing a since-deleted status/field degrades typed (the 6.1 stale-referent rule), never crashes validation.
pnpm test:coverage≥90% on the new files.
Context refs
- 6.1.1
lib/filters/ast.ts+ registry (the condition substrate + the totality/422 pattern to mirror) lib/services/projectAccessService.ts+ the 6.4 role checks (the admin gate);workflowsService(status ids for transition configs)- The verified Jira anatomy/scope/actor facts in the Story 6.6 description
motir-core/CLAUDE.md(4-layer, FK-as-relation, required-tx)