@heretek-ai/epistemic-swarm 0.4.2 → 0.4.4

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 (44) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/hooks/hooks.json +22 -1
  3. package/install.sh +25 -0
  4. package/package.json +1 -1
  5. package/plugins/antigravity/plugin.json +1 -1
  6. package/plugins/gemini/gemini-extension.json +1 -1
  7. package/plugins/opencode/index.js +44 -15
  8. package/plugins/opencode/tui.js +40 -18
  9. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  22. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  23. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  24. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  25. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  26. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  27. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  28. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  35. package/runner/tests/test_bet1_spike.py +95 -0
  36. package/runner/tests/test_opencode_ux.py +76 -0
  37. package/runner/tests/test_webcache.py +222 -0
  38. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  39. package/scripts/bet1_advisory_spike.py +426 -0
  40. package/skills/epistemic_search/scripts/webcache.py +292 -0
  41. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  42. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  43. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  44. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for the WebFetch cache-through gate (webcache.py + hooks.json)."""
3
+
4
+ import json
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import unittest
9
+ from datetime import datetime, timedelta, timezone
10
+ from pathlib import Path
11
+
12
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
13
+ if str(PROJECT_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(PROJECT_ROOT))
15
+
16
+ WEBCACHE = PROJECT_ROOT / "skills" / "epistemic_search" / "scripts" / "webcache.py"
17
+
18
+
19
+ def run_webcache(mode, payload=None, raw=None, cwd=None, env=None):
20
+ stdin = raw if raw is not None else json.dumps(payload or {})
21
+ merged = dict(__import__("os").environ)
22
+ if env:
23
+ merged.update(env)
24
+ return subprocess.run(
25
+ [sys.executable, str(WEBCACHE), mode],
26
+ input=stdin,
27
+ capture_output=True,
28
+ text=True,
29
+ cwd=str(cwd or PROJECT_ROOT),
30
+ env=merged,
31
+ )
32
+
33
+
34
+ def seed_source(sources_dir, url, content, cached_at=None):
35
+ import hashlib
36
+
37
+ digest = hashlib.sha256(content.encode("utf-8")).hexdigest()
38
+ sources_dir.mkdir(parents=True, exist_ok=True)
39
+ sources_dir.joinpath(f"{digest}.md").write_text(content, encoding="utf-8")
40
+ sources_dir.joinpath(f"{digest}.json").write_text(
41
+ json.dumps(
42
+ {
43
+ "hash": digest,
44
+ "url": url,
45
+ "title": url,
46
+ "tier": "WEB_DOCUMENT",
47
+ "cached_at": cached_at
48
+ or datetime.now(timezone.utc).isoformat(),
49
+ "byte_size": len(content.encode("utf-8")),
50
+ "char_count": len(content),
51
+ "custom_metadata": {},
52
+ }
53
+ ),
54
+ encoding="utf-8",
55
+ )
56
+ return digest
57
+
58
+
59
+ def hook_input(url, cwd):
60
+ return {"tool_name": "WebFetch", "tool_input": {"url": url}, "cwd": str(cwd)}
61
+
62
+
63
+ class TestCanonicalization(unittest.TestCase):
64
+ def test_variants_resolve_to_one_entry(self):
65
+ with tempfile.TemporaryDirectory() as tmp:
66
+ root = Path(tmp)
67
+ seed_source(
68
+ root / ".research" / "sources",
69
+ "https://example.com/p?a=1&b=2",
70
+ "hello cached",
71
+ )
72
+ for variant in [
73
+ "https://EXAMPLE.com/p?b=2&a=1#frag",
74
+ "https://example.com:443/p?a=1&b=2",
75
+ ]:
76
+ res = run_webcache("gate", hook_input(variant, root))
77
+ self.assertEqual(res.returncode, 0)
78
+ self.assertTrue(res.stdout.strip(), f"expected HIT for {variant}")
79
+ out = json.loads(res.stdout)
80
+ self.assertEqual(
81
+ out["hookSpecificOutput"]["permissionDecision"], "deny"
82
+ )
83
+ self.assertIn("hello cached", out["hookSpecificOutput"]["additionalContext"])
84
+
85
+
86
+ class TestGate(unittest.TestCase):
87
+ def test_miss_allows_silently(self):
88
+ with tempfile.TemporaryDirectory() as tmp:
89
+ res = run_webcache(
90
+ "gate", hook_input("https://example.com/new-page", Path(tmp))
91
+ )
92
+ self.assertEqual(res.returncode, 0)
93
+ self.assertEqual(res.stdout.strip(), "")
94
+
95
+ def test_hit_serves_cache(self):
96
+ with tempfile.TemporaryDirectory() as tmp:
97
+ root = Path(tmp)
98
+ digest = seed_source(
99
+ root / ".research" / "sources",
100
+ "https://code.claude.com/docs/en/hooks",
101
+ "# Hooks\n\nHook content here.",
102
+ )
103
+ res = run_webcache(
104
+ "gate", hook_input("https://code.claude.com/docs/en/hooks", root)
105
+ )
106
+ self.assertEqual(res.returncode, 0)
107
+ out = json.loads(res.stdout)
108
+ hook = out["hookSpecificOutput"]
109
+ self.assertEqual(hook["hookEventName"], "PreToolUse")
110
+ self.assertEqual(hook["permissionDecision"], "deny")
111
+ self.assertIn("Hook content here.", hook["additionalContext"])
112
+ self.assertIn(f"[VERIFIED: {digest[:16]}]", hook["additionalContext"])
113
+
114
+ def test_stale_allows_for_refresh(self):
115
+ with tempfile.TemporaryDirectory() as tmp:
116
+ root = Path(tmp)
117
+ old = (datetime.now(timezone.utc) - timedelta(days=30)).isoformat()
118
+ seed_source(
119
+ root / ".research" / "sources",
120
+ "https://example.com/aging",
121
+ "old content",
122
+ cached_at=old,
123
+ )
124
+ res = run_webcache("gate", hook_input("https://example.com/aging", root))
125
+ self.assertEqual(res.returncode, 0)
126
+ self.assertEqual(res.stdout.strip(), "")
127
+
128
+ def test_malformed_input_fails_open(self):
129
+ for raw in ["", "not json{{{", '{"tool_input": {}}']:
130
+ res = run_webcache("gate", raw=raw)
131
+ self.assertEqual(res.returncode, 0, f"raw={raw!r} stderr={res.stderr}")
132
+ self.assertEqual(res.stdout.strip(), "")
133
+
134
+
135
+ class TestArchive(unittest.TestCase):
136
+ def test_archive_writes_matching_pair(self):
137
+ with tempfile.TemporaryDirectory() as tmp:
138
+ root = Path(tmp)
139
+ payload = {
140
+ "tool_name": "WebFetch",
141
+ "tool_input": {"url": "https://example.com/live"},
142
+ "tool_response": "# Live\n\nFresh body text.",
143
+ "cwd": str(root),
144
+ }
145
+ res = run_webcache("archive", payload)
146
+ self.assertEqual(res.returncode, 0, res.stderr)
147
+ self.assertEqual(res.stdout.strip(), "")
148
+ sources = root / ".research" / "sources"
149
+ md_files = list(sources.glob("*.md"))
150
+ self.assertEqual(len(md_files), 1)
151
+ import hashlib
152
+
153
+ digest = hashlib.sha256("# Live\n\nFresh body text.".encode("utf-8")).hexdigest()
154
+ self.assertEqual(md_files[0].name, f"{digest}.md")
155
+ meta = json.loads(sources.joinpath(f"{digest}.json").read_text(encoding="utf-8"))
156
+ self.assertEqual(meta["url"], "https://example.com/live")
157
+ self.assertEqual(meta["hash"], digest)
158
+ # Idempotent: archiving again writes the same pair.
159
+ res2 = run_webcache("archive", payload)
160
+ self.assertEqual(res2.returncode, 0)
161
+ self.assertEqual(len(list(sources.glob("*.md"))), 1)
162
+
163
+ def test_archive_round_trip_gate_hit(self):
164
+ with tempfile.TemporaryDirectory() as tmp:
165
+ root = Path(tmp)
166
+ url = "https://example.com/roundtrip"
167
+ run_webcache(
168
+ "archive",
169
+ {
170
+ "tool_input": {"url": url},
171
+ "response": "Roundtrip body.",
172
+ "cwd": str(root),
173
+ },
174
+ )
175
+ res = run_webcache("gate", hook_input(url, root))
176
+ out = json.loads(res.stdout)
177
+ self.assertEqual(out["hookSpecificOutput"]["permissionDecision"], "deny")
178
+ self.assertIn("Roundtrip body.", out["hookSpecificOutput"]["additionalContext"])
179
+
180
+
181
+ class TestStats(unittest.TestCase):
182
+ def test_empty_is_silent(self):
183
+ with tempfile.TemporaryDirectory() as tmp:
184
+ res = run_webcache("stats", {"cwd": str(Path(tmp))})
185
+ self.assertEqual(res.returncode, 0)
186
+ self.assertEqual(res.stdout.strip(), "")
187
+
188
+ def test_populated_reports_count(self):
189
+ with tempfile.TemporaryDirectory() as tmp:
190
+ root = Path(tmp)
191
+ seed_source(root / ".research" / "sources", "https://a.example/1", "one")
192
+ seed_source(root / ".research" / "sources", "https://b.example/2", "two")
193
+ res = run_webcache("stats", {"cwd": str(root)})
194
+ out = json.loads(res.stdout)
195
+ self.assertIn("2 page(s) cached", out["hookSpecificOutput"]["additionalContext"])
196
+
197
+
198
+ class TestHooksManifest(unittest.TestCase):
199
+ def test_hooks_json_wires_cache_through(self):
200
+ hooks = json.loads(
201
+ (PROJECT_ROOT / "hooks" / "hooks.json").read_text(encoding="utf-8")
202
+ )
203
+ pre = hooks["hooks"]["PreToolUse"]
204
+ webfetch_pre = [m for m in pre if m.get("matcher") == "WebFetch"]
205
+ self.assertEqual(len(webfetch_pre), 1)
206
+ pre_cmd = webfetch_pre[0]["hooks"][0]["command"]
207
+ self.assertIn("webcache.py", pre_cmd)
208
+ self.assertTrue(pre_cmd.rstrip().endswith("gate"))
209
+ post = hooks["hooks"]["PostToolUse"]
210
+ webfetch_post = [m for m in post if m.get("matcher") == "WebFetch"]
211
+ self.assertEqual(len(webfetch_post), 1)
212
+ post_cmd = webfetch_post[0]["hooks"][0]["command"]
213
+ self.assertIn("webcache.py", post_cmd)
214
+ self.assertTrue(post_cmd.rstrip().endswith("archive"))
215
+ session_start = hooks["hooks"].get("SessionStart", [])
216
+ self.assertTrue(
217
+ any("webcache.py" in h.get("command", "") and h.get("command", "").rstrip().endswith("stats") for m in session_start for h in m.get("hooks", []))
218
+ )
219
+
220
+
221
+ if __name__ == "__main__":
222
+ unittest.main()
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env python3
2
+ """Bet-1 advisory spike: proof-carrying review on a synthetic PR fixture set.
3
+
4
+ Advisory mode ONLY: findings are formatted as PR comments, never block, and
5
+ the process always exits 0. Measures: seeded-defect hit rate (finding cites
6
+ seeded file + line within +/-3) and clean-fixture false-positive rate.
7
+
8
+ python3 scripts/bet1_advisory_spike.py --generate --dir /tmp/bet1
9
+ python3 scripts/bet1_advisory_spike.py --run --mock --dir /tmp/bet1
10
+ python3 scripts/bet1_advisory_spike.py --run --live --only bet1-01,bet1-02 --dir /tmp/bet1
11
+ python3 scripts/bet1_advisory_spike.py --score --dir /tmp/bet1
12
+
13
+ Live runs invoke real audits (LLM spend); mock runs exercise dispatch only.
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
24
+ LINE_TOLERANCE = 3
25
+
26
+ # name -> (filename, lines, seeded_line_1based, description)
27
+ FIXTURES = {
28
+ "hardcoded-secret-1": (
29
+ "auth.py",
30
+ [
31
+ 'API_KEY = "sk-live-51H7x9Q2mZ4v8K3n" # production key',
32
+ "",
33
+ "def headers():",
34
+ ' return {"Authorization": f"Bearer {API_KEY}"}',
35
+ ],
36
+ 1,
37
+ "hardcoded production secret",
38
+ ),
39
+ "hardcoded-secret-2": (
40
+ "config.py",
41
+ [
42
+ "import os",
43
+ "",
44
+ 'DB_PASSWORD = "P@ssw0rd-2024-prod"',
45
+ "",
46
+ "def dsn():",
47
+ ' return f"postgres://admin:{DB_PASSWORD}@db:5432/app"',
48
+ ],
49
+ 3,
50
+ "hardcoded database password",
51
+ ),
52
+ "sqli-concat-1": (
53
+ "users.py",
54
+ [
55
+ "import sqlite3",
56
+ "",
57
+ "def get_user(db, username):",
58
+ " conn = sqlite3.connect(db)",
59
+ " cur = conn.cursor()",
60
+ ' query = "SELECT * FROM users WHERE name = \'" + username + "\'"',
61
+ " cur.execute(query)",
62
+ " return cur.fetchall()",
63
+ ],
64
+ 6,
65
+ "SQL string concatenation with user input",
66
+ ),
67
+ "sqli-concat-2": (
68
+ "orders.py",
69
+ [
70
+ "import sqlite3",
71
+ "",
72
+ "def get_orders(db, status, limit):",
73
+ " conn = sqlite3.connect(db)",
74
+ " sql = f\"SELECT * FROM orders WHERE status='{status}' LIMIT {limit}\"",
75
+ " return conn.execute(sql).fetchall()",
76
+ ],
77
+ 5,
78
+ "f-string SQL with user input",
79
+ ),
80
+ "race-counter-1": (
81
+ "counter.py",
82
+ [
83
+ "import threading",
84
+ "",
85
+ "total = 0",
86
+ "",
87
+ "def add(n):",
88
+ " global total",
89
+ " total += n",
90
+ "",
91
+ "threads = [threading.Thread(target=add, args=(i,)) for i in range(100)]",
92
+ "[t.start() for t in threads]",
93
+ ],
94
+ 7,
95
+ "unsynchronized shared counter across threads",
96
+ ),
97
+ "race-counter-2": (
98
+ "cache.py",
99
+ [
100
+ "import threading",
101
+ "",
102
+ "_cache = {}",
103
+ "",
104
+ "def get_or_load(key, loader):",
105
+ " if key not in _cache:",
106
+ " _cache[key] = loader()",
107
+ " return _cache[key]",
108
+ ],
109
+ 7,
110
+ "check-then-act race on shared dict",
111
+ ),
112
+ "auth-bypass-1": (
113
+ "login.py",
114
+ [
115
+ "def is_admin(user):",
116
+ " return True",
117
+ "",
118
+ "def delete_account(user, target):",
119
+ " if is_admin(user):",
120
+ " return db.delete(target)",
121
+ " raise PermissionError()",
122
+ ],
123
+ 2,
124
+ "authorization check always returns True",
125
+ ),
126
+ "auth-bypass-2": (
127
+ "views.py",
128
+ [
129
+ "def admin_panel(request):",
130
+ ' if request.args.get("debug") == "1":',
131
+ " return render_admin()",
132
+ " if not request.user or not request.user.is_staff:",
133
+ " abort(403)",
134
+ " return render_admin()",
135
+ ],
136
+ 2,
137
+ "debug query param bypasses staff check",
138
+ ),
139
+ "off-by-one-1": (
140
+ "paging.py",
141
+ [
142
+ "def page(items, n, size):",
143
+ " start = n * size",
144
+ " return items[start:start + size + 1]",
145
+ ],
146
+ 3,
147
+ "page slice returns size+1 items",
148
+ ),
149
+ "off-by-one-2": (
150
+ "retry.py",
151
+ [
152
+ "def fetch_with_retry(url, tries=3):",
153
+ " for i in range(tries + 1):",
154
+ " try:",
155
+ " return http_get(url)",
156
+ " except NetError:",
157
+ " continue",
158
+ " raise GiveUp()",
159
+ ],
160
+ 2,
161
+ "loop runs tries+1 attempts, not tries",
162
+ ),
163
+ "exec-input-1": (
164
+ "render.py",
165
+ [
166
+ "def render(template_name, context):",
167
+ ' expr = "f\'" + open(template_name).read() + "\'"',
168
+ " return eval(expr, {}, context)",
169
+ ],
170
+ 3,
171
+ "eval on template file content with context",
172
+ ),
173
+ "exec-input-2": (
174
+ "calc.py",
175
+ [
176
+ "def calculate(formula, variables):",
177
+ ' return eval(formula, {"__builtins__": {}}, variables)',
178
+ ],
179
+ 2,
180
+ "eval on user-supplied formula string",
181
+ ),
182
+ "weak-crypto-1": (
183
+ "passwords.py",
184
+ [
185
+ "import hashlib",
186
+ "",
187
+ "def store_pw(username, password):",
188
+ " digest = hashlib.md5(password.encode()).hexdigest()",
189
+ " db.save(username, digest)",
190
+ ],
191
+ 4,
192
+ "MD5 for password hashing, no salt",
193
+ ),
194
+ "weak-crypto-2": (
195
+ "tokens.py",
196
+ [
197
+ "import random",
198
+ "",
199
+ "def new_token():",
200
+ ' return "%016x" % random.getrandbits(64)',
201
+ ],
202
+ 4,
203
+ "Mersenne Twister for security tokens",
204
+ ),
205
+ "path-traversal-1": (
206
+ "files.py",
207
+ [
208
+ "import os",
209
+ "",
210
+ 'BASE = "/srv/uploads"',
211
+ "",
212
+ "def read_upload(name):",
213
+ " with open(os.path.join(BASE, name)) as f:",
214
+ " return f.read()",
215
+ ],
216
+ 6,
217
+ "unsanitized join allows ../ escape",
218
+ ),
219
+ "path-traversal-2": (
220
+ "export.py",
221
+ [
222
+ "import os",
223
+ "import shutil",
224
+ "",
225
+ "def export_report(name, dest_dir):",
226
+ ' src = os.path.join("/srv/reports", name)',
227
+ " return shutil.copy(src, dest_dir)",
228
+ ],
229
+ 5,
230
+ "unsanitized report name allows ../ escape",
231
+ ),
232
+ }
233
+
234
+ CLEAN_FIXTURES = {
235
+ "clean-1": (
236
+ "util.py",
237
+ [
238
+ "def clamp(value, lo, hi):",
239
+ " return max(lo, min(hi, value))",
240
+ ],
241
+ "no defect: pure bounds clamp",
242
+ ),
243
+ "clean-2": (
244
+ "queries.py",
245
+ [
246
+ "import sqlite3",
247
+ "",
248
+ "def get_user(db, username):",
249
+ " conn = sqlite3.connect(db)",
250
+ " return conn.execute(",
251
+ ' "SELECT * FROM users WHERE name = ?", (username,)',
252
+ " ).fetchall()",
253
+ ],
254
+ "no defect: parameterized query",
255
+ ),
256
+ "clean-3": (
257
+ "authn.py",
258
+ [
259
+ "import hashlib",
260
+ "import hmac",
261
+ "import secrets",
262
+ "",
263
+ "def check(password, stored):",
264
+ ' digest = hashlib.pbkdf2_hmac("sha256", password.encode(), stored["salt"], 600_000)',
265
+ ' return hmac.compare_digest(digest, stored["digest"])',
266
+ ],
267
+ "no defect: salted KDF with constant-time compare",
268
+ ),
269
+ "clean-4": (
270
+ "counter_safe.py",
271
+ [
272
+ "import threading",
273
+ "",
274
+ "_lock = threading.Lock()",
275
+ "total = 0",
276
+ "",
277
+ "def add(n):",
278
+ " global total",
279
+ " with _lock:",
280
+ " total += n",
281
+ ],
282
+ "no defect: locked counter",
283
+ ),
284
+ }
285
+
286
+
287
+ def generate(root):
288
+ root = Path(root)
289
+ manifest = []
290
+ for fid, (fname, lines, line, desc) in FIXTURES.items():
291
+ d = root / fid
292
+ d.mkdir(parents=True, exist_ok=True)
293
+ d.joinpath(fname).write_text("\n".join(lines) + "\n", encoding="utf-8")
294
+ manifest.append(
295
+ {
296
+ "id": fid,
297
+ "file": fname,
298
+ "line": line,
299
+ "class": fid.rsplit("-", 1)[0],
300
+ "defect": desc,
301
+ "clean": False,
302
+ }
303
+ )
304
+ for fid, (fname, lines, desc) in CLEAN_FIXTURES.items():
305
+ d = root / fid
306
+ d.mkdir(parents=True, exist_ok=True)
307
+ d.joinpath(fname).write_text("\n".join(lines) + "\n", encoding="utf-8")
308
+ manifest.append(
309
+ {
310
+ "id": fid,
311
+ "file": fname,
312
+ "line": None,
313
+ "class": "clean",
314
+ "defect": desc,
315
+ "clean": True,
316
+ }
317
+ )
318
+ root.joinpath("manifest.json").write_text(
319
+ json.dumps(manifest, indent=2), encoding="utf-8"
320
+ )
321
+ print(f"generated {len(manifest)} fixtures in {root}")
322
+ return manifest
323
+
324
+
325
+ def run_audit(target_dir, mock, timeout=600):
326
+ """Invoke the real dispatch path. Advisory: findings only, never blocks."""
327
+ args = {"target": str(target_dir)}
328
+ if mock:
329
+ args["mock_mode"] = True
330
+ proc = subprocess.run(
331
+ [
332
+ sys.executable,
333
+ str(PROJECT_ROOT / "runner" / "mcp_server.py"),
334
+ "call",
335
+ "iumbtems_code_audit",
336
+ json.dumps(args),
337
+ ],
338
+ capture_output=True,
339
+ text=True,
340
+ timeout=timeout,
341
+ cwd=str(PROJECT_ROOT),
342
+ )
343
+ return proc.stdout + proc.stderr
344
+
345
+
346
+ def advisory_comment(fixture_id, findings):
347
+ return (
348
+ f"### IUMBTEMS advisory review: `{fixture_id}`\n\n"
349
+ f"Automated findings below are **non-blocking**. Verify each cited line before acting.\n\n"
350
+ f"{findings}\n"
351
+ )
352
+
353
+
354
+ LINE_RE = re.compile(r"(?:^|[\s(:\[\"'])(?:line\s+)?L?(\d{1,4})\b", re.IGNORECASE)
355
+
356
+
357
+ def score_fixture(entry, findings):
358
+ """Hit = seeded filename cited with a line number within tolerance."""
359
+ if entry["clean"]:
360
+ cited_own_file = entry["file"] in findings
361
+ return {"hit": False, "false_positive": cited_own_file}
362
+ if entry["file"] not in findings:
363
+ return {"hit": False, "false_positive": False}
364
+ lines = {int(n) for n in LINE_RE.findall(findings)}
365
+ hit = any(abs(n - entry["line"]) <= LINE_TOLERANCE for n in lines)
366
+ return {"hit": hit, "false_positive": False}
367
+
368
+
369
+ def score_all(root):
370
+ manifest = json.loads((Path(root) / "manifest.json").read_text(encoding="utf-8"))
371
+ results = []
372
+ for entry in manifest:
373
+ comment_file = Path(root) / entry["id"] / "advisory.md"
374
+ findings = (
375
+ comment_file.read_text(encoding="utf-8") if comment_file.exists() else ""
376
+ )
377
+ results.append({"id": entry["id"], **score_fixture(entry, findings)})
378
+ defective = [r for r, e in zip(results, manifest) if not e["clean"]]
379
+ clean = [r for r, e in zip(results, manifest) if e["clean"]]
380
+ hits = sum(1 for r in defective if r["hit"])
381
+ fps = sum(1 for r in clean if r["false_positive"])
382
+ return {
383
+ "fixtures": len(manifest),
384
+ "defective": len(defective),
385
+ "hits": hits,
386
+ "hit_rate": round(hits / max(len(defective), 1), 3),
387
+ "false_positives": fps,
388
+ "clean_total": len(clean),
389
+ "misses": [r["id"] for r in defective if not r["hit"]],
390
+ "results": results,
391
+ }
392
+
393
+
394
+ def main(argv=None):
395
+ ap = argparse.ArgumentParser()
396
+ ap.add_argument("--dir", default="/tmp/bet1")
397
+ ap.add_argument("--generate", action="store_true")
398
+ ap.add_argument("--run", action="store_true")
399
+ ap.add_argument("--score", action="store_true")
400
+ ap.add_argument("--mock", action="store_true")
401
+ ap.add_argument("--live", action="store_true")
402
+ ap.add_argument("--only", default="")
403
+ args = ap.parse_args(argv)
404
+ root = Path(args.dir)
405
+
406
+ if args.generate or (not args.run and not args.score):
407
+ generate(root)
408
+ if args.run:
409
+ manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
410
+ only = {s.strip() for s in args.only.split(",") if s.strip()}
411
+ for entry in manifest:
412
+ if only and entry["id"] not in only:
413
+ continue
414
+ findings = run_audit(root / entry["id"], mock=args.mock and not args.live)
415
+ (root / entry["id"] / "advisory.md").write_text(
416
+ advisory_comment(entry["id"], findings), encoding="utf-8"
417
+ )
418
+ print(f"advisory written: {entry['id']}")
419
+ if args.score:
420
+ summary = score_all(root)
421
+ print(json.dumps(summary, indent=2))
422
+ return 0 # advisory: never blocks
423
+
424
+
425
+ if __name__ == "__main__":
426
+ sys.exit(main())