jray-project replaced the extractor's per-repo CLI flags with a traceability.toml at each component root, so the invocation this repo documented -- --types, --suffixes, --scan-roots -- no longer exists. Bumps the submodule to 17106f3 and moves those settings into config. The gate is now `extract_traces.py --root . --format coverage`, with nothing per-repo on the command line to drift between a developer's shell and CI. Two settings carry reasons worth keeping. Source roots are listed individually rather than as "scripts", because the latter walks scripts/vendor and harvests the AR-nnn examples in the extractor's own docstrings as orphan tags. ci_executable_tiers omits T4: tiers are per-repo now, but T4 keeps the meaning it has in scene-actor-extraction -- "no CI host can run this" -- so a tier number reads the same across repos. Drops the note about the tool expecting jRay to use UR/DR. Its example config now names jRay: ["JR"], so the prefix is settled in all three repos. Same numbers as before the change: 21 tags, 25/46, 0 orphans. TRACES: JR-021 | PR-004 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
306 lines
22 KiB
Markdown
306 lines
22 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 |
|
|
|
|
## Tests (UT)
|
|
|
|
| ID | Asserts | Covers | Status |
|
|
|---|---|---|---|
|
|
| UT-001 | Marked tag **and its trailing newline** removed — no blank line accumulates per upgrade cycle | JR-022 | Written |
|
|
| UT-002 | Marked tag with no trailing newline removed | JR-022 | Written |
|
|
| UT-003 | Document without the marker left byte-identical | JR-022 | Written |
|
|
| UT-004 | Removal is idempotent — startup runs it on every boot forever after | JR-022 | Written |
|
|
| UT-005 | **Another plugin's injection left intact** — it is their file too | JR-022 | Written |
|
|
| UT-006 | An unmarked look-alike script tag is left alone — JRay did not write it | JR-022 | Written |
|
|
| UT-007 | No matching rule resolves to `null` | JR-016 | Written |
|
|
| UT-008 | Item rule beats Series rule | JR-016 | Written |
|
|
| UT-009 | **Prioritised series inside an ignored genre — series wins** | JR-016 | Written |
|
|
| UT-010 | Genre matching is case-insensitive | JR-016 | Written |
|
|
| UT-011 | A Series rule valued `Guid.Empty` does not swallow every movie | JR-016 | Written |
|
|
|
|
`Written` rather than `Passing`: see the execution blocker below. A test whose
|
|
result nobody has seen is not evidence, and recording it as passing would be the
|
|
same failure as counting a GPU-only test as covered.
|
|
|
|
`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.** Executable tiers are declared per repo in
|
|
[`../traceability.toml`](../traceability.toml), so the numbering is a local
|
|
choice — but jRay keeps **T4** for "no CI host can run this" because that is
|
|
what T4 means in `scene-actor-extraction`. A tier number should mean the same
|
|
thing when read across repos; reusing T3 for a live tier here would make a
|
|
cross-repo reader count live-only requirements as covered.
|
|
|
|
`Jellyfin.Plugin.JRay.Tests` (xUnit, in the solution) carries the T1 tier. It
|
|
builds clean alongside the plugin.
|
|
|
|
### Execution blocker — the suite does not run on this machine
|
|
|
|
The plugin framework-references **`Microsoft.AspNetCore.App`** through
|
|
`Jellyfin.Controller`, so the test host demands that shared framework even for
|
|
tests that touch only pure logic. This box has `Microsoft.NETCore.App` 8.0.29
|
|
and 10.0.10 and **no ASP.NET Core runtime at all**, so `dotnet test` aborts
|
|
before a single test executes:
|
|
|
|
```
|
|
Framework: 'Microsoft.AspNetCore.App', version '9.0.0' — No frameworks were found.
|
|
```
|
|
|
|
`<RollForward>LatestMajor</RollForward>` on the test project solves the *other*
|
|
half — the plugin targets `net9.0` to match Jellyfin's ABI, and no .NET 9
|
|
runtime is installed either — but roll-forward cannot conjure a framework of
|
|
which no version exists.
|
|
|
|
**Fix:** install the ASP.NET Core runtime (`aspnet-runtime` on Arch). It is
|
|
needed on the CI host for the same reason, so this belongs in the CI image
|
|
rather than in a developer's setup notes.
|
|
|
|
Until then every `UT` above is **Written, not Passing**, and no requirement
|
|
should be promoted to `Done` on the strength of them.
|
|
|
|
### 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 extractor is shared and vendored, never forked — there must only ever be one
|
|
implementation. Everything that varies per repo lives in
|
|
[`../traceability.toml`](../traceability.toml), so the invocation carries no
|
|
flags to drift out of sync between a developer's shell and CI:
|
|
|
|
```sh
|
|
python3 scripts/vendor/jray-project/scripts/traceability/extract_traces.py \
|
|
--root . --format coverage
|
|
```
|
|
|
|
That config declares the `JR` prefix, the languages, the source roots, the
|
|
CI-executable tiers, and the path to the vendored system spec.
|
|
|
|
Refresh the pinned tooling with
|
|
`git submodule update --remote scripts/vendor/jray-project`.
|
|
|
|
Two choices in it are worth knowing about. `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. And the source roots are
|
|
listed individually rather than as `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.
|
|
|
|
`JR` is now what the shared tooling expects too — its example config names
|
|
`jRay: ["JR"]` — so the prefix is settled across all three repos. It was chosen
|
|
because `JRay-public-server` already ships `UR-001…018` and `DR-001…014`, and a
|
|
second repo reusing those prefixes would make `UR-007` ambiguous across
|
|
registers, which is precisely the ID the server's own register asks this one to
|
|
cross-reference (see JR-025).
|