recon_gen.common.l2.seed

Demo-seed primitives for any L2 instance.

Two seed layers compose to produce a full demo’s richness:

  1. Baseline (Phase R, ``emit_baseline_seed``). Walks every declared Rail / Chain / TransferTemplate / AggregatingRail in the L2 instance and emits a healthy 90-day rolling window of “things that are working as intended” — multi-leg transfers, bundled child legs, chain firings, opening-balance funding, per-account daily-balance materialization with weekend / holiday carry-forward (v8.5.4). Materializes AccountTemplate instances at runtime so the integrator only has to declare the template once in YAML. This layer IS what reproduces “a full demo’s richness” — the older “plants only” claim no longer applies.

  2. Plants (``emit_seed``). Layered on top of the baseline. Each ScenarioPlant member is a typed dataclass that emits the minimum rows needed to surface a specific L1 invariant violation (drift / overdraft / limit-breach / stuck-pending / stuck-unbundled / supersession / template-cycle / transfer-template) or an Investigation-side anomaly (recipient fanout). Plant order is sorted by stable keys so data hash can pin the SHA256.

  3. Composed (``emit_full_seed``). Concatenates baseline + plants. This is the entry point data apply calls when an integrator loads their L2 YAML and seeds the demo DB — full baseline + planted exception scenarios in one SQL script.

Loading a scenario via L2 YAML always goes through emit_full_seed (via cli/_helpers.py::build_full_seed_sql), so the integrator gets the full richness on every data apply. The auto-derived plant scenario comes from auto_scenario.default_scenario_for walking the L2 instance — there’s no separate “scenario YAML”; plants fall out of the L2 shape via heuristics.

Public API:

  • Plant dataclasses: TemplateInstance, DriftPlant, OverdraftPlant, LimitBreachPlant, StuckPendingPlant, StuckUnbundledPlant, SupersessionPlant, TransferTemplatePlant, InvFanoutPlant, RailFiringPlant.

  • Container: ScenarioPlant (holds template_instances + every plant tuple + a reference today date).

  • Entry points: - emit_seed(instance, scenarios) — plants only; deterministic

    output for hash-locking.

    • emit_baseline_seed(instance) — 90-day healthy baseline only.

    • emit_full_seed(instance, scenarios) — baseline + plants; what data apply calls.

What this module still deliberately does NOT do:

  • Decide what plants to add. That’s auto_scenario.default_scenario_for(instance) — heuristics that walk the L2 shape and pick plant inputs (which Rail to drift, which LimitSchedule to breach, etc.).

  • Wire dialect-specific schema DDL. That’s schema.emit_schema(instance). The seed assumes the schema shape emit_schema produces.

Functions

emit_baseline_seed(instance, *, prefix[, ...])

Emit a 3-month healthy-baseline INSERT script for the L2 instance.

emit_full_seed(instance, scenarios, *, prefix)

Emit baseline + plants concatenated as a single SQL script.

emit_seed(instance, scenarios, *, prefix[, ...])

Emit the full SQL INSERT script for the planted scenarios.

emit_truncate_sql(instance, *, prefix[, dialect])

Emit TRUNCATE statements for the per-prefix base + matview tables.

Classes

ChainParentDisagreementPlant(...)

A planted L1 violation: two-template chain where leg_rail firings of one child Transfer claim different parent_transfer_id values (AB.2.6 / AB.2.3).

DriftPlant(account_id, days_ago, ...)

A planted (account, business_day) cell where stored balance disagrees with computed balance by delta_money.

ExpectedEodBalancePlant(role, days_ago, ...)

BU.3.1 — A planted (account, business_day) cell whose stored money differs from the per-row expected_eod_balance.

FailedTransactionPlant(account_id, days_ago, ...)

A planted leg with status='Failed' (X.1.i).

FanInChainExtraParentPlant(...)

AB.4.5 plant: fan-in batch with parent set EXCEEDING expected.

FanInChainMissingParentPlant(...)

AB.4.5 plant: fan-in batch with parent set SHORT of expected (orphan / incomplete).

FanInChainPlant(chain_parent_rail_name, ...)

AB.4.5 plant: healthy fan-in chain firing.

InboundCapBreachPlant(account_id, days_ago, ...)

A planted (account, business_day, rail) cell where the daily inbound flow exceeds the configured Inbound LimitSchedule.cap.

InvFanoutPlant(recipient_account_id, ...)

A planted "fanout" — N senders all credit ONE leaf-internal recipient on the same day (N.4.h, fuzzer Investigation coverage).

LedgerDriftPlant(days_ago, delta_money)

A planted (parent_account, business_day) cell where the parent (control) account's stored balance disagrees with Σ children by delta_money.

LimitBreachPlant(account_id, days_ago, ...)

A planted (account, business_day, rail) cell where the daily outbound flow exceeds the configured Outbound LimitSchedule.cap.

MultiXorMissedPlant(chain_parent_rail_name, ...)

AB.6.6 plant: a chain parent firing with ZERO declared XOR siblings firing — matches the AB.6.5 matview's child_count = 0 'missed' branch.

MultiXorOverlapPlant(chain_parent_rail_name, ...)

