feat(ar-024): enforce the invariant statically, and delete the fallback it caught
AR-024's register row gives its verification tier as "Static check -- no bare cosine outside a tagged EXCEPTION". No such check existed, so the invariant was enforced by reading, and reading had missed a live violation. scripts/ci/check_raw_cosine.py is that check, wired into the traceability workflow as a blocking step. It is honest about its reach: it catches direct cosine_similarity() uses not routed through a calibration, and it cannot follow a cosine through a variable across statements. That limit is documented in the script rather than left for someone to discover after trusting a pass. What it caught, and what this commit removes with it: The identity matcher's no-calibration fallback thresholded raw cosine distance (match_threshold) plus a ratio test (match_ratio, match_ratio_ceil). Worse than the invariant breach: it fed max(0, cosine) into TrackRegistry::observe, whose contract reads "posterior is a calibrated probability, never a raw cosine (AR-024) ... so the accumulation cannot be fed an uncalibrated number by a careless caller". It could, and did. And it disagreed with the rest of the pipeline about what "the fit failed" means -- same_person_probability answers that with the untuned default sigmoid and a loud warning, so association stayed in probability space while matching alone left it. One run, two policies, no announcement. Now one rule: cal_.probability() always, with a warning when the fit is not real. A worse answer than a fitted calibration, a better one than a number whose units nothing else shares. TrackGallery::set_calibration is mandatory for the same reason. Its default was max(0, cosine), which made expand_band_lo = 0.90 mean "cosine > 0.9" in a test and "P(same person) > 0.9" in production. FaceTrackerFunc already threw without one; the expansion store now matches. One exception is recorded, in the calibration's own dedup. It is not a close call: at 1 - 1e-7 it asks whether two vectors are the same vector, and it runs on the fit's input, so a calibrated comparison there would have to be calibrated by the fit it is feeding. Also drops seven dead keys from the optimizer's CFG_KEYS. Config keys are read with a contains() check, so each one had been silently inert since the field behind it was deleted -- a sweep varying one of them measured nothing and reported an ordinary-looking F1. TRACES: AR-024, AR-023 | SR-002
This commit is contained in:
@@ -162,6 +162,19 @@ inline GalleryCalibration calibrate_gallery(
|
||||
for (const auto& e : by_actor[ai]) {
|
||||
bool dup = false;
|
||||
for (const auto& k : kept) {
|
||||
// EXCEPTION: AR-024 this asks whether two vectors are THE SAME
|
||||
// VECTOR, not whether two faces are the same person.
|
||||
//
|
||||
// Two independent reasons, either sufficient. First, at
|
||||
// 1 - 1e-7 the threshold is a floating-point identity test: it
|
||||
// catches one source image embedded twice, and no genuine pair
|
||||
// of distinct photographs lands there. Nothing about it is a
|
||||
// decision, so there is nothing for a probability to mean.
|
||||
//
|
||||
// Second, and structurally: this IS the calibration fit. The
|
||||
// dedup runs on its input, before (a, b) exist. A calibrated
|
||||
// comparison here would have to be calibrated by the fit it is
|
||||
// feeding, which is not a thing that can be arranged.
|
||||
if (cosine_similarity(e, k) > kDedupSimThreshold) { dup = true; break; }
|
||||
}
|
||||
if (!dup) kept.push_back(e);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -163,10 +164,20 @@ struct TrackGallery {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// bounded — and the default is loud in the header rather than silent.
|
||||
void set_calibration(std::function<float(float)> c) { calibrate_ = std::move(c); }
|
||||
/// Supply the calibration belonging to the active embedder.
|
||||
///
|
||||
/// Required, not optional. The default used to be `max(0, cosine)` — a raw
|
||||
/// cosine worn as a probability, which made `expand_band_lo = 0.90` mean
|
||||
/// "cosine above 0.9" in a test and "P(same person) above 0.9" in
|
||||
/// production. Those are wildly different gates, and nothing announced the
|
||||
/// switch. `FaceTrackerFunc` already refuses to construct without a
|
||||
/// calibration for the same reason; this now matches it.
|
||||
void set_calibration(std::function<float(float)> c) {
|
||||
if (!c) throw std::invalid_argument(
|
||||
"track_gallery: a calibration is required — the admission band is "
|
||||
"expressed in probability space (AR-024)");
|
||||
calibrate_ = std::move(c);
|
||||
}
|
||||
|
||||
/// Embeddings the band refused. A store that admits nothing is as wrong as
|
||||
/// one that admits everything, and neither is visible without this.
|
||||
@@ -341,8 +352,9 @@ private:
|
||||
}
|
||||
|
||||
/// cosine → P(same person). The one probability space the pipeline reasons
|
||||
/// in; see gallery_calibration.hpp's same_person_probability.
|
||||
std::function<float(float)> calibrate_{[](float c) { return std::max(0.f, c); }};
|
||||
/// in; see gallery_calibration.hpp's same_person_probability. Never default
|
||||
/// constructed to an identity-ish stand-in — see set_calibration.
|
||||
std::function<float(float)> calibrate_;
|
||||
std::size_t rejected_{0}; ///< admissions refused by the band
|
||||
|
||||
bool enabled_;
|
||||
|
||||
Reference in New Issue
Block a user