diff --git a/docs/requirements.md b/docs/requirements.md index fbf9c46..7c866c8 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -104,7 +104,7 @@ Status: `Done` · `In Progress` · `Planned` · `TBD` · `Withdrawn` | ID | Requirement | Traces to | Priority | Status | |---|---|---|---|---| | VR-001 | HDF5 post-inference dump at the embedded-frame boundary | PR-002 | High | Done | -| VR-002 | Replay drives the **real** KPN nodes, not a reimplementation | PR-002 | High | **In Progress, and worse than it looked.** The C++ side is real (`tests/test_replay_fixtures.cpp`, determinism asserted). The *Python* side is not runnable: `sae_kpn` has not compiled since the AR-007/AR-008 redesign — the binding builds `FaceTrackerFunc` from a `Config` alone, and the tracker has required a registry and a calibration since. Any `.so` in a stale `build/` predates that. Now behind `SAE_BUILD_KPN_BINDINGS=OFF` so the breakage is attributed rather than rediscovered; fixing it is VR-011. **Also correct the fixture claim:** the dumps are *not* committed (`tests/fixtures/dumps/.gitignore`) — they are Gitea package-registry artifacts, pulled by the CI job | +| VR-002 | Replay drives the **real** KPN nodes, not a reimplementation | PR-002 | High | **Done** — including the sink, as of VR-011. Worth recording what the reimplementation was hiding: `build_minimal` rebuilt windows in Python from per-frame annotations, which never consult the registry, so it kept producing plausible output while registry-based presence in replay was returning **nothing at all**. The first run of the real chain emitted 0 actors on a film where 1647 frames carried an identified face. A reimplementation does not merely risk disagreeing with the pipeline; it can conceal the pipeline being broken | | VR-003 | Scoring: micro-F1 against X-Ray, precision/recall logged at every evaluation | PR-002 | High | Done | | VR-004 | Reproducible validation corpus with ground truth | PR-002 | High | Done | | VR-005 | Minimum face size study — TPI/FPI vs probe size, gallery held at native res | PR-002 | Medium | **Done** — knee at 24–32 px; 32 px gives 98.1% TPI, 0.0 FPI at every size. Degrades an already-aligned 112×112 crop, so it isolates the embedder and is an **upper bound**; VR-013 measures the same question end to end and AR-002 takes its number, not this one | @@ -113,12 +113,13 @@ Status: `Done` · `In Progress` · `Planned` · `TBD` · `Withdrawn` | VR-008 | Gallery scaling benchmark — throughput vs gallery size | PR-002 | Medium | Planned | | VR-009 | Verify accumulated posteriors are calibrated against held-out tracks | PR-002 | High | Planned | | VR-010 | Dump provenance attributes — embedder model, detector settings, `dense_scale`, `scene_detect`, sample rate | PR-002 | **High** | **Done** — `DumpProvenance` in `embedding_dump_node.hpp`, written as root attributes and read back tolerantly. Every field is optional so a pre-VR-010 dump reads as *unknown* rather than as a default; a silently-defaulted `detector_conf` is exactly the fabricated provenance this exists to prevent. This row said `Planned` while five VR-010 tags sat in the code — stale in the opposite direction to the rest of this register | -| VR-011 | Rewrite the replay harness for the post-AR-012 output contract | PR-002 | High | Planned | +| VR-011 | Rewrite the replay harness for the post-AR-012 output contract | PR-002 | High | **Done** — `sae_kpn` compiles again and the replay drives the whole chain including `ResultSinkFunc`, so presence comes from `TrackRegistry` claims rather than being rebuilt in Python. The three per-node factories are replaced by one `add_pipeline` that mirrors `main.cpp`'s construction order — the ordering constraint (matcher fits the calibration, registry needs a discounter from it, tracker needs both, sink needs the claims) is what a factory-per-node API could not express, and is why the tracker factory kept building `FaceTrackerFunc{cfg}` against a signature that had stopped existing. `build_minimal` and `anneal_sec` are gone. Verified end to end on the SuperHero fixture: 5 actors, 32 windows, 0 dropped votes | | VR-012 | Quality-knee study — TPI/FPI vs sharpness and vs pose, as VR-005 did for size; also settles whether the 5-point pose proxy needs a dedicated landmark model | PR-002 | Medium | Planned | | VR-014 | Audio-signature **offset recovery on real content** — a known trim recovered from film audio, not from the synthetic golden tone | PR-002 | Medium | **Done** — 40 random in-cap offsets, every one recovered to the nearest frame: **worst error 46 ms against a 500 ms budget**, and 46 ms is the floor rather than a result, since the offset is quantised to whole 92.88 ms frames. The `runtime/2` anchor confirmed through real head-trimmed files (a `delta` trim moves the window by `delta/2`). The one soft spot is **tier labelling, not accuracy**: the score falls with sub-frame misalignment (0.94–0.99 near a frame boundary, 0.69–0.73 at half a frame), so 27/40 correct alignments were demoted to `loose`. ±1 frame of slack in the *score* fixes it — measured, all 40 back to `audio` (min 0.906), false matches unmoved at 0.12–0.16, costing 81 ms of the budget | | VR-015 | Per-node cost and bottleneck attribution for a run — where the time actually goes | PR-004 | High | **Done** — `--benchmark ` on `scene_analyze`; `src/benchmark.hpp`. Reports cumulative CPU and wall time per node, and locates the pacing node from sampled channel occupancy rather than from time-in-node, which backpressure inflates. Verified UT-120…UT-124 | | VR-013 | Cross-source identification probe — gallery from one recording, probes from another, swept over input resolution end to end | PR-002 | Medium | **In Progress** — holding 90% of the plateau needs ~50 px end to end against VR-005's ~22 px, the gap being detection and landmark error; **`min_face_px` 40, since 32 admits faces in the falling region** (AR-002). FPI 0.0% at every scale. Ceiling is cross-view, not resolution | | VR-016 | **Cut-detection cadence study** — is `cut_threshold` 0.70 meaningful at the rate `camera_pos` is actually fed? | PR-002 | Medium | **Planned.** The histogram cut detector is the one always-on signal with no recorded provenance, and its input rate is not the rate it was fitted at. With `--scene-detect` off, `camera_pos` sits downstream of a source already decimated to `sample_fps`, so at the 1.0 default it compares frames **one second apart** — inside a single shot those differ enormously, and 0.70 correlation is a low bar to clear. With `--scene-detect` on it sees native-rate frames instead, so the same constant means two different things depending on an unrelated flag. This is AR-011's argument ("every model gets the input it was trained for") applied to a non-neural detector, and it matters because `is_cut` drives `track_alpha` to 0 and clears every expansion buffer. Cheap first measurement: run `camera_pos` over a `hero/` clip at 1/2/5 fps and compare cut counts against `tests/fixtures/dumps/scene_bounds.json`. The committed 5 fps dump shows 2.6% of frames flagged; nobody has measured 1 fps | +| VR-017 | **Vote-lag study** — how often does the matcher fall more than `track_extinction_sec` behind the tracker on real content? | PR-002 | **High** | **Planned.** Channel depth is a correctness parameter between `face_tracker` and `identity_matcher`, and the constraint runs opposite to the scene join's: there `kSceneJoinDepth` must EXCEED the TransNetV2 window, here the depth must be UNDER `track_extinction_sec × sample_fps`. Backpressure is what makes it bite — it is working, and a lossless channel converts depth into lag by design. Both nodes are 16 deep in `main.cpp`, which at the default `sample_fps` 1.0 is ~16 s of lag against a 5 s window, so `scene_analyze` can drop identity votes and until now said nothing. It now reports `dropped_votes` at shutdown; this row is the measurement that decides whether that should be fatal, and whether the right fix is bounding the depth or removing the coupling (reap on the matcher's clock rather than the tracker's, so a vote cannot be late by construction) | --- diff --git a/docs/traceability.md b/docs/traceability.md index 321f74b..1031ea6 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -3,7 +3,7 @@ -**Generated:** 2026-08-05T15:50:49+00:00 +**Generated:** 2026-08-08T10:06:24+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`). @@ -11,12 +11,12 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | Metric | Value | |---|---| -| Source files scanned | 118 | -| TRACES tags found | 215 | +| Source files scanned | 119 | +| TRACES tags found | 239 | | EXCEPTION tags found | 1 | -| Requirements defined | 71 | +| Requirements defined | 72 | | Requirements covered | 42 | -| **Coverage** | **59.2%** (42/71) | +| **Coverage** | **58.3%** (42/72) | | Coverage of CI-executable scope | 73.7% (42/57) | | Tagged but unexecuted in CI | 10 | | Orphan tags | 0 | @@ -29,7 +29,7 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev | DP | 2 | 0 | 8 | | IR | 8 | 0 | 8 | | GR | 5 | 0 | 9 | -| VR | 1 | 9 | 16 | +| VR | 1 | 9 | 17 | - **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-002, UT-003, UT-004, UT-005, UT-101, UT-102, UT-103, UT-104, UT-105, UT-106, UT-107, UT-108, UT-120, UT-121, UT-122, UT-123, UT-124, UT-130, UT-131, UT-132, UT-133, UT-134, UT-135, UT-136, UT-137, UT-138, UT-139, UT-140, UT-141 - **IT** tags present (separate taxonomy, not counted in coverage): IT-001 @@ -56,6 +56,7 @@ These requirements have no verification tier this repo's CI host can run, so a t | VR-012 | T4, out-of-ci | no | Quality-knee study — TPI/FPI vs sharpness and vs pose, as VR-005 did … | | VR-013 | T4, out-of-ci | yes | Cross-source identification probe — gallery from one recording, probe… | | VR-015 | out-of-ci | yes | Per-node cost and bottleneck attribution for a run — where the time a… | +| VR-017 | out-of-ci | no | **Vote-lag study** — how often does the matcher fall more than `track… | **Tagged but unexecuted:** AR-027, VR-001, VR-002, VR-003, VR-004, VR-005, VR-010, VR-011, VR-013, VR-015 — a test exists and is tagged, but this CI host cannot run it. Report those runs separately. @@ -86,19 +87,19 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn | 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 | **Done** — `FaceDet… | T2 | SR-002 | covered | `src/nodes/face_detector_node.hpp`, `tests/test_face_detector_node.cpp`, `tests/test_replay_fixtures.cpp` | Minimum face size **40×40 px** (VR-013 measured end to end; VR-005's … | | AR-003 | **Done** — `max_fac… | T1, T2, T4 | SR-002 | covered | `src/config.hpp`, `src/nodes/face_detector_node.hpp`, `src/nodes/identity_matcher_node.hpp` | No fixed per-frame face cap — crowd scenes must not lose background c… | -| AR-004 | **Mostly** — node o… | T1, T4 | SR-002 | covered | `src/benchmark.hpp`, `src/main.cpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_replay_fixtures.cpp` | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… | +| AR-004 | **Mostly** — node o… | T1, T4 | SR-002 | covered | `scripts/optimizer/replay.py`, `src/benchmark.hpp`, `src/main.cpp`, `src/nodes/identity_matcher_node.hpp`, `src/nodes/scene_detector_node.hpp`, `src/types.hpp`, `tests/test_channel_bytes.cpp`, `tests/test_replay_fixtures.cpp`, `tests/test_scene_detector_node.cpp` | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… | | AR-005 | **Done** — `umeyama… | T1, T3 | SR-002 | covered | `src/face_utils.hpp`, `src/nodes/face_aligner_node.hpp`, `tests/test_face_utils.cpp` | Align to 112×112 via ArcFace 5-point similarity transform, fitted by … | | AR-006 | Done | T3 | SR-002 | covered | `src/nodes/embedder_node.hpp` | 512-d L2-normalised embeddings, batched | | AR-007 | **Done** — `track_a… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/scene_preview.cpp`, `tests/test_face_tracker.cpp` | Associate detections by IoU + embedding, with **frame-dependent** wei… | -| AR-008 | **Done** — one pool… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `tests/test_face_tracker.cpp` | One track pool keyed on `last_seen`; no separate revival path | +| AR-008 | **Done** — one pool… | T2 | SR-002 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/track_registry.hpp`, `tests/test_face_tracker.cpp`, `tests/test_track_registry.cpp` | One track pool keyed on `last_seen`; no separate revival path | | AR-009 | Done | T2 | SR-002 | covered | `src/nodes/camera_position_change_detector_node.hpp` | Camera-cut detection (histogram) as an association hint | -| AR-010 | **Done** — decode b… | T2 | SR-002 | covered | `src/main.cpp`, `src/nodes/scene_boundary_annotator_node.hpp`, `src/nodes/scene_detector_node.hpp`, `src/scene_boundaries.hpp` | Scene-boundary detection (TransNetV2) as an association hint | +| AR-010 | **Done** — decode b… | T2 | SR-002 | covered | `src/main.cpp`, `src/nodes/scene_boundary_annotator_node.hpp`, `src/nodes/scene_detector_node.hpp`, `src/scene_boundaries.hpp`, `tests/test_scene_detector_node.cpp` | Scene-boundary detection (TransNetV2) as an association hint | | AR-011 | **Done** — both vio… | T1, T2 | SR-002 | covered | `src/config.hpp`, `src/nodes/scene_detector_node.hpp`, `src/scene_boundaries.hpp`, `tests/test_scene_detector_node.cpp` | **Every model is fed the input it was trained for** — cost reduced by… | | AR-012 | **Done** — `src/tra… | T2 | **SR-002** | covered | `src/config.hpp`, `src/kpn_bindings.cpp`, `src/main.cpp`, `src/nodes/frame_annotation_node.hpp`, `src/nodes/identity_matcher_node.hpp`, `src/nodes/result_sink_node.hpp`, `src/scene_preview.cpp`, `src/track_registry.hpp`, `tests/test_replay_fixtures.cpp`, `tests/test_track_registry.cpp` | Presence follows **track extent**, not per-frame recognition | -| AR-013 | **Done** — `last_se… | T2 | SR-002 | covered | `src/config.hpp`, `src/kpn_bindings.cpp`, `src/nodes/frame_annotation_node.hpp`, `src/track_registry.hpp`, `tests/test_replay_fixtures.cpp`, `tests/test_track_registry.cpp` | `last_seen` optional state machine; window ends at last sighting, nev… | +| AR-013 | **Done** — `last_se… | T2 | SR-002 | covered | `src/config.hpp`, `src/nodes/frame_annotation_node.hpp`, `src/nodes/identity_matcher_node.hpp`, `src/track_registry.hpp`, `tests/test_replay_fixtures.cpp`, `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/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-016 | **Done** — `flush()… | T2 | SR-002 | covered | `src/kpn_bindings.cpp`, `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/config.hpp`, `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 | **Done** — banded a… | T1, T2 | SR-005 | covered | `src/config.hpp`, `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_track_gallery.cpp` | Per-subject embedding store with banded admission (novel enough, safe… | | AR-019 | **Done** — all thre… | T2 | SR-005 | covered | `src/gallery/track_gallery.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_track_gallery.cpp` | Per-film gallery annex from owned tracks; acquires the non-frontal vi… | @@ -106,14 +107,14 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn | 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** — and the … | T1 | SR-002 | covered | `src/gallery/gallery_calibration.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_calibration.cpp` | Fit sigmoid calibration from intra/inter similarity distributions | -| AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `scripts/ci/check_raw_cosine.py`, `scripts/optimizer/replay.py`, `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/track_gallery.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/nodes/identity_matcher_node.hpp`, `src/scene_preview.cpp`, `tests/test_track_gallery.cpp` | **Always the calibrated probability, never a raw cosine** — exception… | -| AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/main.cpp`, `src/nodes/identity_matcher_node.hpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… | +| AR-024 | **Done** — associat… | T1, static | SR-002 | covered | `scripts/ci/check_raw_cosine.py`, `src/config.hpp`, `src/evidence_discount.hpp`, `src/gallery/gallery_calibration.hpp`, `src/gallery/track_gallery.hpp`, `src/main.cpp`, `src/nodes/face_tracker_node.hpp`, `src/nodes/identity_matcher_node.hpp`, `src/scene_preview.cpp`, `tests/test_track_gallery.cpp` | **Always the calibrated probability, never a raw cosine** — exception… | +| AR-025 | **Done** — log-odds… | T1 | SR-002 | covered | `src/config.hpp`, `src/evidence_discount.hpp`, `src/kpn_bindings.cpp`, `src/main.cpp`, `src/nodes/identity_matcher_node.hpp`, `src/track_registry.hpp`, `tests/test_track_registry.cpp` | Per-track Bayesian accumulation in log-odds, with correlated-observat… | | AR-026 | **In Progress** — t… | T1, T4 | SR-001 | covered | `src/backends/gemm_backend.cpp`, `src/gallery/track_gallery.hpp`, `src/inference/similarity.hpp`, `src/nodes/identity_matcher_node.hpp`, `tests/test_similarity.cpp`, `tests/test_track_gallery.cpp` | All similarity computed as GEMM, including annex and deferred pass | | AR-027 | Planned | T4 | SR-001 | tagged, unexecuted | `src/backends/gemm_backend.cpp` | Throughput acceptable for **arbitrary** gallery size | | AR-028 | **Done** — filled i… | T2 | SR-002 | covered | `scripts/optimizer/replay.py`, `src/nodes/embedding_dump_node.hpp`, `src/nodes/face_aligner_node.hpp`, `src/types.hpp`, `tests/test_embedding_dump.cpp`, `tests/test_face_utils.cpp` | **Embedding input quality assessed and carried** — every face scored … | | AR-029 | **Done** — `crop_sh… | T1 | SR-002 | covered | `src/face_utils.hpp`, `src/nodes/face_aligner_node.hpp`, `tests/test_face_utils.cpp` | Sharpness measure on the **aligned crop** (scale-normalised, so it ca… | | AR-030 | **In Progress** — m… | T1 | SR-002 | covered | `src/face_utils.hpp`, `src/nodes/face_aligner_node.hpp`, `tests/test_face_utils.cpp` | Visibility measure from the AR-001 5-point landmarks — extreme pose o… | -| DP-001 | **Done, after a rep… | T1, manual | PR-004 | covered | `src/main.cpp`, `src/scene_preview.cpp` | One analysis core; modes are front-ends and must not fork pipeline lo… | +| DP-001 | **Done, after a rep… | T1, manual | PR-004 | covered | `scripts/optimizer/replay.py`, `src/kpn_bindings.cpp`, `src/main.cpp`, `src/scene_preview.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… | @@ -121,9 +122,9 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn | DP-006 | Planned | T1, manual | PR-003 | untagged | - | Background incremental gallery refresh on a timer | | DP-007 | **Mostly** — image … | 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-001 | Done | T1 | SR-003 | covered | `src/kpn_bindings.cpp`, `src/nodes/result_sink_node.hpp` | Emit the JRay truth format as sibling `.jray.json` | | IR-002 | **Done** — `schema_… | T1 | SR-003 | covered | `src/config.hpp`, `src/main.cpp`, `src/nodes/result_sink_node.hpp`, `src/track_registry.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-003 | **In Progress** — s… | T1 | SR-003 | covered | `src/kpn_bindings.cpp`, `src/main.cpp` | Output written **after** the deferred pass, not at EOF | | IR-004 | **Done** — `src/aud… | T1 | SR-003 | covered | `scripts/validation/test_audio_offset.py`, `src/audio_bindings.cpp`, `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_bindings.cpp`, `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 | @@ -139,7 +140,7 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn | 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 | tagged, unexecuted | `src/nodes/embedding_dump_node.hpp`, `tests/test_embedding_dump.cpp`, `tests/test_replay_fixtures.cpp` | HDF5 post-inference dump at the embedded-frame boundary | -| VR-002 | **In Progress, and … | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/replay.py`, `tests/test_replay_fixtures.cpp` | Replay drives the **real** KPN nodes, not a reimplementation | +| VR-002 | **Done** — includin… | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/replay.py`, `src/kpn_bindings.cpp`, `tests/test_replay_fixtures.cpp` | 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 | tagged, unexecuted | `scripts/validation/ground_truth.py` | Reproducible validation corpus with ground truth | | VR-005 | **Done** — knee at … | out-of-ci | PR-002 | tagged, unexecuted | `scripts/validation/min_face_size.py` | Minimum face size study — TPI/FPI vs probe size, gallery held at nati… | @@ -148,12 +149,13 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn | VR-008 | Planned | out-of-ci | PR-002 | untagged | - | Gallery scaling benchmark — throughput vs gallery size | | VR-009 | Planned | T1, out-of-ci | PR-002 | untagged | - | Verify accumulated posteriors are calibrated against held-out tracks | | VR-010 | **Done** — `DumpPro… | out-of-ci | PR-002 | tagged, unexecuted | `src/nodes/embedding_dump_node.hpp` | Dump provenance attributes — embedder model, detector settings, `dens… | -| VR-011 | Planned | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/replay.py` | Rewrite the replay harness for the post-AR-012 output contract | +| VR-011 | **Done** — `sae_kpn… | out-of-ci | PR-002 | tagged, unexecuted | `scripts/optimizer/replay.py`, `scripts/optimizer/test_sae_kpn.py`, `src/kpn_bindings.cpp` | Rewrite the replay harness for the post-AR-012 output contract | | VR-012 | Planned | T4, out-of-ci | PR-002 | untagged | - | Quality-knee study — TPI/FPI vs sharpness and vs pose, as VR-005 did … | | VR-013 | **In Progress** — h… | T4, out-of-ci | PR-002 | tagged, unexecuted | `experiments/xsource/resolution_sweep.py`, `experiments/xsource/verify_labels.py` | Cross-source identification probe — gallery from one recording, probe… | | VR-014 | **Done** — 40 rando… | T2, out-of-ci | PR-002 | covered | `scripts/validation/test_audio_offset.py` | Audio-signature **offset recovery on real content** — a known trim re… | | VR-015 | **Done** — `--bench… | out-of-ci | PR-004 | tagged, unexecuted | `src/backends/trt_backend.cpp`, `src/benchmark.hpp`, `src/config.hpp`, `src/main.cpp`, `tests/test_benchmark.cpp` | Per-node cost and bottleneck attribution for a run — where the time a… | | VR-016 | **Planned.** The hi… | T2, out-of-ci | PR-002 | untagged | - | **Cut-detection cadence study** — is `cut_threshold` 0.70 meaningful … | +| VR-017 | **Planned.** Channe… | out-of-ci | PR-002 | untagged | - | **Vote-lag study** — how often does the matcher fall more than `track… | ## Detailed mapping @@ -177,21 +179,29 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/config.hpp:52`](../src/config.hpp#L52) — `Unknown` - [`src/nodes/face_detector_node.hpp:64`](../src/nodes/face_detector_node.hpp#L64) — `private:` -- [`src/nodes/identity_matcher_node.hpp:192`](../src/nodes/identity_matcher_node.hpp#L192) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/nodes/identity_matcher_node.hpp:213`](../src/nodes/identity_matcher_node.hpp#L213) — `std::vector host_query(static_cast(kMaxFaces) * 512);` ### AR-004 -**Locations:** 9 +**Locations:** 17 - [`src/benchmark.hpp:176`](../src/benchmark.hpp#L176) — `Unknown` - [`src/benchmark.hpp:472`](../src/benchmark.hpp#L472) — `void print(std::ostream& os, double film_sec) const` - [`src/benchmark.hpp:522`](../src/benchmark.hpp#L522) — `else if (c.in_fill_pct > 50.0)` -- [`src/main.cpp:103`](../src/main.cpp#L103) — `static constexpr std::size_t kSceneJoinDepth = 256;` -- [`src/main.cpp:115`](../src/main.cpp#L115) — `static std::shared_ptr scene_stats;` -- [`src/main.cpp:394`](../src/main.cpp#L394) — `Unknown` -- [`src/main.cpp:437`](../src/main.cpp#L437) — `std::ofstream bf(cfg.benchmark_path);` -- [`src/nodes/identity_matcher_node.hpp:192`](../src/nodes/identity_matcher_node.hpp#L192) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/main.cpp:106`](../src/main.cpp#L106) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:111`](../src/main.cpp#L111) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:132`](../src/main.cpp#L132) — `static constexpr double kSceneJoinSafety = 2.0;` +- [`src/main.cpp:154`](../src/main.cpp#L154) — `static std::size_t scene_join_depth(float sample_fps)` +- [`src/main.cpp:172`](../src/main.cpp#L172) — `static std::shared_ptr scene_stats;` +- [`src/main.cpp:451`](../src/main.cpp#L451) — `Unknown` +- [`src/main.cpp:494`](../src/main.cpp#L494) — `std::ofstream bf(cfg.benchmark_path);` +- [`src/nodes/identity_matcher_node.hpp:213`](../src/nodes/identity_matcher_node.hpp#L213) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/nodes/scene_detector_node.hpp:95`](../src/nodes/scene_detector_node.hpp#L95) — `Unknown` +- [`src/types.hpp:189`](../src/types.hpp#L189) — `Unknown` +- [`tests/test_channel_bytes.cpp:3`](../tests/test_channel_bytes.cpp#L3) — `Unknown` - [`tests/test_replay_fixtures.cpp:3`](../tests/test_replay_fixtures.cpp#L3) — `Unknown` +- [`tests/test_scene_detector_node.cpp:93`](../tests/test_scene_detector_node.cpp#L93) — `Unknown` +- [`scripts/optimizer/replay.py:174`](../scripts/optimizer/replay.py#L174) — `if i < len(frames):` ### AR-005 @@ -212,19 +222,21 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn **Locations:** 5 - [`src/config.hpp:160`](../src/config.hpp#L160) — `Unknown` -- [`src/main.cpp:260`](../src/main.cpp#L260) — `Unknown` +- [`src/main.cpp:317`](../src/main.cpp#L317) — `Unknown` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` - [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` ### AR-008 -**Locations:** 4 +**Locations:** 6 - [`src/config.hpp:160`](../src/config.hpp#L160) — `Unknown` -- [`src/main.cpp:260`](../src/main.cpp#L260) — `Unknown` +- [`src/main.cpp:317`](../src/main.cpp#L317) — `Unknown` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` +- [`src/track_registry.hpp:145`](../src/track_registry.hpp#L145) — `public:` - [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` +- [`tests/test_track_registry.cpp:417`](../tests/test_track_registry.cpp#L417) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` ### AR-009 @@ -234,57 +246,69 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### AR-010 -**Locations:** 9 +**Locations:** 12 -- [`src/main.cpp:103`](../src/main.cpp#L103) — `static constexpr std::size_t kSceneJoinDepth = 256;` -- [`src/main.cpp:447`](../src/main.cpp#L447) — `Unknown` -- [`src/main.cpp:517`](../src/main.cpp#L517) — `return run_net(std::move(net));` -- [`src/main.cpp:549`](../src/main.cpp#L549) — `Unknown` +- [`src/main.cpp:106`](../src/main.cpp#L106) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:111`](../src/main.cpp#L111) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:504`](../src/main.cpp#L504) — `Unknown` +- [`src/main.cpp:613`](../src/main.cpp#L613) — `return run_net(std::move(net));` +- [`src/main.cpp:645`](../src/main.cpp#L645) — `Unknown` - [`src/nodes/scene_boundary_annotator_node.hpp:2`](../src/nodes/scene_boundary_annotator_node.hpp#L2) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:36`](../src/nodes/scene_detector_node.hpp#L36) — `static constexpr std::string_view label() { return "scene_detector"; }` -- [`src/nodes/scene_detector_node.hpp:147`](../src/nodes/scene_detector_node.hpp#L147) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:188`](../src/nodes/scene_detector_node.hpp#L188) — `void write_output()` +- [`src/nodes/scene_detector_node.hpp:38`](../src/nodes/scene_detector_node.hpp#L38) — `static constexpr std::string_view label() { return "scene_detector"; }` +- [`src/nodes/scene_detector_node.hpp:95`](../src/nodes/scene_detector_node.hpp#L95) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:191`](../src/nodes/scene_detector_node.hpp#L191) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:232`](../src/nodes/scene_detector_node.hpp#L232) — `void write_output()` - [`src/scene_boundaries.hpp:2`](../src/scene_boundaries.hpp#L2) — `Unknown` +- [`tests/test_scene_detector_node.cpp:93`](../tests/test_scene_detector_node.cpp#L93) — `Unknown` ### AR-011 **Locations:** 6 - [`src/config.hpp:138`](../src/config.hpp#L138) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:70`](../src/nodes/scene_detector_node.hpp#L70) — `void operator()(Frame f)` -- [`src/nodes/scene_detector_node.hpp:93`](../src/nodes/scene_detector_node.hpp#L93) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:152`](../src/nodes/scene_detector_node.hpp#L152) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:72`](../src/nodes/scene_detector_node.hpp#L72) — `void operator()(Frame f)` +- [`src/nodes/scene_detector_node.hpp:137`](../src/nodes/scene_detector_node.hpp#L137) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:196`](../src/nodes/scene_detector_node.hpp#L196) — `Unknown` - [`src/scene_boundaries.hpp:30`](../src/scene_boundaries.hpp#L30) — `public:` - [`tests/test_scene_detector_node.cpp:4`](../tests/test_scene_detector_node.cpp#L4) — `Unknown` ### AR-012 -**Locations:** 13 +**Locations:** 17 - [`src/config.hpp:210`](../src/config.hpp#L210) — `Unknown` -- [`src/kpn_bindings.cpp:273`](../src/kpn_bindings.cpp#L273) — `Unknown` -- [`src/main.cpp:260`](../src/main.cpp#L260) — `Unknown` -- [`src/main.cpp:286`](../src/main.cpp#L286) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/kpn_bindings.cpp:406`](../src/kpn_bindings.cpp#L406) — `Unknown` +- [`src/main.cpp:317`](../src/main.cpp#L317) — `Unknown` +- [`src/main.cpp:343`](../src/main.cpp#L343) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/main.cpp:519`](../src/main.cpp#L519) — `Unknown` - [`src/nodes/frame_annotation_node.hpp:2`](../src/nodes/frame_annotation_node.hpp#L2) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:151`](../src/nodes/identity_matcher_node.hpp#L151) — `const GalleryCalibration& calibration() const { return cal_; }` -- [`src/nodes/identity_matcher_node.hpp:285`](../src/nodes/identity_matcher_node.hpp#L285) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:173`](../src/nodes/identity_matcher_node.hpp#L173) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` +- [`src/nodes/identity_matcher_node.hpp:306`](../src/nodes/identity_matcher_node.hpp#L306) — `Unknown` - [`src/nodes/result_sink_node.hpp:50`](../src/nodes/result_sink_node.hpp#L50) — `static constexpr std::string_view label() { return "result_sink"; }` - [`src/nodes/result_sink_node.hpp:164`](../src/nodes/result_sink_node.hpp#L164) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` +- [`src/track_registry.hpp:191`](../src/track_registry.hpp#L191) — `void tick(double now) { std::lock_guard g(mu_); tick_locked(now); }` - [`tests/test_replay_fixtures.cpp:3`](../tests/test_replay_fixtures.cpp#L3) — `Unknown` - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` +- [`tests/test_track_registry.cpp:376`](../tests/test_track_registry.cpp#L376) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` ### AR-013 -**Locations:** 6 +**Locations:** 11 - [`src/config.hpp:210`](../src/config.hpp#L210) — `Unknown` -- [`src/kpn_bindings.cpp:273`](../src/kpn_bindings.cpp#L273) — `Unknown` - [`src/nodes/frame_annotation_node.hpp:2`](../src/nodes/frame_annotation_node.hpp#L2) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:173`](../src/nodes/identity_matcher_node.hpp#L173) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` +- [`src/track_registry.hpp:145`](../src/track_registry.hpp#L145) — `public:` +- [`src/track_registry.hpp:191`](../src/track_registry.hpp#L191) — `void tick(double now) { std::lock_guard g(mu_); tick_locked(now); }` +- [`src/track_registry.hpp:222`](../src/track_registry.hpp#L222) — `std::lock_guard g(mu_);` - [`tests/test_replay_fixtures.cpp:3`](../tests/test_replay_fixtures.cpp#L3) — `Unknown` - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` +- [`tests/test_track_registry.cpp:376`](../tests/test_track_registry.cpp#L376) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` +- [`tests/test_track_registry.cpp:417`](../tests/test_track_registry.cpp#L417) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` ### AR-014 @@ -302,9 +326,10 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### AR-016 -**Locations:** 4 +**Locations:** 5 -- [`src/main.cpp:286`](../src/main.cpp#L286) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/kpn_bindings.cpp:406`](../src/kpn_bindings.cpp#L406) — `Unknown` +- [`src/main.cpp:343`](../src/main.cpp#L343) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` - [`src/nodes/result_sink_node.hpp:64`](../src/nodes/result_sink_node.hpp#L64) — `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` @@ -336,9 +361,9 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/gallery/track_gallery.hpp:136`](../src/gallery/track_gallery.hpp#L136) — `Unknown` - [`src/gallery/track_gallery.hpp:161`](../src/gallery/track_gallery.hpp#L161) — `Unknown` - [`src/gallery/track_gallery.hpp:183`](../src/gallery/track_gallery.hpp#L183) — `void set_owner(int track_id, int actor_idx)` -- [`src/nodes/identity_matcher_node.hpp:173`](../src/nodes/identity_matcher_node.hpp#L173) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` -- [`src/nodes/identity_matcher_node.hpp:295`](../src/nodes/identity_matcher_node.hpp#L295) — `Unknown` -- [`src/nodes/identity_matcher_node.hpp:314`](../src/nodes/identity_matcher_node.hpp#L314) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:194`](../src/nodes/identity_matcher_node.hpp#L194) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:316`](../src/nodes/identity_matcher_node.hpp#L316) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:335`](../src/nodes/identity_matcher_node.hpp#L335) — `Unknown` - [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` ### AR-023 @@ -352,7 +377,7 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### AR-024 -**Locations:** 20 +**Locations:** 19 - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` - [`src/config.hpp:160`](../src/config.hpp#L160) — `Unknown` @@ -362,28 +387,32 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/gallery/track_gallery.hpp:193`](../src/gallery/track_gallery.hpp#L193) — `void set_owner(int track_id, int actor_idx)` - [`src/gallery/track_gallery.hpp:233`](../src/gallery/track_gallery.hpp#L233) — `struct TrackState` - [`src/gallery/track_gallery.hpp:331`](../src/gallery/track_gallery.hpp#L331) — `Unknown` -- [`src/main.cpp:260`](../src/main.cpp#L260) — `Unknown` +- [`src/main.cpp:317`](../src/main.cpp#L317) — `Unknown` - [`src/nodes/face_tracker_node.hpp:2`](../src/nodes/face_tracker_node.hpp#L2) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:29`](../src/nodes/identity_matcher_node.hpp#L29) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:109`](../src/nodes/identity_matcher_node.hpp#L109) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:136`](../src/nodes/identity_matcher_node.hpp#L136) — `std::vector host_gallery(static_cast(n_gallery_) * 512);` - [`src/nodes/identity_matcher_node.hpp:143`](../src/nodes/identity_matcher_node.hpp#L143) — `const GalleryCalibration& calibration() const { return cal_; }` -- [`src/nodes/identity_matcher_node.hpp:250`](../src/nodes/identity_matcher_node.hpp#L250) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:271`](../src/nodes/identity_matcher_node.hpp#L271) — `Unknown` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` - [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` - [`tests/test_track_gallery.cpp:414`](../tests/test_track_gallery.cpp#L414) — `TrackGallery tg(expand_cfg());` - [`scripts/ci/check_raw_cosine.py:4`](../scripts/ci/check_raw_cosine.py#L4) — `Unknown` -- [`scripts/optimizer/replay.py:262`](../scripts/optimizer/replay.py#L262) — `Unknown` ### AR-025 -**Locations:** 5 +**Locations:** 10 - [`src/config.hpp:181`](../src/config.hpp#L181) — `Unknown` - [`src/evidence_discount.hpp:2`](../src/evidence_discount.hpp#L2) — `Unknown` -- [`src/main.cpp:268`](../src/main.cpp#L268) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/kpn_bindings.cpp:432`](../src/kpn_bindings.cpp#L432) — `Unknown` +- [`src/main.cpp:325`](../src/main.cpp#L325) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/main.cpp:519`](../src/main.cpp#L519) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:151`](../src/nodes/identity_matcher_node.hpp#L151) — `const GalleryCalibration& calibration() const { return cal_; }` -- [`src/nodes/identity_matcher_node.hpp:285`](../src/nodes/identity_matcher_node.hpp#L285) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:306`](../src/nodes/identity_matcher_node.hpp#L306) — `Unknown` +- [`src/track_registry.hpp:191`](../src/track_registry.hpp#L191) — `void tick(double now) { std::lock_guard g(mu_); tick_locked(now); }` +- [`src/track_registry.hpp:222`](../src/track_registry.hpp#L222) — `std::lock_guard g(mu_);` +- [`tests/test_track_registry.cpp:376`](../tests/test_track_registry.cpp#L376) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` ### AR-026 @@ -399,8 +428,8 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/inference/similarity.hpp:19`](../src/inference/similarity.hpp#L19) — `struct ISimilarityEngine` - [`src/inference/similarity.hpp:39`](../src/inference/similarity.hpp#L39) — `virtual int n_gallery() const = 0;` - [`src/nodes/identity_matcher_node.hpp:60`](../src/nodes/identity_matcher_node.hpp#L60) — `struct IdentityMatcherFunc` -- [`src/nodes/identity_matcher_node.hpp:212`](../src/nodes/identity_matcher_node.hpp#L212) — `std::vector host_query(static_cast(kMaxFaces) * 512);` -- [`src/nodes/identity_matcher_node.hpp:328`](../src/nodes/identity_matcher_node.hpp#L328) — `private:` +- [`src/nodes/identity_matcher_node.hpp:233`](../src/nodes/identity_matcher_node.hpp#L233) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/nodes/identity_matcher_node.hpp:349`](../src/nodes/identity_matcher_node.hpp#L349) — `private:` - [`tests/test_similarity.cpp:1`](../tests/test_similarity.cpp#L1) — `Unknown` - [`tests/test_similarity.cpp:85`](../tests/test_similarity.cpp#L85) — `Unknown` - [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` @@ -424,7 +453,7 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/types.hpp:62`](../src/types.hpp#L62) — `struct DetectedFace` - [`tests/test_embedding_dump.cpp:1`](../tests/test_embedding_dump.cpp#L1) — `Unknown` - [`tests/test_face_utils.cpp:1`](../tests/test_face_utils.cpp#L1) — `Unknown` -- [`scripts/optimizer/replay.py:66`](../scripts/optimizer/replay.py#L66) — `for i in range(len(ts)):` +- [`scripts/optimizer/replay.py:70`](../scripts/optimizer/replay.py#L70) — `for i in range(len(ts)):` ### AR-029 @@ -445,10 +474,12 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### DP-001 -**Locations:** 2 +**Locations:** 4 +- [`src/kpn_bindings.cpp:322`](../src/kpn_bindings.cpp#L322) — `Unknown` - [`src/main.cpp:3`](../src/main.cpp#L3) — `Unknown` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` +- [`scripts/optimizer/replay.py:194`](../scripts/optimizer/replay.py#L194) — `Unknown` ### DP-002 @@ -500,9 +531,9 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/gallery/gallery_store.cpp:82`](../src/gallery/gallery_store.cpp#L82) — `H5::StrType str(H5::PredType::C_S1, H5T_VARIABLE);` - [`src/gallery/gallery_store.cpp:167`](../src/gallery/gallery_store.cpp#L167) — `H5::DataSpace scalar(H5S_SCALAR);` - [`src/gallery/gallery_store.cpp:219`](../src/gallery/gallery_store.cpp#L219) — `Unknown` -- [`src/kpn_bindings.cpp:183`](../src/kpn_bindings.cpp#L183) — `Unknown` -- [`src/kpn_bindings.cpp:233`](../src/kpn_bindings.cpp#L233) — `Unknown` -- [`src/main.cpp:235`](../src/main.cpp#L235) — `Unknown` +- [`src/kpn_bindings.cpp:257`](../src/kpn_bindings.cpp#L257) — `Unknown` +- [`src/kpn_bindings.cpp:359`](../src/kpn_bindings.cpp#L359) — `static std::map> cache;` +- [`src/main.cpp:292`](../src/main.cpp#L292) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:127`](../src/nodes/embedding_dump_node.hpp#L127) — `static constexpr std::string_view label() { return "embedding_dump"; }` - [`src/nodes/embedding_dump_node.hpp:257`](../src/nodes/embedding_dump_node.hpp#L257) — `H5::H5File file(path_, H5F_ACC_TRUNC);` - [`src/scene_preview.cpp:130`](../src/scene_preview.cpp#L130) — `int main(int argc, char** argv)` @@ -530,8 +561,8 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`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:125`](../scripts/optimizer/replay.py#L125) — `Unknown` -- [`scripts/optimizer/replay.py:306`](../scripts/optimizer/replay.py#L306) — `Unknown` +- [`scripts/optimizer/replay.py:144`](../scripts/optimizer/replay.py#L144) — `Unknown` +- [`scripts/optimizer/replay.py:337`](../scripts/optimizer/replay.py#L337) — `Unknown` - [`scripts/sae_embed_loader.py:23`](../scripts/sae_embed_loader.py#L23) — `def resolve_arcface(models_dir: str, arcface: str \| None = None) -> str:` - [`scripts/sae_gallery.py:171`](../scripts/sae_gallery.py#L171) — `if not _stamp_empty(embedder):` - [`scripts/sae_gallery.py:200`](../scripts/sae_gallery.py#L200) — `for a in range(len(offset)):` @@ -546,8 +577,9 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### IR-001 -**Locations:** 1 +**Locations:** 2 +- [`src/kpn_bindings.cpp:261`](../src/kpn_bindings.cpp#L261) — `Unknown` - [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` ### IR-002 @@ -555,7 +587,7 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn **Locations:** 6 - [`src/config.hpp:20`](../src/config.hpp#L20) — `struct Config` -- [`src/main.cpp:286`](../src/main.cpp#L286) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/main.cpp:343`](../src/main.cpp#L343) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` - [`src/nodes/result_sink_node.hpp:50`](../src/nodes/result_sink_node.hpp#L50) — `static constexpr std::string_view label() { return "result_sink"; }` - [`src/nodes/result_sink_node.hpp:123`](../src/nodes/result_sink_node.hpp#L123) — `void write_output()` - [`src/nodes/result_sink_node.hpp:164`](../src/nodes/result_sink_node.hpp#L164) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` @@ -563,9 +595,10 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### IR-003 -**Locations:** 1 +**Locations:** 2 -- [`src/main.cpp:286`](../src/main.cpp#L286) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/kpn_bindings.cpp:406`](../src/kpn_bindings.cpp#L406) — `Unknown` +- [`src/main.cpp:343`](../src/main.cpp#L343) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` ### IR-004 @@ -640,23 +673,34 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### PR-002 -**Locations:** 11 +**Locations:** 22 +- [`src/kpn_bindings.cpp:6`](../src/kpn_bindings.cpp#L6) — `Unknown` +- [`src/kpn_bindings.cpp:70`](../src/kpn_bindings.cpp#L70) — `namespace nb = nanobind;` +- [`src/kpn_bindings.cpp:261`](../src/kpn_bindings.cpp#L261) — `Unknown` +- [`src/kpn_bindings.cpp:322`](../src/kpn_bindings.cpp#L322) — `Unknown` +- [`src/kpn_bindings.cpp:432`](../src/kpn_bindings.cpp#L432) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:2`](../src/nodes/embedding_dump_node.hpp#L2) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:18`](../src/nodes/embedding_dump_node.hpp#L18) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:133`](../src/nodes/embedding_dump_node.hpp#L133) — `static constexpr std::string_view label() { return "embedding_dump"; }` - [`src/nodes/embedding_dump_node.hpp:159`](../src/nodes/embedding_dump_node.hpp#L159) — `void operator()(EmbeddedSceneFrame ef)` - [`src/nodes/embedding_dump_node.hpp:261`](../src/nodes/embedding_dump_node.hpp#L261) — `H5::H5File file(path_, H5F_ACC_TRUNC);` - [`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/replay.py:212`](../scripts/optimizer/replay.py#L212) — `def build_minimal(annotations, movie, fps, cfg) -> dict:` +- [`scripts/optimizer/replay.py:120`](../scripts/optimizer/replay.py#L120) — `ending at the last sighting (AR-013) -- and are byte-for-byte the same` +- [`scripts/optimizer/replay.py:174`](../scripts/optimizer/replay.py#L174) — `if i < len(frames):` +- [`scripts/optimizer/replay.py:194`](../scripts/optimizer/replay.py#L194) — `Unknown` +- [`scripts/optimizer/replay.py:242`](../scripts/optimizer/replay.py#L242) — `Unknown` +- [`scripts/optimizer/replay.py:268`](../scripts/optimizer/replay.py#L268) — `def write_raw_frames(truth: dict, raw_out: str) -> None:` +- [`scripts/optimizer/replay.py:316`](../scripts/optimizer/replay.py#L316) — `def main():` - [`scripts/optimizer/second_score.py:5`](../scripts/optimizer/second_score.py#L5) — `Unknown` +- [`scripts/optimizer/test_sae_kpn.py:7`](../scripts/optimizer/test_sae_kpn.py#L7) — `Unknown` - [`scripts/validation/ground_truth.py:24`](../scripts/validation/ground_truth.py#L24) — `Unknown` - [`experiments/xsource/resolution_sweep.py:4`](../experiments/xsource/resolution_sweep.py#L4) — `Unknown` - [`experiments/xsource/verify_labels.py:4`](../experiments/xsource/verify_labels.py#L4) — `Unknown` ### PR-004 -**Locations:** 23 +**Locations:** 24 - [`src/backends/trt_backend.cpp:49`](../src/backends/trt_backend.cpp#L49) — `throw CudaError(std::string(what) + ": " + cudaGetErrorString(e));` - [`src/benchmark.hpp:2`](../src/benchmark.hpp#L2) — `Unknown` @@ -672,13 +716,14 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/benchmark.hpp:472`](../src/benchmark.hpp#L472) — `void print(std::ostream& os, double film_sec) const` - [`src/benchmark.hpp:522`](../src/benchmark.hpp#L522) — `else if (c.in_fill_pct > 50.0)` - [`src/config.hpp:35`](../src/config.hpp#L35) — `Unknown` +- [`src/kpn_bindings.cpp:322`](../src/kpn_bindings.cpp#L322) — `Unknown` - [`src/main.cpp:3`](../src/main.cpp#L3) — `Unknown` -- [`src/main.cpp:115`](../src/main.cpp#L115) — `static std::shared_ptr scene_stats;` -- [`src/main.cpp:208`](../src/main.cpp#L208) — `int main(int argc, char** argv)` -- [`src/main.cpp:294`](../src/main.cpp#L294) — `Unknown` -- [`src/main.cpp:368`](../src/main.cpp#L368) — `std::lock_guard lk(event_mtx);` -- [`src/main.cpp:394`](../src/main.cpp#L394) — `Unknown` -- [`src/main.cpp:407`](../src/main.cpp#L407) — `Unknown` +- [`src/main.cpp:172`](../src/main.cpp#L172) — `static std::shared_ptr scene_stats;` +- [`src/main.cpp:265`](../src/main.cpp#L265) — `int main(int argc, char** argv)` +- [`src/main.cpp:351`](../src/main.cpp#L351) — `Unknown` +- [`src/main.cpp:425`](../src/main.cpp#L425) — `std::lock_guard lk(event_mtx);` +- [`src/main.cpp:451`](../src/main.cpp#L451) — `Unknown` +- [`src/main.cpp:464`](../src/main.cpp#L464) — `Unknown` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` - [`tests/test_benchmark.cpp:3`](../tests/test_benchmark.cpp#L3) — `Unknown` @@ -715,14 +760,14 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/gallery/track_gallery.hpp:380`](../src/gallery/track_gallery.hpp#L380) — `static constexpr int kEmbDim = 512;` - [`src/inference/similarity.hpp:19`](../src/inference/similarity.hpp#L19) — `struct ISimilarityEngine` - [`src/inference/similarity.hpp:39`](../src/inference/similarity.hpp#L39) — `virtual int n_gallery() const = 0;` -- [`src/kpn_bindings.cpp:183`](../src/kpn_bindings.cpp#L183) — `Unknown` -- [`src/kpn_bindings.cpp:233`](../src/kpn_bindings.cpp#L233) — `Unknown` -- [`src/main.cpp:235`](../src/main.cpp#L235) — `Unknown` +- [`src/kpn_bindings.cpp:257`](../src/kpn_bindings.cpp#L257) — `Unknown` +- [`src/kpn_bindings.cpp:359`](../src/kpn_bindings.cpp#L359) — `static std::map> cache;` +- [`src/main.cpp:292`](../src/main.cpp#L292) — `Unknown` - [`src/nodes/embedding_dump_node.hpp:127`](../src/nodes/embedding_dump_node.hpp#L127) — `static constexpr std::string_view label() { return "embedding_dump"; }` - [`src/nodes/embedding_dump_node.hpp:257`](../src/nodes/embedding_dump_node.hpp#L257) — `H5::H5File file(path_, H5F_ACC_TRUNC);` - [`src/nodes/identity_matcher_node.hpp:60`](../src/nodes/identity_matcher_node.hpp#L60) — `struct IdentityMatcherFunc` -- [`src/nodes/identity_matcher_node.hpp:212`](../src/nodes/identity_matcher_node.hpp#L212) — `std::vector host_query(static_cast(kMaxFaces) * 512);` -- [`src/nodes/identity_matcher_node.hpp:328`](../src/nodes/identity_matcher_node.hpp#L328) — `private:` +- [`src/nodes/identity_matcher_node.hpp:233`](../src/nodes/identity_matcher_node.hpp#L233) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/nodes/identity_matcher_node.hpp:349`](../src/nodes/identity_matcher_node.hpp#L349) — `private:` - [`src/scene_preview.cpp:130`](../src/scene_preview.cpp#L130) — `int main(int argc, char** argv)` - [`src/types.hpp:173`](../src/types.hpp#L173) — `struct Actor` - [`tests/test_calibration.cpp:192`](../tests/test_calibration.cpp#L192) — `Embedding unit_axis(int slot)` @@ -757,8 +802,8 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`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:125`](../scripts/optimizer/replay.py#L125) — `Unknown` -- [`scripts/optimizer/replay.py:306`](../scripts/optimizer/replay.py#L306) — `Unknown` +- [`scripts/optimizer/replay.py:144`](../scripts/optimizer/replay.py#L144) — `Unknown` +- [`scripts/optimizer/replay.py:337`](../scripts/optimizer/replay.py#L337) — `Unknown` - [`scripts/run_from_jellyfin.py:4`](../scripts/run_from_jellyfin.py#L4) — `Unknown` - [`scripts/sae_embed_loader.py:23`](../scripts/sae_embed_loader.py#L23) — `def resolve_arcface(models_dir: str, arcface: str \| None = None) -> str:` - [`scripts/sae_gallery.py:171`](../scripts/sae_gallery.py#L171) — `if not _stamp_empty(embedder):` @@ -768,7 +813,7 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### SR-002 -**Locations:** 62 +**Locations:** 75 - [`src/config.hpp:52`](../src/config.hpp#L52) — `Unknown` - [`src/config.hpp:108`](../src/config.hpp#L108) — `Unknown` @@ -781,15 +826,19 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/face_utils.hpp:147`](../src/face_utils.hpp#L147) — `Unknown` - [`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/kpn_bindings.cpp:273`](../src/kpn_bindings.cpp#L273) — `Unknown` -- [`src/main.cpp:103`](../src/main.cpp#L103) — `static constexpr std::size_t kSceneJoinDepth = 256;` -- [`src/main.cpp:260`](../src/main.cpp#L260) — `Unknown` -- [`src/main.cpp:268`](../src/main.cpp#L268) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` -- [`src/main.cpp:286`](../src/main.cpp#L286) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` -- [`src/main.cpp:437`](../src/main.cpp#L437) — `std::ofstream bf(cfg.benchmark_path);` -- [`src/main.cpp:447`](../src/main.cpp#L447) — `Unknown` -- [`src/main.cpp:517`](../src/main.cpp#L517) — `return run_net(std::move(net));` -- [`src/main.cpp:549`](../src/main.cpp#L549) — `Unknown` +- [`src/kpn_bindings.cpp:406`](../src/kpn_bindings.cpp#L406) — `Unknown` +- [`src/main.cpp:106`](../src/main.cpp#L106) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:111`](../src/main.cpp#L111) — `static constexpr std::size_t kSceneInputDepth = 128;` +- [`src/main.cpp:132`](../src/main.cpp#L132) — `static constexpr double kSceneJoinSafety = 2.0;` +- [`src/main.cpp:154`](../src/main.cpp#L154) — `static std::size_t scene_join_depth(float sample_fps)` +- [`src/main.cpp:317`](../src/main.cpp#L317) — `Unknown` +- [`src/main.cpp:325`](../src/main.cpp#L325) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/main.cpp:343`](../src/main.cpp#L343) — `reg_cfg, EvidenceDiscounter(same_person, disc_cfg));` +- [`src/main.cpp:494`](../src/main.cpp#L494) — `std::ofstream bf(cfg.benchmark_path);` +- [`src/main.cpp:504`](../src/main.cpp#L504) — `Unknown` +- [`src/main.cpp:519`](../src/main.cpp#L519) — `Unknown` +- [`src/main.cpp:613`](../src/main.cpp#L613) — `return run_net(std::move(net));` +- [`src/main.cpp:645`](../src/main.cpp#L645) — `Unknown` - [`src/nodes/camera_position_change_detector_node.hpp:30`](../src/nodes/camera_position_change_detector_node.hpp#L30) — `struct CameraPositionChangeDetectorFunc` - [`src/nodes/embedder_node.hpp:21`](../src/nodes/embedder_node.hpp#L21) — `struct EmbedderFunc` - [`src/nodes/embedding_dump_node.hpp:181`](../src/nodes/embedding_dump_node.hpp#L181) — `Unknown` @@ -804,43 +853,53 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/nodes/identity_matcher_node.hpp:109`](../src/nodes/identity_matcher_node.hpp#L109) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:143`](../src/nodes/identity_matcher_node.hpp#L143) — `const GalleryCalibration& calibration() const { return cal_; }` - [`src/nodes/identity_matcher_node.hpp:151`](../src/nodes/identity_matcher_node.hpp#L151) — `const GalleryCalibration& calibration() const { return cal_; }` -- [`src/nodes/identity_matcher_node.hpp:192`](../src/nodes/identity_matcher_node.hpp#L192) — `std::vector host_query(static_cast(kMaxFaces) * 512);` -- [`src/nodes/identity_matcher_node.hpp:250`](../src/nodes/identity_matcher_node.hpp#L250) — `Unknown` -- [`src/nodes/identity_matcher_node.hpp:285`](../src/nodes/identity_matcher_node.hpp#L285) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:173`](../src/nodes/identity_matcher_node.hpp#L173) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` +- [`src/nodes/identity_matcher_node.hpp:213`](../src/nodes/identity_matcher_node.hpp#L213) — `std::vector host_query(static_cast(kMaxFaces) * 512);` +- [`src/nodes/identity_matcher_node.hpp:271`](../src/nodes/identity_matcher_node.hpp#L271) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:306`](../src/nodes/identity_matcher_node.hpp#L306) — `Unknown` - [`src/nodes/result_sink_node.hpp:50`](../src/nodes/result_sink_node.hpp#L50) — `static constexpr std::string_view label() { return "result_sink"; }` - [`src/nodes/result_sink_node.hpp:64`](../src/nodes/result_sink_node.hpp#L64) — `void set_pre_write_hook(std::function fn) { pre_write_ = std::move(fn); }` - [`src/nodes/result_sink_node.hpp:164`](../src/nodes/result_sink_node.hpp#L164) — `struct ActorMeta { std::string name, imdb_id, tmdb_id, jellyfin_id; };` - [`src/nodes/scene_boundary_annotator_node.hpp:2`](../src/nodes/scene_boundary_annotator_node.hpp#L2) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:36`](../src/nodes/scene_detector_node.hpp#L36) — `static constexpr std::string_view label() { return "scene_detector"; }` -- [`src/nodes/scene_detector_node.hpp:70`](../src/nodes/scene_detector_node.hpp#L70) — `void operator()(Frame f)` -- [`src/nodes/scene_detector_node.hpp:93`](../src/nodes/scene_detector_node.hpp#L93) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:147`](../src/nodes/scene_detector_node.hpp#L147) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:152`](../src/nodes/scene_detector_node.hpp#L152) — `Unknown` -- [`src/nodes/scene_detector_node.hpp:188`](../src/nodes/scene_detector_node.hpp#L188) — `void write_output()` +- [`src/nodes/scene_detector_node.hpp:38`](../src/nodes/scene_detector_node.hpp#L38) — `static constexpr std::string_view label() { return "scene_detector"; }` +- [`src/nodes/scene_detector_node.hpp:72`](../src/nodes/scene_detector_node.hpp#L72) — `void operator()(Frame f)` +- [`src/nodes/scene_detector_node.hpp:95`](../src/nodes/scene_detector_node.hpp#L95) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:137`](../src/nodes/scene_detector_node.hpp#L137) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:191`](../src/nodes/scene_detector_node.hpp#L191) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:196`](../src/nodes/scene_detector_node.hpp#L196) — `Unknown` +- [`src/nodes/scene_detector_node.hpp:232`](../src/nodes/scene_detector_node.hpp#L232) — `void write_output()` - [`src/scene_boundaries.hpp:2`](../src/scene_boundaries.hpp#L2) — `Unknown` - [`src/scene_boundaries.hpp:30`](../src/scene_boundaries.hpp#L30) — `public:` - [`src/scene_preview.cpp:147`](../src/scene_preview.cpp#L147) — `Unknown` - [`src/track_registry.hpp:2`](../src/track_registry.hpp#L2) — `Unknown` - [`src/track_registry.hpp:47`](../src/track_registry.hpp#L47) — `Unknown` +- [`src/track_registry.hpp:145`](../src/track_registry.hpp#L145) — `public:` +- [`src/track_registry.hpp:191`](../src/track_registry.hpp#L191) — `void tick(double now) { std::lock_guard g(mu_); tick_locked(now); }` +- [`src/track_registry.hpp:222`](../src/track_registry.hpp#L222) — `std::lock_guard g(mu_);` - [`src/types.hpp:62`](../src/types.hpp#L62) — `struct DetectedFace` +- [`src/types.hpp:189`](../src/types.hpp#L189) — `Unknown` - [`tests/test_calibration.cpp:1`](../tests/test_calibration.cpp#L1) — `Unknown` +- [`tests/test_channel_bytes.cpp:3`](../tests/test_channel_bytes.cpp#L3) — `Unknown` - [`tests/test_embedding_dump.cpp:1`](../tests/test_embedding_dump.cpp#L1) — `Unknown` - [`tests/test_face_detector_node.cpp:3`](../tests/test_face_detector_node.cpp#L3) — `Unknown` - [`tests/test_face_tracker.cpp:1`](../tests/test_face_tracker.cpp#L1) — `Unknown` - [`tests/test_face_utils.cpp:1`](../tests/test_face_utils.cpp#L1) — `Unknown` - [`tests/test_scene_detector_node.cpp:4`](../tests/test_scene_detector_node.cpp#L4) — `Unknown` +- [`tests/test_scene_detector_node.cpp:93`](../tests/test_scene_detector_node.cpp#L93) — `Unknown` +- [`tests/test_track_registry.cpp:376`](../tests/test_track_registry.cpp#L376) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` +- [`tests/test_track_registry.cpp:417`](../tests/test_track_registry.cpp#L417) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` - [`scripts/ci/check_raw_cosine.py:4`](../scripts/ci/check_raw_cosine.py#L4) — `Unknown` -- [`scripts/optimizer/replay.py:66`](../scripts/optimizer/replay.py#L66) — `for i in range(len(ts)):` -- [`scripts/optimizer/replay.py:262`](../scripts/optimizer/replay.py#L262) — `Unknown` +- [`scripts/optimizer/replay.py:70`](../scripts/optimizer/replay.py#L70) — `for i in range(len(ts)):` ### SR-003 -**Locations:** 8 +**Locations:** 9 - [`src/audio_bindings.cpp:3`](../src/audio_bindings.cpp#L3) — `Unknown` - [`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/kpn_bindings.cpp:261`](../src/kpn_bindings.cpp#L261) — `Unknown` - [`src/nodes/result_sink_node.hpp:2`](../src/nodes/result_sink_node.hpp#L2) — `Unknown` - [`src/nodes/result_sink_node.hpp:50`](../src/nodes/result_sink_node.hpp#L50) — `static constexpr std::string_view label() { return "result_sink"; }` - [`src/nodes/result_sink_node.hpp:123`](../src/nodes/result_sink_node.hpp#L123) — `void write_output()` @@ -859,18 +918,20 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/gallery/track_gallery.hpp:233`](../src/gallery/track_gallery.hpp#L233) — `struct TrackState` - [`src/gallery/track_gallery.hpp:331`](../src/gallery/track_gallery.hpp#L331) — `Unknown` - [`src/nodes/identity_matcher_node.hpp:136`](../src/nodes/identity_matcher_node.hpp#L136) — `std::vector host_gallery(static_cast(n_gallery_) * 512);` -- [`src/nodes/identity_matcher_node.hpp:173`](../src/nodes/identity_matcher_node.hpp#L173) — `MatchedSceneFrame operator()(TrackedSceneFrame tf)` -- [`src/nodes/identity_matcher_node.hpp:295`](../src/nodes/identity_matcher_node.hpp#L295) — `Unknown` -- [`src/nodes/identity_matcher_node.hpp:314`](../src/nodes/identity_matcher_node.hpp#L314) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:194`](../src/nodes/identity_matcher_node.hpp#L194) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:316`](../src/nodes/identity_matcher_node.hpp#L316) — `Unknown` +- [`src/nodes/identity_matcher_node.hpp:335`](../src/nodes/identity_matcher_node.hpp#L335) — `Unknown` - [`tests/test_track_gallery.cpp:1`](../tests/test_track_gallery.cpp#L1) — `Unknown` - [`tests/test_track_gallery.cpp:414`](../tests/test_track_gallery.cpp#L414) — `TrackGallery tg(expand_cfg());` - [`scripts/make_jellyfin_gallery.py:4`](../scripts/make_jellyfin_gallery.py#L4) — `Unknown` ### UT-001 -**Locations:** 1 +**Locations:** 3 - [`tests/test_track_registry.cpp:3`](../tests/test_track_registry.cpp#L3) — `Unknown` +- [`tests/test_track_registry.cpp:376`](../tests/test_track_registry.cpp#L376) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` +- [`tests/test_track_registry.cpp:417`](../tests/test_track_registry.cpp#L417) — `TrackRegistry reg(cfg(/*extinction=*/5.0), disc());` ### UT-002 @@ -880,9 +941,10 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### UT-003 -**Locations:** 1 +**Locations:** 2 - [`tests/test_scene_detector_node.cpp:4`](../tests/test_scene_detector_node.cpp#L4) — `Unknown` +- [`tests/test_scene_detector_node.cpp:93`](../tests/test_scene_detector_node.cpp#L93) — `Unknown` ### UT-004 @@ -1072,10 +1134,14 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### VR-002 -**Locations:** 2 +**Locations:** 6 +- [`src/kpn_bindings.cpp:6`](../src/kpn_bindings.cpp#L6) — `Unknown` +- [`src/kpn_bindings.cpp:322`](../src/kpn_bindings.cpp#L322) — `Unknown` - [`tests/test_replay_fixtures.cpp:3`](../tests/test_replay_fixtures.cpp#L3) — `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/replay.py:120`](../scripts/optimizer/replay.py#L120) — `ending at the last sighting (AR-013) -- and are byte-for-byte the same` +- [`scripts/optimizer/replay.py:194`](../scripts/optimizer/replay.py#L194) — `Unknown` ### VR-003 @@ -1107,9 +1173,21 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn ### VR-011 -**Locations:** 1 +**Locations:** 13 -- [`scripts/optimizer/replay.py:212`](../scripts/optimizer/replay.py#L212) — `def build_minimal(annotations, movie, fps, cfg) -> dict:` +- [`src/kpn_bindings.cpp:6`](../src/kpn_bindings.cpp#L6) — `Unknown` +- [`src/kpn_bindings.cpp:70`](../src/kpn_bindings.cpp#L70) — `namespace nb = nanobind;` +- [`src/kpn_bindings.cpp:261`](../src/kpn_bindings.cpp#L261) — `Unknown` +- [`src/kpn_bindings.cpp:322`](../src/kpn_bindings.cpp#L322) — `Unknown` +- [`src/kpn_bindings.cpp:432`](../src/kpn_bindings.cpp#L432) — `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/replay.py:120`](../scripts/optimizer/replay.py#L120) — `ending at the last sighting (AR-013) -- and are byte-for-byte the same` +- [`scripts/optimizer/replay.py:174`](../scripts/optimizer/replay.py#L174) — `if i < len(frames):` +- [`scripts/optimizer/replay.py:194`](../scripts/optimizer/replay.py#L194) — `Unknown` +- [`scripts/optimizer/replay.py:242`](../scripts/optimizer/replay.py#L242) — `Unknown` +- [`scripts/optimizer/replay.py:268`](../scripts/optimizer/replay.py#L268) — `def write_raw_frames(truth: dict, raw_out: str) -> None:` +- [`scripts/optimizer/replay.py:316`](../scripts/optimizer/replay.py#L316) — `def main():` +- [`scripts/optimizer/test_sae_kpn.py:7`](../scripts/optimizer/test_sae_kpn.py#L7) — `Unknown` ### VR-013 @@ -1142,11 +1220,11 @@ Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn - [`src/benchmark.hpp:472`](../src/benchmark.hpp#L472) — `void print(std::ostream& os, double film_sec) const` - [`src/benchmark.hpp:522`](../src/benchmark.hpp#L522) — `else if (c.in_fill_pct > 50.0)` - [`src/config.hpp:35`](../src/config.hpp#L35) — `Unknown` -- [`src/main.cpp:115`](../src/main.cpp#L115) — `static std::shared_ptr scene_stats;` -- [`src/main.cpp:208`](../src/main.cpp#L208) — `int main(int argc, char** argv)` -- [`src/main.cpp:294`](../src/main.cpp#L294) — `Unknown` -- [`src/main.cpp:368`](../src/main.cpp#L368) — `std::lock_guard lk(event_mtx);` -- [`src/main.cpp:394`](../src/main.cpp#L394) — `Unknown` -- [`src/main.cpp:407`](../src/main.cpp#L407) — `Unknown` +- [`src/main.cpp:172`](../src/main.cpp#L172) — `static std::shared_ptr scene_stats;` +- [`src/main.cpp:265`](../src/main.cpp#L265) — `int main(int argc, char** argv)` +- [`src/main.cpp:351`](../src/main.cpp#L351) — `Unknown` +- [`src/main.cpp:425`](../src/main.cpp#L425) — `std::lock_guard lk(event_mtx);` +- [`src/main.cpp:451`](../src/main.cpp#L451) — `Unknown` +- [`src/main.cpp:464`](../src/main.cpp#L464) — `Unknown` - [`tests/test_benchmark.cpp:3`](../tests/test_benchmark.cpp#L3) — `Unknown` diff --git a/scripts/optimizer/replay.py b/scripts/optimizer/replay.py index d261ec7..22f161e 100644 --- a/scripts/optimizer/replay.py +++ b/scripts/optimizer/replay.py @@ -171,14 +171,24 @@ def replay(dump_path: str, gallery: str, cfg: dict, build_dir: str, time.sleep(0.05) return eof - # Channel capacity must exceed the frame count so the fast source can't overflow - # a downstream FIFO before the serial reader drains it — PyNode DROPS on overflow, - # which would silently truncate the replay. Size to the whole film + slack. - # Every channel gets capacity ≥ the whole film so NOTHING can ever overflow-drop: - # the source can push all frames before any downstream node has drained, and a - # dropped frame silently corrupts the score. Memory is cheap (a few k pointers); - # correctness is not. Generous slack on top. - cap = len(frames) * 2 + 64 + # TRACES: VR-011 | AR-004 | PR-002 + # Purely a throughput and memory choice, and that is the point: the answer + # must not depend on it. It used to be `len(frames) * 2 + 64` -- the whole + # film -- to dodge a PyNode overflow drop that AR-004 has since replaced + # with parking. + # + # Removing backpressure that way was catastrophic and silent. The registry + # reaped on the TRACKER's clock while evidence arrived later from the + # matcher, so a deep channel closed tracks before their votes landed: on the + # SuperHero fixture, capacity 32 gave 5 actors and capacity 10322 gave 0, + # from identical input. + # + # The fix was NOT to bound this against track_extinction_sec. That would put + # an algorithm constant in charge of a throughput knob and leave presence a + # function of scheduling. The registry now reaps on the matcher's evidence + # watermark (TrackRegistry::advance_evidence), so a vote cannot be late by + # construction and this number is free again. + cap = 64 sae_kpn.add_node_python(net, "replay", source, [], ["EmbeddedSceneFrame"], cap) # TRACES: VR-011, VR-002 | DP-001 | PR-002 @@ -224,10 +234,26 @@ def replay(dump_path: str, gallery: str, cfg: dict, build_dir: str, f"({len(frames) - 1} frames); the sink never saw EOF") time.sleep(0.02) + diag = sae_kpn.pipeline_diagnostics(net) if stop: net.stop() sae_kpn.release_pipeline(net) + # TRACES: VR-011 | PR-002 + # A dropped vote means the matcher lagged the tracker by more than + # track_extinction_sec of film, so evidence arrived for a track that had + # already been reaped. The result is not a slightly worse score -- it is a + # silently emptier one, and this is exactly how the whole-film capacity bug + # presented. Refuse the number rather than report it. + dropped = int(diag.get("dropped_votes", 0)) + if dropped: + raise RuntimeError( + f"replay dropped {dropped} identity votes: the matcher fell more " + f"than track_extinction_sec behind the tracker, so presence is " + f"under-reported. Lower the channel capacity (currently {cap}) or " + f"raise track_extinction_sec.") + + with open(out_path) as f: result = json.load(f) diff --git a/src/kpn_bindings.cpp b/src/kpn_bindings.cpp index 76fb025..eaa0762 100644 --- a/src/kpn_bindings.cpp +++ b/src/kpn_bindings.cpp @@ -258,7 +258,7 @@ static Config config_from_dict(nb::dict d) { if (d.contains("require_gallery_stamp")) cfg.require_gallery_stamp = nb::cast(d["require_gallery_stamp"]); - /// TRACES: VR-011, IR-001 | PR-002, SR-003 + /// TRACES: VR-011 | IR-001 | PR-002 | SR-003 // The sink is a real node in this network now, so it needs the two things // that decide what it writes and where. Both used to be irrelevant here // because the replay never had a sink -- Python rebuilt presence instead, @@ -429,6 +429,33 @@ NB_MODULE(sae_kpn, m) { /// replay, which a long sweep will notice. m.def("release_pipeline", [](Net& net) { sessions().erase(&net); }, "net"_a); + /// TRACES: VR-011 | AR-025 | PR-002 + /// The registry's own count of how often it was wrong, exposed so a replay + /// can fail on it instead of returning a plausible-looking empty answer. + /// + /// `dropped_votes` is the one that matters here and it earned its keep + /// immediately. A vote lands on a track the registry has already reaped when + /// the matcher lags the tracker by more than track_extinction_sec of film. + /// In scene_analyze that cannot happen -- channels are 16-64 deep, so + /// backpressure pins the two nodes within a few frames of each other. This + /// harness sized every channel to the whole film to avoid a PyNode overflow + /// drop, which removed the backpressure entirely: the tracker ran the film + /// to the end while the matcher was still in its first minute, every vote + /// arrived after its track was gone, no track was ever owned, and the run + /// produced zero presence windows while cheerfully reporting 1647 frames + /// with an identified face. + m.def("pipeline_diagnostics", [](Net& net) { + nb::dict d; + auto it = sessions().find(&net); + if (it == sessions().end() || !it->second->registry) return d; + const auto& r = *it->second->registry; + d["dropped_votes"] = r.dropped_votes(); + d["belief_swaps"] = r.belief_swaps(); + d["actor_conflicts"] = r.actor_conflicts(); + d["live_tracks"] = static_cast(r.live()); + return d; + }, "net"_a); + /// True once the sink has written its output. The sink flushes on the EOF /// annotation, so a caller that reads the file before this is racing it. m.def("pipeline_done", [](Net& net) { diff --git a/src/main.cpp b/src/main.cpp index 7be3ef0..5e23040 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -516,6 +516,45 @@ int main(int argc, char** argv) { std::cerr << "\n"; } + /// TRACES: AR-025, AR-012 | SR-002 + // How often the registry was asked about a track it had already reaped. + // + // A vote is dropped when the matcher lags the tracker by more than + // track_extinction_sec of FILM time. The two are adjacent nodes with a + // 16-deep channel between them, and the matcher is much the slower of + // the pair (a GEMM over the whole gallery against a Hungarian solve over + // a handful of boxes), so that channel runs full and the lag is close to + // its depth. In frames: + // + // lag_sec ~= channel_depth / sample_fps + // + // At the default sample_fps of 1.0 that is ~16 s against a 5 s window, + // so votes CAN be dropped here, and each one is identity evidence that + // never reached the track it belonged to -- presence under-reported, in + // a way that reads as a recognition miss. + // + // Reported rather than fatal, deliberately, and the distinction from the + // dropped-frame case below is real: a dropped frame means the output + // describes footage nobody analysed, which is always wrong. A dropped + // vote means one observation of a track went missing, which degrades a + // claim without falsifying it. There is also no measurement yet of how + // often it happens on real content -- so this prints the number that + // would justify a harder line rather than presuming it. See VR-017. + if (registry) { + const int dv = registry->dropped_votes(); + if (dv > 0) { + std::cerr << "[registry] WARNING: " << dv << " identity vote(s) " + "arrived for already-reaped tracks. The matcher is " + "lagging the tracker by more than track_extinction_sec (" + << cfg.track_extinction_sec << "s) of film; presence is " + "under-reported. Raise --track-extinction or reduce the " + "face_tracker/identity_matcher channel depth.\n"; + } + std::cerr << "[registry] belief_swaps=" << registry->belief_swaps() + << " actor_conflicts=" << registry->actor_conflicts() + << " dropped_votes=" << dv << "\n"; + } + bool dropped = false; { std::lock_guard lk(event_mtx); diff --git a/src/nodes/identity_matcher_node.hpp b/src/nodes/identity_matcher_node.hpp index ff6ebf7..a0a4b80 100644 --- a/src/nodes/identity_matcher_node.hpp +++ b/src/nodes/identity_matcher_node.hpp @@ -152,7 +152,12 @@ struct IdentityMatcherFunc { /// Where per-frame identity evidence reaches the registry. Optional: with no /// registry attached the matcher behaves exactly as before, which keeps the /// replay harness and the unit tests working unchanged. - void set_registry(std::shared_ptr r) { registry_ = std::move(r); } + void set_registry(std::shared_ptr r) { + registry_ = std::move(r); + // This node is the evidence source, so the registry must not close a + // track until this node's watermark has passed it (AR-013). + if (registry_) registry_->expect_evidence(); + } // Runtime setter — lets a persistent pipeline be reused across a threshold sweep // without rebuilding the (expensive, gallery-resident) matcher. The gallery, @@ -165,6 +170,22 @@ struct IdentityMatcherFunc { return {std::move(tf.source), {}}; } + /// TRACES: AR-012, AR-013 | SR-002 + // Publish the evidence watermark BEFORE voting on this frame: every + // observation strictly before it has now been folded in, so the registry + // may reap against it. Unconditional -- a frame with no faces still + // advances the watermark, or a long faceless stretch would stall reaping + // and hold every dormant track open to the end of the film. + // + // This is what makes presence independent of node speed. The registry + // used to reap on the TRACKER's clock, and backpressure (working as + // AR-004 intends) means the tracker can be a whole channel's depth ahead + // of this node -- so tracks were closed before their votes arrived, the + // votes were dropped, and the run silently under-reported. Measured on + // the SuperHero fixture before this change: channel depth 32 gave 5 + // actors, depth 10322 gave 0, from identical input. + if (registry_) registry_->advance_evidence(tf.source.timestamp_sec); + // A hard cut changes the camera viewpoint. The face_tracker may revive a // track_id across the cut (identity continuity), but promotion must never // mix embeddings from two viewpoints under one buffer, so we still drop diff --git a/src/track_registry.hpp b/src/track_registry.hpp index 0304e0c..a73aa34 100644 --- a/src/track_registry.hpp +++ b/src/track_registry.hpp @@ -138,13 +138,37 @@ public: FrameScope(TrackRegistry& reg, double now) : reg_(reg), lock_(reg.mu_) { reg_.tick_locked(now); } - /// All live tracks — **one pool**. `last_seen` tells the caller whether - /// IoU is meaningful; a dormant track is matched on embedding alone. - /// There is no separate revival path (AR-008). + /// All ASSOCIABLE tracks — **one pool**. `last_seen` tells the caller + /// whether IoU is meaningful; a dormant track is matched on embedding + /// alone. There is no separate revival path (AR-008). + /// + /// TRACES: AR-008, AR-013 | SR-002 + /// Filtered on the TRACKER's clock, deliberately, while reaping runs on + /// the matcher's evidence watermark. The two answer different questions + /// and must not share an answer: + /// + /// "may this detection link to that track?" — a tracking question, + /// asked now, about a box observed `track_extinction_sec` ago. + /// "is that track finished, so its claim can be emitted?" — a presence + /// question, which cannot be answered until every vote is in. + /// + /// Conflating them makes the result depend on node speed in one + /// direction or the other. Reaping on the tracker's clock closed tracks + /// before their votes arrived. Deferring association to the evidence + /// clock — which is what deferring the erase alone did — left retired + /// tracks in the pool for as long as the matcher lagged, so a new face + /// re-associated onto a long-dead track and two people merged into one + /// window. Measured: 5 actors / 16 windows at channel depth 32 against + /// 3 actors / 5 windows at depth 10322, from identical input. std::vector candidates() { std::vector out; out.reserve(reg_.tracks_.size()); - for (auto& [id, t] : reg_.tracks_) out.push_back(&t); + for (auto& [id, t] : reg_.tracks_) { + if (t.last_seen && + (reg_.now_ - *t.last_seen) > reg_.cfg_.track_extinction_sec) + continue; // retired from association; still awaiting evidence + out.push_back(&t); + } return out; } @@ -164,6 +188,50 @@ public: /// happens to appear, and a film ending mid-track never closes. void tick(double now) { std::lock_guard g(mu_); tick_locked(now); } + /// TRACES: AR-012, AR-013, AR-025 | SR-002 + /// The evidence watermark: every observation up to `t` has been folded in. + /// + /// Reaping is driven by THIS, not by the tracker's clock, and the difference + /// is what stops a correct answer from depending on how fast two nodes run. + /// + /// The tracker and the matcher are separate KPN nodes with a channel between + /// them, and the matcher is much the slower of the pair. Backpressure — + /// working exactly as AR-004 intends — turns that channel's depth into lag, + /// so the tracker's timestamp can be far ahead of the last frame anybody has + /// actually voted on. Reaping on the tracker's clock therefore closed tracks + /// before their evidence arrived: the votes landed on ids that no longer + /// existed, were counted as dropped, and the track was emitted unowned or + /// not at all. Deeper channel, fewer identifications, from identical input. + /// + /// The fix is not to bound the channel against `track_extinction_sec`. That + /// makes an algorithm constant police a throughput knob, and leaves the + /// answer a function of scheduling. It is to reap on the watermark, which is + /// the same device `SceneBoundaries::scored_through()` uses for the AR-010 + /// join: a consumer past that point is asking about frames nobody has looked + /// at yet, and the honest response is to wait rather than to guess. + /// + /// Monotonic, and only ever *delays* a reap, so no window can be extended by + /// it — AR-013's "a window ends at the last sighting, never after" is a + /// property of `emit_locked`, which takes `last_seen` and never `now`. + void advance_evidence(double t) { + std::lock_guard g(mu_); + if (t > evidence_through_) evidence_through_ = t; + reap_locked(); + } + + /// TRACES: AR-013, AR-025 | SR-002 + /// Declare that some stage will publish an evidence watermark, so reaping + /// must wait for it. + /// + /// Explicit rather than inferred from "has anyone voted yet". Inferring it + /// re-opens the bug exactly at startup: before the matcher's first frame no + /// vote has been seen, so the registry would fall back to the tracker's + /// clock during precisely the window in which the tracker is furthest + /// ahead. `IdentityMatcherFunc::set_registry` calls this, so any pipeline + /// with a matcher waits, and a test that drives the tracker alone keeps the + /// simple behaviour instead of hanging on a watermark nobody will publish. + void expect_evidence() { std::lock_guard g(mu_); awaits_evidence_ = true; } + // ── Evidence ───────────────────────────────────────────────────────────── /// Fold one observation into a track's belief (AR-025). /// @@ -259,9 +327,20 @@ public: private: // ── Locked internals ───────────────────────────────────────────────────── void tick_locked(double now) { + // The tracker's clock still bounds association (a dormant track is only + // a candidate while it is alive), but it no longer decides death. + now_ = now; + reap_locked(); + } + + /// Reap against the evidence watermark when a producer of one is attached + /// (see expect_evidence); otherwise against the tracker's clock, which is + /// the same thing when there is only one clock. + void reap_locked() { + const double clock = awaits_evidence_ ? evidence_through_ : now_; for (auto it = tracks_.begin(); it != tracks_.end(); ) { const auto& ls = it->second.last_seen; - if (ls && (now - *ls) > cfg_.track_extinction_sec) { + if (ls && (clock - *ls) > cfg_.track_extinction_sec) { emit_locked(it->second, *ls); it = tracks_.erase(it); } else { @@ -383,6 +462,9 @@ private: std::map owner_index_; ///< actor_idx → live track_id (AR-015) DeadTrackFn on_dead_; int next_id_{0}; + double now_{0.0}; ///< tracker's clock (association) + double evidence_through_{0.0}; ///< matcher's watermark (reaping) + bool awaits_evidence_{false}; int dropped_votes_{0}; int belief_swaps_{0}; int actor_conflicts_{0}; diff --git a/tests/test_track_registry.cpp b/tests/test_track_registry.cpp index 1773281..9f67f87 100644 --- a/tests/test_track_registry.cpp +++ b/tests/test_track_registry.cpp @@ -370,3 +370,83 @@ TEST_CASE("confidence grows across frames of the same face", "[registry][AR-025] // ...but it must still be worth far less than 50 independent looks would be. CHECK(sink.claims[0].effective_obs < 25.0f); } + +// ── The evidence watermark: presence must not depend on node speed ─────────── + +/// TRACES: UT-001 | AR-012, AR-013, AR-025 | SR-002 +TEST_CASE("a track is not reaped until the evidence clock passes it", + "[registry][AR-013]") { + // The tracker and the matcher are separate KPN nodes, and backpressure -- + // working exactly as AR-004 intends -- lets the tracker run a whole + // channel's depth ahead. Reaping on the tracker's clock therefore closed + // tracks before their votes arrived: the votes landed on ids that no longer + // existed and the run silently under-reported. On the SuperHero fixture that + // was 5 actors at channel depth 32 against 0 actors at depth 10322, from + // identical input. + TrackRegistry reg(cfg(/*extinction=*/5.0), disc()); + Sink sink; sink.attach(reg); + reg.expect_evidence(); // as IdentityMatcherFunc::set_registry does + + int id; + { + auto s = reg.begin_frame(0.0); + id = s.create(0.0, axis(1)); + s.mark_lost(id, 0.0); + } + + // The tracker races 100 s ahead. Nothing has voted yet, so nothing may die: + // an unvoted track is not a finished track, it is an unanswered question. + { auto s = reg.begin_frame(100.0); (void)s; } + CHECK(sink.claims.empty()); + + // A vote arriving very late still lands, because the track is still there. + reg.observe(id, /*actor*/ 3, /*posterior*/ 0.99f, axis(1)); + CHECK(reg.dropped_votes() == 0); + + // Only once the evidence clock passes last_seen + extinction does it close. + reg.advance_evidence(4.0); + CHECK(sink.claims.empty()); + reg.advance_evidence(6.0); + REQUIRE(sink.claims.size() == 1); + CHECK(sink.claims[0].actor_idx == 3); + // AR-013 still holds: the window ends at the last sighting, never at the + // moment of death, and never at the watermark that authorised it. + CHECK(sink.claims[0].last_seen == 0.0); +} + +/// TRACES: UT-001 | AR-008, AR-013 | SR-002 +TEST_CASE("a track retired from association is still open to evidence", + "[registry][AR-008]") { + // The two clocks answer different questions and must not share an answer. + // Association asks "may this detection link to that track?" on the tracker's + // clock; reaping asks "is that track finished?" and cannot answer until the + // votes are in. Deferring both to the evidence clock was the second half of + // this bug: retired tracks lingered in the candidate pool for as long as the + // matcher lagged, so a new face re-associated onto a long-dead track and two + // people merged into one window. + TrackRegistry reg(cfg(/*extinction=*/5.0), disc()); + Sink sink; sink.attach(reg); + reg.expect_evidence(); + + int id; + { + auto s = reg.begin_frame(0.0); + id = s.create(0.0, axis(1)); + s.mark_lost(id, 0.0); + } + + { + auto s = reg.begin_frame(3.0); // inside the window + CHECK(s.candidates().size() == 1); // still associable + } + { + auto s = reg.begin_frame(50.0); // far outside it + CHECK(s.candidates().empty()); // retired from association... + } + // ...but not gone, and still able to receive the votes in flight for it. + reg.observe(id, 7, 0.99f, axis(1)); + CHECK(reg.dropped_votes() == 0); + reg.advance_evidence(50.0); + REQUIRE(sink.claims.size() == 1); + CHECK(sink.claims[0].actor_idx == 7); +}