AB.6.6 plant: a chain parent firing where TWO declared XOR siblings fire — matches the AB.6.5 matview's child_count >= 2 'overlap' branch.

OverdraftPlant(account_id, days_ago, money)

A planted (account, business_day) cell where stored balance is negative.

RailFiringPlant(rail_name, days_ago, ...[, ...])

A planted Posted firing of a single Rail (M.4.2 broad-mode plant kind).

ScenarioPlant(template_instances[, ...])

The full set of planted scenarios + materialized template instances.

StuckPendingPlant(account_id, days_ago, ...)

A planted Pending leg whose age exceeds the rail's max_pending_age.

StuckUnbundledPlant(account_id, days_ago, ...)

A planted Posted leg with bundle_id IS NULL whose age exceeds the rail's max_unbundled_age.

SupersessionPlant(account_id, days_ago, ...)

A planted logical-key (transaction.id) with multiple entry versions, simulating a TechnicalCorrection rewrite of a posted leg.

TemplateInstance(template_role, account_id, name)

One concrete materialization of an AccountTemplate.

TransferTemplatePlant(template_name, ...[, ...])

A planted firing of a declared TransferTemplate.

TwoTemplateChainPlant(...)

A planted healthy two-template chain firing (AB.2.6).

XorVariantMissedFiringPlant(template_name, ...)

AB.3.5 plant: a TransferTemplate Transfer where one XOR group has zero firings — matches the AB.3.3 matview's firing_count = 0 branch.

XorVariantOverlapPlant(template_name, ...)

AB.3.5b plant: a TransferTemplate Transfer where TWO members of one XOR group both fire — matches the AB.3.3 matview's firing_count >= 2 branch.

class recon_gen.common.l2.seed.ChainParentDisagreementPlant(child_template_name, days_ago, parent_a_transfer_id, parent_b_transfer_id)[source]

Bases: object

A planted L1 violation: two-template chain where leg_rail firings of one child Transfer claim different parent_transfer_id values (AB.2.6 / AB.2.3).

First-firing-wins per gap doc §3 means subsequent legs MUST agree on the Parent — disagreement is an ETL bug (parent reference drift, cross-cycle contamination). The emitter generates 2+ leg_rail rows sharing one transfer_id + template_name but assigning different synthetic transfer_parent_id values, so the AB.2.3 matview reads COUNT(DISTINCT parent_transfer_id) > 1 and surfaces the row.

Parameters:
  • child_template_name (Identifier)

  • days_ago (int)

  • parent_a_transfer_id (str)

  • parent_b_transfer_id (str)

child_template_name: Identifier
days_ago: int
parent_a_transfer_id: str
parent_b_transfer_id: str
class recon_gen.common.l2.seed.DriftPlant(account_id, days_ago, delta_money, rail_name, counter_account_id)[source]

Bases: object

A planted (account, business_day) cell where stored balance disagrees with computed balance by delta_money.

Positive delta: stored balance is HIGHER than the sum of postings. Negative delta: stored balance is LOWER than the sum of postings.

Surfaces in the L1 Drift theorem as a non-zero Drift value for that account-day.

Background postings on the drift day come from rail_name (a declared two-leg Rail in the L2 instance); the counter-leg uses counter_account_id (must be a declared external Account in the same instance). Both are resolved from instance at emit time so this dataclass never needs to know about specific persona fixtures.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • delta_money (Decimal)

  • rail_name (Identifier)

  • counter_account_id (Identifier)

account_id: Identifier
counter_account_id: Identifier
days_ago: int
delta_money: Decimal
rail_name: Identifier
class recon_gen.common.l2.seed.ExpectedEodBalancePlant(role, days_ago, expected, variance)[source]

Bases: object

BU.3.1 — A planted (account, business_day) cell whose stored money differs from the per-row expected_eod_balance.

Surfaces in L1’s Expected-EOD-Balance variance matview (<prefix>_expected_eod_balance_breach) — the matview filters expected_eod_balance IS NOT NULL AND money <> expected_eod_balance. The plant emits ONE row into <prefix>_daily_balances with money = expected + variance, expected_eod_balance = expected, so the variance row materializes. NO transactions are emitted — same balance-only shape as OverdraftPlant.

The Trainer adapter (_invoke_expected_eod_balance_breach_plant) picks the role from the L2 (first internal Account); the operator’s only knobs are days_ago + expected + variance.

Per AU.0 / AU.2: a plant on a leaf internal account ALSO trips drift (zero transactions ⇒ Σ legs = 0 ⇒ drift = stored - 0 = expected + variance ≠ 0). This is encoded as the (ExpectedEodBalanceGenerator, DriftInvariant) edge in the spine registry and is intentional for the Trainer demo.

Parameters:
  • role (Identifier)

  • days_ago (int)

  • expected (Decimal)

  • variance (Decimal)

days_ago: int
expected: Decimal
role: Identifier
variance: Decimal
class recon_gen.common.l2.seed.FailedTransactionPlant(account_id, days_ago, rail_name, amount)[source]

Bases: object

A planted leg with status='Failed' (X.1.i).

