consequence-ledger 0.3.0__tar.gz → 0.3.1__tar.gz

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.
Files changed (52) hide show
  1. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/PKG-INFO +8 -6
  2. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/README.md +7 -5
  3. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/consequence_ledger.egg-info/PKG-INFO +8 -6
  4. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/consequence_ledger.egg-info/SOURCES.txt +1 -0
  5. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/__init__.py +1 -1
  6. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/cli.py +12 -2
  7. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/report.py +67 -1
  8. consequence_ledger-0.3.1/tests/test_report_segments.py +73 -0
  9. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/LICENSE +0 -0
  10. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/consequence_ledger.egg-info/dependency_links.txt +0 -0
  11. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/consequence_ledger.egg-info/entry_points.txt +0 -0
  12. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/consequence_ledger.egg-info/top_level.txt +0 -0
  13. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/__main__.py +0 -0
  14. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/audit.py +0 -0
  15. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/db.py +0 -0
  16. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/demo.py +0 -0
  17. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/estimate.py +0 -0
  18. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/gitio.py +0 -0
  19. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/incidents.py +0 -0
  20. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/label.py +0 -0
  21. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/pagerduty.py +0 -0
  22. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/plan.py +0 -0
  23. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/policy.py +0 -0
  24. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/segments.py +0 -0
  25. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/sentry.py +0 -0
  26. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/squash.py +0 -0
  27. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/ledger/status.py +0 -0
  28. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/pyproject.toml +0 -0
  29. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/setup.cfg +0 -0
  30. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_audit.py +0 -0
  31. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_cache.py +0 -0
  32. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_cli.py +0 -0
  33. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_crossfit.py +0 -0
  34. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_degenerate.py +0 -0
  35. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_demo.py +0 -0
  36. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_errors.py +0 -0
  37. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_estimate.py +0 -0
  38. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_filters.py +0 -0
  39. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_incidents.py +0 -0
  40. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_journey.py +0 -0
  41. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_label.py +0 -0
  42. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_pagerduty.py +0 -0
  43. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_parallel.py +0 -0
  44. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_plan.py +0 -0
  45. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_policy.py +0 -0
  46. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_release.py +0 -0
  47. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_robust.py +0 -0
  48. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_security.py +0 -0
  49. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_segments.py +0 -0
  50. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_sentry.py +0 -0
  51. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_squash.py +0 -0
  52. {consequence_ledger-0.3.0 → consequence_ledger-0.3.1}/tests/test_status.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: consequence-ledger
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Measure what AI coding decisions actually cause: randomized routing, delayed outcome labels, causal estimates.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/sujan-mitta/consequence-ledger
@@ -38,12 +38,14 @@ It is the 90-day MVP from the "AI elephant in the room" research: decision loggi
38
38
  Python 3.9+ and git 2.28+. There are no dependencies.
39
39
 
40
40
  ```bash
41
- git clone https://github.com/sujan-mitta/consequence-ledger.git
42
- cd consequence-ledger
43
- pip install . # gives you the `ledger` command; or skip this and use `python -m ledger` from the folder
41
+ pip install consequence-ledger # gives you the `ledger` command
44
42
  ledger --version
45
43
  ```
46
44
 
45
+ New to it? [docs/QUICKSTART.md](docs/QUICKSTART.md) takes you from install to a first report on your own repo in about five minutes,
46
+ and [builder.html](builder.html) is a form that writes the command for you. To work from source instead:
47
+ `git clone https://github.com/sujan-mitta/consequence-ledger.git`, then `pip install .` inside the folder.
48
+
47
49
  ## Quick start
48
50
 
