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>
22 KiB
jRay — 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 ../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 §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 §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 | 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 §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, 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, so the invocation carries no
flags to drift out of sync between a developer's shell and CI:
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).