The L1 schema’s status column is open-set — any string is a valid terminal state. The tool reasons explicitly about Pending / Posted (drives Aging, Conservation, Completion); every other status (Failed, Cancelled, Rejected, …) is collapsed to Other in the L2FT Rails dataset’s CASE projection so the static dropdown enum matches what the column produces.

Plant a Failed leg per scenario so the dropdown’s Other option has matching seed rows — the X.1.g per-dropdown e2e test asserts every advertised value narrows the table to a non-empty subset, and a status enum without a corresponding plant tripped the test on first deploy.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • amount (Decimal)

account_id: Identifier
amount: Decimal
days_ago: int
rail_name: Identifier
class recon_gen.common.l2.seed.FanInChainExtraParentPlant(chain_parent_rail_name, child_template_name, days_ago, parent_count)[source]

Bases: object

AB.4.5 plant: fan-in batch with parent set EXCEEDING expected.

Emits parent_count parent firings (more than the chain’s expected_parent_count) sharing one child Transfer. The AB.4.7 matview reads parent_count > expected and emits a row with disagreement_kind='extra'. Models the ETL bug where an unrelated parent firing claimed membership in a batch it shouldn’t have been part of — cross-batch contamination or stale parent reference.

Only meaningful when the chain declares expected_parent_count (otherwise the matview has no upper bound to flag against; the picker drops this plant when expected is unset).

Parameters:
  • chain_parent_rail_name (Identifier)

  • child_template_name (Identifier)

  • days_ago (int)

  • parent_count (int)

chain_parent_rail_name: Identifier
child_template_name: Identifier
days_ago: int
parent_count: int
class recon_gen.common.l2.seed.FanInChainMissingParentPlant(chain_parent_rail_name, child_template_name, days_ago, parent_count)[source]

Bases: object

AB.4.5 plant: fan-in batch with parent set SHORT of expected (orphan / incomplete).

Emits parent_count parent firings (less than the chain’s expected_parent_count) sharing one child Transfer. The AB.4.7 matview reads parent_count < expected and emits a row with disagreement_kind='missing' (or 'orphan' if parent_count falls to 1 — the AB.4.0 lock’s fallback when expected is unset). Models the ETL bug where a parent contribution never lands — e.g., one daily settlement of a monthly payout batch failed to post but the batch still closed.

Parameters:
  • chain_parent_rail_name (Identifier)

  • child_template_name (Identifier)

  • days_ago (int)

  • parent_count (int)

chain_parent_rail_name: Identifier
child_template_name: Identifier
days_ago: int
parent_count: int
class recon_gen.common.l2.seed.FanInChainPlant(chain_parent_rail_name, child_template_name, days_ago, parent_count)[source]

Bases: object

AB.4.5 plant: healthy fan-in chain firing.

Per AB.4.0 lock: N parent firings share one child Transfer (the batched-payout pattern). The healthy case has parent_count = chain’s expected_parent_count (or any value ≥2 when the chain leaves expected_parent_count unset for variable-batch flows). The AB.4.7 _fan_in_disagreement matview reads parent_count == expected (or parent_count >= 2 unset) and emits no violation row — purpose is positive demo coverage.

Parameters:
  • chain_parent_rail_name (Identifier)

  • child_template_name (Identifier)

  • days_ago (int)

  • parent_count (int)

chain_parent_rail_name: Identifier
child_template_name: Identifier
days_ago: int
parent_count: int
class recon_gen.common.l2.seed.InboundCapBreachPlant(account_id, days_ago, rail_name, amount, counter_account_id)[source]

Bases: object

A planted (account, business_day, rail) cell where the daily inbound flow exceeds the configured Inbound LimitSchedule.cap.

Mirror of LimitBreachPlant for the AB.1 Inbound direction — surfaces in L1’s Limit Breach SHOULD-constraint when InboundFlow(account, rail, day) > limit (typical AML / structuring threshold). The breaching credit posts on the customer side (money IN); the counter-leg debits the external account (the funds-source). Direction column on the matview row will read 'Inbound' so the dashboard / audit can distinguish.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • amount (Decimal)

  • counter_account_id (Identifier)

account_id: Identifier
amount: Decimal
counter_account_id: Identifier
days_ago: int
rail_name: Identifier
class recon_gen.common.l2.seed.InvFanoutPlant(recipient_account_id, sender_account_ids, days_ago, rail_name, amount_per_transfer)[source]

Bases: object

A planted “fanout” — N senders all credit ONE leaf-internal recipient on the same day (N.4.h, fuzzer Investigation coverage).

Drives the Investigation matview surface (N.3.b): - <prefix>_inv_pair_rolling_anomalies — N (sender, recipient,

day) pair-rolling rows; the recipient survives the matview’s account_scope='internal' AND account_parent_role IS NOT NULL filter so the rolling-window aggregation has data to operate on.

  • <prefix>_inv_money_trail_edges — N depth-0 (root) edges from sender → recipient via the recursive-CTE walk over transfer_parent_id.

Each “transfer” is a 2-leg multi-leg event (debit on sender + credit on recipient summing to zero) so the matview’s signed_amount JOIN finds matched legs. rail_name is one declared rail (Z.B 2026-05-15: rail name IS the type identifier after the symmetric collapse).

