recon_gen.common.l2.seed
Demo-seed primitives for any L2 instance.
Two seed layers compose to produce a full demo’s richness:
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
AccountTemplateinstances 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.Plants (``emit_seed``). Layered on top of the baseline. Each
ScenarioPlantmember 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 sodata hashcan pin the SHA256.Composed (``emit_full_seed``). Concatenates baseline + plants. This is the entry point
data applycalls 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(holdstemplate_instances+ every plant tuple + a referencetodaydate).Entry points: -
emit_seed(instance, scenarios)— plants only; deterministicoutput for hash-locking.
emit_baseline_seed(instance)— 90-day healthy baseline only.emit_full_seed(instance, scenarios)— baseline + plants; whatdata applycalls.
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 shapeemit_schemaproduces.
Functions
|
Emit a 3-month healthy-baseline INSERT script for the L2 instance. |
|
Emit baseline + plants concatenated as a single SQL script. |
|
Emit the full SQL INSERT script for the planted scenarios. |
|
Emit TRUNCATE statements for the per-prefix base + matview tables. |
Classes
A planted L1 violation: two-template chain where leg_rail firings of one child Transfer claim different |
|
|
A planted (account, business_day) cell where stored balance disagrees with computed balance by |
|
BU.3.1 — A planted (account, business_day) cell whose stored |
|
A planted leg with |
AB.4.5 plant: fan-in batch with parent set EXCEEDING expected. |
|
AB.4.5 plant: fan-in batch with parent set SHORT of expected (orphan / incomplete). |
|
|
AB.4.5 plant: healthy fan-in chain firing. |
|
A planted (account, business_day, rail) cell where the daily inbound flow exceeds the configured Inbound |
|
A planted "fanout" — N senders all credit ONE leaf-internal recipient on the same day (N.4.h, fuzzer Investigation coverage). |
|
A planted (parent_account, business_day) cell where the parent (control) account's stored balance disagrees with Σ children by |
|
A planted (account, business_day, rail) cell where the daily outbound flow exceeds the configured Outbound |
|
AB.6.6 plant: a chain parent firing with ZERO declared XOR siblings firing — matches the AB.6.5 matview's |
|
AB.6.6 plant: a chain parent firing where TWO declared XOR siblings fire — matches the AB.6.5 matview's |
|
A planted (account, business_day) cell where stored balance is negative. |
|
A planted Posted firing of a single Rail (M.4.2 broad-mode plant kind). |
|
The full set of planted scenarios + materialized template instances. |
|
A planted Pending leg whose age exceeds the rail's max_pending_age. |
|
A planted Posted leg with bundle_id IS NULL whose age exceeds the rail's max_unbundled_age. |
|
A planted logical-key (transaction.id) with multiple entry versions, simulating a TechnicalCorrection rewrite of a posted leg. |
|
One concrete materialization of an |
|
A planted firing of a declared TransferTemplate. |
A planted healthy two-template chain firing (AB.2.6). |
|
|
AB.3.5 plant: a TransferTemplate Transfer where one XOR group has zero firings — matches the AB.3.3 matview's |
|
AB.3.5b plant: a TransferTemplate Transfer where TWO members of one XOR group both fire — matches the AB.3.3 matview's |
- class recon_gen.common.l2.seed.ChainParentDisagreementPlant(child_template_name, days_ago, parent_a_transfer_id, parent_b_transfer_id)[source]
Bases:
objectA planted L1 violation: two-template chain where leg_rail firings of one child Transfer claim different
parent_transfer_idvalues (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_namebut assigning different synthetictransfer_parent_idvalues, so the AB.2.3 matview readsCOUNT(DISTINCT parent_transfer_id) > 1and 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:
objectA 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
Driftvalue 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 usescounter_account_id(must be a declared external Account in the same instance). Both are resolved frominstanceat 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:
objectBU.3.1 — A planted (account, business_day) cell whose stored
moneydiffers from the per-rowexpected_eod_balance.Surfaces in L1’s Expected-EOD-Balance variance matview (
<prefix>_expected_eod_balance_breach) — the matview filtersexpected_eod_balance IS NOT NULL AND money <> expected_eod_balance. The plant emits ONE row into<prefix>_daily_balanceswithmoney = expected + variance,expected_eod_balance = expected, so the variance row materializes. NO transactions are emitted — same balance-only shape asOverdraftPlant.The Trainer adapter (
_invoke_expected_eod_balance_breach_plant) picks the role from the L2 (first internal Account); the operator’s only knobs aredays_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:
objectA planted leg with
status='Failed'(X.1.i).The L1 schema’s
statuscolumn is open-set — any string is a valid terminal state. The tool reasons explicitly aboutPending/Posted(drives Aging, Conservation, Completion); every other status (Failed, Cancelled, Rejected, …) is collapsed toOtherin 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
Otheroption 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:
objectAB.4.5 plant: fan-in batch with parent set EXCEEDING expected.
Emits
parent_countparent firings (more than the chain’sexpected_parent_count) sharing one child Transfer. The AB.4.7 matview readsparent_count > expectedand emits a row withdisagreement_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:
objectAB.4.5 plant: fan-in batch with parent set SHORT of expected (orphan / incomplete).
Emits
parent_countparent firings (less than the chain’sexpected_parent_count) sharing one child Transfer. The AB.4.7 matview readsparent_count < expectedand emits a row withdisagreement_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:
objectAB.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’sexpected_parent_count(or any value ≥2 when the chain leavesexpected_parent_countunset for variable-batch flows). The AB.4.7_fan_in_disagreementmatview readsparent_count == expected(orparent_count >= 2unset) 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:
objectA planted (account, business_day, rail) cell where the daily inbound flow exceeds the configured Inbound
LimitSchedule.cap.Mirror of
LimitBreachPlantfor the AB.1 Inbound direction — surfaces in L1’s Limit Breach SHOULD-constraint whenInboundFlow(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:
objectA 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 NULLfilter 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 overtransfer_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_amountJOIN finds matched legs.rail_nameis 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
TemplateInstancematerialized from anAccountTemplatewith a non-NULLparent_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:
objectA 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 syntheticparent_roleso the matview’scomputed_ledger_balancefor our parent only sums OUR child (no bleed from baseline accounts with matching parent_role). Operator only picksdays_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:
objectA 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 frominstanceat 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:
objectAB.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_violationmatview readsCOUNT(matched_child_name) = 0→HAVING <> 1→ row surfaces withdisagreement_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:
objectAB.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_idset to the parent’stransfer_id. The AB.6.5 matview’sfired_children_distinctCTE picks up both →COUNT = 2→HAVING <> 1→ row surfaces withdisagreement_kind='overlap',fired_children='<a>,<b>'(concat ordering dialect-specific).Pairs with
MultiXorMissedPlantso 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:
objectA 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:
objectA 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 onaccount_id_b); single-leg rails plant 1 leg (onaccount_id_a, direction perRail.leg_direction).account_id_bis None for single-leg rails.transfer_parent_idis 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_metadatacarries 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 aleg_railsentry of some TransferTemplate — the L2 Flow Tracingtt-instances+tt-legsdatasets read rows bytemplate_name, so leg-rail broad firings need this field populated to surface on the Transfer Templates sheet alongside the structuredTransferTemplatePlantfirings.Nonefor 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:
objectThe full set of planted scenarios + materialized template instances.
Defaults to today (UTC midnight) as the reference date;
days_agoon each plant subtracts from this.- Parameters:
template_instances (tuple[TemplateInstance, ...])
drift_plants (tuple[DriftPlant, ...])
ledger_drift_plants (tuple[LedgerDriftPlant, ...])
overdraft_plants (tuple[OverdraftPlant, ...])
limit_breach_plants (tuple[LimitBreachPlant, ...])
inbound_cap_breach_plants (tuple[InboundCapBreachPlant, ...])
expected_eod_balance_plants (tuple[ExpectedEodBalancePlant, ...])
two_template_chain_plants (tuple[TwoTemplateChainPlant, ...])
chain_parent_disagreement_plants (tuple[ChainParentDisagreementPlant, ...])
xor_variant_missed_firing_plants (tuple[XorVariantMissedFiringPlant, ...])
xor_variant_overlap_plants (tuple[XorVariantOverlapPlant, ...])
fan_in_chain_plants (tuple[FanInChainPlant, ...])
fan_in_chain_missing_parent_plants (tuple[FanInChainMissingParentPlant, ...])
fan_in_chain_extra_parent_plants (tuple[FanInChainExtraParentPlant, ...])
multi_xor_missed_plants (tuple[MultiXorMissedPlant, ...])
multi_xor_overlap_plants (tuple[MultiXorOverlapPlant, ...])
stuck_pending_plants (tuple[StuckPendingPlant, ...])
failed_transaction_plants (tuple[FailedTransactionPlant, ...])
stuck_unbundled_plants (tuple[StuckUnbundledPlant, ...])
supersession_plants (tuple[SupersessionPlant, ...])
transfer_template_plants (tuple[TransferTemplatePlant, ...])
rail_firing_plants (tuple[RailFiringPlant, ...])
inv_fanout_plants (tuple[InvFanoutPlant, ...])
today (date)
- 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:
objectA 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:
objectA 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:
objectA 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:
objectOne 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:
objectA planted firing of a declared TransferTemplate.
Plants one shared Transfer (single
transfer_id) made up of legs whosetemplate_namepoints back to the template. Each leg carries the sametransfer_keymetadata 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 offfiring_seqso two firings of the same template don’t collapse to one shared Transfer.M.3.10g first cut handled only
TwoLegRailfirst leg_rails (debit + credit summing toexpected_net = 0in one firing). Extended to also handleSingleLegRailfirst leg_rails — emits one leg per firing in the rail’sleg_direction(Variabletreated asDebitfor plant purposes; closing-leg semantics aren’t material to surfacing data on the L2FT TT explorer). Single-leg firings surface as ‘Imbalanced’ againstexpected_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_idanddestination_account_idmay each be either aTemplateInstance.account_id(a materialized customer) OR an L2Account.id(a singleton or external counterparty). For SingleLegRail templates onlysource_account_idis used (the leg account); the picker setsdestination_account_idto 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 whoserail_nameis the child +transfer_parent_idpoints 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:
objectA 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:
objectAB.3.5 plant: a TransferTemplate Transfer where one XOR group has zero firings — matches the AB.3.3 matview’s
firing_count = 0branch.Emits a single
witnessleg_rail row carryingtemplate_nameso the synthetic Transfer enters<prefix>_current_transactions(and therefore the matview’stemplate_transfersuniverse), but NO member oftarget_xor_group_indexfires for this transfer_id. The matview’s LEFT JOIN finds zero member-rail firings for(transfer_id, template, target_xor_group_index)→COUNT(*) = 0→HAVING <> 1→ violation row surfaces withfired_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:
objectAB.3.5b plant: a TransferTemplate Transfer where TWO members of one XOR group both fire — matches the AB.3.3 matview’s
firing_count >= 2branch.Emits two leg_rail rows sharing one
transfer_id+template _name, bothrail_namevalues being members oftarget_xor_group_index. The matview’s LEFT JOIN per (transfer, group, member_rail) hits twice →COUNT(*) = 2→HAVING <> 1→ row surfaces withfired_rails='<a>,<b>'. Pairs withXorVariantMissedFiringPlantso the demo dashboard surfaces BOTH branches of the matview’sfiring_count <> 1HAVING.Picker constraint: target group MUST have ≥2 distinct members. Validator C1d already enforces ≥2 at load time, so every declared XOR group qualifies.
variant_aandvariant_bMUST 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 forpsycopg2.cursor.execute(Postgres) orcli._execute_script(Oracle). The script targets the same<prefix>_transactions+<prefix>_daily_balancestables 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 UTCdatetime.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 legacyemit_seedaccepts.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 ofskip_rails. When set, ONLY rails whose name appears in the set are emitted; everything else is silently skipped. Used by the deploy pipeline’sscope: only_templatemode to emit baseline restricted to the template’s leg-rails dependency closure.None(default) means “no narrowing” (preserves locked-seed byte-identity). Mutually exclusive withskip_railsin spirit but tested independently — if the caller passes both, the rail must survive both filters (inonly_railsAND not inskip_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 writescfg.test_generator.seedhere 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_seedand the legacyemit_seedtogether 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 baselinetr-base-*ids.- Parameters:
instance (
L2Instance) – the L2 model instance.scenarios (
ScenarioPlant) – planted scenarios (typically fromauto_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 UTCdatetime.now().date(). The plants’ own anchor lives onscenarios.todayand 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) orcli._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 incli._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-rowINSERT INTO foo VALUES (...), (...)(the M.2 PG-only form) is unsupported on Oracle (which usesINSERT ALLinstead); per-row INSERT is the simpler portability choice and the perf cost is negligible for the demo’s ~few-hundred-row scale.Z.C —
prefixis the cfg.db_table_prefix.- Return type:
str- Parameters:
instance (L2Instance)
scenarios (ScenarioPlant)
prefix (str)
dialect (Dialect)
- 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>_transactionsand<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 CASCADEso the BIGSERIALentrycolumn resets to 1 on the next INSERT (matches the seed’s deterministic-anchor contract). Oracle has no RESTART IDENTITY syntax — uses plainTRUNCATE TABLE; the integrator can re-create the IDENTITY column if exact serial parity matters. SQLite has noTRUNCATEstatement at all — usesDELETE FROM <table>plus aDELETE 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 viaWHERE 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 FILEfor the CLI surface.Z.C —
prefixis the cfg.db_table_prefix.- Return type:
str- Parameters:
instance (L2Instance)
prefix (str)
dialect (Dialect)