ADR-v4-008: Engine schema migrations via explicit ALTER TABLE
Status: Accepted (2026-05-25)
Context:
The v4 diamond adds two engine-persistence surfaces:
passes.remediation_outcome+passes.remediation_rounds_usedcolumns for the adversarial cycle’s per-pass summary.- A new
arrow_invalidationstable for the/invalidate-arrowoperator escape verb.
engine/store.go uses CREATE TABLE IF NOT EXISTS for table
construction. Pre-existing databases (operators upgrading across
v4) do NOT pick up new columns automatically — IF NOT EXISTS
won’t add columns to an already-existing table.
R8 of the v2 adversarial flagged the migration path was unflagged;
R28 added the arrow_invalidations table to the same migration
scope.
Decision:
Both schema additions ship as explicit ALTER TABLE migrations
called from engine.OpenStore after the CREATE TABLE IF NOT EXISTS block:
migrateAddRemediationColumns(db *sql.DB) error: PRAGMAtable_info(passes)to detect column existence; on miss, runALTER TABLE passes ADD COLUMN remediation_outcome TEXT NULLandALTER TABLE passes ADD COLUMN remediation_rounds_used INTEGER NULL. Idempotent (safe to call on fresh DBs).migrateAddArrowInvalidations(db *sql.DB) error: PRAGMAtable_listdetection; on miss,CREATE TABLE arrow_invalidations (arrow_id TEXT PRIMARY KEY, op_id TEXT NOT NULL, reason TEXT, invalidated_at TIMESTAMP, grid_version INTEGER).
Backfill: pre-migration passes rows have NULL in both new
columns; the cycle never ran for those passes so NULL is correct.
Consequences:
- A fresh session against a pre-v4
engine.dbmigrates on first open with no operator action. JSONLaudit records remain the authoritative attestation log per ADR-015; thepassestable extension is a search shortcut only. The cycle’s full report is logged as a JSONL record of kindadversarial-cycle-report.- The migration is the only schema-mutating path on open; a future v5 adds new tables / columns the same way.
- The
arrow_invalidationstable is read at Replay to populaterunner.Grid.Invalidations;/run-arrowconsults this map and forces re-traversal even if the cached arrow status iscomplete.
Producer:
The /invalidate-arrow <arrow-id> [--reason <text>] slash command
(implemented at cmd/ghyll/invalidate_arrow_cmd.go) is the
operator-facing producer for OpEventArrowInvalidated. The handler:
- Refuses without an active
/op-id(the row carries operator identity). - Refuses on unknown arrow-id (looked up via the live
Grid). - Publishes
OpEventArrowInvalidatedwith the typed Payload per ADR-v4-005:arrow_id,op_id,reason,source=operator.
The subscriber wired in cmd/ghyll/session_engine.go:attachJournal
fans out the publish to engine.Store.InsertArrowInvalidation
synchronously, so by the time the operator sees the confirmation
line the row is on disk. Integrator-pass I-C-1 (2026-05-25) closed
the original “consumer chain without producer” gap; the table now
receives rows from operator input, not only from direct-test
publishes.