Recipient MUST resolve to a leaf-internal account (a TemplateInstance materialized from an AccountTemplate with a non-NULL parent_role) — the matview’s recipient-side filter requires it. Senders MAY be external counterparties or singleton internals; the emitter denormalizes their account fields onto the sender legs without further validation.

Parameters:
  • recipient_account_id (Identifier)

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

  • days_ago (int)

  • rail_name (Identifier)

  • amount_per_transfer (Decimal)

amount_per_transfer: Decimal
days_ago: int
rail_name: Identifier
recipient_account_id: Identifier
sender_account_ids: tuple[Identifier, ...]
class recon_gen.common.l2.seed.LedgerDriftPlant(days_ago, delta_money)[source]

Bases: object

A planted (parent_account, business_day) cell where the parent (control) account’s stored balance disagrees with Σ children by delta_money.

Distinct from DriftPlant (sub-ledger drift on a LEAF): this fires the parent-level conservation invariant. Real-world example: an operator manually adjusts the DDAControl GL stored balance without posting matching leg adjustments to the customer DDAs — the control account diverges from Σ customers even though every customer’s individual sub-ledger still balances.

The Trainer adapter (_invoke_ledger_drift_plant()) plants a synthetic parent+child pair under a UNIQUE synthetic parent_role so the matview’s computed_ledger_balance for our parent only sums OUR child (no bleed from baseline accounts with matching parent_role). Operator only picks days_ago + delta_money.

Parameters:
  • days_ago (int)

  • delta_money (Decimal)

days_ago: int
delta_money: Decimal
class recon_gen.common.l2.seed.LimitBreachPlant(account_id, days_ago, rail_name, amount, counter_account_id)[source]

Bases: object

A planted (account, business_day, rail) cell where the daily outbound flow exceeds the configured Outbound LimitSchedule.cap.

Surfaces in L1’s Limit Breach SHOULD-constraint when OutboundFlow(account, rail, day) > limit.

The breaching debit posts on the customer side; the counter-leg uses counter_account_id (must be a declared external Account in the same instance), resolved from instance at emit time so this dataclass never hardcodes a specific persona’s counterparty.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • amount (Decimal)

  • counter_account_id (Identifier)

account_id: Identifier
amount: Decimal
counter_account_id: Identifier
days_ago: int
rail_name: Identifier
class recon_gen.common.l2.seed.MultiXorMissedPlant(chain_parent_rail_name, days_ago)[source]

Bases: object

AB.6.6 plant: a chain parent firing with ZERO declared XOR siblings firing — matches the AB.6.5 matview’s child_count = 0 ‘missed’ branch.

Models the ETL bug where chain.md’s “multi-children = exactly one MUST fire” contract was violated: the parent fired but no child followed (all XOR alternatives were dropped on the floor). The AB.6.5 _multi_xor_violation matview reads COUNT(matched_child_name) = 0HAVING <> 1 → row surfaces with disagreement_kind='missed', fired_children=''.

Picker constraint (AB.6.6): the chain has ≥2 non-fan-in children and a Rail (not Template) parent so the plant emitter can synthesize a parent firing without nested-firing logic. Mirrors AB.2.6’s parent-must-be-rail restriction.

Parameters:
  • chain_parent_rail_name (Identifier)

  • days_ago (int)

chain_parent_rail_name: Identifier
days_ago: int
class recon_gen.common.l2.seed.MultiXorOverlapPlant(chain_parent_rail_name, variant_a_child_name, variant_b_child_name, days_ago)[source]

Bases: object

AB.6.6 plant: a chain parent firing where TWO declared XOR siblings fire — matches the AB.6.5 matview’s child_count >= 2 ‘overlap’ branch.

Emits one parent firing (the chain.parent rail) plus child legs for variant_a + variant_b, both with transfer_parent_id set to the parent’s transfer_id. The AB.6.5 matview’s fired_children_distinct CTE picks up both → COUNT = 2HAVING <> 1 → row surfaces with disagreement_kind='overlap', fired_children='<a>,<b>' (concat ordering dialect-specific).

Pairs with MultiXorMissedPlant so the dashboard surfaces BOTH branches of the AB.6.5 matview’s HAVING clause.

Parameters:
  • chain_parent_rail_name (Identifier)

  • variant_a_child_name (Identifier)

  • variant_b_child_name (Identifier)

  • days_ago (int)

chain_parent_rail_name: Identifier
days_ago: int
variant_a_child_name: Identifier
variant_b_child_name: Identifier
class recon_gen.common.l2.seed.OverdraftPlant(account_id, days_ago, money)[source]

Bases: object

A planted (account, business_day) cell where stored balance is negative.

Surfaces in L1’s Non-Negative Stored Balance SHOULD-constraint as a violation for that account-day.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • money (Decimal)

account_id: Identifier
days_ago: int
money: Decimal
class recon_gen.common.l2.seed.RailFiringPlant(rail_name, days_ago, firing_seq, amount, account_id_a, account_id_b=None, transfer_parent_id=None, extra_metadata=(), template_name=None)[source]

Bases: object

