use std::collections::BTreeSet;
use rusqlite::Connection;
pub(crate) struct Migration {
pub version: u32,
pub sql: &'static str,
}
const M0001_INITIAL: &str = "
CREATE TABLE nodes (
id INTEGER PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
kind TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT,
lang TEXT,
blob_hash TEXT,
span_start INTEGER,
span_end INTEGER,
meta TEXT NOT NULL DEFAULT 'null'
);
CREATE TABLE edges (
id INTEGER PRIMARY KEY,
src INTEGER NOT NULL REFERENCES nodes(id),
dst INTEGER NOT NULL REFERENCES nodes(id),
kind TEXT NOT NULL,
provenance TEXT NOT NULL CHECK (provenance IN ('derived','authored','inferred')),
confidence REAL,
src_ref TEXT,
-- A confidence score is present exactly when the edge is inferred,
-- and when present it lies in [0.0, 1.0]. (Rust `Edge::is_valid` is the
-- primary guard and also rejects NaN/inf; this is defence in depth.)
CHECK ((provenance = 'inferred') = (confidence IS NOT NULL)),
CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0))
);
CREATE INDEX idx_nodes_kind ON nodes(kind);
CREATE INDEX idx_edges_src ON edges(src);
CREATE INDEX idx_edges_dst ON edges(dst);
CREATE INDEX idx_edges_provenance ON edges(provenance);
";
const M0002_SYNC_STATE: &str = "
CREATE TABLE sync_state (
id INTEGER PRIMARY KEY CHECK (id = 0),
tree TEXT NOT NULL
);
";
const M0003_EDGE_UNIQUE: &str = "
DELETE FROM edges WHERE id NOT IN (
SELECT MIN(id) FROM edges GROUP BY src, dst, kind, provenance
);
CREATE UNIQUE INDEX idx_edges_unique ON edges(src, dst, kind, provenance);
";
const M0004_IMPORTS: &str = "
CREATE TABLE imports (
src_ref TEXT PRIMARY KEY,
facts TEXT NOT NULL,
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
);
";
const M0005_NODE_CONTEXT: &str = "
CREATE TABLE node_context (
key TEXT PRIMARY KEY,
fingerprint TEXT NOT NULL,
json TEXT NOT NULL
);
";
const M0006_NODE_PROVENANCE: &str = "
ALTER TABLE nodes ADD COLUMN provenance TEXT NOT NULL DEFAULT 'derived'
CHECK (provenance IN ('derived','authored','inferred'));
CREATE INDEX idx_nodes_provenance ON nodes(provenance);
";
const M0007_SYNC_ENV: &str = "
ALTER TABLE sync_state ADD COLUMN env TEXT;
";
const M0008_FINDINGS: &str = "
CREATE TABLE analysis_runs (
id INTEGER PRIMARY KEY,
layer TEXT NOT NULL UNIQUE,
analyzer TEXT NOT NULL,
analyzer_version TEXT NOT NULL,
runner TEXT NOT NULL CHECK (runner IN ('ingested','subprocess','sandboxed')),
isolation TEXT NOT NULL CHECK (isolation IN ('ingested','microvm','none')),
image_digest TEXT,
rules_digest TEXT,
advisory_db_digest TEXT,
advisory_db_published_at TEXT,
command_policy TEXT NOT NULL,
source_commit TEXT,
source_tree TEXT,
source_lockfile_blob TEXT,
started_at TEXT NOT NULL,
ended_at TEXT NOT NULL,
exit_status INTEGER NOT NULL,
report_digest TEXT NOT NULL,
ingested_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX idx_analysis_runs_analyzer ON analysis_runs(analyzer);
CREATE TABLE findings (
id INTEGER PRIMARY KEY,
run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE,
key TEXT NOT NULL,
rule TEXT NOT NULL,
severity TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
path TEXT,
span_start INTEGER,
span_end INTEGER,
meta TEXT NOT NULL DEFAULT 'null',
-- A span is present as a pair or not at all, and never runs backwards.
CHECK ((span_start IS NULL) = (span_end IS NULL)),
CHECK (span_start IS NULL OR span_end >= span_start)
);
CREATE UNIQUE INDEX idx_findings_run_key ON findings(run_id, key);
CREATE INDEX idx_findings_run_severity ON findings(run_id, severity);
";
const M0009_MEDIA_CONTENT: &str = "
CREATE TABLE media_content (
id INTEGER PRIMARY KEY,
blob_id TEXT NOT NULL,
path TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('audio','vision')),
producer TEXT NOT NULL,
model TEXT NOT NULL,
model_digest TEXT NOT NULL,
quantisation TEXT NOT NULL,
mmproj_digest TEXT NOT NULL,
prompt TEXT NOT NULL,
temperature REAL NOT NULL,
max_tokens INTEGER NOT NULL,
tool_version TEXT NOT NULL,
generation INTEGER NOT NULL,
produced_at TEXT NOT NULL DEFAULT (datetime('now')),
text TEXT NOT NULL,
confidence REAL,
-- A confidence signal, when a runtime exposes one, is a probability. It is
-- **not** the score an `inferred` edge carries and must never be read as one.
CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0))
);
CREATE UNIQUE INDEX idx_media_content_blob_producer ON media_content(blob_id, producer);
CREATE INDEX idx_media_content_producer ON media_content(producer);
CREATE INDEX idx_media_content_kind ON media_content(kind);
";
const M0010_MEDIA_GATE: &str = "
CREATE TABLE media_content_v2 (
id INTEGER PRIMARY KEY,
blob_id TEXT NOT NULL,
path TEXT NOT NULL,
kind TEXT NOT NULL CHECK (kind IN ('audio','vision')),
producer TEXT NOT NULL,
model TEXT NOT NULL,
model_digest TEXT NOT NULL,
quantisation TEXT NOT NULL,
mmproj_digest TEXT NOT NULL,
prompt TEXT NOT NULL,
temperature REAL NOT NULL,
max_tokens INTEGER NOT NULL,
tool_version TEXT NOT NULL,
generation INTEGER NOT NULL,
produced_at TEXT NOT NULL DEFAULT (datetime('now')),
text TEXT NOT NULL,
confidence REAL,
skip_reason TEXT CHECK (skip_reason IS NULL OR skip_reason IN ('silence','uniform')),
skip_value REAL,
skip_threshold REAL,
-- A confidence signal, when a runtime exposes one, is a probability. It is
-- **not** the score an `inferred` edge carries and must never be read as one.
CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0)),
-- The two outcomes, and nothing in between. Either the model ran (no skip
-- columns at all), or the gate refused the blob before it did — in which
-- case the measurement is complete AND the row holds no generated text.
--
-- All three skip columns are named in BOTH branches, so they stand or fall
-- together. `skip_reason` in particular must be named in the second branch:
-- it is the discriminant the Rust decoder reads, so a row with a value and a
-- threshold but no reason would come back as *generated content that happens
-- to be empty* — the exact confusion this constraint exists to prevent.
CHECK (
(skip_reason IS NULL AND skip_value IS NULL AND skip_threshold IS NULL)
OR (skip_reason IS NOT NULL AND skip_value IS NOT NULL
AND skip_threshold IS NOT NULL AND text = '')
)
);
INSERT INTO media_content_v2 (
id, blob_id, path, kind, producer, model, model_digest, quantisation, mmproj_digest,
prompt, temperature, max_tokens, tool_version, generation, produced_at, text, confidence
) SELECT
id, blob_id, path, kind, producer, model, model_digest, quantisation, mmproj_digest,
prompt, temperature, max_tokens, tool_version, generation, produced_at, text, confidence
FROM media_content;
DROP TABLE media_content;
ALTER TABLE media_content_v2 RENAME TO media_content;
CREATE UNIQUE INDEX idx_media_content_blob_producer ON media_content(blob_id, producer);
CREATE INDEX idx_media_content_producer ON media_content(producer);
CREATE INDEX idx_media_content_kind ON media_content(kind);
";
const M0011_AGENT_MEMORY: &str = "
CREATE TABLE agent_memory (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- A coarse NAMESPACE — which repo or project a record belongs to in a
-- multi-repo workspace. **Not a branch label, and never to be repurposed as
-- one.** Whether a record applies to the tree in front of you is decided by
-- resolving its anchor below, not by where it was written: see ADR-0013
-- §Scope. Nothing keys off this column but an exact-match filter.
scope TEXT NOT NULL,
kind TEXT NOT NULL
CHECK (kind IN ('lesson','attempt','decision','pattern','outcome')),
anchor_key TEXT,
anchor_blob TEXT,
anchor_path TEXT,
body TEXT NOT NULL,
confidence REAL,
tree TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
superseded_by INTEGER REFERENCES agent_memory(id),
superseded_at TEXT,
-- A scope and a body that are present but empty are corrupt writes, not
-- minimal ones: an empty memory records nothing and can never be recalled.
CHECK (scope <> ''),
CHECK (body <> ''),
-- A self-reported confidence, when a caller offers one, is a probability. It
-- is **not** the score an `inferred` edge carries and must never be read as
-- one: no memory record is a graph fact.
CHECK (confidence IS NULL OR (confidence >= 0.0 AND confidence <= 1.0)),
-- Anchor evidence without an anchor key names nothing.
CHECK (anchor_blob IS NULL OR anchor_key IS NOT NULL),
CHECK (anchor_path IS NULL OR anchor_key IS NOT NULL),
-- Supersession is recorded, never inferred: the successor and the moment
-- stand or fall together, and nothing supersedes itself.
CHECK ((superseded_by IS NULL) = (superseded_at IS NULL)),
CHECK (superseded_by IS NULL OR superseded_by <> id)
);
CREATE INDEX idx_mem_anchor ON agent_memory(anchor_key);
CREATE INDEX idx_mem_live ON agent_memory(scope, superseded_by, id DESC);
";
const M0012_SYNC_WORKTREE: &str = "
ALTER TABLE sync_state ADD COLUMN worktree TEXT;
";
const M0013_AGENT_CACHE: &str = "
CREATE TABLE agent_cache (
key TEXT PRIMARY KEY,
-- The content-addressed freshness witness, on `node_context`'s terms: a
-- reader compares it with the fingerprint the current graph yields and treats
-- a mismatch as a miss. Eviction is a *capacity* policy and is orthogonal to
-- this one — the house idiom for staleness stays key/fingerprint invalidation.
fingerprint TEXT NOT NULL,
json TEXT NOT NULL,
-- The row's own payload size, so the sweep can order and total by bytes
-- without reading every `json` in the tier.
bytes INTEGER NOT NULL,
-- The sweep generation this row was written in (see `agent_cache_clock`).
generation INTEGER NOT NULL,
-- The access tick this row was last read or written at. Strictly increasing,
-- never a wall-clock: this is `ModelCache`'s list position, made durable.
last_used INTEGER NOT NULL,
hits INTEGER NOT NULL DEFAULT 0,
anchor_key TEXT,
anchor_blob TEXT,
-- Half-states, refused on migration 10's and 11's precedent.
CHECK (key <> ''),
CHECK (bytes >= 0),
CHECK (generation >= 0),
CHECK (last_used >= 0),
CHECK (hits >= 0),
-- Anchor evidence without an anchor key names nothing and can never be
-- checked for drift — the same constraint `agent_memory` carries.
CHECK (anchor_blob IS NULL OR anchor_key IS NOT NULL)
);
-- The eviction order, as an index: least-recently-used first.
CREATE INDEX idx_cache_evict ON agent_cache(last_used);
CREATE INDEX idx_cache_anchor ON agent_cache(anchor_key);
-- The logical clock the two counters above are drawn from. A single row, on
-- `sync_state`'s precedent (migration 2), because there is exactly one of it.
CREATE TABLE agent_cache_clock (
id INTEGER PRIMARY KEY CHECK (id = 0),
ticks INTEGER NOT NULL DEFAULT 0 CHECK (ticks >= 0),
generation INTEGER NOT NULL DEFAULT 0 CHECK (generation >= 0)
);
INSERT INTO agent_cache_clock (id) VALUES (0);
";
pub(crate) const MIGRATIONS: &[Migration] = &[
Migration {
version: 1,
sql: M0001_INITIAL,
},
Migration {
version: 2,
sql: M0002_SYNC_STATE,
},
Migration {
version: 3,
sql: M0003_EDGE_UNIQUE,
},
Migration {
version: 4,
sql: M0004_IMPORTS,
},
Migration {
version: 5,
sql: M0005_NODE_CONTEXT,
},
Migration {
version: 6,
sql: M0006_NODE_PROVENANCE,
},
Migration {
version: 7,
sql: M0007_SYNC_ENV,
},
Migration {
version: 8,
sql: M0008_FINDINGS,
},
Migration {
version: 9,
sql: M0009_MEDIA_CONTENT,
},
Migration {
version: 10,
sql: M0010_MEDIA_GATE,
},
Migration {
version: 11,
sql: M0011_AGENT_MEMORY,
},
Migration {
version: 12,
sql: M0012_SYNC_WORKTREE,
},
Migration {
version: 13,
sql: M0013_AGENT_CACHE,
},
];
const _: () = {
let mut i = 1;
while i < MIGRATIONS.len() {
assert!(
MIGRATIONS[i - 1].version < MIGRATIONS[i].version,
"MIGRATIONS must be in strictly ascending version order, with no \
duplicates: `apply` runs them in slice order and a later migration \
may depend on an earlier one"
);
i += 1;
}
};
pub(crate) fn latest_version() -> u32 {
match MIGRATIONS.last() {
Some(m) => m.version,
None => 0,
}
}
fn applied_versions(conn: &Connection) -> rusqlite::Result<BTreeSet<u32>> {
let mut stmt = conn.prepare("SELECT version FROM schema_migrations")?;
let rows = stmt.query_map([], |r| r.get::<_, i64>(0))?;
let mut applied = BTreeSet::new();
for row in rows {
if let Ok(v) = u32::try_from(row?) {
applied.insert(v);
}
}
Ok(applied)
}
pub(crate) fn apply(conn: &mut Connection) -> rusqlite::Result<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)?;
let applied = applied_versions(conn)?;
for m in MIGRATIONS {
if !applied.contains(&m.version) {
let tx = conn.transaction()?;
tx.execute_batch(m.sql)?;
tx.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[m.version],
)?;
tx.commit()?;
}
}
Ok(())
}
fn contiguous_version(applied: &BTreeSet<u32>) -> u32 {
let mut v = 0;
while applied.contains(&(v + 1)) {
v += 1;
}
v
}
pub(crate) fn store_version(conn: &Connection) -> rusqlite::Result<u32> {
match applied_versions(conn) {
Ok(applied) => Ok(contiguous_version(&applied)),
Err(rusqlite::Error::SqliteFailure(_, Some(ref msg))) if msg.contains("no such table") => {
Ok(0)
}
Err(e) => Err(e),
}
}
pub(crate) fn versions_ahead_of_build(conn: &Connection) -> rusqlite::Result<Vec<u32>> {
let latest = latest_version();
match applied_versions(conn) {
Ok(applied) => Ok(applied.range(latest.saturating_add(1)..).copied().collect()),
Err(rusqlite::Error::SqliteFailure(_, Some(ref msg))) if msg.contains("no such table") => {
Ok(Vec::new())
}
Err(e) => Err(e),
}
}
#[cfg(test)]
mod tests {
use super::{MIGRATIONS, apply, latest_version};
use rusqlite::Connection;
fn recorded_versions(conn: &Connection) -> Vec<u32> {
let mut stmt = conn
.prepare("SELECT version FROM schema_migrations ORDER BY version")
.expect("prepare");
stmt.query_map([], |r| r.get::<_, u32>(0))
.expect("query")
.collect::<Result<Vec<_>, _>>()
.expect("collect")
}
fn store_missing_migration(skip: u32, future: Option<u32>) -> Option<Connection> {
let conn = Connection::open_in_memory().expect("open");
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.expect("bootstrap");
for m in MIGRATIONS.iter().filter(|m| m.version != skip) {
conn.execute_batch(m.sql).ok()?;
conn.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[m.version],
)
.expect("record");
}
if let Some(future) = future {
assert!(
!MIGRATIONS.iter().any(|m| m.version == future),
"`future` must be a version this build does not carry; {future} is \
in MIGRATIONS and was already recorded above"
);
conn.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[future],
)
.expect("record future");
}
Some(conn)
}
fn has_column(conn: &Connection, table: &str, column: &str) -> bool {
let mut stmt = conn
.prepare(&format!("PRAGMA table_info({table})"))
.expect("prepare");
stmt.query_map([], |r| r.get::<_, String>(1))
.expect("query")
.filter_map(Result::ok)
.any(|c| c == column)
}
#[test]
fn a_migration_skipped_below_a_higher_one_is_still_applied() {
let skipped = 12;
let higher = 13;
let mut conn = store_missing_migration(skipped, None).expect("12 is omissible");
assert!(
!recorded_versions(&conn).contains(&skipped),
"seeded store must be missing migration {skipped}"
);
assert!(
recorded_versions(&conn).contains(&higher),
"seeded store must carry the other branch's higher version"
);
assert!(
!has_column(&conn, "sync_state", "worktree"),
"the skipped migration's column must be absent to begin with"
);
apply(&mut conn).expect("apply must repair the gap");
assert!(
recorded_versions(&conn).contains(&skipped),
"migration {skipped} must be recorded after the repair, not skipped \
forever because a higher version was already stamped"
);
assert!(
has_column(&conn, "sync_state", "worktree"),
"the repaired migration's column must exist — a recorded version \
without its schema is the same silent wrong answer in a new costume"
);
assert!(
recorded_versions(&conn).contains(&higher),
"another branch's recorded migration must survive the repair"
);
let clock: (i64, i64) = conn
.query_row(
"SELECT ticks, generation FROM agent_cache_clock WHERE id = 0",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.expect("migration 13's seeded clock row must survive");
assert_eq!(clock, (0, 0), "the seeded clock row is untouched");
let after = recorded_versions(&conn);
apply(&mut conn).expect("second apply");
assert_eq!(after, recorded_versions(&conn), "repair is idempotent");
}
#[test]
fn every_reachable_gap_is_repaired_wherever_it_sits() {
let mut repaired = 0;
for m in MIGRATIONS {
let Some(mut conn) = store_missing_migration(m.version, Some(latest_version() + 1))
else {
continue; };
apply(&mut conn).unwrap_or_else(|e| panic!("repair {}: {e}", m.version));
assert!(
recorded_versions(&conn).contains(&m.version),
"migration {} must be repaired, not skipped because a higher \
version was already recorded",
m.version
);
repaired += 1;
}
assert!(
repaired >= 2,
"expected several omissible migrations, exercised {repaired}"
);
}
#[test]
fn the_reported_version_stops_below_a_gap_rather_than_overstating_it() {
let conn = store_missing_migration(12, None).expect("12 is omissible");
assert_eq!(
super::store_version(&conn).expect("version"),
11,
"with 12 missing, the store provides 11 — reporting 13 (the maximum \
recorded) would name a schema that is not there"
);
let mut conn = conn;
apply(&mut conn).expect("repair");
assert_eq!(
super::store_version(&conn).expect("version"),
13,
"1..=13 contiguous after the repair"
);
let empty = Connection::open_in_memory().expect("open");
empty
.execute_batch(
"CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now')));",
)
.expect("bootstrap");
assert_eq!(super::store_version(&empty).expect("version"), 0);
let bare = Connection::open_in_memory().expect("open");
assert_eq!(
super::store_version(&bare).expect("version"),
0,
"no migration table reads as `no migrations applied`, not an error"
);
}
#[test]
fn migrations_are_strictly_ascending_and_unique() {
let versions: Vec<u32> = MIGRATIONS.iter().map(|m| m.version).collect();
let mut sorted = versions.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(versions, sorted, "must be ascending with no duplicates");
assert_eq!(
versions.first().copied(),
Some(1),
"versions start at 1, which `schema_version`'s contiguity walk assumes"
);
}
#[test]
fn apply_is_idempotent() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("first apply");
let after_first = recorded_versions(&conn);
apply(&mut conn).expect("second apply");
let after_second = recorded_versions(&conn);
assert_eq!(after_first, after_second, "re-applying must not add rows");
assert_eq!(
after_second.last().copied(),
Some(latest_version()),
"schema should be at the latest version"
);
}
#[test]
fn a_later_migration_is_additive_on_a_populated_store() {
let mut conn = Connection::open_in_memory().expect("open");
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.expect("bootstrap");
for m in MIGRATIONS
.iter()
.take_while(|m| m.version < latest_version())
{
conn.execute_batch(m.sql).expect("legacy migration");
conn.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[m.version],
)
.expect("record");
}
conn.execute(
"INSERT INTO nodes (key, kind, name) VALUES ('sym:rust:a.rs#main', 'fn', 'main')",
[],
)
.expect("seed node");
apply(&mut conn).expect("upgrade");
assert_eq!(
recorded_versions(&conn).last().copied(),
Some(latest_version())
);
let nodes: i64 = conn
.query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))
.expect("count");
assert_eq!(nodes, 1, "an upgrade must not disturb existing rows");
for table in ["findings", "media_content", "agent_memory"] {
let rows: i64 = conn
.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |r| r.get(0))
.expect("count");
assert_eq!(rows, 0, "{table} starts empty");
}
}
fn insert_outcome(
conn: &Connection,
blob: &str,
text: &str,
skip_reason: Option<&str>,
skip_value: Option<f64>,
skip_threshold: Option<f64>,
) -> bool {
conn.execute(
"INSERT INTO media_content (
blob_id, path, kind, producer, model, model_digest, quantisation,
mmproj_digest, prompt, temperature, max_tokens, tool_version, generation,
text, skip_reason, skip_value, skip_threshold
) VALUES (
?1, 'assets/clip.wav', 'audio', 'media:audio:m:0', 'm', 'd', 'Q4_K_M',
'p', 'prompt', 0.0, 512, '9.9.9', 1, ?2, ?3, ?4, ?5
)",
rusqlite::params![blob, text, skip_reason, skip_value, skip_threshold],
)
.is_ok()
}
#[test]
fn the_media_outcome_constraint_admits_only_the_two_real_outcomes() {
type Rejected = (
&'static str,
&'static str,
Option<&'static str>,
Option<f64>,
Option<f64>,
);
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
assert!(
insert_outcome(&conn, "blob-generated", "a transcript", None, None, None),
"a generated record carries text and no measurement",
);
assert!(
insert_outcome(
&conn,
"blob-skipped",
"",
Some("silence"),
Some(0.0),
Some(1e-4)
),
"a gated skip carries a complete measurement and no text",
);
let rejected: [Rejected; 6] = [
(
"value-and-threshold-but-no-reason",
"",
None,
Some(0.0),
Some(1e-4),
),
(
"reason-with-no-value",
"",
Some("silence"),
None,
Some(1e-4),
),
(
"reason-with-no-threshold",
"",
Some("silence"),
Some(0.0),
None,
),
("value-alone", "", None, Some(0.0), None),
("threshold-alone", "", None, None, Some(1e-4)),
(
"a-skip-that-also-has-text",
"a transcript",
Some("silence"),
Some(0.0),
Some(1e-4),
),
];
for (name, text, reason, value, threshold) in rejected {
assert!(
!insert_outcome(&conn, name, text, reason, value, threshold),
"`{name}` must be rejected by the outcome CHECK, not stored",
);
}
let stored: i64 = conn
.query_row("SELECT COUNT(*) FROM media_content", [], |r| r.get(0))
.expect("count");
assert_eq!(stored, 2, "no refused shape may have slipped through");
}
#[test]
fn the_media_migration_leaves_the_provenance_vocabulary_alone() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
for provenance in ["derived", "authored", "inferred"] {
conn.execute(
"INSERT INTO nodes (key, kind, name, provenance) VALUES (?1, 'fn', 'n', ?2)",
[provenance, provenance],
)
.unwrap_or_else(|e| panic!("{provenance} must remain a valid node provenance: {e}"));
}
for rejected in ["generated", "media", ""] {
assert!(
conn.execute(
"INSERT INTO nodes (key, kind, name, provenance) VALUES (?1, 'fn', 'n', ?2)",
[&format!("bad-{rejected}"), rejected],
)
.is_err(),
"{rejected:?} must not be an accepted node provenance"
);
assert!(
conn.execute(
"INSERT INTO edges (src, dst, kind, provenance) VALUES (1, 1, 'calls', ?1)",
[rejected],
)
.is_err(),
"{rejected:?} must not be an accepted edge provenance"
);
}
let columns: Vec<String> = {
let mut stmt = conn
.prepare("SELECT name FROM pragma_table_info('media_content')")
.expect("prepare");
stmt.query_map([], |r| r.get::<_, String>(0))
.expect("query")
.collect::<Result<_, _>>()
.expect("collect")
};
assert!(
!columns.iter().any(|c| c == "provenance"),
"generated content must not carry a provenance class: {columns:?}"
);
}
#[test]
fn media_content_is_unique_per_blob_and_producer() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let insert = |producer: &str, kind: &str| {
conn.execute(
"INSERT INTO media_content (
blob_id, path, kind, producer, model, model_digest, quantisation,
mmproj_digest, prompt, temperature, max_tokens, tool_version, generation, text
) VALUES ('blob1', 'a.wav', ?2, ?1, 'm', 'd', 'Q4', 'p', 'say', 0.0, 8, '1.0', 1, 't')",
[producer, kind],
)
};
assert!(insert("media:audio:m:1", "audio").is_ok());
assert!(
insert("media:audio:m:1", "audio").is_err(),
"the same producer must not describe one blob twice"
);
assert!(
insert("media:audio:m:2", "audio").is_ok(),
"a different producer is a new record, not a mutation"
);
assert!(
insert("media:vision:m:3", "ocr").is_err(),
"`ocr` is not a generative modality and has no token"
);
}
#[test]
fn findings_schema_rejects_unknown_runner_and_isolation_tokens() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let insert = |runner: &str, isolation: &str| {
conn.execute(
"INSERT INTO analysis_runs (
layer, analyzer, analyzer_version, runner, isolation, command_policy,
started_at, ended_at, exit_status, report_digest
) VALUES ('security:a:b', 'a', '1', ?1, ?2, '{}', 's', 'e', 0, 'd')",
[runner, isolation],
)
};
assert!(insert("ingested", "ingested").is_ok());
assert!(insert("teleported", "ingested").is_err());
assert!(insert("ingested", "airgapped").is_err());
}
fn insert_memory(conn: &Connection, scope: &str, kind: &str, body: &str) -> bool {
conn.execute(
"INSERT INTO agent_memory (scope, kind, body) VALUES (?1, ?2, ?3)",
[scope, kind, body],
)
.is_ok()
}
#[test]
fn agent_memory_constrains_its_vocabulary_scope_and_body() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
for kind in ["lesson", "attempt", "decision", "pattern", "outcome"] {
assert!(
insert_memory(&conn, "repo", kind, "a body"),
"`{kind}` must remain a valid memory kind",
);
}
for kind in ["note", "Lesson", "lessons", "derived", ""] {
assert!(
!insert_memory(&conn, "repo", kind, "a body"),
"{kind:?} must not be an accepted memory kind",
);
}
assert!(!insert_memory(&conn, "", "lesson", "a body"), "empty scope");
assert!(!insert_memory(&conn, "repo", "lesson", ""), "empty body");
let with_confidence = |c: f64| {
conn.execute(
"INSERT INTO agent_memory (scope, kind, body, confidence)
VALUES ('repo', 'lesson', 'b', ?1)",
[c],
)
.is_ok()
};
assert!(with_confidence(0.0) && with_confidence(1.0));
assert!(!with_confidence(-0.1) && !with_confidence(1.1));
}
#[test]
fn agent_memory_supersession_columns_stand_or_fall_together() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
assert!(insert_memory(&conn, "repo", "lesson", "the old finding"));
assert!(insert_memory(&conn, "repo", "lesson", "the new finding"));
let update = |by: Option<i64>, at: Option<&str>| {
conn.execute(
"UPDATE agent_memory SET superseded_by = ?1, superseded_at = ?2 WHERE id = 1",
rusqlite::params![by, at],
)
.is_ok()
};
assert!(!update(Some(2), None), "a successor with no moment");
assert!(
!update(None, Some("2026-01-01")),
"a moment with no successor"
);
assert!(!update(Some(1), Some("2026-01-01")), "self-supersession");
assert!(
update(Some(2), Some("2026-01-01")),
"the one legitimate shape"
);
assert!(
update(None, None),
"and clearing it again is legitimate too"
);
}
#[test]
fn agent_memory_refuses_anchor_evidence_without_a_key() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let insert = |key: Option<&str>, blob: Option<&str>, path: Option<&str>| {
conn.execute(
"INSERT INTO agent_memory (scope, kind, body, anchor_key, anchor_blob, anchor_path)
VALUES ('repo', 'lesson', 'b', ?1, ?2, ?3)",
rusqlite::params![key, blob, path],
)
.is_ok()
};
assert!(
insert(None, None, None),
"an unanchored memory is legitimate"
);
assert!(insert(Some("sym:rust:a.rs#f"), Some("blob1"), Some("a.rs")));
assert!(!insert(None, Some("blob1"), None), "a blob naming no node");
assert!(!insert(None, None, Some("a.rs")), "a path naming no node");
}
#[test]
fn agent_memory_ids_are_never_reused_after_a_delete() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
assert!(insert_memory(&conn, "repo", "lesson", "first"));
assert!(insert_memory(&conn, "repo", "lesson", "second"));
conn.execute("DELETE FROM agent_memory WHERE id = 2", [])
.expect("delete the newest");
assert!(insert_memory(&conn, "repo", "lesson", "third"));
let id: i64 = conn
.query_row(
"SELECT id FROM agent_memory WHERE body = 'third'",
[],
|r| r.get(0),
)
.expect("query");
assert_eq!(id, 3, "a forgotten id must never be handed out again");
}
#[test]
fn the_memory_migration_leaves_the_provenance_vocabulary_alone() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
for provenance in ["derived", "authored", "inferred"] {
conn.execute(
"INSERT INTO nodes (key, kind, name, provenance) VALUES (?1, 'fn', 'n', ?2)",
[provenance, provenance],
)
.unwrap_or_else(|e| panic!("{provenance} must remain a valid node provenance: {e}"));
}
for rejected in ["memory", "episodic", "remembered", ""] {
assert!(
conn.execute(
"INSERT INTO nodes (key, kind, name, provenance) VALUES (?1, 'fn', 'n', ?2)",
[&format!("bad-{rejected}"), rejected],
)
.is_err(),
"{rejected:?} must not be an accepted node provenance"
);
assert!(
conn.execute(
"INSERT INTO edges (src, dst, kind, provenance) VALUES (1, 1, 'calls', ?1)",
[rejected],
)
.is_err(),
"{rejected:?} must not be an accepted edge provenance"
);
}
let columns: Vec<String> = {
let mut stmt = conn
.prepare("SELECT name FROM pragma_table_info('agent_memory')")
.expect("prepare");
stmt.query_map([], |r| r.get::<_, String>(0))
.expect("query")
.collect::<Result<_, _>>()
.expect("collect")
};
assert!(
!columns.iter().any(|c| c == "provenance"),
"a memory record must not carry a provenance class: {columns:?}"
);
assert!(
!columns.iter().any(|c| c.starts_with("span")),
"a span is byte offsets, not an anchor: {columns:?}"
);
}
#[test]
fn apply_creates_core_tables() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
for table in [
"nodes",
"edges",
"imports",
"analysis_runs",
"findings",
"media_content",
"agent_memory",
"agent_cache",
"agent_cache_clock",
"schema_migrations",
] {
let count: i64 = conn
.query_row(
"SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = ?1",
[table],
|r| r.get(0),
)
.expect("query");
assert_eq!(count, 1, "table {table} should exist");
}
}
#[test]
fn migration_versions_are_unique_and_ascending() {
let versions: Vec<u32> = MIGRATIONS.iter().map(|m| m.version).collect();
let mut sorted = versions.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
versions.len(),
sorted.len(),
"two migrations share a version — which is exactly what a same-number \
collision between two branches looks like after a clean merge: {versions:?}",
);
assert_eq!(
versions, sorted,
"the migration list is out of order, so `apply` would run them out of \
order: {versions:?}",
);
assert!(
versions.first().is_some_and(|&v| v >= 1),
"version 0 is the `nothing applied yet` sentinel and cannot be a migration",
);
}
#[test]
fn agent_cache_refuses_negative_counters_and_orphan_anchor_evidence() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let insert = |key: &str, bytes: i64, generation: i64, last_used: i64, hits: i64| {
conn.execute(
"INSERT INTO agent_cache (key, fingerprint, json, bytes, generation, last_used, hits)
VALUES (?1, 'fp', '{}', ?2, ?3, ?4, ?5)",
rusqlite::params![key, bytes, generation, last_used, hits],
)
.is_ok()
};
assert!(insert("ok", 0, 0, 0, 0), "a zero-size entry is legitimate");
assert!(!insert("", 1, 0, 0, 0), "an empty key names nothing");
assert!(!insert("neg-bytes", -1, 0, 0, 0));
assert!(!insert("neg-gen", 1, -1, 0, 0));
assert!(!insert("neg-used", 1, 0, -1, 0));
assert!(!insert("neg-hits", 1, 0, 0, -1));
let anchored = |key: &str, anchor: Option<&str>, blob: Option<&str>| {
conn.execute(
"INSERT INTO agent_cache
(key, fingerprint, json, bytes, generation, last_used, anchor_key, anchor_blob)
VALUES (?1, 'fp', '{}', 1, 0, 0, ?2, ?3)",
rusqlite::params![key, anchor, blob],
)
.is_ok()
};
assert!(anchored("unanchored", None, None));
assert!(anchored("anchored", Some("sym:rust:a.rs#f"), Some("blob1")));
assert!(
!anchored("orphan-blob", None, Some("blob1")),
"a blob naming no node",
);
}
#[test]
fn the_cache_clock_is_a_single_seeded_row() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let (ticks, generation): (i64, i64) = conn
.query_row(
"SELECT ticks, generation FROM agent_cache_clock WHERE id = 0",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.expect("the migration seeds the clock");
assert_eq!((ticks, generation), (0, 0));
assert!(
conn.execute("INSERT INTO agent_cache_clock (id) VALUES (1)", [])
.is_err(),
"there is exactly one clock",
);
}
#[test]
fn only_the_cache_tier_carries_the_eviction_signal() {
let mut conn = Connection::open_in_memory().expect("open");
apply(&mut conn).expect("apply");
let columns = |table: &str| -> Vec<String> {
let mut stmt = conn
.prepare(&format!("SELECT name FROM pragma_table_info('{table}')"))
.expect("prepare");
stmt.query_map([], |r| r.get::<_, String>(0))
.expect("query")
.collect::<Result<Vec<_>, _>>()
.expect("collect")
};
let cache = columns("agent_cache");
let memory = columns("agent_memory");
for signal in ["bytes", "generation", "last_used", "hits"] {
assert!(
cache.iter().any(|c| c == signal),
"the cache tier carries {signal}",
);
assert!(
!memory.iter().any(|c| c == signal),
"{signal} must not exist on the episodic tier: nothing sweeps it",
);
}
}
}