Completes UR-009. The register recorded the signature as stored; it was not. `ingest` validated `cut.audio_signature` and wrote NULL, so every served manifest came back without one — which also meant the plugin's own alignment (jRay JR-047, Done) had nothing to align against and could never run. That is the failure mode a status field is least able to catch: every validation test passed and the feature delivered nothing. Now stored, coarse-indexed and served back byte-identically, with a held manifest adopting an incoming signature it lacked (§9a). `audio`-tier matching runs on every read endpoint via an `audio_signature` parameter, and `POST /manifests/search` answers the unknown-providence case with a runtime prefilter, a bounded scan and honest truncation reporting. Three rules §3 did not previously state, now normative: - **±1 frame of slack in the score.** scene-actor-extraction VR-014 measured the exact-frame rule demoting 27 of 40 correctly aligned releases to `loose`, because the two windows are cut on their own file's frame grid and those grids do not coincide. With ±1 frame all 40 reach `audio` (worst 0.906) and the strongest false match is unmoved at 0.16. - **The offset has two terms.** Both windows are anchored at their own file's runtime/2, so the slide alone is wrong by half the runtime difference on every shifted release. A signature without a runtime therefore cannot align, and is refused by name rather than answered at a lower tier. - **A signature verdict is final**, including its refusals. Falling back to the runtime tier after the audio declined would let a coincidence overturn direct evidence, inverting the ordering the tier table exists to state. The slide precomputes each frame's neighbourhood as a 32-bit bin set rather than re-deriving it across 1201 slides — 3.3 ms to 1.1 ms per candidate, with a test asserting exact equivalence to the rule written the obvious way. The 1000-candidate search cap follows from that measurement as a ~1.1 s ceiling per request, not a round number. jRay's matcher still implements the pre-slack rule and will label some alignments `loose` that this server calls `audio`. Nothing misaligns — JR-047 makes the local answer win — but that register now carries the follow-up. TRACES: UR-008, UR-009 | SR-003
19 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 | Done |
| 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.
jRayJR-025 is recordedDoneand claims to satisfy UR-007, butjRayJR-031 — the fetch endpoints that would exercise the ordered list — is stillPlanned. 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 Done. The server accepts, validates, stores and serves
cut.audio_signature; content_id correctly excludes it (§9a), and a held
manifest lacking one adopts an incoming signature rather than discarding it.
audio-tier matching runs on every read endpoint, and POST /manifests/search
answers the unknown-providence case.
The register previously recorded this row as storing the signature, and it did not.
ingestvalidatedcut.audio_signatureand then wroteNULL, so every served manifest came back without one — which also meant the plugin's own alignment (jRayJR-047,Done) had nothing to align against and could never run. Recorded here because it is the failure mode a status field is least able to catch: every validation test passed, and the feature delivered nothing.
§3's scoring rule changed with this row. A frame now agrees within ±1
frame rather than exactly, on scene-actor-extraction VR-014's measurement —
the exact rule demoted 27 of 40 correctly aligned releases to loose because
the two windows' frame grids do not coincide. jRay's AudioSignatureMatcher
still implements the pre-change rule, so the plugin will label some alignments
loose that this server calls audio. Its local answer supersedes the server's
on the fetch path (JR-047), so nothing is misaligned by the divergence — but the
two are now out of step with §3, and the plugin register should carry the
follow-up.
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 + T2 | Signature validated, stored, served back, and matched on | Fixed length; reserved high bit; media < 120 s must send no signature at all; a v2: payload is refused rather than parsed; the served signature is byte-identical to the contributed one — a signature that is validated and then dropped passes every validation test and delivers nothing; a slide past the ±600-frame cap and unrelated content are both declined, never given a best-effort alignment; the offset carries the window-anchor term, not the slide alone; a signature disagreement is not overturned by a runtime coincidence; a search never returns a pending manifest |
| 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-Lengthcase. §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 toPR-004(self-hosted) more often than to anSR-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.