agent-runtime-map 0.8.0
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 +21 -0
- package/README.md +40 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/dist/cli.js +195 -0
- package/dist/licenses/classcat.txt +7 -0
- package/dist/licenses/d3-color.txt +13 -0
- package/dist/licenses/d3-dispatch.txt +13 -0
- package/dist/licenses/d3-drag.txt +13 -0
- package/dist/licenses/d3-ease.txt +28 -0
- package/dist/licenses/d3-interpolate.txt +13 -0
- package/dist/licenses/d3-selection.txt +13 -0
- package/dist/licenses/d3-timer.txt +13 -0
- package/dist/licenses/d3-transition.txt +13 -0
- package/dist/licenses/d3-zoom.txt +13 -0
- package/dist/licenses/elkjs.txt +264 -0
- package/dist/licenses/lucide-react.txt +15 -0
- package/dist/licenses/react-dom.txt +21 -0
- package/dist/licenses/react.txt +21 -0
- package/dist/licenses/scheduler.txt +21 -0
- package/dist/licenses/use-sync-external-store.txt +21 -0
- package/dist/licenses/xyflow__react.txt +21 -0
- package/dist/licenses/xyflow__system.txt +21 -0
- package/dist/licenses/zustand.txt +21 -0
- package/dist/python/extract.py +219 -0
- package/dist/viewer/assets/elk-DSSKxAlM.js +24 -0
- package/dist/viewer/assets/index-4tql6hvj.js +3 -0
- package/dist/viewer/assets/index-Dvsvzpjt.css +1 -0
- package/dist/viewer/assets/react-C8zC5XaP.js +9 -0
- package/dist/viewer/assets/xyflow-BnuhLJ6X.css +1 -0
- package/dist/viewer/assets/xyflow-NEmtYq4w.js +7 -0
- package/dist/viewer/index.html +17 -0
- package/package.json +54 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Extracts structural facts from Python sources for Agent Runtime Map.
|
|
2
|
+
|
|
3
|
+
This parses with the standard library `ast` module and never executes the code it
|
|
4
|
+
reads: `ast.parse` builds a tree, it does not import or run anything. It also makes
|
|
5
|
+
no product judgements. It reports what is written — definitions, calls, imports,
|
|
6
|
+
decorators, string constants — and the TypeScript side decides what any of it means,
|
|
7
|
+
so the classification rules stay in one language rather than drifting across two.
|
|
8
|
+
|
|
9
|
+
Input: a JSON array of absolute file paths on stdin.
|
|
10
|
+
Output: a JSON object of facts on stdout.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import ast
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def literal(node):
|
|
19
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
20
|
+
return node.value
|
|
21
|
+
if isinstance(node, ast.JoinedStr):
|
|
22
|
+
parts = [p.value for p in node.values if isinstance(p, ast.Constant) and isinstance(p.value, str)]
|
|
23
|
+
return "".join(parts) if parts else None
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def dotted_name(node):
|
|
28
|
+
"""`a.b.c` and `a.b.c()` both reduce to the text a reader would recognise."""
|
|
29
|
+
if isinstance(node, ast.Name):
|
|
30
|
+
return node.id
|
|
31
|
+
if isinstance(node, ast.Attribute):
|
|
32
|
+
base = dotted_name(node.value)
|
|
33
|
+
return f"{base}.{node.attr}" if base else node.attr
|
|
34
|
+
if isinstance(node, ast.Call):
|
|
35
|
+
return dotted_name(node.func)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def keyword_map(call):
|
|
40
|
+
"""Named arguments, which is how every Agent framework configures a construct."""
|
|
41
|
+
out = {}
|
|
42
|
+
for kw in call.keywords:
|
|
43
|
+
if kw.arg is None:
|
|
44
|
+
continue
|
|
45
|
+
text = literal(kw.value)
|
|
46
|
+
if text is not None:
|
|
47
|
+
out[kw.arg] = {"kind": "string", "value": text[:4000]}
|
|
48
|
+
else:
|
|
49
|
+
names = [n for n in (dotted_name(e) for e in flatten(kw.value)) if n]
|
|
50
|
+
out[kw.arg] = {"kind": "names", "value": names[:24]}
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def flatten(node):
|
|
55
|
+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
|
|
56
|
+
return list(node.elts)
|
|
57
|
+
if isinstance(node, ast.Dict):
|
|
58
|
+
return [v for v in node.values]
|
|
59
|
+
return [node]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def end_line(node):
|
|
63
|
+
return getattr(node, "end_lineno", None) or node.lineno
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FileVisitor(ast.NodeVisitor):
|
|
67
|
+
def __init__(self, path):
|
|
68
|
+
self.path = path
|
|
69
|
+
self.functions = []
|
|
70
|
+
self.classes = []
|
|
71
|
+
self.assignments = []
|
|
72
|
+
self.imports = []
|
|
73
|
+
self.calls = []
|
|
74
|
+
self._scope = []
|
|
75
|
+
|
|
76
|
+
# A scope stack is what lets a call be attributed to the definition it sits in.
|
|
77
|
+
def _push(self, name, kind, node, enclosing_class):
|
|
78
|
+
decorators = [d for d in (dotted_name(x) for x in node.decorator_list) if d]
|
|
79
|
+
record = {
|
|
80
|
+
"name": name,
|
|
81
|
+
"kind": kind,
|
|
82
|
+
"line": node.lineno,
|
|
83
|
+
"endLine": end_line(node),
|
|
84
|
+
"enclosingClass": enclosing_class,
|
|
85
|
+
"decorators": decorators,
|
|
86
|
+
"isAsync": isinstance(node, ast.AsyncFunctionDef),
|
|
87
|
+
"docstring": (ast.get_docstring(node) or "")[:2000],
|
|
88
|
+
"parameters": [a.arg for a in node.args.args],
|
|
89
|
+
"returns": dotted_name(node.returns) if node.returns else None,
|
|
90
|
+
"branches": 0,
|
|
91
|
+
"loops": 0,
|
|
92
|
+
"catches": 0,
|
|
93
|
+
}
|
|
94
|
+
for child in ast.walk(node):
|
|
95
|
+
if isinstance(child, (ast.If, ast.IfExp, ast.Match)):
|
|
96
|
+
record["branches"] += 1
|
|
97
|
+
elif isinstance(child, (ast.For, ast.AsyncFor, ast.While)):
|
|
98
|
+
record["loops"] += 1
|
|
99
|
+
elif isinstance(child, ast.ExceptHandler):
|
|
100
|
+
record["catches"] += 1
|
|
101
|
+
self.functions.append(record)
|
|
102
|
+
return record
|
|
103
|
+
|
|
104
|
+
def _visit_function(self, node):
|
|
105
|
+
enclosing = self._scope[-1]["name"] if self._scope and self._scope[-1]["kind"] == "class" else None
|
|
106
|
+
record = self._push(node.name, "function", node, enclosing)
|
|
107
|
+
self._scope.append({"name": node.name, "kind": "function", "record": record})
|
|
108
|
+
self.generic_visit(node)
|
|
109
|
+
self._scope.pop()
|
|
110
|
+
|
|
111
|
+
visit_FunctionDef = _visit_function
|
|
112
|
+
visit_AsyncFunctionDef = _visit_function
|
|
113
|
+
|
|
114
|
+
def visit_ClassDef(self, node):
|
|
115
|
+
self.classes.append({
|
|
116
|
+
"name": node.name,
|
|
117
|
+
"line": node.lineno,
|
|
118
|
+
"endLine": end_line(node),
|
|
119
|
+
"bases": [b for b in (dotted_name(x) for x in node.bases) if b],
|
|
120
|
+
"decorators": [d for d in (dotted_name(x) for x in node.decorator_list) if d],
|
|
121
|
+
"docstring": (ast.get_docstring(node) or "")[:2000],
|
|
122
|
+
})
|
|
123
|
+
self._scope.append({"name": node.name, "kind": "class", "record": None})
|
|
124
|
+
self.generic_visit(node)
|
|
125
|
+
self._scope.pop()
|
|
126
|
+
|
|
127
|
+
def visit_Assign(self, node):
|
|
128
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
129
|
+
self._record_assignment(node.targets[0].id, node.value, node)
|
|
130
|
+
self.generic_visit(node)
|
|
131
|
+
|
|
132
|
+
def visit_AnnAssign(self, node):
|
|
133
|
+
if isinstance(node.target, ast.Name) and node.value is not None:
|
|
134
|
+
self._record_assignment(node.target.id, node.value, node)
|
|
135
|
+
self.generic_visit(node)
|
|
136
|
+
|
|
137
|
+
def _record_assignment(self, name, value, node):
|
|
138
|
+
text = literal(value)
|
|
139
|
+
call = value if isinstance(value, ast.Call) else None
|
|
140
|
+
if isinstance(value, ast.Await) and isinstance(value.value, ast.Call):
|
|
141
|
+
call = value.value
|
|
142
|
+
self.assignments.append({
|
|
143
|
+
"name": name,
|
|
144
|
+
"line": node.lineno,
|
|
145
|
+
"endLine": end_line(node),
|
|
146
|
+
"scope": self._scope[-1]["name"] if self._scope else None,
|
|
147
|
+
"text": text[:4000] if text else None,
|
|
148
|
+
"factory": dotted_name(call.func) if call else None,
|
|
149
|
+
"options": keyword_map(call) if call else {},
|
|
150
|
+
"elements": [n for n in (dotted_name(e) for e in flatten(value)) if n] if isinstance(value, (ast.List, ast.Tuple)) else [],
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
def visit_Import(self, node):
|
|
154
|
+
for alias in node.names:
|
|
155
|
+
self.imports.append({"module": alias.name, "name": None, "alias": alias.asname, "line": node.lineno})
|
|
156
|
+
self.generic_visit(node)
|
|
157
|
+
|
|
158
|
+
def visit_ImportFrom(self, node):
|
|
159
|
+
for alias in node.names:
|
|
160
|
+
self.imports.append({
|
|
161
|
+
"module": node.module or "",
|
|
162
|
+
"name": alias.name,
|
|
163
|
+
"alias": alias.asname,
|
|
164
|
+
"level": node.level,
|
|
165
|
+
"line": node.lineno,
|
|
166
|
+
})
|
|
167
|
+
self.generic_visit(node)
|
|
168
|
+
|
|
169
|
+
def visit_Call(self, node):
|
|
170
|
+
callee = dotted_name(node.func)
|
|
171
|
+
if callee:
|
|
172
|
+
enclosing = None
|
|
173
|
+
for frame in reversed(self._scope):
|
|
174
|
+
if frame["kind"] == "function":
|
|
175
|
+
enclosing = frame["name"]
|
|
176
|
+
break
|
|
177
|
+
self.calls.append({
|
|
178
|
+
"callee": callee,
|
|
179
|
+
"line": node.lineno,
|
|
180
|
+
"enclosingFunction": enclosing,
|
|
181
|
+
"enclosingClass": next((f["name"] for f in reversed(self._scope) if f["kind"] == "class"), None),
|
|
182
|
+
"stringArguments": [a for a in (literal(x) for x in node.args) if a is not None][:4],
|
|
183
|
+
"nameArguments": [n for n in (dotted_name(a) for a in node.args) if n][:8],
|
|
184
|
+
"options": keyword_map(node),
|
|
185
|
+
})
|
|
186
|
+
self.generic_visit(node)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def analyze(path):
|
|
190
|
+
try:
|
|
191
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
192
|
+
source = handle.read()
|
|
193
|
+
except OSError as error:
|
|
194
|
+
return {"path": path, "error": f"unreadable: {error}"}
|
|
195
|
+
try:
|
|
196
|
+
tree = ast.parse(source, filename=path)
|
|
197
|
+
except SyntaxError as error:
|
|
198
|
+
# A file the interpreter itself cannot parse is reported, not guessed at.
|
|
199
|
+
return {"path": path, "error": f"syntax error on line {error.lineno}"}
|
|
200
|
+
visitor = FileVisitor(path)
|
|
201
|
+
visitor.visit(tree)
|
|
202
|
+
return {
|
|
203
|
+
"path": path,
|
|
204
|
+
"docstring": (ast.get_docstring(tree) or "")[:2000],
|
|
205
|
+
"functions": visitor.functions,
|
|
206
|
+
"classes": visitor.classes,
|
|
207
|
+
"assignments": visitor.assignments,
|
|
208
|
+
"imports": visitor.imports,
|
|
209
|
+
"calls": visitor.calls,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main():
|
|
214
|
+
paths = json.load(sys.stdin)
|
|
215
|
+
json.dump({"pythonVersion": sys.version.split()[0], "files": [analyze(p) for p in paths]}, sys.stdout)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|