Skip to content

Commit 47f5489

Browse files
Your Nameclaude
andcommitted
feat(schema,scip): PR#10 slice 1 -- reference_evidence dual-write (evidence ledger v1 foundation)
Wave 3 evidence ledger v1 (docs/plans/2026-08-19-evidence-architecture-execution-plan.md Part E PR#10), first slice: additive schema only, every existing call_edges write path stays unchanged. - New `reference_evidence` table (schema.rs): generalizes `external_proofs` with a `disposition` axis ('supports' written by this slice; 'excludes' is future work migrating insert_missing_exact_edges's evidence_conflicts INSERT) and an `authority_class` axis ('external_proof' for a live SCIP/LSP result today, leaving room for a future 'heuristic'/'static' class). Same CASCADE-on-reindex lifecycle as external_proofs, keyed by call_site_id. - `record_external_proof_for_edge` (scip/ingest.rs) now dual-writes into reference_evidence under the identical guard as its existing external_proofs INSERT, so the two tables never disagree about which proofs qualified. Idempotent (ON CONFLICT upsert, verified by a repeat- call test). - 2 new tests: reference_evidence's CASCADE-delete lifecycle, and the dual-write (incl. idempotent re-run doesn't duplicate rows in either table). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 7d209a6 commit 47f5489

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

crates/calm-core/src/db/schema.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,45 @@ CREATE TABLE IF NOT EXISTS external_proofs (
287287
);
288288
CREATE INDEX IF NOT EXISTS idx_external_proofs_status ON external_proofs(status, provider);
289289
290+
-- Wave 3 evidence ledger v1 (docs/plans/2026-08-19-evidence-architecture-execution-plan.md
291+
-- Part E PR#10, slice 1 of N). Generalizes external_proofs above -- additive
292+
-- schema + dual-write only in this slice; every call_edges write path is
293+
-- UNCHANGED. disposition distinguishes a provider proof FOR a target
294+
-- ('supports', the only value written so far, from record_external_proof_for_edge
295+
-- which both the SCIP and LSP overlays call) from one recording disagreement
296+
-- ('excludes' -- not written yet; migrating insert_missing_exact_edges's
297+
-- evidence_conflicts INSERT to also land here is future work, not this slice).
298+
-- authority_class names the KIND of evidence: 'external_proof' for a live
299+
-- SCIP/LSP result today, leaving room for a 'heuristic'/'static' class if the
300+
-- static resolver's own confidence tiers are ever folded in here too (this
301+
-- slice deliberately does not attempt that -- see PR#10's real write-path
302+
-- inventory: indexer/pipeline/graph.rs's rebuild_graph/incremental_graph_update
303+
-- DELETE-then-reinsert call_edges wholesale from call_sites+symbols on every
304+
-- pass, making the static tier already a projection rather than a provider in
305+
-- this sense). Same CASCADE-on-reindex lifecycle as external_proofs.
306+
CREATE TABLE IF NOT EXISTS reference_evidence (
307+
id INTEGER PRIMARY KEY AUTOINCREMENT,
308+
call_site_id INTEGER NOT NULL REFERENCES call_sites(id) ON DELETE CASCADE,
309+
to_symbol TEXT NOT NULL,
310+
provider TEXT NOT NULL,
311+
disposition TEXT NOT NULL CHECK (disposition IN ('supports', 'excludes')),
312+
authority_class TEXT NOT NULL DEFAULT 'external_proof',
313+
source_file_hash TEXT NOT NULL,
314+
callee_start_byte INTEGER NOT NULL,
315+
callee_end_byte INTEGER NOT NULL,
316+
provider_fingerprint TEXT NOT NULL,
317+
context_fingerprint TEXT NOT NULL,
318+
graph_generation INTEGER NOT NULL DEFAULT 0,
319+
call_site_identity_version INTEGER NOT NULL DEFAULT 1,
320+
definition_snapshot TEXT,
321+
status TEXT NOT NULL CHECK (status IN ('fresh', 'stale', 'legacy', 'unverified', 'rejected')),
322+
observed_at REAL NOT NULL,
323+
failure_reason TEXT,
324+
UNIQUE(call_site_id, to_symbol, provider, disposition)
325+
);
326+
CREATE INDEX IF NOT EXISTS idx_reference_evidence_status ON reference_evidence(status, provider);
327+
CREATE INDEX IF NOT EXISTS idx_reference_evidence_call_site ON reference_evidence(call_site_id);
328+
290329
-- WS3 (docs/plans/2026-08-18-context-intelligence-upgrade-plan.md, D3): a call
291330
-- site whose bare-name candidate set exceeded MAX_CALLEE_CANDIDATES used to be
292331
-- silently dropped to zero call_edges rows -- unknown read identically to
@@ -2556,4 +2595,41 @@ mod tests {
25562595
"proof lifetime follows CallSite identity, never edge id"
25572596
);
25582597
}
2598+
2599+
2600+
#[test]
2601+
fn reference_evidence_is_keyed_by_call_site_and_deleted_with_it() {
2602+
let conn = Connection::open_in_memory().unwrap();
2603+
init_db(&conn).unwrap();
2604+
conn.execute(
2605+
"INSERT INTO call_sites
2606+
(from_path, enclosing_qn, callee_name, call_line, callee_start_byte,
2607+
callee_end_byte, identity_version, edge_kind)
2608+
VALUES ('main.rs', 'main.rs::main', 'target', 1, 0, 6, 2, 'call')",
2609+
[],
2610+
)
2611+
.unwrap();
2612+
let call_site_id = conn.last_insert_rowid();
2613+
conn.execute(
2614+
"INSERT INTO reference_evidence
2615+
(call_site_id, to_symbol, provider, disposition, source_file_hash, callee_start_byte,
2616+
callee_end_byte, provider_fingerprint, context_fingerprint, status, observed_at)
2617+
VALUES (?1, 'lib.rs::target', 'scip:rust', 'supports', 'source-hash', 0, 6,
2618+
'binary-fingerprint', 'context-fingerprint', 'fresh', 1.0)",
2619+
[call_site_id],
2620+
)
2621+
.unwrap();
2622+
2623+
conn.execute("DELETE FROM call_sites WHERE id = ?1", [call_site_id])
2624+
.unwrap();
2625+
let evidence: i64 = conn
2626+
.query_row("SELECT COUNT(*) FROM reference_evidence", [], |row| {
2627+
row.get(0)
2628+
})
2629+
.unwrap();
2630+
assert_eq!(
2631+
evidence, 0,
2632+
"reference_evidence lifetime follows CallSite identity, same as external_proofs"
2633+
);
2634+
}
25592635
}

