Ship the SR-003 schema bump: jmanifest_version 2
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:
+72
-18
@@ -164,8 +164,16 @@ pub fn compute_content_id(valid: &ValidManifest) -> String {
|
||||
.actor_scenes_cs
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
a.tmdb_id
|
||||
.map(|id| CanonicalActor { tmdb_person_id: id, scenes_cs: a.scenes_cs.clone() })
|
||||
a.tmdb_id.map(|id| CanonicalActor {
|
||||
tmdb_person_id: id,
|
||||
// Timings only. §9a excludes belief and route from identity:
|
||||
// they are producer-side estimates that may differ between
|
||||
// pipeline versions for identical content, so hashing them
|
||||
// would give two servers different ids for the same
|
||||
// manifest — the failure mode centisecond quantisation
|
||||
// exists to remove. They replicate as attributes instead.
|
||||
scenes_cs: a.scenes_cs.iter().map(|s| (s.start_cs, s.end_cs)).collect(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -188,12 +196,12 @@ mod tests {
|
||||
|
||||
fn movie_json(tmdb: &str, runtime: f64) -> String {
|
||||
format!(
|
||||
r#"{{"jmanifest_version":1,
|
||||
r#"{{"jmanifest_version":2,
|
||||
"identity":{{"type":"movie","tmdb_id":"{tmdb}","title":"A Film"}},
|
||||
"cut":{{"runtime_sec":{runtime}}},
|
||||
"extraction":{{"sample_fps":5,"pipeline_version":"test 0.1"}},
|
||||
"actors":[{{"name":"Steve Buscemi","tmdb_id":"884","scenes":[[10.0,20.0]]}},
|
||||
{{"name":"Michael Palin","tmdb_id":"11007","scenes":[[30.0,40.0]]}}]}}"#
|
||||
"actors":[{{"name":"Steve Buscemi","tmdb_id":"884","scenes":[{{"start":10.0,"end":20.0}}]}},
|
||||
{{"name":"Michael Palin","tmdb_id":"11007","scenes":[{{"start":30.0,"end":40.0}}]}}]}}"#
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,10 +278,10 @@ mod tests {
|
||||
// so this exercises the per-contributor 409 path specifically.
|
||||
let a = valid_from(&movie_json("504172", 6420.5));
|
||||
let b = valid_from(
|
||||
r#"{"jmanifest_version":1,
|
||||
r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172","title":"A Film"},
|
||||
"cut":{"runtime_sec":6420.5},
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884","scenes":[[11.0,21.0]]}]}"#,
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884","scenes":[{"start":11.0,"end":21.0}]}]}"#,
|
||||
);
|
||||
|
||||
let second = db
|
||||
@@ -315,11 +323,11 @@ mod tests {
|
||||
async fn episode_manifests_carry_their_coordinates() {
|
||||
let db = Db::open(":memory:").unwrap();
|
||||
let valid = valid_from(
|
||||
r#"{"jmanifest_version":1,
|
||||
r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"episode","series_tmdb_id":"1396","title":"Breaking Bad",
|
||||
"season":2,"episode":5},
|
||||
"cut":{"runtime_sec":2820.0},
|
||||
"actors":[{"name":"Bryan Cranston","tmdb_id":"17419","scenes":[[10.0,20.0]]}]}"#,
|
||||
"actors":[{"name":"Bryan Cranston","tmdb_id":"17419","scenes":[{"start":10.0,"end":20.0}]}]}"#,
|
||||
);
|
||||
let row = db
|
||||
.write(move |tx| {
|
||||
@@ -357,13 +365,13 @@ mod tests {
|
||||
// servers validating the same upload agree.
|
||||
let a = valid_from(&movie_json("504172", 6420.5));
|
||||
let b = valid_from(
|
||||
r#"{"jmanifest_version":1,
|
||||
r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172","title":"A Film"},
|
||||
"cut":{"runtime_sec":6420.5},
|
||||
"extraction":{"sample_fps":1,"extinction_sec":9,"pipeline_version":"other 9.9",
|
||||
"gallery_size":5},
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884","scenes":[[10.0,20.0]]},
|
||||
{"name":"Michael Palin","tmdb_id":"11007","scenes":[[30.0,40.0]]}]}"#,
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884","scenes":[{"start":10.0,"end":20.0}]},
|
||||
{"name":"Michael Palin","tmdb_id":"11007","scenes":[{"start":30.0,"end":40.0}]}]}"#,
|
||||
);
|
||||
assert_eq!(compute_content_id(&a), compute_content_id(&b));
|
||||
}
|
||||
@@ -375,29 +383,75 @@ mod tests {
|
||||
let a = valid_from(&movie_json("504172", 6420.5));
|
||||
let sig = format!("v1:{}", "A".repeat(1720));
|
||||
let with_sig = format!(
|
||||
r#"{{"jmanifest_version":1,
|
||||
r#"{{"jmanifest_version":2,
|
||||
"identity":{{"type":"movie","tmdb_id":"504172","title":"A Film"}},
|
||||
"cut":{{"runtime_sec":6420.5,"audio_signature":"{sig}"}},
|
||||
"extraction":{{"sample_fps":5,"pipeline_version":"test 0.1"}},
|
||||
"actors":[{{"name":"Steve Buscemi","tmdb_id":"884","scenes":[[10.0,20.0]]}},
|
||||
{{"name":"Michael Palin","tmdb_id":"11007","scenes":[[30.0,40.0]]}}]}}"#
|
||||
"actors":[{{"name":"Steve Buscemi","tmdb_id":"884","scenes":[{{"start":10.0,"end":20.0}}]}},
|
||||
{{"name":"Michael Palin","tmdb_id":"11007","scenes":[{{"start":30.0,"end":40.0}}]}}]}}"#
|
||||
);
|
||||
let b = valid_from(&with_sig);
|
||||
assert_eq!(compute_content_id(&a), compute_content_id(&b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_id_excludes_belief_and_route() {
|
||||
// The trap in the SR-003 bump (UR-018). Belief is a producer-side
|
||||
// estimate that may legitimately differ between pipeline versions for
|
||||
// identical timings, so hashing it would give two servers different ids
|
||||
// for the same manifest — the exact failure mode §9a quantises
|
||||
// centiseconds to avoid, reintroduced one field along.
|
||||
//
|
||||
// Two manifests, same windows, wildly different confidence and routes.
|
||||
let bare = r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172","title":"A Film"},
|
||||
"cut":{"runtime_sec":6420.5},
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884",
|
||||
"scenes":[{"start":10.0,"end":20.0}]},
|
||||
{"name":"Michael Palin","tmdb_id":"11007",
|
||||
"scenes":[{"start":30.0,"end":40.0}]}]}"#;
|
||||
let believed = r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172","title":"A Film"},
|
||||
"cut":{"runtime_sec":6420.5},
|
||||
"actors":[{"name":"Steve Buscemi","tmdb_id":"884",
|
||||
"scenes":[{"start":10.0,"end":20.0,"belief":0.98,"route":"live"}]},
|
||||
{"name":"Michael Palin","tmdb_id":"11007",
|
||||
"scenes":[{"start":30.0,"end":40.0,"belief":0.31,"route":"deferred"}]}]}"#;
|
||||
|
||||
assert_eq!(
|
||||
compute_content_id(&valid_from(bare)),
|
||||
compute_content_id(&valid_from(believed)),
|
||||
"belief and route must not enter identity"
|
||||
);
|
||||
|
||||
// And the same content at a *different* belief still deduplicates.
|
||||
let other_belief = believed.replace("0.98", "0.42").replace("live", "pooled");
|
||||
assert_eq!(
|
||||
compute_content_id(&valid_from(believed)),
|
||||
compute_content_id(&valid_from(&other_belief)),
|
||||
);
|
||||
|
||||
// Sanity: a genuine timing change *does* alter the id, so the test above
|
||||
// is not passing because the hash ignores everything.
|
||||
let shifted = bare.replace("\"end\":20.0", "\"end\":21.0");
|
||||
assert_ne!(
|
||||
compute_content_id(&valid_from(bare)),
|
||||
compute_content_id(&valid_from(&shifted))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_id_excludes_submitted_names() {
|
||||
// Names are not persisted, so they must not be part of identity either —
|
||||
// otherwise a renamed resubmission would evade deduplication.
|
||||
let a = valid_from(&movie_json("504172", 6420.5));
|
||||
let b = valid_from(
|
||||
r#"{"jmanifest_version":1,
|
||||
r#"{"jmanifest_version":2,
|
||||
"identity":{"type":"movie","tmdb_id":"504172","title":"A Film"},
|
||||
"cut":{"runtime_sec":6420.5},
|
||||
"extraction":{"sample_fps":5,"pipeline_version":"test 0.1"},
|
||||
"actors":[{"name":"Someone Else","tmdb_id":"884","scenes":[[10.0,20.0]]},
|
||||
{"name":"Another Person","tmdb_id":"11007","scenes":[[30.0,40.0]]}]}"#,
|
||||
"actors":[{"name":"Someone Else","tmdb_id":"884","scenes":[{"start":10.0,"end":20.0}]},
|
||||
{"name":"Another Person","tmdb_id":"11007","scenes":[{"start":30.0,"end":40.0}]}]}"#,
|
||||
);
|
||||
assert_eq!(compute_content_id(&a), compute_content_id(&b));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user