49
51
  ```bash
@@ -103,7 +105,7 @@ python -m ledger recommend --stratum difficulty # and what routing by th
103
105
  | `estimate` | Naive, IPW, self-normalised IPW, direct method and doubly robust estimates per arm. Bootstrap and influence-function CIs, effective sample size, minimum propensity, and a check that unmerged work is balanced across arms. `--cluster developer` resamples whole clusters when outcomes are correlated. |
104
106
  | `breakdown` | Which tool is better for which kind of task (`--stratum difficulty`). Raw and shrunk effects per stratum with Bonferroni-adjusted p-values, so a lucky stratum is not mistaken for a pattern. |
105
107
  | `recommend` | What routing each task type to its best tool would be worth. The tool is chosen on one half of your decisions and judged on the other, so the gain is not inflated; it also shows the gain from simply using the best tool. |
106
- | `report` | A self-contained HTML report (light and dark). |
108
+ | `report` | A self-contained HTML report (light and dark). Includes the `breakdown` and `recommend` tables once you have run them. |
107
109
  | `audit` | Per-tool baseline from history, with Wilson CIs and size-adjusted rates. |
108
110
  | `demo` | Synthetic end-to-end run that checks labels against an oracle. |
109
111
 
@@ -169,7 +171,7 @@ Full details are in [research/FINDINGS.md](research/FINDINGS.md):
169
171
  ```
170
172
  ledger/ the package: policy (routing), label + squash (outcomes), estimate, audit, plan, status,
171
173
  incidents + sentry + pagerduty, report, demo, db, gitio, cli
172
- tests/ 230 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
174
+ tests/ 236 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
173
175
  tools/ coverage_lite.py, dependency-free line coverage
174
176
  docs/ METHODOLOGY.md
175
177
  research/ simulate.py (coverage, power, noise, design, peeking studies), results/, FINDINGS.md
@@ -11,12 +11,14 @@ It is the 90-day MVP from the "AI elephant in the room" research: decision loggi
11
11
  Python 3.9+ and git 2.28+. There are no dependencies.
12
12
 
13
13
  ```bash
14
- git clone https://github.com/sujan-mitta/consequence-ledger.git
15
- cd consequence-ledger
16
- pip install . # gives you the `ledger` command; or skip this and use `python -m ledger` from the folder
14
+ pip install consequence-ledger # gives you the `ledger` command
17
15
  ledger --version
18
16
  ```
19
17
 
18
+ New to it? [docs/QUICKSTART.md](docs/QUICKSTART.md) takes you from install to a first report on your own repo in about five minutes,
19
+ and [builder.html](builder.html) is a form that writes the command for you. To work from source instead:
20
+ `git clone https://github.com/sujan-mitta/consequence-ledger.git`, then `pip install .` inside the folder.
21
+
20
22
  ## Quick start
21
23
 
22
24
  ```bash
@@ -76,7 +78,7 @@ python -m ledger recommend --stratum difficulty # and what routing by th
76
78
  | `estimate` | Naive, IPW, self-normalised IPW, direct method and doubly robust estimates per arm. Bootstrap and influence-function CIs, effective sample size, minimum propensity, and a check that unmerged work is balanced across arms. `--cluster developer` resamples whole clusters when outcomes are correlated. |
77
79
  | `breakdown` | Which tool is better for which kind of task (`--stratum difficulty`). Raw and shrunk effects per stratum with Bonferroni-adjusted p-values, so a lucky stratum is not mistaken for a pattern. |
78
80
  | `recommend` | What routing each task type to its best tool would be worth. The tool is chosen on one half of your decisions and judged on the other, so the gain is not inflated; it also shows the gain from simply using the best tool. |
79
- | `report` | A self-contained HTML report (light and dark). |
81
+ | `report` | A self-contained HTML report (light and dark). Includes the `breakdown` and `recommend` tables once you have run them. |
80
82
  | `audit` | Per-tool baseline from history, with Wilson CIs and size-adjusted rates. |
81
83
  | `demo` | Synthetic end-to-end run that checks labels against an oracle. |
82
84
 
@@ -142,7 +144,7 @@ Full details are in [research/FINDINGS.md](research/FINDINGS.md):
142
144
  ```
143
145
  ledger/ the package: policy (routing), label + squash (outcomes), estimate, audit, plan, status,
144
146
  incidents + sentry + pagerduty, report, demo, db, gitio, cli
145
- tests/ 230 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
147
+ tests/ 236 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
146
148
  tools/ coverage_lite.py, dependency-free line coverage
147
149
  docs/ METHODOLOGY.md
148
150
  research/ simulate.py (coverage, power, noise, design, peeking studies), results/, FINDINGS.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: consequence-ledger
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Measure what AI coding decisions actually cause: randomized routing, delayed outcome labels, causal estimates.
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/sujan-mitta/consequence-ledger
@@ -38,12 +38,14 @@ It is the 90-day MVP from the "AI elephant in the room" research: decision loggi
38
38
  Python 3.9+ and git 2.28+. There are no dependencies.
