Files
JRay-public-server/docs/requirements.md
T
dtourolle 7eb5c175af
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
docs: tag the untagged verifications, and record the UR-007 conflict
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
2026-07-31 16:26:10 +02:00

17 KiB

JRay-public-server — requirements register

Stable IDs for every requirement in ../SPEC.md, which holds the prose. This file is the authoritative list; the CI gate reads its denominators from here (see the system spec §6).

IDs are permanent. A withdrawn requirement is marked Withdrawn and its number is never reused — renumbering is what produces orphan TRACES tags.

Tag code with // TRACES: UR-003 | SR-004.

Type Scope
UR User/functional — what the server does
DR Development — how it is built and operated
UT / IT Unit / integration tests

Status: Done · In Progress · Planned · TBD · Withdrawn

A requirement is Done only when it is implemented and has a test that executes. Everything below runs in CI on any machine — this repo has no GPU requirement and no fixture-generation step, unlike scene-actor-extraction.


User requirements (UR)

ID Requirement Traces to Priority Status
UR-001 Cheap existence probe, separate from the fetch, returning availability and cut-match tier without payload SR-001 High Done
UR-002 Accept a contributed manifest for a media item PR-006 High Done
UR-003 Content verification: strict schema, size caps, approximate TMDB cast match SR-004 High Done
UR-004 Rate limiting, per token where present and per source IP otherwise SR-004 High Done
UR-005 Trust without accounts: not usable as a content store, nor for prank manifests SR-004 High Done
UR-006 Serve and accept a whole series in one operation PR-006 High Done
UR-007 Plugin queries an ordered, configurable list of servers PR-005 High In Progress
UR-008 Servers replicate manifests between each other PR-006 Medium Done
UR-009 Store an audio spectral-peak signature for content-based identification SR-003 Medium In Progress
UR-010 Identity crossing the API boundary is TMDB/IMDB ids, never a name alone SR-001 High Done
UR-011 Reject any field capable of carrying binary or attacker-chosen content SR-004 High Done
UR-012 Never accept, store, or serve gallery data — reference faces or embeddings SR-005 High Done
UR-013 Windows are scene-scoped claims; never reinterpret their boundaries SR-002 High Done
UR-014 Reject an unknown jmanifest_version outright, never guess SR-003 High Done
UR-015 Accept extraction.extinction_sec in place of anneal_sec SR-003 High Done
UR-016 Accept and store extraction.gallery_scope; rank on it (§7) SR-003 Medium Done
UR-017 Accept per-window belief and identification route; scenes are objects SR-003 High Done
UR-018 Exclude belief and route from content_id, replicating them as attributes SR-003 High Done
UR-019 Contributed manifests are CC0 1.0; the grant is delivered with the token, not merely published PR-006 High Done

Notes on status

UR-007 is In Progress, not Done. The plugin now carries the ordered 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 a peer — is deliberately not an API: §9a requires that a peering exist only because an operator typed a URL, so it is a database action, and there_is_no_endpoint_that_creates_a_peering asserts the absence.

UR-009 is In Progress. The server accepts, validates and stores cut.audio_signature, and content_id correctly excludes it (§9a). What is absent is audio-tier matching and POST /manifests/search. This is the sequencing §3 recommends — accumulate signatures first, enable matching once coverage is useful — not an oversight.

UR-012 is satisfied structurally, by absence. There is no field in the Jmanifest capable of carrying an embedding or a crop, and no endpoint that would accept one. Like PR-005 in the system spec, it cannot be verified by pointing at code that does something; UT-024 verifies it by asserting that the obvious attempts are rejected.


Development requirements (DR)

ID Requirement Traces to Priority Status
DR-001 Strict parse boundary: unknown fields rejected structurally, not by validator code SR-004 High Done
DR-002 Fully relational storage — no JSON blob on the write path SR-004 High Done
DR-003 Single serialized writer connection, with a read pool alongside PR-004 High Done
DR-004 All database access behind a repository layer, not scattered through handlers PR-004 Medium Done
DR-005 Background work in-process, with the job queue as a table so it survives restart PR-004 High Done
DR-006 Rate-limit counters in process memory; no external counter store PR-004 Medium Done
DR-007 Ship a single static binary plus one database file; container optional PR-004 High Done
DR-008 X-Forwarded-For honoured only from explicitly configured proxies SR-004 High Done
DR-009 Body caps enforced while streaming, before parsing, per route SR-004 High Done
DR-010 Request bodies are UTF-8 only, rejected with a diagnosable error otherwise SR-003 Medium Done
DR-011 content_id canonical form is byte-stable and cross-implementation tested SR-003 High Done
DR-012 Dependency audit: advisories, licence policy, source policy PR-004 Medium Done
DR-013 API errors use the status codes the spec names, not the framework's defaults SR-003 Medium Done
DR-014 Portable SQL — no SQLite-specific form where a standard one exists PR-004 Medium Done