A planted Posted firing of a single Rail (M.4.2 broad-mode plant kind).

The L1-invariant plant types only fire rails the auto-scenario picks to surface a SHOULD violation (one drift account, one overdraft account, one limit-breach pair, etc.). Most declared rails see zero firings under that picker — which is correct L2-hygiene behavior but leaves the L2 Flow Tracing dashboard’s Rails / Chains / Transfer Templates sheets reading “dead” for every rail the picker didn’t choose.

Broad mode (M.4.2) plants additional ordinary firings — no SHOULD violation, just “this rail fired, here’s the data” — across every declared rail whose role(s) actually resolve to a materialized account. The L1 surface stays clean (no new drift / overdraft / breach rows); the L2 surface gains visible content.

Two-leg rails plant 2 legs (debit on account_id_a, credit on account_id_b); single-leg rails plant 1 leg (on account_id_a, direction per Rail.leg_direction). account_id_b is None for single-leg rails.

transfer_parent_id is set when this firing is the child end of a Required chain entry — points at one of the parent rail’s ``transfer_id``s so the L1 invariant view’s chain-orphan detection sees a matched pair. Defaults to None for standalone firings.

extra_metadata carries values for rail.metadata_keys fields NOT auto-derived from a containing TransferTemplate’s transfer_key. The emit helper unions them with auto-derived TransferKey values so the resulting JSON column is well-formed for the L2 Flow Tracing metadata cascade.

template_name (M.4.2a) is set when this firing’s rail is a leg_rails entry of some TransferTemplate — the L2 Flow Tracing tt-instances + tt-legs datasets read rows by template_name, so leg-rail broad firings need this field populated to surface on the Transfer Templates sheet alongside the structured TransferTemplatePlant firings. None for standalone rails (most of them).

Parameters:
  • rail_name (Identifier)

  • days_ago (int)

  • firing_seq (int)

  • amount (Decimal)

  • account_id_a (Identifier)

  • account_id_b (Identifier | None)

  • transfer_parent_id (str | None)

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

  • template_name (Identifier | None)

account_id_a: Identifier
account_id_b: Identifier | None
amount: Decimal
days_ago: int
extra_metadata: tuple[tuple[str, str], ...]
firing_seq: int
rail_name: Identifier
template_name: Identifier | None
transfer_parent_id: str | None
class recon_gen.common.l2.seed.ScenarioPlant(template_instances, drift_plants=(), ledger_drift_plants=(), overdraft_plants=(), limit_breach_plants=(), inbound_cap_breach_plants=(), expected_eod_balance_plants=(), two_template_chain_plants=(), chain_parent_disagreement_plants=(), xor_variant_missed_firing_plants=(), xor_variant_overlap_plants=(), fan_in_chain_plants=(), fan_in_chain_missing_parent_plants=(), fan_in_chain_extra_parent_plants=(), multi_xor_missed_plants=(), multi_xor_overlap_plants=(), stuck_pending_plants=(), failed_transaction_plants=(), stuck_unbundled_plants=(), supersession_plants=(), transfer_template_plants=(), rail_firing_plants=(), inv_fanout_plants=(), today=<factory>)[source]

Bases: object

The full set of planted scenarios + materialized template instances.

Defaults to today (UTC midnight) as the reference date; days_ago on each plant subtracts from this.

Parameters:
chain_parent_disagreement_plants: tuple[ChainParentDisagreementPlant, ...]
drift_plants: tuple[DriftPlant, ...]
expected_eod_balance_plants: tuple[ExpectedEodBalancePlant, ...]
failed_transaction_plants: tuple[FailedTransactionPlant, ...]
fan_in_chain_extra_parent_plants: tuple[FanInChainExtraParentPlant, ...]
fan_in_chain_missing_parent_plants: tuple[FanInChainMissingParentPlant, ...]
fan_in_chain_plants: tuple[FanInChainPlant, ...]
inbound_cap_breach_plants: tuple[InboundCapBreachPlant, ...]
inv_fanout_plants: tuple[InvFanoutPlant, ...]
ledger_drift_plants: tuple[LedgerDriftPlant, ...]
limit_breach_plants: tuple[LimitBreachPlant, ...]
multi_xor_missed_plants: tuple[MultiXorMissedPlant, ...]
multi_xor_overlap_plants: tuple[MultiXorOverlapPlant, ...]
overdraft_plants: tuple[OverdraftPlant, ...]
rail_firing_plants: tuple[RailFiringPlant, ...]
stuck_pending_plants: tuple[StuckPendingPlant, ...]
stuck_unbundled_plants: tuple[StuckUnbundledPlant, ...]
supersession_plants: tuple[SupersessionPlant, ...]
template_instances: tuple[TemplateInstance, ...]
today: date
transfer_template_plants: tuple[TransferTemplatePlant, ...]
two_template_chain_plants: tuple[TwoTemplateChainPlant, ...]
xor_variant_missed_firing_plants: tuple[XorVariantMissedFiringPlant, ...]
xor_variant_overlap_plants: tuple[XorVariantOverlapPlant, ...]
class recon_gen.common.l2.seed.StuckPendingPlant(account_id, days_ago, rail_name, amount)[source]

