Case Audit
Architecture — four layers
One case generates four different kinds of record. Each has its own purpose,
its own consumer, its own retention policy. They never merge.
Status update 2026-09-30. This page was written against the ADR-010 module pipeline, retired by ADR-061 at substrate-slice-15 (2026-09-12). The four-layer model stands; the writers are updated: Layer 3 is written only by typed Workflows::CaseActivity::Recorder calls (automatic capture: ADR-068, Proposed), and Layer 4 by Contracts::ServicesRendered::Recorder — manual entry today, automatic recording owed (OQ-0059), attributed via source_artifact_instance_id.
The Four Layers
Each layer answers a different question.
The temptation is to build one "audit log" for everything. We don't, because
legal discovery, forensic debugging, the human case narrative, and service
reconciliation are four problems with four different shapes. One table
cannot serve all of them well.
01
Chain of Custody
custody_events
Regulatory proof of physical possession of remains and assets.
Subject to legal discovery.
Answers
"Who had the body, when?"
Written by
Custody scans via ChainOfCustody::RecordEvent only
Consumed by
Regulators, courts, coroners
Retention
Permanent
Immutability
Append-only, supersession pattern
Volume / case
3–8 rows
02
Audited Model Trail
audits
Field-level "who changed what when" on every tracked Rails model.
High volume, machine-oriented, forensic.
Answers
"Which field changed, by whom?"
Written by
audited gem (automatic)
Consumed by
Developers, support, forensic investigation
Retention
7 years (provincial reg)
Immutability
Read-only by application
Volume / case
200–500 rows
03
Case Activity Events
case_activity_events
Curated, human-readable narrative of every meaningful interaction
with a case. The story of what happened.
Answers
"What happened on this case?"
Written by
Typed Workflows::CaseActivity::Recorder calls (controllers, substrate services, custody mirror)
Consumed by
Case Timeline, shift handoffs, owners
Retention
2–3 years (TBD)
Immutability
Append-only by convention
Volume / case
30–80 rows
04
Services Rendered
case_services_rendered
Typed, structured record of what was actually delivered. Backs
contract reconciliation and operational analytics.
Answers
"What did we actually do for them?"
Written by
Contracts::ServicesRendered::Recorder — manual director / admin entry today; automatic recording owed (OQ-0059)
Consumed by
Reconciliation, contract amendments, finance
Retention
Tied to case
Immutability
Structured, queryable
Volume / case
5–15 rows
In Practice
One event, different layers touched.
No single event writes to all four layers. The shape of the event determines
where it lands. These three examples show the pattern.
Event A — Regulatory
Director scans pickup at Vancouver General
David Chen, 08:30, QR code scan on the transfer cot
01
Authoritative custody record written
custody_events
02
Field changes captured (case status, assigned_to)
audits
03
Mirror narrative entry for the Timeline UI
case_activity_events
04
No service rendered — skipped
case_services_rendered
Event B — Service Delivery
Visitation completed — 2 hours in Chapel B
Director records the service as delivered
01
No custody change — skipped
custody_events
02
Tracked-model field changes captured
audits
03
Narrative entry: "Visitation completed — 2 hrs, Chapel B"
case_activity_events
04
Typed service row for reconciliation
case_services_rendered
Event C — UI Interaction
Sophie adds a note about family preferences
Free-text note attached to case
01
No custody change — skipped
custody_events
02
Note model insert captured automatically
audits
03
UI instrumentation writes "Note added by Sophie"
case_activity_events
04
Not a service delivery — skipped
case_services_rendered
Five Boundary Rules
The lines that do not get crossed.
Without explicit rules, four layers drift back into one over time. These
are the load-bearing constraints that keep the architecture honest.
I.
Chain of custody never merges with anything else.
Custody scans write only to custody_events. A mirror narrative
entry is added to Layer 3 — but it carries only a summary and a reference,
never the full custody payload. Regulators read Layer 1; everyone else reads
the mirror.
II.
The audited gem never feeds Layer 3.
The Case Timeline does not render translated audits rows. The
translation cost is too high, the noise ratio is too poor. Developers query
Layer 2 directly; end users never see it.
III.
Layer 3 captures events humans care about — not field changes.
One meaningful event yields one Layer 3 row, regardless of how many
underlying fields moved. The field-level detail lives in Layer 2.
IV.
Layer 4 is typed. Layer 3 is narrative.
A service rendered produces two records of the same event: a structured
row in Layer 4 for reconciliation queries, and a narrative row in Layer 3
for the Timeline. Same event, two shapes, two consumers.
V.
No layer references later layers as foreign keys.
Layer 1, 2, and 4 never point at Layer 3. The reverse — Layer 3 referencing
others via its JSONB payload — is fine. This keeps Layer 3 a derivable
narrative, not a structurally load-bearing reference.
Quick Decision Aid
Where does this event go?
For developers and reviewers: the routing answer in one read.
If the event is…
A QR scan of remains or assets (pickup, receiving, disposition)
→ Layer 1 + 3 mirror
A change to any tracked Rails model field
→ Layer 2 (auto)
A case action taken through a controller or substrate service (the module-engine hook is retired; ADR-068 covers restoring automatic capture)
→ Layer 3 (typed Recorder call)
A meaningful UI action (note, view, document open)
→ Layer 3 (manual)
A service was actually delivered to the family
→ Layer 4 + 3
A bug investigation or "who changed this?"
→ Read Layer 2
Regulator subpoena, coroner inquiry
→ Read Layer 1 only
"Show me everything that happened on this case"
→ Read Layer 3