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);
";
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,
},
];
#[cfg(test)]
pub(crate) fn latest_version() -> u32 {
MIGRATIONS.iter().map(|m| m.version).max().unwrap_or(0)
}
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 current: i64 = conn.query_row(
"SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
[],
|r| r.get(0),
)?;
for m in MIGRATIONS {
if i64::from(m.version) > current {
let tx = conn.transaction()?;
tx.execute_batch(m.sql)?;
tx.execute(
"INSERT INTO schema_migrations (version) VALUES (?1)",
[m.version],
)?;
tx.commit()?;
}
}
Ok(())
}
#[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")
}
#[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",
"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");
}
}
}