diff --git a/orchestrator/progress.py b/orchestrator/progress.py index 82f6d8f..5fa2571 100644 --- a/orchestrator/progress.py +++ b/orchestrator/progress.py @@ -172,6 +172,10 @@ def _kpi_trends(records: list[dict]) -> dict: latest_period = records[-1].get("period") if records else None for s in out.values(): credits = [p["credit"] for p in s["series"] if p["credit"] is not None] + # Accountable = the KPI was actually held to a target at least once. + # One-off unscored mentions (ad-hoc deck metrics) are extraction noise, + # not a trackable KPI — they'd otherwise flood the review. + s["accountable"] = bool(credits) if s["last_period"] != latest_period: s["status"] = "not-reported" # vanished from the latest deck elif not credits: @@ -266,13 +270,16 @@ def _gaps(company: dict, records: list[dict], categories: dict, "Keep KPI names identical period to period (or record aliases in " "Configure Companies so renamed KPIs still chain).") - dropped = [s["name"] for s in kpis.values() if s["status"] == "not-reported"] + dropped = sorted(s["name"] for s in kpis.values() + if s["status"] == "not-reported" and s["accountable"]) if dropped: + shown = ", ".join(dropped[:12]) + \ + (f", … and {len(dropped) - 12} more" if len(dropped) > 12 else "") gap("dropped_kpis", 3, - f"Previously reported KPIs vanished ({len(dropped)})", - f"No longer reported as of {latest.get('period')}: " - f"{', '.join(sorted(dropped))}. Silently dropped KPIs are a classic " - "way bad news leaves a deck.", + f"Previously targeted KPIs vanished ({len(dropped)})", + f"KPIs that were held to a target in an earlier deck are no longer " + f"reported as of {latest.get('period')}: {shown}. Silently dropped " + "KPIs are a classic way bad news leaves a deck.", "Restore these KPIs — or retire them explicitly with one line on why.") thin = [(cid, c) for cid, c in categories.items() @@ -297,10 +304,13 @@ def _gaps(company: dict, records: list[dict], categories: dict, # Recurring flags only demand action while still open in the latest deck — # a flag that recurred historically but is clear now shows under "resolved". + # Scoring-derived codes already have a dedicated gap above; repeating them + # here would duplicate the request. + covered = {"no_profitability_visibility", "no_quantitative_kpis", "kpi_dropped"} latest_codes = {f.get("code") for f in (latest.get("penalties") or {}).get("flags") or []} for f in flags.get("recurring") or []: - if f["code"] in latest_codes: + if f["code"] in latest_codes and f["code"] not in covered: gap(f"recurring_{f['code']}", min(3, f["severity"]), f"Recurring flag: {f['code']}", f"Raised in {len(f['periods'])} decks ({', '.join(str(p) for p in f['periods'])}) " @@ -404,8 +414,10 @@ def render_progress_md(analysis: dict) -> str: kpis = a.get("kpis") or {} up = [(cid, c) for cid, c in cats.items() if c.get("direction") == "improving"] down = [(cid, c) for cid, c in cats.items() if c.get("direction") == "declining"] - kpi_good = [s for s in kpis.values() if s["status"] in ("on-track", "recovered")] - kpi_bad = [s for s in kpis.values() + tracked = [s for s in kpis.values() if s["accountable"]] + untracked = len(kpis) - len(tracked) + kpi_good = [s for s in tracked if s["status"] in ("on-track", "recovered")] + kpi_bad = [s for s in tracked if s["status"] in ("slipped", "missing-repeatedly", "missed-latest", "not-reported")] lines.append("## What's moving") @@ -435,13 +447,13 @@ def render_progress_md(analysis: dict) -> str: lines.append("- Nothing declining.") lines.append("") - # --- KPI trajectory - if kpis: + # --- KPI trajectory (only KPIs that were ever held to a target) + if tracked: lines.append("## KPI trajectory") lines.append("") lines.append("| KPI | Profit | Trend (oldest → latest) | Hits | Status |") lines.append("|---|---|---|---|---|") - ordered = sorted(kpis.values(), + ordered = sorted(tracked, key=lambda s: (not s["profitability"], s["name"].lower())) for s in ordered: marks = [] @@ -453,6 +465,15 @@ def render_progress_md(analysis: dict) -> str: f"| {_STATUS_LABEL[s['status']]} |") lines.append("") lines.append("✓ target hit · ✗ missed · `·` no target that period") + if untracked: + lines.append(f"({untracked} more KPI(s) were mentioned in decks without " + "ever carrying a target — not tracked here.)") + lines.append("") + elif untracked: + lines.append("## KPI trajectory") + lines.append("") + lines.append(f"No KPI has ever carried a target — {untracked} KPI(s) were " + "mentioned across the decks but none can be held to account.") lines.append("") # --- flags diff --git a/orchestrator/tests/test_progress.py b/orchestrator/tests/test_progress.py index 20cd498..236d235 100644 --- a/orchestrator/tests/test_progress.py +++ b/orchestrator/tests/test_progress.py @@ -135,6 +135,24 @@ class TestGapEngine(unittest.TestCase): a = progress.analyze(_company(), recs) self.assertIn("dropped_kpis", self.codes(a)) + def test_unaccountable_kpi_mention_is_not_dropped(self): + # A KPI mentioned once without ever carrying a target is extraction + # noise, not a "vanished KPI". + recs = [_record("2026-Q1", 50, kpis=[_kpi("ships launched", None)]), + _record("2026-Q2", 51, kpis=[])] + a = progress.analyze(_company(), recs) + self.assertNotIn("dropped_kpis", self.codes(a)) + self.assertFalse(a["kpis"]["ships launched"]["accountable"]) + + def test_scoring_flags_not_duplicated_as_recurring(self): + f = {"code": "no_profitability_visibility", "description": "none", + "severity": 3, "points": 3.0, "sources": ["scoring"]} + recs = [_record("2026-Q1", 50, flags=[f], prof_na=True), + _record("2026-Q2", 51, flags=[f], prof_na=True)] + a = progress.analyze(_company(), recs) + self.assertIn("no_profitability_kpis", self.codes(a)) + self.assertNotIn("recurring_no_profitability_visibility", self.codes(a)) + def test_thin_evidence_gap_names_material(self): a = progress.analyze(_company(), [_record("2026-Q2", 50, cats={"E": _cat(3.0, 0.05)})])