crates/calm-core/src/scip/ingest.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,56 @@ pub(crate) fn record_external_proof_for_edge(
574574
context.graph_generation,
575575
],
576576
)?;
577+
// Wave 3 evidence ledger v1 (PR#10 slice 1): mirrors the external_proofs
578+
// write above into reference_evidence under the identical guard, so the
579+
// two never disagree about which proofs qualified. disposition is always
580+
// 'supports' here -- record_external_proof_for_edge only ever runs after
581+
// a proof was ACCEPTED (see this fn's own doc comment); 'excludes' is
582+
// written by a future slice migrating insert_missing_exact_edges's
583+
// evidence_conflicts INSERT.
584+
conn.execute(
585+
"INSERT INTO reference_evidence
586+
(call_site_id, to_symbol, provider, disposition, authority_class, source_file_hash,
587+
callee_start_byte, callee_end_byte, provider_fingerprint, context_fingerprint,
588+
graph_generation, call_site_identity_version, definition_snapshot, status, observed_at)
589+
SELECT ce.call_site_id, ce.to_symbol, ?1, 'supports', 'external_proof', fi.hash,
590+
cs.callee_start_byte, cs.callee_end_byte, ?2, ?3, graph.generation,
591+
cs.identity_version,
592+
printf('%s:%d:%d:%s', COALESCE(def_file.hash, ''), def.line_start, def.line_end, def.signature),
593+
'fresh', unixepoch('now')
594+
FROM call_edges ce
595+
JOIN call_sites cs ON cs.id = ce.call_site_id
596+
JOIN file_index fi ON fi.path = cs.from_path
597+
JOIN symbols def ON def.qualified_name = ce.to_symbol
598+
LEFT JOIN file_index def_file ON def_file.path = def.path
599+
JOIN graph_generation_state graph ON graph.id = 1
600+
WHERE ce.id = ?4
601+
AND ce.formal_source = ?5
602+
AND cs.identity_version >= 2
603+
AND cs.callee_start_byte IS NOT NULL
604+
AND cs.callee_end_byte IS NOT NULL
605+
AND (?6 IS NULL OR graph.generation = ?6)
606+
ON CONFLICT(call_site_id, to_symbol, provider, disposition) DO UPDATE SET
607+
source_file_hash = excluded.source_file_hash,
608+
callee_start_byte = excluded.callee_start_byte,
609+
callee_end_byte = excluded.callee_end_byte,
610+
provider_fingerprint = excluded.provider_fingerprint,
611+
context_fingerprint = excluded.context_fingerprint,
612+
graph_generation = excluded.graph_generation,
613+
call_site_identity_version = excluded.call_site_identity_version,
614+
definition_snapshot = excluded.definition_snapshot,
615+
status = 'fresh',
616+
observed_at = excluded.observed_at,
617+
failure_reason = NULL",
618+
rusqlite::params![
619+
context.provider,
620+
context.provider_fingerprint,
621+
context.context_fingerprint,
622+
edge_id,
623+
expected_formal_source,
624+
context.graph_generation,
625+
],
626+
)?;
577627
Ok(())
578628
}
579629

