@evoclock/pi-agentic-driver 0.4.3
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.
- package/LICENSE +736 -0
- package/PROVENANCE.md +69 -0
- package/README.md +325 -0
- package/config/herdr-worker-repositories.v1.json +9 -0
- package/extensions/aidr.ts +5 -0
- package/extensions/code-phage.js +144 -0
- package/extensions/herdr-communication.ts +7 -0
- package/extensions/herdr-lifecycle.ts +7 -0
- package/extensions/linux-microvm.ts +10 -0
- package/lib/adapters/diff-scope.mjs +148 -0
- package/lib/adapters/evidence.mjs +151 -0
- package/lib/adapters/narrative.mjs +171 -0
- package/lib/adapters/review-feedback.mjs +77 -0
- package/lib/adapters/visualization.mjs +176 -0
- package/lib/code-phage-core.mjs +882 -0
- package/lib/python_ast_metrics.py +378 -0
- package/lib/typescript_ast_metrics.mjs +441 -0
- package/package.json +50 -0
- package/scripts/aidr_writing_review.js +468 -0
- package/scripts/enforcement/herdr_communication_pi.js +1198 -0
- package/scripts/enforcement/herdr_lifecycle_pi.js +902 -0
- package/scripts/enforcement/linux_microvm_cutover_pi.js +328 -0
- package/scripts/enforcement/linux_microvm_remote_fixture.sh +366 -0
- package/scripts/enforcement/native_tui_context.js +11 -0
- package/templates/AGENTS.md +72 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
#!/usr/bin/env python3
|
|
5
|
+
"""AST-backed complexity metrics for code-phage.
|
|
6
|
+
|
|
7
|
+
The process accepts one JSON object on stdin and emits one JSON object on
|
|
8
|
+
stdout. It reads no files and writes no files; the JavaScript caller supplies
|
|
9
|
+
source text after enforcing repository containment.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import io
|
|
16
|
+
import json
|
|
17
|
+
import tokenize
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LOGICAL_NODES = (ast.And, ast.Or)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Metric:
|
|
27
|
+
name: str
|
|
28
|
+
kind: str
|
|
29
|
+
line: int
|
|
30
|
+
end_line: int = 0
|
|
31
|
+
cyclomatic: int = 1
|
|
32
|
+
cognitive: int = 0
|
|
33
|
+
decisions: list[dict] = field(default_factory=list)
|
|
34
|
+
logical_sequences: list[dict] = field(default_factory=list)
|
|
35
|
+
recursive_calls: int = 0
|
|
36
|
+
max_nesting: int = 0
|
|
37
|
+
|
|
38
|
+
def as_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"name": self.name,
|
|
41
|
+
"kind": self.kind,
|
|
42
|
+
"line": self.line,
|
|
43
|
+
"endLine": self.end_line,
|
|
44
|
+
"cyclomaticComplexity": self.cyclomatic,
|
|
45
|
+
"cognitiveComplexity": self.cognitive,
|
|
46
|
+
"decisionPoints": self.decisions,
|
|
47
|
+
"logicalSequences": self.logical_sequences,
|
|
48
|
+
"recursiveCalls": self.recursive_calls,
|
|
49
|
+
"maxNesting": self.max_nesting,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ComplexityVisitor(ast.NodeVisitor):
|
|
54
|
+
def __init__(self, metric: Metric, current_name: str, root: ast.AST):
|
|
55
|
+
self.metric = metric
|
|
56
|
+
self.current_name = current_name
|
|
57
|
+
self.root = root
|
|
58
|
+
self.nesting = 0
|
|
59
|
+
|
|
60
|
+
def _decision(self, node: ast.AST, kind: str, cognitive_nesting: int | None = None) -> None:
|
|
61
|
+
nesting = self.nesting if cognitive_nesting is None else cognitive_nesting
|
|
62
|
+
self.metric.cyclomatic += 1
|
|
63
|
+
self.metric.cognitive += 1 + nesting
|
|
64
|
+
self.metric.max_nesting = max(self.metric.max_nesting, self.nesting)
|
|
65
|
+
self.metric.decisions.append({
|
|
66
|
+
"kind": kind,
|
|
67
|
+
"line": getattr(node, "lineno", 0),
|
|
68
|
+
"nesting": self.nesting,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
def _visit_sequence(self, nodes: list[ast.AST], nesting: int | None = None) -> None:
|
|
72
|
+
previous = self.nesting
|
|
73
|
+
if nesting is not None:
|
|
74
|
+
self.nesting = nesting
|
|
75
|
+
self.metric.max_nesting = max(self.metric.max_nesting, self.nesting)
|
|
76
|
+
for node in nodes:
|
|
77
|
+
self.visit(node)
|
|
78
|
+
self.nesting = previous
|
|
79
|
+
|
|
80
|
+
def visit_If(self, node: ast.If) -> None:
|
|
81
|
+
self._decision(node, "if", 0 if getattr(node, "_code_phage_elif", False) else None)
|
|
82
|
+
self.visit(node.test)
|
|
83
|
+
self._visit_sequence(node.body, self.nesting + 1)
|
|
84
|
+
if not node.orelse:
|
|
85
|
+
return
|
|
86
|
+
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
|
|
87
|
+
child = node.orelse[0]
|
|
88
|
+
child._code_phage_elif = True
|
|
89
|
+
self.visit(child)
|
|
90
|
+
return
|
|
91
|
+
self.metric.cognitive += 1
|
|
92
|
+
self.metric.decisions.append({"kind": "else", "line": getattr(node.orelse[0], "lineno", 0), "nesting": self.nesting})
|
|
93
|
+
self._visit_sequence(node.orelse, self.nesting + 1)
|
|
94
|
+
|
|
95
|
+
def _visit_loop(self, node: ast.AST, kind: str, fields: tuple[str, ...]) -> None:
|
|
96
|
+
self._decision(node, kind)
|
|
97
|
+
for field_name in fields:
|
|
98
|
+
value = getattr(node, field_name, None)
|
|
99
|
+
if isinstance(value, ast.AST):
|
|
100
|
+
self.visit(value)
|
|
101
|
+
elif isinstance(value, list):
|
|
102
|
+
for child in value:
|
|
103
|
+
if isinstance(child, ast.AST):
|
|
104
|
+
self.visit(child)
|
|
105
|
+
body = getattr(node, "body", [])
|
|
106
|
+
self._visit_sequence(body, self.nesting + 1)
|
|
107
|
+
orelse = getattr(node, "orelse", [])
|
|
108
|
+
if orelse:
|
|
109
|
+
self._visit_sequence(orelse, self.nesting + 1)
|
|
110
|
+
|
|
111
|
+
def visit_For(self, node: ast.For) -> None:
|
|
112
|
+
self._visit_loop(node, "for", ("target", "iter"))
|
|
113
|
+
|
|
114
|
+
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
|
|
115
|
+
self._visit_loop(node, "async-for", ("target", "iter"))
|
|
116
|
+
|
|
117
|
+
def visit_While(self, node: ast.While) -> None:
|
|
118
|
+
self._decision(node, "while")
|
|
119
|
+
self.visit(node.test)
|
|
120
|
+
self._visit_sequence(node.body, self.nesting + 1)
|
|
121
|
+
if node.orelse:
|
|
122
|
+
self._visit_sequence(node.orelse, self.nesting + 1)
|
|
123
|
+
|
|
124
|
+
def visit_Try(self, node: ast.Try) -> None:
|
|
125
|
+
self._visit_sequence(node.body, self.nesting)
|
|
126
|
+
for handler in node.handlers:
|
|
127
|
+
self._decision(handler, "except")
|
|
128
|
+
self._visit_sequence(handler.body, self.nesting + 1)
|
|
129
|
+
self._visit_sequence(node.orelse, self.nesting)
|
|
130
|
+
self._visit_sequence(node.finalbody, self.nesting)
|
|
131
|
+
|
|
132
|
+
def visit_TryStar(self, node: ast.TryStar) -> None:
|
|
133
|
+
self.visit_Try(node)
|
|
134
|
+
|
|
135
|
+
def visit_Match(self, node: ast.Match) -> None:
|
|
136
|
+
self.metric.cognitive += 1 + self.nesting
|
|
137
|
+
self.metric.max_nesting = max(self.metric.max_nesting, self.nesting)
|
|
138
|
+
self.metric.decisions.append({"kind": "match", "line": getattr(node, "lineno", 0), "nesting": self.nesting})
|
|
139
|
+
self.visit(node.subject)
|
|
140
|
+
for case in node.cases:
|
|
141
|
+
if not _is_wildcard_case(case):
|
|
142
|
+
self.metric.cyclomatic += 1
|
|
143
|
+
self.metric.decisions.append({"kind": "case", "line": getattr(case, "lineno", 0), "nesting": self.nesting + 1})
|
|
144
|
+
self._visit_sequence([case.pattern], self.nesting + 1)
|
|
145
|
+
if case.guard:
|
|
146
|
+
self.visit(case.guard)
|
|
147
|
+
self._visit_sequence(case.body, self.nesting + 1)
|
|
148
|
+
|
|
149
|
+
def visit_IfExp(self, node: ast.IfExp) -> None:
|
|
150
|
+
self._decision(node, "conditional-expression")
|
|
151
|
+
self.visit(node.test)
|
|
152
|
+
self._visit_sequence([node.body, node.orelse], self.nesting + 1)
|
|
153
|
+
|
|
154
|
+
def visit_BoolOp(self, node: ast.BoolOp) -> None:
|
|
155
|
+
operators = [type(node.op).__name__.lower()] * max(0, len(node.values) - 1)
|
|
156
|
+
self.metric.cyclomatic += len(operators)
|
|
157
|
+
if operators:
|
|
158
|
+
self.metric.cognitive += 1
|
|
159
|
+
self.metric.logical_sequences.append({
|
|
160
|
+
"line": getattr(node, "lineno", 0),
|
|
161
|
+
"operators": operators,
|
|
162
|
+
"cognitiveIncrement": 1,
|
|
163
|
+
})
|
|
164
|
+
for value in node.values:
|
|
165
|
+
self.visit(value)
|
|
166
|
+
|
|
167
|
+
def visit_comprehension(self, node: ast.comprehension) -> None:
|
|
168
|
+
self.metric.cyclomatic += 1
|
|
169
|
+
self.metric.cognitive += 1 + self.nesting
|
|
170
|
+
self.metric.max_nesting = max(self.metric.max_nesting, self.nesting)
|
|
171
|
+
self.metric.decisions.append({"kind": "comprehension-for", "line": getattr(node, "lineno", 0), "nesting": self.nesting})
|
|
172
|
+
self.visit(node.target)
|
|
173
|
+
self.visit(node.iter)
|
|
174
|
+
for condition in node.ifs:
|
|
175
|
+
self.metric.cyclomatic += 1
|
|
176
|
+
self.metric.cognitive += 1 + self.nesting
|
|
177
|
+
self.metric.decisions.append({"kind": "comprehension-if", "line": getattr(condition, "lineno", 0), "nesting": self.nesting})
|
|
178
|
+
self.visit(condition)
|
|
179
|
+
|
|
180
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
181
|
+
if self.current_name != "<module>" and isinstance(node.func, ast.Name) and node.func.id == self.current_name:
|
|
182
|
+
self.metric.cognitive += 1
|
|
183
|
+
self.metric.recursive_calls += 1
|
|
184
|
+
self.generic_visit(node)
|
|
185
|
+
|
|
186
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
187
|
+
if node is not self.root:
|
|
188
|
+
return
|
|
189
|
+
self.generic_visit(node)
|
|
190
|
+
|
|
191
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
192
|
+
if node is not self.root:
|
|
193
|
+
return
|
|
194
|
+
self.generic_visit(node)
|
|
195
|
+
|
|
196
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
197
|
+
if node is not self.root:
|
|
198
|
+
return
|
|
199
|
+
self.generic_visit(node)
|
|
200
|
+
|
|
201
|
+
def visit_Break(self, node: ast.Break) -> None:
|
|
202
|
+
self.generic_visit(node)
|
|
203
|
+
|
|
204
|
+
def visit_Continue(self, node: ast.Continue) -> None:
|
|
205
|
+
self.generic_visit(node)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _is_wildcard_case(case: ast.match_case) -> bool:
|
|
209
|
+
return isinstance(case.pattern, ast.MatchAs) and case.pattern.pattern is None and case.guard is None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _callables(tree: ast.AST):
|
|
213
|
+
found: list[tuple[ast.AST, str, str, int, int]] = []
|
|
214
|
+
|
|
215
|
+
class Collector(ast.NodeVisitor):
|
|
216
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
217
|
+
found.append((node, node.name, "function", node.lineno, node.end_lineno or node.lineno))
|
|
218
|
+
self.generic_visit(node)
|
|
219
|
+
|
|
220
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
221
|
+
found.append((node, node.name, "async-function", node.lineno, node.end_lineno or node.lineno))
|
|
222
|
+
self.generic_visit(node)
|
|
223
|
+
|
|
224
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
225
|
+
found.append((node, "<lambda>", "lambda", node.lineno, node.end_lineno or node.lineno))
|
|
226
|
+
self.generic_visit(node)
|
|
227
|
+
|
|
228
|
+
Collector().visit(tree)
|
|
229
|
+
return found
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _metric_for(root: ast.AST, name: str, kind: str, line: int, end_line: int = 0) -> Metric:
|
|
233
|
+
metric = Metric(name=name, kind=kind, line=line, end_line=end_line)
|
|
234
|
+
ComplexityVisitor(metric, name, root).visit(root)
|
|
235
|
+
return metric
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _parameter_count(node: ast.FunctionDef | ast.AsyncFunctionDef) -> int:
|
|
239
|
+
args = node.args
|
|
240
|
+
return (
|
|
241
|
+
len(getattr(args, "posonlyargs", []))
|
|
242
|
+
+ len(args.args)
|
|
243
|
+
+ len(args.kwonlyargs)
|
|
244
|
+
+ int(args.vararg is not None)
|
|
245
|
+
+ int(args.kwarg is not None)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _static_export_names(node: ast.AST) -> list[str] | None:
|
|
250
|
+
"""Return a literal ``__all__`` value, or None when it is dynamic."""
|
|
251
|
+
if not isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
252
|
+
return None
|
|
253
|
+
names = []
|
|
254
|
+
for item in node.elts:
|
|
255
|
+
if not isinstance(item, ast.Constant) or not isinstance(item.value, str):
|
|
256
|
+
return None
|
|
257
|
+
names.append(item.value)
|
|
258
|
+
return names
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _code_facts(tree: ast.Module, purpose: str | None) -> dict:
|
|
262
|
+
"""Extract conservative, execution-free facts for structural prior-art checks."""
|
|
263
|
+
functions: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
|
|
264
|
+
public_names: list[str] = []
|
|
265
|
+
explicit_exports: list[str] | None = None
|
|
266
|
+
dependencies: list[str] = []
|
|
267
|
+
|
|
268
|
+
for node in tree.body:
|
|
269
|
+
if isinstance(node, ast.Import):
|
|
270
|
+
dependencies.extend(alias.name for alias in node.names)
|
|
271
|
+
elif isinstance(node, ast.ImportFrom):
|
|
272
|
+
prefix = "." * node.level
|
|
273
|
+
dependencies.append(prefix + (node.module or ""))
|
|
274
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
275
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
276
|
+
functions[node.name] = node
|
|
277
|
+
if not node.name.startswith("_"):
|
|
278
|
+
public_names.append(node.name)
|
|
279
|
+
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
|
|
280
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
281
|
+
for target in targets:
|
|
282
|
+
if not isinstance(target, ast.Name):
|
|
283
|
+
continue
|
|
284
|
+
if target.id == "__all__":
|
|
285
|
+
value = node.value if isinstance(node, ast.Assign) else node.value
|
|
286
|
+
explicit_exports = _static_export_names(value) if value is not None else None
|
|
287
|
+
elif not target.id.startswith("_"):
|
|
288
|
+
public_names.append(target.id)
|
|
289
|
+
|
|
290
|
+
exported = explicit_exports if explicit_exports is not None else public_names
|
|
291
|
+
exported = list(dict.fromkeys(exported))
|
|
292
|
+
exported_functions = [
|
|
293
|
+
{"name": name, "parameterCount": _parameter_count(functions[name])}
|
|
294
|
+
for name in exported
|
|
295
|
+
if name in functions
|
|
296
|
+
]
|
|
297
|
+
return {
|
|
298
|
+
"exportedSymbols": exported[:100],
|
|
299
|
+
"exportedFunctions": exported_functions[:50],
|
|
300
|
+
"dependencies": list(dict.fromkeys(dependencies))[:50],
|
|
301
|
+
"purpose": purpose,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _line_counts(source: str) -> dict:
|
|
306
|
+
lines = source.splitlines() or [""]
|
|
307
|
+
comment_lines = set()
|
|
308
|
+
try:
|
|
309
|
+
for token in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
310
|
+
if token.type == tokenize.COMMENT:
|
|
311
|
+
comment_lines.update(range(token.start[0], token.end[0] + 1))
|
|
312
|
+
except (IndentationError, tokenize.TokenError):
|
|
313
|
+
pass
|
|
314
|
+
blank_lines = {index for index, line in enumerate(lines, start=1) if not line.strip()}
|
|
315
|
+
code_lines = len(lines) - len(comment_lines | blank_lines)
|
|
316
|
+
return {
|
|
317
|
+
"lines": len(lines),
|
|
318
|
+
"codeLines": code_lines,
|
|
319
|
+
"commentLines": len(comment_lines - blank_lines),
|
|
320
|
+
"blankLines": len(blank_lines),
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def analyze(payload: dict) -> dict:
|
|
325
|
+
source = payload["source"]
|
|
326
|
+
path = payload["path"]
|
|
327
|
+
try:
|
|
328
|
+
tree = ast.parse(source, filename=path, mode="exec")
|
|
329
|
+
except (SyntaxError, ValueError) as error:
|
|
330
|
+
return {
|
|
331
|
+
"status": "parse-error",
|
|
332
|
+
"path": path,
|
|
333
|
+
"language": "python",
|
|
334
|
+
"method": "ast-v1",
|
|
335
|
+
"parser": "python.ast",
|
|
336
|
+
"parseErrors": 1,
|
|
337
|
+
"error": f"Source contains syntax errors; complexity metrics are unavailable: {error.msg if isinstance(error, SyntaxError) else error}",
|
|
338
|
+
**_line_counts(source),
|
|
339
|
+
}
|
|
340
|
+
module = _metric_for(tree, "<module>", "module", 1)
|
|
341
|
+
callables = [_metric_for(node, name, kind, line, end_line).as_dict() for node, name, kind, line, end_line in _callables(tree)]
|
|
342
|
+
all_metrics = [module.as_dict(), *callables]
|
|
343
|
+
dependencies = sum(isinstance(node, (ast.Import, ast.ImportFrom)) for node in ast.walk(tree))
|
|
344
|
+
total_cyclomatic = sum(item["cyclomaticComplexity"] for item in all_metrics)
|
|
345
|
+
total_cognitive = sum(item["cognitiveComplexity"] for item in all_metrics)
|
|
346
|
+
counts = _line_counts(source)
|
|
347
|
+
return {
|
|
348
|
+
"status": "parsed",
|
|
349
|
+
"path": path,
|
|
350
|
+
"language": "python",
|
|
351
|
+
"method": "ast-v1",
|
|
352
|
+
"parser": "python.ast",
|
|
353
|
+
**counts,
|
|
354
|
+
"module": module.as_dict(),
|
|
355
|
+
"callables": callables,
|
|
356
|
+
"codeFacts": _code_facts(tree, ast.get_docstring(tree)),
|
|
357
|
+
"cyclomaticComplexity": max(item["cyclomaticComplexity"] for item in all_metrics),
|
|
358
|
+
"cognitiveComplexity": max(item["cognitiveComplexity"] for item in all_metrics),
|
|
359
|
+
"totalCyclomaticComplexity": total_cyclomatic,
|
|
360
|
+
"totalCognitiveComplexity": total_cognitive,
|
|
361
|
+
"dependencyCount": dependencies,
|
|
362
|
+
"uncertainty": "AST-derived for Python syntax; metric semantics follow the documented code-phage rules and are not a universal quality gate.",
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def main() -> int:
|
|
367
|
+
try:
|
|
368
|
+
payload = json.load(sys.stdin)
|
|
369
|
+
result = analyze(payload)
|
|
370
|
+
except Exception as error: # pragma: no cover - defensive process boundary
|
|
371
|
+
result = {"status": "unavailable", "error": str(error)}
|
|
372
|
+
json.dump(result, sys.stdout, separators=(",", ":"))
|
|
373
|
+
sys.stdout.write("\n")
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
raise SystemExit(main())
|