39
39
 
40
40
  ```bash
41
- git clone https://github.com/sujan-mitta/consequence-ledger.git
42
- cd consequence-ledger
43
- pip install . # gives you the `ledger` command; or skip this and use `python -m ledger` from the folder
41
+ pip install consequence-ledger # gives you the `ledger` command
44
42
  ledger --version
45
43
  ```
46
44
 
45
+ New to it? [docs/QUICKSTART.md](docs/QUICKSTART.md) takes you from install to a first report on your own repo in about five minutes,
46
+ and [builder.html](builder.html) is a form that writes the command for you. To work from source instead:
47
+ `git clone https://github.com/sujan-mitta/consequence-ledger.git`, then `pip install .` inside the folder.
48
+
47
49
  ## Quick start
48
50
 
49
51
  ```bash
@@ -103,7 +105,7 @@ python -m ledger recommend --stratum difficulty # and what routing by th
103
105
  | `estimate` | Naive, IPW, self-normalised IPW, direct method and doubly robust estimates per arm. Bootstrap and influence-function CIs, effective sample size, minimum propensity, and a check that unmerged work is balanced across arms. `--cluster developer` resamples whole clusters when outcomes are correlated. |
104
106
  | `breakdown` | Which tool is better for which kind of task (`--stratum difficulty`). Raw and shrunk effects per stratum with Bonferroni-adjusted p-values, so a lucky stratum is not mistaken for a pattern. |
105
107
  | `recommend` | What routing each task type to its best tool would be worth. The tool is chosen on one half of your decisions and judged on the other, so the gain is not inflated; it also shows the gain from simply using the best tool. |
106
- | `report` | A self-contained HTML report (light and dark). |
108
+ | `report` | A self-contained HTML report (light and dark). Includes the `breakdown` and `recommend` tables once you have run them. |
107
109
  | `audit` | Per-tool baseline from history, with Wilson CIs and size-adjusted rates. |
108
110
  | `demo` | Synthetic end-to-end run that checks labels against an oracle. |
109
111
 
@@ -169,7 +171,7 @@ Full details are in [research/FINDINGS.md](research/FINDINGS.md):
169
171
  ```
170
172
  ledger/ the package: policy (routing), label + squash (outcomes), estimate, audit, plan, status,
171
173
  incidents + sentry + pagerduty, report, demo, db, gitio, cli
172
- tests/ 230 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
174
+ tests/ 236 unittest cases: real git repos in temp dirs, local fake servers, estimator Monte Carlo checks
173
175
  tools/ coverage_lite.py, dependency-free line coverage
174
176
  docs/ METHODOLOGY.md
175
177
  research/ simulate.py (coverage, power, noise, design, peeking studies), results/, FINDINGS.md
@@ -41,6 +41,7 @@ tests/test_parallel.py
41
41
  tests/test_plan.py
42
42
  tests/test_policy.py
43
43
  tests/test_release.py
44
+ tests/test_report_segments.py
44
45
  tests/test_robust.py
45
46
  tests/test_security.py
46
47
  tests/test_segments.py
@@ -1,2 +1,2 @@
1
1
  """Consequence Ledger: measure what AI coding decisions actually cause."""
2
- __version__ = "0.3.0"
2
+ __version__ = "0.3.1"
@@ -4,7 +4,8 @@ import datetime as dt
4
4
  import json
5
5
  import sys
6
6
  import tempfile
7
- from contextlib import closing
7
+ from contextlib import closing, redirect_stdout
8
+ from io import StringIO
8
9
  from pathlib import Path
9
10
 
10
11
  from . import __version__
@@ -298,9 +299,13 @@ def run_report(repo, out=None, primary="dr", title="Consequence Ledger"):
298
299
  stats = json.loads((d / "label.json").read_text())
299
300
  truth = json.loads((d / "truth.json").read_text()) if (d / "truth.json").exists() else None
300
301
  validation = json.loads((d / "validation.json").read_text()) if (d / "validation.json").exists() else None
