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) {
|
||||
|
||||
@@ -144,7 +144,18 @@ struct IdentityMatcherFunc {
|
||||
// mix embeddings from two viewpoints under one buffer, so we still drop
|
||||
// every diversity buffer here — a revived track simply re-accumulates its
|
||||
// buffer from post-cut frames. Stale cross-cut embeddings are never promoted.
|
||||
if (tf.source.is_cut) track_gallery_.clear_tracks();
|
||||
/// TRACES: AR-019 | SR-005
|
||||
// Promotion may only borrow same-identity evidence from a span where
|
||||
// identity is certain, so ALL THREE discontinuity signals clear the
|
||||
// buffers, not just the histogram cut:
|
||||
// is_cut — camera-angle change
|
||||
// is_scene_boundary — different scene (AR-010; previously never set,
|
||||
// so this half of the gate was dead)
|
||||
// The third, an identity contradiction (AR-015), is enforced by the
|
||||
// registry: a track whose belief swapped is closed outright, so it can
|
||||
// no longer promote anything.
|
||||
if (tf.source.is_cut || tf.source.is_scene_boundary)
|
||||
track_gallery_.clear_tracks();
|
||||
|
||||
const int n_faces = static_cast<int>(tf.embeddings.size());
|
||||
std::vector<IdentifiedActor> actors;
|
||||
@@ -271,6 +282,16 @@ struct IdentityMatcherFunc {
|
||||
registry_->observe(tf.track_ids[fi], best_actor, p, tf.embeddings[fi]);
|
||||
}
|
||||
|
||||
// TRACES: AR-019 | SR-005
|
||||
// Ownership is the registry's, computed once. TrackGallery used to
|
||||
// tally its own plurality vote over accepted frames, which meant two
|
||||
// different answers to "who is this track" could coexist — and the
|
||||
// expansion one ignored the Bayesian accumulation entirely.
|
||||
if (registry_ && tf.track_ids[fi] >= 0) {
|
||||
if (auto owner = registry_->owner(tf.track_ids[fi]))
|
||||
track_gallery_.set_owner(tf.track_ids[fi], *owner);
|
||||
}
|
||||
|
||||
track_gallery_.observe(tf.track_ids[fi], tf.embeddings[fi],
|
||||
best_actor, best_s, accept, tf.crops[fi]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user