rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
8bccaf2
·
1 Parent(s): 22e3bdb

docs(plan): error/risk audit framework implementation plan

Browse files

7 tasks, TDD, ~25 checks across 5 tiers + self-verifying selftest. Local
commit only — not pushed (no redeploy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

docs/superpowers/plans/2026-05-17-error-audit-framework.md ADDED
@@ -0,0 +1,745 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Error / Risk Audit Framework — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a single runnable, self-verifying `audit/` framework that mechanically catches every known error/risk class and tests all functionality before commit/deploy.
6
+
7
+ **Architecture:** Modular Python package `audit/`. One pure function per check, decorated with `@register(id, tier, title)`, returning a `Result(status, evidence, remediation)`. A runner selects tiers, prints a table, exits non-zero on any FAIL. A `--selftest` mode proves every check fails on a deliberately-broken fixture (so the auditor can't be silently broken). Read-only against production.
8
+
9
+ **Tech Stack:** Python 3.11 stdlib only (subprocess/pathlib/ast/json/urllib), the repo's `.venv` pytest, `ruff`, `npm`, the existing playwright-skill for E2E.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-05-17-error-audit-framework-design.md`
12
+
13
+ ---
14
+
15
+ ## File Structure
16
+
17
+ | File | Responsibility |
18
+ | --- | --- |
19
+ | `audit/__init__.py` | empty package marker |
20
+ | `audit/core.py` | `Status`, `Result`, `Check`, `register`, `CHECKS`, `git()`/`sh()` helpers, `run()`, `selftest()`, tier map, `REPO` |
21
+ | `audit/__main__.py` | CLI: arg parse → `run()`/`selftest()`; exit code |
22
+ | `audit/tier1_repo.py` | T1.1–T1.5 (repo integrity) |
23
+ | `audit/tier2_code.py` | T2.1–T2.6 (code soundness) |
24
+ | `audit/tier3_build.py` | T3.1–T3.3 (build & test gates) |
25
+ | `audit/tier4_functional.py` | T4-smoke + T4-e2e |
26
+ | `audit/tier5_deploy.py` | T5.1–T5.4 (deploy safety, read-only) |
27
+ | `audit/selftest_fixtures.py` | one broken-input factory per check id |
28
+ | `tools/audit` | thin executable entrypoint (`exec .venv/bin/python -m audit "$@"`) |
29
+ | `.githooks/pre-commit` | optional: `tools/audit --static` |
30
+ | `.githooks/pre-push` | optional: `tools/audit --build` + T5.1/T5.2 |
31
+ | `tests/test_audit_selftest.py` | runs `audit.selftest()` so the auditor is in the 215-green pytest gate |
32
+ | `audit/README.md` | usage |
33
+
34
+ Checks are grouped one module per tier (files that change together live together). Each check is independently testable via its selftest fixture.
35
+
36
+ ---
37
+
38
+ ## Task 1: Core contract + runner + CLI
39
+
40
+ **Files:**
41
+ - Create: `audit/__init__.py`, `audit/core.py`, `audit/__main__.py`, `tools/audit`
42
+ - Test: `tests/test_audit_selftest.py` (created here, expanded in Task 7)
43
+
44
+ - [ ] **Step 1: Write the failing test** — `tests/test_audit_selftest.py`
45
+
46
+ ```python
47
+ import subprocess, sys, pathlib
48
+ REPO = pathlib.Path(__file__).resolve().parent.parent
49
+
50
+ def test_core_runner_passes_a_trivial_pass_check():
51
+ # audit.core.run with only a dummy PASS check exits 0
52
+ code = subprocess.run(
53
+ [sys.executable, "-c",
54
+ "import audit.core as c; "
55
+ "c.CHECKS.clear(); "
56
+ "c.register('X.1','static','dummy')(lambda: c.Result('X.1', c.Status.PASS, 'ok')); "
57
+ "import sys; sys.exit(c.run({'static'}))"],
58
+ cwd=REPO).returncode
59
+ assert code == 0
60
+
61
+ def test_core_runner_fails_on_a_fail_check():
62
+ code = subprocess.run(
63
+ [sys.executable, "-c",
64
+ "import audit.core as c; "
65
+ "c.CHECKS.clear(); "
66
+ "c.register('X.2','static','dummy')(lambda: c.Result('X.2', c.Status.FAIL, 'bad')); "
67
+ "import sys; sys.exit(c.run({'static'}))"],
68
+ cwd=REPO).returncode
69
+ assert code == 1
70
+ ```
71
+
72
+ - [ ] **Step 2: Run test to verify it fails**
73
+
74
+ Run: `.venv/bin/python -m pytest tests/test_audit_selftest.py -q`
75
+ Expected: FAIL — `ModuleNotFoundError: No module named 'audit'`
76
+
77
+ - [ ] **Step 3: Write `audit/__init__.py`** — empty file.
78
+
79
+ - [ ] **Step 4: Write `audit/core.py`**
80
+
81
+ ```python
82
+ """Audit framework core: check contract, registry, runner, selftest."""
83
+ from __future__ import annotations
84
+ import dataclasses, enum, json, pathlib, subprocess, sys
85
+ from typing import Callable
86
+
87
+ REPO = pathlib.Path(__file__).resolve().parent.parent
88
+
89
+
90
+ class Status(enum.Enum):
91
+ PASS = "PASS"
92
+ WARN = "WARN"
93
+ FAIL = "FAIL"
94
+ SKIP = "SKIP"
95
+
96
+
97
+ @dataclasses.dataclass
98
+ class Result:
99
+ check_id: str
100
+ status: Status
101
+ evidence: str
102
+ remediation: str = ""
103
+
104
+
105
+ @dataclasses.dataclass
106
+ class Check:
107
+ id: str
108
+ tier: str # static | build | functional | deploy
109
+ title: str
110
+ fn: Callable[[], Result]
111
+
112
+
113
+ CHECKS: list[Check] = []
114
+
115
+
116
+ def register(id: str, tier: str, title: str):
117
+ def deco(fn: Callable[[], Result]) -> Callable[[], Result]:
118
+ CHECKS.append(Check(id, tier, title, fn))
119
+ return fn
120
+ return deco
121
+
122
+
123
+ TIER_SETS = {
124
+ "static": {"static"},
125
+ "build": {"static", "build"},
126
+ "functional": {"static", "build", "functional"},
127
+ "deploy": {"deploy"},
128
+ "all": {"static", "build", "functional", "deploy"},
129
+ }
130
+
131
+
132
+ def sh(cmd: list[str], timeout: int = 120) -> subprocess.CompletedProcess:
133
+ return subprocess.run(cmd, cwd=REPO, capture_output=True, text=True, timeout=timeout)
134
+
135
+
136
+ def git(*args: str) -> str:
137
+ return sh(["git", *args]).stdout.strip()
138
+
139
+
140
+ def _load_all_checks() -> None:
141
+ from audit import (tier1_repo, tier2_code, tier3_build, # noqa: F401
142
+ tier4_functional, tier5_deploy)
143
+
144
+
145
+ def run(selected: set[str], as_json: bool = False) -> int:
146
+ if not CHECKS:
147
+ _load_all_checks()
148
+ rows = []
149
+ for c in sorted(CHECKS, key=lambda c: c.id):
150
+ if c.tier not in selected:
151
+ continue
152
+ try:
153
+ r = c.fn()
154
+ except Exception as e: # a broken check is a FAIL, never silent
155
+ r = Result(c.id, Status.FAIL, f"check raised {type(e).__name__}: {e}",
156
+ "fix the check or the underlying issue")
157
+ rows.append((c, r))
158
+ fails = [r for _, r in rows if r.status is Status.FAIL]
159
+ if as_json:
160
+ print(json.dumps([{"id": c.id, "status": r.status.value,
161
+ "evidence": r.evidence, "remediation": r.remediation}
162
+ for c, r in rows], indent=2))
163
+ else:
164
+ for c, r in rows:
165
+ mark = {"PASS": "✓", "WARN": "▲", "FAIL": "✗", "SKIP": "·"}[r.status.value]
166
+ print(f" {mark} [{c.id}] {c.title}: {r.status.value}")
167
+ if r.status in (Status.FAIL, Status.WARN):
168
+ print(f" {r.evidence}")
169
+ if r.remediation:
170
+ print(f" fix: {r.remediation}")
171
+ n = len(rows)
172
+ print(f"\n {n} checks · "
173
+ f"{sum(1 for _,r in rows if r.status is Status.PASS)} pass · "
174
+ f"{sum(1 for _,r in rows if r.status is Status.WARN)} warn · "
175
+ f"{len(fails)} fail · "
176
+ f"{sum(1 for _,r in rows if r.status is Status.SKIP)} skip")
177
+ return 1 if fails else 0
178
+
179
+
180
+ def selftest() -> int:
181
+ """Every check must FAIL on its deliberately-broken fixture."""
182
+ from audit.selftest_fixtures import FIXTURES
183
+ if not CHECKS:
184
+ _load_all_checks()
185
+ bad = []
186
+ for c in CHECKS:
187
+ fx = FIXTURES.get(c.id)
188
+ if fx is None:
189
+ bad.append(f"{c.id}: NO selftest fixture")
190
+ continue
191
+ with fx() as broken_ctx:
192
+ r = c.fn()
193
+ if r.status is not Status.FAIL:
194
+ bad.append(f"{c.id}: expected FAIL on broken fixture, got {r.status.value}")
195
+ for b in bad:
196
+ print(f" ✗ {b}")
197
+ print(f"\n selftest: {len(CHECKS)} checks · {len(bad)} not self-verifying")
198
+ return 1 if bad else 0
199
+ ```
200
+
201
+ - [ ] **Step 5: Write `audit/__main__.py`**
202
+
203
+ ```python
204
+ import argparse, sys
205
+ from audit import core
206
+
207
+ def main() -> int:
208
+ p = argparse.ArgumentParser(prog="python -m audit")
209
+ g = p.add_mutually_exclusive_group()
210
+ for t in ("static", "build", "functional", "deploy", "all"):
211
+ g.add_argument(f"--{t}", action="store_const", const=t, dest="tier")
212
+ p.add_argument("--selftest", action="store_true")
213
+ p.add_argument("--json", action="store_true")
214
+ a = p.parse_args()
215
+ if a.selftest:
216
+ return core.selftest()
217
+ return core.run(core.TIER_SETS[a.tier or "all"], as_json=a.json)
218
+
219
+ if __name__ == "__main__":
220
+ sys.exit(main())
221
+ ```
222
+
223
+ - [ ] **Step 6: Write `tools/audit`**
224
+
225
+ ```bash
226
+ #!/usr/bin/env bash
227
+ # Entrypoint for the audit framework. Usage: tools/audit [--static|--build|--functional|--deploy|--all|--selftest] [--json]
228
+ set -euo pipefail
229
+ cd "$(dirname "$0")/.."
230
+ exec .venv/bin/python -m audit "$@"
231
+ ```
232
+
233
+ Then: `chmod +x tools/audit`
234
+
235
+ - [ ] **Step 7: Create empty stub modules so `_load_all_checks` imports cleanly**
236
+
237
+ Create `audit/tier1_repo.py`, `audit/tier2_code.py`, `audit/tier3_build.py`, `audit/tier4_functional.py`, `audit/tier5_deploy.py`, `audit/selftest_fixtures.py` each containing only:
238
+
239
+ ```python
240
+ # filled in a later task
241
+ ```
242
+
243
+ And in `audit/selftest_fixtures.py`:
244
+
245
+ ```python
246
+ FIXTURES: dict = {}
247
+ ```
248
+
249
+ - [ ] **Step 8: Run test to verify it passes**
250
+
251
+ Run: `.venv/bin/python -m pytest tests/test_audit_selftest.py -q`
252
+ Expected: PASS (2 passed)
253
+
254
+ - [ ] **Step 9: Commit**
255
+
256
+ ```bash
257
+ git add audit/ tools/audit tests/test_audit_selftest.py
258
+ git commit -m "feat(audit): core contract + runner + CLI"
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Task 2: Tier 1 — Repo integrity (T1.1–T1.5)
264
+
265
+ **Files:**
266
+ - Modify: `audit/tier1_repo.py`, `audit/selftest_fixtures.py`
267
+
268
+ - [ ] **Step 1: Write `audit/tier1_repo.py`**
269
+
270
+ ```python
271
+ """Tier 1 — repo integrity (pre-commit, fast)."""
272
+ from __future__ import annotations
273
+ import re
274
+ from audit.core import register, Result, Status, REPO, git, sh
275
+
276
+
277
+ @register("T1.1", "static", "no tracked symlinks")
278
+ def t1_1() -> Result:
279
+ out = git("ls-files", "-s")
280
+ syms = [ln.split("\t", 1)[1] for ln in out.splitlines() if ln.startswith("120000")]
281
+ if syms:
282
+ return Result("T1.1", Status.FAIL, f"tracked symlinks: {syms}",
283
+ "git rm --cached <path>; add to .gitignore (no trailing slash)")
284
+ return Result("T1.1", Status.PASS, "no tracked symlinks")
285
+
286
+
287
+ @register("T1.2", "static", "LFS coverage for binary/large files")
288
+ def t1_2() -> Result:
289
+ ga = (REPO / ".gitattributes").read_text(encoding="utf-8", errors="replace")
290
+ lfs_globs = [ln.split()[0] for ln in ga.splitlines()
291
+ if "filter=lfs" in ln and ln.strip() and not ln.startswith("#")]
292
+ lfs_files = set(sh(["git", "lfs", "ls-files", "-n"]).stdout.split())
293
+ bad = []
294
+ for ln in git("ls-files", "-s").splitlines():
295
+ mode, _, _, path = ln.replace("\t", " ").split(maxsplit=3)
296
+ if mode == "120000":
297
+ continue
298
+ blob_sz = sh(["git", "cat-file", "-s", ln.split()[1]]).stdout.strip()
299
+ is_lfs = path in lfs_files
300
+ big = blob_sz.isdigit() and int(blob_sz) > 512 * 1024
301
+ if big and not is_lfs:
302
+ bad.append(f"{path} ({int(blob_sz)//1024} KB) not LFS")
303
+ if bad:
304
+ return Result("T1.2", Status.FAIL, "; ".join(bad[:8]),
305
+ "add a filter=lfs rule to .gitattributes; git rm --cached + re-add the files")
306
+ return Result("T1.2", Status.PASS, f"{len(lfs_files)} LFS files; no oversized non-LFS blobs")
307
+
308
+
309
+ @register("T1.3", "static", "no real secrets tracked")
310
+ def t1_3() -> Result:
311
+ KEYISH = re.compile(r"(hf_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_\-]{20,}|xox[bp]-[A-Za-z0-9-]{20,})")
312
+ suspects = []
313
+ for path in git("ls-files").splitlines():
314
+ base = path.rsplit("/", 1)[-1]
315
+ if base == ".env" or (base.startswith(".env") and not base.endswith((".example", ".sample"))):
316
+ suspects.append(f"{path} (real dotenv tracked)")
317
+ continue
318
+ if path.endswith((".png", ".jpg", ".pdf", ".duckdb", ".bin", ".ico", ".woff", ".woff2", ".ttf")):
319
+ continue
320
+ try:
321
+ txt = (REPO / path).read_text(encoding="utf-8", errors="ignore")
322
+ except Exception:
323
+ continue
324
+ if KEYISH.search(txt) and "example" not in path and "ADR-010" not in path:
325
+ suspects.append(f"{path} (key-shaped string)")
326
+ if suspects:
327
+ return Result("T1.3", Status.FAIL, "; ".join(suspects[:8]),
328
+ "remove from index + history; rotate the key; gitignore the file")
329
+ return Result("T1.3", Status.PASS, "no real .env / key material tracked")
330
+
331
+
332
+ @register("T1.4", "static", ".gitignore robust for file AND dir")
333
+ def t1_4() -> Result:
334
+ intents = ["tools/.pdf_text_cache", "rag/corpus", "rag/extracted", "rag/vectors"]
335
+ gi = (REPO / ".gitignore").read_text(encoding="utf-8", errors="replace").splitlines()
336
+ missing = []
337
+ for it in intents:
338
+ # a slash-less line ignores BOTH the file and the dir form
339
+ if it not in gi:
340
+ missing.append(it)
341
+ if missing:
342
+ return Result("T1.4", Status.FAIL, f"only dir-form (or absent) ignore for: {missing}",
343
+ "add a slash-less line per intent so a symlink/file of that name is also ignored")
344
+ return Result("T1.4", Status.PASS, "file+dir ignore intents present")
345
+
346
+
347
+ @register("T1.5", "static", "no junk/build artifacts tracked")
348
+ def t1_5() -> Result:
349
+ JUNK = ("tools/.pdf_text_cache/", ".pytest_cache/", ".DS_Store",
350
+ "frontend/out/", "frontend/.next/", "node_modules/", ".tsbuildinfo")
351
+ tracked = git("ls-files").splitlines()
352
+ hits = [p for p in tracked if any(j in p for j in JUNK)]
353
+ if hits:
354
+ return Result("T1.5", Status.FAIL, f"{len(hits)} junk paths e.g. {hits[:5]}",
355
+ "git rm -r --cached <path> and gitignore it")
356
+ return Result("T1.5", Status.PASS, "no caches/build artifacts tracked")
357
+ ```
358
+
359
+ - [ ] **Step 2: Add the 5 selftest fixtures** — append to `audit/selftest_fixtures.py`
360
+
361
+ ```python
362
+ import contextlib, pathlib, subprocess
363
+ from audit.core import REPO
364
+
365
+ @contextlib.contextmanager
366
+ def _staged_symlink():
367
+ p = REPO / "_audit_selftest_link"
368
+ p.symlink_to("/nonexistent")
369
+ subprocess.run(["git", "add", "-f", str(p)], cwd=REPO)
370
+ try:
371
+ yield
372
+ finally:
373
+ subprocess.run(["git", "rm", "-f", "--cached", "-q", str(p)], cwd=REPO)
374
+ p.unlink(missing_ok=True)
375
+
376
+ # (Analogous tiny context managers for T1.2..T1.5: stage an oversized non-LFS
377
+ # blob; a file containing 'hf_' + 40 chars; remove the rag/corpus gitignore
378
+ # line in a temp copy; stage a .DS_Store. Each yields a broken repo state and
379
+ # restores it. Full code written in this step — no placeholders.)
380
+ FIXTURES.update({
381
+ "T1.1": _staged_symlink,
382
+ # "T1.2": _staged_big_blob, "T1.3": _staged_fake_key,
383
+ # "T1.4": _broken_gitignore, "T1.5": _staged_dsstore
384
+ })
385
+ ```
386
+
387
+ > Implementer note: write the four remaining context managers in full here using the same `@contextlib.contextmanager` + git add/rm + restore pattern as `_staged_symlink`. They are 6–10 lines each; do not abbreviate in the actual code.
388
+
389
+ - [ ] **Step 3: Verify Tier 1 runs**
390
+
391
+ Run: `.venv/bin/python -m audit --static`
392
+ Expected: T1.1–T1.5 all `✓ PASS` on the current clean tree.
393
+
394
+ - [ ] **Step 4: Verify selftest catches breakage**
395
+
396
+ Run: `.venv/bin/python -m audit --selftest 2>&1 | grep -E 'T1\.'`
397
+ Expected: no `T1.x: ...not self-verifying` lines (each FAILs on its broken fixture).
398
+
399
+ - [ ] **Step 5: Commit**
400
+
401
+ ```bash
402
+ git add audit/tier1_repo.py audit/selftest_fixtures.py
403
+ git commit -m "feat(audit): Tier 1 repo-integrity checks + selftest fixtures"
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Task 3: Tier 2 — Code soundness (T2.1–T2.6)
409
+
410
+ **Files:** Modify `audit/tier2_code.py`, `audit/selftest_fixtures.py`
411
+
412
+ - [ ] **Step 1: Write `audit/tier2_code.py`** (full code)
413
+
414
+ ```python
415
+ """Tier 2 — code soundness (pre-commit)."""
416
+ from __future__ import annotations
417
+ import ast, re, subprocess, sys
418
+ from audit.core import register, Result, Status, REPO, git, sh
419
+
420
+ PY = [p for p in git("ls-files").splitlines() if p.endswith(".py")]
421
+ DEAD = ("backend.orchestrator", "import sales_brain", "qa_brain", "faithfulness",
422
+ "backend.translator", "profile_extractor", "get_judge_llm", "get_fast_brain_llm")
423
+
424
+
425
+ @register("T2.1", "static", "all .py parse (AST)")
426
+ def t2_1() -> Result:
427
+ bad = []
428
+ for p in PY:
429
+ try:
430
+ ast.parse((REPO / p).read_text(encoding="utf-8", errors="replace"), p)
431
+ except SyntaxError as e:
432
+ bad.append(f"{p}: {e}")
433
+ return (Result("T2.1", Status.FAIL, "; ".join(bad[:5]), "fix the syntax error")
434
+ if bad else Result("T2.1", Status.PASS, f"{len(PY)} files parse"))
435
+
436
+
437
+ @register("T2.2", "static", "runtime-import every backend/rag module")
438
+ def t2_2() -> Result:
439
+ mods = []
440
+ for p in PY:
441
+ if (p.startswith("backend/") or p.startswith("rag/")) and not p.endswith("__init__.py") \
442
+ and "_smoke_test" not in p and "/tests/" not in p:
443
+ mods.append(p[:-3].replace("/", "."))
444
+ code = "import importlib,sys\nbad=[]\n" + \
445
+ "".join(f"try:\n importlib.import_module({m!r})\nexcept Exception as e:\n bad.append(({m!r},repr(e)))\n"
446
+ for m in mods) + "print(bad)\nsys.exit(1 if bad else 0)"
447
+ r = sh([".venv/bin/python", "-c", code], timeout=300)
448
+ if r.returncode != 0:
449
+ return Result("T2.2", Status.FAIL, r.stdout.strip()[:600],
450
+ "fix the import (often: import wrongly placed inside a docstring)")
451
+ return Result("T2.2", Status.PASS, f"{len(mods)} modules import clean")
452
+
453
+
454
+ @register("T2.3", "static", "no refs to deleted modules/symbols")
455
+ def t2_3() -> Result:
456
+ code_hits, doc_hits = [], []
457
+ for p in PY:
458
+ for i, ln in enumerate(open(REPO / p, encoding="utf-8", errors="replace"), 1):
459
+ for d in DEAD:
460
+ if d in ln:
461
+ s = ln.strip()
462
+ (doc_hits if s.startswith("#") or s.startswith(('"', "'", "*")) else code_hits
463
+ ).append(f"{p}:{i} {d}")
464
+ if code_hits:
465
+ return Result("T2.3", Status.FAIL, "; ".join(code_hits[:6]),
466
+ "remove/replace the dead reference (e.g. get_judge_llm → get_brain_llm)")
467
+ if doc_hits:
468
+ return Result("T2.3", Status.WARN, f"{len(doc_hits)} stale comment refs e.g. {doc_hits[:3]}",
469
+ "tidy the stale comment")
470
+ return Result("T2.3", Status.PASS, "no dead-symbol references")
471
+
472
+
473
+ @register("T2.4", "static", "no */ inside CSS/JS block-comment body")
474
+ def t2_4() -> Result:
475
+ bad = []
476
+ for p in git("ls-files").splitlines():
477
+ if not p.endswith((".css", ".scss")):
478
+ continue
479
+ txt = (REPO / p).read_text(encoding="utf-8", errors="replace")
480
+ for m in re.finditer(r"/\*.*?\*/", txt, re.S):
481
+ body = m.group(0)[2:-2]
482
+ if "*/" in body:
483
+ bad.append(f"{p}: nested */ in comment")
484
+ return (Result("T2.4", Status.FAIL, "; ".join(bad), "space the token: '* /' or reword")
485
+ if bad else Result("T2.4", Status.PASS, "no comment-terminator footgun"))
486
+
487
+
488
+ @register("T2.5", "static", "no hardcoded 40-data path construction")
489
+ def t2_5() -> Result:
490
+ pat = re.compile(r'/\s*["\']40-data["\']')
491
+ bad = []
492
+ for p in PY:
493
+ if not (p.startswith("backend/") or p.startswith("rag/")):
494
+ continue
495
+ if p.endswith("config.py"):
496
+ continue
497
+ for i, ln in enumerate(open(REPO / p, encoding="utf-8", errors="replace"), 1):
498
+ if pat.search(ln) and not ln.strip().startswith("#"):
499
+ bad.append(f"{p}:{i}")
500
+ return (Result("T2.5", Status.FAIL, "; ".join(bad[:8]), "use settings.DATA_DIR")
501
+ if bad else Result("T2.5", Status.PASS, "DATA_DIR centralized"))
502
+
503
+
504
+ @register("T2.6", "static", "ruff + tsc clean")
505
+ def t2_6() -> Result:
506
+ ruff = sh([".venv/bin/ruff", "check", "backend", "rag", "audit"], timeout=120)
507
+ tsc = sh(["npx", "--prefix", "frontend", "--no-install", "tsc", "-p", "frontend", "--noEmit"], timeout=240)
508
+ probs = []
509
+ if ruff.returncode not in (0, 127):
510
+ probs.append("ruff: " + (ruff.stdout or ruff.stderr).strip().splitlines()[-1][:200])
511
+ if tsc.returncode not in (0, 127):
512
+ probs.append("tsc: " + (tsc.stdout or tsc.stderr).strip().splitlines()[-1][:200])
513
+ if 127 in (ruff.returncode, tsc.returncode):
514
+ return Result("T2.6", Status.SKIP, "ruff/tsc not installed", "pip install ruff / npm i")
515
+ return (Result("T2.6", Status.FAIL, " | ".join(probs), "fix lint/type errors")
516
+ if probs else Result("T2.6", Status.PASS, "ruff + tsc clean"))
517
+ ```
518
+
519
+ - [ ] **Step 2: Add T2.1–T2.6 selftest fixtures** to `audit/selftest_fixtures.py` — each a context manager that creates the broken condition (a temp `.py` with a SyntaxError; a temp module with an import inside its docstring; a temp file referencing `get_judge_llm`; a temp `.css` with `/* a */ b */`; a temp backend file with `/ "40-data" /`; monkeypatch ruff to fail). Full code, same pattern as Task 2; register all six in `FIXTURES`.
520
+
521
+ - [ ] **Step 3: Verify** — `.venv/bin/python -m audit --static` → T2.* PASS on clean tree.
522
+ - [ ] **Step 4: Selftest** — `.venv/bin/python -m audit --selftest` → no T2 self-verify gaps.
523
+ - [ ] **Step 5: Commit** — `git add audit/tier2_code.py audit/selftest_fixtures.py && git commit -m "feat(audit): Tier 2 code-soundness checks"`
524
+
525
+ ---
526
+
527
+ ## Task 4: Tier 3 — Build & test gates (T3.1–T3.3)
528
+
529
+ **Files:** Modify `audit/tier3_build.py`, `audit/selftest_fixtures.py`
530
+
531
+ - [ ] **Step 1: Write `audit/tier3_build.py`** (full code)
532
+
533
+ ```python
534
+ """Tier 3 — build & test gates."""
535
+ from __future__ import annotations
536
+ import re, urllib.request
537
+ from audit.core import register, Result, Status, sh
538
+
539
+
540
+ @register("T3.1", "build", "pytest green (clean-clone scoping)")
541
+ def t3_1() -> Result:
542
+ r = sh([".venv/bin/python", "-m", "pytest", "-q"], timeout=600)
543
+ tail = (r.stdout + r.stderr).strip().splitlines()[-1] if (r.stdout or r.stderr) else ""
544
+ if r.returncode != 0 or "error" in tail.lower() or "failed" in tail.lower():
545
+ return Result("T3.1", Status.FAIL, tail[:200], "fix failing tests / collection error")
546
+ m = re.search(r"(\d+) passed", tail)
547
+ return Result("T3.1", Status.PASS, f"{m.group(0) if m else 'green'}")
548
+
549
+
550
+ @register("T3.2", "build", "next build (production static export)")
551
+ def t3_2() -> Result:
552
+ r = sh(["npm", "--prefix", "frontend", "run", "build"], timeout=900)
553
+ htmls = sh(["bash", "-lc", "ls frontend/out/*.html 2>/dev/null | wc -l"]).stdout.strip()
554
+ if r.returncode != 0:
555
+ return Result("T3.2", Status.FAIL,
556
+ (r.stdout + r.stderr).strip().splitlines()[-1][:200],
557
+ "fix the production build error")
558
+ if htmls in ("", "0"):
559
+ return Result("T3.2", Status.FAIL, "no static export emitted",
560
+ "ensure next.config has output:'export'")
561
+ return Result("T3.2", Status.PASS, f"build ok, {htmls} html exported")
562
+
563
+
564
+ @register("T3.3", "build", "backend boots + /api/health (local)")
565
+ def t3_3() -> Result:
566
+ # backend already runs locally on :8000 during dev; verify it imports + health
567
+ imp = sh([".venv/bin/python", "-c", "import backend.main"], timeout=120)
568
+ if imp.returncode != 0:
569
+ return Result("T3.3", Status.FAIL, imp.stderr.strip()[:200], "fix backend import")
570
+ try:
571
+ with urllib.request.urlopen("http://127.0.0.1:8000/api/health", timeout=8) as f:
572
+ body = f.read(200).decode()
573
+ ok = '"status":"ok"' in body
574
+ return (Result("T3.3", Status.PASS, "backend imports + /api/health ok") if ok
575
+ else Result("T3.3", Status.FAIL, body[:120], "backend unhealthy"))
576
+ except Exception:
577
+ return Result("T3.3", Status.SKIP, "no local backend on :8000",
578
+ "start uvicorn backend.main:app --port 8000 to run this check")
579
+ ```
580
+
581
+ - [ ] **Step 2: Fixtures** for T3.1–T3.3 (a temp failing test file; force `next build` failure via a temp broken `.tsx` import; monkeypatch the health URL to an unreachable port → must FAIL not SKIP when import itself broken). Full code; register in `FIXTURES`.
582
+ - [ ] **Step 3: Verify** — `.venv/bin/python -m audit --build` (T3 may take minutes).
583
+ - [ ] **Step 4: Selftest** — no T3 self-verify gaps.
584
+ - [ ] **Step 5: Commit** — `git add audit/tier3_build.py audit/selftest_fixtures.py && git commit -m "feat(audit): Tier 3 build/test gates"`
585
+
586
+ ---
587
+
588
+ ## Task 5: Tier 4 — Functionality, both sub-tiers (T4-smoke, T4-e2e)
589
+
590
+ **Files:** Modify `audit/tier4_functional.py`, `audit/selftest_fixtures.py`
591
+ Create: `audit/e2e/insurancebot_e2e.js` (Playwright journeys, driven via the playwright-skill runner)
592
+
593
+ - [ ] **Step 1: Write `audit/tier4_functional.py`** — `T4.SMOKE` (one Result per endpoint group; each does a single local HTTP call to :8000 and asserts shape/sane counts: health, version, coverage≈148/20/~7300±5%, chat one turn returns non-error reply, upload-policy accepts a real corpus PDF → chunks_added>0 and rejects 100 bytes of junk, profile, scorecard, session/clear) and `T4.E2E` (shells the playwright-skill runner on `audit/e2e/insurancebot_e2e.js`, parses its `RJSON`, asserts: chat→inline cards, marketplace renders, compare modal opens, profile→premium recompute, voice copy touch-vs-desktop, PDF-upload UI ack, no console errors, no 390px overflow). Both return `SKIP` (not PASS) when the local backend / Playwright is unavailable. Full code following the Tier-3 pattern.
594
+
595
+ - [ ] **Step 2: Write `audit/e2e/insurancebot_e2e.js`** — a single Playwright script (headless) running the enumerated journeys, emitting one `RJSON {...}` line of booleans, modelled on the verified `/tmp/pw-3-multiturn.js` from the recovery session (reuse its multi-turn fact-find→recommendation logic).
596
+ - [ ] **Step 3: Fixtures** — T4.SMOKE fixture monkeypatches the base URL to a stub returning wrong coverage counts (must FAIL); T4.E2E fixture feeds a canned `RJSON` with `cardsRenderInlineOnMobile:false` (must FAIL). Full code; register.
597
+ - [ ] **Step 4: Verify** — with local backend up: `.venv/bin/python -m audit --functional`.
598
+ - [ ] **Step 5: Selftest + Commit** — `git add audit/tier4_functional.py audit/e2e/ audit/selftest_fixtures.py && git commit -m "feat(audit): Tier 4 functionality (smoke + exhaustive E2E)"`
599
+
600
+ ---
601
+
602
+ ## Task 6: Tier 5 — Deploy safety, read-only (T5.1–T5.4)
603
+
604
+ **Files:** Modify `audit/tier5_deploy.py`, `audit/selftest_fixtures.py`
605
+
606
+ - [ ] **Step 1: Write `audit/tier5_deploy.py`** (full code)
607
+
608
+ ```python
609
+ """Tier 5 — deploy safety. READ-ONLY against production (GET + git ls-remote only)."""
610
+ from __future__ import annotations
611
+ import json, urllib.request
612
+ from audit.core import register, Result, Status, REPO, git, sh
613
+
614
+ SPACE_API = "https://huggingface.co/api/spaces/rohitsar567/InsuranceBot"
615
+ LIVE = "https://rohitsar567-insurancebot.hf.space"
616
+
617
+
618
+ @register("T5.1", "deploy", "LFS pre-push validation (HF hook simulation)")
619
+ def t5_1() -> Result:
620
+ ga = (REPO / ".gitattributes").read_text("utf-8", "replace")
621
+ globs = [l.split()[0] for l in ga.splitlines() if "filter=lfs" in l and not l.startswith("#")]
622
+ lfs = set(sh(["git", "lfs", "ls-files", "-n"]).stdout.split())
623
+ import fnmatch
624
+ bad = [p for p in git("ls-files").splitlines()
625
+ if any(fnmatch.fnmatch(p, g) for g in globs) and p not in lfs]
626
+ return (Result("T5.1", Status.FAIL, f"would be HF-rejected: {bad[:6]}",
627
+ "git rm --cached + re-add so they store as LFS pointers")
628
+ if bad else Result("T5.1", Status.PASS, "all LFS-pattern files are pointers"))
629
+
630
+
631
+ @register("T5.2", "deploy", "Dockerfile coherence")
632
+ def t5_2() -> Result:
633
+ df = (REPO / "Dockerfile").read_text("utf-8", "replace")
634
+ missing = [m.split()[1] for m in df.splitlines()
635
+ if m.strip().startswith("COPY ")
636
+ and not (REPO / m.split()[1]).exists()]
637
+ collide = [p for p in git("ls-files").splitlines()
638
+ if p in ("rag/corpus", "rag/extracted", "rag/vectors")]
639
+ if missing or collide:
640
+ return Result("T5.2", Status.FAIL,
641
+ f"COPY missing: {missing}; hydration-collide: {collide}",
642
+ "fix COPY source / untrack rag/corpus|extracted|vectors")
643
+ return Result("T5.2", Status.PASS, "COPY paths exist; no hydration collision")
644
+
645
+
646
+ @register("T5.3", "deploy", "post-deploy guarded sha + live smoke")
647
+ def t5_3() -> Result:
648
+ head = git("rev-parse", "HEAD")[:12]
649
+ try:
650
+ rt = json.load(urllib.request.urlopen(SPACE_API, timeout=20))["runtime"]
651
+ sha = (rt.get("sha") or "")[:12]
652
+ if rt.get("stage") != "RUNNING":
653
+ return Result("T5.3", Status.WARN, f"Space stage={rt.get('stage')} sha={sha}",
654
+ "wait for build / check HF build log")
655
+ if sha != head:
656
+ return Result("T5.3", Status.WARN,
657
+ f"live sha {sha} != local HEAD {head} (not yet deployed?)",
658
+ "push + verify; never trust 'RUNNING' alone (LFS silent-failure)")
659
+ h = urllib.request.urlopen(f"{LIVE}/api/health", timeout=20).read(200).decode()
660
+ logo = urllib.request.urlopen(f"{LIVE}/insurer-logos/oriental-insurance.png", timeout=20)
661
+ ok = '"status":"ok"' in h and logo.headers.get_content_type() == "image/png"
662
+ return (Result("T5.3", Status.PASS, f"sha {sha} live, health ok, LFS logo image/png")
663
+ if ok else Result("T5.3", Status.FAIL, f"health/logo bad: {h[:80]}",
664
+ "investigate live deploy"))
665
+ except Exception as e:
666
+ return Result("T5.3", Status.SKIP, f"no network/API: {e}", "run with network to verify deploy")
667
+
668
+
669
+ @register("T5.4", "deploy", "standing tripwires (bloat/disk/stale-docs)")
670
+ def t5_4() -> Result:
671
+ warns = []
672
+ bloat = sh(["bash", "-lc",
673
+ "find rag -name link_lists.bin -size +200M 2>/dev/null; "
674
+ "du -sm rag/_hf_dataset_backup 2>/dev/null | awk '$1>20000{print}'"]).stdout.strip()
675
+ if bloat:
676
+ warns.append(f"chroma/backup bloat: {bloat}")
677
+ free = sh(["bash", "-lc", "df -m . | tail -1 | awk '{print $4}'"]).stdout.strip()
678
+ if free.isdigit() and int(free) < 2000:
679
+ warns.append(f"low disk: {free} MB free")
680
+ stale = sh(["bash", "-lc",
681
+ "grep -rl 'Status | Live' 70-docs 2>/dev/null | xargs -r grep -l 'orchestrator.py' 2>/dev/null"]).stdout.strip()
682
+ if stale:
683
+ warns.append(f"stale present-state docs: {stale.splitlines()[:3]}")
684
+ return (Result("T5.4", Status.WARN, " | ".join(warns), "address the flagged tripwire")
685
+ if warns else Result("T5.4", Status.PASS, "no bloat/disk/stale-doc tripwire"))
686
+ ```
687
+
688
+ - [ ] **Step 2: Fixtures** — T5.1 stage a non-LFS file matching an LFS glob; T5.2 add a bogus `COPY nonexistent ./x` to a temp Dockerfile copy; T5.3 monkeypatch SPACE_API to a stub with mismatched sha (must WARN→treat as FAIL in selftest by asserting status≠PASS — fixture asserts not-PASS); T5.4 create an oversized dummy file. Full code; register. (Note: for WARN-only checks, the selftest asserts `status is not PASS` rather than strictly FAIL — add a `selftest_expect` field to `Check`/`register` defaulting to `FAIL`, set `WARN` for T2.3/T5.4/T5.3; update `core.selftest()` to honor it. Make this small core change here.)
689
+ - [ ] **Step 3: Verify** — `.venv/bin/python -m audit --deploy`.
690
+ - [ ] **Step 4: Selftest + Commit** — `git add audit/tier5_deploy.py audit/core.py audit/selftest_fixtures.py && git commit -m "feat(audit): Tier 5 deploy-safety (read-only) + WARN selftest expectation"`
691
+
692
+ ---
693
+
694
+ ## Task 7: Self-test wiring, git hooks, README, full run
695
+
696
+ **Files:** Modify `tests/test_audit_selftest.py`; Create `.githooks/pre-commit`, `.githooks/pre-push`, `audit/README.md`
697
+
698
+ - [ ] **Step 1: Extend `tests/test_audit_selftest.py`**
699
+
700
+ ```python
701
+ def test_every_check_is_self_verifying():
702
+ import audit.core as c
703
+ assert c.selftest() == 0, "some checks are not self-verifying (see stdout)"
704
+ ```
705
+
706
+ - [ ] **Step 2: Run** `.venv/bin/python -m pytest tests/test_audit_selftest.py -q` → PASS (now in the 215-gate; count becomes 216).
707
+ - [ ] **Step 3: Write `.githooks/pre-commit`**
708
+
709
+ ```bash
710
+ #!/usr/bin/env bash
711
+ exec "$(git rev-parse --show-toplevel)/tools/audit" --static
712
+ ```
713
+
714
+ `.githooks/pre-push`:
715
+
716
+ ```bash
717
+ #!/usr/bin/env bash
718
+ root="$(git rev-parse --show-toplevel)"
719
+ "$root/tools/audit" --build || exit 1
720
+ "$root/.venv/bin/python" -m audit --deploy || exit 1
721
+ ```
722
+
723
+ `chmod +x .githooks/*`. Document opt-in: `git config core.hooksPath .githooks`.
724
+
725
+ - [ ] **Step 4: Write `audit/README.md`** — usage table (tiers, when to run, exit codes, `--selftest`, hook opt-in).
726
+ - [ ] **Step 5: Full exhaustive run** — `tools/audit --all` (backend up). Capture the report; every check PASS or justified WARN/SKIP.
727
+ - [ ] **Step 6: Commit** — `git add tests/test_audit_selftest.py .githooks audit/README.md && git commit -m "feat(audit): selftest in pytest gate + git hooks + docs"`
728
+
729
+ ---
730
+
731
+ ## Self-Review
732
+
733
+ **Spec coverage:** T1.1–T1.5 → Task 2 · T2.1–T2.6 → Task 3 · T3.1–T3.3 → Task 4 · T4-smoke/T4-e2e → Task 5 · T5.1–T5.4 → Task 6 · self-verifying → Task 1 (`selftest`) + Task 7 (pytest wiring) · CLI/tiers/hooks → Task 1 + Task 7 · read-only-prod → Task 6 (GET/ls-remote only) · "test all functionalities" both sub-tiers → Task 5. No spec section unmapped.
734
+
735
+ **Placeholder scan:** Core/Tier1/Tier2/Tier3/Tier5 checks given as complete code. Tier-1 fixtures: `_staged_symlink` complete; the other four + Tier2/3/4/5 fixtures specified with exact pattern + explicit "write in full, do not abbreviate" instruction (they are mechanical 6–10-line repeats of the shown context-manager pattern — acceptable as a per-fixture instruction, not a logic placeholder). Tier-4 bodies are specified behaviourally with the exact endpoints/assertions + reuse of the verified `/tmp/pw-3-multiturn.js`; flagged as the one area to expand to full code during implementation.
736
+
737
+ **Type consistency:** `Result(check_id,status,evidence,remediation)`, `Status.{PASS,WARN,FAIL,SKIP}`, `register(id,tier,title)`, `CHECKS`, `TIER_SETS`, `git()/sh()`, `FIXTURES` dict — names identical across all tasks. Task 6 adds `selftest_expect` to `register`/`Check`/`selftest()` consistently in one place.
738
+
739
+ **Scope:** one cohesive framework, one repo, single plan. Good.
740
+
741
+ ---
742
+
743
+ ## Execution Handoff
744
+
745
+ (Provided after user reviews this plan.)