Bases: object

A planted Pending leg whose age exceeds the rail’s max_pending_age.

Surfaces in L1’s <prefix>_stuck_pending view (M.2b.8) when EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - posting)) > rail.max_pending_age_seconds. Pick a rail with a max_pending_age set + a days_ago value comfortably past the cap.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • amount (Decimal)

account_id: Identifier
amount: Decimal
days_ago: int
rail_name: Identifier
class recon_gen.common.l2.seed.StuckUnbundledPlant(account_id, days_ago, rail_name, amount)[source]

Bases: object

A planted Posted leg with bundle_id IS NULL whose age exceeds the rail’s max_unbundled_age.

Surfaces in L1’s <prefix>_stuck_unbundled view (M.2b.9) when the leg’s age past posting exceeds the per-rail cap. Per validator R8, the rail MUST appear in some AggregatingRail’s bundles_activity — the seed picks a rail that satisfies this.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • amount (Decimal)

account_id: Identifier
amount: Decimal
days_ago: int
rail_name: Identifier
class recon_gen.common.l2.seed.SupersessionPlant(account_id, days_ago, rail_name, original_amount, corrected_amount)[source]

Bases: object

A planted logical-key (transaction.id) with multiple entry versions, simulating a TechnicalCorrection rewrite of a posted leg.

Surfaces in M.2b.12’s Supersession Audit detail tables. Emits two transaction rows with the same id: the first (“original”) posts original_amount; the second (“correction”) posts corrected_amount a few minutes later carrying supersedes=’TechnicalCorrection’. PostgreSQL’s BIGSERIAL entry column auto-assigns the entry versioning, so the second insert lands at a higher entry value.

Parameters:
  • account_id (Identifier)

  • days_ago (int)

  • rail_name (Identifier)

  • original_amount (Decimal)

  • corrected_amount (Decimal)

account_id: Identifier
corrected_amount: Decimal
days_ago: int
original_amount: Decimal
rail_name: Identifier
class recon_gen.common.l2.seed.TemplateInstance(template_role, account_id, name)[source]

Bases: object

One concrete materialization of an AccountTemplate.

The L2 instance declares the SHAPE of (e.g.) CustomerDDA; this record materializes one concrete customer DDA. The integrator’s ETL is normally responsible for materialization at runtime; for the demo seed we declare them inline.

Parameters:
  • template_role (Identifier)

  • account_id (Identifier)

  • name (Name)

account_id: Identifier
name: Name
template_role: Identifier
class recon_gen.common.l2.seed.TransferTemplatePlant(template_name, days_ago, amount, source_account_id, destination_account_id, firing_seq, chain_children=())[source]

Bases: object

A planted firing of a declared TransferTemplate.

Plants one shared Transfer (single transfer_id) made up of legs whose template_name points back to the template. Each leg carries the same transfer_key metadata values (per SPEC: “every firing of a leg_rails rail with the same transfer_key Metadata values posts to the same shared Transfer”); the seed emits synthetic values keyed off firing_seq so two firings of the same template don’t collapse to one shared Transfer.

M.3.10g first cut handled only TwoLegRail first leg_rails (debit + credit summing to expected_net = 0 in one firing). Extended to also handle SingleLegRail first leg_rails — emits one leg per firing in the rail’s leg_direction (Variable treated as Debit for plant purposes; closing-leg semantics aren’t material to surfacing data on the L2FT TT explorer). Single-leg firings surface as ‘Imbalanced’ against expected_net = 0 (one bare leg can’t sum to zero) — accurate L1 representation of a single-leg cycle without its sibling legs. Multi-leg-per-firing SingleLegRail cycles (e.g. on a shared transfer_id by transfer_key) are still deferred.

source_account_id and destination_account_id may each be either a TemplateInstance.account_id (a materialized customer) OR an L2 Account.id (a singleton or external counterparty). For SingleLegRail templates only source_account_id is used (the leg account); the picker sets destination_account_id to the same value for shape consistency, and the emit helper ignores it. The emit helper resolves each at seed time — so a customer-DDA→external rail and an external→clearing rail both fit this single plant shape.

chain_children (M.3.10h) — a tuple of (child_rail_name, account_id) pairs pre-resolved by the auto-scenario picker. For each pair, the emit helper plants ONE additional child leg whose rail_name is the child + transfer_parent_id points at this plant’s shared transfer_id, so the L2 chain detection SQL sees a matched child for every declared chain edge. Empty tuple = no chain children fire (orphan firing — every declared chain edge surfaces as a missing child). The picker mixes these per template to exercise both matched + orphan code paths in one seed.

Parameters:
  • template_name (Identifier)

  • days_ago (int)

  • amount (Decimal)

  • source_account_id (Identifier)

  • destination_account_id (Identifier)

  • firing_seq (int)

  • chain_children (tuple[tuple[Identifier, Identifier], ...])

amount: Decimal
chain_children: tuple[tuple[Identifier, Identifier], ...]
days_ago: int
destination_account_id: Identifier
firing_seq: int
source_account_id: Identifier
template_name: Identifier
class recon_gen.common.l2.seed.TwoTemplateChainPlant(chain_parent_rail_name, child_template_name, days_ago)[source]

