Withdraw the file-hash tier; document the legal posture
Removes `cut.video_hash` and the `exact` match tier on legal grounds. The OpenSubtitles hash was the strongest technical signal available — it identifies a specific file, so it cannot produce a false positive — and that is exactly the problem. Every tier must be a claim about a *cut*, never about a copy. A TMDB id discloses "some copy of this film", which is what a library catalogue discloses. A file hash discloses "this exact release": it made a read endpoint into a release-level oracle, and made an instance's database a mapping from file fingerprints to the instances holding them. That is a far more specific disclosure than PR-005 permits, and a dataset no volunteer operator should be asked to hold. The audio signature is the replacement: derived from content, it identifies the cut rather than the copy, so two encodes of the same edit agree. The field is deleted rather than kept as a vestigial null, on the same reasoning §2 applied to `anneal_sec` — a key naming a signal the format no longer has is actively misleading — so an upload carrying one is now an unknown-field 400, with a test asserting it. **Every content_id changes**, including for manifests that never carried a hash, because the canonical `cut` object lost a key. The golden vector is regenerated and re-verified against an independent Python implementation; the plugin and extraction repos must adopt the new value or federation deduplication silently breaks. Free now, pre-release; not free later. Adds docs/legal-posture.md, the operator-facing half of what §5a asks for: what an instance holds exhaustively, what it structurally cannot do, and how that sits against the intermediary-liability regimes that plausibly apply. 208 tests. Coverage 25/32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: UR-011 | SR-004, PR-005
This commit is contained in:
@@ -347,26 +347,12 @@ fn validate_cut(m: &Jmanifest) -> VResult<()> {
|
||||
return Err(err("cut.container_duration_sec", "must be a finite duration"));
|
||||
}
|
||||
}
|
||||
if let Some(h) = &m.cut.video_hash {
|
||||
validate_video_hash(h)?;
|
||||
}
|
||||
if let Some(sig) = &m.cut.audio_signature {
|
||||
validate_audio_signature(sig, rt)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// §3: the OpenSubtitles hash, in the fixed `opensubtitles:<16 hex>` form.
|
||||
fn validate_video_hash(h: &str) -> VResult<()> {
|
||||
let Some(hex) = h.strip_prefix("opensubtitles:") else {
|
||||
return Err(err("cut.video_hash", "must be prefixed 'opensubtitles:'"));
|
||||
};
|
||||
if hex.len() != 16 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||
return Err(err("cut.video_hash", "expected 16 hex digits after the prefix"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// §3 "Validation and abuse": fixed length, base64, and each byte structurally
|
||||
/// constrained (5-bit bin index + 2-bit energy class).
|
||||
///
|
||||
@@ -973,19 +959,6 @@ mod tests {
|
||||
assert_eq!(e.field, "actors");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_hash_format_is_enforced() {
|
||||
let mut m = base_manifest();
|
||||
m.cut.video_hash = Some("opensubtitles:8e245d9679d31e12".into());
|
||||
assert!(validate_manifest(m).is_ok());
|
||||
|
||||
for bad in ["8e245d9679d31e12", "opensubtitles:xyz", "opensubtitles:8e245d9679d31e1"] {
|
||||
let mut m = base_manifest();
|
||||
m.cut.video_hash = Some(bad.into());
|
||||
assert!(validate_manifest(m).is_err(), "{bad} should be rejected");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centisecond_quantisation_is_stable_for_accumulated_float_error() {
|
||||
// §9a: real corpus values look like 8045.066666660665.
|
||||
|
||||
Reference in New Issue
Block a user