Contributed manifests were in no declared condition at all, which left §9a replication with no grant flowing through it: peers mirror each other's catalogues wholesale, and every hop of that was unlicensed. CC0 rather than a share-alike licence, because a share-alike works by asserting a right in the data and then conditioning its use. The position in docs/legal-posture.md §3 is that presence timings are facts rather than protectable expression — asserting copyright in them in order to license them would contradict that argument in the same repository, and that contradiction is worth more to an opponent than the licence is worth to us. CC0 also waives the sui generis database right by name, closing the EU-specific residual exposure from the contributor's side. The grant is taken at token issuance, and that is not incidental. There are no accounts, so there is no sign-up to attach terms to, and a manifest arrives over POST /manifests with no channel to negotiate over. Acquiring the contribute capability is the only moment a grant can be made, so POST /tokens now returns the licence and its terms alongside the token — a licence the server publishes but never delivers is one no contributor agreed to. The test pins the scope limit as well as the identifier. Bounding the grant to the manifest is the half that can fail silently: a reworded term reading onto the underlying work would purport to grant what no contributor can. Also records the settled code-licence position across all four repositories in the legal posture, correcting an earlier claim there that the plugin and extraction repos declared nothing. Both already carried LICENSE files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: UR-019 | PR-006
218 lines
15 KiB
Markdown
218 lines
15 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 | 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.
|
|
|
|
**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 |
|
|
|
|
There is no tier that does not run. A requirement here is either verified or
|
|
visibly not.
|
|
|
|
**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 | — | *No server-side test.* Plugin-side (`jRay` JR-025); the register there carries it | — |
|
|
| 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-003 | T1 | Concurrent writes serialize rather than returning `SQLITE_BUSY` | Failed transaction rolls back fully |
|
|
| 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` |
|
|
|
|
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.
|