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,353 @@
|
|
|
1
|
+
"""python-wizard core: standalone syntax doctor for Python files."""
|
|
2
|
+
import ast
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SKIP_DIRS = {".venv", ".git", "__pycache__", ".bootstrap", "node_modules"}
|
|
9
|
+
STDLIB = set(sys.stdlib_module_names)
|
|
10
|
+
|
|
11
|
+
class PythonWizard:
|
|
12
|
+
"""AST analysis engine. Never executes the target code — parse only."""
|
|
13
|
+
|
|
14
|
+
# ---------- existing ----------
|
|
15
|
+
def check_file(self, path: Path) -> dict:
|
|
16
|
+
try:
|
|
17
|
+
ast.parse(path.read_text())
|
|
18
|
+
return {"file": str(path), "valid": True}
|
|
19
|
+
except FileNotFoundError:
|
|
20
|
+
return {"file": str(path), "valid": False, "error": "file not found"}
|
|
21
|
+
except SyntaxError as e:
|
|
22
|
+
return {"file": str(path), "valid": False, "line": e.lineno,
|
|
23
|
+
"col": e.offset, "error": e.msg}
|
|
24
|
+
|
|
25
|
+
def check_dir(self, root: Path) -> list:
|
|
26
|
+
results = []
|
|
27
|
+
for py in self.py_files(root):
|
|
28
|
+
results.append(self.check_file(py))
|
|
29
|
+
return results
|
|
30
|
+
|
|
31
|
+
def check_entry(self, path: Path) -> dict:
|
|
32
|
+
if not path.exists():
|
|
33
|
+
return {"file": str(path), "valid": False, "error": "file not found"}
|
|
34
|
+
content = path.read_text()
|
|
35
|
+
first = content.splitlines()[0] if content else ""
|
|
36
|
+
if not first.startswith("#!"):
|
|
37
|
+
return {"file": str(path), "valid": False, "error": "no shebang line"}
|
|
38
|
+
parts = first[2:].split()
|
|
39
|
+
if not parts:
|
|
40
|
+
return {"file": str(path), "valid": False, "error": "empty shebang line"}
|
|
41
|
+
exe = parts[0]
|
|
42
|
+
if Path(exe).name == "env":
|
|
43
|
+
# env form: #!/usr/bin/env [-flags] [VAR=value ...] <interpreter> [args]
|
|
44
|
+
cmd = None
|
|
45
|
+
for tok in parts[1:]:
|
|
46
|
+
if tok.startswith("-"):
|
|
47
|
+
continue
|
|
48
|
+
if "=" in tok and "/" not in tok.split("=")[0]:
|
|
49
|
+
continue # VAR=value assignment, not the command
|
|
50
|
+
cmd = tok
|
|
51
|
+
break
|
|
52
|
+
if cmd is None:
|
|
53
|
+
return {"file": str(path), "valid": False,
|
|
54
|
+
"error": f"interpreter missing: {first[2:]}"}
|
|
55
|
+
found = shutil.which(cmd)
|
|
56
|
+
if not found:
|
|
57
|
+
return {"file": str(path), "valid": False,
|
|
58
|
+
"error": f"interpreter missing: {cmd}"}
|
|
59
|
+
return {"file": str(path), "valid": True, "interpreter": found}
|
|
60
|
+
interp = Path(exe)
|
|
61
|
+
if interp.is_absolute():
|
|
62
|
+
if not interp.exists():
|
|
63
|
+
return {"file": str(path), "valid": False,
|
|
64
|
+
"error": f"interpreter missing: {exe}"}
|
|
65
|
+
return {"file": str(path), "valid": True, "interpreter": str(interp)}
|
|
66
|
+
found = shutil.which(exe)
|
|
67
|
+
if not found:
|
|
68
|
+
return {"file": str(path), "valid": False,
|
|
69
|
+
"error": f"interpreter missing: {exe}"}
|
|
70
|
+
return {"file": str(path), "valid": True, "interpreter": found}
|
|
71
|
+
|
|
72
|
+
# ---------- helpers ----------
|
|
73
|
+
@staticmethod
|
|
74
|
+
def py_files(root: Path) -> list:
|
|
75
|
+
return [p for p in sorted(root.rglob("*.py"))
|
|
76
|
+
if not any(part in SKIP_DIRS for part in p.parts)]
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def parse(path: Path) -> ast.Module:
|
|
80
|
+
return ast.parse(path.read_text())
|
|
81
|
+
|
|
82
|
+
# ---------- new commands ----------
|
|
83
|
+
def outline(self, path: Path) -> list:
|
|
84
|
+
tree = self.parse(path)
|
|
85
|
+
symbols = []
|
|
86
|
+
|
|
87
|
+
def visit(node, prefix=""):
|
|
88
|
+
for child in ast.iter_child_nodes(node):
|
|
89
|
+
if isinstance(child, ast.ClassDef):
|
|
90
|
+
symbols.append(("class", prefix + child.name, child.lineno))
|
|
91
|
+
visit(child, prefix + child.name + ".")
|
|
92
|
+
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
93
|
+
symbols.append(("def", prefix + child.name, child.lineno))
|
|
94
|
+
|
|
95
|
+
visit(tree)
|
|
96
|
+
return symbols
|
|
97
|
+
|
|
98
|
+
def imports_of(self, path: Path) -> dict:
|
|
99
|
+
tree = self.parse(path)
|
|
100
|
+
siblings = {p.stem for p in path.parent.glob("*.py")}
|
|
101
|
+
result = {"stdlib": set(), "third_party": set(), "local": set(), "relative": set()}
|
|
102
|
+
for node in ast.walk(tree):
|
|
103
|
+
if isinstance(node, ast.Import):
|
|
104
|
+
for a in node.names:
|
|
105
|
+
top = a.name.split(".")[0]
|
|
106
|
+
if top in STDLIB:
|
|
107
|
+
result["stdlib"].add(a.name)
|
|
108
|
+
elif top in siblings:
|
|
109
|
+
result["local"].add(a.name)
|
|
110
|
+
else:
|
|
111
|
+
result["third_party"].add(a.name)
|
|
112
|
+
elif isinstance(node, ast.ImportFrom):
|
|
113
|
+
if node.level > 0:
|
|
114
|
+
result["relative"].add("." * node.level + (node.module or ""))
|
|
115
|
+
elif node.module:
|
|
116
|
+
top = node.module.split(".")[0]
|
|
117
|
+
if top in siblings:
|
|
118
|
+
result["local"].add(node.module)
|
|
119
|
+
elif top in STDLIB:
|
|
120
|
+
result["stdlib"].add(node.module)
|
|
121
|
+
else:
|
|
122
|
+
result["third_party"].add(node.module)
|
|
123
|
+
return {k: sorted(v) for k, v in result.items()}
|
|
124
|
+
|
|
125
|
+
def todos_of(self, path: Path) -> list:
|
|
126
|
+
pat = re.compile(r"#\s*(TODO|FIXME|XXX|BUG|HACK)\b[:]?(.*)", re.I)
|
|
127
|
+
out = []
|
|
128
|
+
for i, line in enumerate(path.read_text().splitlines(), 1):
|
|
129
|
+
m = pat.search(line)
|
|
130
|
+
if m:
|
|
131
|
+
out.append((i, m.group(1).upper(), m.group(2).strip()))
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
def deadcode(self, path: Path) -> list:
|
|
135
|
+
"""Candidates only: names defined but never referenced by name.
|
|
136
|
+
Attribute-called methods may appear here — static analysis limit."""
|
|
137
|
+
tree = self.parse(path)
|
|
138
|
+
defined = {}
|
|
139
|
+
for node in ast.walk(tree):
|
|
140
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
141
|
+
defined[node.name] = node.lineno
|
|
142
|
+
used = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
143
|
+
dead = [(n, ln) for n, ln in defined.items()
|
|
144
|
+
if n not in used and not n.startswith("__")]
|
|
145
|
+
return sorted(dead, key=lambda x: x[1])
|
|
146
|
+
|
|
147
|
+
def complexity(self, path: Path) -> list:
|
|
148
|
+
tree = self.parse(path)
|
|
149
|
+
out = []
|
|
150
|
+
for node in ast.walk(tree):
|
|
151
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
152
|
+
c = 1
|
|
153
|
+
for sub in ast.walk(node):
|
|
154
|
+
if isinstance(sub, (ast.If, ast.For, ast.While,
|
|
155
|
+
ast.ExceptHandler, ast.With)):
|
|
156
|
+
c += 1
|
|
157
|
+
elif isinstance(sub, ast.BoolOp):
|
|
158
|
+
c += len(sub.values) - 1
|
|
159
|
+
out.append((node.name, node.lineno, c))
|
|
160
|
+
return sorted(out, key=lambda x: -x[2])
|
|
161
|
+
|
|
162
|
+
def loc_stats(self, path: Path) -> dict:
|
|
163
|
+
lines = path.read_text().splitlines()
|
|
164
|
+
code = blank = comment = 0
|
|
165
|
+
for l in lines:
|
|
166
|
+
s = l.strip()
|
|
167
|
+
if not s:
|
|
168
|
+
blank += 1
|
|
169
|
+
elif s.startswith("#"):
|
|
170
|
+
comment += 1
|
|
171
|
+
else:
|
|
172
|
+
code += 1
|
|
173
|
+
tree = self.parse(path)
|
|
174
|
+
funcs = sum(1 for n in ast.walk(tree)
|
|
175
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)))
|
|
176
|
+
classes = sum(1 for n in ast.walk(tree) if isinstance(n, ast.ClassDef))
|
|
177
|
+
return {"lines": len(lines), "code": code, "blank": blank,
|
|
178
|
+
"comment": comment, "functions": funcs, "classes": classes}
|
|
179
|
+
|
|
180
|
+
def docstring_report(self, path: Path) -> list:
|
|
181
|
+
tree = self.parse(path)
|
|
182
|
+
out = []
|
|
183
|
+
for node in ast.walk(tree):
|
|
184
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
185
|
+
if ast.get_docstring(node) is None:
|
|
186
|
+
out.append((node.lineno, node.name))
|
|
187
|
+
return sorted(out)
|
|
188
|
+
|
|
189
|
+
def explain_error(self, path: Path) -> dict:
|
|
190
|
+
try:
|
|
191
|
+
ast.parse(path.read_text())
|
|
192
|
+
return {"valid": True}
|
|
193
|
+
except SyntaxError as e:
|
|
194
|
+
lines = path.read_text().splitlines()
|
|
195
|
+
ctx = []
|
|
196
|
+
lo = max(0, (e.lineno or 1) - 2)
|
|
197
|
+
hi = min(len(lines), (e.lineno or 1) + 1)
|
|
198
|
+
for i in range(lo, hi):
|
|
199
|
+
mark = "->" if i + 1 == e.lineno else " "
|
|
200
|
+
ctx.append(f"{mark} {i+1:4d}| {lines[i]}")
|
|
201
|
+
return {"valid": False, "line": e.lineno, "col": e.offset,
|
|
202
|
+
"error": e.msg, "context": ctx}
|
|
203
|
+
|
|
204
|
+
def shebangs_dir(self, root: Path) -> list:
|
|
205
|
+
results = []
|
|
206
|
+
for p in self.py_files(root):
|
|
207
|
+
with p.open() as f:
|
|
208
|
+
first = f.readline().strip()
|
|
209
|
+
if first.startswith("#!"):
|
|
210
|
+
interp = first[2:]
|
|
211
|
+
ok = Path(interp).is_absolute() and Path(interp).exists()
|
|
212
|
+
results.append((p, interp, ok))
|
|
213
|
+
return results
|
|
214
|
+
|
|
215
|
+
def deps(self, path: Path) -> list:
|
|
216
|
+
return self.imports_of(path)["third_party"]
|
|
217
|
+
|
|
218
|
+
def call_map(self, path: Path) -> dict:
|
|
219
|
+
tree = self.parse(path)
|
|
220
|
+
m = {}
|
|
221
|
+
for node in ast.walk(tree):
|
|
222
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
223
|
+
calls = set()
|
|
224
|
+
for n in ast.walk(node):
|
|
225
|
+
if isinstance(n, ast.Call):
|
|
226
|
+
if isinstance(n.func, ast.Name):
|
|
227
|
+
calls.add(n.func.id)
|
|
228
|
+
elif isinstance(n.func, ast.Attribute):
|
|
229
|
+
calls.add("." + getattr(n.func, "attr", "?"))
|
|
230
|
+
m[node.name] = sorted(calls)
|
|
231
|
+
return m
|
|
232
|
+
|
|
233
|
+
def longest_functions(self, path: Path) -> list:
|
|
234
|
+
tree = self.parse(path)
|
|
235
|
+
out = []
|
|
236
|
+
for n in ast.walk(tree):
|
|
237
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.end_lineno:
|
|
238
|
+
out.append((n.name, n.lineno, (n.end_lineno - n.lineno) + 1))
|
|
239
|
+
return sorted(out, key=lambda x: -x[2])
|
|
240
|
+
|
|
241
|
+
def bare_excepts(self, path: Path) -> list:
|
|
242
|
+
tree = self.parse(path)
|
|
243
|
+
out = []
|
|
244
|
+
for n in ast.walk(tree):
|
|
245
|
+
if isinstance(n, ast.ExceptHandler):
|
|
246
|
+
name = n.type.id if isinstance(n.type, ast.Name) else None
|
|
247
|
+
if n.type is None or name in ("Exception", "BaseException"):
|
|
248
|
+
swallows = any(isinstance(s, ast.Pass) for s in n.body)
|
|
249
|
+
out.append((n.lineno, "bare" if n.type is None else name, swallows))
|
|
250
|
+
return out
|
|
251
|
+
|
|
252
|
+
def secrets_scan(self, path: Path, show_secrets: bool = False) -> list:
|
|
253
|
+
"""Hardcoded-credential findings as (lineno, kind, text) rows.
|
|
254
|
+
|
|
255
|
+
Redaction is the default (AVS ruling, 2026-11-09 — the RS006
|
|
256
|
+
paste-safety standard applied toolkit-wide): `text` is the
|
|
257
|
+
source line with the secret value replaced by <redacted>, so
|
|
258
|
+
the value appears nowhere in the report; `kind` names the
|
|
259
|
+
credential pattern matched. Only the deliberate opt-in
|
|
260
|
+
show_secrets=True returns the line exactly as written.
|
|
261
|
+
"""
|
|
262
|
+
pat = re.compile(
|
|
263
|
+
r"(?i)(password|passwd|pwd|token|secret|api[_-]?key)"
|
|
264
|
+
r"\s*[=:]\s*(['\"])([^'\"]{3,})")
|
|
265
|
+
out = []
|
|
266
|
+
for i, line in enumerate(path.read_text().splitlines(), 1):
|
|
267
|
+
m = pat.search(line)
|
|
268
|
+
if not m:
|
|
269
|
+
continue
|
|
270
|
+
kind = m.group(1).lower().replace("-", "_")
|
|
271
|
+
if kind == "apikey":
|
|
272
|
+
kind = "api_key"
|
|
273
|
+
text = line.strip()
|
|
274
|
+
if not show_secrets:
|
|
275
|
+
lead = len(line) - len(line.lstrip())
|
|
276
|
+
start = m.start(3) - lead
|
|
277
|
+
end = m.end(3) - lead
|
|
278
|
+
text = text[:start] + "<redacted>" + text[end:]
|
|
279
|
+
out.append((i, kind, text))
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
def main_guard(self, path: Path) -> dict:
|
|
283
|
+
tree = self.parse(path)
|
|
284
|
+
has = False
|
|
285
|
+
for node in ast.walk(tree):
|
|
286
|
+
if isinstance(node, ast.If):
|
|
287
|
+
src = ast.dump(node.test)
|
|
288
|
+
if "__name__" in src and "__main__" in src:
|
|
289
|
+
has = True
|
|
290
|
+
return {"has_main_guard": has}
|
|
291
|
+
|
|
292
|
+
def globals_audit(self, path: Path) -> list:
|
|
293
|
+
tree = self.parse(path)
|
|
294
|
+
out = []
|
|
295
|
+
for node in tree.body:
|
|
296
|
+
if isinstance(node, ast.Assign):
|
|
297
|
+
val = node.value
|
|
298
|
+
mutable = isinstance(val, (ast.List, ast.Dict, ast.Set,
|
|
299
|
+
ast.ListComp, ast.DictComp, ast.SetComp))
|
|
300
|
+
names = [t.id for t in node.targets if isinstance(t, ast.Name)]
|
|
301
|
+
out.append((node.lineno, ", ".join(names),
|
|
302
|
+
"mutable" if mutable else "immutable"))
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
def magic_numbers(self, path: Path) -> list:
|
|
306
|
+
COMMON = {0, 1, 2, -1, 3, 4, 8, 10, 16, 32, 64, 100, 1000}
|
|
307
|
+
tree = self.parse(path)
|
|
308
|
+
found = []
|
|
309
|
+
for node in ast.walk(tree):
|
|
310
|
+
if (isinstance(node, ast.Constant)
|
|
311
|
+
and isinstance(node.value, (int, float))
|
|
312
|
+
and not isinstance(node.value, bool)
|
|
313
|
+
and node.value not in COMMON):
|
|
314
|
+
found.append((node.lineno, node.value))
|
|
315
|
+
return sorted(set(found), key=lambda x: x[0])
|
|
316
|
+
|
|
317
|
+
def compare_symbols(self, a: Path, b: Path) -> dict:
|
|
318
|
+
try:
|
|
319
|
+
sa = {(k, n) for k, n, _ in self.outline(a)}
|
|
320
|
+
sb = {(k, n) for k, n, _ in self.outline(b)}
|
|
321
|
+
except SyntaxError as e:
|
|
322
|
+
return {"error": f"line {e.lineno}, col {e.offset}: {e.msg}"}
|
|
323
|
+
return {"only_a": sorted(sa - sb), "only_b": sorted(sb - sa),
|
|
324
|
+
"common": sorted(sa & sb)}
|
|
325
|
+
|
|
326
|
+
def tree_summary(self, root: Path) -> dict:
|
|
327
|
+
files = self.py_files(root)
|
|
328
|
+
total_lines = funcs = classes = invalid = 0
|
|
329
|
+
for p in files:
|
|
330
|
+
if not self.check_file(p)["valid"]:
|
|
331
|
+
invalid += 1
|
|
332
|
+
continue
|
|
333
|
+
text = p.read_text()
|
|
334
|
+
total_lines += len(text.splitlines())
|
|
335
|
+
for n in ast.walk(ast.parse(text)):
|
|
336
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
337
|
+
funcs += 1
|
|
338
|
+
elif isinstance(n, ast.ClassDef):
|
|
339
|
+
classes += 1
|
|
340
|
+
return {"files": len(files), "lines": total_lines,
|
|
341
|
+
"functions": funcs, "classes": classes, "invalid": invalid}
|
|
342
|
+
|
|
343
|
+
def health_score(self, path: Path) -> dict:
|
|
344
|
+
r = self.check_file(path)
|
|
345
|
+
if not r["valid"]:
|
|
346
|
+
return {"score": 0, "reason": f"syntax error at line {r.get('line')}"}
|
|
347
|
+
docs_missing = len(self.docstring_report(path))
|
|
348
|
+
bare = len(self.bare_excepts(path))
|
|
349
|
+
td = len(self.todos_of(path))
|
|
350
|
+
hotspots = len([c for _, _, c in self.complexity(path) if c > 10])
|
|
351
|
+
penalty = 5 * docs_missing + 10 * bare + 3 * td + 10 * hotspots
|
|
352
|
+
return {"score": max(0, 100 - penalty), "docstrings_missing": docs_missing,
|
|
353
|
+
"bare_excepts": bare, "todos": td, "complexity_hotspots": hotspots}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point for readwiz dispatch."""
|
|
2
|
+
import sys
|
|
3
|
+
from .lib import cli as internal_cli
|
|
4
|
+
|
|
5
|
+
def main(argv=None):
|
|
6
|
+
old_argv = sys.argv
|
|
7
|
+
if argv is not None:
|
|
8
|
+
sys.argv = [sys.argv[0]] + list(argv)
|
|
9
|
+
try:
|
|
10
|
+
return internal_cli.main()
|
|
11
|
+
finally:
|
|
12
|
+
sys.argv = old_argv
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(main())
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""read-wizard face — thin CLI over the policy brain."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .lib.read_wizard import check_command, LAW_TEXT, ALLOW_VERBS, DENY_VERBS
|
|
6
|
+
|
|
7
|
+
USAGE = """Read Wizard — read-only mode policy engine (verb gate)
|
|
8
|
+
usage: readwiz check "<raw shell command>"
|
|
9
|
+
readwiz verbs dump the allow/deny tables
|
|
10
|
+
readwiz law print the READ LAW alone
|
|
11
|
+
verdicts: ALLOW / DENIED | exit 0 = allow, 1 = denied, 2 = usage error"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
args = sys.argv[1:]
|
|
16
|
+
if args and args[0] in ("-h", "--help"):
|
|
17
|
+
print(USAGE)
|
|
18
|
+
return 0
|
|
19
|
+
if not args or args[0] not in ("check", "verbs", "law"):
|
|
20
|
+
print(USAGE)
|
|
21
|
+
return 2
|
|
22
|
+
|
|
23
|
+
if args[0] == "law":
|
|
24
|
+
print(LAW_TEXT)
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
if args[0] == "verbs":
|
|
28
|
+
print("[ALLOW]")
|
|
29
|
+
for v in sorted(ALLOW_VERBS):
|
|
30
|
+
print(f" {v}")
|
|
31
|
+
print("[DENY]")
|
|
32
|
+
for v in sorted(DENY_VERBS):
|
|
33
|
+
print(f" {v}")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
# check mode
|
|
37
|
+
if len(args) < 2:
|
|
38
|
+
print("[FAIL] readwiz check requires a quoted command string")
|
|
39
|
+
return 2
|
|
40
|
+
|
|
41
|
+
verdict, reason = check_command(" ".join(args[1:]))
|
|
42
|
+
if verdict == "ALLOW":
|
|
43
|
+
print(f"[ALLOW] {reason}")
|
|
44
|
+
return 0
|
|
45
|
+
print(f"[DENIED] {reason}")
|
|
46
|
+
return 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""read-wizard face — thin CLI over the policy brain."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .read_wizard import check_command, LAW_TEXT, ALLOW_VERBS, DENY_VERBS
|
|
6
|
+
|
|
7
|
+
USAGE = """Read Wizard — read-only mode policy engine (verb gate)
|
|
8
|
+
alias: readwiz
|
|
9
|
+
usage: read_wizard check "<raw shell command>"
|
|
10
|
+
read_wizard verbs dump the allow/deny tables
|
|
11
|
+
read_wizard law print the READ LAW alone
|
|
12
|
+
verdicts: ALLOW / DENIED | exit 0 = allow, 1 = denied, 2 = usage error"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
args = sys.argv[1:]
|
|
17
|
+
if args and args[0] in ("-h", "--help", "help"):
|
|
18
|
+
print(USAGE)
|
|
19
|
+
return 0
|
|
20
|
+
if not args or args[0] not in ("check", "verbs", "law"):
|
|
21
|
+
print(USAGE)
|
|
22
|
+
return 2
|
|
23
|
+
|
|
24
|
+
if args[0] == "law":
|
|
25
|
+
print(LAW_TEXT)
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
if args[0] == "verbs":
|
|
29
|
+
print("[ALLOW]")
|
|
30
|
+
for v in sorted(ALLOW_VERBS):
|
|
31
|
+
print(f" {v}")
|
|
32
|
+
print("[DENY]")
|
|
33
|
+
for v in sorted(DENY_VERBS):
|
|
34
|
+
print(f" {v}")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
# check mode
|
|
38
|
+
if len(args) < 2:
|
|
39
|
+
print("[FAIL] read_wizard check requires a quoted command string")
|
|
40
|
+
return 2
|
|
41
|
+
|
|
42
|
+
verdict, reason = check_command(" ".join(args[1:]))
|
|
43
|
+
if verdict == "ALLOW":
|
|
44
|
+
print(f"[ALLOW] {reason}")
|
|
45
|
+
return 0
|
|
46
|
+
print(f"[DENIED] {reason}")
|
|
47
|
+
return 1
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""read-wizard brain — the read-only policy engine.
|
|
2
|
+
|
|
3
|
+
READ LAW:
|
|
4
|
+
This wizard judges, it never acts. Given a raw shell command,
|
|
5
|
+
it answers ALLOW / DENIED / out of scope. Its tables are the
|
|
6
|
+
single source of truth for read-only mode in every REPL that
|
|
7
|
+
binds it. Mortal and stateless: one verdict per invocation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import shlex
|
|
11
|
+
|
|
12
|
+
# ------------------------------------------------------------------
|
|
13
|
+
# THE VERB TABLES — the law itself. Amend only by Governor decree.
|
|
14
|
+
# ------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
ALLOW_VERBS = frozenset({
|
|
17
|
+
# observation
|
|
18
|
+
"ls", "ll", "la", "cat", "head", "tail", "less", "more", "wc",
|
|
19
|
+
"file", "stat", "du", "df", "tree", "find", "locate", "which",
|
|
20
|
+
"whereis", "type", "echo", "date", "whoami", "hostname", "pwd",
|
|
21
|
+
"uname", "uptime", "env", "printenv", "id", "groups", "history",
|
|
22
|
+
# search
|
|
23
|
+
"grep", "rg", "fgrep", "ag", "awk", "cut", "sort", "uniq", "nl",
|
|
24
|
+
"column", "strings",
|
|
25
|
+
# fleet read-only citizens — other wizards ARE read verbs
|
|
26
|
+
"helpwiz", "pythonwiz", "diffwiz", "jsonwiz", "cmdwiz", "manualwiz",
|
|
27
|
+
"gitwiz", "mapwiz", # Governor decree 2026-09-18 — newborn inspectors admitted
|
|
28
|
+
"gitwiz", "mapwiz", # Governor decree 2026-09-18 — newborn inspectors admitted
|
|
29
|
+
"truthwiz", "ollamawiz", "probewiz", "rustwiz", "readwiz",
|
|
30
|
+
"benchwiz", "modelfilewiz", # exam/show/hash/verify/list only
|
|
31
|
+
# introspection
|
|
32
|
+
"jq", "git", # git gated separately (see GIT_DENY_SUBS)
|
|
33
|
+
"python3", # gated: only with -c check or read scripts
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
# git subverbs that mutate — git itself is allowed, these are not
|
|
37
|
+
GIT_DENY_SUBS = frozenset({
|
|
38
|
+
"push", "commit", "add", "reset", "checkout", "merge", "rebase",
|
|
39
|
+
"cherry-pick", "stash", "clean", "rm", "mv", "apply", "tag",
|
|
40
|
+
"restore", "switch", "worktree",
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# absolute denies — anything touching these dies regardless of position
|
|
44
|
+
DENY_VERBS = frozenset({
|
|
45
|
+
"rm", "rmdir", "mv", "cp", "ln", "touch", "mkdir", "tee", "truncate",
|
|
46
|
+
"dd", "shred", "chmod", "chown", "chattr", "setfacl", "install",
|
|
47
|
+
"kill", "pkill", "shutdown", "reboot", "poweroff", "systemctl",
|
|
48
|
+
"service", "crontab", "useradd", "userdel", "passwd", "sudo", "su",
|
|
49
|
+
"doas", "mount", "umount", "umount", "mkfs", "pip", "pip3", "apt",
|
|
50
|
+
"apt-get", "snap", "npm", "curl", "wget", "scp", "rsync", "ssh",
|
|
51
|
+
"sftp", "nc", "socat", "bootwiz", # birthing citizens is Tier 2
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
# raw-string tokens that mean writing no matter which verb fronts them
|
|
55
|
+
WRITE_MARKERS = (">>", ">", "$(", "`")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _first_verb(segment):
|
|
59
|
+
"""Peel the first word of one pipe-segment, tolerating quoting."""
|
|
60
|
+
try:
|
|
61
|
+
words = shlex.split(segment)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return None
|
|
64
|
+
words = [w for w in words if w and w != "sudo"]
|
|
65
|
+
return words[0] if words else None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_command(raw):
|
|
69
|
+
"""Judge one raw shell line. Returns (verdict, reason).
|
|
70
|
+
|
|
71
|
+
verdict: 'ALLOW' | 'DENIED'
|
|
72
|
+
"""
|
|
73
|
+
if not raw or not raw.strip():
|
|
74
|
+
return "DENIED", "empty command"
|
|
75
|
+
raw = raw.strip()
|
|
76
|
+
|
|
77
|
+
# stage 1 — write markers anywhere in the raw line
|
|
78
|
+
for marker in WRITE_MARKERS:
|
|
79
|
+
if marker in raw:
|
|
80
|
+
return "DENIED", f"write marker '{marker}' present"
|
|
81
|
+
|
|
82
|
+
# stage 2 — judge every segment of every pipe
|
|
83
|
+
for segment in raw.split("|"):
|
|
84
|
+
seg = segment.strip()
|
|
85
|
+
if not seg:
|
|
86
|
+
continue
|
|
87
|
+
verb = _first_verb(seg)
|
|
88
|
+
if verb is None:
|
|
89
|
+
return "DENIED", "unparseable segment"
|
|
90
|
+
# peel path prefixes: /usr/bin/grep -> grep
|
|
91
|
+
verb_clean = verb.rsplit("/", 1)[-1]
|
|
92
|
+
|
|
93
|
+
if verb_clean in DENY_VERBS:
|
|
94
|
+
return "DENIED", f"verb '{verb_clean}' is a write/mutate verb"
|
|
95
|
+
if verb_clean not in ALLOW_VERBS:
|
|
96
|
+
return "DENIED", f"verb '{verb_clean}' not on the read allowlist"
|
|
97
|
+
|
|
98
|
+
# git subverb gate
|
|
99
|
+
if verb_clean in ("git", "/usr/bin/git"):
|
|
100
|
+
try:
|
|
101
|
+
words = shlex.split(seg)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return "DENIED", "unparseable git command"
|
|
104
|
+
subs = [w for w in words[1:] if not w.startswith("-")]
|
|
105
|
+
if subs and subs[0] in GIT_DENY_SUBS:
|
|
106
|
+
return "DENIED", f"git subverb '{subs[0]}' mutates state"
|
|
107
|
+
|
|
108
|
+
return "ALLOW", "read-only"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------- charter text ----------------
|
|
112
|
+
|
|
113
|
+
LAW_TEXT = """READ LAW (Read Wizard, the verb gate)
|
|
114
|
+
1. The Read Wizard judges; it never executes. REPLs bind it as policy engine.
|
|
115
|
+
2. ALLOW means read-only: observation, search, fleet read-only citizens.
|
|
116
|
+
3. Redirection (>, >>), substitution ($(), `) and every write verb: DENIED.
|
|
117
|
+
4. Unknown verbs default DENIED — absence from the table is not permission.
|
|
118
|
+
5. git is read-only ONLY for its observing subverbs (status, log, diff, show...).
|
|
119
|
+
6. Tables amend only by Governor decree, witnessed by commit.
|
|
120
|
+
7. Mortal and stateless: one verdict, then death. No memory of commands seen.
|
|
121
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# wizard package init
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
try:
|
|
3
|
+
from .rust_wizard import LAW_TEXT, RustWizard, package_version
|
|
4
|
+
except ImportError: # run directly as a script, outside the package
|
|
5
|
+
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent))
|
|
6
|
+
from rust_wizard import LAW_TEXT, RustWizard, package_version
|
|
7
|
+
|
|
8
|
+
USAGE = """Rust Wizard — Rust project checker
|
|
9
|
+
alias: rustwiz
|
|
10
|
+
usage: rust_wizard check <file.rs|dir> ...
|
|
11
|
+
rust_wizard build <crate-dir> cargo check the crate
|
|
12
|
+
rust_wizard run <crate-dir> build and run the crate's binary
|
|
13
|
+
rust_wizard test <crate-dir> cargo test
|
|
14
|
+
rust_wizard clippy <crate-dir> cargo clippy lints
|
|
15
|
+
rust_wizard fmt <crate-dir> cargo fmt --check (reports drift only)
|
|
16
|
+
rust_wizard doc <crate-dir> cargo doc --no-deps
|
|
17
|
+
rust_wizard clean <crate-dir> cargo clean (removes target/ only)
|
|
18
|
+
rust_wizard law print the RUST LAW alone
|
|
19
|
+
rust_wizard version print the version
|
|
20
|
+
rust_wizard --help | --version"""
|
|
21
|
+
|
|
22
|
+
CRATE_COMMANDS = ("build", "run", "test", "clippy", "fmt", "doc", "clean")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv=None):
|
|
26
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
27
|
+
if argv and argv[0] in ("-h", "--help", "help"):
|
|
28
|
+
print(USAGE)
|
|
29
|
+
return 0
|
|
30
|
+
if argv and argv[0] == "--version":
|
|
31
|
+
print(f"rust_wizard {package_version()}")
|
|
32
|
+
return 0
|
|
33
|
+
if not argv:
|
|
34
|
+
print(f"out of scope - usage:\n{USAGE}")
|
|
35
|
+
return 2
|
|
36
|
+
cmd, args = argv[0], argv[1:]
|
|
37
|
+
if cmd == "law":
|
|
38
|
+
print(LAW_TEXT)
|
|
39
|
+
return 0
|
|
40
|
+
if cmd == "version":
|
|
41
|
+
print(f"rust_wizard {package_version()}")
|
|
42
|
+
return 0
|
|
43
|
+
wz = RustWizard()
|
|
44
|
+
if cmd == "check" and args:
|
|
45
|
+
return wz.check(args)
|
|
46
|
+
if cmd in CRATE_COMMANDS and args:
|
|
47
|
+
return getattr(wz, cmd)(args[0])
|
|
48
|
+
print(f"out of scope - unknown or incomplete command: {' '.join(argv)}")
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
sys.exit(main())
|