Status and verification rows for the six requirements landed in this branch, plus UT-029 … UT-052, and the generated traceability matrix that `docs/traceability.md` holds in the other two components but was missing here. `traces-report.json` is gitignored to match the server: the matrix is committed, the JSON report is not, since nothing reads it back. Two corrections rather than additions: `AudioSignatureTests` was tagged UT-029 … UT-035, IDs the register had already assigned to the schema tests. Renumbered to UT-038 … UT-044, matching the register, which was right — duplicate IDs defeat the point of IDs being permanent. `ReadCappedAsync` implements JR-028's response cap and carried no tag, so the requirement read as uncovered. The gate reports 0 orphan tags. TRACES: JR-002, JR-003, JR-028, JR-041, JR-042, JR-043, JR-044, JR-045 | SR-003
399 lines
33 KiB
Markdown
399 lines
33 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 | **Passing** |
|
||
| UT-002 | Marked tag with no trailing newline removed | JR-022 | **Passing** |
|
||
| UT-003 | Document without the marker left byte-identical | JR-022 | **Passing** |
|
||
| UT-004 | Removal is idempotent — startup runs it on every boot forever after | JR-022 | **Passing** |
|
||
| UT-005 | **Another plugin's injection left intact** — it is their file too | JR-022 | **Passing** |
|
||
| UT-006 | An unmarked look-alike script tag is left alone — JRay did not write it | JR-022 | **Passing** |
|
||
| UT-007 | No matching rule resolves to `null` | JR-016 | **Passing** |
|
||
| UT-008 | Item rule beats Series rule | JR-016 | **Passing** |
|
||
| UT-009 | **Prioritised series inside an ignored genre — series wins** | JR-016 | **Passing** |
|
||
| UT-010 | Genre matching is case-insensitive | JR-016 | **Passing** |
|
||
| UT-011 | A Series rule valued `Guid.Empty` does not swallow every movie | JR-016 | **Passing** |
|
||
| UT-012 | `TryRegister` returns `false` when File Transformation is absent | JR-023 | **Passing** |
|
||
| UT-013 | …and **warns** naming the install URL, with no "falling back" claim | JR-023 | **Passing** |
|
||
| UT-014 | Overlay disabled ⇒ `index.html` returned unchanged | JR-023 | **Passing** |
|
||
| UT-015 | Null contents return empty rather than throwing — this callback runs on every page another plugin serves | JR-023 | **Passing** |
|
||
| UT-016 | Both bounds **inclusive** — start, interior and end all present | JR-005 | **Passing** |
|
||
| UT-017 | Just outside either bound is absent | JR-005 | **Passing** |
|
||
| UT-018 | A zero-length window is a real sighting, not a degenerate one to discard | JR-005 | **Passing** |
|
||
| UT-019 | **Overlapping windows** — present inside an enclosing window | JR-005 | **Passing** |
|
||
| UT-020 | **Unsorted windows still resolve**; sortedness is a producer guarantee, not a correctness dependency | JR-006 | **Passing** |
|
||
| UT-021 | **Adjacent windows are never merged** — reported once, from two windows | JR-004 | **Passing** |
|
||
| UT-022 | Truth file round-trips with windows byte-identical | JR-004 | **Passing** |
|
||
| UT-023 | 50 actors × 1000 windows: response bounded by actor count, lookup not quadratic | JR-006 | **Passing** |
|
||
| UT-024 | A fetched claim round-trips: server, tier, **offset**, caveat, timestamp | JR-010 | **Passing** |
|
||
| UT-025 | A local push records no server and **no tier** — there is no cut to match | JR-010 | **Passing** |
|
||
| UT-026 | Provenance is **not** written into the truth file | JR-010, JR-004 | **Passing** |
|
||
| UT-027 | `Delete` removes provenance too — no record outliving its claim | JR-010 | **Passing** |
|
||
| UT-028 | Unknown item yields null rather than a fabricated record | JR-010 | **Passing** |
|
||
| UT-029 | A v2 file round-trips; `extraction.*` and `cut.*` survive intact — `sample_fps` read from the block, not the top level | JR-002 | **Passing** |
|
||
| UT-030 | `scenes` objects retain **belief and route** — a window that loses them is indistinguishable from v1 | JR-002 | **Passing** |
|
||
| UT-031 | All three routes (`live`, `deferred`, `pooled`) survive a round trip | JR-002 | **Passing** |
|
||
| UT-032 | A window without belief reads `null`, **not `0.0`** — absent and disbelieved are different claims | JR-002 | **Passing** |
|
||
| UT-033 | `schema_version` 1, 3 and 0 are all **refused**, and the message names the version found | JR-003 | **Passing** |
|
||
| UT-034 | A **missing** `schema_version` is refused, never assumed current | JR-003 | **Passing** |
|
||
| UT-035 | A `null` truth file is refused without throwing | JR-003 | **Passing** |
|
||
| UT-036 | **A v1 file never half-parses into usable windows** — it either fails to deserialise or is stopped by the gate | JR-003 | **Passing** |
|
||
| UT-037 | **The producer's actual output parses** — the exact shape `result_sink_node.hpp` writes, omitting `cut` and two `extraction` fields, not the spec's fully populated example | JR-002 | **Passing** |
|
||
| UT-038 | The **regenerated** fixture PCM matches the recorded `s16le` and `f32le` checksums — the input is proven identical before any DSP claim is made | JR-043 | **Passing** |
|
||
| UT-039 | **Signature equals the shared golden vector, byte for byte** — the same string the C++ producer emits | JR-042, JR-043 | **Passing** |
|
||
| UT-040 | Band → FFT-bin table matches the recorded one, and tiles 300–3000 Hz contiguously with no empty band | JR-042 | **Passing** |
|
||
| UT-041 | Well-formed: `v1:` prefix, 1288 frames, bit 7 always clear, **and the fixture still exercises all 32 bands and all 4 energy classes** | JR-042 | **Passing** |
|
||
| UT-042 | Whole frames only — 4095 samples yield nothing, 5120 yield two; a partial frame is never padded into a signature | JR-042 | **Passing** |
|
||
| UT-043 | The **real FFmpeg decode** of the fixture reproduces the golden signature | JR-042 | **Passing** (needs a binary) |
|
||
| UT-044 | **The window is taken from the centre**: the fixture wrapped in 90 s of silence either side signs identically | JR-042 | **Passing** (needs a binary) |
|
||
| UT-045 | **The 120 s boundary, on one file**: runtime 119.999 emits nothing, runtime 120.000 emits the golden signature — only the runtime differs, so a null cannot be blamed on the decode | JR-044 | **Passing** (boundary needs a binary) |
|
||
| UT-046 | Two identical, perfectly valid signatures still yield **no match and no offset** when either runtime is under the window — the rule is read off the runtime, not inferred from a missing string | JR-044 | **Passing** |
|
||
| UT-047 | A `v2:` signature whose payload is byte-identical to a valid v1 one is **refused, not parsed** — by the matcher as well as the parser | JR-045 | **Passing** |
|
||
| UT-048 | A v1 signature parses to exactly the produced frames, checked against the **golden vector** rather than against the producer's own output | JR-045 | **Passing** |
|
||
| UT-049 | Missing prefix, empty payload, invalid base64 and a **set reserved bit** are each refused without throwing — the client never accepts what the server would reject | JR-045 | **Passing** |
|
||
| UT-050 | Identical signatures score 1.0 at offset 0 and reach the `audio` tier | JR-044 | **Passing** |
|
||
| UT-051 | **A shifted release recovers its offset** rather than failing to match — the case the feature exists for | JR-044 | **Passing** |
|
||
| UT-052 | Unrelated content yields **no match at all**, and runtime skew contributes its window-anchor term to the offset | JR-044 | **Passing** |
|
||
|
||
All execute and pass. UT-043 and UT-044 are the two that need an FFmpeg binary,
|
||
which the plugin gets from Jellyfin at run time and a bare CI container may not
|
||
have; they self-skip without one. That is why the cross-repo claim rests on
|
||
UT-038 and UT-039, which regenerate the fixture PCM from `make_fixture.py`'s
|
||
arithmetic and need no codec at all — a check that skips is not a check.
|
||
|
||
UT-045 follows the same rule: its *below-the-boundary* half is codec-free and
|
||
always binds, because the runtime check short-circuits before the encoder is
|
||
consulted — asserted with a deliberately invalid encoder path, so passing proves
|
||
the short-circuit rather than merely a failed decode.
|
||
|
||
The suite is also checked to **fail** on deliberate mutations, because a suite
|
||
that has only ever passed is not evidence that it tests anything. Each was
|
||
restored and re-verified afterwards:
|
||
|
||
| Mutation | Fails | Blast radius |
|
||
|---|---|---|
|
||
| Drop the newline-stripping in `RemoveInjection` | UT-001 | 1 test |
|
||
| Downgrade the missing-dependency warning to `Information` | UT-013 | 1 test |
|
||
| Make the window end bound exclusive (`t < end`) | UT-016, UT-018 | 2 tests |
|
||
| Stop `Delete` removing provenance | UT-027 | 1 test |
|
||
| Make `TruthSchema.IsSupported` accept any version | UT-033, UT-034, UT-035, UT-036 | 4 tests |
|
||
| Emit `v2:` as the signature prefix | UT-039, UT-043, UT-044 | 3 tests |
|
||
| Aggregate a band by **sum** instead of mean | UT-039, UT-043, UT-044 | 3 tests |
|
||
| Anchor the decode window at the head instead of the centre | UT-044 | 1 test |
|
||
|
||
Two further mutations were tried and **did not fail**, which is worth recording
|
||
rather than hiding: the symmetric `N-1` Hann window in place of the periodic one,
|
||
and the lower median in place of the upper as the energy reference. Both are
|
||
pinned by prose in the shared fixture, and on this synthetic vector neither moves
|
||
a peak bin or crosses an energy-class edge. They are conventions the golden
|
||
vector does not police, so a second implementation could get either wrong and
|
||
still pass — the fixture would need frames sitting nearer those boundaries to
|
||
catch it.
|
||
|
||
The window-bound mutation is the one worth keeping: a single character turns an
|
||
inclusive window into a half-open one, which would drop an actor at exactly the
|
||
moment a scene ends — and nothing else in the suite would have noticed.
|
||
|
||
`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 | **Done** (UT-029…032) — `TruthScene`, `TruthExtraction`, `TruthCut`; `anneal_sec` and top-level `sample_fps` deleted, not zeroed. `ManifestConverter` carries belief, route and both blocks through |
|
||
| JR-003 | Reject an unknown `schema_version`, never guess. **Flag day: v2 only**, no dual-accept | SR-003 | High | **Done** (UT-033…036) — `TruthSchema.IsSupported` is the single gate, applied on all four paths: sidecar read, managed store load, managed `PUT`, converted manifest. Rejections name the file and the version found |
|
||
| JR-004 | A window is a **scene-membership claim**, not a recognition event — never reinterpreted, merged, split or trimmed | **SR-002** | High | **Done** (UT-021, UT-022) |
|
||
| JR-005 | Query semantics: actor present at `t` if any window contains `t`; presentation must not assert instantaneous visibility | **SR-002** | High | **Done** (UT-016…019) |
|
||
| JR-006 | Read path holds up under **numerous** windows — no assumption of a handful of long ones | SR-002 | Medium | **Done** (UT-020, UT-023) |
|
||
| 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 | **Done** (UT-024…028) |
|
||
| 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** (UT-007…011) |
|
||
| 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 | **Done** (UT-001…006) |
|
||
| 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 | **Done** (UT-012…015; config page is T4) |
|
||
| 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 | Done |
|
||
| 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 | Done |
|
||
| JR-028 | Enforce response size caps **while streaming** — 2 MiB single, 25 MiB bundle — aborting rather than buffering | SR-004 | High | Done |
|
||
| JR-029 | HTTPS required for non-loopback servers; certificate validation must not be disabled | SR-004 | High | Done |
|
||
| 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 | Done |
|
||
| 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 | Done |
|
||
|
||
## 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 | **Done** (UT-039…044) — `AudioSignature` is the DSP, `AudioSignatureService` the decode; FFmpeg is invoked as a child process for decode, downmix and resample, and nothing was added to the project's dependencies |
|
||
| JR-043 | Golden-vector fixture **shared with the extraction repo**, proving the two implementations are bit-exact | SR-003 | High | **Done** (UT-038, UT-039) — `fixtures/audio/` holds the extraction repo's three files byte-identically; the C# signature equals the recorded vector exactly |
|
||
| JR-044 | Media shorter than 120 s: emit no signature and apply no sync offset — identical rule in both producers | SR-003 | Low | **Done** (UT-045, UT-046, UT-050…052) — the producer half was already in `AudioSignatureService`; the consumer half needed a reader, so `AudioSignatureMatcher` implements the §3 slide and declines an offset outright below the window. The boundary is asserted on one file at 119.999 s and 120.000 s |
|
||
| 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 | **Done** (UT-047…049) — `AudioSignatureMatcher.TryParseFrames` refuses any prefix but `v1:`, and refuses malformed or structurally invalid payloads, so a future producer's `v2:` drops the item to the runtime tier instead of scoring as if it were understood |
|
||
|
||
## 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.
|
||
|
||
**It carries tiers (T2 + T4) despite being undesigned, and stays in the coverage
|
||
denominator.** Tiers say *how* it will be verified, which is knowable — an
|
||
association endpoint is CI-testable, the UI is not — without asserting *what*
|
||
the assertions are, which is not. Recording it as T4-only would have been the
|
||
tempting move, because that drops it out of CI scope and lifts the CI
|
||
percentage; it would also have been the 158%-coverage error in miniature, a
|
||
number improved by reclassifying work rather than by doing it. An unbuilt
|
||
requirement should count against coverage until it is built.
|
||
|
||
---
|
||
|
||
## 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 and all 15 tests pass.
|
||
|
||
### Running the suite on a box without the web runtime
|
||
|
||
`dotnet test` from the repo root. Two properties on the test project make that
|
||
work anywhere, and both are load-bearing rather than incidental:
|
||
|
||
- **`RollForward=LatestMajor`.** The plugin targets `net9.0` to match Jellyfin's
|
||
ABI, but a machine that can *build* it need not have the 9.0 runtime. Rolling
|
||
the test host forward keeps the suite runnable without pinning developers to a
|
||
runtime the plugin does not otherwise need.
|
||
- **`DisableTransitiveFrameworkReferences=true`.** The plugin
|
||
framework-references `Microsoft.AspNetCore.App` through `Jellyfin.Controller`,
|
||
and that flows into anything referencing it — so the test host would otherwise
|
||
demand a web runtime that no version of exists in the Arch repositories for
|
||
.NET 9 (8 and 10 only).
|
||
- **Explicit `Jellyfin.Controller` / `Jellyfin.Model` references.** The plugin
|
||
sets `ExcludeAssets=runtime` on both, because at run time the *server* supplies
|
||
them and shipping copies would risk loading a second, different
|
||
`MediaBrowser.Common`. The test host is not the server, so it must bring its
|
||
own — hence the same packages without that exclusion, and only in the test
|
||
project.
|
||
|
||
Those two together are what make it work: the first drops the demand for the web
|
||
*framework*, the second supplies the Jellyfin *assemblies*. Cutting the framework
|
||
reference alone is not enough — `ILogger` and `MediaBrowser.Common` live in the
|
||
excluded assets, so anything beyond genuinely dependency-free logic fails to load
|
||
with `FileNotFoundException` at run time rather than at build.
|
||
|
||
**T2 — controllers and authorisation — is still a separate matter**, since
|
||
instantiating MVC types needs the ASP.NET Core runtime itself, not just its
|
||
reference assemblies. Those tests belong in a second project that keeps the
|
||
framework reference and leans on `RollForward` to reach the 10.0 runtime.
|
||
|
||
### 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 | All three route values; belief absent reads `null` not `0.0`; **the producer's real output**, which omits `cut` and two `extraction` fields — a test written only against the spec's populated example would have passed throughout the break |
|
||
| JR-003 | **T1** | `schema_version` 1 and 3 are both **rejected**, not coerced | Missing field entirely; `null` document; **a v1 file must not half-parse into usable windows** |
|
||
| 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 | Response bounded by actor count, not window count; lookup not quadratic | 50 × 1000 windows; **unsorted input still resolves** — sortedness is a producer guarantee, never a correctness dependency |
|
||
| 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 a round trip and is deleted with its truth | Fetched vs pushed for the same item; **provenance never inside the truth file**; unknown item yields null |
|
||
| 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, and the log says so | Detection and log content covered by UT-012…015; the config-page banner is T4, verifiable only against a live server |
|
||
| 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 `audio` with only a `runtime` match available; **`MatchTier` has no `Exact` member** — the file-hash tier is withdrawn on legal grounds, so a test naming it would not compile |
|
||
| 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; the decode itself is covered only where an FFmpeg binary exists, so it must not be the only cover for any claim |
|
||
| JR-043 | **T1** | Signature matches the shared golden vector **bit-for-bit** | The fixture PCM is regenerated from `make_fixture.py` and checked against the recorded decode checksums first, so the check binds on a host with no codec and a decode divergence is distinguishable from a DSP one |
|
||
| 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 | T2 + **T4** | *Assertions deferred* — recording an association and persisting it is T2; the review UI itself is T4 | Cannot be written until the truth-file interface for unidentified presence is settled (system open question 2) and AR-021/AR-022 land |
|
||
|
||
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).
|