# Append-Only Events Contract **Name:** append-only-events **Version:** 1.0 **Date:** 2026-03-31 **Status:** Active **Layer:** Core **Immutable:** Yes — this invariant is permanent and cannot be relaxed --- ## Purpose Interaction events and outcome signals are the primary observational record of the IHF. Their integrity as an append-only log is a foundational invariant: governance, traceability, and antifragility all depend on the fact that the historical record cannot be silently rewritten. --- ## Invariant The following tables are **append-only**: | Table | Trigger (no update) | Trigger (no delete) | |---|---|---| | `interaction_events` | `interaction_events_no_update` | `interaction_events_no_delete` | | `outcome_signals` | `outcome_signals_no_update` | `outcome_signals_no_delete` | No row in either table may be modified or deleted after insertion. This invariant is enforced at the PostgreSQL level by `BEFORE UPDATE` and `BEFORE DELETE` triggers that raise exceptions. It cannot be bypassed by application code. --- ## Enforcement ```sql -- Implemented in Application/Schema.sql CREATE TRIGGER interaction_events_no_update BEFORE UPDATE ON interaction_events FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation(); CREATE TRIGGER interaction_events_no_delete BEFORE DELETE ON interaction_events FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation(); -- Same pattern for outcome_signals ``` The trigger function raises: ``` "interaction_events is append-only: UPDATE and DELETE are not permitted" ``` --- ## Correction Policy Erroneous events **must not** be corrected by modifying the original row. The correct approach is to insert a new event: ```json { "event_type": "retracted", "metadata": { "retracted_event_id": "", "reason": "incorrect actor attribution" } } ``` The original event remains in the table. Downstream analysis should treat `retracted` events as markers that exclude the referenced event from calculations. --- ## Failure Mode Any attempt to UPDATE or DELETE a row in `interaction_events` or `outcome_signals` raises a PostgreSQL exception with SQLSTATE `P0001`. The calling transaction is aborted. No partial mutation is possible. --- ## Scope This contract applies **only** to `interaction_events` and `outcome_signals`. Other append-oriented tables (`triage_states`, `widget_ownerships`, `stewardship_roles`) use the same conceptual pattern (soft expiry instead of update, no hard delete) but are not covered by DB-trigger enforcement. Their append semantics are enforced by application-layer controller conventions. --- ## Phase 12 Addendum — Lineage Enrichment Trigger **Added in Phase 12 (IHUB-WP-0013 T06):** `outcome_signals` now has an `AFTER INSERT` trigger `trg_enrich_lineage` that calls `enrich_lineage_on_outcome()`. This trigger: - Is **AFTER INSERT only** — it never fires on UPDATE or DELETE. - Does **not modify `outcome_signals`** — it only enriches upstream records (`decision_records.outcome_summary`, `requirement_candidates.outcome_summary`). - Is consistent with the append-only invariant: the `outcome_signals` row itself remains immutable after insertion. --- ## Implementation Reference - Functions: `prevent_interaction_event_mutation()`, `prevent_outcome_signal_mutation()` in `Application/Schema.sql` - Phase 12: `enrich_lineage_on_outcome()` — AFTER INSERT trigger on `outcome_signals`; enriches non-append-only columns on upstream tables only - The architectural fitness function `Test/Architecture/LayerBoundarySpec.hs` (Test 1) verifies these trigger names are present in the schema