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
258 lines
19 KiB
Markdown
258 lines
19 KiB
Markdown
# JRay-public-server — requirements register
|
|
|
|
Stable IDs for every requirement in [`../SPEC.md`](../SPEC.md), which holds the
|
|
prose. This file is the **authoritative list**; the CI gate reads its
|
|
denominators from here (see the [system spec](../../SPEC.md) §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.**
|
|
> `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 `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.** `ingest` validated `cut.audio_signature` and then 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. 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`](../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-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_id`s
|
|
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.
|