Traceability: vendor the shared gate, annotate the source
CI / fmt, clippy, test (push) Failing after 1m21s
CI / static musl binary (push) Has been skipped
CI / advisories and licences (push) Successful in 24s

Adds jray-project as a submodule at scripts/vendor/jray-project, so this repo
runs the same extractor as every other component rather than its own copy, and
gains the system spec that defines the PR/SR requirements its register traces
up to.

scripts/traceability-gate.sh is a thin wrapper holding only what is specific to
this repo: UR/DR prefixes, .rs sources, and REPO_ROOT — which the shared gate
cannot infer once vendored, since its default resolves to the submodule itself.
Each override fails silently in a way that looks like "no work done" rather
than "misconfigured", so the wrapper documents why each is needed.

Annotates 35 units with TRACES tags, on the code that decides rather than every
helper it calls. Coverage is 23/32 (71.9%) with no orphan tags. The nine
untraced are genuinely unimplemented: UR-007 is plugin-side, UR-008 is
federation, and UR-015..018 are the pending SR-003 schema bump.

The gate caught a real error in the first pass: several tags separated IDs of
different types with commas. A comma joins IDs within one type; a pipe
separates types. Fixed, and the diagnostics are now clean.

MIN_COVERAGE stays 0 deliberately. The gate still fails on orphan tags, a >100%
ratio, a register parsing to nothing, or an empty source scan — raise the
threshold as a ratchet once the remaining work lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 18:27:16 +02:00
co-authored by Claude Opus 5
parent a848750a65
commit a1e789a6fe
24 changed files with 464 additions and 2 deletions
+352
View File
@@ -0,0 +1,352 @@
# Requirements traceability matrix
<!-- GENERATED FILE - do not edit by hand. -->
<!-- Regenerate: python3 scripts/traceability/extract_traces.py --format markdown --markdown-out docs/traceability.md -->
**Generated:** 2026-07-30T16:25:39+00:00
Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this CI host can execute — CI is an Intel N100 with no discrete GPU.
## Summary
| Metric | Value |
|---|---|
| Source files scanned | 26 |
| TRACES tags found | 35 |
| EXCEPTION tags found | 0 |
| Requirements defined | 32 |
| Requirements covered | 23 |
| **Coverage** | **71.9%** (23/32) |
| Coverage of CI-executable scope | 71.9% (23/32) |
| Tagged but unexecuted in CI (T4/GPU) | 0 |
| Orphan tags | 0 |
### By type
| Type | Covered | Tagged but unexecuted | Defined |
|---|---|---|---|
| UR | 13 | 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
CI runs on an Intel N100 with no discrete GPU. These requirements have no verification tier that can run here, 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: AR-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 | Planned | unset | SR-003 | untagged | - | Accept `extraction.extinction_sec` in place of `anneal_sec` |
| UR-016 | Planned | unset | SR-003 | untagged | - | Accept and store `extraction.gallery_scope`; rank on it (§7) |
| UR-017 | Planned | unset | SR-003 | untagged | - | Accept per-window belief and identification route; `scenes` becomes o… |
| UR-018 | Planned | unset | SR-003 | untagged | - | Exclude belief from `content_id`, replicating it as an attribute |
| 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`](../src/api/fetch.rs#L267) — `Unknown`
- [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown`
### DR-003
**Locations:** 1
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `struct ReadPool`
### DR-004
**Locations:** 1
- [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
### DR-005
**Locations:** 1
- [`src/db/repo.rs:691`](../src/db/repo.rs#L691) — `pub fn lease_jobs(tx: &Transaction<'_>, now: &str, limit: usize) -> anyhow::Result<Vec<Jo…`
### DR-006
**Locations:** 1
- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window`
### DR-008
**Locations:** 2
- [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…`
- [`src/config.rs:10`](../src/config.rs#L10) — `Unknown`
### DR-009
**Locations:** 1
- [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router`
### DR-010
**Locations:** 1
- [`src/api/json.rs:100`](../src/api/json.rs#L100) — `fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>`
### DR-011
**Locations:** 3
- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `Unknown`
- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `fn push_opt_num(s: &mut String, v: Option<i64>)`
- [`src/validate.rs:78`](../src/validate.rs#L78) — `pub fn to_centiseconds(secs: f64) -> i64`
### DR-013
**Locations:** 3
- [`src/api/json.rs:100`](../src/api/json.rs#L100) — `fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>`
- [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router`
- [`src/error.rs:8`](../src/error.rs#L8) — `Unknown`
### PR-004
**Locations:** 3
- [`src/config.rs:10`](../src/config.rs#L10) — `Unknown`
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `struct ReadPool`
- [`src/db/repo.rs:691`](../src/db/repo.rs#L691) — `pub fn lease_jobs(tx: &Transaction<'_>, now: &str, limit: usize) -> anyhow::Result<Vec<Jo…`
### PR-005
**Locations:** 1
- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown`
### PR-006
**Locations:** 5
- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `Unknown`
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown`
- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `Unknown`
- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown`
- [`src/validate.rs:540`](../src/validate.rs#L540) — `pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>`
### SR-001
**Locations:** 7
- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `Unknown`
- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown`
- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown`
- [`src/matching.rs:52`](../src/matching.rs#L52) — `pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option<CutMatch>`
- [`src/model.rs:179`](../src/model.rs#L179) — `Unknown`
### SR-002
**Locations:** 3
- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown`
- [`src/model.rs:179`](../src/model.rs#L179) — `Unknown`
- [`src/validate.rs:486`](../src/validate.rs#L486) — `fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult<Vec<(i64, i64)>>`
### SR-003
**Locations:** 8
- [`src/api/json.rs:100`](../src/api/json.rs#L100) — `fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>`
- [`src/content_id.rs:52`](../src/content_id.rs#L52) — `Unknown`
- [`src/content_id.rs:129`](../src/content_id.rs#L129) — `fn push_opt_num(s: &mut String, v: Option<i64>)`
- [`src/error.rs:8`](../src/error.rs#L8) — `Unknown`
- [`src/model.rs:198`](../src/model.rs#L198) — `Unknown`
- [`src/validate.rs:78`](../src/validate.rs#L78) — `pub fn to_centiseconds(secs: f64) -> i64`
- [`src/validate.rs:203`](../src/validate.rs#L203) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
- [`src/validate.rs:354`](../src/validate.rs#L354) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
### SR-004
**Locations:** 16
- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `Unknown`
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown`
- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `Unknown`
- [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router`
- [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String`
- [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/castcheck.rs:215`](../src/castcheck.rs#L215) — `pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…`
- [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
- [`src/model.rs:156`](../src/model.rs#L156) — `Unknown`
- [`src/model.rs:198`](../src/model.rs#L198) — `Unknown`
- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window`
- [`src/validate.rs:112`](../src/validate.rs#L112) — `fn is_allowed_text_char(c: char) -> bool`
- [`src/validate.rs:203`](../src/validate.rs#L203) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
- [`src/validate.rs:354`](../src/validate.rs#L354) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
- [`src/worker.rs:96`](../src/worker.rs#L96) — `async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>`
### SR-005
**Locations:** 2
- [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown`
### UR-001
**Locations:** 3
- [`src/api/exists.rs:61`](../src/api/exists.rs#L61) — `Unknown`
- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown`
- [`src/matching.rs:52`](../src/matching.rs#L52) — `pub fn match_cut(client: &ClientCut, stored: &StoredCut) -> Option<CutMatch>`
### UR-002
**Locations:** 2
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown`
- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown`
### UR-003
**Locations:** 6
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `Unknown`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/model.rs:156`](../src/model.rs#L156) — `Unknown`
- [`src/model.rs:198`](../src/model.rs#L198) — `Unknown`
- [`src/validate.rs:203`](../src/validate.rs#L203) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
- [`src/worker.rs:96`](../src/worker.rs#L96) — `async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>`
### UR-004
**Locations:** 2
- [`src/auth.rs:76`](../src/auth.rs#L76) — `pub fn client_ip(headers: &HeaderMap, peer: Option<IpAddr>, trusted_proxies: &[IpAddr]) -…`
- [`src/ratelimit.rs:76`](../src/ratelimit.rs#L76) — `struct Window`
### UR-005
**Locations:** 6
- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `Unknown`
- [`src/api/upload.rs:223`](../src/api/upload.rs#L223) — `Unknown`
- [`src/auth.rs:22`](../src/auth.rs#L22) — `pub fn hash_token(token: &str) -> String`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/castcheck.rs:215`](../src/castcheck.rs#L215) — `pub fn category_guard_violation(matched: &[MatchedActor], title_is_adult: bool) -> Option…`
- [`src/worker.rs:96`](../src/worker.rs#L96) — `async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>`
### UR-006
**Locations:** 3
- [`src/api/fetch.rs:128`](../src/api/fetch.rs#L128) — `Unknown`
- [`src/api/upload.rs:101`](../src/api/upload.rs#L101) — `Unknown`
- [`src/validate.rs:540`](../src/validate.rs#L540) — `pub fn validate_bundle_envelope(b: &SeriesBundle) -> VResult<()>`
### UR-007
**Locations:** 1
- [`src/api/exists.rs:74`](../src/api/exists.rs#L74) — `Unknown`
### UR-009
**Locations:** 1
- [`src/validate.rs:354`](../src/validate.rs#L354) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
### UR-010
**Locations:** 4
- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown`
- [`src/castcheck.rs:82`](../src/castcheck.rs#L82) — `pub fn evaluate(submitted: &[SubmittedActor], credits: &[CastMember]) -> CastCheckOutcome`
- [`src/db/repo.rs:405`](../src/db/repo.rs#L405) — `Unknown`
- [`src/model.rs:179`](../src/model.rs#L179) — `Unknown`
### UR-011
**Locations:** 4
- [`src/model.rs:156`](../src/model.rs#L156) — `Unknown`
- [`src/model.rs:198`](../src/model.rs#L198) — `Unknown`
- [`src/validate.rs:112`](../src/validate.rs#L112) — `fn is_allowed_text_char(c: char) -> bool`
- [`src/validate.rs:354`](../src/validate.rs#L354) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
### UR-012
**Locations:** 2
- [`src/db/repo.rs:337`](../src/db/repo.rs#L337) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
- [`src/ingest.rs:50`](../src/ingest.rs#L50) — `Unknown`
### UR-013
**Locations:** 3
- [`src/api/fetch.rs:267`](../src/api/fetch.rs#L267) — `Unknown`
- [`src/model.rs:179`](../src/model.rs#L179) — `Unknown`
- [`src/validate.rs:486`](../src/validate.rs#L486) — `fn validate_scenes(idx: usize, a: &Actor, runtime_sec: f64) -> VResult<Vec<(i64, i64)>>`
### UR-014
**Locations:** 2
- [`src/model.rs:198`](../src/model.rs#L198) — `Unknown`
- [`src/validate.rs:203`](../src/validate.rs#L203) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`