302
+ extra = {}
303
+ for key, name in (("breakdown", "breakdown.json"), ("recommendation", "recommend.json")):
304
+ if (d / name).exists():
305
+ extra[key] = json.loads((d / name).read_text())
301
306
  out = Path(out or d / "report.html")
302
307
  out.write_text(report_mod.render(est, stats, title, primary, truth, {"repo": Path(repo).name},
303
- validation), encoding="utf-8")
308
+ validation, **extra), encoding="utf-8")
304
309
  return out
305
310
 
306
311
 
@@ -468,6 +473,11 @@ def cmd_demo(args):
468
473
  print(f"labeler vs oracle: precision {v['precision']:.2f}, recall {v['recall']:.2f} "
469
474
  f"(tp {v['tp']}, fp {v['fp']}, fn {v['fn']})")
470
475
  print("DR on oracle labels: " + ", ".join(f"{a} {e['value'] * 100:.1f}%" for a, e in v["oracle_dr"].items()))
476
+ seg = argparse.Namespace(repo=repo, stratum="stratum", cluster=None, baseline=None, clip=None,
477
+ folds=DEFAULT_FOLDS, json=False)
478
+ with redirect_stdout(StringIO()):
479
+ cmd_breakdown(seg)
480
+ cmd_recommend(seg)
471
481
  print(f"\nreport: {run_report(repo, title='Consequence Ledger demo')}")
472
482
 
473
483
 
@@ -241,6 +241,60 @@ def early_card(est, label_stats):
241
241
  f'<tbody>{rows}</tbody></table></div><p class="note">{trust}</p></div>')
242
242
 
243
243
 
244
+ def _pp(x):
245
+ return f"{x * 100:+.1f}&nbsp;pp"
246
+
247
+
248
+ def breakdown_card(bd, baseline, stratum, stale=""):
249
+ if not bd:
250
+ return ""
251
+ parts = []
252
+ for arm, d in bd.items():
253
+ rows = "".join(
254
+ f"<tr><td>{html.escape(str(x['stratum']))}</td><td>{x['n']}</td>"
255
+ f"<td>{_pp(x['effect'])} <span class=\"ci\">[{_pp(x['raw_lo'])}, {_pp(x['raw_hi'])}]</span></td>"
256
+ f"<td>{_pp(x['shrunk'])} <span class=\"ci\">[{_pp(x['shrunk_lo'])}, {_pp(x['shrunk_hi'])}]</span></td>"
257
+ f"<td>{x['p_adjusted']:.3f}</td></tr>"
258
+ for x in sorted(d["strata"], key=lambda x: x["shrunk"]))
259
+ verdict = ("At least one stratum differs from the rest, even after allowing for how many were examined."
260
+ if d["heterogeneous"] else
261
+ "No stratum stands out once you allow for how many were examined.")
262
+ parts.append(
263
+ f"<p><b>{html.escape(arm)}</b> against {html.escape(baseline)}: overall {_pp(d['overall'])}, "
264
+ f"spread between strata {d['tau'] * 100:.1f}&nbsp;pp. {verdict}</p>"
265
+ '<div class="table"><table><thead><tr><th>Stratum</th><th>Decisions</th><th>Raw effect [95% CI]</th>'
266
+ '<th>Shrunk effect [95% CI]</th><th>Adjusted p</th></tr></thead>'
267
+ f"<tbody>{rows}</tbody></table></div>")
268
+ return (f"<h2>Which tool for which kind of task</h2><div class=\"card\">{''.join(parts)}"
269
+ f"<p class=\"note\">Effects are changes in the bad-outcome rate, by <code>{html.escape(stratum)}</code>. "
270
+ "Shrunk effects pull noisy strata toward the overall effect; adjusted p-values allow for looking at "
271
+ f"every stratum. Negative is better than the baseline.{stale}</p></div>")
272
+
273
+
274
+ def recommend_card(rec, stratum, stale=""):
275
+ if not rec:
276
+ return ""
277
+ if rec["worth_it"]:
278
+ verdict = ("The interval is above zero: shifting routing toward these choices is likely to help. "
279
+ "Keep epsilon at 0.2 or more so later estimates stay valid.")
280
+ elif rec["best_single_lo"] > 0:
281
+ verdict = ("Any gain comes from one tool being better overall, not from matching tools to task types.")
282
+ else:
283
+ verdict = "The interval includes zero: no evidence yet that routing by task type beats what you did."
284
+ stable = ", ".join(f"{html.escape(str(s))} → {html.escape(a)}" for s, a in sorted(rec["stable_policy"].items()))
285
+ unstable = ", ".join(html.escape(str(s)) for s in rec["unstable_strata"])
286
+ return (
287
+ '<h2>What routing by task type is worth</h2><div class="card">'
288
+ f"<p>{rec['current_rate'] * 100:.1f}% of decisions went bad. Sending each <code>{html.escape(stratum)}</code> to its "
289
+ f"best tool, chosen on one half of the decisions and judged on the other, would have changed that by "
290
+ f"<b>{_pp(-rec['gain'])}</b> <span class=\"ci\">[{_pp(-rec['gain_hi'])}, {_pp(-rec['gain_lo'])}]</span>. "
291
+ f"Always using the single best tool: <b>{_pp(-rec['best_single_gain'])}</b> "
292
+ f"<span class=\"ci\">[{_pp(-rec['best_single_hi'])}, {_pp(-rec['best_single_lo'])}]</span>.</p>"
293
+ + (f"<p>Choices that held in both halves: {stable}.</p>" if stable else "")
294
+ + (f"<p>Changed with the data (no real preference): {unstable}.</p>" if unstable else "")
295
+ + f"<p class=\"note\">{verdict}{stale}</p></div>")
296
+
297
+
244
298
  def incident_rows(links):
