From b35d49c772209bce178a6fbb66124b0a6b5c5025 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Thu, 30 Jul 2026 21:20:27 +0200 Subject: [PATCH] docs: tag the implemented core with its requirement IDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds TRACES tags to code that already satisfies a Done requirement, so coverage reflects what exists rather than starting from zero: AR-001 face detection, AR-005 ArcFace alignment, AR-023 calibration fit, DP-001/DP-002 the single analysis core behind the CLI, IR-001 truth-file emission, IR-006 the Jellyfin round trip, GR-001/GR-002 gallery build and incremental merge, VR-001 the embedding dump, VR-002 replay through the real nodes, VR-003 per-second scoring. Only Done requirements are tagged. A tag on Planned work would inflate coverage with fiction that looks plausible — the same failure family as a gate that cannot fail, and harder to spot. GR-005 (gallery never leaves the instance) stays untagged deliberately: it is a prohibition satisfied by the absence of an egress path, so there is no unit that decides it. Same shape as PR-005 in the system spec, which has no software row for the same reason. A goal held only by prohibitions cannot be verified by pointing at code. Coverage 5/63 to 14/63. The three VR tags are reported as tagged-but-unexecuted and excluded from the numerator, since their tier cannot run on the CI host — tagging deliberately cannot raise the number on its own. Suite still 64 cases, 3199 assertions. Co-Authored-By: Claude Opus 5 TRACES: AR-001, AR-005, AR-023, DP-001, DP-002, IR-001, IR-006, GR-001, GR-002, VR-001, VR-002, VR-003 --- docs/traceability.md | 198 +++++++++++++++++++++------- scripts/make_jellyfin_gallery.py | 2 + scripts/optimizer/replay.py | 2 + scripts/optimizer/second_score.py | 2 + scripts/run_from_jellyfin.py | 2 + src/face_utils.hpp | 1 + src/gallery/gallery_calibration.hpp | 1 + src/main.cpp | 4 + src/nodes/embedding_dump_node.hpp | 1 + src/nodes/face_detector_node.hpp | 1 + src/nodes/result_sink_node.hpp | 1 + 11 files changed, 169 insertions(+), 46 deletions(-) diff --git a/docs/traceability.md b/docs/traceability.md index b2bdada..b8eb05f 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -3,7 +3,7 @@ -**Generated:** 2026-07-30T19:15:45+00:00 +**Generated:** 2026-07-30T19:19:47+00:00 Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, T3, static`). @@ -12,27 +12,28 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | Metric | Value | |---|---| | Source files scanned | 92 | -| TRACES tags found | 64 | +| TRACES tags found | 74 | | EXCEPTION tags found | 0 | | Requirements defined | 63 | -| Requirements covered | 5 | -| **Coverage** | **7.9%** (5/63) | -| Coverage of CI-executable scope | 9.6% (5/52) | -| Tagged but unexecuted in CI | 0 | +| Requirements covered | 14 | +| **Coverage** | **22.2%** (14/63) | +| Coverage of CI-executable scope | 26.9% (14/52) | +| Tagged but unexecuted in CI | 3 | | Orphan tags | 0 | ### By type | Type | Covered | Tagged but unexecuted | Defined | |---|---|---|---| -| AR | 0 | 0 | 27 | -| DP | 0 | 0 | 8 | -| IR | 4 | 0 | 8 | -| GR | 1 | 0 | 9 | -| VR | 0 | 0 | 11 | +| AR | 3 | 0 | 27 | +| DP | 2 | 0 | 8 | +| IR | 6 | 0 | 8 | +| GR | 3 | 0 | 9 | +| VR | 0 | 3 | 11 | - **UT** tags present (separate taxonomy, not counted in coverage): UT-101, UT-102, UT-103, UT-104 -- **SR** tags present (separate taxonomy, not counted in coverage): SR-001, SR-003 +- **PR** tags present (separate taxonomy, not counted in coverage): PR-002, PR-004 +- **SR** tags present (separate taxonomy, not counted in coverage): SR-001, SR-002, SR-003, SR-005 ## Not executable in CI @@ -41,9 +42,9 @@ These requirements have no verification tier this repo's CI host can run, so a t | ID | Tiers | Tagged in source | Requirement | |---|---|---|---| | AR-027 | T4 | no | Throughput acceptable for **arbitrary** gallery size | -| VR-001 | out-of-ci | no | HDF5 post-inference dump at the embedded-frame boundary | -| VR-002 | out-of-ci | no | Replay drives the **real** KPN nodes, not a reimplementation | -| VR-003 | out-of-ci | no | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… | +| VR-001 | out-of-ci | yes | HDF5 post-inference dump at the embedded-frame boundary | +| VR-002 | out-of-ci | yes | Replay drives the **real** KPN nodes, not a reimplementation | +| VR-003 | out-of-ci | yes | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… | | VR-004 | out-of-ci | no | Reproducible validation corpus with ground truth | | VR-005 | out-of-ci | no | Minimum face size study — TPI/FPI vs probe size, gallery held at nati… | | VR-006 | out-of-ci | no | Re-tune `scene_threshold` once native-rate decode lands | @@ -52,6 +53,8 @@ These requirements have no verification tier this repo's CI host can run, so a t | VR-010 | out-of-ci | no | Dump provenance attributes — embedder model, detector settings, `dens… | | VR-011 | out-of-ci | no | Rewrite the replay harness for the post-AR-012 output contract | +**Tagged but unexecuted:** VR-001, VR-002, VR-003 — a test exists and is tagged, but this CI host cannot run it. Report those runs separately. + ## Orphan tags A tag naming an ID `requirements.md` does not define. This is what renumbering produces, and what a typo produces. @@ -74,11 +77,11 @@ _None._ | ID | Status | Tier | Traces to | Trace state | Tagged in | Requirement | |---|---|---|---|---|---|---| -| AR-001 | Done | T3 | SR-002 | untagged | - | Detect faces in sampled frames; emit bbox, confidence, 5-point landma… | +| AR-001 | Done | T3 | SR-002 | covered | `src/nodes/face_detector_node.hpp` | Detect faces in sampled frames; emit bbox, confidence, 5-point landma… | | AR-002 | Planned | T2 | SR-002 | untagged | - | Minimum face size 66×66 px, expressed in **original** resolution (dec… | | AR-003 | Planned | T1, T2, T4 | SR-002 | untagged | - | No fixed per-frame face cap — crowd scenes must not lose background c… | | AR-004 | Planned | T1, T4 | SR-002 | untagged | - | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… | -| AR-005 | Done | T1, T3 | SR-002 | untagged | - | Align to 112×112 via ArcFace 5-point similarity transform | +| AR-005 | Done | T1, T3 | SR-002 | covered | `src/face_utils.hpp` | Align to 112×112 via ArcFace 5-point similarity transform | | AR-006 | Done | T3 | SR-002 | untagged | - | 512-d L2-normalised embeddings, batched | | AR-007 | In Progress | T2 | SR-002 | untagged | - | Associate detections by IoU + embedding, with **frame-dependent** wei… | | AR-008 | Planned | T2 | SR-002 | untagged | - | One track pool keyed on `last_seen`; no separate revival path | @@ -96,29 +99,29 @@ _None._ | AR-020 | Planned | T2 | SR-005 | untagged | - | Deferred re-identification of unknown tracks against the final expand… | | AR-021 | Planned | T2 | SR-005 | untagged | - | Cluster unknown tracks into one entity per person, under temporal can… | | AR-022 | Planned | T1, T2 | §4 | untagged | - | Capture still-unidentified tracks: embeddings, metadata, **context cr… | -| AR-023 | Done | T1 | SR-002 | untagged | - | Fit sigmoid calibration from intra/inter similarity distributions | +| AR-023 | Done | T1 | SR-002 | covered | `src/gallery/gallery_calibration.hpp` | Fit sigmoid calibration from intra/inter similarity distributions | | AR-024 | Planned | T1, static | SR-002 | untagged | - | **Always the calibrated probability, never a raw cosine** — exception… | | AR-025 | Planned | T1 | SR-002 | untagged | - | Per-track Bayesian accumulation in log-odds, with correlated-observat… | | AR-026 | In Progress | T1, T4 | SR-001 | untagged | - | All similarity computed as GEMM, including annex and deferred pass | | AR-027 | Planned | T4 | SR-001 | untagged | - | Throughput acceptable for **arbitrary** gallery size | -| DP-001 | Done | T1, manual | PR-004 | untagged | - | One analysis core; modes are front-ends and must not fork pipeline lo… | -| DP-002 | Done | T1, manual | PR-004 | untagged | - | Batch CLI over one title | +| DP-001 | Done | T1, manual | PR-004 | covered | `src/main.cpp` | One analysis core; modes are front-ends and must not fork pipeline lo… | +| DP-002 | Done | T1, manual | PR-004 | covered | `src/main.cpp` | Batch CLI over one title | | DP-003 | Planned | T1, manual | PR-004 | untagged | - | On-demand resident service with bounded, observable queue | | DP-004 | Planned | T1, manual | PR-004 | untagged | - | Opportunistic/idle mode: external trigger, hard stop, implicit re-que… | | DP-005 | Planned | T1, manual | PR-004 | untagged | - | Native installer, no Docker; Fedora + Arch | | DP-006 | Planned | T1, manual | PR-003 | untagged | - | Background incremental gallery refresh on a timer | | DP-007 | Planned | T1, manual | PR-004 | untagged | - | CI builder image, CPU-only, pinned by tag in the Gitea container regi… | | DP-008 | Planned | T1, manual | PR-004 | untagged | - | Builder images + release jobs per backend (cpu / cuda / rocm); ship b… | -| IR-001 | Done | T1 | SR-003 | untagged | - | Emit the JRay truth format as sibling `.jray.json` | +| IR-001 | Done | T1 | SR-003 | covered | `src/nodes/result_sink_node.hpp` | Emit the JRay truth format as sibling `.jray.json` | | IR-002 | Planned | T1 | SR-003 | untagged | - | Windows carry belief + route; `extraction.*` carries `extinction_sec`… | | IR-003 | Planned | T1 | SR-003 | untagged | - | Output written **after** the deferred pass, not at EOF | | IR-004 | **Done** — `src/aud… | T1 | SR-003 | covered | `src/audio_signature.cpp`, `src/audio_signature.hpp`, `tests/test_audio_signature.cpp` | Compute the audio signature exactly per server spec §3 | | IR-005 | **Done** — `tests/f… | T1 | SR-003 | covered | `src/audio_signature.cpp`, `src/audio_signature.hpp`, `tests/test_audio_signature.cpp` | Golden-vector fixture shared with the plugin repo to prove bit-exactn… | -| IR-006 | Done | T1, manual | SR-001 | untagged | - | Jellyfin round-trip: pull pending queue, push complete results only | +| IR-006 | Done | T1, manual | SR-001 | covered | `scripts/run_from_jellyfin.py` | Jellyfin round-trip: pull pending queue, push complete results only | | IR-007 | **Done** | T1 | SR-003 | covered | `src/audio_signature.cpp`, `src/audio_signature.hpp`, `tests/test_audio_signature.cpp` | Media < 120 s: emit no signature, apply no sync offset — identical ru… | | IR-008 | **Done** | T1 | SR-003 | covered | `src/audio_signature.cpp`, `src/audio_signature.hpp`, `tests/test_audio_signature.cpp` | Emit and honour the signature's own `v1:` version prefix | -| GR-001 | Done | T1, T3 | SR-001, SR-005 | untagged | - | Build gallery from Jellyfin library cast, TMDB profile fallback | -| GR-002 | Done | T1, T3 | PR-003 | untagged | - | Incremental `--merge` refresh without re-embedding known actors | +| GR-001 | Done | T1, T3 | SR-001, SR-005 | covered | `scripts/make_jellyfin_gallery.py` | Build gallery from Jellyfin library cast, TMDB profile fallback | +| GR-002 | Done | T1, T3 | PR-003 | covered | `scripts/make_jellyfin_gallery.py` | Incremental `--merge` refresh without re-embedding known actors | | GR-003 | Planned | T1, T3 | SR-001 | untagged | - | Report coverage: zero-image actors, under-referenced actors, dedup, c… | | GR-004 | **Done** — basename… | T1, T3 | SR-001 | covered | `scripts/filter_gallery.py`, `scripts/make_gallery.py`, `scripts/make_jellyfin_gallery.py`, `scripts/movienet_eval.py`, `scripts/optimizer/fetch_missing_actors.py`, `scripts/optimizer/optimize.py`, `scripts/optimizer/reembed_gallery.py`, `scripts/optimizer/replay.py`, `scripts/sae_embed_loader.py`, `scripts/sae_gallery.py`, `scripts/sae_stamp.py`, `scripts/stamp_gallery.py`, `src/config.hpp`, `src/gallery/embedder_stamp.cpp`, `src/gallery/embedder_stamp.hpp`, `src/gallery/gallery_builder.cpp`, `src/gallery/gallery_store.cpp`, `src/kpn_bindings.cpp`, `src/main.cpp`, `src/nodes/embedding_dump_node.hpp`, `src/scene_preview.cpp`, `src/types.hpp`, `tests/test_gallery_store.cpp` | Stamp embedder identity into the gallery; **hard startup error** on m… | | GR-005 | Done | T1, T3 | **SR-005** | untagged | - | Gallery data never leaves the instance | @@ -126,9 +129,9 @@ _None._ | GR-007 | Planned | T1 | SR-005 | untagged | - | Persist harvested embeddings **flagged and reviewable**, never silent… | | GR-008 | Planned | T1 | SR-005 | untagged | - | Flag distributional outliers among an actor's references (poisoning g… | | GR-009 | TBD | T1 | §4 | untagged | - | Human-confirmed associations persist and improve future extractions | -| VR-001 | Done | out-of-ci | PR-002 | untagged | - | HDF5 post-inference dump at the embedded-frame boundary | -| VR-002 | Done | out-of-ci | PR-002 | untagged | - | Replay drives the **real** KPN nodes, not a reimplementation | -| VR-003 | Done | out-of-ci | PR-002 | untagged | - | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… | +| VR-001 | Done | out-of-ci | PR-002 | tagged, unexecuted | `src/nodes/embedding_dump_node.hpp` | HDF5 post-inference dump at the embedded-frame boundary | +| VR-002 | Done | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/replay.py` | Replay drives the **real** KPN nodes, not a reimplementation | +| VR-003 | Done | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/second_score.py` | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… | | VR-004 | Done | out-of-ci | PR-002 | untagged | - | Reproducible validation corpus with ground truth | | VR-005 | Planned | out-of-ci | PR-002 | untagged | - | Minimum face size study — TPI/FPI vs probe size, gallery held at nati… | | VR-006 | Planned | out-of-ci | PR-002 | untagged | - | Re-tune `scene_threshold` once native-rate decode lands | @@ -140,6 +143,48 @@ _None._ ## Detailed mapping +### AR-001 + +**Locations:** 1 + +- [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown` + +### AR-005 + +**Locations:** 1 + +- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,` + +### AR-023 + +**Locations:** 1 + +- [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown` + +### DP-001 + +**Locations:** 1 + +- [`src/main.cpp:3`](../src/main.cpp#L3) — `Unknown` + +### DP-002 + +**Locations:** 1 + +- [`src/main.cpp:3`](../src/main.cpp#L3) — `Unknown` + +### GR-001 + +**Locations:** 1 + +- [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` + +### GR-002 + +**Locations:** 1 + +- [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` + ### GR-004 **Locations:** 44 @@ -153,9 +198,9 @@ _None._ - [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown` - [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown` - [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown` -- [`src/main.cpp:172`](../src/main.cpp#L172) — `Unknown` -- [`src/nodes/embedding_dump_node.hpp:29`](../src/nodes/embedding_dump_node.hpp#L29) — `static constexpr std::string_view label() { return "embedding_dump"; }` -- [`src/nodes/embedding_dump_node.hpp:101`](../src/nodes/embedding_dump_node.hpp#L101) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);` +- [`src/main.cpp:176`](../src/main.cpp#L176) — `Unknown` +- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }` +- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);` - [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)` - [`src/types.hpp:141`](../src/types.hpp#L141) — `struct Actor` - [`tests/test_gallery_store.cpp:182`](../tests/test_gallery_store.cpp#L182) — `TempFile tf("gallery_stamped.h5");` @@ -172,8 +217,8 @@ _None._ - [`tests/test_gallery_store.cpp:384`](../tests/test_gallery_store.cpp#L384) — `TempFile tf("fake_model.onnx");` - [`scripts/filter_gallery.py:80`](../scripts/filter_gallery.py#L80) — `if actor_jellyfin_id(a) in cast_ids]` - [`scripts/make_gallery.py:181`](../scripts/make_gallery.py#L181) — `Unknown` -- [`scripts/make_jellyfin_gallery.py:446`](../scripts/make_jellyfin_gallery.py#L446) — `Unknown` -- [`scripts/make_jellyfin_gallery.py:454`](../scripts/make_jellyfin_gallery.py#L454) — `Unknown` +- [`scripts/make_jellyfin_gallery.py:448`](../scripts/make_jellyfin_gallery.py#L448) — `Unknown` +- [`scripts/make_jellyfin_gallery.py:456`](../scripts/make_jellyfin_gallery.py#L456) — `Unknown` - [`scripts/movienet_eval.py:65`](../scripts/movienet_eval.py#L65) — `with open(args.gt) as f:` - [`scripts/optimizer/fetch_missing_actors.py:62`](../scripts/optimizer/fetch_missing_actors.py#L62) — `def fetch(missing_path, out_path, token, build_dir, models_dir, arcface,` - [`scripts/optimizer/fetch_missing_actors.py:109`](../scripts/optimizer/fetch_missing_actors.py#L109) — `def merge(base_path, add_path, out_path):` @@ -181,14 +226,20 @@ _None._ - [`scripts/optimizer/optimize.py:186`](../scripts/optimizer/optimize.py#L186) — `Unknown` - [`scripts/optimizer/optimize.py:202`](../scripts/optimizer/optimize.py#L202) — `if not Path(f["dump"]).exists():` - [`scripts/optimizer/reembed_gallery.py:62`](../scripts/optimizer/reembed_gallery.py#L62) — `for i, a in enumerate(ref["actors"], 1):` -- [`scripts/optimizer/replay.py:111`](../scripts/optimizer/replay.py#L111) — `Unknown` -- [`scripts/optimizer/replay.py:250`](../scripts/optimizer/replay.py#L250) — `Unknown` +- [`scripts/optimizer/replay.py:113`](../scripts/optimizer/replay.py#L113) — `Unknown` +- [`scripts/optimizer/replay.py:252`](../scripts/optimizer/replay.py#L252) — `Unknown` - [`scripts/sae_embed_loader.py:22`](../scripts/sae_embed_loader.py#L22) — `return arcface if arcface else str(Path(models_dir) / DEFAULT_ARCFACE)` - [`scripts/sae_gallery.py:171`](../scripts/sae_gallery.py#L171) — `if not _stamp_empty(embedder):` - [`scripts/sae_gallery.py:199`](../scripts/sae_gallery.py#L199) — `for a in range(len(offset)):` - [`scripts/sae_stamp.py:3`](../scripts/sae_stamp.py#L3) — `Unknown` - [`scripts/stamp_gallery.py:4`](../scripts/stamp_gallery.py#L4) — `Unknown` +### IR-001 + +**Locations:** 1 + +- [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` + ### IR-004 **Locations:** 16 @@ -220,6 +271,12 @@ _None._ - [`tests/test_audio_signature.cpp:129`](../tests/test_audio_signature.cpp#L129) — `Unknown` - [`tests/test_audio_signature.cpp:138`](../tests/test_audio_signature.cpp#L138) — `Unknown` +### IR-006 + +**Locations:** 1 + +- [`scripts/run_from_jellyfin.py:4`](../scripts/run_from_jellyfin.py#L4) — `Unknown` + ### IR-007 **Locations:** 8 @@ -245,9 +302,23 @@ _None._ - [`tests/test_audio_signature.cpp:129`](../tests/test_audio_signature.cpp#L129) — `Unknown` - [`tests/test_audio_signature.cpp:167`](../tests/test_audio_signature.cpp#L167) — `Unknown` +### PR-002 + +**Locations:** 3 + +- [`src/nodes/embedding_dump_node.hpp:2`](../src/nodes/embedding_dump_node.hpp#L2) — `Unknown` +- [`scripts/optimizer/replay.py:5`](../scripts/optimizer/replay.py#L5) — `Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an` +- [`scripts/optimizer/second_score.py:5`](../scripts/optimizer/second_score.py#L5) — `Unknown` + +### PR-004 + +**Locations:** 1 + +- [`src/main.cpp:3`](../src/main.cpp#L3) — `Unknown` + ### SR-001 -**Locations:** 44 +**Locations:** 46 - [`src/config.hpp:43`](../src/config.hpp#L43) — `Unknown` - [`src/gallery/embedder_stamp.cpp:1`](../src/gallery/embedder_stamp.cpp#L1) — `Unknown` @@ -258,9 +329,9 @@ _None._ - [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown` - [`src/kpn_bindings.cpp:167`](../src/kpn_bindings.cpp#L167) — `Unknown` - [`src/kpn_bindings.cpp:217`](../src/kpn_bindings.cpp#L217) — `Unknown` -- [`src/main.cpp:172`](../src/main.cpp#L172) — `Unknown` -- [`src/nodes/embedding_dump_node.hpp:29`](../src/nodes/embedding_dump_node.hpp#L29) — `static constexpr std::string_view label() { return "embedding_dump"; }` -- [`src/nodes/embedding_dump_node.hpp:101`](../src/nodes/embedding_dump_node.hpp#L101) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);` +- [`src/main.cpp:176`](../src/main.cpp#L176) — `Unknown` +- [`src/nodes/embedding_dump_node.hpp:30`](../src/nodes/embedding_dump_node.hpp#L30) — `static constexpr std::string_view label() { return "embedding_dump"; }` +- [`src/nodes/embedding_dump_node.hpp:102`](../src/nodes/embedding_dump_node.hpp#L102) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);` - [`src/scene_preview.cpp:133`](../src/scene_preview.cpp#L133) — `int main(int argc, char** argv)` - [`src/types.hpp:141`](../src/types.hpp#L141) — `struct Actor` - [`tests/test_gallery_store.cpp:182`](../tests/test_gallery_store.cpp#L182) — `TempFile tf("gallery_stamped.h5");` @@ -277,8 +348,9 @@ _None._ - [`tests/test_gallery_store.cpp:384`](../tests/test_gallery_store.cpp#L384) — `TempFile tf("fake_model.onnx");` - [`scripts/filter_gallery.py:80`](../scripts/filter_gallery.py#L80) — `if actor_jellyfin_id(a) in cast_ids]` - [`scripts/make_gallery.py:181`](../scripts/make_gallery.py#L181) — `Unknown` -- [`scripts/make_jellyfin_gallery.py:446`](../scripts/make_jellyfin_gallery.py#L446) — `Unknown` -- [`scripts/make_jellyfin_gallery.py:454`](../scripts/make_jellyfin_gallery.py#L454) — `Unknown` +- [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` +- [`scripts/make_jellyfin_gallery.py:448`](../scripts/make_jellyfin_gallery.py#L448) — `Unknown` +- [`scripts/make_jellyfin_gallery.py:456`](../scripts/make_jellyfin_gallery.py#L456) — `Unknown` - [`scripts/movienet_eval.py:65`](../scripts/movienet_eval.py#L65) — `with open(args.gt) as f:` - [`scripts/optimizer/fetch_missing_actors.py:62`](../scripts/optimizer/fetch_missing_actors.py#L62) — `def fetch(missing_path, out_path, token, build_dir, models_dir, arcface,` - [`scripts/optimizer/fetch_missing_actors.py:109`](../scripts/optimizer/fetch_missing_actors.py#L109) — `def merge(base_path, add_path, out_path):` @@ -286,20 +358,36 @@ _None._ - [`scripts/optimizer/optimize.py:186`](../scripts/optimizer/optimize.py#L186) — `Unknown` - [`scripts/optimizer/optimize.py:202`](../scripts/optimizer/optimize.py#L202) — `if not Path(f["dump"]).exists():` - [`scripts/optimizer/reembed_gallery.py:62`](../scripts/optimizer/reembed_gallery.py#L62) — `for i, a in enumerate(ref["actors"], 1):` -- [`scripts/optimizer/replay.py:111`](../scripts/optimizer/replay.py#L111) — `Unknown` -- [`scripts/optimizer/replay.py:250`](../scripts/optimizer/replay.py#L250) — `Unknown` +- [`scripts/optimizer/replay.py:113`](../scripts/optimizer/replay.py#L113) — `Unknown` +- [`scripts/optimizer/replay.py:252`](../scripts/optimizer/replay.py#L252) — `Unknown` +- [`scripts/run_from_jellyfin.py:4`](../scripts/run_from_jellyfin.py#L4) — `Unknown` - [`scripts/sae_embed_loader.py:22`](../scripts/sae_embed_loader.py#L22) — `return arcface if arcface else str(Path(models_dir) / DEFAULT_ARCFACE)` - [`scripts/sae_gallery.py:171`](../scripts/sae_gallery.py#L171) — `if not _stamp_empty(embedder):` - [`scripts/sae_gallery.py:199`](../scripts/sae_gallery.py#L199) — `for a in range(len(offset)):` - [`scripts/sae_stamp.py:3`](../scripts/sae_stamp.py#L3) — `Unknown` - [`scripts/stamp_gallery.py:4`](../scripts/stamp_gallery.py#L4) — `Unknown` +### SR-002 + +**Locations:** 3 + +- [`src/face_utils.hpp:2`](../src/face_utils.hpp#L2) — `inline cv::Mat align_face(const cv::Mat& img,` +- [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown` +- [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown` + ### SR-003 -**Locations:** 2 +**Locations:** 3 - [`src/audio_signature.cpp:3`](../src/audio_signature.cpp#L3) — `Unknown` - [`src/audio_signature.hpp:4`](../src/audio_signature.hpp#L4) — `Unknown` +- [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` + +### SR-005 + +**Locations:** 1 + +- [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` ### UT-101 @@ -338,20 +426,38 @@ _None._ - [`tests/test_audio_signature.cpp:328`](../tests/test_audio_signature.cpp#L328) — `std::vector a(kWindowSamples / 50);` - [`tests/test_audio_signature.cpp:344`](../tests/test_audio_signature.cpp#L344) — `return base64_encode(reinterpret_cast(s.data()), s.size());` +### VR-001 + +**Locations:** 1 + +- [`src/nodes/embedding_dump_node.hpp:2`](../src/nodes/embedding_dump_node.hpp#L2) — `Unknown` + +### VR-002 + +**Locations:** 1 + +- [`scripts/optimizer/replay.py:5`](../scripts/optimizer/replay.py#L5) — `Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an` + +### VR-003 + +**Locations:** 1 + +- [`scripts/optimizer/second_score.py:5`](../scripts/optimizer/second_score.py#L5) — `Unknown` + ## Tag diagnostics **Malformed tags:** - `scripts/filter_gallery.py:80` — {'ignored': ['— a filtered gallery holds the SAME vectors as its']} - `scripts/make_gallery.py:181` — {'ignored': ['— stamp with the model actually loaded', 'resolved']} -- `scripts/make_jellyfin_gallery.py:454` — {'ignored': ["— --merge keeps the existing actors' vectors and"]} +- `scripts/make_jellyfin_gallery.py:456` — {'ignored': ["— --merge keeps the existing actors' vectors and"]} - `scripts/movienet_eval.py:65` — {'ignored': ['— match() below is a bare dot product against the']} - `scripts/optimizer/fetch_missing_actors.py:109` — {'ignored': ['— the legacy JSON gallery carries the same stamp as']} - `scripts/optimizer/fetch_missing_actors.py:123` — {'ignored': ['— merging two galleries from different models makes']} - `scripts/optimizer/optimize.py:202` — {'ignored': ['— every (dump', 'gallery) pair is checked ONCE here']} - `scripts/optimizer/reembed_gallery.py:62` — {'ignored': ['— this script exists to produce a gallery in a']} -- `scripts/optimizer/replay.py:111` — {'ignored': ['— checked here', 'before any network is built', 'so a']} -- `scripts/optimizer/replay.py:250` — {'ignored': ['— promote an unprovable gallery/dump binding from a']} +- `scripts/optimizer/replay.py:113` — {'ignored': ['— checked here', 'before any network is built', 'so a']} +- `scripts/optimizer/replay.py:252` — {'ignored': ['— promote an unprovable gallery/dump binding from a']} - `scripts/sae_embed_loader.py:22` — {'ignored': ['— single source of truth for "which model is this"']} - `scripts/sae_gallery.py:171` — {'ignored': ['— omitted entirely when unknown', 'so "unstamped"']} - `scripts/sae_gallery.py:199` — {'ignored': ['— carried through so a derived gallery (filter']} diff --git a/scripts/make_jellyfin_gallery.py b/scripts/make_jellyfin_gallery.py index 556757c..915982c 100644 --- a/scripts/make_jellyfin_gallery.py +++ b/scripts/make_jellyfin_gallery.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """make_jellyfin_gallery.py — build a gallery.h5 spanning an entire Jellyfin library. +TRACES: GR-001, GR-002 | SR-001, SR-005 + Queries the Jellyfin API for every Movie/Series, collects the unique cast across the whole library, downloads each actor's headshot directly from Jellyfin (no TMDB key needed), embeds them with the sae_embed module (SCRFD + diff --git a/scripts/optimizer/replay.py b/scripts/optimizer/replay.py index 320c632..8f93e17 100644 --- a/scripts/optimizer/replay.py +++ b/scripts/optimizer/replay.py @@ -2,6 +2,8 @@ """ replay.py — replay a dumped embedding HDF5 through the real KPN downstream nodes. +TRACES: VR-002 | PR-002 + Reads an embedding dump (scripts/optimizer/SCHEMA.md), feeds each frame as an EmbeddedSceneFrame into a Python-assembled KPN network wiring the *real* C++ face_tracker → identity_matcher → scene_tracker, and returns the same presence-window diff --git a/scripts/optimizer/second_score.py b/scripts/optimizer/second_score.py index 57fd391..c4ed6aa 100644 --- a/scripts/optimizer/second_score.py +++ b/scripts/optimizer/second_score.py @@ -2,6 +2,8 @@ """ second_score.py — uniform per-second agreement with X-Ray. +TRACES: VR-003 | PR-002 + Unlike scene_score.py (which unions our detections over a whole X-Ray scene), this samples EVERY SECOND of the film and asks: at second t, do we name the same actors X-Ray says are on screen? diff --git a/scripts/run_from_jellyfin.py b/scripts/run_from_jellyfin.py index 86321eb..d652f6e 100644 --- a/scripts/run_from_jellyfin.py +++ b/scripts/run_from_jellyfin.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """run_from_jellyfin.py — resolve a Jellyfin title to its media file and run scene_analyze. +TRACES: IR-006 | SR-001 + Looks up a Movie/Episode in Jellyfin, reads its on-disk Path (Jellyfin and this tool must share the same media mount), filters the gallery down to that title's credited cast (via filter_gallery's logic, fewer look-alike diff --git a/src/face_utils.hpp b/src/face_utils.hpp index 969741f..be93f86 100644 --- a/src/face_utils.hpp +++ b/src/face_utils.hpp @@ -1,4 +1,5 @@ #pragma once +/// TRACES: AR-005 | SR-002 #include "types.hpp" #include diff --git a/src/gallery/gallery_calibration.hpp b/src/gallery/gallery_calibration.hpp index acf761d..de20637 100644 --- a/src/gallery/gallery_calibration.hpp +++ b/src/gallery/gallery_calibration.hpp @@ -1,4 +1,5 @@ #pragma once +/// TRACES: AR-023 | SR-002 #include "types.hpp" #include diff --git a/src/main.cpp b/src/main.cpp index 8dacbd6..4388da4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,9 @@ // scene_analyze — identify actors in a movie using a KPN pipeline // +// TRACES: DP-001, DP-002 | PR-004 +// One analysis core; the CLI is a front-end over it and must not fork pipeline +// logic. Other deployment modes (DP-003, DP-004) wrap this same core. +// // KPN topology (release build): // // [frame_source] ──Frame──► [face_detector] ──SceneFrame──► [face_aligner] diff --git a/src/nodes/embedding_dump_node.hpp b/src/nodes/embedding_dump_node.hpp index 13cf361..91ae7a5 100644 --- a/src/nodes/embedding_dump_node.hpp +++ b/src/nodes/embedding_dump_node.hpp @@ -1,4 +1,5 @@ #pragma once +/// TRACES: VR-001 | PR-002 #include "types.hpp" #include "config.hpp" #include "gallery/embedder_stamp.hpp" diff --git a/src/nodes/face_detector_node.hpp b/src/nodes/face_detector_node.hpp index 0cb2ddf..15cfc7f 100644 --- a/src/nodes/face_detector_node.hpp +++ b/src/nodes/face_detector_node.hpp @@ -1,4 +1,5 @@ #pragma once +/// TRACES: AR-001 | SR-002 #include "config.hpp" #include "inference/face_detector.hpp" diff --git a/src/nodes/result_sink_node.hpp b/src/nodes/result_sink_node.hpp index 278ef86..d604a8f 100644 --- a/src/nodes/result_sink_node.hpp +++ b/src/nodes/result_sink_node.hpp @@ -1,4 +1,5 @@ #pragma once +/// TRACES: IR-001 | SR-003 #include "types.hpp" #include "config.hpp"