recon_gen.common.l2.trainer

plants_per_node() — derive per-topology-node planted-exception counts from an L2 scenario object (X.4.c.6).

The trainer-mode overlay shows the trainer “this role has 2 drift plants and 1 overdraft” without involving the demo DB — every plant primitive in common/l2/seed.py carries its host (account_id / rail_name / template_name) directly, so a pure walk of the ScenarioPlant aggregates into a per-node count map.

Symmetric in shape to coverage.py — the chrome chrome consumes both through the same data-presence / data-trainer-kinds SVG attr pattern. They differ only in the data-source: coverage hits the DB, trainer reads the in-memory scenario.

Severability: pure Python, no DB import, no async. The Studio route that wraps this calls it at request time and serializes to JSON.

Functions

plants_per_node(instance[, scenario])

Count planted plants per topology node.

Classes

TrainerMap(by_node_id)

Per-topology-node planted-plant counts derived from a ScenarioPlant.

class recon_gen.common.l2.trainer.TrainerMap(by_node_id)[source]

Bases: object

Per-topology-node planted-plant counts derived from a ScenarioPlant.

by_node_id keys are the same topology IDs coverage.py uses (role__X / rail__X / tmpl__X); values are {plant_kind: count} mappings with one entry per plant kind that landed on that node. Nodes with zero plants are absent from the map (the chrome’s “no badge” default is the empty case).

Parameters:

by_node_id (Mapping[str, Mapping[Literal['drift', 'overdraft', 'limit_breach', 'stuck_pending', 'stuck_unbundled', 'supersession', 'failed', 'transfer_template', 'inv_fanout', 'inbound_cap_breach', 'two_template_chain', 'chain_parent_disagreement', 'xor_variant_missed_firing', 'xor_variant_overlap', 'fan_in_chain', 'fan_in_chain_missing_parent', 'fan_in_chain_extra_parent', 'multi_xor_missed', 'multi_xor_overlap'], int]])

by_node_id: Mapping[str, Mapping[Literal['drift', 'overdraft', 'limit_breach', 'stuck_pending', 'stuck_unbundled', 'supersession', 'failed', 'transfer_template', 'inv_fanout', 'inbound_cap_breach', 'two_template_chain', 'chain_parent_disagreement', 'xor_variant_missed_firing', 'xor_variant_overlap', 'fan_in_chain', 'fan_in_chain_missing_parent', 'fan_in_chain_extra_parent', 'multi_xor_missed', 'multi_xor_overlap'], int]]
recon_gen.common.l2.trainer.plants_per_node(instance, scenario=None)[source]

Count planted plants per topology node.

When scenario is None, derives the auto-scenario via default_scenario_for(instance) — same default the demo apply pipeline uses so the trainer surface previews the same plants the deployed DB will carry.

Each plant kind contributes:

  • drift: role(account_id) + rail(rail_name)

  • overdraft: role(account_id)

  • limit_breach: role(account_id) + rail(rail_name)

  • stuck_pending: rail(rail_name)

  • stuck_unbundled: rail(rail_name)

  • supersession: rail(rail_name)

  • failed: rail(rail_name)

  • transfer_template: template(template_name)

  • inv_fanout: rail(rail_name) (recipient role isn’t a clean “host” — the recipient is one of N senders’ targets, not a plant-singularity owner)

RailFiringPlant is excluded — it’s broad-mode bulk firings, not a SHOULD-violation per the SPEC, so it shouldn’t show up as a “planted exception”.

A plant kind that lands on multiple nodes (e.g. drift on both a role and a rail) increments the count on each node — the trainer chrome shows badges per node, not per plant.

Return type:

TrainerMap

Parameters: