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,364 @@
|
|
|
1
|
+
"""debug-wizard brain — the diagnosis oracle. Never mutates; only proposes."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import builtins
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
HOME = Path.home()
|
|
12
|
+
BANK = HOME / ".debug-wizard"
|
|
13
|
+
IGNORE_LIST = BANK / "ignored.json"
|
|
14
|
+
|
|
15
|
+
DEBUG_LAW = """DEBUG LAW (Debug Wizard, the diagnosis oracle)
|
|
16
|
+
1. Diagnose freely: autopsy tracebacks, classify errors, count hotspots, report facts.
|
|
17
|
+
2. Never mutate: no writes, no patches, no fixes applied — only proposals staged for the Write Wizard.
|
|
18
|
+
3. Consent wall: any fix suggestion goes through write_wizard propose; Governor consents.
|
|
19
|
+
4. Plain verdicts: [OK]/[FAIL]/out of scope, exit 0/1/2, no tracebacks.
|
|
20
|
+
5. Truth first: run python_wizard check or python -m py_compile before analysis.
|
|
21
|
+
6. Ignore list amends only by Governor decree; protected paths stay ignored.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def _ensure_bank():
|
|
25
|
+
BANK.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
if not IGNORE_LIST.exists():
|
|
27
|
+
IGNORE_LIST.write_text("[]")
|
|
28
|
+
|
|
29
|
+
def load_ignored() -> list[str]:
|
|
30
|
+
_ensure_bank()
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(IGNORE_LIST.read_text())
|
|
33
|
+
except (json.JSONDecodeError, OSError):
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
def add_ignore(path_str: str) -> dict:
|
|
37
|
+
target = Path(path_str).expanduser().resolve()
|
|
38
|
+
ign = load_ignored()
|
|
39
|
+
if str(target) in ign:
|
|
40
|
+
return {"ok": False, "error": f"already ignored: {target}"}
|
|
41
|
+
ign.append(str(target))
|
|
42
|
+
IGNORE_LIST.write_text(json.dumps(ign, indent=2))
|
|
43
|
+
return {"ok": True, "ignored": str(target)}
|
|
44
|
+
|
|
45
|
+
def remove_ignore(path_str: str) -> dict:
|
|
46
|
+
target = Path(path_str).expanduser().resolve()
|
|
47
|
+
ign = load_ignored()
|
|
48
|
+
if str(target) not in ign:
|
|
49
|
+
return {"ok": False, "error": f"not ignored: {target}"}
|
|
50
|
+
ign.remove(str(target))
|
|
51
|
+
IGNORE_LIST.write_text(json.dumps(ign, indent=2))
|
|
52
|
+
return {"ok": True, "unignored": str(target)}
|
|
53
|
+
|
|
54
|
+
def _walk_py_files(root: Path):
|
|
55
|
+
root = Path(root)
|
|
56
|
+
if not root.exists():
|
|
57
|
+
return
|
|
58
|
+
if root.is_file() and root.suffix == ".py":
|
|
59
|
+
yield root
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
ignored = [Path(p) for p in load_ignored()]
|
|
63
|
+
for p in sorted(root.rglob("*.py")):
|
|
64
|
+
if any(part in {".venv", ".git", "__pycache__"} for part in p.parts):
|
|
65
|
+
continue
|
|
66
|
+
resolved = p.resolve()
|
|
67
|
+
if any(resolved == item or item in resolved.parents for item in ignored):
|
|
68
|
+
continue
|
|
69
|
+
yield p
|
|
70
|
+
|
|
71
|
+
def _compile_errors(file: Path) -> list[dict]:
|
|
72
|
+
"""Return list of compilation errors without tracebacks."""
|
|
73
|
+
errors = []
|
|
74
|
+
try:
|
|
75
|
+
with file.open("r", encoding="utf-8", errors="replace") as f:
|
|
76
|
+
source = f.read()
|
|
77
|
+
ast.parse(source)
|
|
78
|
+
except SyntaxError as e:
|
|
79
|
+
errors.append({
|
|
80
|
+
"line": e.lineno or 1,
|
|
81
|
+
"type": "SyntaxError",
|
|
82
|
+
"message": e.msg or "invalid syntax",
|
|
83
|
+
"col": e.offset or 0,
|
|
84
|
+
"snippet": e.text.strip() if e.text else ""
|
|
85
|
+
})
|
|
86
|
+
except Exception as e:
|
|
87
|
+
errors.append({"line": 0, "type": type(e).__name__, "message": str(e)})
|
|
88
|
+
return errors
|
|
89
|
+
|
|
90
|
+
def _classify(errors: list[dict]) -> dict[str, int]:
|
|
91
|
+
counts = Counter(e["type"] for e in errors)
|
|
92
|
+
return dict(counts)
|
|
93
|
+
|
|
94
|
+
_MODULE_DUNDERS = frozenset(
|
|
95
|
+
{"__file__", "__name__", "__doc__", "__package__", "__spec__",
|
|
96
|
+
"__loader__", "__builtins__", "__path__", "__cached__"}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _missing_imports(file: Path) -> list[str]:
|
|
101
|
+
"""Names read but never bound anywhere in the file (likely missing imports)."""
|
|
102
|
+
try:
|
|
103
|
+
tree = ast.parse(file.read_text(encoding="utf-8", errors="replace"))
|
|
104
|
+
except (SyntaxError, ValueError, OSError):
|
|
105
|
+
return []
|
|
106
|
+
bound: set[str] = set(dir(builtins)) | _MODULE_DUNDERS
|
|
107
|
+
for node in ast.walk(tree):
|
|
108
|
+
if isinstance(node, ast.ImportFrom) and any(a.name == "*" for a in node.names):
|
|
109
|
+
return [] # star import: cannot know what it defines
|
|
110
|
+
if isinstance(node, ast.Import):
|
|
111
|
+
for alias in node.names:
|
|
112
|
+
bound.add(alias.asname or alias.name.split(".")[0])
|
|
113
|
+
elif isinstance(node, ast.ImportFrom):
|
|
114
|
+
for alias in node.names:
|
|
115
|
+
bound.add(alias.asname or alias.name)
|
|
116
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
117
|
+
bound.add(node.name)
|
|
118
|
+
elif isinstance(node, ast.arg):
|
|
119
|
+
bound.add(node.arg)
|
|
120
|
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
121
|
+
bound.add(node.id)
|
|
122
|
+
elif isinstance(node, ast.ExceptHandler) and node.name:
|
|
123
|
+
bound.add(node.name)
|
|
124
|
+
elif isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
125
|
+
bound.update(node.names)
|
|
126
|
+
elif isinstance(node, (ast.MatchAs, ast.MatchStar)) and node.name:
|
|
127
|
+
bound.add(node.name)
|
|
128
|
+
elif isinstance(node, ast.MatchMapping) and node.rest:
|
|
129
|
+
bound.add(node.rest)
|
|
130
|
+
elif type(node).__name__ in ("TypeVar", "ParamSpec", "TypeVarTuple"):
|
|
131
|
+
bound.add(str(getattr(node, "name", "")))
|
|
132
|
+
used = {
|
|
133
|
+
node.id
|
|
134
|
+
for node in ast.walk(tree)
|
|
135
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
|
|
136
|
+
}
|
|
137
|
+
return sorted(used - bound)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _signature_issues(file: Path) -> list[dict]:
|
|
141
|
+
"""Detect call vs definition mismatches."""
|
|
142
|
+
issues = []
|
|
143
|
+
try:
|
|
144
|
+
tree = ast.parse(file.read_text(encoding="utf-8", errors="replace"))
|
|
145
|
+
except SyntaxError:
|
|
146
|
+
return [{"line": 0, "type": "SyntaxError", "message": "file unparseable"}]
|
|
147
|
+
|
|
148
|
+
defs = {node.name: {"args": len(node.args.args), "lineno": node.lineno}
|
|
149
|
+
for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
|
|
150
|
+
calls = [(node.func.id, len(node.args), node.lineno)
|
|
151
|
+
for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)]
|
|
152
|
+
|
|
153
|
+
for func_name, call_args, lineno in calls:
|
|
154
|
+
if func_name in defs and defs[func_name]["args"] != call_args:
|
|
155
|
+
issues.append({"line": lineno, "type": "SignatureMismatch",
|
|
156
|
+
"message": f"{func_name} def={defs[func_name]['args']} args, call={call_args}"})
|
|
157
|
+
return issues
|
|
158
|
+
|
|
159
|
+
_TB_FRAME = re.compile(
|
|
160
|
+
r'^\s*File "(?P<file>.+)", line (?P<line>\d+)(?:, in (?P<func>.*))?$')
|
|
161
|
+
_TB_EXC = re.compile(
|
|
162
|
+
r"^(?P<type>[A-Za-z_][\w.]*)\s*(?::\s*(?P<msg>.*))?$")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_traceback(text: str) -> dict | None:
|
|
166
|
+
"""Parse a real Python traceback pasted/saved in a file, or None.
|
|
167
|
+
|
|
168
|
+
A traceback is its header line, at least one File frame, and a
|
|
169
|
+
final exception line. Anything else is not a traceback and the
|
|
170
|
+
caller falls back to the compile check.
|
|
171
|
+
"""
|
|
172
|
+
lines = text.splitlines()
|
|
173
|
+
if not any(l.strip().startswith("Traceback (most recent call last)")
|
|
174
|
+
for l in lines):
|
|
175
|
+
return None
|
|
176
|
+
frames = []
|
|
177
|
+
for l in lines:
|
|
178
|
+
m = _TB_FRAME.match(l)
|
|
179
|
+
if m:
|
|
180
|
+
frames.append({"file": m.group("file"),
|
|
181
|
+
"line": int(m.group("line")),
|
|
182
|
+
"func": (m.group("func") or "").strip()})
|
|
183
|
+
exc = None
|
|
184
|
+
for l in reversed(lines):
|
|
185
|
+
s = l.strip()
|
|
186
|
+
if not s or s.startswith("File ") or s.startswith("Traceback"):
|
|
187
|
+
continue
|
|
188
|
+
m = _TB_EXC.match(s)
|
|
189
|
+
if m and (m.group("type")[0].isupper() or "." in m.group("type")):
|
|
190
|
+
exc = {"type": m.group("type"), "message": m.group("msg") or ""}
|
|
191
|
+
break # only the last content line can be the exception line
|
|
192
|
+
if not frames or exc is None:
|
|
193
|
+
return None
|
|
194
|
+
return {"frames": frames, "type": exc["type"], "message": exc["message"]}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _read_text(file: Path) -> str:
|
|
198
|
+
try:
|
|
199
|
+
return Path(file).read_text(encoding="utf-8", errors="replace")
|
|
200
|
+
except OSError:
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _frame_home(file: str) -> str:
|
|
205
|
+
"""'library' for stdlib / site-packages frames, else 'project'."""
|
|
206
|
+
import sysconfig
|
|
207
|
+
f = file.replace("\\", "/")
|
|
208
|
+
roots = [sysconfig.get_path("stdlib"), sysconfig.get_path("purelib"),
|
|
209
|
+
sysconfig.get_path("platlib")]
|
|
210
|
+
if "/site-packages/" in f:
|
|
211
|
+
return "library"
|
|
212
|
+
if any(r and f.startswith(r.replace("\\", "/")) for r in roots):
|
|
213
|
+
return "library"
|
|
214
|
+
return "project"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def stack_digest(text: str) -> dict | None:
|
|
218
|
+
"""Digest a saved traceback: exception, frame chain, deepest frames.
|
|
219
|
+
|
|
220
|
+
Returns None when the text is not a traceback. The deepest
|
|
221
|
+
project frame is the last frame outside the library — where the
|
|
222
|
+
Governor's own code was standing when it fell. A frame repeated
|
|
223
|
+
three or more times is flagged as suspected recursion.
|
|
224
|
+
"""
|
|
225
|
+
tb = _parse_traceback(text)
|
|
226
|
+
if tb is None:
|
|
227
|
+
return None
|
|
228
|
+
frames = [{**fr, "home": _frame_home(fr["file"])} for fr in tb["frames"]]
|
|
229
|
+
counts = Counter((f["file"], f["line"], f["func"]) for f in frames)
|
|
230
|
+
repeated = [(k, c) for k, c in counts.most_common() if c >= 3]
|
|
231
|
+
project = [f for f in frames if f["home"] == "project"]
|
|
232
|
+
return {"type": tb["type"], "message": tb["message"],
|
|
233
|
+
"frames": frames,
|
|
234
|
+
"deepest": frames[-1],
|
|
235
|
+
"deepest_project": project[-1] if project else None,
|
|
236
|
+
"repeated": repeated}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def trace(file: Path) -> dict:
|
|
240
|
+
"""Autopsy one file: a saved traceback if it is one, else compilation errors."""
|
|
241
|
+
tb = _parse_traceback(_read_text(file))
|
|
242
|
+
if tb is not None:
|
|
243
|
+
last = tb["frames"][-1]
|
|
244
|
+
return {"file": str(file), "ok": False, "traceback": tb,
|
|
245
|
+
"errors": [{"line": last["line"], "type": tb["type"],
|
|
246
|
+
"message": tb["message"], "col": 0, "snippet": ""}]}
|
|
247
|
+
errors = _compile_errors(file)
|
|
248
|
+
return {"file": str(file), "errors": errors, "ok": len(errors) == 0}
|
|
249
|
+
|
|
250
|
+
def classify_file(file: Path) -> dict:
|
|
251
|
+
"""Error type breakdown."""
|
|
252
|
+
tb = _parse_traceback(_read_text(file))
|
|
253
|
+
if tb is not None:
|
|
254
|
+
return {"file": str(file), "classification": {tb["type"]: 1}, "total": 1}
|
|
255
|
+
errors = _compile_errors(file)
|
|
256
|
+
return {"file": str(file), "classification": _classify(errors), "total": len(errors)}
|
|
257
|
+
|
|
258
|
+
def locate(root: Path, pattern: str) -> list[str]:
|
|
259
|
+
"""Find files whose contents contain the pattern, or whose errors match it."""
|
|
260
|
+
hits = []
|
|
261
|
+
for f in _walk_py_files(root):
|
|
262
|
+
if pattern in _read_text(f):
|
|
263
|
+
hits.append(str(f))
|
|
264
|
+
continue
|
|
265
|
+
err = trace(f)
|
|
266
|
+
for e in err.get("errors", []):
|
|
267
|
+
if pattern.lower() in e["message"].lower() or pattern in e["type"]:
|
|
268
|
+
hits.append(str(f))
|
|
269
|
+
break
|
|
270
|
+
return hits
|
|
271
|
+
|
|
272
|
+
def count_dir(root: Path) -> dict:
|
|
273
|
+
"""Errors by category."""
|
|
274
|
+
all_errors = []
|
|
275
|
+
for f in _walk_py_files(root):
|
|
276
|
+
all_errors.extend(_compile_errors(f))
|
|
277
|
+
return {"totals": _classify(all_errors), "files_checked": sum(1 for _ in _walk_py_files(root))}
|
|
278
|
+
|
|
279
|
+
def hotspot(root: Path) -> list[tuple[str, int]]:
|
|
280
|
+
"""Files with most errors, ranked."""
|
|
281
|
+
scores = []
|
|
282
|
+
for f in _walk_py_files(root):
|
|
283
|
+
err_count = len(_compile_errors(f))
|
|
284
|
+
if err_count > 0:
|
|
285
|
+
scores.append((str(f), err_count))
|
|
286
|
+
return sorted(scores, key=lambda x: -x[1])[:10]
|
|
287
|
+
|
|
288
|
+
def timeline(root: Path) -> list[dict]:
|
|
289
|
+
"""Error frequency by modification time."""
|
|
290
|
+
data = []
|
|
291
|
+
for f in _walk_py_files(root):
|
|
292
|
+
err_count = len(_compile_errors(f))
|
|
293
|
+
if err_count > 0:
|
|
294
|
+
data.append({
|
|
295
|
+
"file": str(f),
|
|
296
|
+
"errors": err_count,
|
|
297
|
+
"mtime": f.stat().st_mtime
|
|
298
|
+
})
|
|
299
|
+
data.sort(key=lambda x: -x["mtime"])
|
|
300
|
+
return data
|
|
301
|
+
|
|
302
|
+
def imports(file: Path) -> dict:
|
|
303
|
+
"""Missing import detection."""
|
|
304
|
+
missing = _missing_imports(file)
|
|
305
|
+
return {"file": str(file), "likely_missing": missing}
|
|
306
|
+
|
|
307
|
+
def names(file: Path) -> list[str]:
|
|
308
|
+
"""Undefined names."""
|
|
309
|
+
return _missing_imports(file)
|
|
310
|
+
|
|
311
|
+
def signatures(file: Path) -> list[dict]:
|
|
312
|
+
"""Function signature issues."""
|
|
313
|
+
return _signature_issues(file)
|
|
314
|
+
|
|
315
|
+
def suggest_fix(file: Path) -> str:
|
|
316
|
+
"""Human-readable fix suggestion (no AI — heuristic)."""
|
|
317
|
+
tb = _parse_traceback(_read_text(file))
|
|
318
|
+
if tb is not None:
|
|
319
|
+
last = tb["frames"][-1]
|
|
320
|
+
lines = [f"[TRACEBACK] {file}"]
|
|
321
|
+
for fr in tb["frames"]:
|
|
322
|
+
lines.append(f" File \"{fr['file']}\", line {fr['line']}, in {fr['func']}")
|
|
323
|
+
lines.append(f" {tb['type']}: {tb['message']}" if tb["message"]
|
|
324
|
+
else f" {tb['type']}")
|
|
325
|
+
lines.append(f"raised at {last['file']}:{last['line']} in {last['func']} "
|
|
326
|
+
f"(innermost of {len(tb['frames'])} frame(s)) — start there.")
|
|
327
|
+
return "\n".join(lines)
|
|
328
|
+
errs = trace(file)
|
|
329
|
+
if errs["ok"]:
|
|
330
|
+
return f"[OK] {file}: no compilation errors"
|
|
331
|
+
lines = [f"[ISSUE] {file}"]
|
|
332
|
+
for e in errs["errors"][:5]:
|
|
333
|
+
lines.append(f" Line {e['line']}: {e['type']} — {e['message']}")
|
|
334
|
+
if errs["errors"][5:]:
|
|
335
|
+
lines.append(f" ... and {len(errs['errors']) - 5} more errors")
|
|
336
|
+
return "\n".join(lines)
|
|
337
|
+
|
|
338
|
+
def audit_dir(root: Path) -> dict:
|
|
339
|
+
"""Full error census."""
|
|
340
|
+
files = list(_walk_py_files(root))
|
|
341
|
+
healthy = 0
|
|
342
|
+
unhealthy = 0
|
|
343
|
+
total_errors = 0
|
|
344
|
+
for f in files:
|
|
345
|
+
err = trace(f)
|
|
346
|
+
if err["ok"]:
|
|
347
|
+
healthy += 1
|
|
348
|
+
else:
|
|
349
|
+
unhealthy += 1
|
|
350
|
+
total_errors += len(err["errors"])
|
|
351
|
+
return {"healthy": healthy, "unhealthy": unhealthy, "total_errors": total_errors,
|
|
352
|
+
"files_checked": len(files), "health_pct": round(healthy / max(len(files), 1) * 100, 1)}
|
|
353
|
+
|
|
354
|
+
def report_json(root: Path) -> str:
|
|
355
|
+
"""JSON report for CI/CD."""
|
|
356
|
+
data = {"timestamp": dt.datetime.now().isoformat(timespec="seconds"), "root": str(root),
|
|
357
|
+
"audit": audit_dir(root),
|
|
358
|
+
"hotspots": [{"file": f, "errors": e} for f, e in hotspot(root)]}
|
|
359
|
+
return json.dumps(data, indent=2)
|
|
360
|
+
|
|
361
|
+
def stats_fleet() -> dict:
|
|
362
|
+
"""Fleet statistics derivable anywhere: the current directory's error census."""
|
|
363
|
+
root = Path.cwd()
|
|
364
|
+
return {str(root): audit_dir(root)}
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Entry point for wiz dispatch."""
|
|
2
|
+
import sys
|
|
3
|
+
from .lib import cli as internal_cli
|
|
4
|
+
|
|
5
|
+
# Wrap main() to accept argv (wiz dispatcher calls main(args))
|
|
6
|
+
def main(argv=None):
|
|
7
|
+
# Save original sys.argv
|
|
8
|
+
old_argv = sys.argv
|
|
9
|
+
# Replace with provided argv (or empty list)
|
|
10
|
+
if argv is not None:
|
|
11
|
+
sys.argv = [sys.argv[0]] + list(argv)
|
|
12
|
+
try:
|
|
13
|
+
return internal_cli.main()
|
|
14
|
+
finally:
|
|
15
|
+
sys.argv = old_argv
|
|
16
|
+
|
|
17
|
+
# Also expose for module-level access
|
|
18
|
+
__all__ = ['main']
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
sys.exit(main())
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""diff-wizard CLI front-end."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .lib.diff_wizard import DiffWizard
|
|
5
|
+
|
|
6
|
+
def main() -> int:
|
|
7
|
+
args = sys.argv[1:]
|
|
8
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
9
|
+
print("Diff Wizard — standalone change-truth doctor")
|
|
10
|
+
print("usage: diffwiz <command> [args]")
|
|
11
|
+
print(" files <a> <b> unified diff of two files")
|
|
12
|
+
print(" dirs <a> <b> compare directory trees")
|
|
13
|
+
print(" context <N> <a> <b> unified diff with N context lines")
|
|
14
|
+
print(" summary <a> <b> diff statistics without full output")
|
|
15
|
+
print(" word <a> <b> word-level diff")
|
|
16
|
+
print(" same <a> <b> exit 0 if identical, 1 if different (silent)")
|
|
17
|
+
print(" reverse <a> <b> diff b -> a (swap sides)")
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
wiz = DiffWizard()
|
|
21
|
+
|
|
22
|
+
# --- existing commands ---
|
|
23
|
+
if args[0] == "files":
|
|
24
|
+
if len(args) < 3:
|
|
25
|
+
print("out of scope")
|
|
26
|
+
return 2
|
|
27
|
+
a, b = Path(args[1]), Path(args[2])
|
|
28
|
+
for p in (a, b):
|
|
29
|
+
if not p.exists():
|
|
30
|
+
print(f"[FAIL] not found: {p}")
|
|
31
|
+
return 1
|
|
32
|
+
text = wiz.diff_files(a, b)
|
|
33
|
+
if not text:
|
|
34
|
+
print("[OK] files identical")
|
|
35
|
+
return 0
|
|
36
|
+
print(text)
|
|
37
|
+
print("[FAIL] files differ")
|
|
38
|
+
return 1
|
|
39
|
+
if args[0] == "dirs":
|
|
40
|
+
if len(args) < 3:
|
|
41
|
+
print("out of scope")
|
|
42
|
+
return 2
|
|
43
|
+
da, db = Path(args[1]), Path(args[2])
|
|
44
|
+
for p in (da, db):
|
|
45
|
+
if not p.is_dir():
|
|
46
|
+
print(f"[FAIL] not a directory: {p}")
|
|
47
|
+
return 1
|
|
48
|
+
r = wiz.diff_dirs(da, db)
|
|
49
|
+
count = len(r["only_a"]) + len(r["only_b"]) + len(r["differing"])
|
|
50
|
+
for rel in r["only_a"]:
|
|
51
|
+
print(f"only in {args[1]}: {rel}")
|
|
52
|
+
for rel in r["only_b"]:
|
|
53
|
+
print(f"only in {args[2]}: {rel}")
|
|
54
|
+
for rel in r["differing"]:
|
|
55
|
+
print(f"differing: {rel}")
|
|
56
|
+
if count == 0:
|
|
57
|
+
print("[OK] trees identical")
|
|
58
|
+
return 0
|
|
59
|
+
print(f"[FAIL] {count} differences")
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
# --- new commands ---
|
|
63
|
+
if args[0] == "context":
|
|
64
|
+
if len(args) < 4:
|
|
65
|
+
print("out of scope - context requires N a b")
|
|
66
|
+
return 2
|
|
67
|
+
n = int(args[1])
|
|
68
|
+
a, b = Path(args[2]), Path(args[3])
|
|
69
|
+
for p in (a, b):
|
|
70
|
+
if not p.exists():
|
|
71
|
+
print(f"[FAIL] not found: {p}")
|
|
72
|
+
return 1
|
|
73
|
+
text = wiz.diff_files_context(a, b, n)
|
|
74
|
+
if not text:
|
|
75
|
+
print("[OK] files identical")
|
|
76
|
+
return 0
|
|
77
|
+
print(text)
|
|
78
|
+
print("[FAIL] files differ")
|
|
79
|
+
return 1
|
|
80
|
+
if args[0] == "summary":
|
|
81
|
+
if len(args) < 3:
|
|
82
|
+
print("out of scope")
|
|
83
|
+
return 2
|
|
84
|
+
a, b = Path(args[1]), Path(args[2])
|
|
85
|
+
for p in (a, b):
|
|
86
|
+
if not p.exists():
|
|
87
|
+
print(f"[FAIL] not found: {p}")
|
|
88
|
+
return 1
|
|
89
|
+
r = wiz.diff_summary(a, b)
|
|
90
|
+
if r["identical"]:
|
|
91
|
+
print("[OK] identical")
|
|
92
|
+
return 0
|
|
93
|
+
print(f"added={r['added_lines']} removed={r['removed_lines']} hunks={r['hunks']}")
|
|
94
|
+
return 1
|
|
95
|
+
if args[0] == "word":
|
|
96
|
+
if len(args) < 3:
|
|
97
|
+
print("out of scope")
|
|
98
|
+
return 2
|
|
99
|
+
a, b = Path(args[1]), Path(args[2])
|
|
100
|
+
for p in (a, b):
|
|
101
|
+
if not p.exists():
|
|
102
|
+
print(f"[FAIL] not found: {p}")
|
|
103
|
+
return 1
|
|
104
|
+
print(wiz.diff_word_level(a, b))
|
|
105
|
+
return 0
|
|
106
|
+
if args[0] == "same":
|
|
107
|
+
if len(args) < 3:
|
|
108
|
+
print("out of scope")
|
|
109
|
+
return 2
|
|
110
|
+
a, b = Path(args[1]), Path(args[2])
|
|
111
|
+
for p in (a, b):
|
|
112
|
+
if not p.exists():
|
|
113
|
+
print(f"[FAIL] not found: {p}")
|
|
114
|
+
return 1
|
|
115
|
+
return 0 if wiz.files_identical(a, b) else 1
|
|
116
|
+
if args[0] == "reverse":
|
|
117
|
+
if len(args) < 3:
|
|
118
|
+
print("out of scope")
|
|
119
|
+
return 2
|
|
120
|
+
a, b = Path(args[1]), Path(args[2])
|
|
121
|
+
for p in (a, b):
|
|
122
|
+
if not p.exists():
|
|
123
|
+
print(f"[FAIL] not found: {p}")
|
|
124
|
+
return 1
|
|
125
|
+
text = wiz.diff_files_reverse(a, b)
|
|
126
|
+
if not text:
|
|
127
|
+
print("[OK] files identical")
|
|
128
|
+
return 0
|
|
129
|
+
print(text)
|
|
130
|
+
print("[FAIL] files differ")
|
|
131
|
+
return 1
|
|
132
|
+
|
|
133
|
+
print("out of scope")
|
|
134
|
+
return 2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""diff-wizard CLI front-end."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .diff_wizard import DiffWizard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main(argv=None) -> int:
|
|
8
|
+
args = sys.argv[1:] if argv is None else list(argv)
|
|
9
|
+
if not args or args[0] in ("-h", "--help", "help"):
|
|
10
|
+
print("Diff Wizard — standalone change-truth doctor")
|
|
11
|
+
print("alias: diffwiz")
|
|
12
|
+
print("usage: diff_wizard <command> [args]")
|
|
13
|
+
print(" files <a> <b> unified diff of two files")
|
|
14
|
+
print(" dirs <a> <b> compare directory trees")
|
|
15
|
+
print(" context <N> <a> <b> unified diff with N context lines")
|
|
16
|
+
print(" summary <a> <b> diff statistics without full output")
|
|
17
|
+
print(" word <a> <b> word-level diff")
|
|
18
|
+
print(" same <a> <b> exit 0 if identical, 1 if different (silent)")
|
|
19
|
+
print(" reverse <a> <b> diff b -> a (swap sides)")
|
|
20
|
+
return 0
|
|
21
|
+
|
|
22
|
+
wiz = DiffWizard()
|
|
23
|
+
|
|
24
|
+
# --- existing commands ---
|
|
25
|
+
if args[0] == "files":
|
|
26
|
+
if len(args) < 3:
|
|
27
|
+
print("out of scope")
|
|
28
|
+
return 2
|
|
29
|
+
a, b = Path(args[1]), Path(args[2])
|
|
30
|
+
for p in (a, b):
|
|
31
|
+
if not p.exists():
|
|
32
|
+
print(f"[FAIL] not found: {p}")
|
|
33
|
+
return 1
|
|
34
|
+
text = wiz.diff_files(a, b)
|
|
35
|
+
if not text:
|
|
36
|
+
print("[OK] files identical")
|
|
37
|
+
return 0
|
|
38
|
+
print(text)
|
|
39
|
+
print("[FAIL] files differ")
|
|
40
|
+
return 1
|
|
41
|
+
|
|
42
|
+
if args[0] == "dirs":
|
|
43
|
+
if len(args) < 3:
|
|
44
|
+
print("out of scope")
|
|
45
|
+
return 2
|
|
46
|
+
da, db = Path(args[1]), Path(args[2])
|
|
47
|
+
for p in (da, db):
|
|
48
|
+
if not p.is_dir():
|
|
49
|
+
print(f"[FAIL] not a directory: {p}")
|
|
50
|
+
return 1
|
|
51
|
+
r = wiz.diff_dirs(da, db)
|
|
52
|
+
count = len(r["only_a"]) + len(r["only_b"]) + len(r["differing"])
|
|
53
|
+
for rel in r["only_a"]:
|
|
54
|
+
print(f"only in {args[1]}: {rel}")
|
|
55
|
+
for rel in r["only_b"]:
|
|
56
|
+
print(f"only in {args[2]}: {rel}")
|
|
57
|
+
for rel in r["differing"]:
|
|
58
|
+
print(f"differing: {rel}")
|
|
59
|
+
if count == 0:
|
|
60
|
+
print("[OK] trees identical")
|
|
61
|
+
return 0
|
|
62
|
+
print(f"[FAIL] {count} differences")
|
|
63
|
+
return 1
|
|
64
|
+
|
|
65
|
+
# --- new commands ---
|
|
66
|
+
if args[0] == "context":
|
|
67
|
+
if len(args) < 4:
|
|
68
|
+
print("out of scope - context requires N a b")
|
|
69
|
+
return 2
|
|
70
|
+
n = int(args[1])
|
|
71
|
+
a, b = Path(args[2]), Path(args[3])
|
|
72
|
+
for p in (a, b):
|
|
73
|
+
if not p.exists():
|
|
74
|
+
print(f"[FAIL] not found: {p}")
|
|
75
|
+
return 1
|
|
76
|
+
text = wiz.diff_files_context(a, b, n)
|
|
77
|
+
if not text:
|
|
78
|
+
print("[OK] files identical")
|
|
79
|
+
return 0
|
|
80
|
+
print(text)
|
|
81
|
+
print("[FAIL] files differ")
|
|
82
|
+
return 1
|
|
83
|
+
|
|
84
|
+
if args[0] == "summary":
|
|
85
|
+
if len(args) < 3:
|
|
86
|
+
print("out of scope")
|
|
87
|
+
return 2
|
|
88
|
+
a, b = Path(args[1]), Path(args[2])
|
|
89
|
+
for p in (a, b):
|
|
90
|
+
if not p.exists():
|
|
91
|
+
print(f"[FAIL] not found: {p}")
|
|
92
|
+
return 1
|
|
93
|
+
if not p.is_file():
|
|
94
|
+
print(f"[FAIL] not a file: {p}")
|
|
95
|
+
return 1
|
|
96
|
+
r = wiz.diff_summary(a, b)
|
|
97
|
+
if r["identical"]:
|
|
98
|
+
print("[OK] identical")
|
|
99
|
+
return 0
|
|
100
|
+
print(f"added={r['added_lines']} removed={r['removed_lines']} hunks={r['hunks']}")
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
if args[0] == "word":
|
|
104
|
+
if len(args) < 3:
|
|
105
|
+
print("out of scope")
|
|
106
|
+
return 2
|
|
107
|
+
a, b = Path(args[1]), Path(args[2])
|
|
108
|
+
for p in (a, b):
|
|
109
|
+
if not p.exists():
|
|
110
|
+
print(f"[FAIL] not found: {p}")
|
|
111
|
+
return 1
|
|
112
|
+
print(wiz.diff_word_level(a, b))
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
if args[0] == "same":
|
|
116
|
+
if len(args) < 3:
|
|
117
|
+
print("out of scope")
|
|
118
|
+
return 2
|
|
119
|
+
a, b = Path(args[1]), Path(args[2])
|
|
120
|
+
for p in (a, b):
|
|
121
|
+
if not p.exists():
|
|
122
|
+
print(f"[FAIL] not found: {p}")
|
|
123
|
+
return 1
|
|
124
|
+
return 0 if wiz.files_identical(a, b) else 1
|
|
125
|
+
|
|
126
|
+
if args[0] == "reverse":
|
|
127
|
+
if len(args) < 3:
|
|
128
|
+
print("out of scope")
|
|
129
|
+
return 2
|
|
130
|
+
a, b = Path(args[1]), Path(args[2])
|
|
131
|
+
for p in (a, b):
|
|
132
|
+
if not p.exists():
|
|
133
|
+
print(f"[FAIL] not found: {p}")
|
|
134
|
+
return 1
|
|
135
|
+
text = wiz.diff_files_reverse(a, b)
|
|
136
|
+
if not text:
|
|
137
|
+
print("[OK] files identical")
|
|
138
|
+
return 0
|
|
139
|
+
print(text)
|
|
140
|
+
print("[FAIL] files differ")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
print("out of scope")
|
|
144
|
+
return 2
|