Files
FarmHouse-Holdings/adr/ADR-003-event-schema-and-data-model.md
T
fez5587andClaude Fable 5 ad4f603799 Step 5+6: event schema v1, core DDL, policy table, Slice 1 acceptance demo
- db/schema.sql: 9 core tables, append-only event log (trigger-enforced),
  deny-by-default policy_rule with seeded globals, runaway budgets on work items.
  Validated against Postgres 16 (DDL applies, trigger rejects UPDATE).
- engine/schemas/events.py: AgentMessage (flat, inference-enforced) +
  EventEnvelope pydantic models; models are the schema (ADR-003).
- adr/ADR-003: event schema and data model decisions.
- SLICE1_DEMO.md: verbatim acceptance script for Slice 1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 23:26:49 -05:00

2.3 KiB

ADR-003: Event Schema and Core Data Model

Status: Accepted Date: 2026-08-03

Decision

Sources of truth:

  • db/schema.sql — DDL for the Slice 1 entity subset: company, employee, work_item, event, approval, artifact, tool_execution, cost_entry, policy_rule. Remaining PRD §14.3 entities (Schedule, KnowledgeItem, Incident, ConsultantEngagement, …) are added by the slice that needs them, not up front.
  • engine/schemas/events.py — pydantic models. AgentMessage (agent→engine, enforced at inference via structured output) and EventEnvelope (immutable log record). The pydantic models ARE the schema; no parallel JSON-schema files to drift.

Key choices:

  1. Append-only event log in Postgres with a trigger rejecting UPDATE/DELETE. Projections (work_item.status, employee.status) are ordinary tables updated in the same transaction as the event insert. No event-sourcing framework.
  2. Flat agent message schema. One shape for all 12 agent message types, optional fields per type. Benchmarked at 100% validity on qwen3.5:9b under format enforcement; nested per-type schemas degrade small models and multiply retry logic.
  3. task.blocked is triage (benchmark finding: model confuses blocked vs clarification vs approval). Engine manager step reclassifies before user-facing routing.
  4. Deny-by-default policy table. policy_rule rows grant max authority per (company, employee, tool-glob, environment); most specific enabled rule wins; no matching rule = denied. Seeded globals: sandbox=2, internal=1, external/production=0. Every tool execution stores the decision it ran under.
  5. Runaway caps live in work_item.budget (max_iterations 15, max_tokens 100k, max_wall_seconds 1800 default) and are engine-enforced; run.capped event fires when tripped (PRD_ADDENDUM §3).
  6. UUIDs everywhere, UTC timestamptz everywhere. Display timezone (America/Indiana/Indianapolis) is a UI concern only.

Rejected

  • Kafka/NATS event bus — Postgres insert + Redis pub/sub notify covers 4-worker scale.
  • Full event-sourcing (rebuild state from log) — projections are authoritative for current state; the log is authoritative for history and audit.
  • Per-company database — schema-shared, row-scoped by company_id; revisit if isolation demands grow.