Skip to content

Commit 1a73d3a

Browse files
feat(provenance): fork-first-class chain model — ADR-0010 (#31; supersedes #32) (#109)
Implements ADR-0010 (now Accepted). The provenance integrity property is tamper-evidence + no-silent-loss, NOT linearity: legitimate divergence (partitioned/replicated/offline writers, simulation branches) must be representable, persisted, detectable and verifiable. Schema - NO `UNIQUE(entity_id, previous_hash)` (#32, superseded): it would reject a divergent writer's honest history at insert time. The `hash` PRIMARY KEY is the correct duplicate guard (domain-tagged preimage). - Add non-unique `idx_provenance_predecessor` → O(log n) fork detection. - `verisimdb_provenance_chain_heads(entity_id, head_hash)` multi-head tip set; legacy single-head table kept one release with an idempotent `INSERT … SELECT` migration (no destructive DROP ships here). Behaviour - `append_provenance`: linear fast-path — extends the unique head; errors (not silently collapses) if the entity has >1 head. - `append_provenance_fork(from_hash)`: extends a specific ancestor, *adds* a head without removing one → the entity now has ≥2 tips. - `fork_points()`: every predecessor with >1 child. - `verify_chain`: per-branch walk (each tip → genesis) so a forked- but-honest entity verifies true while tampering any branch still fails. codegen `overlay.rs` mirrors the schema (harmful `ux_provenance_chain` removed); its DDL tests inverted to assert the ADR-0010 contract. Tests: `tests/provenance_fork_test.rs` rewritten to the 4 ADR-0010 cases (the previously failing-by-design test now passes) + `exact_duplicate_entry_is_rejected`. Full suite green (107 lib + 9 + 4 + 2, 0 failed). ADR-0010 Proposed → Accepted. Closes #31. Supersedes #32 (closed not-planned). Co-authored-by: hyperpolymath <hyperpolymath@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 038ae49 commit 1a73d3a

4 files changed

Lines changed: 513 additions & 197 deletions

File tree

docs/decisions/0010-provenance-forks-are-first-class.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ Date: 2026-05-16
88

99
## Status
1010

11-
Proposed (design + failing test; tracks #31 and #32)
11+
Accepted (2026-05-17) — implemented in #31 (multi-head tip set,
12+
fork-aware append, `fork_points`, per-branch `verify_chain`,
13+
non-destructive migration). #32 (the `UNIQUE INDEX(entity_id,
14+
previous_hash)`) is superseded by this ADR and closed not-planned:
15+
the `hash` PRIMARY KEY is the correct duplicate guard.
1216

1317
## Context
1418

src/codegen/overlay.rs

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,19 @@ fn generate_metadata_table(schema: &ParsedSchema) -> String {
185185
/// append-only, tamper-evident log (see
186186
/// `docs/theory/provenance-threat-model.adoc`).
187187
///
188-
/// The `chain_head` table is the per-entity head pointer used for the
189-
/// write-path lock (V-L2-L1). The UNIQUE INDEX on `(entity_id,
190-
/// previous_hash)` (V-L2-L2) makes chain forks structurally impossible
191-
/// — defence in depth for if the lock is ever bypassed.
188+
/// ADR-0010 (provenance forks are first-class): the `hash` PRIMARY KEY
189+
/// is the duplicate guard (the preimage covers every tamper-relevant
190+
/// field). We deliberately do **not** emit `UNIQUE(entity_id,
191+
/// previous_hash)` (#32, superseded) — that rejects a divergent second
192+
/// writer's legitimate history at insert time. Instead a **non-unique**
193+
/// `idx_provenance_predecessor` makes fork *detection* O(log n), and the
194+
/// chain tip is a *set* (`verisimdb_provenance_chain_heads`): one row
195+
/// for a linear entity, several when it has legitimately forked. The
196+
/// legacy single-head `verisimdb_provenance_chain_head` is kept one
197+
/// release for non-destructive migration. Mirrors
198+
/// `tier1::provenance::SIDECAR_DDL` (kept in sync).
192199
fn generate_provenance_table() -> String {
193-
"-- Provenance: SHA-256 hash-chained audit trail\n\
200+
"-- Provenance: SHA-256 hash-chained audit trail (ADR-0010)\n\
194201
CREATE TABLE IF NOT EXISTS verisimdb_provenance_log (\n\
195202
\x20 hash TEXT PRIMARY KEY,\n\
196203
\x20 previous_hash TEXT NOT NULL,\n\
@@ -203,19 +210,25 @@ fn generate_provenance_table() -> String {
203210
\x20 transformation TEXT, -- description of transformation applied\n\
204211
\x20 CHECK (operation IN ('insert','update','delete','transform'))\n\
205212
);\n\
206-
-- V-L2-L2: forbid chain forks at the DB level. Genesis records all\n\
207-
-- carry previous_hash='' so this also enforces a single genesis per\n\
208-
-- entity.\n\
209-
CREATE UNIQUE INDEX IF NOT EXISTS ux_provenance_chain\n\
213+
-- ADR-0010 #32 (superseded): NO UNIQUE(entity_id, previous_hash) —\n\
214+
-- a fork that cannot be written cannot be detected or audited. The\n\
215+
-- non-unique index below makes fork detection O(log n) instead.\n\
216+
CREATE INDEX IF NOT EXISTS idx_provenance_predecessor\n\
210217
\x20 ON verisimdb_provenance_log(entity_id, previous_hash);\n\
211218
CREATE INDEX IF NOT EXISTS idx_provenance_entity ON verisimdb_provenance_log(entity_id);\n\
212219
CREATE INDEX IF NOT EXISTS idx_provenance_table ON verisimdb_provenance_log(table_name);\n\
213220
\n\
214-
-- V-L2-L1: per-entity head pointer. The write path takes a row\n\
215-
-- lock here (SELECT … FOR UPDATE / BEGIN IMMEDIATE) so concurrent\n\
216-
-- appenders on the same entity serialise; cross-entity appends\n\
217-
-- remain parallel. Each successful append updates head_hash in\n\
218-
-- the same transaction as the INSERT into verisimdb_provenance_log.\n\
221+
-- ADR-0010 #31: chain-tip *set*. `append_provenance` keeps a\n\
222+
-- BEGIN IMMEDIATE write so racing duplicate appends on one node\n\
223+
-- still serialise; a linear append swaps its single tip, a\n\
224+
-- deliberate fork adds a tip without removing one.\n\
225+
CREATE TABLE IF NOT EXISTS verisimdb_provenance_chain_heads (\n\
226+
\x20 entity_id TEXT NOT NULL,\n\
227+
\x20 head_hash TEXT NOT NULL,\n\
228+
\x20 PRIMARY KEY (entity_id, head_hash)\n\
229+
);\n\
230+
-- Legacy single-head table: kept one release for non-destructive\n\
231+
-- migration (see tier1::provenance::SIDECAR_DDL). No DROP ships here.\n\
219232
CREATE TABLE IF NOT EXISTS verisimdb_provenance_chain_head (\n\
220233
\x20 entity_id TEXT PRIMARY KEY,\n\
221234
\x20 head_hash TEXT NOT NULL,\n\
@@ -532,22 +545,47 @@ mod tests {
532545
assert!(ddl.contains("actor"));
533546
}
534547

535-
/// V-L2-L2: forks are forbidden by a UNIQUE INDEX on
536-
/// (entity_id, previous_hash).
548+
/// ADR-0010 (#32 superseded): forks are first-class. The fork guard
549+
/// is the `hash` PRIMARY KEY (duplicate-rejection); there must be
550+
/// NO `UNIQUE(entity_id, previous_hash)` (it would discard a
551+
/// divergent writer's legitimate history). A *non-unique*
552+
/// predecessor index provides O(log n) fork detection instead.
537553
#[test]
538-
fn test_provenance_table_has_unique_chain_index() {
554+
fn test_provenance_table_fork_detection_index_is_not_unique() {
539555
let ddl = generate_provenance_table();
540-
assert!(ddl.contains("UNIQUE INDEX"));
541-
assert!(ddl.contains("ux_provenance_chain"));
556+
assert!(
557+
!ddl.contains("ux_provenance_chain"),
558+
"the superseded UNIQUE(entity_id, previous_hash) must not be emitted"
559+
);
560+
assert!(
561+
!ddl.contains("CREATE UNIQUE INDEX IF NOT EXISTS ux_provenance"),
562+
"no unique provenance-chain index (ADR-0010)"
563+
);
564+
assert!(
565+
ddl.contains("idx_provenance_predecessor"),
566+
"non-unique fork-detection index must be present"
567+
);
542568
assert!(ddl.contains("(entity_id, previous_hash)"));
543569
}
544570

545-
/// V-L2-L1: chain_head table exists for per-entity write serialisation.
571+
/// ADR-0010 #31: the chain tip is a *set* (multi-head); the legacy
572+
/// single-head table is retained one release for migration.
546573
#[test]
547-
fn test_provenance_table_has_chain_head() {
574+
fn test_provenance_table_has_multihead_and_legacy_head() {
548575
let ddl = generate_provenance_table();
549-
assert!(ddl.contains("verisimdb_provenance_chain_head"));
576+
assert!(
577+
ddl.contains("verisimdb_provenance_chain_heads"),
578+
"multi-head set table must exist"
579+
);
580+
assert!(
581+
ddl.contains("verisimdb_provenance_chain_head ("),
582+
"legacy single-head table retained for migration"
583+
);
550584
assert!(ddl.contains("head_hash"));
585+
assert!(
586+
ddl.contains("PRIMARY KEY (entity_id, head_hash)"),
587+
"multi-head table keyed by (entity_id, head_hash)"
588+
);
551589
}
552590

553591
#[test]

0 commit comments

Comments
 (0)