Phase 2 — Runtime Engine Scope

Phase 1 (built) renders the 16-model schema as an AI-Safe CRUD register: contracts, parties, clauses, obligations, occurrences, notices, events, variations, claims, breaches, evidence, approvals and tasks — browsable, with a dashboard. But the states are static: the seed shows an obligation already overdue, a notice already issued. Nothing is computed or transitioned by the system.

Phase 2 is the runtime that makes it live — computing deadlines, generating recurring occurrences, enforcing the state machines, running the scans and reacting to the rules the pack specifies. Same shape as the (built, cluster-proven) orchestrator engine, but single-app.

Everything below is drawn from the pack's workflows.yaml, rules.yaml, the DSL actions {} / scheduled_jobs {} / derived {} blocks and the ContractPortfolio dashboard. It is a scope, not a design; each workstream still needs the noted decisions.


A. Derived-field engine

The DSL derived fields on Obligation are seeded statically today; Phase 2 recomputes them (the heart of "nothing lapses"):

Field Rule
Obligation.days_to_due business-days between today and due_date
Obligation.overdue due_date < today() AND status ∉ {satisfied, waived, closed}
Obligation.due_soon 0 ≤ days_to_due ≤ warning_days AND status active/pending

Decisions: compute-on-read vs materialise-on-write vs recompute-on-tick; ObligationOccurrence due dates likewise.

B. State machines & guarded actions

The pack defines two workflows plus the DSL actions {}. Today a user free-edits status; Phase 2 exposes each transition as a guarded action that enforces the guard, applies effects atomically and records the change.

Workflow / action Notable guards & effects
contract (draft→under_review→active→…→archived) complete requires no open material obligations; role-gated
obligation (draft→pending→active→submitted→satisfied) satisfy requires required evidence present + required approval complete; waive requires a waiver reference; active→overdue is automatic on deadline
activate_contract requires under_review; sets status=active; emits contract.activated
satisfy_obligation requires evidence linked; sets status=satisfied, satisfied_at=now(), satisfied_by=user; emits obligation.satisfied
issue_notice requires approved; sets status=issued, issued_at=now(), notice_date=coalesce(...); emits notice.issued
acknowledge_notice requires issued/received; sets acknowledged; emits notice.acknowledged

Decisions: UI buttons vs API-only; role → PIN/RBAC mapping; guard blocks vs warns.

C. Scheduled jobs — the deadline & recurrence engine

The DSL scheduled_jobs {} are the headline background engine — a tick that:

  • obligation_deadline_scan (hourly) — flip obligations/occurrences to due_soon/overdue as dates pass (rules OBL-DUE-001 / OBL-OVERDUE-001).
  • recurring_obligation_generator (daily) — generate the next ObligationOccurrence from each active obligation's recurrence_rule (FREQ=MONTHLY;…) and due_rule. This is what turns a monthly obligation into a rolling series of dated instances.
  • notice_acknowledgement_scan (4-hourly) — chase issued notices past their acknowledgement_due.
  • contract_expiry_scan (daily) — warn on contracts nearing expiry_date.

Decisions: cadence; business-calendar (AU_QLD) for business-day math (the pack ships business_calendars.yaml); recurrence-rule parser (RRULE subset); exception dedupe so a scan doesn't re-raise every tick.

D. Rules / cross-record automation

Reactive rules from rules.yaml:

  • OBL-DUE-001 — obligation due_soon → set status, create a review Task assigned to accountable_person_id, emit obligation.due_soon.
  • OBL-OVERDUE-001 — obligation past due → set overdue, emit obligation.overdue, escalate to contract_manager/legal after 2 days.
  • NOTICE-DEADLINE-001 — a notice with a required_by_date still unsent → tiered warnings at 7 / 2 / 0 days (warning → high → critical), emit notice.deadline_warning.
  • NOTICE-SERVICE-001before issue, require notice_date, sender_party_id, recipient_party_id (service details).

The DSL also implies event-driven links: a ContractEvent flagged potential → notice/claim/variation/breach could auto-create the downstream record or a task.

Decisions: event bus (in-app only vs emit to orchestrator); notify = email vs in-app; escalation targets.

E. Executive dashboard & KPIs

The DSL ContractPortfolio dashboard defines metrics Phase 1 renders generically; Phase 2 wires them via the repo KPI framework (kpi_engine.py + kpis.py):

  • Metrics: active contracts; obligations due in 30 days; overdue obligations; open notices; open claims; unresolved breaches.
  • Panels: contracts at risk; upcoming deadlines; overdue items; recent notices; open variations.

How it would be built

  1. A per-app engine module (mirror server/lib/orchestrator_engine/, single-app): compute pass (A), scheduler/tick with the four jobs (C), guarded actions (B), rules evaluator (D). Opt-in env flag, single worker.
  2. KPIs (E) via the existing framework — independent of the engine, quick win.
  3. Business-calendar + RRULE underpin the deadline/recurrence engine.
  4. Tests per workstream (test_orchestrator_flows.py is the template).

Suggested sequencing

  1. KPIs / dashboard (E) — visible, no engine.
  2. Derived fields (A) — accurate overdue/due_soon badges.
  3. Recurrence + deadline engine (C) — the core value; needs calendar + RRULE.
  4. State machines + guards (B) — governed transitions.
  5. Rules / automation (D) — reactive escalations and tasks.

Each slice is independently shippable and demoable. The orchestrator (built and run live this session) is the working template for the engine, scheduler and tests.