"""Module functions for the status rows and the Why line. Moved unchanged from app/main.py (MAINT-21). DECISIONS/MAIN-orchestration.md.""" from .arbiter import ACTUATOR_SITE, SET from . import control as _ctl from . import messages def why_raised(resolution, claims) -> bool: """F2 (OPT-11 P1 test): did the limit that bound the site target push it ABOVE what the winner asked (the peak floor asking for more power)? Then the Why line must not say "held back". False when nothing bound it.""" if not resolution.bound_by or resolution.contradiction: return False asked = next((c.value for c in sorted( (c for c in claims if c.kind == SET and c.actuator == ACTUATOR_SITE and c.source == resolution.winner), key=lambda c: c.priority, reverse=True)), None) return asked is not None and resolution.target_w > float(asked) def why_key(winner: str, claims) -> str: """The key of `messages.WHY_WINNER` for a resolution's winner. "safety" and "failsafe" stand for several causes, so their own claim reason is the key ("stopped", "inputs missing", "sign disagreement", or "unsplit" when this module refuses to split the power).""" if winner == "loop": # SAFETY-52: a loop holding a refused direction says so. return next((c.reason for c in claims if c.source == "loop" and c.reason in (_ctl.REFUSED, _ctl.PROBING)), winner) if winner in ("safety", "failsafe"): return next((c.reason for c in claims if c.source == winner and c.kind == "set"), "no claim was made" if not any( c.kind == "set" for c in claims) else "unsplit") return winner def tariff_plan_row(plan, tick_words: str, *, age_s, horizon_h: float, provider: bool, max_age_s: float) -> str: """The *Tariff* status row: the 30 s tick's sentence AND the provider plan (TARIFF-11): two sources, two clocks, and the row says so. The plan is stamped with its own age from `tariff_at` (not moved on a kept plan, TARIFF-12), up to TARIFF_MAX_AGE_S (5400 s); `age_s` is the caller's measurement. "No plan" is said aloud on a site that configured a provider. DECISIONS/MAIN-orchestration.md.""" # UX-08: plain words, every figure kept. The tick's sentence is "now"; the # plan carries its own age. if plan is None: if not provider: return tick_words return (f"{tick_words}. No price plan: none yet, or the last one is " f"over {max_age_s:.0f} s old") return (f"{tick_words}. Price plan from {age_s:.0f} s ago, for " f"{horizon_h:.0f} hours ahead: {plan.words}") def arbitrage_row(*, on: bool, decision, decided_at, cycle_at, now, why: str) -> str: """The *Arbitrage* status row: three states (TARIFF-11): off (not armed), armed with no decision this cycle (`why` is the loop's reason), or a decision with its age. `on` is read from the options, never inferred from `decision is None`: `cycle()` returns at the input hold before `self.arb = None`, so the old row named an option that was ON. The age is always printed, even 0. DECISIONS/MAIN-orchestration.md.""" # UX-08: plain words; the setting is named by its label. if not on: return (f"Off, “{messages.LABELS['arbitrage_enabled']}” is not " f"switched on") if decided_at is None: return (f"On, but no decision yet, because control has not got that " f"far. Control says: {why or 'no reason recorded'}") age = (now - decided_at).total_seconds() stale = cycle_at is not None and decided_at < cycle_at head = (f"On, no decision this cycle ({why or 'no reason recorded'}), " f"the last one {age:.0f} s ago: " if stale else f"On, decided {age:.0f} s ago: ") if decision is None: return (f"On, no decision this cycle ({why or 'no reason recorded'}) " f"and none before") return head + decision.explain()