recon_gen.common.spine.anomaly

Anomaly family — windowed-statistical L2 invariant + generator.

Promoted from tests/unit/test_at0_anomaly_full_spine.py (AT.0 spike). The matview <prefix>_inv_pair_rolling_anomalies computes a rolling 2-day SUM per (sender, recipient) pair, then z-scores against the population mean+stddev of all pair-windows. The AnomalyInvariant detector projects EVERY (pair, window_end) row as a Violation; the AnomalyView (AT.2, anomaly_view.py) slices on σ threshold.

Per AP.3 finding #2: statistical invariants CAN’T be generated from a single row — they need a POPULATION + a spike. AnomalyGenerator plants N baseline pairs (small uniform amounts) + 1 spike pair (large amount between the target sender + recipient). The spike’s z-score against the population distribution → high σ bucket → detector fires.

AT.0 finding (caught mid-spike, encoded as default + docstring): the spike’s z-score is REDUCED by its own contribution to the mean (outlier self-shift). With small baselines (e.g. 8 pairs) + 100k spike, z ≈ 2.67 (too low to fire 3σ). Default baseline_pair_count=100 dilutes the outlier effect to ~1% → z ≈ 9.95 (clearly ‘4+ sigma’).

AT.3 refactored emit() onto the Transfer / LedgerSimulation primitive — every leg pair goes through the same _emit_transfer path that MoneyTrailGenerator uses. Single-edge property preserved (transfers-only ledger → no balance rows → no drift trip). The detector + scenario_for are stable across the refactor.

Classes

AnomalyGenerator(sender_account_id, ...[, ...])

Plant a baseline distribution + a spike between sender ↔ recipient.

AnomalyInvariant([prefix])

Pair-rolling-anomaly detector.

class recon_gen.common.spine.anomaly.AnomalyGenerator(sender_account_id, sender_account_role, sender_account_parent_role, recipient_account_id, recipient_account_role, recipient_account_parent_role, anchor_day, spike_magnitude, baseline_pair_count, baseline_amount, prefix='spec_example')[source]

Bases: object

Plant a baseline distribution + a spike between sender ↔ recipient.

Emits baseline_pair_count extra pairs of background accounts with small uniform amounts on the anchor day (populates the matview’s pop_stddev) plus ONE spike pair (sender → recipient) with spike_magnitude (sits far above baseline → high z-score → fires).

Per AP.3 finding #2 (statistical invariants are multi-row by nature): the generator’s emit() writes ALL the rows in one call — the Protocol stays minimal; the per-row-iterator shape isn’t pushed onto the Generator contract.

AT.3 refactor: pairs are now emitted as Transfer`s through a transfers-only `LedgerSimulation. Single-edge property preserved (no AccountSimulation folds → no balance rows → no drift trip). Each baseline pair = one Posted 2-leg balanced Transfer; the spike is the same shape with spike_magnitude. Shape is identical to MoneyTrailGenerator’s — both consume the AT.3 primitive.

Parameters:
  • sender_account_id (str)

  • sender_account_role (str)

  • sender_account_parent_role (str | None)

  • recipient_account_id (str)

  • recipient_account_role (str)

  • recipient_account_parent_role (str)

  • anchor_day (date)

  • spike_magnitude (float)

  • baseline_pair_count (int)

  • baseline_amount (float)

  • prefix (str)

anchor_day: date
baseline_amount: float
baseline_pair_count: int
property claimed_accounts: frozenset[str]

The 2 + 2*baseline_pair_count account_ids this plant touches: the spike pair + every baseline pair’s sender/recipient. Used by AV.5 ScenarioContext.compose to catch cross-generator collisions at the wiring site.

emit(conn, *, scenario_id=None)[source]
Return type:

None

Parameters:
  • conn (SyncConnection)

  • scenario_id (str | None)

property intended: RuleViolation
prefix: str = 'spec_example'
recipient_account_id: str
recipient_account_parent_role: str
recipient_account_role: str
sender_account_id: str
sender_account_parent_role: str | None
sender_account_role: str
spike_magnitude: float
class recon_gen.common.spine.anomaly.AnomalyInvariant(prefix='spec_example')[source]

Bases: object

Pair-rolling-anomaly detector. Reads <prefix>_inv_pair_rolling_anomalies and projects EVERY row as a Violation — every (pair, window_end) the matview computed, across every z_bucket (including ‘0-1 sigma’ background).

Per AP.3 finding #3, the σ threshold belongs on the View, not the detector. AT.2 promoted AnomalyView (anomaly_view.py) that slices over the detected violation set on sigma_threshold. The detector here is now bucket-agnostic — AnomalyView(3.0).slice(…) reproduces AT.1’s behaviour exactly; other thresholds (2.0 for deep-dive triage, etc.) work over the same detect() result with no re-query.

Parameters:

prefix (str)

detect(conn)[source]
Return type:

set[Violation]

Parameters:

conn (SyncConnection)

name: ClassVar[str] = 'inv_pair_rolling_anomalies'
prefix: str = 'spec_example'
scenario_for(sender_role, recipient_role, *, spike_magnitude=100000.0, baseline_pair_count=100, baseline_amount=100.0, anchor_day=datetime.date(2030, 1, 1), instance=None, sender_account_id=None, recipient_account_id=None)[source]

Resolve sender + recipient roles; return a generator that plants baseline_pair_count baseline pairs + 1 spike between sender + recipient.

See AT.0 spike’s docstring for the full statistical-coverage argument. The defaults (100 baseline / 100_000 spike) give a clear ~10σ separation; tweak for tests that explore the threshold boundary (set spike=baseline to defuse).

Raises ValueError if either role is missing from the shape’s internal accounts (sender) or leaf internal accounts (recipient — the matview’s recipient filter requires account_parent_role IS NOT NULL).

AY.4.c — sender_account_id / recipient_account_id override the default synthetic IDs. The plant adapter (AY.4.c.3) threads OLD AnomalyPlant account_ids through these kwargs so N anomaly plants on the same (sender_role, recipient_role) pair produce N distinct generators (the default f”acct-anomaly-{sender,recipient}-{role}” derivations would collide). Existing test callers can pass nothing → preserves the synthetic defaults byte-stable.

Return type:

AnomalyGenerator

Parameters:
  • sender_role (str)

  • recipient_role (str)

  • spike_magnitude (float)

  • baseline_pair_count (int)

  • baseline_amount (float)

  • anchor_day (date)

  • instance (L2Instance | None)

  • sender_account_id (str | None)

  • recipient_account_id (str | None)