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
18 KiB
Requirements traceability matrix
Generated: 2026-07-31T07:12:27+00:00
Denominators are read from requirements.md at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source and has a verification tier this repo's CI host can execute (T1, T2, static).
Summary
| Metric | Value |
|---|---|
| Source files scanned | 26 |
| TRACES tags found | 37 |
| EXCEPTION tags found | 0 |
| Requirements defined | 32 |
| Requirements covered | 24 |
| Coverage | 75.0% (24/32) |
| Coverage of CI-executable scope | 75.0% (24/32) |
| Tagged but unexecuted in CI | 0 |
| Orphan tags | 0 |
By type
| Type | Covered | Tagged but unexecuted | Defined |
|---|---|---|---|
| UR | 14 | 0 | 18 |
| DR | 10 | 0 | 14 |
- PR tags present (separate taxonomy, not counted in coverage): PR-004, PR-005, PR-006
- SR tags present (separate taxonomy, not counted in coverage): SR-001, SR-002, SR-003, SR-004, SR-005
Not executable in CI
These requirements have no verification tier this repo's CI host can run, so a tag on them is evidence of intent, not of verification. They are never counted as covered.
None.
Orphan tags
A tag naming an ID requirements.md does not define. This is what renumbering produces, and what a typo produces.
None.
Requirements tracing up to nothing
A register row whose Traces to cell names no parent. Work serving no stated goal is how scope creeps in, and it is invisible unless something looks.
None.
Recorded exceptions
Deliberate, documented departures from an invariant (EXCEPTION: XX-nnn <reason>). Reported separately and never counted as coverage — an exception is a decision to be reviewed, not evidence a requirement is met.
None.
Register
| ID | Status | Tier | Traces to | Trace state | Tagged in | Requirement |
|---|---|---|---|---|---|---|
| UR-001 | Done | T2 | SR-001 | covered | src/api/exists.rs, src/matching.rs |
Cheap existence probe, separate from the fetch, returning availabilit… |
| UR-002 | Done | T2 | PR-006 | covered | src/api/upload.rs, src/ingest.rs |
Accept a contributed manifest for a media item |
| UR-003 | Done | T1, T2 | SR-004 | covered | src/api/upload.rs, src/castcheck.rs, src/model.rs, src/validate.rs, src/worker.rs |
Content verification: strict schema, size caps, approximate TMDB cast… |
| UR-004 | Done | T1, T2 | SR-004 | covered | src/auth.rs, src/ratelimit.rs |
Rate limiting, per token where present and per source IP otherwise |
| UR-005 | Done | T1, T2 | SR-004 | covered | src/api/report.rs, src/api/upload.rs, src/auth.rs, src/castcheck.rs, src/worker.rs |
Trust without accounts: not usable as a content store, nor for prank … |
| UR-006 | Done | T2 | PR-006 | covered | src/api/fetch.rs, src/api/upload.rs, src/validate.rs |
Serve and accept a whole series in one operation |
| UR-007 | In Progress | unset | PR-005 | covered | src/api/exists.rs |
Plugin queries an ordered, configurable list of servers |
| UR-008 | Planned | unset | PR-006 | untagged | - | Servers replicate manifests between each other |
| UR-009 | In Progress | T1 | SR-003 | covered | src/validate.rs |
Store an audio spectral-peak signature for content-based identificati… |
| UR-010 | Done | T1, T2 | SR-001 | covered | src/api/fetch.rs, src/castcheck.rs, src/db/repo.rs, src/model.rs |
Identity crossing the API boundary is TMDB/IMDB ids, never a name alo… |
| UR-011 | Done | T2 | SR-004 | covered | src/model.rs, src/validate.rs |
Reject any field capable of carrying binary or attacker-chosen content |
| UR-012 | Done | T2 | SR-005 | covered | src/db/repo.rs, src/ingest.rs |
Never accept, store, or serve gallery data — reference faces or embed… |
| UR-013 | Done | T1 | SR-002 | covered | src/api/fetch.rs, src/model.rs, src/validate.rs |
Windows are scene-scoped claims; never reinterpret their boundaries |
| UR-014 | Done | T1 | SR-003 | covered | src/model.rs, src/validate.rs |
Reject an unknown jmanifest_version outright, never guess |
| UR-015 | Done | T2 | SR-003 | untagged | - | Accept extraction.extinction_sec in place of anneal_sec |
| UR-016 | Done | T2 | SR-003 | untagged | - | Accept and store extraction.gallery_scope; rank on it (§7) |
| UR-017 | Done | T1, T2 | SR-003 | covered | src/model.rs |
Accept per-window belief and identification route; scenes are objec… |
| UR-018 | Done | T1 | SR-003 | untagged | - | Exclude belief and route from content_id, replicating them as attri… |
| DR-001 | Done | T1 | SR-004 | untagged | - | Strict parse boundary: unknown fields rejected structurally, not by v… |
| DR-002 | Done | unset | SR-004 | covered | src/api/fetch.rs, src/db/repo.rs |
Fully relational storage — no JSON blob on the write path |
| DR-003 | Done | T1 | PR-004 | covered | src/db/mod.rs |
Single serialized writer connection, with a read pool alongside |
| DR-004 | Done | unset | PR-004 | covered | src/db/repo.rs |
All database access behind a repository layer, not scattered through … |
| DR-005 | Done | T1 | PR-004 | covered | src/db/repo.rs |
Background work in-process, with the job queue as a table so it survi… |
| DR-006 | Done | unset | PR-004 | covered | src/ratelimit.rs |
Rate-limit counters in process memory; no external counter store |
| DR-007 | Done | unset | PR-004 | untagged | - | Ship a single static binary plus one database file; container optional |
| DR-008 | Done | T2 | SR-004 | covered | src/auth.rs, src/config.rs |
X-Forwarded-For honoured only from explicitly configured proxies |
| DR-009 | Done | T2 | SR-004 | covered | src/app.rs |
Body caps enforced while streaming, before parsing, per route |
| DR-010 | Done | T1 | SR-003 | covered | src/api/json.rs |
Request bodies are UTF-8 only, rejected with a diagnosable error othe… |
| DR-011 | Done | T1 | SR-003 | covered | src/content_id.rs, src/validate.rs |
content_id canonical form is byte-stable and cross-implementation t… |
| DR-012 | Done | unset | PR-004 | untagged | - | Dependency audit: advisories, licence policy, source policy |
| DR-013 | Done | T1 | SR-003 | covered | src/api/json.rs, src/app.rs, src/error.rs |
API errors use the status codes the spec names, not the framework's d… |
| DR-014 | Done | unset | PR-004 | untagged | - | Portable SQL — no SQLite-specific form where a standard one exists |
Detailed mapping
DR-002
Locations: 3
src/api/fetch.rs:267—pub fn reconstruct(src/db/repo.rs:337—pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>src/db/repo.rs:412—pub fn actors_for_manifest(
DR-003
Locations: 1
src/db/mod.rs:30—struct ReadPool
DR-004
Locations: 1
src/db/repo.rs:337—pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>
DR-005
Locations: 1
src/db/repo.rs:709—pub fn lease_jobs(tx: &Transaction<'_>, now: &str, limit: usize) -> anyhow::Result<Vec<Jo…
DR-006
Locations: 1
src/ratelimit.rs:76—impl Default for RateLimiter
DR-008
Locations: 2
src/auth.rs:76—pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…src/config.rs:10—Unknown
DR-009
Locations: 1
src/app.rs:26—pub fn router(state: AppState) -> Router
DR-010
Locations: 1
src/api/json.rs:100—fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>
DR-011
Locations: 3
src/content_id.rs:52—pub fn canonical_json(src/content_id.rs:129—pub fn content_id(src/validate.rs:102—pub fn to_centiseconds(secs: f64) -> i64
DR-013
Locations: 3
src/api/json.rs:100—fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>src/app.rs:26—pub fn router(state: AppState) -> Routersrc/error.rs:8—Unknown
PR-004
Locations: 3
src/config.rs:10—Unknownsrc/db/mod.rs:30—struct ReadPoolsrc/db/repo.rs:709—pub fn lease_jobs(tx: &Transaction<'_>, now: &str, limit: usize) -> anyhow::Result<Vec<Jo…
PR-005
Locations: 1
src/api/exists.rs:74—pub async fn exists_batch(
PR-006
Locations: 5
src/api/fetch.rs:128—pub async fn get_series(src/api/upload.rs:29—pub async fn post_manifest(src/api/upload.rs:101—pub async fn post_bundle(src/ingest.rs:50—pub fn persist(src/validate.rs:586—pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>
SR-001
Locations: 7
src/api/exists.rs:61—pub async fn exists(src/api/exists.rs:74—pub async fn exists_batch(src/api/fetch.rs:267—pub fn reconstruct(src/castcheck.rs:82—pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcomesrc/db/repo.rs:412—pub fn actors_for_manifest(src/matching.rs:52—pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option<CutMatch>src/model.rs:179—Unknown
SR-002
Locations: 4
src/api/fetch.rs:267—pub fn reconstruct(src/model.rs:179—Unknownsrc/model.rs:233—pub fn from_str(s: &str) -> Option<Self>src/validate.rs:510—fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult<Vec<SceneCs>>
SR-003
Locations: 10
src/api/json.rs:100—fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>src/content_id.rs:52—pub fn canonical_json(src/content_id.rs:129—pub fn content_id(src/error.rs:8—Unknownsrc/model.rs:197—Unknownsrc/model.rs:233—pub fn from_str(s: &str) -> Option<Self>src/model.rs:259—Unknownsrc/validate.rs:102—pub fn to_centiseconds(secs: f64) -> i64src/validate.rs:227—pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>src/validate.rs:378—pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>
SR-004
Locations: 16
src/api/report.rs:56—pub async fn post_report(src/api/upload.rs:29—pub async fn post_manifest(src/api/upload.rs:223—pub async fn post_token(src/app.rs:26—pub fn router(state: AppState) -> Routersrc/auth.rs:22—pub fn hash_token(token: &str) -> Stringsrc/auth.rs:76—pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…src/castcheck.rs:82—pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcomesrc/castcheck.rs:215—pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…src/db/repo.rs:337—pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>src/model.rs:156—Unknownsrc/model.rs:259—Unknownsrc/ratelimit.rs:76—impl Default for RateLimitersrc/validate.rs:136—fn is_allowed_text_char(c: char) -> boolsrc/validate.rs:227—pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>src/validate.rs:378—pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>src/worker.rs:96—async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>
SR-005
Locations: 2
src/db/repo.rs:337—pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>src/ingest.rs:50—pub fn persist(
UR-001
Locations: 3
src/api/exists.rs:61—pub async fn exists(src/api/exists.rs:74—pub async fn exists_batch(src/matching.rs:52—pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option<CutMatch>
UR-002
Locations: 2
src/api/upload.rs:29—pub async fn post_manifest(src/ingest.rs:50—pub fn persist(
UR-003
Locations: 6
src/api/upload.rs:29—pub async fn post_manifest(src/castcheck.rs:82—pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcomesrc/model.rs:156—Unknownsrc/model.rs:259—Unknownsrc/validate.rs:227—pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>src/worker.rs:96—async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>
UR-004
Locations: 2
src/auth.rs:76—pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…src/ratelimit.rs:76—impl Default for RateLimiter
UR-005
Locations: 6
src/api/report.rs:56—pub async fn post_report(src/api/upload.rs:223—pub async fn post_token(src/auth.rs:22—pub fn hash_token(token: &str) -> Stringsrc/castcheck.rs:82—pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcomesrc/castcheck.rs:215—pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…src/worker.rs:96—async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>
UR-006
Locations: 3
src/api/fetch.rs:128—pub async fn get_series(src/api/upload.rs:101—pub async fn post_bundle(src/validate.rs:586—pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>
UR-007
Locations: 1
src/api/exists.rs:74—pub async fn exists_batch(
UR-009
Locations: 1
src/validate.rs:378—pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>
UR-010
Locations: 4
src/api/fetch.rs:267—pub fn reconstruct(src/castcheck.rs:82—pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcomesrc/db/repo.rs:412—pub fn actors_for_manifest(src/model.rs:179—Unknown
UR-011
Locations: 4
src/model.rs:156—Unknownsrc/model.rs:259—Unknownsrc/validate.rs:136—fn is_allowed_text_char(c: char) -> boolsrc/validate.rs:378—pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>
UR-012
Locations: 2
src/db/repo.rs:337—pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>src/ingest.rs:50—pub fn persist(
UR-013
Locations: 4
src/api/fetch.rs:267—pub fn reconstruct(src/model.rs:179—Unknownsrc/model.rs:233—pub fn from_str(s: &str) -> Option<Self>src/validate.rs:510—fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult<Vec<SceneCs>>
UR-014
Locations: 2
src/model.rs:259—Unknownsrc/validate.rs:227—pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>
UR-017
Locations: 2
src/model.rs:197—Unknownsrc/model.rs:233—pub fn from_str(s: &str) -> Option<Self>