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
This commit is contained in:
2026-07-31 16:24:40 +02:00
parent 8b430d53c0
commit e8ce779ad3
5 changed files with 923 additions and 27 deletions
+64 -11
View File
@@ -55,9 +55,14 @@ The truth file is **not** the Jmanifest. It carries installation-local fields
identity block the exchange adds. See §6.
**Current:** [`Models/TruthFile.cs`](Jellyfin.Plugin.JRay/Models/TruthFile.cs),
`schema_version: 1`. **Gap:** the schema below is not implemented, and the other
two specs describe the pending bump in more detail than this one does — the
ownership is stated but not yet exercised.
`schema_version: 2` the schema below is implemented, with
[`Models/TruthScene.cs`](Jellyfin.Plugin.JRay/Models/TruthScene.cs),
[`Models/TruthExtraction.cs`](Jellyfin.Plugin.JRay/Models/TruthExtraction.cs) and
[`Models/TruthCut.cs`](Jellyfin.Plugin.JRay/Models/TruthCut.cs).
**Gap:** the ownership claim is still not *enforced* — nothing checks that the
other two repos reference this section rather than restating the schema. JR-001's
verification tier is `static` for that reason, and no such check exists yet.
### JR-002 — `schema_version: 2`
@@ -118,9 +123,12 @@ same upload must agree on its `content_id`, and belief is a producer-side
estimate that may legitimately differ between pipeline versions for identical
timings. It replicates the way `audio_signature` does — see the server spec §9a.
**Gap:** entire requirement. The changes are all breaking and ship as **one**
bump (SR-003), together with extraction `IR-002` and the server's acceptance of
the new shape.
**Current:** implemented. `scenes` are `TruthScene` objects carrying `belief` and
`route`; `extraction.*` and `cut.*` are their own types; `anneal_sec` and the
top-level `sample_fps` are **deleted, not zeroed**. `ManifestConverter` carries
belief, route and both blocks through from a fetched manifest rather than
flattening them. **Gap:** none — this shipped with extraction `IR-002` and the
server's `jmanifest_version: 2` as the one coordinated SR-003 bump.
### JR-003 — Unknown `schema_version` is refused, never guessed
@@ -140,9 +148,20 @@ version found, rather than silently reporting no coverage — an item that looks
un-extracted when it was merely stale is the failure mode that wastes a user's
compute.
**Current:** `PUT` rejects `schema_version != 1` with `400`; sidecar reads do not
check the version at all. **Gap:** the version check must move into the shared
read path so all three sources are covered, and the target becomes `2`.
**Current:** implemented.
[`Services/TruthSchema.cs`](Jellyfin.Plugin.JRay/Services/TruthSchema.cs) holds
the single `SupportedVersion = 2` and is consulted on **all four** paths — sidecar
read, managed store load, managed `PUT`, and the converter that writes a fetched
manifest. Rejections name the file and the version found, per the paragraph above.
Managed truth is checked on load as well as on `PUT`, because data written by an
earlier plugin build is already on disk and never passed today's `PUT`.
One consequence is worth stating: a v1 file usually fails to *deserialise* before
its version can be read, since `scenes` was a float pair and is now an object. So
both the parse-failure path and the version-gate path must name the file — a
rejection the user cannot attribute to a stale sidecar is the failure mode JR-003
exists to prevent. **Gap:** none.
### JR-004 — A window is a scene-membership claim
@@ -765,8 +784,42 @@ frames whose peak bin matches — is a **consumer** concern and belongs to this
plugin. Offsets are applied client-side per JR-030; manifests are never
rewritten.
**Current:** `ComputeAudioSignatures` exists as a configuration switch. **Gap:**
entire requirement, both computation and matching.
**Current:** JR-042 and JR-043 hold. `AudioSignature` implements the construction
above — band table, periodic Hann, radix-2 FFT, band-mean peak, energy class,
packing — and `AudioSignatureService` decodes the centre window by running the
FFmpeg binary `IMediaEncoder.EncoderPath` names, so the plugin gained no
dependency. `fixtures/audio/` holds the extraction repo's three golden-vector
files byte-identically, and the computed signature equals the recorded vector
exactly (UT-038 … UT-044). The binding check regenerates the fixture PCM from
`make_fixture.py`'s arithmetic and verifies it against the recorded decode
checksums, so it runs on a CI host with no codec at all; the two tests that
exercise the real FFmpeg decode self-skip without a binary.
JR-044 and JR-045 hold as well, both halves. `AudioSignatureMatcher` is the
reader they were waiting on: it implements the ±600-frame slide above, scoring
the fraction of overlapping frames whose peak band agrees, and returns the tier
and the offset. `TryParseFrames` refuses any prefix but `v1:`, so a future
producer's `v2:` drops the item to the runtime tier rather than being scored as
if it were understood; and a runtime under 120 s yields no match and therefore no
offset, read off the runtime rather than inferred from a missing string, because
the runtime is what both producers test (UT-045 … UT-052).
The offset has **two terms**, which is easy to miss: the recovered slide, and the
difference between where the two windows are anchored. Both windows are centred
on their own file's midpoint, so unequal runtimes start them at different
absolute times. A release carrying 40 s of extra head material recovers 20 s from
each term.
One parameter is **not** from the specification: an alignment must overlap by at
least 64 frames (~6 s) before its score counts. Without a floor the extreme
offsets compare a handful of frames, where a chance agreement scores 1.0 and
beats the true alignment. It never binds on the real case — two full-length
signatures still overlap by 688 frames at the widest offset.
**Gap:** the signature is computed and can now be read, but nothing yet *calls*
either: `ComputeAudioSignatures` still gates nothing, no fetch path attaches a
signature or consults the matcher, and no contribution carries one. That wiring
belongs to the fetch and contribute paths (JR-031, JR-034), not here.
---