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.
- package/.gemini/extensions/bigpowers/gemini-extension.json +1 -1
- package/.pi/package.json +1 -1
- package/AGENTS.md +98 -0
- package/CHANGELOG.md +14 -0
- package/CONVENTIONS.md +47 -427
- package/package.json +1 -1
- package/scripts/ab-sandbox.sh +3 -3
- package/scripts/adapters/agy.sh +1 -1
- package/scripts/adapters/cursor.sh +1 -1
- package/scripts/adapters/gemini.sh +1 -1
- package/scripts/adapters/mimo.sh +1 -1
- package/scripts/adapters/pi.sh +1 -1
- package/scripts/adapters/zcode.sh +1 -1
- package/scripts/generate-context-bundle.sh +2 -1
- package/scripts/lib/__pycache__/atomic_write.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/link_utils.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-310.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_okf.cpython-310.pyc +0 -0
- package/scripts/lib/ab-eval.py +19 -5
- package/scripts/lib/ab_lean.py +116 -0
- package/scripts/lib/ab_metrics.py +8 -0
- package/scripts/lib/ab_progress.py +3 -1
- package/scripts/lib/ab_report.py +15 -4
- package/scripts/lib/ab_vsm.py +122 -0
- package/scripts/lib/ab_waste.py +173 -0
- package/scripts/test-adapter-render.sh +1 -1
- package/scripts/test-test-lock-hook.sh +7 -7
- package/scripts/verify-adversarial.sh +8 -1
- package/tests/test_ab_eval.py +0 -44
- package/tests/test_ab_lean.py +81 -0
- package/tests/test_ab_score.py +12 -2
- package/tests/test_ab_tasks.py +75 -0
- package/tests/test_ab_vsm.py +109 -0
- package/tests/test_ab_waste.py +106 -0
- package/CLAUDE.md +0 -400
- 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
|
|
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:"
|
package/tests/test_ab_eval.py
CHANGED
|
@@ -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)
|
package/tests/test_ab_score.py
CHANGED
|
@@ -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 = [
|
|
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)
|