bigpowers 2.91.1 → 2.93.0

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 (36) hide show
  1. package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
  2. package/.pi/package.json +1 -1
  3. package/AGENTS.md +98 -0
  4. package/CHANGELOG.md +14 -0
  5. package/CONVENTIONS.md +47 -427
  6. package/package.json +1 -1
  7. package/scripts/ab-sandbox.sh +3 -3
  8. package/scripts/adapters/agy.sh +1 -1
  9. package/scripts/adapters/cursor.sh +1 -1
  10. package/scripts/adapters/gemini.sh +1 -1
  11. package/scripts/adapters/mimo.sh +1 -1
  12. package/scripts/adapters/pi.sh +1 -1
  13. package/scripts/adapters/zcode.sh +1 -1
  14. package/scripts/generate-context-bundle.sh +2 -1
  15. package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
  16. package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
  17. package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
  18. package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
  19. package/scripts/lib/ab-eval.py +19 -5
  20. package/scripts/lib/ab_lean.py +116 -0
  21. package/scripts/lib/ab_metrics.py +8 -0
  22. package/scripts/lib/ab_progress.py +3 -1
  23. package/scripts/lib/ab_report.py +15 -4
  24. package/scripts/lib/ab_vsm.py +122 -0
  25. package/scripts/lib/ab_waste.py +173 -0
  26. package/scripts/test-adapter-render.sh +1 -1
  27. package/scripts/test-test-lock-hook.sh +7 -7
  28. package/scripts/verify-adversarial.sh +8 -1
  29. package/tests/test_ab_eval.py +0 -44
  30. package/tests/test_ab_lean.py +81 -0
  31. package/tests/test_ab_score.py +12 -2
  32. package/tests/test_ab_tasks.py +75 -0
  33. package/tests/test_ab_vsm.py +109 -0
  34. package/tests/test_ab_waste.py +106 -0
  35. package/CLAUDE.md +0 -400
  36. package/GEMINI.md +0 -75
@@ -109,9 +109,16 @@ fi
109
109
  DEFECTS_FOUND=0
110
110
  mkdir -p specs/verifications
111
111
 
112
+ # A JS/TS source file that exists and is not a test: the files the boundary scan inspects.
113
+ is_scannable_source() {
114
+ local file="$1"
115
+ [[ "$file" =~ \.(js|ts|mjs)$ && -f "$file" ]] || return 1
116
+ [[ ! "$file" =~ (^|/)test/ && ! "$file" =~ \.test\. ]]
117
+ }
118
+
112
119
  # 2. Check for common unsafe property access patterns on JS/TS files
113
120
  for file in $CHANGED_FILES; do
114
- if [[ "$file" =~ \.(js|ts|mjs)$ ]] && [[ -f "$file" ]] && [[ ! "$file" =~ (^|/)test/ ]] && [[ ! "$file" =~ \.test\. ]]; then
121
+ if is_scannable_source "$file"; then
115
122
  # Look for unsafe chained property access like item.brand.toLowerCase() or item.prop.subprop
116
123
  if grep -nE '\.[a-zA-Z0-9_]+\.(toLowerCase|toUpperCase|trim|length|slice)\(' "$file" | grep -v '\?\.' >/dev/null 2>&1; then
117
124
  echo "WARNING [boundary-mutation]: Potential unhandled undefined access without optional chaining in $file:"
@@ -9,7 +9,6 @@ import importlib.util
9
9
  import io
10
10
  import json
11
11
  import shutil
12
- import subprocess
13
12
  import sys
14
13
  import tempfile
15
14
  import unittest
@@ -249,49 +248,6 @@ class TestCli(Sandbox):
249
248
  self.assertIsNone(report["delta"])
250
249
 
251
250
 
