codi-api-agent 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
api_agent/metrics.py ADDED
@@ -0,0 +1,1030 @@
1
+ """DERIVED METRICS — the arithmetic the model is forbidden to do, done by code.
2
+
3
+ Why this module exists
4
+ ----------------------
5
+ `SYNTHESIS_SYSTEM` bans the model from computing any number (no sums, deltas, percentages,
6
+ margins, shares). That ban was earned: every prompt-level attempt to make the model do arithmetic
7
+ correctly has failed, and a wrong total stated confidently is worse than no total. See the
8
+ `reliability-over-prompts` rule.
9
+
10
+ But the ban is also the quality ceiling. Measured against the incumbent agent we are replacing,
11
+ its substantive answers average ~24 derived expressions ("$1.2M (-18.7%) versus prior year",
12
+ "margin collapsed from 25.8% to 8.5%", "three locations account for $1.1M of the decline") and
13
+ ours contain **zero** — because `_computed_facts` only ever offered min, max and average, so
14
+ min/max/average is the entire analytical vocabulary the model has to work with.
15
+
16
+ This module widens that vocabulary without touching the guarantee. Everything here is computed
17
+ deterministically from the evidence rows and handed to synthesis as VERIFIED facts; the model
18
+ quotes, it never calculates. Each fact also enters the grounding pool, so a figure the model
19
+ copies from here validates instead of being flagged as unsupported.
20
+
21
+ Design rules
22
+ ------------
23
+ - **Pure.** No LLM, no I/O, no config. Rows in, facts out.
24
+ - **Never guess a pairing.** A delta is emitted only when the two columns are unambiguously
25
+ related by naming convention (see `_classify`). An unrecognised column produces no fact rather
26
+ than a wrong one.
27
+ - **Distinguish a DELTA column from a LEVEL column.** The warehouse ships both: `rev_vs_ly` is
28
+ already a difference, while `prior_year_actual` is a level. Treating one as the other silently
29
+ doubles or halves every percentage, so the distinction is explicit, not inferred from position.
30
+ - **Bounded output.** Facts are capped per evidence item; a 60-column table must not flood the
31
+ prompt with 400 derived figures.
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import itertools
36
+ import re
37
+ import statistics
38
+ from dataclasses import dataclass
39
+
40
+ # Columns whose values must not be summed or differenced as if they were quantities. Mirrors
41
+ # agent._NON_ADDITIVE_KEY; duplicated here to keep this module free of agent imports (agent
42
+ # imports metrics, not the other way round).
43
+ _NON_ADDITIVE = re.compile(
44
+ r"pct|percent|%|ratio|(?:^|_)rate(?:$|_)|margin|average|(?:^|_)avg(?:$|_)|(?:^|_)mean(?:$|_)"
45
+ r"|median|(?:^|_| )per(?:$|_| )|yoy|mom|qoq|growth", re.I)
46
+ _ID_LIKE = re.compile(r"(?:^|_)(?:id|ids|code|zip|postal|rank|index|idx|key)(?:$|_)", re.I)
47
+ # A bare calendar column is a dimension, not a measure — but only when the WHOLE name is that
48
+ # dimension. `year`/`p_month` are dimensions; `prior_year_actual` is a money column and matching
49
+ # it here silently deleted every year-over-year comparison the warehouse ships.
50
+ _CALENDAR_COL = re.compile(r"^(?:p_)?(?:year|month|quarter|week|day|period)$", re.I)
51
+ _LABEL_HINT = ("month", "period", "date", "name", "label", "account", "item", "location",
52
+ "category", "card", "procedure", "provider")
53
+
54
+ # --- column-role vocabulary -------------------------------------------------------------------- #
55
+ # A column that is ALREADY a difference (actual minus comparison). Emitting `actual - column`
56
+ # for one of these would be nonsense, so they are handled as deltas: the comparison LEVEL is
57
+ # recovered as `actual - delta`.
58
+ _DELTA_LY = re.compile(r"(?:^|_)(?:vs_ly|vs_last_year|var_ly|delta_ly)(?:$|_)", re.I)
59
+ _DELTA_BUDGET = re.compile(r"(?:^|_)(?:vs_budget|vs_bud|var_budget|delta_budget)(?:$|_)", re.I)
60
+ _DELTA_PLAIN = re.compile(r"(?:^|_)(?:delta|variance|var)(?:$|_)", re.I)
61
+ # A column that is a LEVEL for a comparison period (a value, not a difference).
62
+ _LEVEL_PRIOR = re.compile(r"(?:^|_)(?:prior_year|prior|last_year|ly|py|previous)(?:$|_)", re.I)
63
+ _LEVEL_BUDGET = re.compile(r"(?:^|_)(?:budget|bud|plan|target)(?:$|_)", re.I)
64
+ # The current-period value.
65
+ _ACTUAL = re.compile(r"(?:^|_)(?:actual|curr|current|value|ytd_value|net|total)(?:$|_)", re.I)
66
+ _PCT_COL = re.compile(r"(?:^|_)(?:pct|percent|percentage)(?:$|_)|_pct$", re.I)
67
+
68
+ # Margin = profit / revenue. Both sides matched by name so a margin is only ever computed from
69
+ # two columns that genuinely are a profit and a revenue.
70
+ _PROFIT_COL = re.compile(r"ebitda|profit|income|margin_dollars", re.I)
71
+ _REVENUE_COL = re.compile(r"(?:^|_)(?:rev|revenue|net_production|production|sales)(?:$|_)|^rev", re.I)
72
+
73
+ MAX_FACTS_PER_ITEM = 24 # keeps a wide table from flooding the synthesis prompt
74
+ TOP_N = 3 # "the top 3 movers account for X of the change"
75
+ SMALL_TABLE = 12 # at or below this, every row gets its own derived comparison
76
+
77
+ # --- WHEN MAY WE AGGREGATE ACROSS ROWS? --------------------------------------------------------- #
78
+ # This is the guard that stops this module manufacturing confident nonsense, and it is deliberately
79
+ # structural rather than a list of known column names — a name list would encode one warehouse's
80
+ # conventions and fail silently on the next.
81
+ #
82
+ # The hazard: a result may be one metric across many entities/periods (summable), or a list where
83
+ # each row is a DIFFERENT metric. A chain-total result returns Revenue, Staff Expenses, Non Staff
84
+ # Expenses, Office Adj. and Adjusted EBITDA as five rows; summing that column adds revenue to
85
+ # expenses to EBITDA and yields a meaningless figure carrying a "code-verified" label — precisely
86
+ # the failure the no-derived-numbers rule exists to prevent.
87
+ #
88
+ # The rule adopted: CROSS-ROW SUMS ARE ALLOWED ONLY WHEN THE ROWS ARE INDEXED BY TIME. Summing a
89
+ # measure over successive periods is valid by construction; summing it over arbitrary row keys is
90
+ # not. PER-ROW arithmetic (a row's own actual against its own budget) is always safe because it
91
+ # involves no cross-row addition at all, and it carries most of the value anyway — "Adjusted EBITDA
92
+ # $5.5M, -18.7% versus prior year" is one row's numbers.
93
+ #
94
+ # `_has_rollup_row` is kept as a second, independent guard for the contribution analysis: if one
95
+ # row equals the sum of the others, the rows are components of a rollup rather than peers.
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class Fact:
100
+ """One code-computed number, with the arithmetic that produced it.
101
+
102
+ `basis` is shown to the model and to the user-facing trace: a derived figure that cannot say
103
+ how it was derived is exactly the kind of unfalsifiable claim this project refuses to make.
104
+ """
105
+ label: str
106
+ value: float
107
+ basis: str
108
+ unit: str = "" # "" money/count · "%" percent · "pp" percentage points
109
+
110
+ def render(self) -> str:
111
+ if self.unit == "%":
112
+ num = f"{self.value:+,.1f}%"
113
+ elif self.unit == "pp":
114
+ num = f"{self.value:+,.1f} pp"
115
+ else:
116
+ num = f"{self.value:,.2f}"
117
+ return f"{self.label} = {num} [{self.basis}]"
118
+
119
+
120
+ # --------------------------------------------------------------------------- #
121
+ # Column inspection
122
+ # --------------------------------------------------------------------------- #
123
+ def _num(v) -> float | None:
124
+ """A cell → float, or None when it isn't a real number.
125
+
126
+ COERCES rather than isinstance-tests. Postgres returns `Decimal` for every numeric column,
127
+ which is neither `int` nor `float`; a strict type check therefore skips every money column and
128
+ yields an empty result against the live warehouse while passing every test written with float
129
+ literals. That exact trap has already been paid for once in `sql_loader._describe_stdlib`.
130
+ Strings are not coerced — a numeric-looking label is a label, not a measure.
131
+ """
132
+ if isinstance(v, bool) or isinstance(v, str) or v is None:
133
+ return None
134
+ try:
135
+ f = float(v)
136
+ except (TypeError, ValueError):
137
+ return None
138
+ return None if f != f or f in (float("inf"), float("-inf")) else f
139
+
140
+
141
+ def numeric_columns(rows: list[dict]) -> dict[str, list[float]]:
142
+ """`{column: values}` for every column numeric in at least one row, skipping id-like columns.
143
+
144
+ Rows with a missing/non-numeric cell contribute nothing for that column, so a column that is
145
+ numeric in only some rows still yields facts over the rows where it is real.
146
+ """
147
+ cols: dict[str, list[float]] = {}
148
+ for row in rows:
149
+ if not isinstance(row, dict):
150
+ continue
151
+ for k, v in row.items():
152
+ if _ID_LIKE.search(k or "") or _CALENDAR_COL.match(k or ""):
153
+ continue
154
+ if (f := _num(v)) is not None:
155
+ cols.setdefault(k, []).append(f)
156
+ return cols
157
+
158
+
159
+ def label_of(row: dict) -> str:
160
+ """Best-effort human label for a row — a period/name/label field, else the first string."""
161
+ if not isinstance(row, dict):
162
+ return "?"
163
+ for k, v in row.items():
164
+ if isinstance(v, str) and v.strip() and any(t in k.lower() for t in _LABEL_HINT):
165
+ return v.strip()
166
+ for k, v in row.items():
167
+ if isinstance(v, str) and v.strip() and not _ID_LIKE.search(k):
168
+ return v.strip()
169
+ return "?"
170
+
171
+
172
+ def _classify(col: str) -> tuple[str, str]:
173
+ """`column -> (role, stem)`.
174
+
175
+ role ∈ {actual, delta_ly, delta_budget, delta, level_prior, level_budget, pct, other}
176
+ stem is the column with its role marker stripped, so `rev_actual` and `rev_vs_ly` share the
177
+ stem "rev" and can be paired. A flat schema (`actual` / `vs_ly`) shares the empty stem.
178
+
179
+ Order matters: `vs_ly_pct` is a percentage, not a delta, so pct is tested first; and
180
+ `prior_year_actual` is a prior LEVEL, not an actual, so level markers are tested before
181
+ `_ACTUAL`.
182
+ """
183
+ c = col or ""
184
+ def stem(pattern: re.Pattern) -> str:
185
+ return re.sub(r"_+", "_", pattern.sub("_", c)).strip("_").replace("actual", "").strip("_")
186
+
187
+ if _PCT_COL.search(c):
188
+ return "pct", stem(_PCT_COL)
189
+ if _DELTA_LY.search(c):
190
+ return "delta_ly", stem(_DELTA_LY)
191
+ if _DELTA_BUDGET.search(c):
192
+ return "delta_budget", stem(_DELTA_BUDGET)
193
+ if _LEVEL_PRIOR.search(c):
194
+ return "level_prior", stem(_LEVEL_PRIOR)
195
+ if _LEVEL_BUDGET.search(c):
196
+ return "level_budget", stem(_LEVEL_BUDGET)
197
+ if _DELTA_PLAIN.search(c):
198
+ return "delta", stem(_DELTA_PLAIN)
199
+ if _ACTUAL.search(c):
200
+ return "actual", stem(_ACTUAL)
201
+ # A bare metric name (`ebitda`, `rev`, `curr_val`) IS the current-period level — the warehouse
202
+ # only spells out "actual" when a comparison column sits beside it in the same grid. Defaulting
203
+ # these to `actual` is what lets `ebitda` pair with `ebitda_prior`.
204
+ return "actual", c
205
+
206
+
207
+ _ISO_DATE = re.compile(r"^\s*(\d{4})-(\d{2})(?:-(\d{2}))?")
208
+
209
+
210
+ def _as_period(v) -> tuple | None:
211
+ """A cell → a sortable period key, or None when it isn't a period.
212
+
213
+ Detected by VALUE, not by column name, so this works on any warehouse: a `date`/`datetime`
214
+ object, or a string starting with an ISO year-month. Anything else is not a period.
215
+ """
216
+ if hasattr(v, "year") and hasattr(v, "month"): # date / datetime
217
+ return (v.year, v.month, getattr(v, "day", 1))
218
+ if isinstance(v, str) and (m := _ISO_DATE.match(v)):
219
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3) or 1))
220
+ return None
221
+
222
+
223
+ def period_column(rows: list[dict]) -> str | None:
224
+ """The column that indexes these rows by TIME, or None.
225
+
226
+ Requires every row to carry a parseable period, all distinct, in non-decreasing order — i.e.
227
+ a genuine time series as the warehouse returns it. This is the sole licence for cross-row
228
+ aggregation (see the guard note above).
229
+ """
230
+ if len(rows) < 2 or not isinstance(rows[0], dict):
231
+ return None
232
+ for col in rows[0]:
233
+ keys = [_as_period(r.get(col)) for r in rows if isinstance(r, dict)]
234
+ if len(keys) != len(rows) or any(k is None for k in keys):
235
+ continue
236
+ if len(set(keys)) == len(keys) and keys == sorted(keys):
237
+ return col
238
+ return None
239
+
240
+
241
+ def _has_rollup_row(vals: list[float]) -> bool:
242
+ """True when one value equals the sum of the others — the rows are components of a total,
243
+ not peers, so ranking or combining them double-counts."""
244
+ for i, v in enumerate(vals):
245
+ others = sum(x for j, x in enumerate(vals) if j != i)
246
+ if abs(v - others) <= max(1.0, abs(v) * 0.001):
247
+ return True
248
+ return False
249
+
250
+
251
+ def _pct(delta: float, base: float) -> float | None:
252
+ """Percent change, or None when the base is zero/absent — a percentage off a zero base is
253
+ meaningless (it is how "-100.3% YoY" got reported as a business fact off a broken load)."""
254
+ if not base:
255
+ return None
256
+ return round(delta / abs(base) * 100.0, 1)
257
+
258
+
259
+ # --------------------------------------------------------------------------- #
260
+ # Fact producers
261
+ # --------------------------------------------------------------------------- #
262
+ # The metric's identity is often NOT in the column name. Every `fn_rca_*_trend` in this warehouse
263
+ # returns the same generic `actual` / `budget` / `prior_year`, and names what it measures in the
264
+ # FUNCTION: `fn_rca_opendental_hyg_rev_per_patient_trend`. So a per-patient ratio arrived in a
265
+ # column called `actual`, `_NON_ADDITIVE` saw nothing to object to, and twelve months of "hygiene
266
+ # revenue per patient" were added together into a "Total: 10,614.9" — a number that is not a
267
+ # quantity of anything. Set by the caller from the evidence's tool name; blank keeps the old
268
+ # column-only behaviour, so nothing that does not pass it can change.
269
+ _CONTEXT_NON_ADDITIVE = _NON_ADDITIVE
270
+
271
+
272
+ def _is_additive(col: str, source: str = "") -> bool:
273
+ """Can this column be summed across rows? The COLUMN name decides, and the SOURCE can veto."""
274
+ if _NON_ADDITIVE.search(col or ""):
275
+ return False
276
+ # Only a GENERIC column defers to its source. A column that names its own measure is trusted:
277
+ # `fn_..._per_patient_trend` also returns real money columns, and letting the function name
278
+ # veto them all would delete every legitimate total the warehouse ships.
279
+ if source and col.lower() in _GENERIC_MEASURE and _CONTEXT_NON_ADDITIVE.search(source):
280
+ return False
281
+ return True
282
+
283
+
284
+ # Columns that carry no measure identity of their own — whatever the operation happens to return.
285
+ _GENERIC_MEASURE = frozenset({
286
+ "actual", "value", "current", "current_value", "curr_val", "amount", "total",
287
+ "budget", "prior_year", "prior", "prior_value", "last_year", "last_year_val",
288
+ })
289
+
290
+
291
+ def column_totals(rows: list[dict], source: str = "") -> list[Fact]:
292
+ """Sum every additive numeric column — TIME SERIES ONLY.
293
+
294
+ Gated on `period_column` because a total is only meaningful when the rows are successive
295
+ periods of the same measure. See the guard note above for the failure this prevents.
296
+
297
+ `source` is the operation the rows came from, and it exists because being a time series is not
298
+ on its own a licence to add: twelve successive months of hygiene revenue PER PATIENT are a
299
+ perfectly good series whose sum means nothing. The measure's name lives in the function, not in
300
+ the generic `actual` column, so without it the guard has nothing to see (`_is_additive`).
301
+ """
302
+ period = period_column(rows)
303
+ if not period:
304
+ return []
305
+ span = f"{label_of(rows[0])} to {label_of(rows[-1])}"
306
+ return [Fact(f"{col}: total", round(sum(vals), 2), f"sum of {len(vals)} periods, {span}")
307
+ for col, vals in numeric_columns(rows).items()
308
+ if len(vals) >= 2 and _is_additive(col, source) and _classify(col)[0] != "pct"]
309
+
310
+
311
+ def _pairs_in(cols) -> list[tuple[str, str, str]]:
312
+ """Every `(actual_column, comparison_column, against)` pair implied by the column names."""
313
+ roles = {c: _classify(c) for c in cols}
314
+ actuals = {stem: c for c, (role, stem) in roles.items() if role == "actual"}
315
+ # Flat single-actual fallback: `curr_val` + `delta` don't share a stem, but if the table has
316
+ # exactly one actual-like column the pairing is unambiguous.
317
+ lone = next(iter(actuals.values())) if len(actuals) == 1 else None
318
+ out = []
319
+ for col, (role, stem) in roles.items():
320
+ if role not in ("delta_ly", "delta_budget", "delta", "level_prior", "level_budget"):
321
+ continue
322
+ actual_col = actuals.get(stem) or lone
323
+ if actual_col and actual_col != col:
324
+ out.append((actual_col, col, {"delta_ly": "last year", "level_prior": "last year",
325
+ "delta_budget": "budget", "level_budget": "budget"
326
+ }.get(role, "comparison")))
327
+ return out
328
+
329
+
330
+ def _compare(actual: float, other: float, is_delta: bool) -> tuple[float, float]:
331
+ """`(change, base)` for one actual/comparison pair.
332
+
333
+ The two warehouse shapes differ and conflating them halves or doubles every percentage:
334
+ * DELTA column (`rev_vs_ly`) — the change is GIVEN; the comparison level is `actual - change`.
335
+ * LEVEL column (`prior_year_actual`) — the change is `actual - level`.
336
+ """
337
+ return (other, actual - other) if is_delta else (round(actual - other, 2), other)
338
+
339
+
340
+ def row_comparison_facts(rows: list[dict], focus: "tuple | set" = ()) -> list[Fact]:
341
+ """Per-row change and percent change against each comparison column.
342
+
343
+ Always safe: every figure comes from a single row, so no cross-row addition happens and the
344
+ homogeneity of the rows is irrelevant. Emitted exhaustively for a small result (a KPI-card
345
+ list, a handful of locations); larger grids are covered by `contribution_facts` instead, which
346
+ surfaces the movers that matter rather than all N rows.
347
+
348
+ Skipped entirely for a TIME SERIES. There, the aggregates (year-to-date total, change versus
349
+ last year over the whole span, growth across the period) ARE the story, and the per-period
350
+ detail is already visible in the rendered table. Emitting twelve monthly comparisons alongside
351
+ them measurably backfired: the model quoted two arbitrary months and omitted the total and the
352
+ year-over-year change entirely, because the useful four facts were buried under a dozen
353
+ lookalikes. Fewer, better facts beat more facts.
354
+ """
355
+ if period_column(rows) or not rows:
356
+ return []
357
+ if len(rows) > SMALL_TABLE:
358
+ # A BIG GRID still owes its comparisons to the rows the QUESTION is about. Suppressing
359
+ # them wholesale was measured as the largest single scoring defect: `fn_category_view` has
360
+ # no location parameter, so "the category view for Indianapolis, Fort Wayne and Clyde"
361
+ # fetches all 41 locations, per-row facts were dropped, and the answer could only state
362
+ # "revenue actual is 0.0, staff actual is 0.0 …" — no versus-last-year, no versus-budget,
363
+ # because the prompt forbids computing them and none had been supplied. Seven cases in one
364
+ # run stated no comparison at all while the data carried it.
365
+ #
366
+ # `focus` names the entities the question asked about; only those rows get their
367
+ # comparisons, so the "fewer, better facts" reasoning below still holds for the rest.
368
+ if not focus:
369
+ return []
370
+ want = {f.strip().lower() for f in focus if f and len(f.strip()) >= 4}
371
+ rows = [r for r in rows
372
+ if any(w in label_of(r).lower() for w in want)][:SMALL_TABLE]
373
+ if not rows:
374
+ return []
375
+ pairs = _pairs_in(numeric_columns(rows))
376
+ multi = len(rows) > 1
377
+ out: list[Fact] = []
378
+ for row in rows:
379
+ who = f"{label_of(row)}: " if multi else ""
380
+ for actual_col, other_col, against in pairs:
381
+ a, o = _num(row.get(actual_col)), _num(row.get(other_col))
382
+ if a is None or o is None:
383
+ continue
384
+ change, base = _compare(a, o, _classify(other_col)[0].startswith("delta"))
385
+ out.append(Fact(f"{who}{actual_col} vs {against}: change", round(change, 2),
386
+ f"{a:,.2f} against {o:,.2f}"))
387
+ if (p := _pct(change, base)) is not None:
388
+ out.append(Fact(f"{who}{actual_col} vs {against}: change %", p, unit="%",
389
+ basis=f"{change:,.2f} / {abs(base):,.2f}"))
390
+ return out
391
+
392
+
393
+ def series_comparison_facts(rows: list[dict], source: str = "") -> list[Fact]:
394
+ """Pooled change vs last year / budget over a TIME SERIES — the year-to-date comparison.
395
+
396
+ Gated on `period_column` for the same reason as `column_totals`: this sums each column before
397
+ differencing them, which is only meaningful across periods — and it takes the same `source`
398
+ veto, because a year-to-date comparison of two summed RATIOS is exactly as meaningless as the
399
+ total was. Both sums would be nonsense, so their difference is too.
400
+ """
401
+ if not period_column(rows):
402
+ return []
403
+ cols = {c: v for c, v in numeric_columns(rows).items() if _is_additive(c, source)}
404
+ sums = {c: round(sum(v), 2) for c, v in cols.items()}
405
+ span = f"{len(rows)} periods, {label_of(rows[0])} to {label_of(rows[-1])}"
406
+ out: list[Fact] = []
407
+ for actual_col, other_col, against in _pairs_in(cols):
408
+ change, base = _compare(sums[actual_col], sums[other_col],
409
+ _classify(other_col)[0].startswith("delta"))
410
+ out.append(Fact(f"{actual_col} vs {against}: change over {len(rows)} periods",
411
+ round(change, 2), f"{sums[actual_col]:,.2f} against {sums[other_col]:,.2f}, {span}"))
412
+ if (p := _pct(change, base)) is not None:
413
+ out.append(Fact(f"{actual_col} vs {against}: change % over {len(rows)} periods", p,
414
+ unit="%", basis=f"{change:,.2f} / {abs(base):,.2f}"))
415
+ return out
416
+
417
+
418
+ def margin_facts(rows: list[dict]) -> list[Fact]:
419
+ """Margin (profit ÷ revenue) for the row set, and the widest/narrowest per-row margins.
420
+
421
+ A per-location margin is what turns "revenue is flat" into "revenue is flat but the margin
422
+ fell 17 points" — a movement invisible in the level columns alone.
423
+ """
424
+ cols = numeric_columns(rows)
425
+ prof = next((c for c in cols if _PROFIT_COL.search(c) and _classify(c)[0] == "actual"), None)
426
+ rev = next((c for c in cols if _REVENUE_COL.search(c) and _classify(c)[0] == "actual"), None)
427
+ if not prof or not rev or prof == rev:
428
+ return []
429
+ out: list[Fact] = []
430
+ # An overall margin needs a pooled numerator and denominator, so it is allowed only where
431
+ # pooling is — across periods. Across arbitrary rows the per-row margins below stand alone.
432
+ if period_column(rows):
433
+ tot_p, tot_r = sum(cols[prof]), sum(cols[rev])
434
+ if tot_r:
435
+ out.append(Fact(f"{prof} margin (overall)", round(tot_p / tot_r * 100, 1), unit="%",
436
+ basis=f"{tot_p:,.2f} / {tot_r:,.2f} over {len(rows)} periods"))
437
+ per: list[tuple[float, str]] = []
438
+ for row in rows:
439
+ p, r = _num(row.get(prof)), _num(row.get(rev))
440
+ if p is not None and r:
441
+ per.append((round(p / r * 100, 1), label_of(row)))
442
+ if len(per) >= 2:
443
+ hi, lo = max(per), min(per)
444
+ out.append(Fact(f"{prof} margin: widest", hi[0], unit="%", basis=f"at {hi[1]}"))
445
+ out.append(Fact(f"{prof} margin: narrowest", lo[0], unit="%", basis=f"at {lo[1]}"))
446
+ out.append(Fact(f"{prof} margin: spread", round(hi[0] - lo[0], 1), unit="pp",
447
+ basis=f"{hi[1]} minus {lo[1]}"))
448
+ return out
449
+
450
+
451
+ def contribution_facts(rows: list[dict]) -> list[Fact]:
452
+ """Top-N contributors to a change, and how concentrated that change is.
453
+
454
+ This is the "three locations account for $1.1M of the decline" shape — the single most
455
+ load-bearing sentence in an executive briefing, and one the model must never estimate.
456
+ """
457
+ if len(rows) < 3:
458
+ return []
459
+ cols = numeric_columns(rows)
460
+ delta_col = next((c for c, (role, _) in ((c, _classify(c)) for c in cols)
461
+ if role in ("delta_ly", "delta_budget", "delta")), None)
462
+ if not delta_col:
463
+ return []
464
+ pairs = [(f, label_of(r)) for r in rows if (f := _num(r.get(delta_col))) is not None]
465
+ # Second guard, independent of the time-series rule: if one row equals the sum of the others,
466
+ # these rows are components of a rollup rather than peers, so ranking them and calling the
467
+ # leader a "top contributor" double-counts the total it is being measured against.
468
+ if len(pairs) < 3 or _has_rollup_row([v for v, _ in pairs]):
469
+ return []
470
+ falls = sorted((p for p in pairs if p[0] < 0), key=lambda p: p[0])
471
+ top = falls[:TOP_N]
472
+ out: list[Fact] = []
473
+ for val, name in top:
474
+ out.append(Fact(f"{delta_col}: {name}", round(val, 2), "row value"))
475
+ if not top:
476
+ return out
477
+ share = sum(v for v, _ in top)
478
+ out.append(Fact(f"{delta_col}: worst {len(top)} combined", round(share, 2),
479
+ basis=" + ".join(n for _, n in top)))
480
+ # Share is measured against the TOTAL DECLINE (the sum of the negative movers), not the net
481
+ # change. Against the net it exceeds 100% whenever gainers offset losers — arithmetically
482
+ # true, but "the top 3 are 108.7% of the change" reads as an error to an executive and
483
+ # invites them to distrust the rest of the briefing.
484
+ decline = sum(v for v, _ in falls)
485
+ # A share is only informative when something was left out; "the worst 3 are 100% of the
486
+ # decline" when exactly 3 rows declined states nothing and reads as padding.
487
+ if len(falls) > len(top) and (p := _pct(share, decline)) is not None:
488
+ out.append(Fact(f"{delta_col}: worst {len(top)} share of the total decline", abs(p),
489
+ unit="%", basis=f"{share:,.2f} of {decline:,.2f} across "
490
+ f"{len(falls)} declining rows"))
491
+ return out
492
+
493
+
494
+ def series_facts(rows: list[dict]) -> list[Fact]:
495
+ """First/last levels and the growth between them, for a time-ordered table.
496
+
497
+ Only fires when a period column is present AND rows are already in period order, which is how
498
+ the warehouse returns trends. Growth is stated over the actual span so "+11% over 12 months"
499
+ can never be mistaken for a monthly rate.
500
+ """
501
+ if len(rows) < 3 or not period_column(rows):
502
+ return []
503
+ out: list[Fact] = []
504
+ for col, vals in numeric_columns(rows).items():
505
+ if len(vals) < 3 or not _is_additive(col) or _classify(col)[0] in ("pct", "delta",
506
+ "delta_ly", "delta_budget"):
507
+ continue
508
+ first, last = vals[0], vals[-1]
509
+ span = f"{label_of(rows[0])} to {label_of(rows[-1])}"
510
+ out.append(Fact(f"{col}: change over the period", round(last - first, 2),
511
+ f"{last:,.2f} at {label_of(rows[-1])} minus {first:,.2f} at {label_of(rows[0])}"))
512
+ if (p := _pct(last - first, first)) is not None:
513
+ out.append(Fact(f"{col}: change over the period %", p, unit="%", basis=span))
514
+ return out
515
+
516
+
517
+ # --------------------------------------------------------------------------- #
518
+ # ATTRIBUTION — why a number moved, derived from structured data
519
+ # --------------------------------------------------------------------------- #
520
+ # The incumbent agent's most impressive answers quote manager commentary ("Dr. Peterson on personal
521
+ # leave since late Feb"). That data does not exist in this warehouse and cannot be obtained, so the
522
+ # causal story is reconstructed from the numbers instead — which is reproducible and checkable
523
+ # where a quoted note is neither.
524
+ #
525
+ # Two independent detectors, both self-gating on the SHAPE of the result, so neither needs to know
526
+ # which function produced it:
527
+ # * `decomposition_facts` — "the change splits into price / volume / mix"
528
+ # * `provider_continuity_facts` — "this earner's output stopped in March"
529
+
530
+ def _additive_identity(rows: list[dict]) -> tuple[str, list[str]] | None:
531
+ """Find `(total_column, [component_columns])` where the components SUM to the total in every
532
+ row — i.e. the result is already a decomposition of a change into its drivers.
533
+
534
+ Detected ARITHMETICALLY rather than by column name, so this works on any warehouse that ships
535
+ a decomposition under any naming convention. An identity that holds across every row of a
536
+ real result is not a coincidence: with 100+ rows a spurious 3-column match is vanishingly
537
+ unlikely, and the all-rows requirement is what makes name-free detection safe here.
538
+ """
539
+ cols = {c: v for c, v in numeric_columns(rows).items() if len(v) == len(rows)}
540
+ # A decomposition splits a CHANGE into its drivers, so every column in it — the total and each
541
+ # component — must be able to go negative. This is what separates the useful identity from the
542
+ # trivial one: a result carrying `curr_prod`, `prior_prod` and `total_delta` satisfies BOTH
543
+ # `total_delta = mix + price + volume` and `curr_prod = prior_prod + total_delta`, and the
544
+ # second is a bigger number but says nothing — it just restates that the level moved. Level
545
+ # columns are never negative; delta columns are, so requiring a negative excludes them.
546
+ signed = {c: v for c, v in cols.items() if any(x < 0 for x in v)}
547
+ if len(signed) < 3:
548
+ return None
549
+ names = sorted(signed)
550
+ best: tuple[str, list[str]] | None = None
551
+ for total in names:
552
+ others = [c for c in names if c != total]
553
+ if len(others) > 12: # keep the combinatorics bounded on a wide table
554
+ others = others[:12]
555
+ # THREE components minimum. A two-way split is an ACCOUNTING IDENTITY, not a decomposition
556
+ # of drivers: `revenue = ebitda + expenses` is exactly true on every row and says only that
557
+ # the books balance. Live, it was picked up on a financial trend and produced "change
558
+ # attributed to expenses" — a meaningless attribution, which then let the repair below
559
+ # overwrite an unrelated number in the prose. A driver split (price / volume / mix) has
560
+ # three or more.
561
+ for size in (3, 4):
562
+ for combo in itertools.combinations(others, size):
563
+ if all(abs(sum(r[c] for c in combo) - r[total]) <= max(0.01, abs(r[total]) * 1e-6)
564
+ for r in ({k: _num(v) for k, v in row.items()} for row in rows)
565
+ if all(r.get(c) is not None for c in combo) and r.get(total) is not None):
566
+ cand = (total, list(combo))
567
+ # Prefer the identity over the LARGEST total — among genuine change
568
+ # decompositions that is the headline one, not an internal sub-total.
569
+ if best is None or abs(sum(signed[total])) > abs(sum(signed[best[0]])):
570
+ best = cand
571
+ break
572
+ return best
573
+
574
+
575
+ def _shared_affix(names: list[str]) -> str:
576
+ """The common trailing token shared by every component column (`mix_delta`, `price_delta`,
577
+ `volume_delta` → `_delta`), so labels can be shortened to `mix` / `price` / `volume` without
578
+ hardcoding what the columns are called."""
579
+ parts = [n.split("_") for n in names]
580
+ if len(parts) < 2 or not all(len(p) > 1 for p in parts):
581
+ return ""
582
+ tail = parts[0][-1]
583
+ return f"_{tail}" if all(p[-1] == tail for p in parts) else ""
584
+
585
+
586
+ def decomposition_facts(rows: list[dict]) -> list[Fact]:
587
+ """Split a change into the drivers that produced it — the "why", not just the "what".
588
+
589
+ This is the analysis the incumbent explicitly told us it does NOT have ("a formal
590
+ volume/mix/price analysis is not available"), while our warehouse ships it per procedure code.
591
+ Rolling the per-item rows up to `total = A + B + C` turns a 162-row table nobody reads into
592
+ one sentence: the change was $X, of which $A came from price, $B from volume, and mix cost $C.
593
+ """
594
+ if len(rows) < 3:
595
+ return []
596
+ ident = _additive_identity(rows)
597
+ if not ident:
598
+ return []
599
+ total_col, comps = ident
600
+ cols = numeric_columns(rows)
601
+ total = round(sum(cols[total_col]), 2)
602
+ if not total:
603
+ return []
604
+ affix = _shared_affix(comps)
605
+
606
+ def nice(c: str) -> str:
607
+ return (c[:-len(affix)] if affix and c.endswith(affix) else c).replace("_", " ")
608
+
609
+ out = [Fact("total change", total, f"sum of {len(rows)} items")]
610
+ # Largest absolute contribution first — the driver that explains the move leads.
611
+ for comp in sorted(comps, key=lambda c: -abs(sum(cols[c]))):
612
+ val = round(sum(cols[comp]), 2)
613
+ out.append(Fact(f"change attributed to {nice(comp)}", val,
614
+ f"sum of {comp} across {len(rows)} items"))
615
+ # SIGNED share, unlike `contribution_facts`' share-of-a-decline: here the sign is the
616
+ # whole point. Price and volume ADDED to the move while mix SUBTRACTED from it, and an
617
+ # unsigned "62.3%" next to a negative dollar figure reads as a contradiction.
618
+ if (p := _pct(val, total)) is not None:
619
+ out.append(Fact(f"change attributed to {nice(comp)}: share of the move", p,
620
+ unit="%", basis=f"{val:,.2f} of {total:,.2f}"))
621
+ return out
622
+
623
+
624
+ # A provider/employee production series: one row per person per period. Detected structurally —
625
+ # a repeating period column plus a text key — never by column name.
626
+ _COLLAPSE_RATIO = 0.25 # below a quarter of the person's own median = a discontinuity, not a dip
627
+ _ARTIFACT_SHARE = 0.6 # this share of people collapsing AT ONCE means the PERIOD is incomplete
628
+
629
+
630
+ def _entity_period_value(rows: list[dict]) -> tuple[list[str], str, str] | None:
631
+ """`(entity_columns, period_column, value_column)` for a per-entity-per-period table."""
632
+ if len(rows) < 6 or not isinstance(rows[0], dict):
633
+ return None
634
+ period = next((c for c in rows[0]
635
+ if all(_as_period(r.get(c)) is not None for r in rows)
636
+ and len({_as_period(r.get(c)) for r in rows}) < len(rows)), None)
637
+ if not period:
638
+ return None # every period distinct → a plain trend, not this
639
+ entity = [c for c in rows[0]
640
+ if c != period and all(isinstance(r.get(c), str) for r in rows)]
641
+ nums = [c for c, v in numeric_columns(rows).items() if _is_additive(c) and len(v) == len(rows)]
642
+ if not entity or len(nums) != 1:
643
+ return None
644
+ return entity, period, nums[0]
645
+
646
+
647
+ def provider_continuity_facts(rows: list[dict]) -> list[Fact]:
648
+ """Detect an earner whose output STOPPED — the structured, dated shadow of the staffing event
649
+ a manager's note would have described.
650
+
651
+ Deliberately excludes an INCOMPLETE trailing period, detected structurally: if most people
652
+ collapse in the same final period, that is a partial data load, not simultaneous resignations.
653
+ Verified on the live warehouse — 9% of providers "collapsed" in each of the last two complete
654
+ months versus **100%** in the partial current month. Without this the detector would announce
655
+ that the entire clinical staff quit, every single month.
656
+
657
+ Reports nothing when nothing dropped out. That is the honest answer, and the whole point: it
658
+ is what lets an analysis say "the driver is not identifiable from the data" instead of
659
+ inventing a cause.
660
+ """
661
+ shape = _entity_period_value(rows)
662
+ if not shape:
663
+ return []
664
+ ent_cols, period, value = shape
665
+ series: dict[str, dict] = {}
666
+ for r in rows:
667
+ key = " ".join(str(r[c]).strip() for c in ent_cols if r.get(c))
668
+ p, v = _as_period(r.get(period)), _num(r.get(value))
669
+ if key and p and v is not None:
670
+ series.setdefault(key, {})[p] = v
671
+ periods = sorted({p for s in series.values() for p in s})
672
+ if len(periods) < 4 or len(series) < 2:
673
+ return []
674
+
675
+ def collapsed(at, known: list) -> list[tuple[str, float, float]]:
676
+ """(name, own median before `at`, value at `at`) for everyone who fell off a cliff.
677
+
678
+ A missing row is only read as zero when the person was ACTIVE IN THE PERIOD IMMEDIATELY
679
+ BEFORE — that is what makes it a stop rather than an absence. Without that check, anyone
680
+ who left years ago has no rows at all, reads as zero forever, and is re-reported as having
681
+ "just stopped" in whatever period is currently last: three long-departed providers were
682
+ announced as June departures on the live data.
683
+ """
684
+ idx = known.index(at)
685
+ if idx == 0:
686
+ return []
687
+ just_before = known[idx - 1]
688
+ hits = []
689
+ for name, s in series.items():
690
+ prior = [v for p, v in s.items() if p < at]
691
+ was_active = s.get(just_before, 0.0)
692
+ if len(prior) < 3 or was_active <= 1000:
693
+ continue
694
+ base = statistics.median(prior[-6:]) # their own recent run-rate, not a lifetime mean
695
+ cur = s.get(at, 0.0)
696
+ if base > 1000 and cur < base * _COLLAPSE_RATIO:
697
+ hits.append((name, round(base, 2), round(cur, 2)))
698
+ return hits
699
+
700
+ last = periods[-1]
701
+ # Everyone who was active in the period before `last` is a candidate to have stopped at it;
702
+ # if MOST of them appear to have stopped at once, the period is a partial load, not an exodus.
703
+ eligible = sum(1 for s in series.values()
704
+ if len(s) >= 4 and s.get(periods[-2], 0.0) > 1000)
705
+ if eligible and len(collapsed(last, periods)) >= _ARTIFACT_SHARE * eligible:
706
+ periods = periods[:-1] # partial load — not a staffing event
707
+ if len(periods) < 4:
708
+ return []
709
+ last = periods[-1]
710
+
711
+ hits = sorted(collapsed(last, periods), key=lambda h: -(h[1] - h[2]))[:TOP_N]
712
+ when = f"{last[0]}-{last[1]:02d}"
713
+ return [Fact(f"production stopped: {name}", round(cur, 2),
714
+ f"ran at {base:,.2f}/period before {when}, then {cur:,.2f} in {when}")
715
+ for name, base, cur in hits]
716
+
717
+
718
+ # --------------------------------------------------------------------------- #
719
+ # DATA QUALITY — values that are almost certainly a loading fault, not a result
720
+ # --------------------------------------------------------------------------- #
721
+ # A broken load looks exactly like a catastrophic business event, and the difference is invisible
722
+ # to grounding: the figure IS in the data, so a "-100.3% year over year" reads as fully verified.
723
+ # Measured on this warehouse — enterprise revenue runs ~$5.2M/month for ten months, then reports
724
+ # -$32,063 and -$13,748, and expenses go NEGATIVE. Left unflagged, an answer reports that as a
725
+ # collapse and an executive acts on it.
726
+ #
727
+ # Every check is convention-free. Nothing here knows that "revenue" should be positive or that
728
+ # "expenses" are stored negative — sign conventions differ per table and per warehouse. What it
729
+ # knows is that a column which held ONE sign for months does not legitimately flip, and that a
730
+ # metric running at millions does not legitimately land near zero. Both are properties of the
731
+ # series, not of the domain.
732
+
733
+ COLLAPSE_FLOOR = 0.10 # under a tenth of the established run-rate is a fault, not a bad month
734
+ STABLE_MIN = 4 # periods needed before "normal" is established at all
735
+ ZERO_SHARE = 0.6 # this share of entities reporting nothing = the load, not the business
736
+
737
+
738
+ @dataclass(frozen=True)
739
+ class Flag:
740
+ """One data-quality finding. `where` locates it, `detail` states the evidence for it."""
741
+ kind: str # collapse | sign_flip | all_null | zero_population
742
+ column: str
743
+ where: str
744
+ detail: str
745
+
746
+ def render(self) -> str:
747
+ return f"{self.column} — {self.detail} ({self.where})"
748
+
749
+
750
+ def _series_flags(rows: list[dict], period: str) -> list[Flag]:
751
+ """Trailing periods where a column breaks from its own established behaviour."""
752
+ out: list[Flag] = []
753
+ # Label from the PERIOD column itself, not `label_of` — the period is frequently a `date`
754
+ # object, and `label_of` only considers string fields, so every flag came out labelled "?".
755
+ labels = []
756
+ for r in rows:
757
+ p = _as_period(r.get(period))
758
+ labels.append(f"{p[0]}-{p[1]:02d}" if p else label_of(r))
759
+ for col, vals in numeric_columns(rows).items():
760
+ if len(vals) != len(rows) or not _is_additive(col) or _classify(col)[0] == "pct":
761
+ continue
762
+ bad: list[tuple[int, str]] = []
763
+ for i in range(len(vals) - 1, STABLE_MIN - 1, -1):
764
+ prior = vals[:i]
765
+ if len(prior) < STABLE_MIN:
766
+ break
767
+ base = statistics.median([abs(v) for v in prior])
768
+ if base < 1000: # too small for "collapse" to mean anything
769
+ break
770
+ signs = {v > 0 for v in prior if v}
771
+ flipped = len(signs) == 1 and vals[i] and (vals[i] > 0) not in signs
772
+ collapsed = abs(vals[i]) < base * COLLAPSE_FLOOR
773
+ if not (flipped or collapsed):
774
+ break # the trailing run of anomalies ends here
775
+ bad.append((i, "sign_flip" if flipped else "collapse"))
776
+ for i, kind in reversed(bad):
777
+ base = statistics.median([abs(v) for v in vals[:i]])
778
+ out.append(Flag(
779
+ kind=kind, column=col, where=labels[i],
780
+ detail=(f"{vals[i]:,.2f} against a {base:,.2f} run-rate over the preceding "
781
+ f"{i} periods"
782
+ + (" — and the opposite sign to every one of them" if kind == "sign_flip"
783
+ else ""))))
784
+ return out
785
+
786
+
787
+ def _population_flags(rows: list[dict]) -> list[Flag]:
788
+ """A per-entity result where most entities report nothing.
789
+
790
+ Deliberately a NOTE, never a rejection: a real location with no activity legitimately returns
791
+ zero, so this reports the shape of the result and lets the reader judge. It is a majority
792
+ threshold precisely so a handful of genuinely quiet entities never trips it.
793
+ """
794
+ out: list[Flag] = []
795
+ for col, vals in numeric_columns(rows).items():
796
+ if len(vals) != len(rows) or len(vals) < 5 or not _is_additive(col):
797
+ continue
798
+ if _classify(col)[0] != "actual": # a delta is legitimately zero for a flat entity
799
+ continue
800
+ quiet = sum(1 for v in vals if abs(v) < 1)
801
+ if quiet >= ZERO_SHARE * len(vals) and max(abs(v) for v in vals) > 1000:
802
+ out.append(Flag(kind="zero_population", column=col,
803
+ where=f"{quiet} of {len(vals)} rows",
804
+ detail="report no value while others in the same result do"))
805
+ return out
806
+
807
+
808
+ def data_quality_flags(rows: list[dict]) -> list[Flag]:
809
+ """Values that are almost certainly a data fault rather than a result. See the note above.
810
+
811
+ Returns [] for healthy data, which is the common case and costs nothing.
812
+ """
813
+ rows = [r for r in (rows or []) if isinstance(r, dict)]
814
+ if len(rows) < 2:
815
+ return []
816
+ out: list[Flag] = []
817
+ try:
818
+ if period := period_column(rows):
819
+ out += _series_flags(rows, period)
820
+ else:
821
+ out += _population_flags(rows)
822
+ # A column declared by the result but null in EVERY row — the model otherwise reports the
823
+ # metric as though it were measured and found to be nothing.
824
+ keys = {k for r in rows for k in r}
825
+ numeric = set(numeric_columns(rows))
826
+ for k in sorted(keys - numeric):
827
+ if all(r.get(k) is None for r in rows):
828
+ out.append(Flag(kind="all_null", column=k, where=f"all {len(rows)} rows",
829
+ detail="present in the result but empty throughout"))
830
+ except Exception: # a quality check must never be the reason an answer fails
831
+ return out
832
+ return out[:6]
833
+
834
+
835
+ # --------------------------------------------------------------------------- #
836
+ # Public entry points
837
+ # --------------------------------------------------------------------------- #
838
+ def derive(rows: list[dict], cap: int = MAX_FACTS_PER_ITEM,
839
+ focus: "tuple | set" = (), source: str = "",
840
+ cumulative: bool = False) -> list[Fact]:
841
+ """Every derived fact for one result set, de-duplicated and capped.
842
+
843
+ Ordered by usefulness to an executive summary, because `cap` truncates the tail: the pooled
844
+ comparisons and contributions carry the story ("down $4.2M, -11.5% versus last year"; "three
845
+ locations are 60% of the decline"), so they must survive the cap. Exhaustive per-row facts go
846
+ last and simply spend whatever budget remains — on a 12-month trend they would otherwise
847
+ crowd out the year-to-date totals they are far less useful than.
848
+
849
+ `cumulative` says each row ALREADY CONTAINS the rows before it — a trailing-twelve-month or
850
+ to-date series. Summing those double-counts every period: measured, a TTM revenue trend was
851
+ added up into "Total: $652,831,245.13", roughly twelve times the revenue that exists, and the
852
+ same rolling values were narrated as "the monthly actual revenue figures" when a month is
853
+ ~$5M and the rows read ~$57M. Neither the column name (`actual`) nor the function name
854
+ (`fn_income_statement_trend`) says so — the fact lives in the ARGUMENT (`p_time_period=TTM`),
855
+ which is why `_is_additive`'s column/source test cannot see it and the caller must pass it.
856
+ """
857
+ rows = [r for r in (rows or []) if isinstance(r, dict)]
858
+ if not rows:
859
+ return []
860
+ facts: list[Fact] = []
861
+ # ATTRIBUTION FIRST. When a result carries a decomposition or a dropped-out earner, that IS
862
+ # the answer to "why did this move" — it must survive the cap ahead of the ordinary totals and
863
+ # comparisons, which are merely restating what the table already shows.
864
+ # WHEN THE QUESTION NAMES ENTITIES, their own comparisons lead. `row_comparison_facts` runs
865
+ # last by default — right for a briefing, where the aggregates carry the story — but the cap
866
+ # then truncates before reaching it, so a question about three named locations got the
867
+ # enterprise decomposition and none of its own figures. Asked for, so first.
868
+ order = (decomposition_facts, provider_continuity_facts,
869
+ series_comparison_facts, contribution_facts, margin_facts, series_facts,
870
+ column_totals, row_comparison_facts)
871
+ if focus:
872
+ order = (row_comparison_facts,) + tuple(p for p in order if p is not row_comparison_facts)
873
+ if cumulative:
874
+ # The two producers that ADD ACROSS ROWS. Everything else — per-row comparisons, margins,
875
+ # extremes — stays: a rolling series has a perfectly good highest and lowest month, and
876
+ # that is most of what a trend question wants.
877
+ order = tuple(p for p in order if p not in (column_totals, series_comparison_facts))
878
+ for producer in order:
879
+ try:
880
+ if producer is row_comparison_facts:
881
+ facts.extend(producer(rows, focus))
882
+ elif producer in (column_totals, series_comparison_facts):
883
+ facts.extend(producer(rows, source)) # these two add ACROSS periods
884
+ else:
885
+ facts.extend(producer(rows))
886
+ except Exception: # a derived fact is an enhancement; never let one break synthesis
887
+ continue
888
+ seen, out = set(), []
889
+ for f in facts:
890
+ if f.label in seen:
891
+ continue
892
+ seen.add(f.label)
893
+ out.append(f)
894
+ return out[:cap]
895
+
896
+
897
+ def facts_block(by_item: list[tuple[str, list[Fact]]]) -> str:
898
+ """The DERIVED FACTS section appended to the synthesis prompt.
899
+
900
+ Deliberately emphatic about provenance: the model's standing instruction is that it may not
901
+ compute anything, so these have to read as values it is permitted to quote — otherwise it
902
+ omits them and we are back to the min/max/average ceiling.
903
+ """
904
+ blocks = [f"[{eid}]\n" + "\n".join(f" - {f.render()}" for f in facts)
905
+ for eid, facts in by_item if facts]
906
+ if not blocks:
907
+ return ""
908
+ return (
909
+ "\n\nDERIVED FACTS — computed by CODE from the evidence above, VERIFIED and safe to quote. "
910
+ "These are the ONLY derived figures you may state (totals, changes, percentages, margins, "
911
+ "shares). Use them verbatim — do NOT recompute, adjust or round them differently, and do "
912
+ "NOT derive any figure that is not listed here. Each is followed by the arithmetic that "
913
+ "produced it; quote the figure, not the arithmetic:\n" + "\n\n".join(blocks)
914
+ )
915
+
916
+
917
+ # Facts an executive summary is incomplete without, most important first: the movement against
918
+ # last year, then against budget, then the period total, then growth across the span. Matched as
919
+ # SUBSTRINGS — the labels carry a trailing qualifier ("change over 12 periods"), and an
920
+ # `endswith` test silently dropped the single most important fact of the set.
921
+ _HEADLINE = ("vs last year: change", "vs budget: change", ": total", ": change over the period")
922
+
923
+
924
+ def headline_facts(facts: list[Fact], limit: int = 3) -> list[Fact]:
925
+ """The few facts a summary must not omit, most important first.
926
+
927
+ Each absolute figure is returned with its percentage immediately after it, so the guard can
928
+ never emit a bare "-2.6%" with nothing to anchor it. `limit` counts absolute figures, not
929
+ list entries — the percentages ride along.
930
+ """
931
+ out: list[Fact] = []
932
+ for key in _HEADLINE:
933
+ for f in facts:
934
+ # A total of the COMPARISON column (last year's, budget's) is not a headline — it is
935
+ # the base the headline is measured against, and printing both reads as clutter.
936
+ if key == ": total" and _classify(f.label.split(":")[0])[0] != "actual":
937
+ continue
938
+ if key in f.label and f.unit != "%" and f not in out:
939
+ out.append(f)
940
+ # The percentage's label is the absolute one with "%" inserted, so it does not
941
+ # start with it — pair by normalising the "%" away rather than by prefix.
942
+ pct = next((p for p in facts
943
+ if p.unit == "%" and p.label.replace(" %", "") == f.label), None)
944
+ if pct:
945
+ out.append(pct)
946
+ if sum(1 for x in out if x.unit != "%") >= limit:
947
+ return out
948
+ return out
949
+
950
+
951
+ def _is_movement(f: Fact) -> bool:
952
+ """A change carries an explicit sign; a level does not."""
953
+ return "change" in f.label or " vs " in f.label
954
+
955
+
956
+ def _fmt(f: Fact, money: str) -> str:
957
+ if f.unit == "%":
958
+ return f"{f.value:+,.1f}%"
959
+ if f.unit == "pp":
960
+ return f"{f.value:+,.1f} pp"
961
+ body = f"{money}{abs(f.value):,.2f}"
962
+ if f.value < 0:
963
+ return f"-{body}"
964
+ return f"+{body}" if _is_movement(f) else body
965
+
966
+
967
+ def headline_facts_by_item(by_item: list[tuple[str, list[Fact]]], limit: int = 3) -> list[Fact]:
968
+ """Headline facts from ONE evidence item — the first one that has any. NEVER blended across
969
+ items, and this is the entry point `agent.py` must call (not `headline_facts` on a pooled
970
+ list).
971
+
972
+ The hazard: two evidence items of the same shape (this year's trend, fetched alongside last
973
+ year's trend as its own call for comparison) produce IDENTICALLY LABELED facts with different
974
+ values — both are "actual vs last year: change", one -$2.5M, the other +$2.7M for an unrelated
975
+ older window. `derive()` dedupes by label WITHIN one item, so the collision only exists once
976
+ facts from several items are pooled. Pooling let the wrong item's figure outrank the one the
977
+ narrative actually discussed, appended right beside a different number under the identical
978
+ name — confusing at best, and it silently mispairs the percentage too, since a label-based
979
+ pairing search finds whichever item's percentage comes first, not the one beside the value
980
+ being paired. Scoping to one item at a time is the same principle as this codebase's existing
981
+ ONE SOURCE PER TABLE rule (`agent.py`): each evidence item tells its own story.
982
+ """
983
+ for _eid, facts in by_item:
984
+ if heads := headline_facts(facts, limit):
985
+ return heads
986
+ return []
987
+
988
+
989
+ def render_headline(facts: list[Fact], currency: bool = True) -> str:
990
+ """One compact line of headline figures, each change followed by its percentage in brackets.
991
+
992
+ Pairing normalises the "%" away rather than testing a prefix: the percentage's label is the
993
+ absolute one with "%" INSERTED (`change % over 12 periods`), so it never starts with the
994
+ absolute label, and a prefix test silently split the pair across two clauses.
995
+ """
996
+ money = "$" if currency else ""
997
+ parts, i = [], 0
998
+ while i < len(facts):
999
+ f = facts[i]
1000
+ nxt = facts[i + 1] if i + 1 < len(facts) else None
1001
+ paired = (nxt is not None and nxt.unit == "%"
1002
+ and nxt.label.replace(" %", "") == f.label)
1003
+ tail = f" ({nxt.value:+,.1f}%)" if paired else ""
1004
+ parts.append(f"{_clean(f.label)} {_fmt(f, money)}{tail}")
1005
+ i += 2 if paired else 1
1006
+ return " · ".join(parts)
1007
+
1008
+
1009
+ def _clean(label: str) -> str:
1010
+ """A fact label → a short phrase a reader can parse without the column name.
1011
+
1012
+ `actual vs last year: change over 12 periods` → `vs last year`
1013
+ `prior_year: total` → `prior year total`
1014
+ `actual: change over the period` → `change across the period`
1015
+ """
1016
+ s = re.sub(r"\s*over \d+ periods$", "", label)
1017
+ s = re.sub(r"^actual\b\s*", "", s) # the default column name says nothing
1018
+ s = s.replace(": change over the period", "change across the period")
1019
+ s = re.sub(r":\s*change$", "", s)
1020
+ s = s.replace(": total", " total")
1021
+ return re.sub(r"\s+", " ", s.replace("_", " ")).strip(": ").strip() or label
1022
+
1023
+
1024
+ def fact_values(by_item: list[tuple[str, list[Fact]]]) -> list[float]:
1025
+ """Every derived value, for the grounding pool.
1026
+
1027
+ Without this the model quotes a figure we handed it and the grounding check flags its own
1028
+ verified number as unsupported — which then sinks the rubric and can trigger an abstain.
1029
+ """
1030
+ return [f.value for _eid, facts in by_item for f in facts]