Phase 3 Slice 11a (m's Q5 option C: "I need to see these things,
admin only"). RuleEditorService owns the admin-only lifecycle for
paliad.deadline_rules:
Create → INSERT row with lifecycle_state='draft', published_at=NULL.
UpdateDraft → UPDATE WHERE id=$1 AND lifecycle_state='draft'.
Published or archived rows must clone-as-draft first
(ErrInvalidLifecycleState otherwise — 409).
CloneAsDraft → INSERT deep copy of source row (published OR archived)
as a new draft with draft_of pointing at the source.
Lets editors propose changes to live rules without
mutating the live row.
Publish → UPDATE lifecycle_state='published', set published_at.
When draft_of != NULL, also archives the cloned-from
peer so each rule has at most one live row.
Archive → UPDATE lifecycle_state='archived' (allowed from
published OR draft).
Restore → UPDATE lifecycle_state='published' (only from archived).
Preview → Calls FristenrechnerService.Calculate with the draft
as a RuleOverrides entry — pure simulation, no DB
write. If draft_of is set, the override substitutes
for the peer (matching ID); otherwise it's appended.
ListAudit → SELECT paliad.deadline_rule_audit rows for one rule,
newest-first, with offset/limit pagination. Joined
with paliad.users.display_name for the changed_by
column.
ListRules → Admin list view with filters (proceeding_type_id,
trigger_event_id, lifecycle_state, fuzzy q over
name / name_en / rule_code).
ExportMigrationsSince → SQL blob generator for the migration-export
admin flow (Q-H-5 pure SQL format). v1 emits one
statement per audit row in chronological order;
Slice 11b polishes the output (header comment,
collapse consecutive UPDATEs).
Audit-reason invariant: every write method requires a non-empty
reason string. setAuditReasonTx writes it into the session-local
paliad.audit_reason setting in the same transaction as the
INSERT/UPDATE, so mig 079's trigger captures the rationale
forever. Empty reason → ErrAuditReasonRequired (400 in the handler).
Spawn cycle guard: validateSpawnNoCycle pre-checks Create + UpdateDraft
edits that touch spawn_proceeding_type_id against the global rule
graph. Reuses the design §6 cycle-guard semantics — walks the
target proceeding's spawn rules transitively; raises ErrCyclicSpawn
if any reachable proceeding is the source. Slice 7's runtime guard
catches anything this misses; the editor surface catches it at
edit time so the editor sees a clear 409 instead of a silent
projection failure.
Typed errors:
ErrRuleNotFound → 404 in handler
ErrInvalidLifecycleState → 409 in handler
ErrAuditReasonRequired → 400 in handler
ErrInvalidInput → 400 (re-uses the existing services-wide error)
ErrCyclicSpawn → 409 (re-uses Slice 7's typed error)
RuleAuditEntry struct extends models.DeadlineRuleAudit with a
display_name for the admin UI; distinct from services.AuditEntry
(the cross-source union for the site-wide audit panel) so the two
read paths don't conflict.