Ship the SR-003 schema bump: jmanifest_version 2
CI / fmt, clippy, test (push) Failing after 1m22s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 26s

Moves the exchange envelope to version 2 in lockstep with the truth file's
schema_version, per SR-003's requirement that breaking changes be batched and
ship together rather than piecemeal. The plugin had already moved to
schema_version 2; the server declaring 1 while accepting the new fields
defeated the point of having a version at all.

Flag day, not dual-accept (JR-003): version 1 is now rejected outright. All
three components are pre-release, and a v1 read path would be the one nobody
exercises, so it is the one that would rot while being dragged through every
later change to the reader. A pipeline still emitting v1 is incompatible until
updated — stated plainly rather than papered over with a shim nobody tests.

scenes become objects carrying belief and route (extraction AR-017) instead of
float pairs. Belief is bounded to [0, 1] rather than merely stored: §5a's
Threat 1 argument rests on every accepted value being bounded, and an unbounded
float is a 64-bit channel however harmless it looks. route is a closed enum, so
an invented value cannot be stored.

UR-018 is the requirement with the trap in it, and the reason content_id.rs is
untouched by this commit: belief is a producer-side estimate that may
legitimately differ between pipeline versions for identical timings, so
including it in the canonical form would give two servers different ids for the
same content — the exact failure mode §9a quantises centiseconds to avoid,
reintroduced one field along. It replicates as an attribute, exactly as
audio_signature does. The golden vector still passes unchanged, which is the
evidence rather than the claim.

191 tests. UR-015..018 move from Planned to Done; coverage 24/32 (75%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: UR-014, UR-015, UR-016, UR-017, UR-018 | SR-003
This commit is contained in:
2026-07-31 09:13:14 +02:00
co-authored by Claude Opus 5
parent c73f417d45
commit 88c7264094
12 changed files with 494 additions and 179 deletions
+38 -11
View File
@@ -374,7 +374,7 @@ pub fn insert_actor_scenes(
tx: &Transaction<'_>,
manifest_id: &str,
tmdb_person_id: u64,
scenes_cs: &[(i64, i64)],
scenes_cs: &[crate::validate::SceneCs],
) -> anyhow::Result<()> {
tx.execute(
"INSERT INTO manifest_actors (manifest_id, tmdb_person_id) VALUES (?1, ?2)
@@ -382,11 +382,18 @@ pub fn insert_actor_scenes(
params![manifest_id, tmdb_person_id as i64],
)?;
let mut stmt = tx.prepare_cached(
"INSERT INTO scenes (manifest_id, tmdb_person_id, start_cs, end_cs)
VALUES (?1, ?2, ?3, ?4)",
"INSERT INTO scenes (manifest_id, tmdb_person_id, start_cs, end_cs, belief, route)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)?;
for (start, end) in scenes_cs {
stmt.execute(params![manifest_id, tmdb_person_id as i64, start, end])?;
for scene in scenes_cs {
stmt.execute(params![
manifest_id,
tmdb_person_id as i64,
scene.start_cs,
scene.end_cs,
scene.belief,
scene.route.map(|r| r.as_str()),
])?;
}
Ok(())
}
@@ -399,7 +406,7 @@ pub fn insert_actor_scenes(
pub struct StoredActor {
pub tmdb_person_id: u64,
pub name: Option<String>,
pub scenes_cs: Vec<(i64, i64)>,
pub scenes_cs: Vec<crate::validate::SceneCs>,
}
/// TRACES: UR-010 | DR-002 | SR-001
@@ -421,7 +428,7 @@ pub fn actors_for_manifest(
.collect::<rusqlite::Result<Vec<_>>>()?;
let mut scene_stmt = conn.prepare_cached(
"SELECT start_cs, end_cs FROM scenes
"SELECT start_cs, end_cs, belief, route FROM scenes
WHERE manifest_id = ?1 AND tmdb_person_id = ?2
ORDER BY start_cs ASC",
)?;
@@ -429,7 +436,18 @@ pub fn actors_for_manifest(
let mut out = Vec::with_capacity(people.len());
for (id, name) in people {
let scenes_cs = scene_stmt
.query_map(params![manifest_id, id as i64], |r| Ok((r.get(0)?, r.get(1)?)))?
.query_map(params![manifest_id, id as i64], |r| {
Ok(crate::validate::SceneCs {
start_cs: r.get(0)?,
end_cs: r.get(1)?,
belief: r.get(2)?,
// An unrecognised stored route means a row from a schema
// this build does not know; report absent rather than guess.
route: r
.get::<_, Option<String>>(3)?
.and_then(|v| crate::model::Route::from_stored(&v)),
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
out.push(StoredActor { tmdb_person_id: id, name, scenes_cs });
}
@@ -742,6 +760,7 @@ pub fn release_all_leases(tx: &Transaction<'_>) -> anyhow::Result<usize> {
mod tests {
use super::*;
use crate::db::Db;
use crate::validate::SceneCs;
async fn db() -> Db {
Db::open(":memory:").unwrap()
@@ -837,7 +856,12 @@ mod tests {
},
)?;
upsert_person(tx, 884, "Steve Buscemi", false, NOW)?;
insert_actor_scenes(tx, &id, 884, &[(19160, 20920), (43820, 46560)])?;
insert_actor_scenes(
tx,
&id,
884,
&[SceneCs::plain(19160, 20920), SceneCs::plain(43820, 46560)],
)?;
Ok(id)
})
.await
@@ -857,7 +881,10 @@ mod tests {
assert_eq!(actors[0].tmdb_person_id, 884);
// §7: names come from `people`, populated from TMDB, never from upload.
assert_eq!(actors[0].name.as_deref(), Some("Steve Buscemi"));
assert_eq!(actors[0].scenes_cs, vec![(19160, 20920), (43820, 46560)]);
assert_eq!(
actors[0].scenes_cs,
vec![SceneCs::plain(19160, 20920), SceneCs::plain(43820, 46560)]
);
}
#[tokio::test]
@@ -1097,7 +1124,7 @@ mod tests {
},
)?;
upsert_person(tx, 1, "A", false, NOW)?;
insert_actor_scenes(tx, "m", 1, &[(0, 100)])?;
insert_actor_scenes(tx, "m", 1, &[SceneCs::plain(0, 100)])?;
delete_manifest(tx, "m")?;
let scenes: i64 = tx.query_row("SELECT COUNT(*) FROM scenes", [], |r| r.get(0))?;
let actors: i64 =
+7 -1
View File
@@ -70,7 +70,13 @@ CREATE TABLE IF NOT EXISTS scenes (
manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE,
tmdb_person_id INTEGER NOT NULL,
start_cs INTEGER NOT NULL,
end_cs INTEGER NOT NULL
end_cs INTEGER NOT NULL,
-- Per-window provenance (SR-003, extraction AR-017). Deliberately NOT part
-- of `content_id`: belief is a producer-side estimate that may differ
-- between pipeline versions for identical timings, so hashing it would give
-- two servers different ids for the same content (§9a).
belief REAL,
route TEXT -- live | deferred | pooled
);
CREATE TABLE IF NOT EXISTS reports (