recon_gen.common.l2.auto_scenario

Auto-derive a ScenarioPlant covering every L1 invariant from an L2 instance.

Companion to common.l2.seed — that module owns the typed plant primitives + emit_seed machinery; this module knows how to walk an arbitrary L2 instance and pick representative entities so an integrator can run recon-gen demo seed-l2 myorg.yaml and get a working seed without authoring scenarios in Python.

Heuristics (deterministic, sorted by stable keys at every choice point):

  • TemplateInstance: materialize 2 synthetic instances under the first AccountTemplate (sorted by role name). Synthetic ids are cust-001 / cust-002, names Customer 1 / Customer 2. Persona-blind by construction.

  • DriftPlant: pick the first 2-leg Rail (sorted by name) whose destination_role matches the template, AND has at least one external-scope Account whose role matches the source side. Use that external Account as the counter.

  • OverdraftPlant: needs only a TemplateInstance — no rail. Plant on the second customer.

  • LimitBreachPlant: first LimitSchedule (sorted by parent_role + transfer_type) whose transfer_type matches some outbound 2-leg Rail (source = template role, destination = external role). Plant amount = cap × 1.5 to guarantee breach.

  • StuckPendingPlant: first Rail (sorted by name) with max_pending_age set.

  • StuckUnbundledPlant: first Rail with max_unbundled_age set. Validator R8 guarantees such a rail is bundled by some aggregating rail, so the resulting Posted leg surfaces in <prefix>_stuck_unbundled.

  • SupersessionPlant: first single-leg Rail or any Rail with a customer-side leg.

Plants that can’t be derived (e.g., no LimitSchedule declared, no 2-leg inbound rail) are omitted from the returned ScenarioPlant. The CLI surface logs a one-line warning per omission so the integrator knows what’s missing from their YAML for full coverage.

The auto-scenario deliberately does NOT try to produce byte-identical output to the curated default_ar_scenario — the two are different contracts. default_ar_scenario is the hash-locked canonical AR fixture; this module produces a reasonable starting demo for ANY L2.

Functions

add_broken_rail_plants(base, instance, *[, ...])

Layer a single broken-Rail spike on top of an existing scenario (R.3.c).

boost_inv_fanout_plants(base, *[, ...])

Tune Investigation fanout plants for visibility (R.3.d).

default_scenario_for(instance, *[, today, ...])

Walk instance and return an auto-derived ScenarioPlant.

densify_scenario(base, *[, factor, day_stride])

Replicate per-kind plants across the window for visibility (R.3.b).

filter_scenario_plants(base, kinds)

Return a copy of base keeping only the requested L1 plant kinds.

template_instance_ids(template)

The account_ids this template will materialize during seed.

Classes

AutoScenarioReport(scenario, omitted)

Describes which plants the auto-scenario emitted vs. omitted.

class recon_gen.common.l2.auto_scenario.AutoScenarioReport(scenario, omitted)[source]

Bases: object

Describes which plants the auto-scenario emitted vs. omitted.

The CLI prints this so the integrator knows what’s missing from their YAML for full L1 coverage.

Parameters:
omitted: tuple[tuple[str, str], ...]
scenario: ScenarioPlant
recon_gen.common.l2.auto_scenario.add_broken_rail_plants(base, instance, *, broken_count=15)[source]

Layer a single broken-Rail spike on top of an existing scenario (R.3.c).

Picks one Rail with max_pending_age set + plants broken_count stuck_pending entries on it across the window. L1 Exceptions KPI then has a magnitude that matters; the L2 Exceptions sheet’s bar chart shows the broken Rail spike immediately.

Picker rule: deterministic — sorted by rail name, the FIRST rail with max_pending_age set. Different from default_scenario_for’s pending_rail picker by intent — the broken rail is a separate concept; using the same picker would just stack plants on the existing stuck_pending row.

No-op when no max_pending_age-eligible rail exists OR the picked rail’s role doesn’t resolve to any materialized account.

Return type:

ScenarioPlant

Parameters:
recon_gen.common.l2.auto_scenario.boost_inv_fanout_plants(base, *, amount_multiplier=5, extra_recipient_count=0)[source]

Tune Investigation fanout plants for visibility (R.3.d).

The Phase R baseline puts ~600 customer-ACH transfers per day into the system at median ~$665 per transfer. The default InvFanoutPlant.amount_per_transfer = $500 from the auto-scenario sits BELOW the baseline median — its cluster is structurally visible (12 senders → 1 recipient) but per-transfer amounts don’t stand out.

