feat: expansion promotion gated on all three discontinuity signals
AR-019 — promotion may only borrow same-identity evidence from a span where identity is certain, so every discontinuity signal now clears the buffers rather than just the histogram cut. is_scene_boundary was already named in the gate but never set by anything, so that half of it was dead until AR-010 gave it a producer. It now does what the spec always said. The third signal, an identity contradiction, needs no code here: AR-015 closes a track whose belief swapped, so it can no longer promote. Ownership now comes from the registry rather than a second tally. TrackGallery was computing its own plurality vote over accepted frames, which meant two different answers to "who is this track" could coexist in one run — and the expansion one ignored the Bayesian accumulation entirely, weighting thirty near-identical looks the same as thirty distinct ones. The local tally survives only as a fallback for callers with no registry attached, which is the unit tests and the replay harness. Suite: 92 cases, 6133 assertions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: AR-019, AR-010, AR-015 | SR-005
This commit is contained in:
@@ -119,6 +119,16 @@ struct TrackGallery {
|
||||
// matcher when it observes a cut or track disappearance.
|
||||
void forget(int track_id) { tracks_.erase(track_id); }
|
||||
|
||||
/// TRACES: AR-019 | SR-005
|
||||
/// The registry's verdict on who this track is. Authoritative: it comes from
|
||||
/// the Bayesian accumulation (AR-025), where the local tally counted raw
|
||||
/// accepted frames and so weighted thirty near-identical looks the same as
|
||||
/// thirty distinct ones.
|
||||
void set_owner(int track_id, int actor_idx) {
|
||||
if (track_id < 0 || actor_idx < 0) return;
|
||||
tracks_[track_id].registry_owner = actor_idx;
|
||||
}
|
||||
|
||||
/// TRACES: AR-024 | SR-005
|
||||
/// Supply the calibration belonging to the active embedder. Without it the
|
||||
/// band falls back to treating cosine as probability, which is wrong but
|
||||
@@ -145,6 +155,7 @@ private:
|
||||
std::map<int, int> actor_votes; // actor_idx → accepted-frame count
|
||||
int accepted_frames{0};
|
||||
bool promoted{false};
|
||||
int registry_owner{-1}; ///< AR-019: authoritative
|
||||
};
|
||||
|
||||
/// TRACES: AR-018, AR-024 | SR-005
|
||||
@@ -210,7 +221,7 @@ private:
|
||||
void promote(int track_id, TrackState& ts) {
|
||||
ts.promoted = true; // idempotent: never promote a track twice
|
||||
|
||||
int actor = plurality_actor(ts);
|
||||
int actor = owning_actor(ts);
|
||||
if (actor < 0) return;
|
||||
|
||||
// ── Safety gate: internal spread ─────────────────────────────────────
|
||||
@@ -246,6 +257,13 @@ private:
|
||||
<< annex_.size() << "\n";
|
||||
}
|
||||
|
||||
/// Prefer the registry's verdict; fall back to the local tally only when no
|
||||
/// registry is attached (unit tests, replay harness).
|
||||
static int owning_actor(const TrackState& ts) {
|
||||
if (ts.registry_owner >= 0) return ts.registry_owner;
|
||||
return plurality_actor(ts);
|
||||
}
|
||||
|
||||
static int plurality_actor(const TrackState& ts) {
|
||||
int best = -1, best_votes = 0;
|
||||
for (const auto& [ai, v] : ts.actor_votes) {
|
||||
|
||||
Reference in New Issue
Block a user