jRay had no requirement IDs, so nothing in this repo could be traced to and the CI gate had no denominator to read. The other two components had already moved to registers; this brings the plugin level with them. Adds docs/requirements.md with 46 permanent JR-nnn IDs, each carrying a parent requirement, priority, status and verification tier, plus a per-requirement verification plan. JR is flat rather than split by theme: the plugin is one deployable with one audience, and JRay-public-server already ships UR/DR, so a second repo using those prefixes would make UR-007 ambiguous across registers. Rewrites SPEC.md as requirements prose with Current:/Gap: on every one. It had drifted into a format-plus-API reference that documented schema_version 1 while owning a format whose v2 shape was specified only in the other two repos, said nothing about SR-002's scene-scoped semantics, and carried the manifest exchange as a "planned" aside while its configuration classes were already implemented. Plugin-side exchange obligations move here from the server's spec, where they were an ownership inversion. Adds JR-038..041 for PR-005, which had no software row in any repo -- it was held structurally by SR-004 and GR-005 both being prohibitions, and a goal preserved only by prohibitions is the kind that erodes unnoticed. jRay is the component that actually opens a socket. Tags 18 units with the requirements they satisfy. Tags name what the code satisfies, so FileTransformationRegistration is not tagged JR-021: that requirement is a prohibition and was still violated elsewhere when this was written. Vendors jray-project as a submodule for the system spec and shared gate. TRACES: JR-001, JR-004, JR-005, JR-007, JR-008, JR-009, JR-010, JR-011 TRACES: JR-012, JR-013, JR-014, JR-015, JR-016, JR-017, JR-018, JR-019 TRACES: JR-020, JR-024, JR-025, JR-036, JR-038 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
270 lines
20 KiB
Markdown
270 lines
20 KiB
Markdown
# jRay — 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 [`../scripts/vendor/jray-project/SPEC.md`](../scripts/vendor/jray-project/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. This
|
|
register replaces the earlier section-numbering of `SPEC.md`, which gave the
|
|
plugin no way to be traced to and left it outside the chain entirely.
|
|
|
|
Tag code with `// TRACES: JR-012 | SR-002`.
|
|
|
|
| Type | Scope |
|
|
|---|---|
|
|
| `JR` | Everything this plugin does — truth format, API, overlay, exchange client |
|
|
| `UT` / `IT` | Unit / integration tests |
|
|
|
|
`JR` is flat rather than split by theme. The plugin is one deployable with one
|
|
audience, and the thematic grouping lives in the section headings below, where it
|
|
costs nothing and cannot go stale against a prefix.
|
|
|
|
Status: `Done` · `In Progress` · `Planned` · `TBD` · `Withdrawn`
|
|
|
|
---
|
|
|
|
## Truth-file format (JR-001 … JR-007)
|
|
|
|
jRay **owns** this format ([system spec](../scripts/vendor/jray-project/SPEC.md) §1); extraction is the
|
|
producer and the public server carries a derived envelope. Changes are
|
|
coordinated `schema_version` bumps (SR-003).
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-001 | The truth-file format is normatively defined here; other repos reference it rather than restating it | SR-003 | High | In Progress |
|
|
| JR-002 | `schema_version: 2` shape — `extraction.*` provenance block, `cut.*` block, `scenes` as objects carrying belief and route | SR-003 | High | Planned |
|
|
| JR-003 | Reject an unknown `schema_version`, never guess. **Flag day: v2 only**, no dual-accept | SR-003 | High | Planned |
|
|
| JR-004 | A window is a **scene-membership claim**, not a recognition event — never reinterpreted, merged, split or trimmed | **SR-002** | High | Planned |
|
|
| JR-005 | Query semantics: actor present at `t` if any window contains `t`; presentation must not assert instantaneous visibility | **SR-002** | High | In Progress |
|
|
| JR-006 | Read path holds up under **numerous** windows — no assumption of a handful of long ones | SR-002 | Medium | Planned |
|
|
| JR-007 | Identity is public identifiers: prefer `jellyfin_id` locally, else resolve `imdb_id`/`tmdb_id` against the item's People `ProviderIds` | SR-001 | High | Done |
|
|
|
|
## Truth-data sources and precedence (JR-008 … JR-011)
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-008 | Discover a sidecar truth file beside the media, by configurable suffix | PR-001 | High | Done |
|
|
| JR-009 | Accept truth data pushed by a remote worker (`PUT`/`DELETE`), admin key | PR-004 | High | Done |
|
|
| JR-010 | Precedence: managed truth (pushed **or** fetched) overrides a sidecar; provenance is recorded so the UI can distinguish the three sources | PR-001 | High | In Progress |
|
|
| JR-011 | Loaded truth is cached; any write invalidates the item's cache entry immediately | PR-001 | Medium | Done |
|
|
|
|
## Read API (JR-012 … JR-014)
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-012 | `GET .../Timeline` returns the full truth file for an item | PR-001 | High | Done |
|
|
| JR-013 | `GET .../jray?t=` returns an **extensible** context envelope; consumers ignore unknown keys | PR-001 | High | Done |
|
|
| JR-014 | Authorisation: reads need an authenticated user, admin routes need the Administrator role, only `ClientScript` is anonymous | PR-004 | High | Done |
|
|
|
|
## Work discovery, policy and coverage (JR-015 … JR-019)
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-015 | `Tasks/Pending` serves a random sample of items with no truth data, so pollers spread across the backlog without server-side task state | PR-003 | High | Done |
|
|
| JR-016 | Prioritise/ignore rules scoped `Genre` / `Series` / `Item`; **most specific wins**; scope+value is the unique key | PR-003 | Medium | Done |
|
|
| JR-017 | Rules steer **work discovery only** — never the overlay or the read endpoints | PR-003 | Medium | Done |
|
|
| JR-018 | Coverage report by media type and genre; ignored items leave the percent-done denominator rather than dragging it down | PR-003 | Medium | Done |
|
|
| JR-019 | Picker endpoints (genres, series, item search) populate the rule editor | PR-003 | Low | Done |
|
|
|
|
## Player overlay (JR-020 … JR-024)
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-020 | Pause overlay: injected client script queries `jray?t=` and renders the scene's cast | **PR-001** | High | Done |
|
|
| JR-021 | **jRay never injects into `index.html` on disk.** File Transformation is a hard dependency; there is no on-disk fallback. The only permitted write is JR-022's removal | PR-004 | High | **Done** |
|
|
| JR-022 | Migration: remove any on-disk patch left by an earlier jRay, identified by the `<!-- jray-overlay -->` marker | PR-004 | High | In Progress |
|
|
| JR-023 | Absent the dependency, disable **only** the overlay and say so in the log and the config page; never bundle the assembly | PR-004 | Medium | In Progress |
|
|
| JR-024 | Actor names and all server-supplied strings render as **text, never markup** | SR-004 | High | Done |
|
|
|
|
## Manifest exchange client (JR-025 … JR-037)
|
|
|
|
Plugin-side requirements for the exchange specified in
|
|
[`../../JRay-public-server/SPEC.md`](../../JRay-public-server/SPEC.md) §9. The
|
|
wire format is the server's; **the client's obligations are jRay's**, and belong
|
|
in this register rather than in the server's spec.
|
|
|
|
The server's register already anticipates this: its `UR-007` is recorded as
|
|
having "no server-side test and cannot have one — it is a requirement on the
|
|
plugin", to be cross-referenced from the plugin's register once one exists. This
|
|
is that register, and `JR-025` is that row. `UR-007` should now point here and
|
|
stay `In Progress` until `JR-025` is `Done`.
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-025 | Query an **ordered list** of servers; first result clearing the configured tier wins — **satisfies `JRay-public-server` UR-007** | PR-006 | High | In Progress |
|
|
| JR-026 | For a series, first-match applies per **episode** — later servers are queried only for the episodes earlier ones lacked | PR-006 | Medium | Planned |
|
|
| JR-027 | Treat **every** server as untrusted, including the default: re-validate on receipt against the strict upload schema, bounds-check windows against the item's real runtime | SR-004 | High | Planned |
|
|
| JR-028 | Enforce response size caps **while streaming** — 2 MiB single, 25 MiB bundle — aborting rather than buffering | SR-004 | High | Planned |
|
|
| JR-029 | HTTPS required for non-loopback servers; certificate validation must not be disabled | SR-004 | High | Planned |
|
|
| JR-030 | Apply an `audio`-tier `offset` to **every** window before storing — stored truth is always in the local file's timebase, so read paths need no offset awareness | SR-003 | High | Planned |
|
|
| JR-031 | Fetch endpoints: item fetch, series bundle fetch, per-server status, content identify | PR-006 | High | Planned |
|
|
| JR-032 | Identify is **never automatic** — storing a candidate is a separate confirmation step | PR-006 | Medium | Planned |
|
|
| JR-033 | Scheduled sweep over items lacking truth data, using the **batch** `exists` endpoint | PR-006 | Medium | Planned |
|
|
| JR-034 | Contribution strips `movie` and `jellyfin_id`, attaches identity from `ProviderIds` plus measured runtime, and posts **only** to contribute-enabled servers — never fanned out | PR-005 | High | Planned |
|
|
| JR-035 | Uploads set `Expect: 100-continue`, so a rejection lands before a bundle body is transmitted | PR-006 | Low | Planned |
|
|
| JR-036 | Minimum accepted match tier is configurable; a `loose` match surfaces as a caveat rather than being applied silently | PR-006 | Medium | In Progress |
|
|
| JR-037 | A server that is unreachable or failing is skipped on a short timeout with backoff; one dead server never stalls a sweep | PR-006 | Medium | Planned |
|
|
|
|
## Egress and privacy (JR-038 … JR-041)
|
|
|
|
`PR-005` had **no software row in any repo** — it was held structurally, by
|
|
SR-004 and GR-005 both being prohibitions. jRay is the component that actually
|
|
performs egress, so these are the rows that make it verifiable rather than merely
|
|
preserved.
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-038 | Every exchange feature is **opt-in and off by default**, including the pre-configured community server | **PR-005** | High | Done |
|
|
| JR-039 | No library-wide inventory in one request: batch `exists` capped at 100 items, sweeps paced | **PR-005** | High | Planned |
|
|
| JR-040 | The config page states plainly that **each configured server multiplies the exposure** | **PR-005** | Medium | Planned |
|
|
| JR-041 | The plugin never fetches, stores, or transmits gallery data — reference faces or embeddings. It has no gallery code path at all | **SR-005** | High | Done |
|
|
|
|
## Audio signature (JR-042 … JR-045)
|
|
|
|
Mirror-image of extraction `IR-004`/`IR-005`/`IR-007`/`IR-008`. Both producers
|
|
must agree **bit-for-bit**, so each obligation is stated on both sides rather
|
|
than assumed to be inherited.
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-042 | Compute the signature **exactly** per server spec §3, using the FFmpeg binary Jellyfin already ships via `IMediaEncoder.EncoderPath` — no new dependency | SR-003 | Medium | Planned |
|
|
| JR-043 | Golden-vector fixture **shared with the extraction repo**, proving the two implementations are bit-exact | SR-003 | High | Planned |
|
|
| JR-044 | Media shorter than 120 s: emit no signature and apply no sync offset — identical rule in both producers | SR-003 | Low | Planned |
|
|
| JR-045 | Emit and honour the signature's own `v1:` prefix, so a DSP change is detectable rather than silently non-matching | SR-003 | Low | Planned |
|
|
|
|
## Human-in-the-loop association (JR-046)
|
|
|
|
| ID | Requirement | Traces to | Priority | Status |
|
|
|---|---|---|---|---|
|
|
| JR-046 | Review UI for unidentified track clusters: show context crops, pick from the title's cast or search TMDB, record the association | [system §4](../scripts/vendor/jray-project/SPEC.md) | Medium | **TBD** |
|
|
|
|
Deliberately a single placeholder row rather than a decomposed set. It depends on
|
|
extraction `AR-021`/`AR-022` landing, and on system open question 2 (whether
|
|
unidentified presence is published at all) — decomposing it now would fix an
|
|
interface against an undecided upstream.
|
|
|
|
---
|
|
|
|
## Verification strategy
|
|
|
|
**CI is an Intel N100** ([system spec](../scripts/vendor/jray-project/SPEC.md) §6). Unlike the extraction
|
|
pipeline this costs jRay almost nothing: the plugin is CPU-only managed code, and
|
|
every requirement above except the live-integration ones is executable in CI.
|
|
|
|
| Tier | Runs in CI | What it covers |
|
|
|---|---|---|
|
|
| **T1 — Unit** | Yes | Parsing, precedence, policy resolution, coverage arithmetic, offset application, audio DSP, schema rejection |
|
|
| **T2 — Host integration** | Yes | Controllers and authorisation against a test host with a faked `ILibraryManager` |
|
|
| **T4 — Live** | **No** | Real Jellyfin + File Transformation + web client; real manifest server round-trip |
|
|
| **static** | Yes | Grep/analyzer checks — e.g. no injection path into `index.html` (JR-021) |
|
|
|
|
**T3 is deliberately unused.** The gate's `CI_EXECUTABLE_TIERS` treats T1/T2/T3
|
|
as CI-runnable and T4 as not, which is right for extraction (where T3 is slow CPU
|
|
inference and T4 is GPU). jRay has only two CI tiers and one live tier, so its
|
|
non-CI tier is numbered **T4** to match that shared constant rather than
|
|
renumbering it. Calling jRay's live tier "T3" would make the gate count
|
|
live-only requirements as covered — the exact class of error the 158% coverage
|
|
bug belongs to.
|
|
|
|
**There is no test project today.** That is the single largest gap in this
|
|
register: 46 requirements, zero `UT`/`IT` IDs, so measured coverage will open at
|
|
zero and every `Done` above rests on inspection rather than evidence.
|
|
|
|
### Per-requirement verification plan
|
|
|
|
| ID | Tier | Test asserts | Edge cases to cover |
|
|
|---|---|---|---|
|
|
| JR-001 | static | Other repos' specs link here rather than restating the schema | A second copy of the schema anywhere is the failure |
|
|
| JR-002 | T1 | A v2 file round-trips; `scenes` objects retain belief and route | Window with belief exactly at the ownership threshold; all three route values |
|
|
| JR-003 | **T1** | `schema_version` 1 and 3 are both **rejected**, not coerced | Missing field entirely; non-integer value |
|
|
| JR-004 | T1 | Windows are stored and served byte-identical to input | Adjacent windows that "look" mergeable must **not** merge |
|
|
| JR-005 | T1 | `t` exactly on `start` and on `end` are both present | Zero-length window; overlapping windows for one actor |
|
|
| JR-006 | T1 | Query cost is acceptable with 10³ windows on one actor | Sorted-window assumption stated and tested |
|
|
| JR-007 | T1 | `jellyfin_id` preferred; falls back to provider ids | All three ids empty → actor still displayable by name |
|
|
| JR-008 | T1 | Sidecar path derived from the item path plus the configured suffix | Item with no path; suffix changed at runtime |
|
|
| JR-009 | T2 | `PUT` stores, `DELETE` removes, both admin-only | `DELETE` on an item with no managed truth is still `204` |
|
|
| JR-010 | T1 | Managed overrides sidecar; provenance survives | Fetched and pushed truth for the same item |
|
|
| JR-011 | T1 | A write invalidates the cached entry immediately | Read, push, read again within the cache window |
|
|
| JR-012 | T2 | Returns the file, or `404` when no source has data | Sidecar present but unparseable |
|
|
| JR-013 | T2 | Envelope shape is stable; extra keys are additive | Item with truth data but no actor present at `t` |
|
|
| JR-014 | T2 | Anonymous request to each admin route is refused | Authenticated non-admin on an admin route |
|
|
| JR-015 | T2 | Sample excludes covered items and clamps `limit` | `limit` of 0 and of 1000; library of missing-path ghosts |
|
|
| JR-019 | T2 | Pickers return `{value,label}`; empty search returns `[]` | Two episodes named "Pilot" — labels must disambiguate |
|
|
| JR-020 | **T4** | Overlay appears on pause and lists the scene cast | Live web client only |
|
|
| JR-016 | T1 | Item beats Series beats Genre | Prioritised series inside an ignored genre — the case that motivated the rule |
|
|
| JR-017 | **T1** | An ignored item still serves its overlay | Rule added after truth data exists |
|
|
| JR-018 | T1 | `covered / (total - ignored)` | Item carrying two genres counts in both rows |
|
|
| JR-021 | **static** | No code path *adds* the script tag to `index.html` | `scripts/checks/no-index-injection.sh`. Removal (JR-022) is the one permitted write, so the check is on injection, not on writing. Verified to **fail** on a reintroduced `Apply()` and on reintroduced `ReplaceLast` injection, not merely to pass today |
|
|
| JR-022 | T1 | A marked legacy patch is removed; unmarked content untouched | Foreign plugin's injection left intact |
|
|
| JR-023 | T1 + **T4** | Absent dependency disables only the overlay | Detection unit-testable; config-page display is live |
|
|
| JR-024 | T1 | A name containing markup renders escaped | `<script>` in an actor name from a hostile server |
|
|
| JR-025 | T1 | First result clearing the tier wins; disabled servers skipped | All servers fail; first server returns a below-tier match |
|
|
| JR-026 | T1 | Server 2 queried only for episodes server 1 lacked | Bundle with a gap in the middle of a season |
|
|
| JR-027 | T1 | Unknown field, oversized body, and out-of-range window each rejected | Window ending beyond the item's runtime |
|
|
| JR-028 | T1 | Stream aborts past the cap rather than buffering | Server declaring a small length and sending more |
|
|
| JR-029 | T1 | Plain `http` to a non-loopback host is refused | `http://localhost` allowed; `http://192.168.x` refused |
|
|
| JR-030 | **T1** | Offset added to every window before storage | Negative offset; offset that would push a window below zero |
|
|
| JR-031 | T2 | All four routes exist and are admin-only | — |
|
|
| JR-032 | T1 | `Identify` returns candidates and stores nothing | A single high-confidence candidate still does not auto-store |
|
|
| JR-033 | T1 | Sweep batches through `exists` and paces | Backlog smaller than one batch |
|
|
| JR-034 | **T1** | `movie` and `jellyfin_id` absent from the upload body | Contribution attempted to a `FetchOnly` server must not send |
|
|
| JR-035 | T1 | `Expect: 100-continue` set on uploads | — |
|
|
| JR-036 | T1 | Below-tier match is not stored; `loose` is flagged | Tier configured to `exact` with only a `runtime` match available |
|
|
| JR-037 | T1 | Failing server skipped, backoff grows | Every server failing must not hang the sweep |
|
|
| JR-038 | **T1** | Every exchange switch defaults off; community server disabled | Fresh config object, no user input |
|
|
| JR-039 | T1 | Batch never exceeds 100 items | Library of 10⁴ items produces a paced sweep |
|
|
| JR-040 | **T4** | Config page states the per-server exposure | Manual review of copy |
|
|
| JR-041 | **static** | No embedding or image field is parsed or stored | Grep-based, mirroring the server's UR-012 |
|
|
| JR-042 | T1 | DSP chain matches the specified parameters exactly | Window, hop, band, bin count each asserted individually |
|
|
| JR-043 | **T1** | Signature matches the shared golden vector **bit-for-bit** | Media < 120 s → no signature; identical result in both repos |
|
|
| JR-044 | T1 | Media < 120 s yields no signature and no offset | Exactly 120 s — the boundary both repos must agree on |
|
|
| JR-045 | T1 | `v1:` emitted; an unknown prefix is refused, not parsed | `v2:` signature from a future producer |
|
|
| JR-046 | **TBD** | — | Undesigned; depends on AR-021/AR-022 and system open question 2 |
|
|
|
|
Three are worth singling out. **JR-021** and **JR-041** are static checks because
|
|
both are requirements to *not do something*, and a prohibition is verified by
|
|
absence, not by a passing test. **JR-043** is the cross-repo check: it is the only
|
|
test in this repo whose fixture is shared with another, and it is CPU-only DSP,
|
|
which is exactly why it can be the binding check rather than an aspiration.
|
|
|
|
---
|
|
|
|
## Running the gate
|
|
|
|
The shared extractor now takes the three things that vary per repo as arguments,
|
|
so this repo needs **no fork of it** — there must only ever be one
|
|
implementation:
|
|
|
|
```sh
|
|
python3 scripts/vendor/jray-project/scripts/traceability/extract_traces.py \
|
|
--root . \
|
|
--requirements docs/requirements.md \
|
|
--system-spec scripts/vendor/jray-project/SPEC.md \
|
|
--types JR \
|
|
--suffixes .cs,.js,.sh \
|
|
--scan-roots Jellyfin.Plugin.JRay,scripts/checks \
|
|
--format coverage
|
|
```
|
|
|
|
`scripts/checks` is scanned so the static checks carry their own TRACES tags —
|
|
an enforcement script is evidence for a requirement exactly as a unit test is.
|
|
The scan root is `scripts/checks` and **not** `scripts`, because the latter would
|
|
walk `scripts/vendor/jray-project` and harvest the `AR-nnn` examples in the
|
|
extractor's own docstrings as orphan tags.
|
|
|
|
`--root` must be **absolute or `.`**; the scan roots resolve beneath it. Both the
|
|
extractor and the system spec come from the submodule, so the only thing this
|
|
repo supplies is its own register and the three per-repo arguments.
|
|
|
|
Refresh the pinned tooling with
|
|
`git submodule update --remote scripts/vendor/jray-project`.
|
|
|
|
**Naming conflict to resolve.** The tool's header comment expects
|
|
`jRay → UR/DR`. This register uses `JR`, decided deliberately: `JRay-public-server`
|
|
already ships `UR-001…018` and `DR-001…014`, so a second repo using the same
|
|
prefixes would make `UR-007` ambiguous across registers — and `UR-007` is
|
|
precisely the ID the server's own register asks the plugin to cross-reference
|
|
(see JR-025). Either the comment or this register is wrong; the comment is the
|
|
cheaper of the two to change.
|