@@ -1246,6 +1296,72 @@ mod tests {
12461296
);
12471297
}
12481298

1299+
1300+
#[test]
1301+
fn record_external_proof_for_edge_dual_writes_reference_evidence() {
1302+
let conn = Connection::open_in_memory().unwrap();
1303+
crate::db::schema::init_db(&conn).unwrap();
1304+
conn.execute_batch(
1305+
"INSERT INTO symbols (qualified_name, name, kind, language, path, line_start, line_end)
1306+
VALUES ('core/src/engine.rs::Engine::start', 'start', 'method', 'rust',
1307+
'core/src/engine.rs', 6, 8);
1308+
INSERT INTO file_index (path, hash, language, symbol_count, last_indexed)
1309+
VALUES ('app/src/main.rs', 'fresh-source', 'rust', 1, 0);
1310+
INSERT INTO call_sites
1311+
(from_path, enclosing_qn, callee_name, call_line, callee_start_byte,
1312+
callee_end_byte, identity_version, edge_kind)
1313+
VALUES ('app/src/main.rs', 'app/src/main.rs::main', 'start', 5, 4, 9, 2, 'call');",
1314+
)
1315+
.unwrap();
1316+
let call_site_id: i64 = conn
1317+
.query_row("SELECT id FROM call_sites", [], |row| row.get(0))
1318+
.unwrap();
1319+
conn.execute(
1320+
"INSERT INTO call_edges
1321+
(from_symbol, to_symbol, call_site_line, call_site_id, edge_confidence,
1322+
formal_source, from_path, to_path, edge_kind)
1323+
VALUES ('app/src/main.rs::main', 'core/src/engine.rs::Engine::start', 5, ?1,
1324+
'formal', 'scip', 'app/src/main.rs', 'core/src/engine.rs', 'call')",
1325+
[call_site_id],
1326+
)
1327+
.unwrap();
1328+
let edge_id = conn.last_insert_rowid();
1329+
1330+
let context = super::ExternalProofContext::new("scip:test", "test-binary", "test-context");
1331+
super::record_external_proof_for_edge(&conn, &context, edge_id, "scip").unwrap();
1332+
1333+
let proof_count: i64 = conn
1334+
.query_row("SELECT COUNT(*) FROM external_proofs", [], |row| row.get(0))
1335+
.unwrap();
1336+
assert_eq!(proof_count, 1);
1337+
1338+
let (provider, disposition, authority_class, status): (String, String, String, String) = conn
1339+
.query_row(
1340+
"SELECT provider, disposition, authority_class, status FROM reference_evidence",
1341+
[],
1342+
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
1343+
)
1344+
.unwrap();
1345+
assert_eq!(provider, "scip:test");
1346+
assert_eq!(disposition, "supports");
1347+
assert_eq!(authority_class, "external_proof");
1348+
assert_eq!(status, "fresh");
1349+
1350+
// Idempotent re-run (a repeat overlay pass) must not duplicate rows in
1351+
// either table -- same ON CONFLICT guarantee external_proofs already had.
1352+
super::record_external_proof_for_edge(&conn, &context, edge_id, "scip").unwrap();
1353+
let proof_count_again: i64 = conn
1354+
.query_row("SELECT COUNT(*) FROM external_proofs", [], |row| row.get(0))
1355+
.unwrap();
1356+
let evidence_count_again: i64 = conn
1357+
.query_row("SELECT COUNT(*) FROM reference_evidence", [], |row| {
1358+
row.get(0)
1359+
})
1360+
.unwrap();
1361+
assert_eq!(proof_count_again, 1);
1362+
assert_eq!(evidence_count_again, 1);
1363+
}
1364+
12491365
#[test]
12501366
fn ambiguous_guessed_encoding_upgrades_neither_candidate() {
12511367
// Two real call sites at the same line, same callee — mirrors the

0 commit comments

Comments
 (0)