recon_gen.common.spine.anomaly_view

Anomaly View — owns the σ-threshold (AP.3 finding #3 lock).

The AnomalyInvariant detector reads every row in <prefix>_inv_pair_rolling_anomalies — every (pair, window_end) tuple the matview computed, across every z_bucket. The threshold for “is this anomalous?” lives on the View, not the detector. AT.2 lands that separation.

The split mirrors the L1 design (detector returns the matview’s truth; the analyst/View slices over it). Three reasons:

  1. Same data, multiple thresholds. An investigator might want 3σ at triage time but 2σ during deep-dive; the same detect() result feeds both — no re-query.

  2. Threshold isn’t a property of the invariant. The “this is suspicious” judgement is analyst-facing — same as money_trail’s depth-threshold (AT.3 territory) — so it belongs on the View knob the analyst configures, not the math the matview pins.

  3. Composition. A SeverityView could compose threshold + window length + minimum transfer count; AT.2’s AnomalyView is just the threshold knob, but the slice-over-detected-violations shape extends cleanly.

The bucket → σ lower-bound mapping is fixed by the matview SQL (common/l2/schema.py’s anomaly CASE branches); this module re-encodes it so the slice can compare numerically. Tests pin both directions — schema-shape-drift on the bucket vocab fails loud here, not at the analyst’s surface.

Module Attributes

BUCKET_LOWER_BOUNDS

Lower bound (in σ) of each z_bucket label the matview emits.

Classes

AnomalyView([sigma_threshold])

Analyst-facing slice over the anomaly detector's full output.

class recon_gen.common.spine.anomaly_view.AnomalyView(sigma_threshold=3.0)[source]

Bases: object

Analyst-facing slice over the anomaly detector’s full output.

Holds the σ threshold for “include this bucket in the violation set.” A violation with bucket B is included iff BUCKET_LOWER_BOUNDS[B] >= sigma_threshold — i.e. the bucket’s lower edge is at-or-above the threshold. Defaults to 3.0 to match AT.1’s baked-in cutoff (analyst convention: “anomaly” starts at 3σ).

The View is pure (no IO; deterministic on its inputs); the detector still does the SQL read. slice(violations) is the only behaviour — other Views (depth-threshold for money_trail, etc.) will mirror this shape: pure projection over the detector’s output set.

Parameters:

sigma_threshold (float)

sigma_threshold: float = 3.0
slice(violations)[source]

Return the subset of violations whose z_bucket’s lower bound is ≥ sigma_threshold. Violations with no z_bucket key (defensive — non-anomaly invariants would be passed by mistake) are dropped silently; the caller’s job is to pass anomaly violations only.

Bucket strings not in BUCKET_LOWER_BOUNDS raise KeyError — that’s a matview-shape drift signal, not a normal runtime case, so it should fail loud rather than silently drop.

Return type:

set[Violation]

Parameters:

violations (set[Violation])

recon_gen.common.spine.anomaly_view.BUCKET_LOWER_BOUNDS: Final[dict[str, float]] = {'0-1 sigma': 0.0, '1-2 sigma': 1.0, '2-3 sigma': 2.0, '3-4 sigma': 3.0, '4+ sigma': 4.0}

Lower bound (in σ) of each z_bucket label the matview emits. The matview’s CASE rounds |z| down to the nearest integer; ‘4+ sigma’ catches everything ≥ 4. Re-encoded here for the View slice; pinned against the schema by test_anomaly_view_bucket_vocab_matches_matview.