This helper bumps each inv_fanout plant’s amount by amount_multiplier (5× default → $2,500 per transfer) so the cluster’s aggregate inflow (~$30,000 across 12 senders in one day) stands out clearly on the Recipient Fanout sheet’s Sankey + the Volume Anomalies sheet’s z-score band.

Optional extra_recipient_count: synthesize N extra fanout plants targeting different recipients (cycles through the existing template instances) so multiple clusters appear on the dashboards. Out of scope for the first land — defaults to 0.

Return type:

ScenarioPlant

Parameters:
  • base (ScenarioPlant)

  • amount_multiplier (int)

  • extra_recipient_count (int)

recon_gen.common.l2.auto_scenario.default_scenario_for(instance, *, today=None, mode='l1_invariants', per_rail_firings=3)[source]

Walk instance and return an auto-derived ScenarioPlant.

Modes (M.4.2):

  • l1_invariants (default) — only L1 SHOULD-violation plants (drift, overdraft, limit-breach, stuck-pending, stuck-unbundled, supersession, transfer-template). The legacy / pre-M.4.2 shape; L2 Flow Tracing surfaces dead for any rail not picked.

  • broad — only RailFiringPlant rows: every declared rail whose role(s) resolve to a materialized account fires per_rail_firings times across stratified days. No L1 invariant plants. Useful for visual verification of the L2 surface in isolation.

  • l1_plus_broad — both layers. The harness (M.4.1.b) uses this so Playwright can assert both planted SHOULD violations AND planted-rail visibility on the same deploy.

See module docstring for per-plant heuristics. Returns the scenario plus a report of any plant kinds that couldn’t be materialized from this instance (e.g., no LimitSchedule declared → no LimitBreachPlant).

Return type:

AutoScenarioReport

Parameters:
  • instance (L2Instance)

  • today (date | None)

  • mode (Literal['l1_invariants', 'broad', 'l1_plus_broad'])

  • per_rail_firings (int)

recon_gen.common.l2.auto_scenario.densify_scenario(base, *, factor=5, day_stride=7)[source]

Replicate per-kind plants across the window for visibility (R.3.b).

The R.2 baseline puts ~60k legs per L2 instance into the window; a single drift / overdraft / etc plant gets lost in the noise. This helper takes a base ScenarioPlant (typically from default_scenario_for) and replicates each plant kind by varying days_ago so each kind shows N rows on the dashboards instead of 1.

For stuck-pending / stuck-unbundled, the days_ago stride keeps every replica well past the rail’s max_*_age cap so all replicas surface. For drift / overdraft / breach / supersession, the stride spreads them across the window for visual diversity.

inv_fanout_plants and transfer_template_plants are NOT replicated — the fanout already plants N senders per recipient (its own density), and TransferTemplate plants already produce 3 firings per template (the Complete / Orphan / Required-met cases).

Return type:

ScenarioPlant

Parameters:
recon_gen.common.l2.auto_scenario.filter_scenario_plants(base, kinds)[source]

Return a copy of base keeping only the requested L1 plant kinds.

The data-shaping panel’s plant-toggle checkboxes (X.4.h.2) write a subset of the PlantKind enum into cfg.test_generator.plants; this is the projection that consumes that subset. Per SPEC’s “Data-shaping model / plants” section: None or empty tuple ⇒ “all kinds” (today’s behavior), so the absent-block case stays byte-identical to the locked seeds.

Only the 6 L1-invariant plant kinds in PlantKind are gated: drift / overdraft / limit_breach / stuck_pending / stuck_unbundled / supersession. The other plant tuples on ScenarioPlantfailed_transaction_plants (X.1.i — separate Failed-status fixture), transfer_template_plants, rail_firing_plants, inv_fanout_plants — are L2-shape / Investigation fixtures, not L1 SHOULD-violations, and pass through unchanged. Same with template_instances (the customer materialization, needed by every plant kind that references customer-side accounts) and today (the reference date).

Return type:

ScenarioPlant

Parameters:
recon_gen.common.l2.auto_scenario.template_instance_ids(template)[source]

The account_ids this template will materialize during seed.

Public surface for the validator (U7 — collision with singleton Account.id) so the validator never replicates the rendering rule. Resolves the same template-rendering path the seed uses inside _materialize_instances — change one, the other follows.

Return type:

tuple[str, ...]

Parameters:

template (AccountTemplate)