diff --git a/docs/requirements.md b/docs/requirements.md index 3ba8cd7..d1b68f1 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -213,6 +213,46 @@ structurally unverifiable on the CI host. It needs a GPU host and a synthetic large gallery, so it is the requirement most likely to silently regress. Its benchmark (VR-008) should run on a schedule rather than on demand. +### CI never calls a model + +**Not "should not" — cannot.** The N100 has no GPU, and even the ONNX Runtime CPU +provider is impractical: a measured run of the embedder on this hardware sits at +~930 ms per frame, so a 77 s clip at 5 fps would take roughly six minutes of +inference alone. Every model invocation therefore happens **locally, ahead of +time**, and CI consumes the result as data. + +This is what makes the T1/T2 split load-bearing rather than a preference: T1 and +T2 are the only tiers that can exist in CI at all. + +### Fixture corpus — `bali/` + +Five clips of **Road to Bali (1952)**, ~77 s each, 480×360, 30 fps, 42 MB total. + +Public domain, and that is the reason to use it rather than a convenience: +**derived fixtures — dumps, crops, golden outputs — can be committed without the +rights question that rules out sharing gallery data (SR-005).** A fixture cut +from a copyrighted title could not live in the repository at all. + +Two properties to design around rather than discover: + +- **480×360 means small faces.** At this resolution a face is often 40–80 px, so + the AR-002 minimum of 66 px (original resolution) rejects much of what is + there. Fixture generation must set `--min-face-px` explicitly and record it, + or the dumps will be sparse for reasons unrelated to what is being tested. +- **77 s is short.** At 1 fps that is 77 frames — too thin to exercise an + extinction window measured in tens of seconds. Generate at 5 fps (≈385 frames, + ~1 MB) and record the rate in provenance, since the behaviour under test + changes with it. + +> **AR-004 blocks reproducible fixture generation.** A trial run of one clip +> produced 49 frames of an expected ~385, ending at 51 s of 77 s, with the +> diagnostics reporting 285 frames dropped at `camera_pos` and 51 at +> `face_aligner`. Channels overflow and **drop** rather than blocking, and what +> gets dropped depends on timing — so the same command run twice can produce +> different dumps. Golden fixtures cannot be built on that. AR-004 is therefore +> a prerequisite for VR-001 fixtures, not merely a throughput concern for crowd +> scenes. + ### Fixtures — precomputed inference, pulled by CI The N100 cannot run inference at any useful rate, so **inference output is diff --git a/docs/traceability.md b/docs/traceability.md index 542f37a..1259108 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -3,7 +3,7 @@ -**Generated:** 2026-07-31T08:01:31+00:00 +**Generated:** 2026-07-31T08:10:51+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,12 +12,12 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | Metric | Value | |---|---| | Source files scanned | 95 | -| TRACES tags found | 82 | +| TRACES tags found | 90 | | EXCEPTION tags found | 0 | | Requirements defined | 63 | -| Requirements covered | 24 | -| **Coverage** | **38.1%** (24/63) | -| Coverage of CI-executable scope | 46.2% (24/52) | +| Requirements covered | 26 | +| **Coverage** | **41.3%** (26/63) | +| Coverage of CI-executable scope | 50.0% (26/52) | | Tagged but unexecuted in CI | 3 | | Orphan tags | 0 | @@ -27,7 +27,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev |---|---|---|---| | AR | 13 | 0 | 27 | | DP | 2 | 0 | 8 | -| IR | 6 | 0 | 8 | +| IR | 8 | 0 | 8 | | GR | 3 | 0 | 9 | | VR | 0 | 3 | 11 | @@ -88,12 +88,12 @@ _None._ | AR-009 | Done | T2 | SR-002 | untagged | - | Camera-cut detection (histogram) as an association hint | | AR-010 | **Not started** — `… | T2 | SR-002 | untagged | - | Scene-boundary detection (TransNetV2) as an association hint | | AR-011 | Planned | T1, T2 | SR-002 | untagged | - | **Every model is fed the input it was trained for** — cost reduced by… | -| AR-012 | **Done** — `src/tra… | T2 | **SR-002** | covered | `src/main.cpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Presence follows **track extent**, not per-frame recognition | +| AR-012 | **Done** — `src/tra… | T2 | **SR-002** | covered | `src/main.cpp`, `src/nodes/identity_matcher_node.hpp`, `src/nodes/result_sink_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Presence follows **track extent**, not per-frame recognition | | AR-013 | **Done** — `last_se… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | `last_seen` optional state machine; window ends at last sighting, nev… | | AR-014 | **Done** — swap clo… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Belief swap A→B terminates the track and starts a new one | | AR-015 | **Done** — reverse … | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Two live tracks owned by one actor ⇒ treat as a detected cut, re-asso… | -| AR-016 | **Done** — `flush()… | T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | All tracks closed at EOF — a film ends with faces on screen | -| AR-017 | **Done** — `DeadTra… | T1, T2 | SR-002 | covered | `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Every presence claim carries its belief and identification route | +| AR-016 | **Done** — `flush()… | T2 | SR-002 | covered | `src/main.cpp`, `src/nodes/result_sink_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | All tracks closed at EOF — a film ends with faces on screen | +| AR-017 | **Done** — `DeadTra… | T1, T2 | SR-002 | covered | `src/nodes/result_sink_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Every presence claim carries its belief and identification route | | AR-018 | Planned | T1, T2 | SR-005 | untagged | - | Per-subject embedding store with banded admission (novel enough, safe… | | AR-019 | In Progress | T2 | SR-005 | untagged | - | Per-film gallery annex from owned tracks; acquires the non-frontal vi… | | AR-020 | Planned | T2 | SR-005 | untagged | - | Deferred re-identification of unknown tracks against the final expand… | @@ -101,7 +101,7 @@ _None._ | AR-022 | Planned | T1, T2 | §4 | untagged | - | Capture still-unidentified tracks: embeddings, metadata, **context cr… | | AR-023 | Done | T1 | SR-002 | covered | `src/gallery/gallery_calibration.hpp`, `src/nodes/identity_matcher_node.hpp` | Fit sigmoid calibration from intra/inter similarity distributions | | AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/nodes/identity_matcher_node.hpp` | **Always the calibrated probability, never a raw cosine** — exception… | -| AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/evidence_discount.hpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… | +| AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/evidence_discount.hpp`, `src/nodes/identity_matcher_node.hpp` | 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 | covered | `src/main.cpp` | One analysis core; modes are front-ends and must not fork pipeline lo… | @@ -113,8 +113,8 @@ _None._ | 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 | 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-002 | **Done** — `schema_… | T1 | SR-003 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/result_sink_node.hpp` | Windows carry belief + route; `extraction.*` carries `extinction_sec`… | +| IR-003 | **In Progress** — s… | T1 | SR-003 | covered | `src/main.cpp` | 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 | covered | `scripts/run_from_jellyfin.py` | Jellyfin round-trip: pull pending queue, push complete results only | @@ -159,7 +159,7 @@ _None._ **Locations:** 3 -- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown` +- [`src/config.hpp:103`](../src/config.hpp#L103) — `Unknown` - [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` @@ -167,15 +167,20 @@ _None._ **Locations:** 3 -- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown` +- [`src/config.hpp:103`](../src/config.hpp#L103) — `Unknown` - [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` ### AR-012 -**Locations:** 3 +**Locations:** 8 - [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));` +- [`src/main.cpp:216`](../src/main.cpp#L216) — `reg_cfg, EvidenceDiscounter(same_person));` +- [`src/nodes/identity_matcher_node.hpp:119`](../src/nodes/identity_matcher_node.hpp#L119) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:242`](../src/nodes/identity_matcher_node.hpp#L242) — `Unknown` +- [`src/nodes/result_sink_node.hpp:49`](../src/nodes/result_sink_node.hpp#L49) — `static constexpr std::string_view label() { return "result_sink"; }` +- [`src/nodes/result_sink_node.hpp:161`](../src/nodes/result_sink_node.hpp#L161) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` @@ -202,15 +207,18 @@ _None._ ### AR-016 -**Locations:** 2 +**Locations:** 4 +- [`src/main.cpp:216`](../src/main.cpp#L216) — `reg_cfg, EvidenceDiscounter(same_person));` +- [`src/nodes/result_sink_node.hpp:63`](../src/nodes/result_sink_node.hpp#L63) — `void set_pre_write_hook(std::function fn) { pre_write_ = std::move(fn); }` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` ### AR-017 -**Locations:** 2 +**Locations:** 3 +- [`src/nodes/result_sink_node.hpp:49`](../src/nodes/result_sink_node.hpp#L49) — `static constexpr std::string_view label() { return "result_sink"; }` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` @@ -220,24 +228,26 @@ _None._ - [`src/gallery/gallery_calibration.hpp:2`](../src/gallery/gallery_calibration.hpp#L2) — `Unknown` - [`src/gallery/gallery_calibration.hpp:53`](../src/gallery/gallery_calibration.hpp#L53) — `float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const` -- [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:111`](../src/nodes/identity_matcher_node.hpp#L111) — `const GalleryCalibration& calibration() const { return cal_; }` ### AR-024 **Locations:** 6 -- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown` +- [`src/config.hpp:103`](../src/config.hpp#L103) — `Unknown` - [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown` - [`src/gallery/gallery_calibration.hpp:53`](../src/gallery/gallery_calibration.hpp#L53) — `float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const` - [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` -- [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:111`](../src/nodes/identity_matcher_node.hpp#L111) — `const GalleryCalibration& calibration() const { return cal_; }` ### AR-025 -**Locations:** 1 +**Locations:** 3 - [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:119`](../src/nodes/identity_matcher_node.hpp#L119) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:242`](../src/nodes/identity_matcher_node.hpp#L242) — `Unknown` ### DP-001 @@ -267,7 +277,7 @@ _None._ **Locations:** 44 -- [`src/config.hpp:43`](../src/config.hpp#L43) — `Unknown` +- [`src/config.hpp:49`](../src/config.hpp#L49) — `Unknown` - [`src/gallery/embedder_stamp.cpp:1`](../src/gallery/embedder_stamp.cpp#L1) — `Unknown` - [`src/gallery/embedder_stamp.hpp:2`](../src/gallery/embedder_stamp.hpp#L2) — `Unknown` - [`src/gallery/gallery_builder.cpp:45`](../src/gallery/gallery_builder.cpp#L45) — `ActorGallery build_gallery(const BuildConfig& cfg)` @@ -318,6 +328,22 @@ _None._ - [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` +### IR-002 + +**Locations:** 5 + +- [`src/config.hpp:20`](../src/config.hpp#L20) — `struct Config` +- [`src/main.cpp:216`](../src/main.cpp#L216) — `reg_cfg, EvidenceDiscounter(same_person));` +- [`src/nodes/result_sink_node.hpp:49`](../src/nodes/result_sink_node.hpp#L49) — `static constexpr std::string_view label() { return "result_sink"; }` +- [`src/nodes/result_sink_node.hpp:122`](../src/nodes/result_sink_node.hpp#L122) — `void write_output()` +- [`src/nodes/result_sink_node.hpp:161`](../src/nodes/result_sink_node.hpp#L161) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` + +### IR-003 + +**Locations:** 1 + +- [`src/main.cpp:216`](../src/main.cpp#L216) — `reg_cfg, EvidenceDiscounter(same_person));` + ### IR-004 **Locations:** 16 @@ -398,7 +424,7 @@ _None._ **Locations:** 46 -- [`src/config.hpp:43`](../src/config.hpp#L43) — `Unknown` +- [`src/config.hpp:49`](../src/config.hpp#L49) — `Unknown` - [`src/gallery/embedder_stamp.cpp:1`](../src/gallery/embedder_stamp.cpp#L1) — `Unknown` - [`src/gallery/embedder_stamp.hpp:2`](../src/gallery/embedder_stamp.hpp#L2) — `Unknown` - [`src/gallery/gallery_builder.cpp:45`](../src/gallery/gallery_builder.cpp#L45) — `ActorGallery build_gallery(const BuildConfig& cfg)` @@ -447,26 +473,35 @@ _None._ ### SR-002 -**Locations:** 10 +**Locations:** 16 -- [`src/config.hpp:97`](../src/config.hpp#L97) — `Unknown` +- [`src/config.hpp:103`](../src/config.hpp#L103) — `Unknown` - [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown` - [`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/gallery/gallery_calibration.hpp:53`](../src/gallery/gallery_calibration.hpp#L53) — `float boundary_at(float p = 0.5f, float log_prior_odds = 0.f) const` - [`src/main.cpp:199`](../src/main.cpp#L199) — `reg_cfg, EvidenceDiscounter(same_person));` +- [`src/main.cpp:216`](../src/main.cpp#L216) — `reg_cfg, EvidenceDiscounter(same_person));` - [`src/nodes/face_detector_node.hpp:2`](../src/nodes/face_detector_node.hpp#L2) — `Unknown` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` -- [`src/nodes/identity_matcher_node.hpp:110`](../src/nodes/identity_matcher_node.hpp#L110) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:111`](../src/nodes/identity_matcher_node.hpp#L111) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:119`](../src/nodes/identity_matcher_node.hpp#L119) — `const GalleryCalibration& calibration() const { return cal_; }` +- [`src/nodes/identity_matcher_node.hpp:242`](../src/nodes/identity_matcher_node.hpp#L242) — `Unknown` +- [`src/nodes/result_sink_node.hpp:49`](../src/nodes/result_sink_node.hpp#L49) — `static constexpr std::string_view label() { return "result_sink"; }` +- [`src/nodes/result_sink_node.hpp:63`](../src/nodes/result_sink_node.hpp#L63) — `void set_pre_write_hook(std::function fn) { pre_write_ = std::move(fn); }` +- [`src/nodes/result_sink_node.hpp:161`](../src/nodes/result_sink_node.hpp#L161) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` ### SR-003 -**Locations:** 3 +**Locations:** 6 - [`src/audio_signature.cpp:3`](../src/audio_signature.cpp#L3) — `Unknown` - [`src/audio_signature.hpp:4`](../src/audio_signature.hpp#L4) — `Unknown` +- [`src/config.hpp:20`](../src/config.hpp#L20) — `struct Config` - [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` +- [`src/nodes/result_sink_node.hpp:49`](../src/nodes/result_sink_node.hpp#L49) — `static constexpr std::string_view label() { return "result_sink"; }` +- [`src/nodes/result_sink_node.hpp:122`](../src/nodes/result_sink_node.hpp#L122) — `void write_output()` ### SR-005 @@ -553,3 +588,9 @@ _None._ - `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']} +**Groups mixing requirement types (pipe separates types):** + +- `src/main.cpp:216` — {'group': ['AR-012', 'AR-016', 'IR-002', 'IR-003']} +- `src/nodes/result_sink_node.hpp:49` — {'group': ['AR-012', 'AR-017', 'IR-002']} +- `src/nodes/result_sink_node.hpp:161` — {'group': ['AR-012', 'IR-002']} +