245
299
  names = {"suspect": "Suspect commit in the export", "files_off": "Files only, not linked (default)",
246
300
  "files_latest": "Files only, latest matching deploy", "files_window": "Files only, every match in 14 days"}
@@ -268,7 +322,8 @@ def validation_card(v):
268
322
  f'{incident_table}</div>')
269
323
 
270
324
 
271
- def render(est, label_stats, title="Consequence Ledger", primary="dr", truth=None, meta=None, validation=None):
325
+ def render(est, label_stats, title="Consequence Ledger", primary="dr", truth=None, meta=None, validation=None,
326
+ breakdown=None, recommendation=None):
272
327
  meta = meta or {}
273
328
  as_of = dt.datetime.fromtimestamp(label_stats["as_of"], dt.timezone.utc).strftime("%Y-%m-%d")
274
329
  rows = []
@@ -299,6 +354,16 @@ def render(est, label_stats, title="Consequence Ledger", primary="dr", truth=Non
299
354
  + ('<span><i class="dot" style="background:var(--truth)"></i>True rate (synthetic demo)</span>' if truth else "")
300
355
  + "</div>")
301
356
  source = html.escape(meta.get("repo", ""))
357
+ stale = ""
358
+ for part in (breakdown, recommendation):
359
+ if part and part.get("n") not in (None, est["n"]):
360
+ stale = (f" These tables were computed from {part['n']} decisions, the estimate above from {est['n']}; "
361
+ "rerun <code>ledger breakdown</code> and <code>ledger recommend</code> to refresh them.")
362
+ segments = ""
363
+ if breakdown:
364
+ segments += breakdown_card(breakdown["arms"], breakdown["baseline"], breakdown["stratum"], stale)
365
+ if recommendation:
366
+ segments += recommend_card(recommendation, recommendation["stratum"], stale)
302
367
  clusters = est.get("clusters", est["n"])
303
368
  cluster_note = f", resampling {clusters} clusters of related decisions" if clusters < est["n"] else ""
304
369
  return f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