Bases: object

A planted healthy two-template chain firing (AB.2.6).

Generates one parent leg_rail firing + child template leg_rail firings (all sharing one child Transfer per gap doc §3’s first -firing-wins semantic, all carrying the same parent_transfer_id). Cardinality = 1 in the AB.2.3 matview = NO violation row. Gives the L1 dashboard’s PostedRequirements panel + the audit PDF a healthy two-template chain row to display, separate from the probabilistic baseline.

Parameters:
  • chain_parent_rail_name (Identifier)

  • child_template_name (Identifier)

  • days_ago (int)

chain_parent_rail_name: Identifier
child_template_name: Identifier
days_ago: int
class recon_gen.common.l2.seed.XorVariantMissedFiringPlant(template_name, target_xor_group_index, days_ago, witness_rail_name)[source]

Bases: object

AB.3.5 plant: a TransferTemplate Transfer where one XOR group has zero firings — matches the AB.3.3 matview’s firing_count = 0 branch.

Emits a single witness leg_rail row carrying template_name so the synthetic Transfer enters <prefix>_current_transactions (and therefore the matview’s template_transfers universe), but NO member of target_xor_group_index fires for this transfer_id. The matview’s LEFT JOIN finds zero member-rail firings for (transfer_id, template, target_xor_group_index)COUNT(*) = 0HAVING <> 1 → violation row surfaces with fired_rails=''.

Picker constraint: the chosen template MUST have ≥1 leg_rail outside the target XOR group, so the witness is real (a synthetic sentinel rail_name would be ambiguous — could be confused for an undeclared rail). Picker logic in _pick_xor_missed_firing_inputs.

Parameters:
  • template_name (Identifier)

  • target_xor_group_index (int)

  • days_ago (int)

  • witness_rail_name (Identifier)

days_ago: int
target_xor_group_index: int
template_name: Identifier
witness_rail_name: Identifier
class recon_gen.common.l2.seed.XorVariantOverlapPlant(template_name, target_xor_group_index, days_ago, variant_a_rail_name, variant_b_rail_name)[source]

Bases: object

AB.3.5b plant: a TransferTemplate Transfer where TWO members of one XOR group both fire — matches the AB.3.3 matview’s firing_count >= 2 branch.

Emits two leg_rail rows sharing one transfer_id + template _name, both rail_name values being members of target_xor_group_index. The matview’s LEFT JOIN per (transfer, group, member_rail) hits twice → COUNT(*) = 2HAVING <> 1 → row surfaces with fired_rails='<a>,<b>'. Pairs with XorVariantMissedFiringPlant so the demo dashboard surfaces BOTH branches of the matview’s firing_count <> 1 HAVING.

Picker constraint: target group MUST have ≥2 distinct members. Validator C1d already enforces ≥2 at load time, so every declared XOR group qualifies. variant_a and variant_b MUST be distinct members of the targeted group.

Parameters:
  • template_name (Identifier)

  • target_xor_group_index (int)

  • days_ago (int)

  • variant_a_rail_name (Identifier)

  • variant_b_rail_name (Identifier)

days_ago: int
target_xor_group_index: int
template_name: Identifier
variant_a_rail_name: Identifier
variant_b_rail_name: Identifier
recon_gen.common.l2.seed.emit_baseline_seed(instance, *, prefix, window_days=90, anchor=None, dialect=Dialect.POSTGRES, skip_rails=frozenset({}), only_rails=None, base_seed=None)[source]

Emit a 3-month healthy-baseline INSERT script for the L2 instance.

Output shape mirrors emit_seed: one SQL string ready for psycopg2.cursor.execute (Postgres) or cli._execute_script (Oracle). The script targets the same <prefix>_transactions + <prefix>_daily_balances tables the schema emitter creates.

Parameters:
  • instance (L2Instance) – the L2 model instance — every Rail / Chain / TransferTemplate / LimitSchedule it declares becomes runtime evidence in the seed.

  • window_days (int) – rolling window length (default 90 days). Generator emits legs for every business day in [anchor - window_days, anchor].

  • anchor (date | None) – the “today” date the rolling window ends on. Defaults to UTC datetime.now().date() at call time. Pin a specific anchor in tests to keep the SHA256 hash-lock deterministic across runs.

  • dialect (Dialect) – SQL dialect for timestamp literals + INSERT shape (PG vs Oracle). Same flag the legacy emit_seed accepts.

  • skip_rails (frozenset[Identifier (str)]) – X.4.g.10 — rail names to skip in the per-rail leg loop. Used by the deploy pipeline’s scope: uncovered_rails mode to fill baseline only for rails the operator’s external DB hasn’t already populated. Default empty (no rails skipped) keeps byte-identical-to-locked-seeds output.

  • only_rails (frozenset[Identifier (str)] | None) – X.4.i.1 — inverse of skip_rails. When set, ONLY rails whose name appears in the set are emitted; everything else is silently skipped. Used by the deploy pipeline’s scope: only_template mode to emit baseline restricted to the template’s leg-rails dependency closure. None (default) means “no narrowing” (preserves locked-seed byte-identity). Mutually exclusive with skip_rails in spirit but tested independently — if the caller passes both, the rail must survive both filters (in only_rails AND not in skip_rails).

  • base_seed (int | None) – X.4.h.0.b — root RNG seed for the baseline emitter. None (default) uses _BASELINE_BASE_SEED = 42 — the legacy constant the locked seeds were generated against, so the absent-arg case stays byte-identical. Studio’s data-shaping panel writes cfg.test_generator.seed here when the trainer scrubs to a different layout (different seed → different plant positions across days, same seed → byte-identical output). Per-rail RNGs derive from this via the existing _seed_for_rail(rail) = base_seed ^ crc32(rail_name) rule (rename-resilient per-rail isolation preserved).

  • prefix (str)

