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 -16
View File
@@ -48,10 +48,45 @@ Tag code with `// TRACES: JR-012 | SR-002`.
| 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. 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. Three so far, each restored and re-verified afterwards:
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 |
|---|---|---|
@@ -59,10 +94,23 @@ tests anything. Three so far, each restored and re-verified afterwards:
| 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 |
The third 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.
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
@@ -81,8 +129,8 @@ 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-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) |
@@ -176,10 +224,10 @@ 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 |
| 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)
@@ -263,8 +311,8 @@ framework reference and leans on `RollForward` to reach the 10.0 runtime.
| 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-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 |
@@ -303,8 +351,8 @@ framework reference and leans on `RollForward` to reach the 10.0 runtime.
| 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-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 |