@@ -315,6 +380,7 @@ def render(est, label_stats, title="Consequence Ledger", primary="dr", truth=Non
315
380
  <div class="card table"><table><thead><tr><th>Arm</th><th>Decisions</th><th>Bad</th><th>Naive</th><th>IPW</th><th>SNIPW</th>
316
381
  <th>{NAMES[primary]} [95% CI]</th><th>vs baseline</th><th>ESS</th>{truth_head}</tr></thead><tbody>{''.join(rows)}</tbody></table></div>
317
382
  {early_card(est, label_stats)}
383
+ {segments}
318
384
  {validation_card(validation)}
319
385
  <h2>How to read this</h2>
320
386
  <div class="card note">
@@ -0,0 +1,73 @@
1
+ """The report shows the task-type tables, and only honestly."""
2
+ import random
3
+ import unittest
4
+
5
+ from ledger.estimate import estimate
6
+ from ledger.report import breakdown_card, recommend_card, render
7
+ from ledger.segments import breakdown, recommend
8
+ from test_degenerate import STATS, has_bad_token
9
+
10
+
11
+ def make_rows(n=400, seed=3):
12
+ rng = random.Random(seed)
13
+ out = []
14
+ for i in range(n):
15
+ arm = "A" if rng.random() < 0.5 else "B"
16
+ stratum = rng.choice(["easy", "hard"])
17
+ bad = rng.random() < (0.08 if arm == "A" else 0.14)
18
+ out.append({"arm": arm, "y": int(bad), "p": {"A": 0.5, "B": 0.5}, "s": stratum, "c": f"c{i}"})
19
+ return out
20
+
21
+
22
+ class SegmentsInReportTests(unittest.TestCase):
23
+ def setUp(self):
24
+ self.rows = make_rows()
25
+ self.est = estimate(self.rows, arms=["A", "B"], baseline="A", boot=20)
26
+ self.bd = {"baseline": "A", "stratum": "difficulty", "n": len(self.rows),
27
+ "arms": breakdown(self.rows, ["A", "B"], "A", None, 5)}
28
+ self.rec = dict(recommend(self.rows, ["A", "B"], "A", None, 5), baseline="A", stratum="difficulty")
29
+
30
+ def test_tables_appear_when_given(self):
31
+ page = render(self.est, STATS, breakdown=self.bd, recommendation=self.rec)
32
+ self.assertIn("Which tool for which kind of task", page)
33
+ self.assertIn("What routing by task type is worth", page)
34
+ self.assertIn("easy", page)
35
+ self.assertIn("hard", page)
36
+ self.assertFalse(has_bad_token(page))
37
+
38
+ def test_report_without_them_is_unchanged(self):
39
+ page = render(self.est, STATS)
40
+ self.assertNotIn("Which tool for which kind of task", page)
41
+ self.assertNotIn("What routing by task type", page)
42
+
43
+ def test_stale_tables_are_flagged(self):
44
+ old = dict(self.bd, n=len(self.rows) - 50)
45
+ page = render(self.est, STATS, breakdown=old)
46
+ self.assertIn("rerun", page)
47
+ fresh = render(self.est, STATS, breakdown=self.bd)
48
+ self.assertNotIn("rerun", fresh)
49
+
50
+ def test_stratum_and_arm_names_are_escaped(self):
51
+ bd = {"baseline": "A", "stratum": "<b>x</b>", "n": 1, "arms": {"<i>B</i>": {
52
+ "overall": 0.0, "tau": 0.0, "heterogeneous": False,
53
+ "strata": [{"stratum": "<script>alert(1)</script>", "n": 5, "effect": 0.01, "se": 0.02, "raw_lo": -0.03,
54
+ "raw_hi": 0.05, "shrunk": 0.0, "shrunk_lo": -0.02, "shrunk_hi": 0.02, "p_adjusted": 1.0}]}}}
55
+ page = breakdown_card(bd["arms"], "A", bd["stratum"])
56
+ self.assertNotIn("<script>", page)
57
+ self.assertNotIn("<i>B</i>", page)
58
+
59
+ def test_empty_inputs_render_nothing(self):
60
+ self.assertEqual(breakdown_card({}, "A", "s"), "")
61
+ self.assertEqual(recommend_card(None, "s"), "")
62
+
63
+ def test_each_verdict_wording(self):
64
+ base = {"n": 10, "current_rate": 0.1, "gain": 0.0, "gain_lo": -0.01, "gain_hi": 0.01,
65
+ "best_single_gain": 0.0, "best_single_lo": -0.01, "best_single_hi": 0.01,
66
+ "stable_policy": {}, "unstable_strata": [], "worth_it": False}
67
+ self.assertIn("includes zero", recommend_card(base, "s"))
68
+ self.assertIn("one tool being better overall", recommend_card(dict(base, best_single_lo=0.01), "s"))
69
+ self.assertIn("likely to help", recommend_card(dict(base, worth_it=True), "s"))
70
+
71
+
72
+ if __name__ == "__main__":
73
+ unittest.main()