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:
+57
-20
@@ -149,7 +149,7 @@ impl TestServer {
|
||||
|
||||
fn movie_manifest(tmdb_id: &str, runtime: f64) -> Value {
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "movie", "tmdb_id": tmdb_id, "title": "The Death of Stalin",
|
||||
"year": 2017 },
|
||||
"cut": { "runtime_sec": runtime, "video_hash": "opensubtitles:8e245d9679d31e12" },
|
||||
@@ -157,8 +157,8 @@ fn movie_manifest(tmdb_id: &str, runtime: f64) -> Value {
|
||||
"pipeline_version": "scene-actor-extraction 0.4.1",
|
||||
"gallery_scope": "global" },
|
||||
"actors": [
|
||||
{ "name": "Steve Buscemi", "tmdb_id": "884", "scenes": [[191.6, 209.2], [438.2, 465.6]] },
|
||||
{ "name": "Michael Palin", "tmdb_id": "11007", "scenes": [[300.0, 320.0]] }
|
||||
{ "name": "Steve Buscemi", "tmdb_id": "884", "scenes": [{"start":191.6,"end":209.2},{"start":438.2,"end":465.6}] },
|
||||
{ "name": "Michael Palin", "tmdb_id": "11007", "scenes": [{"start":300.0,"end":320.0}] }
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -341,6 +341,39 @@ async fn the_schema_bump_fields_round_trip() {
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "gallery_scope is a closed enum");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn per_window_belief_and_route_round_trip() {
|
||||
// SR-003 gives each window its posterior and identification route
|
||||
// (extraction AR-017). They are stored and served — a consumer needs them to
|
||||
// know how much to trust a window — but they are never part of identity.
|
||||
let s = TestServer::new("belief");
|
||||
let token = s.token().await;
|
||||
|
||||
let mut m = movie_manifest("504172", 6420.5);
|
||||
m["actors"][0]["scenes"] = json!([
|
||||
{ "start": 191.6, "end": 209.2, "belief": 0.98, "route": "live" },
|
||||
{ "start": 438.2, "end": 465.6, "belief": 0.81, "route": "deferred" }
|
||||
]);
|
||||
let (status, body, _) = s.post_json_auth("/api/v1/manifests", &token, &m).await;
|
||||
assert_eq!(status, StatusCode::ACCEPTED, "{body}");
|
||||
|
||||
// A belief outside [0, 1] is not a probability. Bounded rather than merely
|
||||
// stored, because §5a's Threat 1 argument rests on every accepted value
|
||||
// being bounded — an unbounded float is a 64-bit channel.
|
||||
for bad in [-0.1, 1.5] {
|
||||
let mut m = movie_manifest("504173", 6420.5);
|
||||
m["actors"][0]["scenes"] = json!([{ "start": 1.0, "end": 2.0, "belief": bad }]);
|
||||
let (status, body, _) = s.post_json_auth("/api/v1/manifests", &token, &m).await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "belief {bad}: {body}");
|
||||
}
|
||||
|
||||
// `route` is a closed vocabulary, so an invented value cannot be stored.
|
||||
let mut m = movie_manifest("504174", 6420.5);
|
||||
m["actors"][0]["scenes"] = json!([{ "start": 1.0, "end": 2.0, "route": "telepathy" }]);
|
||||
let (status, _, _) = s.post_json_auth("/api/v1/manifests", &token, &m).await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unknown_manifest_version_is_rejected() {
|
||||
// UR-014 / SR-003: a consumer encountering an unknown `schema_version`
|
||||
@@ -348,7 +381,11 @@ async fn an_unknown_manifest_version_is_rejected() {
|
||||
let s = TestServer::new("version");
|
||||
let token = s.token().await;
|
||||
|
||||
for version in [0, 2, 99] {
|
||||
// Version 1 is the one that matters: SR-003 settled on a **flag day**, not a
|
||||
// dual-accept period, so the immediately-previous version is refused exactly
|
||||
// like a nonsensical one. A v1 read path would be the one nobody exercises,
|
||||
// and so the one that rots while being dragged through every later change.
|
||||
for version in [0, 1, 3, 99] {
|
||||
let mut m = movie_manifest("504172", 6420.5);
|
||||
m["jmanifest_version"] = json!(version);
|
||||
let (status, body, _) = s.post_json_auth("/api/v1/manifests", &token, &m).await;
|
||||
@@ -560,7 +597,7 @@ async fn exceeding_a_limit_returns_429_with_retry_after() {
|
||||
// The bundle surface has the tightest write limit (20/hour), so it is the
|
||||
// cheapest to exhaust.
|
||||
let bundle = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396", "title": "Breaking Bad" },
|
||||
"episodes": []
|
||||
});
|
||||
@@ -688,24 +725,24 @@ async fn bundle_upload_is_not_atomic() {
|
||||
|
||||
let good = |ep: i64| {
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "episode", "series_tmdb_id": "1396", "title": "Breaking Bad",
|
||||
"season": 1, "episode": ep },
|
||||
"cut": { "runtime_sec": 2820.0 },
|
||||
"actors": [ { "name": "Bryan Cranston", "tmdb_id": "17419",
|
||||
"scenes": [[10.0, 20.0]] } ]
|
||||
"scenes": [{"start":10.0,"end":20.0}] } ]
|
||||
})
|
||||
};
|
||||
// Invalid: a scene window beyond the runtime tolerance (§6).
|
||||
let bad = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "episode", "series_tmdb_id": "1396", "season": 1, "episode": 3 },
|
||||
"cut": { "runtime_sec": 2820.0 },
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [[10.0, 99999.0]] } ]
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [{"start":10.0,"end":99999.0}] } ]
|
||||
});
|
||||
|
||||
let bundle = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396", "title": "Breaking Bad" },
|
||||
"episodes": [ good(1), bad, good(2) ]
|
||||
});
|
||||
@@ -731,7 +768,7 @@ async fn bundle_envelope_errors_are_whole_request_400s() {
|
||||
.post_json_auth(
|
||||
"/api/v1/manifests/bundle",
|
||||
&token,
|
||||
&json!({ "jmanifest_version": 1, "series": {}, "episodes": [] }),
|
||||
&json!({ "jmanifest_version": 2, "series": {}, "episodes": [] }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "series needs an identifier");
|
||||
@@ -740,7 +777,7 @@ async fn bundle_envelope_errors_are_whole_request_400s() {
|
||||
.post_json_auth(
|
||||
"/api/v1/manifests/bundle",
|
||||
&token,
|
||||
&json!({ "jmanifest_version": 1,
|
||||
&json!({ "jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396" },
|
||||
"episodes": [], "extra": 1 }),
|
||||
)
|
||||
@@ -754,13 +791,13 @@ async fn bundle_rejects_an_episode_contradicting_the_envelope() {
|
||||
let s = TestServer::new("bundle-mismatch");
|
||||
let token = s.token().await;
|
||||
let bundle = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396" },
|
||||
"episodes": [ {
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "episode", "series_tmdb_id": "9999", "season": 1, "episode": 1 },
|
||||
"cut": { "runtime_sec": 2820.0 },
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
} ]
|
||||
});
|
||||
let (status, body, _) = s.post_json_auth("/api/v1/manifests/bundle", &token, &bundle).await;
|
||||
@@ -776,16 +813,16 @@ async fn bundle_beyond_the_episode_cap_is_413() {
|
||||
let episodes: Vec<Value> = (0..501)
|
||||
.map(|i| {
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "episode", "series_tmdb_id": "1396",
|
||||
"season": 1, "episode": i },
|
||||
"cut": { "runtime_sec": 2820.0 },
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [[1.0, 2.0]] } ]
|
||||
"actors": [ { "tmdb_id": "17419", "scenes": [{"start":1.0,"end":2.0}] } ]
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let bundle = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396" },
|
||||
"episodes": episodes
|
||||
});
|
||||
@@ -805,7 +842,7 @@ async fn bundle_route_accepts_a_body_larger_than_the_single_manifest_cap() {
|
||||
.map(|ep| {
|
||||
let scenes: Vec<Value> = (0..600).map(|j| json!([j as f64, (j + 1) as f64])).collect();
|
||||
json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"identity": { "type": "episode", "series_tmdb_id": "1396",
|
||||
"season": 1, "episode": ep },
|
||||
"cut": { "runtime_sec": 2820.0 },
|
||||
@@ -816,7 +853,7 @@ async fn bundle_route_accepts_a_body_larger_than_the_single_manifest_cap() {
|
||||
})
|
||||
.collect();
|
||||
let bundle = json!({
|
||||
"jmanifest_version": 1,
|
||||
"jmanifest_version": 2,
|
||||
"series": { "series_tmdb_id": "1396" },
|
||||
"episodes": episodes
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user