252
- @unittest.skipUnless(shutil.which("node"), "node is needed for the starter tasks")
253
- class TestStarterTasks(unittest.TestCase):
254
- """Each shipped task must fail as given and pass with its reference solution."""
255
-
256
- TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
257
-
258
- def grade_copy(self, task_dir: Path, with_solution: bool) -> bool:
259
- task = yaml.safe_load((task_dir / "task.yaml").read_text())
260
- work = Path(tempfile.mkdtemp(prefix="ab-starter-"))
261
- try:
262
- shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
263
- if with_solution:
264
- shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
265
- shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
266
- res = subprocess.run(["bash", "-c", task["grade"]], cwd=work, capture_output=True)
267
- return res.returncode == 0
268
- finally:
269
- shutil.rmtree(work, ignore_errors=True)
270
-
271
- def test_eight_tasks_ship(self):
272
- self.assertGreaterEqual(len(list(self.TASKS.glob("*/task.yaml"))), 8)
273
-
274
- def test_fixture_fails_and_solution_passes(self):
275
- for task_yaml in sorted(self.TASKS.glob("*/task.yaml")):
276
- with self.subTest(task=task_yaml.parent.name):
277
- self.assertFalse(self.grade_copy(task_yaml.parent, with_solution=False))
278
- self.assertTrue(self.grade_copy(task_yaml.parent, with_solution=True))
279
-
280
- def test_prompts_never_name_bigpowers(self):
281
- for task_yaml in self.TASKS.glob("*/task.yaml"):
282
- self.assertNotIn("bigpowers", yaml.safe_load(task_yaml.read_text())["prompt"].lower())
283
-
284
- def test_grade_commands_never_pass_a_bare_directory(self):
285
- """`node --test test/` exits 1 on node 24 (it resolves the directory as a module path), so
286
- such a task can never pass however good the solution is, while a hidden-only `score`
287
- command still looks healthy (BUG found in e58s07: ab-11 and ab-13 read pass=0 forever)."""
288
- for task_yaml in self.TASKS.glob("*/task.yaml"):
289
- with self.subTest(task=task_yaml.parent.name):
290
- for token in yaml.safe_load(task_yaml.read_text())["grade"].split():
291
- self.assertNotIn(token.rstrip("/"), ("test", "test-hidden"),
292
- f"{task_yaml.parent.name} grades a directory, not a glob")
293
-
294
-
295
251
  class TestRunBenchmarkHonesty(unittest.TestCase):
296
252
  def test_without_skill_arm_is_unmeasured_not_zero(self):
