Files
jRay/docs/requirements.md
T
dtourolle e8ce779ad3 docs: land the registers for schema v2 and the audio signature
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
2026-07-31 16:24:40 +02:00

33 KiB
Raw Blame History

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 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 3003000 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 §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 §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 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 §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 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, 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).