docs: tag the untagged verifications, and record the UR-007 conflict
CI / fmt, clippy, test (pull_request) Failing after 54s
CI / static musl binary (pull_request) Skipped
CI / advisories and licences (pull_request) Successful in 39s

Five tests and the schema constant implemented requirements without
carrying a tag, so those requirements read as uncovered when they were
not. No behaviour changes here — every edit is a comment.

Also documents `static` as a real tier rather than an exemption: it is
already in `ci_executable_tiers` and its checks run in CI, and it exists
because DR-007's single binary and DR-012's licence policy are properties
of the build that a unit test could only assert as theatre.

The UR-007 note records a cross-repo status conflict rather than
resolving it. `jRay` JR-025 is `Done` and claims UR-007, but the fetch
path that would exercise it (`jRay` JR-031) is still `Planned`. Either
JR-025 is scoped to selection alone or it over-claims; until that is
settled neither register should be trusted for UR-007 coverage.

The gate reports 0 orphan tags and 19/19 UR coverage.

TRACES: DR-001, DR-014, UR-015, UR-016, UR-018 | SR-003, SR-004
This commit is contained in:
2026-07-31 16:26:10 +02:00
parent 4afa36e7a2
commit 7eb5c175af
7 changed files with 98 additions and 23 deletions
+23 -1
View File
@@ -53,6 +53,14 @@ requirement and no fixture-generation step, unlike `scene-actor-extraction`.
server list and its per-server trust settings, with the community instance
pre-configured but disabled. The fetch path that consumes it does not exist yet.
> **The two registers disagree about this and the disagreement is unresolved.**
> `jRay` JR-025 is recorded `Done` and claims to satisfy UR-007, but `jRay`
> JR-031 — the fetch endpoints that would exercise the ordered list — is still
> `Planned`. Either JR-025 is scoped to the *selection* logic alone (in which
> case UR-007 stays open until JR-031 lands) or it over-claims. **Settle this
> before either register is trusted for coverage**; it is the only cross-repo
> status conflict currently outstanding.
**UR-008 is `Done` for the replication surface**: change feed, fetch by
`content_id`, batch `have`, the peer directory, and a pull worker that
re-validates everything it ingests. Peer *administration* — adding and enabling
@@ -107,10 +115,18 @@ make the test suite hermetic.
|---|---|---|
| **T1 — unit** | Yes | Pure logic: validation, cut matching, cast-check scoring, canonicalisation, rate limiting |
| **T2 — integration** | Yes | End-to-end through the real router against a temporary on-disk database |
| **static** | Yes | Build and dependency properties no runtime test can assert — `cargo deny check`, the musl release build, architectural greps |
There is no tier that does not run. A requirement here is either verified or
visibly not.
**`static` is a real tier, not an exemption.** `ci_executable_tiers` in
[`../traceability.toml`](../traceability.toml) already lists it, and the checks it
names execute in CI like any other. It exists because a handful of requirements
are properties of the *build* rather than of the running program — DR-007's single
binary, DR-012's licence and advisory policy — and asserting those from a unit
test would be theatre.
**Integration tests use an on-disk temporary database, not `:memory:`.** DR-003
specifies one writer connection plus a read pool, and in-memory SQLite is
per-connection — the readers would see an empty database. Testing the real
@@ -126,7 +142,7 @@ topology is the point, so this is a deliberate choice rather than an oversight.
| UR-004 | T1 + T2 | Limits engage and carry the documented headers | Window reset; a rejected request does not extend its own lockout; surfaces have independent budgets |
| UR-005 | T1 + T2 | Prank manifests rejected; no free-text channel | Uncredited cast rejected; name-only matches capped; automatic revocation needs a minimum sample |
| UR-006 | T2 | Bundle accepted per-episode, non-atomically | One bad episode rejected while its neighbours are accepted; envelope errors are whole-request `400` |
| UR-007 | — | *No server-side test.* Plugin-side (`jRay` JR-025); the register there carries it | — |
| UR-007 | **external** | *No server-side test, and cannot have one.* The obligation is the plugin's: `jRay` JR-025, which is `Done` and tagged in that register | Verified there, not here — counted as covered by cross-reference, never by a test in this repo. **See the status note: JR-025 being `Done` does not by itself close UR-007**, because the fetch path (`jRay` JR-031) is still `Planned` |
| UR-008 | T1 + T2 | Feed, fetch-by-hash, batch have, peer directory | **Cursor is strictly monotonic** — a ULID would sort out of write order within a millisecond and silently skip entries; a peer retraction flags rather than delists; only the opt-in abuse channel delists; `pending` is never replicated; **no endpoint can create a peering** |
| UR-009 | T1 | Signature structurally validated | Fixed length; reserved high bit; **media < 120 s must send no signature at all** |
| UR-010 | T1 + T2 | Actors persist as TMDB person ids | A name the upload invented does not round-trip |
@@ -136,7 +152,10 @@ topology is the point, so this is a deliberate choice rather than an oversight.
| UR-014 | T1 | Unknown `jmanifest_version` rejected | Version `2` and version `0` both refused, naming the field |
| UR-019 | T2 | `POST /tokens` returns the licence and its terms | The terms **bound the grant to the manifest** — a reading that covers the underlying work is the failure, not a missing field |
| DR-001 | T1 | Unknown field at any nesting depth fails to parse | `movie` and `jellyfin_id` named in the error |
| DR-002 | T1 | Every manifest field lands in a typed column; no JSON blob on the write path | A round-trip through storage reconstructs the manifest from columns alone |
| DR-003 | T1 | Concurrent writes serialize rather than returning `SQLITE_BUSY` | Failed transaction rolls back fully |
| DR-004 | T1 | Handlers reach the database only through `db::repo` | No `Connection` or raw SQL outside the repository layer |
| DR-006 | T1 | Rate-limit counters live in process memory | Counters reset on restart — the documented trade-off, not a bug |
| DR-005 | T1 | Jobs lease once, reschedule with backoff, survive restart | Stranded lease released at startup; future job not leased early |
| DR-008 | T2 | Forged `X-Forwarded-For` cannot mint a fresh budget | Untrusted peer ignored; trusted proxy honoured; client-supplied entries to the left cannot spoof |
| DR-009 | T2 | Oversized body rejected as `413` | **A lying `Content-Length` does not bypass the cap**; per-route limits differ |
@@ -147,6 +166,9 @@ topology is the point, so this is a deliberate choice rather than an oversight.
| UR-017 | T1 + T2 | Windows carry belief and route through storage | Belief outside `[0, 1]` rejected; an invented `route` rejected |
| UR-018 | **T1** | Belief and route absent from `content_id` | Same windows at different belief hash identically; **a real timing change still does not**, so the test cannot pass vacuously |
| DR-013 | T1 | Schema mismatch is `400`, not the framework's `422` | §4 names `400` for a forbidden field, and a client checking for it would mishandle `422` |
| DR-007 | **static** | The musl release target builds and links one binary needing only a database file | Build property, not a runtime one — a unit test asserting it would assert nothing |
| DR-012 | **static** | `cargo deny check` passes advisories, licences and sources | A copyleft-incompatible transitive dependency must fail the build, not be discovered later |
| DR-014 | **static** | `schema.sql` uses no SQLite-specific form where a standard one exists | `INSERT OR REPLACE` must not reappear in place of `INSERT ... ON CONFLICT` |
Three are worth singling out, because each verifies a claim that would otherwise
be an assertion:
+63 -22
View File
@@ -3,7 +3,7 @@
<!-- GENERATED FILE - do not edit by hand. -->
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
**Generated:** 2026-07-31T08:06:49+00:00
**Generated:** 2026-07-31T14:25:51+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 repo's CI host can execute (`T1, T2, static`).
@@ -12,12 +12,12 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Metric | Value |
|---|---|
| Source files scanned | 29 |
| TRACES tags found | 43 |
| TRACES tags found | 50 |
| EXCEPTION tags found | 0 |
| Requirements defined | 33 |
| Requirements covered | 26 |
| **Coverage** | **78.8%** (26/33) |
| Coverage of CI-executable scope | 78.8% (26/33) |
| Requirements covered | 31 |
| **Coverage** | **93.9%** (31/33) |
| Coverage of CI-executable scope | 93.9% (31/33) |
| Tagged but unexecuted in CI | 0 |
| Orphan tags | 0 |
@@ -25,8 +25,8 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
| Type | Covered | Tagged but unexecuted | Defined |
|---|---|---|---|
| UR | 16 | 0 | 19 |
| DR | 10 | 0 | 14 |
| UR | 19 | 0 | 19 |
| DR | 12 | 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
@@ -73,28 +73,35 @@ _None._
| 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/api/federation.rs`, `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-015 | Done | T2 | SR-003 | covered | `src/validate.rs`, `tests/api.rs` | Accept `extraction.extinction_sec` in place of `anneal_sec` |
| UR-016 | Done | T2 | SR-003 | covered | `src/validate.rs`, `tests/api.rs` | 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… |
| UR-018 | Done | T1 | SR-003 | covered | `src/ingest.rs` | Exclude belief and route from `content_id`, replicating them as attri… |
| UR-019 | Done | T2 | PR-006 | covered | `src/api/upload.rs` | Contributed manifests are CC0 1.0; the grant is delivered with the to… |
| 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-001 | Done | T1 | SR-004 | covered | `src/model.rs`, `tests/api.rs` | Strict parse boundary: unknown fields rejected structurally, not by v… |
| DR-002 | Done | T1 | 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-004 | Done | T1 | 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-006 | Done | T1 | PR-004 | covered | `src/ratelimit.rs` | Rate-limit counters in process memory; no external counter store |
| DR-007 | Done | static | 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-012 | Done | static | 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 |
| DR-014 | Done | static | PR-004 | covered | `src/db/mod.rs` | Portable SQL — no SQLite-specific form where a standard one exists |
## Detailed mapping
### DR-001
**Locations:** 2
- [`src/model.rs:323`](../src/model.rs#L323) — `fn unknown_field_at_top_level_is_rejected()`
- [`tests/api.rs:278`](../tests/api.rs#L278) — `async fn unknown_field_anywhere_is_rejected_with_400()`
### DR-002
**Locations:** 3
@@ -107,7 +114,7 @@ _None._
**Locations:** 1
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `struct ReadPool`
- [`src/db/mod.rs:36`](../src/db/mod.rs#L36) — `struct ReadPool`
### DR-004
@@ -162,12 +169,19 @@ _None._
- [`src/app.rs:26`](../src/app.rs#L26) — `pub fn router(state: AppState) -> Router`
- [`src/error.rs:8`](../src/error.rs#L8) — `Unknown`
### DR-014
**Locations:** 1
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `pub mod repo;`
### PR-004
**Locations:** 3
**Locations:** 4
- [`src/config.rs:10`](../src/config.rs#L10) — `Unknown`
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `struct ReadPool`
- [`src/db/mod.rs:30`](../src/db/mod.rs#L30) — `pub mod repo;`
- [`src/db/mod.rs:36`](../src/db/mod.rs#L36) — `struct ReadPool`
- [`src/db/repo.rs:702`](../src/db/repo.rs#L702) — `pub fn lease_jobs(tx: &Transaction<'_>, now: &str, limit: usize) -> anyhow::Result<Vec<Jo…`
### PR-005
@@ -215,23 +229,27 @@ _None._
### SR-003
**Locations:** 11
**Locations:** 15
- [`src/api/federation.rs:258`](../src/api/federation.rs#L258) — `pub async fn get_capabilities(State(state): State<AppState>) -> ApiResult<Response>`
- [`src/api/json.rs:100`](../src/api/json.rs#L100) — `fn require_utf8(bytes: &[u8]) -> Result<&str, ApiError>`
- [`src/content_id.rs:57`](../src/content_id.rs#L57) — `pub fn canonical_json(`
- [`src/content_id.rs:132`](../src/content_id.rs#L132) — `pub fn content_id(`
- [`src/error.rs:8`](../src/error.rs#L8) — `Unknown`
- [`src/ingest.rs:387`](../src/ingest.rs#L387) — `fn content_id_excludes_belief_and_route()`
- [`src/model.rs:191`](../src/model.rs#L191) — `Unknown`
- [`src/model.rs:232`](../src/model.rs#L232) — `pub fn from_stored(s: &str) -> Option<Self>`
- [`src/model.rs:258`](../src/model.rs#L258) — `Unknown`
- [`src/validate.rs:102`](../src/validate.rs#L102) — `pub fn to_centiseconds(secs: f64) -> i64`
- [`src/validate.rs:227`](../src/validate.rs#L227) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
- [`src/validate.rs:364`](../src/validate.rs#L364) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
- [`src/validate.rs:731`](../src/validate.rs#L731) — `fn extinction_sec_replaces_anneal_sec()`
- [`tests/api.rs:323`](../tests/api.rs#L323) — `async fn the_withdrawn_anneal_sec_field_is_rejected()`
- [`tests/api.rs:347`](../tests/api.rs#L347) — `async fn the_schema_bump_fields_round_trip()`
### SR-004
**Locations:** 16
**Locations:** 18
- [`src/api/report.rs:56`](../src/api/report.rs#L56) — `pub async fn post_report(`
- [`src/api/upload.rs:29`](../src/api/upload.rs#L29) — `pub async fn post_manifest(`
@@ -244,11 +262,13 @@ _None._
- [`src/db/repo.rs:331`](../src/db/repo.rs#L331) — `pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()>`
- [`src/model.rs:150`](../src/model.rs#L150) — `Unknown`
- [`src/model.rs:258`](../src/model.rs#L258) — `Unknown`
- [`src/model.rs:323`](../src/model.rs#L323) — `fn unknown_field_at_top_level_is_rejected()`
- [`src/ratelimit.rs:93`](../src/ratelimit.rs#L93) — `impl Default for RateLimiter`
- [`src/validate.rs:136`](../src/validate.rs#L136) — `fn is_allowed_text_char(c: char) -> bool`
- [`src/validate.rs:227`](../src/validate.rs#L227) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
- [`src/validate.rs:364`](../src/validate.rs#L364) — `pub fn validate_audio_signature(sig: &str, runtime_sec: f64) -> VResult<()>`
- [`src/worker.rs:150`](../src/worker.rs#L150) — `async fn run_cast_check(&self, payload: &str) -> Result<(), JobError>`
- [`tests/api.rs:278`](../tests/api.rs#L278) — `async fn unknown_field_anywhere_is_rejected_with_400()`
### SR-005
@@ -374,6 +394,21 @@ _None._
- [`src/model.rs:258`](../src/model.rs#L258) — `Unknown`
- [`src/validate.rs:227`](../src/validate.rs#L227) — `pub fn validate_manifest(mut m: Jmanifest) -> VResult<ValidManifest>`
### UR-015
**Locations:** 3
- [`src/validate.rs:731`](../src/validate.rs#L731) — `fn extinction_sec_replaces_anneal_sec()`
- [`tests/api.rs:323`](../tests/api.rs#L323) — `async fn the_withdrawn_anneal_sec_field_is_rejected()`
- [`tests/api.rs:347`](../tests/api.rs#L347) — `async fn the_schema_bump_fields_round_trip()`
### UR-016
**Locations:** 2
- [`src/validate.rs:731`](../src/validate.rs#L731) — `fn extinction_sec_replaces_anneal_sec()`
- [`tests/api.rs:347`](../tests/api.rs#L347) — `async fn the_schema_bump_fields_round_trip()`
### UR-017
**Locations:** 2
@@ -381,6 +416,12 @@ _None._
- [`src/model.rs:191`](../src/model.rs#L191) — `Unknown`
- [`src/model.rs:232`](../src/model.rs#L232) — `pub fn from_stored(s: &str) -> Option<Self>`
### UR-018
**Locations:** 1
- [`src/ingest.rs:387`](../src/ingest.rs#L387) — `fn content_id_excludes_belief_and_route()`
### UR-019
**Locations:** 1
+6
View File
@@ -22,6 +22,12 @@ use std::sync::{Arc, Mutex};
use anyhow::Context;
use rusqlite::Connection;
/// The whole schema, as portable SQL — it runs unchanged on Postgres, so
/// SQLite-specific forms (`INSERT OR REPLACE`) are avoided in favour of the
/// standard `INSERT ... ON CONFLICT` (§8). Keeping it in one `.sql` file rather
/// than scattered through the repository is what makes that reviewable.
///
/// TRACES: DR-014 | PR-004
const SCHEMA: &str = include_str!("schema.sql");
/// Handle to the database: one serialized writer, plus read connections.
+1
View File
@@ -384,6 +384,7 @@ mod tests {
assert_eq!(compute_content_id(&a), compute_content_id(&b));
}
/// TRACES: UR-018 | SR-003
#[test]
fn content_id_excludes_belief_and_route() {
// The trap in the SR-003 bump (UR-018). Belief is a producer-side
+1
View File
@@ -320,6 +320,7 @@ pub const JMANIFEST_VERSION: u32 = 2;
mod tests {
use super::*;
/// TRACES: DR-001 | SR-004
#[test]
fn unknown_field_at_top_level_is_rejected() {
let json = r#"{"jmanifest_version":2,"identity":{"type":"movie","tmdb_id":"1"},
+1
View File
@@ -728,6 +728,7 @@ mod tests {
);
}
/// TRACES: UR-015, UR-016 | SR-003
#[test]
fn extinction_sec_replaces_anneal_sec() {
// The SR-003 withdrawal. `anneal_sec` cannot even be constructed here —
+3
View File
@@ -275,6 +275,7 @@ async fn a_pending_manifest_is_not_served() {
assert_eq!(body["status"], "pending");
}
/// TRACES: DR-001 | SR-004
#[tokio::test]
async fn unknown_field_anywhere_is_rejected_with_400() {
// §6 stage 2, enforced by `deny_unknown_fields` on every DTO.
@@ -319,6 +320,7 @@ async fn contributor_local_identifiers_are_rejected_not_ignored() {
assert!(body["message"].as_str().unwrap_or("").contains("jellyfin_id"), "{body}");
}
/// TRACES: UR-015 | SR-003
#[tokio::test]
async fn the_withdrawn_anneal_sec_field_is_rejected() {
// `anneal_sec` was withdrawn in the SR-003 bump: presence now follows track
@@ -342,6 +344,7 @@ async fn the_withdrawn_anneal_sec_field_is_rejected() {
);
}
/// TRACES: UR-015, UR-016 | SR-003
#[tokio::test]
async fn the_schema_bump_fields_round_trip() {
// `extinction_sec` and `gallery_scope` are the SR-003 additions. They are