recon_gen.common.l2.trainer_timeline

compute_plant_timeline — project planted exceptions onto a per-day timeline (X.4.h.6.a).

The Studio data-shaping panel renders this as a vertical column — one row per day in the plant window, annotated with which exception kinds hit that day. The trainer can scan top-to-bottom to learn how each plant lands across time, click a day to jump the end_date knob there, and re-deploy.

Pure projection: walks the same ScenarioPlant the deploy pipeline emits (build_default_scenario from auto_scenario.py), reads plant.days_ago + scenario.today, and bins each plant onto today - timedelta(days=days_ago). No new generator logic — this is a read-only view of what the pipeline already plants.

Scope-aware: when tg.scope == "uncovered_rails" the deploy pipeline emits NO plants (just baseline fill), so this returns an empty timeline. "full" and "exceptions_only" both emit the same plant set, so they share the same timeline output.

Severability: pure Python, no DB import, no async. Same posture as trainer.py’s plants_per_node. The Studio route that wraps this calls it at request time and renders to HTML.

Functions

compute_plant_timeline(instance, tg)

Walk the auto-scenario for instance + tg and return one TimelineDay per distinct plant date, sorted oldest → newest.

hits_by_kind(timeline)

Aggregate count per PlantKind across the whole timeline.

Classes

PlantHit(kind, account_id, rail_name, amount)

One planted exception, projected onto its emit date.

TimelineDay(day, hits)

A row in the rendered timeline — date + the plants that hit it.

class recon_gen.common.l2.trainer_timeline.PlantHit(kind, account_id, rail_name, amount)[source]

Bases: object

One planted exception, projected onto its emit date.

kind matches one of the 6 PlantKind values the trainer panel’s plant-toggle filters on. rail_name is None for overdraft (no rail involved — overdraft is an account-balance state, not a rail-bound transfer). amount is the canonical money magnitude — for supersession, the corrected_amount.

Parameters:
  • kind (Literal['drift', 'overdraft', 'limit_breach', 'stuck_pending', 'stuck_unbundled', 'supersession'])

  • account_id (str)

  • rail_name (str | None)

  • amount (Decimal | None)

account_id: str
amount: Decimal | None
kind: Literal['drift', 'overdraft', 'limit_breach', 'stuck_pending', 'stuck_unbundled', 'supersession']
rail_name: str | None
class recon_gen.common.l2.trainer_timeline.TimelineDay(day, hits)[source]

Bases: object

A row in the rendered timeline — date + the plants that hit it.

Parameters:
  • day (date)

  • hits (tuple[PlantHit, ...])

day: date
hits: tuple[PlantHit, ...]
recon_gen.common.l2.trainer_timeline.compute_plant_timeline(instance, tg)[source]

Walk the auto-scenario for instance + tg and return one TimelineDay per distinct plant date, sorted oldest → newest.

Days with zero plants are omitted — the operator scans the timeline for “what landed when”, not “every calendar day”. The window is determined by the plant set itself (typically 7 days back from tg.end_date).

When tg.scope == "uncovered_rails" the deploy pipeline emits NO plants (only baseline fill), so this returns an empty list. The trainer’s UI can then render a “no plants in this scope” hint.

Threads tg.plants (None or empty = all kinds; non-empty = subset filter) through the same filter_scenario_plants chain the deploy pipeline uses, so the timeline reflects exactly what the next Deploy changes will land.

Return type:

list[TimelineDay]

Parameters:
recon_gen.common.l2.trainer_timeline.hits_by_kind(timeline)[source]

Aggregate count per PlantKind across the whole timeline.

Helper for the timeline header — surfaces “12 drift, 2 overdraft, 1 supersession” so the operator gets a one-line summary before scrolling the per-day rows. Returns kinds with zero count omitted.

Return type:

Mapping[Literal['drift', 'overdraft', 'limit_breach', 'stuck_pending', 'stuck_unbundled', 'supersession'], int]

Parameters:

timeline (Sequence[TimelineDay])