recon_gen.common.spine.inv_fanout

Investigation fanout (seed-color) family — ViolationGenerator only.

AY.2.b promotion of common/l2/seed.py::InvFanoutPlant (N.4.h fuzzer Investigation coverage). This is a SEED-COLOR generator: it plants N two-leg transfers (each sender → one shared recipient) on the anchor day so the Investigation dashboard’s inv_money_trail_edges + inv_pair_rolling_anomalies matviews have data to operate on.

Per the AY.2.b evidence-currency layering:

  • intended returns a CoverageObservation keyed on (recipient_account_id, sender_count, rail_name, anchor_day). The seed claims “I planted a healthy N-sender fanout to recipient R.”

  • Edge to MoneyTrailInvariant (registered): every emitted transfer has transfer_parent_id=NULL → each is a depth-0 (root) edge the inv_money_trail_edges recursive CTE surfaces. The registered edge documents this deterministic side-effect; it does NOT make InvFanoutGenerator a “rule violation” generator (the primary intent is still seed-color coverage). AU.5’s gate permits coverage generators to carry edges; the bucket membership (ALL_COVERAGE_GENERATORS) is the discriminator on “this generator’s primary purpose.”

  • NOT registered for AnomalyInvariant: the rolling-window z-score is probabilistic. A 2-sender × $100 plant on a fresh DB won’t pop the bucket threshold (the OLD boost_inv_fanout_plants (5×) deliberately multiplies to push it over). The minimal- viable smart constructor stays under threshold; a deferred scenario_for_anomaly_spike(…) variant would emit at higher density + carry an Anomaly edge.

Recipient is caller-supplied (the factory provides a deterministic synthetic default + the leaf-internal fields the inv_pair_rolling_anomalies filter requires: account_scope=’internal’ AND account_parent_role IS NOT NULL).

Classes

InvFanoutFactory([prefix])

Smart constructor namespace for InvFanoutGenerator.

InvFanoutGenerator(recipient_account_id, ...)

Emit N two-leg transfers (one per sender) all crediting one leaf-internal recipient on the anchor day.

class recon_gen.common.spine.inv_fanout.InvFanoutFactory(prefix='spec_example')[source]

Bases: object

Smart constructor namespace for InvFanoutGenerator.

No L2-instance resolution needed — the Investigation matviews don’t read rail/template declarations (they walk transactions by account_scope + account_parent_role + transfer_id). The factory just supplies sensible defaults + synthetic accounts.

Parameters:

prefix (str)

name: ClassVar[str] = 'inv_fanout'
prefix: str = 'spec_example'
scenario_for_fanout(*, sender_count=2, recipient_account_id='acct-inv-fanout-recipient', rail_name='_spine_plant', amount_per_transfer=100.0, anchor_day=datetime.date(2030, 1, 1))[source]

Build a generator that plants sender_count distinct senders all crediting one recipient on the anchor day.

Defaults are minimal-viable: 2 senders × $100 stays under the anomaly z-score threshold (the OLD plant’s boost_inv_ fanout_plants(5×) multiplies for anomaly density; the spine generator delegates that decision to the picker layer).

Return type:

InvFanoutGenerator

Parameters:
  • sender_count (int)

  • recipient_account_id (str)

  • rail_name (str)

  • amount_per_transfer (float)

  • anchor_day (date)

class recon_gen.common.spine.inv_fanout.InvFanoutGenerator(recipient_account_id, sender_account_ids, rail_name, amount_per_transfer, anchor_day, prefix='spec_example', recipient_role='CustomerSubledger', recipient_parent_role='CustomerLedger', sender_role='ExternalCounterparty', sender_scope='external', sender_parent_role=None)[source]

Bases: object

Emit N two-leg transfers (one per sender) all crediting one leaf-internal recipient on the anchor day. Each transfer is a debit on the sender + credit on the recipient summing to zero, sharing one transfer_id, both legs stamped with metadata.sender_id / recipient_id for downstream investigation.

Recipient’s account fields (role, parent_role) MUST satisfy the inv_pair_rolling_anomalies matview filter (account_scope=’internal’ AND account_parent_role IS NOT NULL) — defaults below populate them; production picker threads real TemplateInstance.account_id / role tuples post-AY.4.

intended returns a CoverageObservation. Registered edge to MoneyTrailInvariant: each transfer is a depth-0 edge the inv_money_trail_edges recursive CTE surfaces.

Parameters:
  • recipient_account_id (str)

  • sender_account_ids (tuple[str, ...])

  • rail_name (str)

  • amount_per_transfer (float)

  • anchor_day (date)

  • prefix (str)

  • recipient_role (str)

  • recipient_parent_role (str)

  • sender_role (str)

  • sender_scope (str)

  • sender_parent_role (str | None)

amount_per_transfer: float
anchor_day: date
property claimed_accounts: frozenset[str]

recipient + every sender. Two fanouts to the same recipient on the same day would collide via the recipient account_id.

Type:

AV.5 contract

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

None

Parameters:
  • conn (SyncConnection)

  • scenario_id (str | None)

property intended: CoverageObservation

a fanout of sender_count senders to recipient_account_id landed on anchor_day. Identity carries the natural-key tuple a coverage detector would round-trip against if/when one lands.

Type:

Presence evidence

prefix: str = 'spec_example'
rail_name: str
recipient_account_id: str
recipient_parent_role: str = 'CustomerLedger'
recipient_role: str = 'CustomerSubledger'
sender_account_ids: tuple[str, ...]
sender_parent_role: str | None = None
sender_role: str = 'ExternalCounterparty'
sender_scope: str = 'external'
property transfer_ids: tuple[str, ...]

The len(sender_account_ids) transfer_ids this plant emits, in stable sender-sorted order (matches the OLD emitter’s deterministic ordering).