Verification

No GPU, no fixtures, no external services. Every test here runs on any machine in under three seconds. The TMDB dependency is the only external service, and it is absent from tests by construction: an unconfigured client makes uploads stay pending, which is the correct production failure mode (§8) and happens to make the test suite hermetic.

Tier Runs in CI What it covers
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 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 topology is the point, so this is a deliberate choice rather than an oversight.

Per-requirement verification

ID Tier Test asserts Edge cases covered
UR-001 T2 exists reports availability and tier without payload Absent title returns 200 with false, not 404; batch form is positional; one bad item does not fail the batch
UR-002 T2 Valid upload accepted as 202 pending Duplicate content deduplicates; same contributor resubmitting the same cut is 409
UR-003 T1 + T2 Strict schema, caps, and cast-match thresholds Ratio boundaries at 0.6 and 0.3 exactly; small-|M| all-but-one rule; missing TMDB credits flags rather than rejects
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 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
UR-011 T2 Every payload-shaped field rejected base64, hex, markup, control characters, bidi overrides, compatibility homoglyphs
UR-012 T2 No endpoint accepts embeddings or image data An embedding or crop field is an unknown-field 400
UR-013 T1 Stored windows are byte-identical to those submitted Adjacent windows never merged; a window is never trimmed to a shorter one
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
DR-010 T1 Non-UTF-8 rejected by name UTF-16 with and without BOM; UTF-8 BOM; declared charset=utf-16
DR-011 T1 Canonical form is stable and order-independent Accumulated float error hashes identically; audio_signature and extraction excluded; golden vector verified against an independent Python implementation
UR-015 T2 extinction_sec accepted and range-checked A manifest still carrying anneal_sec is a hard 400 naming the field
UR-016 T2 gallery_scope stored and served An unrecognised scope is a closed-vocabulary 400, not a free string
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:

  • DR-009's lying-Content-Length case. §6 stage 0 is explicit that the header is a claim by the client, so the streaming cap is mandatory rather than redundant. A test that only sends honest bodies verifies nothing.
  • DR-011's golden vector. Two servers that validated the same upload must reach the same content_id, and the plugin must reproduce it byte-identically from a different language. The fixture is the only thing that can catch divergence before it silently breaks federation deduplication.
  • UR-011's homoglyph case. Writing this test found a real gap: compatibility variants (𝐒𝐭𝐞𝐯𝐞, Actor) are letters by Unicode category and NFC does not fold them, so a fullwidth-digit alphabet would have reopened the encoding channel §5a's "no digits" rule closes.

Withdrawn

ID Requirement Reason
— anneal_sec in extraction Withdrawn upstream (scene-actor-extraction AR-012/AR-013): presence follows track extent, so a track survives its own gaps and there is nothing to anneal. Ships as part of the SR-003 bump

Deleted rather than retained at zero: a field naming a mechanism the pipeline no longer has is actively misleading to anyone reading a manifest, and would outlive everyone who remembers why it is zero.

No UR/DR number was ever assigned to it — it was a field, not a requirement — so nothing is orphaned by its removal.


The SR-003 schema bump — shipped at version 2

jmanifest_version moved to 2 in lockstep with the truth file's schema_version, per SR-003's requirement that breaking changes be batched and ship together. The two fields stay independent by design; they coincide at 2 only because this bump touched both.

Flag day, not dual-accept (jRay JR-003): version 1 is 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.

UR-018 was the one with a trap in it. 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 content_ids for the same content — the exact failure mode §9a quantises centiseconds to avoid, reintroduced one field along. It follows audio_signature's precedent: replicated as an attribute, never as identity. src/content_id.rs is unchanged by the bump, and its golden vector still passes — which is the evidence, not the claim.

Notes on coverage

  • DR-* traces to PR-004 (self-hosted) more often than to an SR-nnn. Operational simplicity is a single-repo concern serving the project goal directly. This is correct rather than a gap: §8's whole argument for one binary and one file is that federation only works if running an instance is easy.
  • UR-007 has no server-side test and cannot have one — it is a requirement on the plugin, recorded here because this spec is where it is stated. It should be cross-referenced from the plugin's register when that is created, and until then it is visibly unverified rather than quietly assumed.
  • UR-012 and UR-013 are preserved by prohibition, like PR-005 in the system spec. They cannot be verified by pointing at code that does something, only by asserting that the attempts fail. They die the moment either prohibition is relaxed, which is precisely why they are stated rather than left implicit.