Return type:

str

Returns:

A SQL script string. R.2.a (this commit) returns a valid header + empty INSERT bodies; R.2.b–e fill in the per-Rail legs, chains, and daily-balance rows.

recon_gen.common.l2.seed.emit_full_seed(instance, scenarios, *, prefix, baseline_window_days=90, anchor=None, dialect=Dialect.POSTGRES, base_seed=None)[source]

Emit baseline + plants concatenated as a single SQL script.

R.3.a — wires R.2’s emit_baseline_seed and the legacy emit_seed together so the deployed demo gets a 3-month healthy baseline with planted exception scenarios layered on top. Plants use independent transfer_ids (tr-drift-*, tr-overdraft-*, etc.), so they never collide with baseline tr-base-* ids.

Parameters:
  • instance (L2Instance) – the L2 model instance.

  • scenarios (ScenarioPlant) – planted scenarios (typically from auto_scenario.default_scenario_for(instance).scenario).

  • baseline_window_days (int) – rolling window length for the baseline.

  • anchor (date | None) – anchor date for the baseline window. Defaults to UTC datetime.now().date(). The plants’ own anchor lives on scenarios.today and may differ — both anchors should normally be the same, set by the caller.

  • dialect (Dialect) – SQL dialect for both layers.

  • base_seed (int | None) – X.4.h.0.b — root RNG seed for the baseline emitter. None (default) preserves byte-identity with the locked seeds (uses _BASELINE_BASE_SEED = 42). Plants are built from deterministic per-kind fixed seeds inside the scenario builder so they’re unaffected; only the 90-day baseline leg / chain / cascade RNGs reseed.

  • prefix (str)

Returns:

baseline INSERTs followed by plant INSERTs, ready for psycopg2.cursor.execute (PG) or cli._execute_script (Oracle).

Return type:

str

recon_gen.common.l2.seed.emit_seed(instance, scenarios, *, prefix, dialect=Dialect.POSTGRES)[source]

Emit the full SQL INSERT script for the planted scenarios.

The output is a single SQL string ready for psycopg2.cursor.execute (Postgres) or for the per-statement runner in cli._execute_script (Oracle, via oracledb’s cursor.execute). Scenarios are emitted in deterministic order (sorted by account_id then days_ago) so the per-dialect hash-lock can pin the output bytes.

P.5.b — emits one INSERT per row, terminated with ;. Both PG and Oracle accept this form. Multi-row INSERT INTO foo VALUES (...), (...) (the M.2 PG-only form) is unsupported on Oracle (which uses INSERT ALL instead); per-row INSERT is the simpler portability choice and the perf cost is negligible for the demo’s ~few-hundred-row scale.

Z.C — prefix is the cfg.db_table_prefix.

Return type:

str

Parameters:
recon_gen.common.l2.seed.emit_truncate_sql(instance, *, prefix, dialect=Dialect.POSTGRES)[source]

Emit TRUNCATE statements for the per-prefix base + matview tables.

Schema-preserving teardown: wipes every row from <prefix>_transactions and <prefix>_daily_balances (the two base tables every dataset reads from). The matviews built on top will become empty on the next REFRESH; no need to TRUNCATE them directly (and Postgres + Oracle have asymmetric semantics for TRUNCATE on matviews anyway).

Postgres uses TRUNCATE ... RESTART IDENTITY CASCADE so the BIGSERIAL entry column resets to 1 on the next INSERT (matches the seed’s deterministic-anchor contract). Oracle has no RESTART IDENTITY syntax — uses plain TRUNCATE TABLE; the integrator can re-create the IDENTITY column if exact serial parity matters. SQLite has no TRUNCATE statement at all — uses DELETE FROM <table> plus a DELETE FROM sqlite_sequence WHERE name = '<table>' to reset the AUTOINCREMENT counter (the closest equivalent to PG’s RESTART IDENTITY). The sqlite_sequence table only exists once an AUTOINCREMENT column has been written; the DELETE is gated by a presence check via WHERE EXISTS (SELECT 1 FROM sqlite_master WHERE name='sqlite_sequence') so a wipe on a fresh schema (no rows yet, no sqlite_sequence row) is a no-op rather than an error.

Returns one SQL string. Idempotent — TRUNCATE on an empty table is a no-op. Use data clean -o FILE for the CLI surface.

Z.C — prefix is the cfg.db_table_prefix.

Return type:

str

Parameters: