wizard-central-toolkit 0.2.0__py3-none-any.whl
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.
- wiz_central_toolkit/__init__.py +2 -0
- wiz_central_toolkit/bannerwiz/__main__.py +6 -0
- wiz_central_toolkit/bannerwiz/cli.py +79 -0
- wiz_central_toolkit/bannerwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wizard.py +999 -0
- wiz_central_toolkit/bannerwiz/lib/banner_wrapper.py +199 -0
- wiz_central_toolkit/bannerwiz/lib/cli.py +79 -0
- wiz_central_toolkit/codeguardwiz/__init__.py +1 -0
- wiz_central_toolkit/codeguardwiz/__main__.py +19 -0
- wiz_central_toolkit/codeguardwiz/cli.py +971 -0
- wiz_central_toolkit/codeguardwiz/codeguard_wizard.py +269 -0
- wiz_central_toolkit/codeguardwiz/rust_engine.py +348 -0
- wiz_central_toolkit/codeguardwiz/static_engine.py +307 -0
- wiz_central_toolkit/common/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__init__.py +1 -0
- wiz_central_toolkit/debugwiz/__main__.py +360 -0
- wiz_central_toolkit/debugwiz/debug_wizard.py +364 -0
- wiz_central_toolkit/diffwiz/__init__.py +0 -0
- wiz_central_toolkit/diffwiz/__main__.py +21 -0
- wiz_central_toolkit/diffwiz/cli.py +134 -0
- wiz_central_toolkit/diffwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/diffwiz/lib/cli.py +144 -0
- wiz_central_toolkit/diffwiz/lib/diff_wizard.py +89 -0
- wiz_central_toolkit/godfatherwiz/__init__.py +0 -0
- wiz_central_toolkit/godfatherwiz/__main__.py +18 -0
- wiz_central_toolkit/godfatherwiz/ai.py +136 -0
- wiz_central_toolkit/godfatherwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/godfatherwiz/lib/cli.py +925 -0
- wiz_central_toolkit/godfatherwiz/lib/godfather_wizard.py +1552 -0
- wiz_central_toolkit/jsonwiz/__init__.py +0 -0
- wiz_central_toolkit/jsonwiz/__main__.py +21 -0
- wiz_central_toolkit/jsonwiz/cli.py +4 -0
- wiz_central_toolkit/jsonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/jsonwiz/lib/cli.py +138 -0
- wiz_central_toolkit/jsonwiz/lib/json_wizard.py +133 -0
- wiz_central_toolkit/pythonwiz/__init__.py +0 -0
- wiz_central_toolkit/pythonwiz/__main__.py +21 -0
- wiz_central_toolkit/pythonwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/pythonwiz/lib/cli.py +247 -0
- wiz_central_toolkit/pythonwiz/lib/python_wizard.py +353 -0
- wiz_central_toolkit/readwiz/__init__.py +5 -0
- wiz_central_toolkit/readwiz/__main__.py +15 -0
- wiz_central_toolkit/readwiz/cli.py +46 -0
- wiz_central_toolkit/readwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/readwiz/lib/cli.py +47 -0
- wiz_central_toolkit/readwiz/lib/read_wizard.py +121 -0
- wiz_central_toolkit/rustwiz/__init__.py +5 -0
- wiz_central_toolkit/rustwiz/__main__.py +7 -0
- wiz_central_toolkit/rustwiz/cli.py +4 -0
- wiz_central_toolkit/rustwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/rustwiz/lib/cli.py +53 -0
- wiz_central_toolkit/rustwiz/lib/rust_wizard.py +128 -0
- wiz_central_toolkit/tmuxwiz/__init__.py +0 -0
- wiz_central_toolkit/tmuxwiz/__main__.py +15 -0
- wiz_central_toolkit/tmuxwiz/cli.py +7 -0
- wiz_central_toolkit/tmuxwiz/lib/__init__.py +1 -0
- wiz_central_toolkit/tmuxwiz/lib/cli.py +10 -0
- wiz_central_toolkit/tmuxwiz/lib/tmux_wizard.py +229 -0
- wiz_central_toolkit/todowiz/__init__.py +0 -0
- wiz_central_toolkit/todowiz/__main__.py +22 -0
- wiz_central_toolkit/todowiz/cli.py +4 -0
- wiz_central_toolkit/todowiz/lib/__init__.py +1 -0
- wiz_central_toolkit/todowiz/lib/cli.py +82 -0
- wiz_central_toolkit/todowiz/lib/todo_wizard.py +134 -0
- wiz_central_toolkit/tools_registry.py +62 -0
- wiz_central_toolkit/wiz/__init__.py +0 -0
- wiz_central_toolkit/wiz/__main__.py +141 -0
- wiz_central_toolkit/writewiz/__init__.py +1 -0
- wiz_central_toolkit/writewiz/__main__.py +9 -0
- wiz_central_toolkit/writewiz/cli.py +264 -0
- wiz_central_toolkit/writewiz/write_wizard.py +384 -0
- wizard_central_toolkit-0.2.0.dist-info/METADATA +203 -0
- wizard_central_toolkit-0.2.0.dist-info/RECORD +77 -0
- wizard_central_toolkit-0.2.0.dist-info/WHEEL +5 -0
- wizard_central_toolkit-0.2.0.dist-info/entry_points.txt +50 -0
- wizard_central_toolkit-0.2.0.dist-info/licenses/LICENSE +201 -0
- wizard_central_toolkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
"""codeguardwiz — CLI face. Plain verdicts, exit codes 0/1/2, no tracebacks.
|
|
2
|
+
|
|
3
|
+
Two engines (AVS's R1 ruling — both, and the user's choice):
|
|
4
|
+
the model seat when a daemon serves one, and the built-in static
|
|
5
|
+
rules (static_engine) which are always available. Default: model
|
|
6
|
+
when a seat is served, static when none is. Every audit-family
|
|
7
|
+
output opens with an engine line naming the engine that spoke.
|
|
8
|
+
"""
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from . import rust_engine as reng
|
|
18
|
+
from . import static_engine as se
|
|
19
|
+
from .codeguard_wizard import (
|
|
20
|
+
AUDIT_PREFS,
|
|
21
|
+
FIX_PREFS,
|
|
22
|
+
OLLAMA_BASE,
|
|
23
|
+
audit,
|
|
24
|
+
detect_language,
|
|
25
|
+
fix,
|
|
26
|
+
load_roster,
|
|
27
|
+
pick_seat,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ENGINE_HELP = ("engine: static (built-in rules) or model (seat); "
|
|
31
|
+
"default: model when a seat is served, else static")
|
|
32
|
+
NO_SEAT_HOWTO = (" seat one: run a local Ollama serving a coder model "
|
|
33
|
+
"(e.g. `ollama pull qwen2.5-coder:3b-instruct`), "
|
|
34
|
+
"then re-run")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _line_in_range(line, lr):
|
|
38
|
+
"""True if `line` falls inside `lr` (never a substring match).
|
|
39
|
+
|
|
40
|
+
`lr` arrives in more than one shape: the model engine's audit
|
|
41
|
+
contract emits a JSON pair [lo, hi]; text forms are '10-12',
|
|
42
|
+
'10:12' or a bare '5'. A list/tuple of two numbers is read as
|
|
43
|
+
(lo, hi). Anything malformed is a plain False.
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(lr, (list, tuple)):
|
|
46
|
+
if len(lr) != 2:
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
lo, hi = int(lr[0]), int(lr[1])
|
|
50
|
+
except (TypeError, ValueError):
|
|
51
|
+
return False
|
|
52
|
+
return lo <= line <= hi
|
|
53
|
+
import re
|
|
54
|
+
m = re.fullmatch(r"\s*(\d+)\s*(?:[-:]\s*(\d+))?\s*", str(lr))
|
|
55
|
+
if not m:
|
|
56
|
+
return False
|
|
57
|
+
lo = int(m.group(1))
|
|
58
|
+
hi = int(m.group(2) or lo)
|
|
59
|
+
return lo <= line <= hi
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _engine_line(engine):
|
|
63
|
+
return "engine: model seat" if engine == "model" \
|
|
64
|
+
else f"engine: {se.ENGINE_LABEL}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_no_seat(result):
|
|
68
|
+
return str(result.get("reason", "")).startswith("no audit seat")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _audit_seat():
|
|
72
|
+
"""The audit seat the model engine would pick right now, or None."""
|
|
73
|
+
return pick_seat(load_roster(), AUDIT_PREFS, "CODEGUARD_AUDIT_MODEL")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _no_seat_refusal():
|
|
77
|
+
"""--engine model demanded, no seat served: honest refusal, exit 1."""
|
|
78
|
+
print(_engine_line("model"))
|
|
79
|
+
print("[FAIL] no audit seat: daemon unreachable or no coder seat served")
|
|
80
|
+
print(NO_SEAT_HOWTO)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _stage_via_writewiz(target: Path, content: str):
|
|
85
|
+
"""Stage content through writewiz, the fleet sibling in this package.
|
|
86
|
+
|
|
87
|
+
Import lane first (writewiz ships inside this toolkit); fall back to
|
|
88
|
+
the resolved writewiz executable — the same two lanes debugwiz
|
|
89
|
+
uses. Staging only: the commit keystroke (and its double ask)
|
|
90
|
+
stays the Governor's, inside writewiz's wall.
|
|
91
|
+
Returns (ok, detail).
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
from wiz_central_toolkit.writewiz.write_wizard import propose as _propose
|
|
95
|
+
draft, _meta, t = _propose(str(target), content)
|
|
96
|
+
return True, f"{draft.name} -> {t} ({len(content)} chars quarantined)"
|
|
97
|
+
except ImportError:
|
|
98
|
+
pass
|
|
99
|
+
import shutil
|
|
100
|
+
import subprocess
|
|
101
|
+
exe = shutil.which("write_wizard") or shutil.which("writewiz")
|
|
102
|
+
if exe is None:
|
|
103
|
+
for home_name in ("write_wizard", "writewiz"):
|
|
104
|
+
home_exe = Path.home() / "bin" / home_name
|
|
105
|
+
if home_exe.exists():
|
|
106
|
+
exe = str(home_exe)
|
|
107
|
+
break
|
|
108
|
+
if exe is None:
|
|
109
|
+
return False, "write_wizard not found (package import or PATH)"
|
|
110
|
+
try:
|
|
111
|
+
r = subprocess.run([exe, "propose", str(target)], input=content,
|
|
112
|
+
capture_output=True, text=True, timeout=60)
|
|
113
|
+
except OSError as e:
|
|
114
|
+
return False, f"write_wizard unreachable: {e}"
|
|
115
|
+
if r.returncode != 0:
|
|
116
|
+
return False, (r.stdout + r.stderr).strip() or "write_wizard propose failed"
|
|
117
|
+
return True, (r.stdout.strip() or f"staged -> {target}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _ask_fix_consent(target):
|
|
121
|
+
"""The double ask (house law) before a model fix lands.
|
|
122
|
+
|
|
123
|
+
First the plain ask; then a typed confirmation naming the file.
|
|
124
|
+
Anything but the two exact answers — including EOF — is a no,
|
|
125
|
+
and the file stays untouched.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
first = input(f"⚠️ write the model's fix to {target}? [y/N] ")
|
|
129
|
+
except (KeyboardInterrupt, EOFError):
|
|
130
|
+
return False
|
|
131
|
+
if first.strip().lower() != "y":
|
|
132
|
+
return False
|
|
133
|
+
try:
|
|
134
|
+
typed = input(f"confirm by typing: yes, write {target}\n> ")
|
|
135
|
+
except (KeyboardInterrupt, EOFError):
|
|
136
|
+
return False
|
|
137
|
+
return typed.strip() == f"yes, write {target}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _land_via_writewiz(target: Path):
|
|
141
|
+
"""Land the staged draft for target through writewiz's commit.
|
|
142
|
+
|
|
143
|
+
The Governor has just given the double ask at this terminal, for
|
|
144
|
+
this file — that live consent is the answer commit's prompt
|
|
145
|
+
records. Inside the wall, writewiz still runs its own gate,
|
|
146
|
+
backup, journal and post-write verification. Returns (ok, detail).
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
from wiz_central_toolkit.writewiz import write_wizard as _ww
|
|
150
|
+
except ImportError:
|
|
151
|
+
return False, ("write_wizard not importable — the staged fix waits "
|
|
152
|
+
f"in quarantine; land it with: write_wizard commit {target}")
|
|
153
|
+
draft, meta = _ww._draft_path(Path(target))
|
|
154
|
+
if not draft.exists():
|
|
155
|
+
return False, "no staged draft found in write_wizard's quarantine"
|
|
156
|
+
ok, msg, _t = _ww.commit(draft, meta,
|
|
157
|
+
confirm_reader=lambda prompt="": "y")
|
|
158
|
+
return ok, msg
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------ static engine
|
|
162
|
+
|
|
163
|
+
def _static_out_of_scope(path, lang):
|
|
164
|
+
print(_engine_line("static"))
|
|
165
|
+
print(f"out of scope - static engine audits Python and Rust "
|
|
166
|
+
f"files only: {path} (detected: {lang})")
|
|
167
|
+
return 2
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _static_kind(path):
|
|
171
|
+
"""Which static-family engine owns this file: 'python' | 'rust'."""
|
|
172
|
+
p = Path(path)
|
|
173
|
+
if p.suffix == ".py":
|
|
174
|
+
return "python"
|
|
175
|
+
if p.suffix == ".rs" or p.name == "Cargo.toml":
|
|
176
|
+
return "rust"
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _scan_static_file(path, kind):
|
|
181
|
+
"""Findings for one file from its static-family engine."""
|
|
182
|
+
if kind == "python":
|
|
183
|
+
return se.scan_file(path)
|
|
184
|
+
return reng.scan_file(path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _collect_static_files(target):
|
|
188
|
+
"""All static-auditable files under a dir: .py + .rs + Cargo.toml."""
|
|
189
|
+
files = {str(f) for f in se.collect_files(target)}
|
|
190
|
+
files.update(str(f) for f in reng.collect_files(target))
|
|
191
|
+
return [Path(f) for f in sorted(files)]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ------------------------------------------------------------------
|
|
195
|
+
# BASELINES — the audit remembers, so watch can compare.
|
|
196
|
+
# A single-file audit (either engine) records a baseline in the
|
|
197
|
+
# wizard's state: per-issue fingerprints, the score, and the engine
|
|
198
|
+
# that produced them. `watch` re-audits the same file with the same
|
|
199
|
+
# engine and reports the true delta — new / resolved / unchanged.
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
STATE_DIR = Path.home() / ".codeguard-wizard"
|
|
203
|
+
BASELINES = STATE_DIR / "baselines.json"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _static_score(counts):
|
|
207
|
+
return max(0, 100 - 10 * counts.get("high", 0)
|
|
208
|
+
- 5 * counts.get("medium", 0) - 2 * counts.get("low", 0))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _load_baselines():
|
|
212
|
+
if BASELINES.exists():
|
|
213
|
+
try:
|
|
214
|
+
return json.loads(BASELINES.read_text())
|
|
215
|
+
except Exception:
|
|
216
|
+
return {}
|
|
217
|
+
return {}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _record_baseline(path, engine, score, issues):
|
|
221
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
data = _load_baselines()
|
|
223
|
+
data[str(Path(path).resolve())] = {
|
|
224
|
+
"engine": engine,
|
|
225
|
+
"score": score,
|
|
226
|
+
"at": int(time.time()),
|
|
227
|
+
"issues": sorted(issues),
|
|
228
|
+
}
|
|
229
|
+
BASELINES.write_text(json.dumps(data, indent=2) + "\n")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _load_baseline(path):
|
|
233
|
+
return _load_baselines().get(str(Path(path).resolve()))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _static_fingerprints(findings):
|
|
237
|
+
return [f"{f['rule']} @ line {f['line']}: {f['detail']}"
|
|
238
|
+
for f in findings]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _model_fingerprints(result):
|
|
242
|
+
return [f"[{i.get('severity', '?')}] {i.get('title', '?')} "
|
|
243
|
+
f"(line {i.get('line_range', '?')})"
|
|
244
|
+
for i in result.get("audit", {}).get("issues", [])]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _static_audit_file(path, as_json):
|
|
248
|
+
kind = _static_kind(path)
|
|
249
|
+
if kind is None:
|
|
250
|
+
if as_json:
|
|
251
|
+
print(json.dumps({"engine": "static", "filepath": path,
|
|
252
|
+
"status": "FAILED",
|
|
253
|
+
"reason": "static engine audits Python "
|
|
254
|
+
"and Rust files only"}, indent=2))
|
|
255
|
+
return 2
|
|
256
|
+
return _static_out_of_scope(path, detect_language(path))
|
|
257
|
+
try:
|
|
258
|
+
findings = _scan_static_file(path, kind)
|
|
259
|
+
except OSError:
|
|
260
|
+
print(_engine_line("static"))
|
|
261
|
+
print(f"[FAIL] cannot read file: {path}")
|
|
262
|
+
return 1
|
|
263
|
+
c = se.counts(findings)
|
|
264
|
+
_record_baseline(path, "static", _static_score(c),
|
|
265
|
+
_static_fingerprints(findings))
|
|
266
|
+
if as_json:
|
|
267
|
+
print(json.dumps({"engine": "static", "filepath": path,
|
|
268
|
+
"language": kind, "counts": c,
|
|
269
|
+
"findings": findings}, indent=2))
|
|
270
|
+
return 1 if c["high"] else 0
|
|
271
|
+
print(_engine_line("static"))
|
|
272
|
+
print(f"[OK] {path} ({kind}, static rules)")
|
|
273
|
+
print(f" findings: HIGH={c['high']} MEDIUM={c['medium']} "
|
|
274
|
+
f"LOW={c['low']}")
|
|
275
|
+
for f in findings:
|
|
276
|
+
print(f" [{f['severity'].upper()}] {f['rule']} {f['name']} "
|
|
277
|
+
f"(line {f['line']}) — {f['detail']}")
|
|
278
|
+
if c["high"]:
|
|
279
|
+
print(f" verdict: [FAIL] {c['high']} high-severity "
|
|
280
|
+
"finding(s) present")
|
|
281
|
+
return 1
|
|
282
|
+
print(" verdict: [OK] no high-severity findings")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _static_scan(files, as_json, verdict=False):
|
|
287
|
+
"""Static findings over a file set, scan-shaped. Shared by scan/audit."""
|
|
288
|
+
all_findings = []
|
|
289
|
+
failed = []
|
|
290
|
+
scanned = 0
|
|
291
|
+
for f in files:
|
|
292
|
+
kind = _static_kind(f)
|
|
293
|
+
if kind is None:
|
|
294
|
+
print(_engine_line("static"))
|
|
295
|
+
print(f"out of scope - static engine audits Python and "
|
|
296
|
+
f"Rust files only: {f}")
|
|
297
|
+
return 2
|
|
298
|
+
scanned += 1
|
|
299
|
+
try:
|
|
300
|
+
all_findings.extend(_scan_static_file(f, kind))
|
|
301
|
+
except OSError:
|
|
302
|
+
failed.append(str(f))
|
|
303
|
+
c = se.counts(all_findings)
|
|
304
|
+
if as_json:
|
|
305
|
+
print(json.dumps({"engine": "static", "files_scanned": scanned,
|
|
306
|
+
"failed": len(failed), "counts": c,
|
|
307
|
+
"findings": all_findings}, indent=2))
|
|
308
|
+
return 1 if c["high"] or failed else 0
|
|
309
|
+
print(_engine_line("static"))
|
|
310
|
+
for f in all_findings:
|
|
311
|
+
print(f"{f['path']}:{f['line']}: [{f['severity'].upper()}] "
|
|
312
|
+
f"{f['rule']} {f['name']} — {f['detail']}")
|
|
313
|
+
for name in failed:
|
|
314
|
+
print(f"[FAIL] {name}: cannot read file")
|
|
315
|
+
print(f"[OK] scanned {scanned} files")
|
|
316
|
+
print(f" HIGH={c['high']} MEDIUM={c['medium']} LOW={c['low']}")
|
|
317
|
+
if verdict:
|
|
318
|
+
if c["high"]:
|
|
319
|
+
print(f" verdict: [FAIL] {c['high']} high-severity "
|
|
320
|
+
"finding(s) present")
|
|
321
|
+
else:
|
|
322
|
+
print(" verdict: [OK] no high-severity findings")
|
|
323
|
+
return 1 if c["high"] or failed else 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _resolve_engine(files, pref):
|
|
327
|
+
"""Decide the sweep engine once, from the first file's audit.
|
|
328
|
+
|
|
329
|
+
Returns (engine, first_result). 'model' keeps first_result (the
|
|
330
|
+
audit already spent on files[0]); a non-seat failure also stays
|
|
331
|
+
model — a served-but-broken seat is reported, never masked by a
|
|
332
|
+
silent switch to static.
|
|
333
|
+
"""
|
|
334
|
+
if pref == "static":
|
|
335
|
+
return "static", None
|
|
336
|
+
if pref == "model":
|
|
337
|
+
return "model", None
|
|
338
|
+
if not files:
|
|
339
|
+
return ("model" if _audit_seat() else "static"), None
|
|
340
|
+
first = audit(str(files[0]))
|
|
341
|
+
if first["status"] == "OK" or not _is_no_seat(first):
|
|
342
|
+
return "model", first
|
|
343
|
+
return "static", None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _static_severity(target, threshold):
|
|
347
|
+
t = Path(target)
|
|
348
|
+
if t.is_file():
|
|
349
|
+
if _static_kind(t) is None:
|
|
350
|
+
return _static_out_of_scope(target, detect_language(target))
|
|
351
|
+
files = [t]
|
|
352
|
+
else:
|
|
353
|
+
files = _collect_static_files(t)
|
|
354
|
+
findings = []
|
|
355
|
+
for f in files:
|
|
356
|
+
try:
|
|
357
|
+
findings.extend(_scan_static_file(f, _static_kind(f)))
|
|
358
|
+
except OSError:
|
|
359
|
+
print(_engine_line("static"))
|
|
360
|
+
print(f"[FAIL] cannot read file: {f}")
|
|
361
|
+
return 1
|
|
362
|
+
levels = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
363
|
+
shown = levels if threshold is None \
|
|
364
|
+
else levels[: levels.index(threshold) + 1]
|
|
365
|
+
print(_engine_line("static"))
|
|
366
|
+
print(f"[OK] {target} — findings by severity (static rules)")
|
|
367
|
+
total = 0
|
|
368
|
+
for lv in shown:
|
|
369
|
+
group = [f for f in findings if f["severity"].upper() == lv]
|
|
370
|
+
print(f" {lv} ({len(group)}):")
|
|
371
|
+
for f in group:
|
|
372
|
+
print(f" {f['path']}:{f['line']} {f['rule']} {f['name']} "
|
|
373
|
+
f"— {f['detail']}")
|
|
374
|
+
total += len(group)
|
|
375
|
+
return 1 if total else 0
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _static_explain(path, line):
|
|
379
|
+
kind = _static_kind(path)
|
|
380
|
+
if kind is None:
|
|
381
|
+
return _static_out_of_scope(path, detect_language(path))
|
|
382
|
+
try:
|
|
383
|
+
findings = _scan_static_file(path, kind)
|
|
384
|
+
except OSError:
|
|
385
|
+
print(_engine_line("static"))
|
|
386
|
+
print(f"[FAIL] cannot read file: {path}")
|
|
387
|
+
return 1
|
|
388
|
+
print(_engine_line("static"))
|
|
389
|
+
hits = [f for f in findings if f["line"] == line]
|
|
390
|
+
if not hits:
|
|
391
|
+
print(f"[FAIL] no issue found at line {line}")
|
|
392
|
+
return 1
|
|
393
|
+
f = hits[0]
|
|
394
|
+
rule = (se.RULE_BY_ID if kind == "python"
|
|
395
|
+
else reng.RULE_BY_ID)[f["rule"]]
|
|
396
|
+
print(f"[OK] Rule at line {line}:")
|
|
397
|
+
print(f" Severity: {f['severity'].upper()}")
|
|
398
|
+
print(f" Title: {f['rule']} {f['name']}")
|
|
399
|
+
print(f" Message: {rule.explanation}")
|
|
400
|
+
print(f" Detail: {f['detail']}")
|
|
401
|
+
return 0
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _static_fix(path):
|
|
405
|
+
kind = _static_kind(path)
|
|
406
|
+
if kind is None:
|
|
407
|
+
return _static_out_of_scope(path, detect_language(path))
|
|
408
|
+
if kind == "rust":
|
|
409
|
+
try:
|
|
410
|
+
findings = reng.scan_file(path)
|
|
411
|
+
except OSError:
|
|
412
|
+
print(_engine_line("static"))
|
|
413
|
+
print(f"[FAIL] cannot read file: {path}")
|
|
414
|
+
return 1
|
|
415
|
+
print(_engine_line("static"))
|
|
416
|
+
if not findings:
|
|
417
|
+
print(f"[OK] no findings in {path} — nothing to stage")
|
|
418
|
+
return 0
|
|
419
|
+
print("[OK] no deterministic rewrite available — nothing staged")
|
|
420
|
+
print(" Rust findings have no safe mechanical fix: the "
|
|
421
|
+
"static engine never rewrites Rust, so there is "
|
|
422
|
+
"nothing to stage. A rewrite proposal is the model "
|
|
423
|
+
"engine's job (seat one, then re-run so it can "
|
|
424
|
+
"propose).")
|
|
425
|
+
for f in findings:
|
|
426
|
+
print(f" line {f['line']}: {f['rule']} {f['name']} "
|
|
427
|
+
"— no deterministic rewrite")
|
|
428
|
+
return 0
|
|
429
|
+
lang = detect_language(path)
|
|
430
|
+
if lang != "python":
|
|
431
|
+
return _static_out_of_scope(path, lang)
|
|
432
|
+
try:
|
|
433
|
+
source = Path(path).read_text(encoding="utf-8", errors="ignore")
|
|
434
|
+
except OSError:
|
|
435
|
+
print(_engine_line("static"))
|
|
436
|
+
print(f"[FAIL] cannot read file: {path}")
|
|
437
|
+
return 1
|
|
438
|
+
findings = se.scan_source(source, path)
|
|
439
|
+
print(_engine_line("static"))
|
|
440
|
+
if not findings:
|
|
441
|
+
print(f"[OK] no findings in {path} — nothing to stage")
|
|
442
|
+
return 0
|
|
443
|
+
fixed, applied = se.apply_fixes(source, findings)
|
|
444
|
+
rest = [f for f in findings if f["rule"] not in se.FIXABLE_RULES]
|
|
445
|
+
if applied:
|
|
446
|
+
ok, detail = _stage_via_writewiz(Path(path), fixed)
|
|
447
|
+
if not ok:
|
|
448
|
+
print(f"[FAIL] fix could not stage via write_wizard: {detail}")
|
|
449
|
+
return 1
|
|
450
|
+
print(f"[STAGED] fix for {path} — {detail}")
|
|
451
|
+
print(f"to write it: write_wizard commit {path} "
|
|
452
|
+
"# your keystroke, Governor")
|
|
453
|
+
else:
|
|
454
|
+
print("[OK] no deterministic rewrite available — nothing staged")
|
|
455
|
+
if rest:
|
|
456
|
+
print(" needs a person or the model engine:")
|
|
457
|
+
for f in rest:
|
|
458
|
+
print(f" line {f['line']}: {f['rule']} {f['name']} "
|
|
459
|
+
"— no deterministic rewrite")
|
|
460
|
+
return 0
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _static_staged_only_refusal():
|
|
464
|
+
print(_engine_line("static"))
|
|
465
|
+
print("[FAIL] static fixes are staged through write_wizard — "
|
|
466
|
+
"codeguard_wizard never writes them itself")
|
|
467
|
+
print(" run without --write/--out to stage the fix, then "
|
|
468
|
+
"commit it inside write_wizard's wall")
|
|
469
|
+
return 1
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ------------------------------------------------------------ CLI proper
|
|
473
|
+
|
|
474
|
+
class _NamedParser(argparse.ArgumentParser):
|
|
475
|
+
"""Every help surface of this wizard leads with its name."""
|
|
476
|
+
|
|
477
|
+
def format_help(self):
|
|
478
|
+
return ("Code Guard Wizard — code audit wizard\n"
|
|
479
|
+
"alias: codeguardwiz\n"
|
|
480
|
+
+ super().format_help())
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def main():
|
|
484
|
+
p = _NamedParser(prog="codeguard_wizard")
|
|
485
|
+
sub = p.add_subparsers(dest="cmd", parser_class=_NamedParser)
|
|
486
|
+
a = sub.add_parser("audit", help="audit a file or directory, print plain verdict")
|
|
487
|
+
a.add_argument("file")
|
|
488
|
+
a.add_argument("--json", action="store_true", help="machine-readable report")
|
|
489
|
+
a.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
490
|
+
f = sub.add_parser("fix", help="generate repaired code for a file")
|
|
491
|
+
f.add_argument("file")
|
|
492
|
+
f.add_argument("--write", action="store_true",
|
|
493
|
+
help="CONSENT: stage the fix via write_wizard, ask twice "
|
|
494
|
+
"at the terminal, then commit through the wall")
|
|
495
|
+
f.add_argument("--out", help="write to this path instead (same wall)")
|
|
496
|
+
f.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
497
|
+
l = sub.add_parser("lang", help="detect language of a file")
|
|
498
|
+
l.add_argument("file")
|
|
499
|
+
r = sub.add_parser("roster", help="show the engines: static rules, and live seats when served")
|
|
500
|
+
r.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
501
|
+
|
|
502
|
+
# --- CODE REPAIR LANE EXPANSION ---
|
|
503
|
+
sc = sub.add_parser("scan", help="recursive audit sweep on a file or directory")
|
|
504
|
+
sc.add_argument("dir", help="file or directory to scan")
|
|
505
|
+
sc.add_argument("--json", action="store_true", help="machine-readable report")
|
|
506
|
+
sc.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
507
|
+
|
|
508
|
+
wh = sub.add_parser("watch", help="diff-since-last-audit: re-run audit, report changes")
|
|
509
|
+
wh.add_argument("file", help="file to watch")
|
|
510
|
+
|
|
511
|
+
sv = sub.add_parser("severity", help="findings grouped by severity")
|
|
512
|
+
sv.add_argument("file", help="file or directory to audit")
|
|
513
|
+
sv.add_argument("--threshold", default=None, choices=["CRITICAL", "HIGH", "MEDIUM"],
|
|
514
|
+
help="show this severity and above (default: HIGH for the "
|
|
515
|
+
"model engine, all groups for static)")
|
|
516
|
+
sv.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
517
|
+
|
|
518
|
+
ex = sub.add_parser("explain", help="explain a static rule, or the rule that fired at a line")
|
|
519
|
+
ex.add_argument("file", help="file with finding, or a static rule id/name")
|
|
520
|
+
ex.add_argument("line", type=int, nargs="?", default=None,
|
|
521
|
+
help="line number (omit to explain a rule by id/name)")
|
|
522
|
+
ex.add_argument("--engine", choices=["static", "model"], help=ENGINE_HELP)
|
|
523
|
+
|
|
524
|
+
sf = sub.add_parser("scaffold", help="generate unit-test skeleton for file")
|
|
525
|
+
sf.add_argument("file", help="source file to test")
|
|
526
|
+
sf.add_argument("--framework", default="pytest", choices=["pytest", "unittest", "nose"])
|
|
527
|
+
|
|
528
|
+
if sys.argv[1:2] == ["help"]:
|
|
529
|
+
p.print_help()
|
|
530
|
+
return 0
|
|
531
|
+
args = p.parse_args()
|
|
532
|
+
if not args.cmd:
|
|
533
|
+
p.print_help()
|
|
534
|
+
return 0
|
|
535
|
+
|
|
536
|
+
if args.cmd == "roster":
|
|
537
|
+
roster = load_roster()
|
|
538
|
+
audit_seat = pick_seat(roster, AUDIT_PREFS, "CODEGUARD_AUDIT_MODEL") \
|
|
539
|
+
if roster else None
|
|
540
|
+
fix_seat = pick_seat(roster, FIX_PREFS, "CODEGUARD_FIX_MODEL") \
|
|
541
|
+
if roster else None
|
|
542
|
+
resolved = "static" if args.engine == "static" or not audit_seat \
|
|
543
|
+
else "model"
|
|
544
|
+
print(_engine_line(resolved))
|
|
545
|
+
print("engines:")
|
|
546
|
+
print("[OK] static (built-in rules) — always available, "
|
|
547
|
+
"no daemon needed")
|
|
548
|
+
print(" rules:")
|
|
549
|
+
for rule in se.RULES:
|
|
550
|
+
print(f" {rule.id} {rule.name} ({rule.severity.upper()})")
|
|
551
|
+
print(" rust rules:")
|
|
552
|
+
for rule in reng.RULES:
|
|
553
|
+
print(f" {rule.id} {rule.name} ({rule.severity.upper()})")
|
|
554
|
+
if roster:
|
|
555
|
+
print(f"[OK] model seat — daemon serves {len(roster)} "
|
|
556
|
+
"model(s):")
|
|
557
|
+
for name in roster:
|
|
558
|
+
marks = []
|
|
559
|
+
if name == audit_seat:
|
|
560
|
+
marks.append("audit")
|
|
561
|
+
if name == fix_seat:
|
|
562
|
+
marks.append("fix")
|
|
563
|
+
print(f"[OK] {name}" + (f" <- {'/'.join(marks)}" if marks else ""))
|
|
564
|
+
else:
|
|
565
|
+
print("[FAIL] model seat — none served: daemon unreachable "
|
|
566
|
+
"or no coder seat served")
|
|
567
|
+
print(NO_SEAT_HOWTO)
|
|
568
|
+
return 0
|
|
569
|
+
|
|
570
|
+
if args.cmd == "lang":
|
|
571
|
+
path = args.file
|
|
572
|
+
if not Path(path).is_file():
|
|
573
|
+
print(f"[FAIL] not a file: {path}")
|
|
574
|
+
return 1
|
|
575
|
+
lang = detect_language(path)
|
|
576
|
+
if lang == "unknown":
|
|
577
|
+
print("out of scope - language not detectable")
|
|
578
|
+
return 2
|
|
579
|
+
print(f"[OK] {path}: {lang}")
|
|
580
|
+
return 0
|
|
581
|
+
|
|
582
|
+
if args.cmd == "audit":
|
|
583
|
+
target = Path(args.file)
|
|
584
|
+
if target.is_dir():
|
|
585
|
+
files = se.collect_files(target)
|
|
586
|
+
if args.engine == "model" and _audit_seat() is None:
|
|
587
|
+
return _no_seat_refusal()
|
|
588
|
+
engine, first = _resolve_engine(files, args.engine)
|
|
589
|
+
if engine == "static":
|
|
590
|
+
return _static_scan(_collect_static_files(target),
|
|
591
|
+
args.json, verdict=True)
|
|
592
|
+
print(_engine_line("model"))
|
|
593
|
+
totals = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
|
|
594
|
+
failed = 0
|
|
595
|
+
for i, fp in enumerate(files):
|
|
596
|
+
result = first if (i == 0 and first is not None) \
|
|
597
|
+
else audit(str(fp))
|
|
598
|
+
if result["status"] != "OK":
|
|
599
|
+
failed += 1
|
|
600
|
+
print(f"[FAIL] {fp}: "
|
|
601
|
+
f"{result.get('reason', 'audit failed')}")
|
|
602
|
+
continue
|
|
603
|
+
counts = result["issues_count"]
|
|
604
|
+
for k in totals:
|
|
605
|
+
totals[k] += counts.get(k, 0)
|
|
606
|
+
print(f"[OK] {result['filepath']} "
|
|
607
|
+
f"({result['language']}, model {result['model']}, "
|
|
608
|
+
f"score {result['audit'].get('score', '?')})")
|
|
609
|
+
for issue in result["audit"].get("issues", []):
|
|
610
|
+
print(f" [{issue.get('severity', '?')}] "
|
|
611
|
+
f"{issue.get('title', '?')} "
|
|
612
|
+
f"(line {issue.get('line_range', '?')})")
|
|
613
|
+
print(f" findings: CRITICAL={totals['CRITICAL']} "
|
|
614
|
+
f"HIGH={totals['HIGH']} MEDIUM={totals['MEDIUM']} "
|
|
615
|
+
f"LOW={totals['LOW']}")
|
|
616
|
+
bad = totals["CRITICAL"] + totals["HIGH"]
|
|
617
|
+
if bad or failed:
|
|
618
|
+
print(f" verdict: [FAIL] {bad} critical/high "
|
|
619
|
+
"finding(s) present"
|
|
620
|
+
+ (f", {failed} file(s) unaudited" if failed else ""))
|
|
621
|
+
return 1
|
|
622
|
+
print(" verdict: [OK] no critical/high findings")
|
|
623
|
+
return 0
|
|
624
|
+
if not target.is_file():
|
|
625
|
+
print(f"[FAIL] not a file: {args.file}")
|
|
626
|
+
return 1
|
|
627
|
+
path = args.file
|
|
628
|
+
if args.engine == "static":
|
|
629
|
+
return _static_audit_file(path, args.json)
|
|
630
|
+
result = audit(path)
|
|
631
|
+
if result["status"] != "OK":
|
|
632
|
+
if _is_no_seat(result):
|
|
633
|
+
if args.engine == "model":
|
|
634
|
+
return _no_seat_refusal()
|
|
635
|
+
return _static_audit_file(path, args.json)
|
|
636
|
+
if args.json:
|
|
637
|
+
print(json.dumps(result, indent=2))
|
|
638
|
+
print(f"[FAIL] {result.get('reason', 'audit failed')}")
|
|
639
|
+
return 1
|
|
640
|
+
if args.json:
|
|
641
|
+
print(json.dumps(result, indent=2))
|
|
642
|
+
else:
|
|
643
|
+
print(_engine_line("model"))
|
|
644
|
+
counts = result["issues_count"]
|
|
645
|
+
score = result["audit"].get("score", "?")
|
|
646
|
+
print(f"[OK] {result['filepath']} ({result['language']}, "
|
|
647
|
+
f"model {result['model']}, score {score})")
|
|
648
|
+
print(f" issues: CRITICAL={counts['CRITICAL']} HIGH={counts['HIGH']} "
|
|
649
|
+
f"MEDIUM={counts['MEDIUM']} LOW={counts['LOW']}")
|
|
650
|
+
for issue in result["audit"].get("issues", []):
|
|
651
|
+
print(f" [{issue.get('severity', '?')}] {issue.get('title', '?')} "
|
|
652
|
+
f"(line {issue.get('line_range', '?')})")
|
|
653
|
+
if isinstance(score, (int, float)):
|
|
654
|
+
_record_baseline(path, "model", score,
|
|
655
|
+
_model_fingerprints(result))
|
|
656
|
+
return 0
|
|
657
|
+
|
|
658
|
+
if args.cmd == "scan":
|
|
659
|
+
t = Path(args.dir)
|
|
660
|
+
if t.is_file():
|
|
661
|
+
files = [t]
|
|
662
|
+
elif t.is_dir():
|
|
663
|
+
files = se.collect_files(t)
|
|
664
|
+
else:
|
|
665
|
+
print(f"[FAIL] not a directory: {args.dir}")
|
|
666
|
+
return 1
|
|
667
|
+
if args.engine == "model" and _audit_seat() is None:
|
|
668
|
+
return _no_seat_refusal()
|
|
669
|
+
engine, first = _resolve_engine(files, args.engine)
|
|
670
|
+
if engine == "static":
|
|
671
|
+
sweep = files if t.is_file() else _collect_static_files(t)
|
|
672
|
+
return _static_scan(sweep, args.json)
|
|
673
|
+
results = []
|
|
674
|
+
total = 0
|
|
675
|
+
critical = 0
|
|
676
|
+
high = 0
|
|
677
|
+
failed = 0
|
|
678
|
+
for i, fp in enumerate(files):
|
|
679
|
+
r = first if (i == 0 and first is not None) else audit(str(fp))
|
|
680
|
+
if r["status"] == "OK":
|
|
681
|
+
counts = r.get("issues_count", {})
|
|
682
|
+
crit = counts.get("CRITICAL", 0)
|
|
683
|
+
h = counts.get("HIGH", 0)
|
|
684
|
+
if crit or h:
|
|
685
|
+
results.append({
|
|
686
|
+
"file": str(fp), "crit": crit, "high": h
|
|
687
|
+
})
|
|
688
|
+
critical += crit
|
|
689
|
+
high += h
|
|
690
|
+
else:
|
|
691
|
+
failed += 1
|
|
692
|
+
total += 1
|
|
693
|
+
if args.json:
|
|
694
|
+
print(json.dumps({"files_scanned": total, "failed": failed, "findings": results}, indent=2))
|
|
695
|
+
else:
|
|
696
|
+
print(_engine_line("model"))
|
|
697
|
+
print(f"[OK] scanned {total} files")
|
|
698
|
+
if failed:
|
|
699
|
+
print(f"[FAIL] {failed} file(s) could not be audited (daemon down or bad reply)")
|
|
700
|
+
print(f" CRITICAL={critical} HIGH={high}")
|
|
701
|
+
if results:
|
|
702
|
+
print(" findings:")
|
|
703
|
+
for r in results[:10]:
|
|
704
|
+
print(f" {r['file']}: C={r['crit']} H={r['high']}")
|
|
705
|
+
return 1 if critical or high or failed else 0
|
|
706
|
+
|
|
707
|
+
if args.cmd == "watch":
|
|
708
|
+
path = args.file
|
|
709
|
+
if not Path(path).is_file():
|
|
710
|
+
print(f"[FAIL] not a file: {path}")
|
|
711
|
+
return 1
|
|
712
|
+
base = _load_baseline(path)
|
|
713
|
+
if base is None:
|
|
714
|
+
print(f"[FAIL] no baseline recorded for {path}")
|
|
715
|
+
print(" run `codeguard_wizard audit <file>` first — "
|
|
716
|
+
"an audit records the baseline watch compares against")
|
|
717
|
+
return 1
|
|
718
|
+
engine = base.get("engine", "static")
|
|
719
|
+
if engine == "static":
|
|
720
|
+
kind = _static_kind(path)
|
|
721
|
+
if kind is None:
|
|
722
|
+
print(_engine_line("static"))
|
|
723
|
+
print(f"[FAIL] static engine audits Python and Rust "
|
|
724
|
+
f"files only: {path}")
|
|
725
|
+
return 1
|
|
726
|
+
try:
|
|
727
|
+
findings = _scan_static_file(path, kind)
|
|
728
|
+
except OSError:
|
|
729
|
+
print(_engine_line("static"))
|
|
730
|
+
print(f"[FAIL] cannot read file: {path}")
|
|
731
|
+
return 1
|
|
732
|
+
now_score = _static_score(se.counts(findings))
|
|
733
|
+
now_issues = _static_fingerprints(findings)
|
|
734
|
+
else:
|
|
735
|
+
result = audit(path)
|
|
736
|
+
if result["status"] != "OK":
|
|
737
|
+
if _is_no_seat(result):
|
|
738
|
+
return _no_seat_refusal()
|
|
739
|
+
print(f"[FAIL] {result.get('reason', 'audit failed')}")
|
|
740
|
+
return 1
|
|
741
|
+
now_score = result["audit"].get("score", "?")
|
|
742
|
+
now_issues = _model_fingerprints(result)
|
|
743
|
+
before = Counter(base.get("issues", []))
|
|
744
|
+
after = Counter(now_issues)
|
|
745
|
+
new = after - before
|
|
746
|
+
resolved = before - after
|
|
747
|
+
unchanged = sum((before & after).values())
|
|
748
|
+
print(_engine_line(engine))
|
|
749
|
+
print(f"[OK] watch {path} — baseline score {base.get('score', '?')} "
|
|
750
|
+
f"-> now {now_score}")
|
|
751
|
+
print(f" new: {sum(new.values())} resolved: "
|
|
752
|
+
f"{sum(resolved.values())} unchanged: {unchanged}")
|
|
753
|
+
for fp, n in sorted(new.items()):
|
|
754
|
+
print(f" NEW {fp}" + (f" (x{n})" if n > 1 else ""))
|
|
755
|
+
for fp, n in sorted(resolved.items()):
|
|
756
|
+
print(f" RESOLVED {fp}" + (f" (x{n})" if n > 1 else ""))
|
|
757
|
+
return 0
|
|
758
|
+
|
|
759
|
+
if args.cmd == "severity":
|
|
760
|
+
target = Path(args.file)
|
|
761
|
+
if not target.exists():
|
|
762
|
+
print(f"[FAIL] not a file: {args.file}")
|
|
763
|
+
return 1
|
|
764
|
+
if args.engine == "static":
|
|
765
|
+
return _static_severity(args.file, args.threshold)
|
|
766
|
+
if target.is_dir():
|
|
767
|
+
files = se.collect_files(target)
|
|
768
|
+
if args.engine == "model" and _audit_seat() is None:
|
|
769
|
+
return _no_seat_refusal()
|
|
770
|
+
engine, first = _resolve_engine(files, args.engine)
|
|
771
|
+
if engine == "static":
|
|
772
|
+
return _static_severity(args.file, args.threshold)
|
|
773
|
+
threshold = args.threshold or "HIGH"
|
|
774
|
+
levels = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
775
|
+
shown = levels[: levels.index(threshold) + 1]
|
|
776
|
+
print(_engine_line("model"))
|
|
777
|
+
groups = {lv: [] for lv in shown}
|
|
778
|
+
for i, fp in enumerate(files):
|
|
779
|
+
result = first if (i == 0 and first is not None) \
|
|
780
|
+
else audit(str(fp))
|
|
781
|
+
if result["status"] != "OK":
|
|
782
|
+
print(f"[FAIL] {fp}: "
|
|
783
|
+
f"{result.get('reason', 'audit failed')}")
|
|
784
|
+
return 1
|
|
785
|
+
for iss in result.get("audit", {}).get("issues", []):
|
|
786
|
+
sev = iss.get("severity", "")
|
|
787
|
+
if sev in groups:
|
|
788
|
+
groups[sev].append((str(fp), iss))
|
|
789
|
+
total_above = sum(len(v) for v in groups.values())
|
|
790
|
+
print(f"[OK] {threshold}+ severity: {total_above} findings")
|
|
791
|
+
for lv in shown:
|
|
792
|
+
for fp, iss in groups[lv]:
|
|
793
|
+
print(f" [{lv}] {iss.get('title')} "
|
|
794
|
+
f"({fp} line {iss.get('line_range')})")
|
|
795
|
+
return 1 if total_above > 0 else 0
|
|
796
|
+
path = args.file
|
|
797
|
+
result = audit(path)
|
|
798
|
+
if result["status"] != "OK":
|
|
799
|
+
if _is_no_seat(result):
|
|
800
|
+
if args.engine == "model":
|
|
801
|
+
return _no_seat_refusal()
|
|
802
|
+
return _static_severity(path, args.threshold)
|
|
803
|
+
print(f"[FAIL] {result.get('reason', 'audit failed')}")
|
|
804
|
+
return 1
|
|
805
|
+
print(_engine_line("model"))
|
|
806
|
+
threshold = args.threshold or "HIGH"
|
|
807
|
+
counts = result.get("issues_count", {})
|
|
808
|
+
levels = ["CRITICAL", "HIGH", "MEDIUM"]
|
|
809
|
+
levels = levels[: levels.index(threshold) + 1]
|
|
810
|
+
total_above = sum(counts.get(lv, 0) for lv in levels)
|
|
811
|
+
print(f"[OK] {threshold}+ severity: {total_above} findings")
|
|
812
|
+
if total_above > 0:
|
|
813
|
+
for iss in result.get("audit", {}).get("issues", []):
|
|
814
|
+
sev = iss.get("severity", "")
|
|
815
|
+
if sev in levels:
|
|
816
|
+
print(f" [{sev}] {iss.get('title')} (line {iss.get('line_range')})")
|
|
817
|
+
return 1 if total_above > 0 else 0
|
|
818
|
+
|
|
819
|
+
if args.cmd == "explain":
|
|
820
|
+
if args.line is None:
|
|
821
|
+
rule = se.find_rule(args.file) or reng.find_rule(args.file)
|
|
822
|
+
print(_engine_line("static"))
|
|
823
|
+
if rule is None:
|
|
824
|
+
print(f"[FAIL] no rule '{args.file}' in the static rule "
|
|
825
|
+
"roster (see: codeguard_wizard roster)")
|
|
826
|
+
return 1
|
|
827
|
+
print(f"[OK] {rule.id} {rule.name} ({rule.severity.upper()})")
|
|
828
|
+
print(f" {rule.explanation}")
|
|
829
|
+
return 0
|
|
830
|
+
line = int(args.line)
|
|
831
|
+
path = args.file
|
|
832
|
+
if not Path(path).is_file():
|
|
833
|
+
print(f"[FAIL] not a file: {path}")
|
|
834
|
+
return 1
|
|
835
|
+
if args.engine == "static":
|
|
836
|
+
return _static_explain(path, line)
|
|
837
|
+
result = audit(path)
|
|
838
|
+
if result["status"] != "OK" and _is_no_seat(result):
|
|
839
|
+
if args.engine == "model":
|
|
840
|
+
return _no_seat_refusal()
|
|
841
|
+
return _static_explain(path, line)
|
|
842
|
+
print(_engine_line("model"))
|
|
843
|
+
for iss in result.get("audit", {}).get("issues", []):
|
|
844
|
+
lr = iss.get("line_range", "")
|
|
845
|
+
if _line_in_range(line, lr):
|
|
846
|
+
print(f"[OK] Rule at line {line}:")
|
|
847
|
+
print(f" Severity: {iss.get('severity', '?')}")
|
|
848
|
+
print(f" Title: {iss.get('title', '?')}")
|
|
849
|
+
print(f" Message: {iss.get('description', '(no description)')}")
|
|
850
|
+
return 0
|
|
851
|
+
print(f"[FAIL] no issue found at line {line}")
|
|
852
|
+
return 1
|
|
853
|
+
|
|
854
|
+
if args.cmd == "scaffold":
|
|
855
|
+
path = args.file
|
|
856
|
+
if not Path(path).is_file():
|
|
857
|
+
print(f"[FAIL] not a file: {path}")
|
|
858
|
+
return 1
|
|
859
|
+
# Real skeleton: read actual top-level functions from the source
|
|
860
|
+
import ast as _ast
|
|
861
|
+
try:
|
|
862
|
+
tree = _ast.parse(Path(path).read_text())
|
|
863
|
+
except SyntaxError as e:
|
|
864
|
+
print(f"[FAIL] cannot parse {path}: {e}")
|
|
865
|
+
return 1
|
|
866
|
+
funcs = [n.name for n in tree.body if isinstance(n, _ast.FunctionDef)]
|
|
867
|
+
classes = [n.name for n in tree.body if isinstance(n, _ast.ClassDef)]
|
|
868
|
+
stem = Path(path).stem
|
|
869
|
+
test_name = f"test_{stem}.py"
|
|
870
|
+
lines = [
|
|
871
|
+
f'"""Generated by codeguard_wizard scaffold — test skeleton for {path}."""',
|
|
872
|
+
"import pytest",
|
|
873
|
+
"",
|
|
874
|
+
]
|
|
875
|
+
for fn in funcs:
|
|
876
|
+
if fn.startswith("_"):
|
|
877
|
+
continue
|
|
878
|
+
lines += [f"def test_{fn}_exists():", f' """SMOKE: {fn} exists."""',
|
|
879
|
+
" assert True", ""]
|
|
880
|
+
for cls in classes:
|
|
881
|
+
lines += [f"def test_{cls}_importable():", f' """SMOKE: {cls} importable."""',
|
|
882
|
+
" import importlib.util as _iu",
|
|
883
|
+
f" spec = _iu.spec_from_file_location('{stem}', r'{path}')",
|
|
884
|
+
" assert spec is not None or True", ""]
|
|
885
|
+
if not funcs and not classes:
|
|
886
|
+
lines += [f"# (no public top-level defs found in {path})", ""]
|
|
887
|
+
print(f"[OK] test skeleton generated (preview only, disk untouched): {test_name}")
|
|
888
|
+
print(f" functions: {len([f for f in funcs if not f.startswith('_')])}, "
|
|
889
|
+
f"classes: {len(classes)}")
|
|
890
|
+
print("--- skeleton follows ---")
|
|
891
|
+
print("\n".join(lines))
|
|
892
|
+
print("--- end ---")
|
|
893
|
+
print("consent lane: pipe this output to a file yourself if you want it on disk")
|
|
894
|
+
return 0
|
|
895
|
+
|
|
896
|
+
if args.cmd == "fix":
|
|
897
|
+
path = args.file
|
|
898
|
+
if not Path(path).is_file():
|
|
899
|
+
print(f"[FAIL] not a file: {path}")
|
|
900
|
+
return 1
|
|
901
|
+
if args.engine == "static":
|
|
902
|
+
if args.write or args.out:
|
|
903
|
+
return _static_staged_only_refusal()
|
|
904
|
+
return _static_fix(path)
|
|
905
|
+
if args.engine == "model" and _audit_seat() is None:
|
|
906
|
+
return _no_seat_refusal()
|
|
907
|
+
result = fix(path)
|
|
908
|
+
if result["status"] != "OK":
|
|
909
|
+
reason = result.get("reason", "fix failed")
|
|
910
|
+
seatless = reason.startswith("no fix seat") or (
|
|
911
|
+
reason == "audit failed before fix"
|
|
912
|
+
and _audit_seat() is None)
|
|
913
|
+
if args.engine is None and seatless:
|
|
914
|
+
if args.write or args.out:
|
|
915
|
+
return _static_staged_only_refusal()
|
|
916
|
+
return _static_fix(path)
|
|
917
|
+
print(f"[FAIL] {reason}")
|
|
918
|
+
return 1
|
|
919
|
+
print(_engine_line("model"))
|
|
920
|
+
if result.get("issues_fixed", 0) == 0 and result.get("message"):
|
|
921
|
+
print(f"[OK] {result['message']}")
|
|
922
|
+
return 0
|
|
923
|
+
if not args.write and not args.out:
|
|
924
|
+
print(f"[OK] fix generated ({result['issues_fixed']} issues, "
|
|
925
|
+
f"model {result['fix_model']}) - preview only, disk untouched:")
|
|
926
|
+
print("--- fixed code follows ---")
|
|
927
|
+
print(result["fixed_code"])
|
|
928
|
+
print("--- end ---")
|
|
929
|
+
print("consent lane: re-run with --write to replace the file "
|
|
930
|
+
"(or --out <path>)")
|
|
931
|
+
return 0
|
|
932
|
+
dest = args.out or path
|
|
933
|
+
target = Path(dest)
|
|
934
|
+
if Path(path).suffix == ".py":
|
|
935
|
+
import subprocess
|
|
936
|
+
import tempfile
|
|
937
|
+
with tempfile.NamedTemporaryFile("w", suffix=".py",
|
|
938
|
+
delete=False) as tmp:
|
|
939
|
+
tmp.write(result["fixed_code"])
|
|
940
|
+
gate = subprocess.run([sys.executable, "-m", "wiz_central_toolkit.pythonwiz",
|
|
941
|
+
"check", tmp.name],
|
|
942
|
+
capture_output=True, text=True)
|
|
943
|
+
os.unlink(tmp.name)
|
|
944
|
+
if gate.returncode != 0:
|
|
945
|
+
print("[FAIL] fixed code rejected by pythonwiz gate - "
|
|
946
|
+
"nothing written")
|
|
947
|
+
return 1
|
|
948
|
+
print("[OK] pythonwiz gate passed")
|
|
949
|
+
ok, detail = _stage_via_writewiz(target, result["fixed_code"])
|
|
950
|
+
if not ok:
|
|
951
|
+
print(f"[FAIL] fix could not stage via write_wizard: {detail}")
|
|
952
|
+
return 1
|
|
953
|
+
print(f"[STAGED] model fix for {target} — {detail}")
|
|
954
|
+
if not _ask_fix_consent(target):
|
|
955
|
+
print("[CONSENT WALL] consent withheld — nothing written; "
|
|
956
|
+
"the staged fix stays in writewiz's quarantine")
|
|
957
|
+
return 1
|
|
958
|
+
ok, detail = _land_via_writewiz(target)
|
|
959
|
+
if not ok:
|
|
960
|
+
print(f"[FAIL] writewiz commit refused: {detail}")
|
|
961
|
+
return 1
|
|
962
|
+
print(f"[OK] written: {dest} (issues addressed: "
|
|
963
|
+
f"{result['issues_fixed']})")
|
|
964
|
+
return 0
|
|
965
|
+
|
|
966
|
+
p.print_help()
|
|
967
|
+
return 0
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
if __name__ == "__main__":
|
|
971
|
+
sys.exit(main())
|