297
253
  sc = {"id": "s1", "grader": {"type": "code", "command": "true"}}
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s10
3
+ """tests/test_ab_lean.py — lean views: per-arm VSM and waste summary, deltas, bars, redundant tests.
4
+
5
+ Run: python3 tests/test_ab_lean.py
6
+ """
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+ from test_ab_eval import load_module, rep # noqa: E402
13
+ from test_ab_waste import Stream, waste # noqa: E402
14
+
15
+ lean = load_module("ab_lean", "ab_lean.py")
16
+
17
+
18
+ def run(arm, lead, build, test, passed=True, cost=0.01, test_runs=3, rework=0):
19
+ stages = {"plan": {"s": lead * 0.1}, "design": {"s": lead * 0.2}, "build": {"s": build}, "test": {"s": test}}
20
+ vsm = {"lead_time_s": lead, "time_to_first_build_s": lead * 0.3, "switches": 4, "stages": stages}
21
+ w = {"test_runs": test_runs, "redundant_test_runs": 1, "rework_loops": rework, "reedits": 0,
22
+ "rereads": 0, "repeated_commands": 0, "review_load": 50, "diff_ratio": 1.2, "ftt": rework == 0,
23
+ "pce": round((build + test) / lead, 3), "leftovers": [], "extra_files": []}
24
+ return {"task": "t", "arm": arm, "pass": passed, "score": None, "tampered": False, "agent_error": False,
25
+ "cost_usd": cost, "input_tokens": None, "output_tokens": None, "turns": 5, "duration_s": lead,
26
+ "diff_lines": 5, "vsm": vsm, "waste": w}
27
+
28
+
29
+ def records():
30
+ return ([run("vanilla", 70, 28, 21) for _ in range(5)]
31
+ + [run("bigpowers", 140, 20, 70, cost=0.02, test_runs=8, rework=1) for _ in range(5)])
32
+
33
+
34
+ class TestLeanSummary(unittest.TestCase):
35
+ def test_per_arm_summary_and_deltas(self):
36
+ summary = lean.lean_summary(records(), ["vanilla", "bigpowers"])
37
+ va, bp = summary["arms"]["vanilla"], summary["arms"]["bigpowers"]
38
+ self.assertEqual(va["lead_time_s"], 70)
39
+ self.assertEqual(round(va["stage_share"]["build"], 2), 0.4)
40
+ self.assertEqual(bp["waste"]["test_runs"], 8)
41
+ self.assertEqual(bp["ftt_rate"], 0.0)
42
+ self.assertEqual(va["cost_per_pass"], 0.01)
43
+ d = summary["deltas"]["bigpowers"]
44
+ self.assertEqual(d["lead_time_s"]["delta"], 70)
45
+ self.assertEqual(d["test_runs"]["delta"], 5)
46
+ self.assertEqual(len(d["build_share"]["ci95"]), 2)
47
+
48
+ def test_old_records_give_none(self):
49
+ old = [{k: v for k, v in r.items() if k not in ("vsm", "waste")} for r in records()]
50
+ self.assertIsNone(lean.lean_summary(old, ["vanilla", "bigpowers"]))
51
+ self.assertEqual(lean.lean_lines(None), [])
52
+
53
+ def test_report_carries_lean(self):
54
+ result = rep.aggregate(records(), ["vanilla", "bigpowers"], ["t"])
55
+ self.assertIn("bigpowers", result["lean"]["deltas"])
56
+
57
+
58
+ class TestViews(unittest.TestCase):
59
+ def test_bars_scale_with_lead_time(self):
60
+ lines = lean.lean_lines(lean.lean_summary(records(), ["vanilla", "bigpowers"]))
61
+ bars = {line.split()[0]: line for line in lines if line.split() and line.split()[0] in ("vanilla", "bigpowers")}
62
+ va_len = bars["vanilla"].count("|") and len(bars["vanilla"].split("|")[1].rstrip())
63
+ bp_len = len(bars["bigpowers"].split("|")[1].rstrip())
64
+ self.assertAlmostEqual(bp_len / va_len, 2, delta=0.2)
65
+ self.assertIn("T", bars["bigpowers"])
66
+ self.assertTrue(any("rework" in line for line in lines))
67
+
68
+
69
+ class TestRedundantTestRuns(unittest.TestCase):
70
+ def test_repeated_green_run_without_edit_is_redundant(self):
71
+ stream = (Stream()
72
+ .call("edit", {"path": "/work/src/a.js"})
73
+ .call("bash", {"command": "npm test"}, "# pass 3\n# fail 0\n")
74
+ .call("bash", {"command": "npm test 2>&1 | tail -3"}, "# pass 3\n# fail 0\n")
75
+ .call("edit", {"path": "/work/src/a.js"})
76
+ .call("bash", {"command": "npm test"}, "# pass 3\n# fail 0\n"))
77
+ self.assertEqual(waste.transcript_waste(stream.text())["redundant_test_runs"], 1)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main(verbosity=1)
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- # story: e58s03
2
+ # story: e58s03 e58s11
3
3
  """tests/test_ab_score.py — partial-credit scoring and the hard tasks (ab-11, ab-13).
4
4
 
5
5
  Run: python3 tests/test_ab_score.py
@@ -18,7 +18,17 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
18
  from test_ab_eval import REPO_ROOT, Sandbox, met, rep # noqa: E402
19
19
 
20
20
  TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
21
- HARD_TASKS = ["ab-11-ledger-rounding", "ab-13-orders-split"]
21
+ HARD_TASKS = [
22
+ "ab-09-inventory-reservations",
23
+ "ab-11-ledger-rounding",
24
+ "ab-12-todo-due-dates",
25
+ "ab-13-orders-split",
26
+ "ab-15-config-precedence",
27
+ "ab-16-site-tags-rss",
28
+ "ab-17-payment-alias",
29
+ "ab-19-catalog-segregation",
30
+ "ab-20-brand-discount",
31
+ ]
22
32
 
23
33
 
24
34
  class TestTapScore(unittest.TestCase):
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s01 e58s02 e58s07 e58s09
3
+ """tests/test_ab_tasks.py — every shipped A/B task is calibrated: its fixture fails, its reference
4
+ solution passes, its prompt never names bigpowers, and its grade command uses file globs.
5
+
6
+ Run: python3 tests/test_ab_tasks.py
7
+ Split from test_ab_eval.py (300-line cap).
8
+ """
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ import yaml
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+ from test_ab_eval import REPO_ROOT # noqa: E402
21
+
22
+
23
+ @unittest.skipUnless(shutil.which("node"), "node is needed for the starter tasks")
24
+ class TestStarterTasks(unittest.TestCase):
25
+ """Each shipped task must fail as given and pass with its reference solution."""
26
+
27
+ TASKS = REPO_ROOT / "specs" / "benchmarks" / "ab" / "tasks"
28
+
29
+ def grade_copy(self, task_dir: Path, with_solution: bool) -> bool:
30
+ task = yaml.safe_load((task_dir / "task.yaml").read_text())
31
+ work = Path(tempfile.mkdtemp(prefix="ab-starter-"))
32
+ try:
33
+ shutil.copytree(task_dir / "fixture", work, dirs_exist_ok=True)
34
+ if with_solution:
35
+ shutil.copytree(task_dir / "solution", work, dirs_exist_ok=True)
36
+ shutil.copytree(task_dir / "hidden", work, dirs_exist_ok=True)
37
+ res = subprocess.run(["bash", "-c", task["grade"]], cwd=work, capture_output=True)
38
+ return res.returncode == 0
39
+ finally:
40
+ shutil.rmtree(work, ignore_errors=True)
41
+
42
+ def test_eight_tasks_ship(self):
43
+ self.assertGreaterEqual(len(list(self.TASKS.glob("*/task.yaml"))), 8)
44
+
45
+ def test_fixture_fails_and_solution_passes(self):
46
+ for task_yaml in sorted(self.TASKS.glob("*/task.yaml")):
47
+ with self.subTest(task=task_yaml.parent.name):
48
+ self.assertFalse(self.grade_copy(task_yaml.parent, with_solution=False))
49
+ self.assertTrue(self.grade_copy(task_yaml.parent, with_solution=True))
50
+
51
+ def test_prompts_never_name_bigpowers(self):
52
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
53
+ self.assertNotIn("bigpowers", yaml.safe_load(task_yaml.read_text())["prompt"].lower())
54
+
55
+ def test_fixture_test_scripts_never_pass_a_bare_directory(self):
56
+ """`npm test` must work in every fixture: a `node --test test/` script fails on node 24, and
57
+ agents then spend the run repairing the harness (found by e58s09 waste metrics)."""
58
+ for package in self.TASKS.glob("*/fixture/package.json"):
59
+ with self.subTest(task=package.parent.parent.name):
60
+ script = json.loads(package.read_text()).get("scripts", {}).get("test", "")
61
+ self.assertNotRegex(script, r"--test\s+\S*/(\s|$)", script)
62
+
63
+ def test_grade_commands_never_pass_a_bare_directory(self):
64
+ """`node --test test/` exits 1 on node 24 (it resolves the directory as a module path), so
65
+ such a task can never pass however good the solution is, while a hidden-only `score`
66
+ command still looks healthy (BUG found in e58s07: ab-11 and ab-13 read pass=0 forever)."""
67
+ for task_yaml in self.TASKS.glob("*/task.yaml"):
68
+ with self.subTest(task=task_yaml.parent.name):
69
+ for token in yaml.safe_load(task_yaml.read_text())["grade"].split():
70
+ self.assertNotIn(token.rstrip("/"), ("test", "test-hidden"),
71
+ f"{task_yaml.parent.name} grades a directory, not a glob")
72
+
73
+
74
+ if __name__ == "__main__":
75
+ unittest.main(verbosity=1)
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s08
3
+ """tests/test_ab_vsm.py — value stream map per run: stage rules, per-turn time, run records.
4
+
5
+ Run: python3 tests/test_ab_vsm.py
6
+ """
7
+ import json
8
+ import sys
9
+ import unittest
10
+ from pathlib import Path
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
13
+ from test_ab_eval import Sandbox, load_module # noqa: E402
14
+
15
+ vsm = load_module("ab_vsm", "ab_vsm.py")
16
+ STAGES = {"plan", "design", "build", "test", "deploy", "maintain", "wrap-up"}
17
+
18
+
19
+ def assistant(ts_s, cost=0.001, out=10, calls=()):
20
+ content = [{"type": "toolCall", "name": tool, "arguments": args} for tool, args in calls]
21
+ usage = {"input": 100, "output": out, "cacheRead": 0, "cacheWrite": 0, "cost": {"total": cost}}
22
+ return {"type": "message_end", "message": {"role": "assistant", "timestamp": int(ts_s * 1000),
23
+ "content": content, "usage": usage}}
24
+
25
+
26
+ def tool_result(ts_s):
27
+ return {"type": "message_end", "message": {"role": "toolResult", "timestamp": int(ts_s * 1000)}}
28
+
29
+
30
+ def transcript():
31
+ events = [
32
+ {"type": "session", "id": "s"},
33
+ {"type": "message_end", "message": {"role": "user", "timestamp": 0}},
34
+ assistant(0, calls=[("read", {"path": "/work/ISSUE.md"})]), tool_result(9),
35
+ assistant(10, calls=[("bash", {"command": "cat src/reports.js"})]), tool_result(24),
36
+ assistant(25, calls=[("edit", {"path": "/work/src/reports.js"})]), tool_result(39),
37
+ assistant(40, calls=[("bash", {"command": "npm test 2>&1 | tail"})]), tool_result(49),
38
+ assistant(50, calls=[("bash", {"command": "git commit -am fix"})]), tool_result(54),
39
+ assistant(55, out=40),
40
+ {"type": "agent_settled", "aborted": False},
41
+ ]
42
+ return "\n".join(json.dumps(e) for e in events)
43
+
44
+
45
+ class TestRules(unittest.TestCase):
46
+ def setUp(self):
47
+ self.rules = vsm.load_rules()
48
+
49
+ def test_rules_file_uses_known_stages(self):
50
+ self.assertTrue({r["stage"] for r in self.rules["rules"]} <= STAGES)
51
+ self.assertTrue(set(self.rules["turn_priority"]) <= STAGES)
52
+
53
+ def test_calls_are_classified(self):
54
+ cases = [
55
+ ("read", {"path": "/work/ISSUE.md"}, "plan"),
56
+ ("bash", {"command": "sed -n 1,80p .agents/skills/fix-bug/SKILL.md"}, "plan"),
57
+ ("read", {"path": "/work/src/reports.js"}, "design"),
58
+ ("edit", {"path": "/work/src/reports.js"}, "build"),
59
+ ("edit", {"path": "/work/test/ledger.test.js"}, "test"),
60
+ ("bash", {"command": "npm test 2>&1 | tail -20"}, "test"),
61
+ ("bash", {"command": "node --test test/"}, "test"),
62
+ ("bash", {"command": "git checkout -b fix/rounding"}, "deploy"),
63
+ ("bash", {"command": "git commit -am 'fix'"}, "deploy"),
64
+ ("write", {"path": "/work/specs/bugs/BUG-001.md"}, "maintain"),
65
+ ("edit", {"path": "/work/docs/LEDGER.md"}, "maintain"),
66
+ ("write", {"path": "/work/specs/plan.md"}, "plan"),
67
+ ]
68
+ for tool, args, expected in cases:
69
+ with self.subTest(tool=tool, args=args):
70
+ self.assertEqual(vsm.classify_call(tool, args, self.rules), expected)
71
+
72
+
73
+ class TestStageTime(unittest.TestCase):
74
+ def test_turn_time_goes_to_the_stage_of_its_action(self):
75
+ block = vsm.vsm_from("pi-json", transcript())
76
+ seconds = {stage: round(v["s"], 1) for stage, v in block["stages"].items()}
77
+ self.assertEqual(seconds, {"plan": 10.0, "design": 15.0, "build": 15.0, "test": 10.0,
78
+ "deploy": 5.0, "wrap-up": 0.0})
79
+ self.assertEqual(block["lead_time_s"], 55.0)
80
+ self.assertEqual(block["time_to_first_build_s"], 25.0)
81
+ self.assertEqual(block["switches"], 5)
82
+ self.assertEqual(block["sequence"], ["plan", "design", "build", "test", "deploy", "wrap-up"])
83
+ self.assertEqual(block["stages"]["wrap-up"]["output_tokens"], 40)
84
+ self.assertAlmostEqual(sum(v["cost_usd"] for v in block["stages"].values()), 0.006)
85
+
86
+ def test_build_wins_a_turn_with_several_calls(self):
87
+ events = [assistant(0, calls=[("read", {"path": "/work/ISSUE.md"}), ("edit", {"path": "/work/src/a.js"})]),
88
+ tool_result(5)]
89
+ block = vsm.vsm_from("pi-json", "\n".join(json.dumps(e) for e in events))
90
+ self.assertEqual(block["sequence"], ["build"])
91
+
92
+ def test_unsupported_or_empty_is_none(self):
93
+ self.assertIsNone(vsm.vsm_from("none", transcript()))
94
+ self.assertIsNone(vsm.vsm_from("pi-json", "not json"))
95
+
96
+
97
+ class TestRunRecord(Sandbox):
98
+ def test_record_carries_vsm(self):
99
+ script = self.tmp / "transcript.jsonl"
100
+ script.write_text(transcript() + "\n")
101
+ self.run_main("--adapter-cmd", f"cat {script}; echo 42 > answer.txt", "--parser", "pi-json",
102
+ "--arms", "vanilla")
103
+ rec = self.records()[0]
104
+ self.assertEqual(rec["vsm"]["stages"]["build"]["turns"], 1)
105
+ self.assertEqual(rec["vsm"]["lead_time_s"], 55.0)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ unittest.main(verbosity=1)
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ # story: e58s09
3
+ """tests/test_ab_waste.py — lean waste metrics: relearning, rework, first-time-through, patch metrics.
4
+
5
+ Run: python3 tests/test_ab_waste.py
6
+ """
7
+ import gzip
8
+ import json
9
+ import sys
10
+ import unittest
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+ from test_ab_eval import Sandbox, load_module # noqa: E402
15
+
16
+ waste = load_module("ab_waste", "ab_waste.py")
17
+
18
+
19
+ class Stream:
20
+ """Builds a pi JSON transcript: assistant turns with one tool call each, plus tool results."""
21
+
22
+ def __init__(self):
23
+ self.events, self.t, self.n = [], 0, 0
24
+
25
+ def call(self, tool, args, result_text="ok", exit_code=0):
26
+ self.n += 1
27
+ cid = f"c{self.n}"
28
+ self.events.append({"type": "message_end", "message": {
29
+ "role": "assistant", "timestamp": self.t * 1000, "usage": {"output": 5, "cost": {"total": 0.001}},
30
+ "content": [{"type": "toolCall", "name": tool, "arguments": args}]}})
31
+ self.events.append({"type": "tool_execution_start", "toolCallId": cid, "toolName": tool, "args": args})
32
+ result = {"content": [{"type": "text", "text": result_text}], "details": {"exit_code": exit_code}}
33
+ self.events.append({"type": "tool_execution_end", "toolCallId": cid, "toolName": tool,
34
+ "result": result, "isError": False})
35
+ self.events.append({"type": "message_end", "message": {"role": "toolResult",
36
+ "timestamp": (self.t + 1) * 1000}})
37
+ self.t += 10
38
+ return self
39
+
40
+ def text(self):
41
+ return "\n".join(json.dumps(e) for e in self.events)
42
+
43
+
44
+ class TestTranscriptWaste(unittest.TestCase):
45
+ def test_relearning_and_rework_are_counted(self):
46
+ stream = (Stream()
47
+ .call("read", {"path": "/work/src/a.js"})
48
+ .call("read", {"path": "/work/src/a.js"})
49
+ .call("bash", {"command": "ls"})
50
+ .call("bash", {"command": "ls"})
51
+ .call("edit", {"path": "/work/src/a.js"})
52
+ .call("bash", {"command": "npm test 2>&1 | tail"}, "ℹ pass 3\nℹ fail 1\n")
53
+ .call("edit", {"path": "/work/src/a.js"})
54
+ .call("bash", {"command": "npm test 2>&1 | tail"}, "ℹ pass 4\nℹ fail 0\n"))
55
+ got = waste.transcript_waste(stream.text())
56
+ self.assertEqual((got["rereads"], got["repeated_commands"], got["test_runs"], got["rework_loops"],
57
+ got["reedits"], got["ftt"]), (1, 1, 2, 1, 1, False))
58
+
59
+ def test_non_zero_exit_is_a_failing_run(self):
60
+ stream = Stream().call("edit", {"path": "/work/src/a.js"}).call(
61
+ "bash", {"command": "node --test test/"}, "boom", exit_code=1)
62
+ self.assertEqual(waste.transcript_waste(stream.text())["rework_loops"], 1)
63
+
64
+ def test_clean_run_is_first_time_through(self):
65
+ stream = Stream().call("edit", {"path": "/work/src/a.js"}).call(
66
+ "bash", {"command": "npm test"}, "# pass 5\n# fail 0\n")
67
+ got = waste.transcript_waste(stream.text())
68
+ self.assertEqual((got["rework_loops"], got["ftt"]), (0, True))
69
+ self.assertGreater(got["pce"], 0)
70
+
71
+ def test_no_transcript_gives_none(self):
72
+ self.assertIsNone(waste.transcript_waste("not json"))
73
+
74
+
75
+ class TestPatchWaste(Sandbox):
76
+ AGENT = ("echo 42 > answer.txt; printf 'a\\nb\\n' > extra.js; "
77
+ "printf 'one two three\\n' > notes.md; echo x > debug.tmp.js")
78
+
79
+ def setUp(self):
80
+ super().setUp()
81
+ solution = self.tasks_dir / "t-01" / "solution"
82
+ solution.mkdir()
83
+ (solution / "answer.txt").write_text("42\n")
84
+
85
+ def test_patch_metrics_and_saved_patch(self):
86
+ self.run_main("--adapter-cmd", self.AGENT, "--arms", "vanilla")
87
+ got = self.records()[0]["waste"]
88
+ self.assertEqual(got["solution_lines"], 1)
89
+ self.assertIn("extra.js", got["extra_files"])
90
+ self.assertNotIn("answer.txt", got["extra_files"])
91
+ self.assertEqual(got["leftovers"], ["debug.tmp.js"])
92
+ self.assertEqual(got["doc_lines"], 1)
93
+ self.assertGreater(got["diff_ratio"], 1)
94
+ self.assertGreaterEqual(got["review_load"], got["product_lines"] + 3)
95
+ patch = self.out / "traces" / "t-01-vanilla-1.patch.gz"
96
+ self.assertIn("extra.js", gzip.decompress(patch.read_bytes()).decode())
97
+
98
+ def test_non_pi_adapter_has_patch_metrics_only(self):
99
+ self.run_main("--adapter-cmd", self.AGENT, "--arms", "vanilla")
100
+ got = self.records()[0]["waste"]
101
+ self.assertIsNone(got["rework_loops"])
102
+ self.assertIsNotNone(got["review_load"])
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main(verbosity=1)