jac-coder 0.1.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.
- jac_coder/__init__.jac +0 -0
- jac_coder/api.jac +82 -0
- jac_coder/cli_entry.py +25 -0
- jac_coder/config.jac +36 -0
- jac_coder/context.jac +17 -0
- jac_coder/data/examples/ai_agent.md +90 -0
- jac_coder/data/examples/blog_app.md +386 -0
- jac_coder/data/examples/core_patterns.md +321 -0
- jac_coder/data/examples/todo_app.md +321 -0
- jac_coder/data/reference/ai.md +131 -0
- jac_coder/data/reference/backend.md +215 -0
- jac_coder/data/reference/frontend.md +271 -0
- jac_coder/data/reference/osp.md +229 -0
- jac_coder/data/reference/pitfalls.md +141 -0
- jac_coder/data/reference/syntax.md +159 -0
- jac_coder/data/rules/core_jac.md +559 -0
- jac_coder/data/rules/fullstack.md +362 -0
- jac_coder/data/rules/workflow.md +88 -0
- jac_coder/events.jac +110 -0
- jac_coder/impl/api.impl.jac +399 -0
- jac_coder/impl/config.impl.jac +163 -0
- jac_coder/impl/context.impl.jac +117 -0
- jac_coder/impl/mcp_manager.impl.jac +380 -0
- jac_coder/impl/memory.impl.jac +247 -0
- jac_coder/impl/nodes.impl.jac +259 -0
- jac_coder/impl/permission.impl.jac +62 -0
- jac_coder/impl/walkers.impl.jac +298 -0
- jac_coder/mcp_manager.jac +35 -0
- jac_coder/memory.jac +15 -0
- jac_coder/nodes.jac +306 -0
- jac_coder/permission.jac +19 -0
- jac_coder/serve_entry.jac +30 -0
- jac_coder/server.jac +324 -0
- jac_coder/tool/__init__.jac +17 -0
- jac_coder/tool/checked.jac +10 -0
- jac_coder/tool/delegation.jac +23 -0
- jac_coder/tool/filesystem.jac +25 -0
- jac_coder/tool/git.jac +18 -0
- jac_coder/tool/guarded.jac +23 -0
- jac_coder/tool/impl/checked.impl.jac +38 -0
- jac_coder/tool/impl/delegation.impl.jac +157 -0
- jac_coder/tool/impl/filesystem.impl.jac +781 -0
- jac_coder/tool/impl/git.impl.jac +115 -0
- jac_coder/tool/impl/guarded.impl.jac +72 -0
- jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
- jac_coder/tool/impl/jac_docs.impl.jac +136 -0
- jac_coder/tool/impl/jac_tools.impl.jac +79 -0
- jac_coder/tool/impl/mcp.impl.jac +32 -0
- jac_coder/tool/impl/preview.impl.jac +233 -0
- jac_coder/tool/impl/question.impl.jac +29 -0
- jac_coder/tool/impl/scaffold.impl.jac +231 -0
- jac_coder/tool/impl/search.impl.jac +85 -0
- jac_coder/tool/impl/shell.impl.jac +89 -0
- jac_coder/tool/impl/task.impl.jac +12 -0
- jac_coder/tool/impl/think.impl.jac +4 -0
- jac_coder/tool/impl/todo.impl.jac +58 -0
- jac_coder/tool/impl/validate.impl.jac +236 -0
- jac_coder/tool/impl/web.impl.jac +91 -0
- jac_coder/tool/jac_analyzer.jac +21 -0
- jac_coder/tool/jac_docs.jac +9 -0
- jac_coder/tool/jac_tools.jac +11 -0
- jac_coder/tool/mcp.jac +17 -0
- jac_coder/tool/preview.jac +31 -0
- jac_coder/tool/question.jac +7 -0
- jac_coder/tool/scaffold.jac +10 -0
- jac_coder/tool/search.jac +14 -0
- jac_coder/tool/shell.jac +12 -0
- jac_coder/tool/task.jac +9 -0
- jac_coder/tool/think.jac +5 -0
- jac_coder/tool/todo.jac +12 -0
- jac_coder/tool/validate.jac +11 -0
- jac_coder/tool/vision.jac +17 -0
- jac_coder/tool/web.jac +10 -0
- jac_coder/util/__init__.jac +18 -0
- jac_coder/util/colors.jac +20 -0
- jac_coder/util/impl/sandbox.impl.jac +38 -0
- jac_coder/util/impl/tool_output.impl.jac +208 -0
- jac_coder/util/sandbox.jac +8 -0
- jac_coder/util/tool_output.jac +29 -0
- jac_coder/walkers.jac +67 -0
- jac_coder-0.1.0.dist-info/METADATA +9 -0
- jac_coder-0.1.0.dist-info/RECORD +85 -0
- jac_coder-0.1.0.dist-info/WHEEL +5 -0
- jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
- jac_coder-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
glob _analysis_cache: dict = {};
|
|
2
|
+
glob _CACHE_TTL: int = 60;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _get_analysis(directory: str) -> dict {
|
|
6
|
+
real_dir = os.path.realpath(directory);
|
|
7
|
+
cached = _analysis_cache.get(real_dir);
|
|
8
|
+
if cached {
|
|
9
|
+
(ts, result) = cached;
|
|
10
|
+
if time.time() - ts < _CACHE_TTL {
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
result = _analyze_project(directory);
|
|
15
|
+
_analysis_cache[real_dir] = (time.time(), result);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# AST extraction helpers
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
def _parse_file(file_path: str) -> dict | None {
|
|
24
|
+
try {
|
|
25
|
+
prog = JacProgram();
|
|
26
|
+
module = prog.compile(file_path=file_path, type_check=False, no_cgen=True);
|
|
27
|
+
if module is None {
|
|
28
|
+
return None;
|
|
29
|
+
}
|
|
30
|
+
# Continue even with errors — partial AST is still useful
|
|
31
|
+
} except Exception as e {
|
|
32
|
+
sys.stderr.write(f"[jac_analyzer] Parse failed for {file_path}: {e}\n");
|
|
33
|
+
return None;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
archetypes: list = [];
|
|
37
|
+
for arch in module.get_all_sub_nodes(uni.Archetype, brute_force=True) {
|
|
38
|
+
arch_info = _extract_archetype(arch, file_path);
|
|
39
|
+
if arch_info {
|
|
40
|
+
archetypes.append(arch_info);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
imports: list = [];
|
|
45
|
+
for imp in module.get_all_sub_nodes(uni.Import, brute_force=True) {
|
|
46
|
+
imp_info = _extract_import(imp);
|
|
47
|
+
if imp_info {
|
|
48
|
+
imports.append(imp_info);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {"archetypes": archetypes, "imports": imports};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_archetype(arch: Any, file_path: str) -> dict | None {
|
|
57
|
+
try {
|
|
58
|
+
name = arch.name.sym_name if hasattr(arch.name, "sym_name") else str(arch.name);
|
|
59
|
+
arch_type = arch.arch_type.name
|
|
60
|
+
if hasattr(arch.arch_type, "name")
|
|
61
|
+
else str(arch.arch_type);
|
|
62
|
+
line = arch.loc.first_line if hasattr(arch, "loc") and arch.loc else 0;
|
|
63
|
+
} except Exception {
|
|
64
|
+
return None;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Map token names to readable types
|
|
68
|
+
type_map: dict = {
|
|
69
|
+
"KW_NODE": "node",
|
|
70
|
+
"KW_WALKER": "walker",
|
|
71
|
+
"KW_EDGE": "edge",
|
|
72
|
+
"KW_OBJECT": "obj"
|
|
73
|
+
};
|
|
74
|
+
kind = type_map.get(arch_type, arch_type);
|
|
75
|
+
|
|
76
|
+
# Extract fields
|
|
77
|
+
fields: list = [];
|
|
78
|
+
try {
|
|
79
|
+
for v in arch.get_has_vars() {
|
|
80
|
+
vname = v.name.sym_name if hasattr(v.name, "sym_name") else str(v.name);
|
|
81
|
+
vtype = _type_str(v);
|
|
82
|
+
fields.append(f"{vname}:{vtype}");
|
|
83
|
+
}
|
|
84
|
+
} except Exception { }
|
|
85
|
+
|
|
86
|
+
# Extract abilities
|
|
87
|
+
abilities: list = [];
|
|
88
|
+
entry_points: list = [];
|
|
89
|
+
try {
|
|
90
|
+
for m in arch.get_methods() {
|
|
91
|
+
mname = m.name_ref.sym_name
|
|
92
|
+
if hasattr(m.name_ref, "sym_name")
|
|
93
|
+
else str(m.name_ref);
|
|
94
|
+
is_llm = getattr(m, "is_genai_ability", False);
|
|
95
|
+
suffix = " [by llm]" if is_llm else "";
|
|
96
|
+
abilities.append(f"{mname}(){suffix}");
|
|
97
|
+
|
|
98
|
+
# Walker entry points: can X with NodeType entry
|
|
99
|
+
sig = getattr(m, "signature", None);
|
|
100
|
+
if sig and hasattr(sig, "arch_tag_info") and sig.arch_tag_info {
|
|
101
|
+
node_type = _expr_name(sig.arch_tag_info);
|
|
102
|
+
event_name = sig.event.name
|
|
103
|
+
if hasattr(sig, "event") and hasattr(sig.event, "name")
|
|
104
|
+
else "";
|
|
105
|
+
if node_type {
|
|
106
|
+
entry_points.append(
|
|
107
|
+
{"node_type": node_type, "event": event_name, "ability": mname}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} except Exception { }
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"name": name,
|
|
116
|
+
"kind": kind,
|
|
117
|
+
"file": file_path,
|
|
118
|
+
"line": line,
|
|
119
|
+
"fields": fields,
|
|
120
|
+
"abilities": abilities,
|
|
121
|
+
"entry_points": entry_points
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _collect_type_params(ast_node: Any, out: list) -> None {
|
|
127
|
+
type_name = type(ast_node).__name__;
|
|
128
|
+
# Skip punctuation tokens (brackets, commas)
|
|
129
|
+
if type_name == "Token" {
|
|
130
|
+
name_str = str(getattr(ast_node, "name", ""));
|
|
131
|
+
if "LSQUARE" in name_str or "RSQUARE" in name_str or "COMMA" in name_str {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
# Leaf type node — extract name
|
|
136
|
+
if hasattr(ast_node, "sym_name")
|
|
137
|
+
and type_name in ("BuiltinType", "Name", "NameAtom") {
|
|
138
|
+
out.append(ast_node.sym_name);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
# Recurse into children
|
|
142
|
+
if hasattr(ast_node, "kid") and ast_node.kid {
|
|
143
|
+
for child in ast_node.kid {
|
|
144
|
+
_collect_type_params(child, out);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _type_str(var: Any) -> str {
|
|
151
|
+
try {
|
|
152
|
+
tag = getattr(var, "type_tag", None);
|
|
153
|
+
if tag is None {
|
|
154
|
+
return "Any";
|
|
155
|
+
}
|
|
156
|
+
inner = tag.tag if hasattr(tag, "tag") else tag;
|
|
157
|
+
return _expr_to_type_str(inner);
|
|
158
|
+
} except Exception {
|
|
159
|
+
return "Any";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _expr_to_type_str(expr: Any) -> str {
|
|
165
|
+
try {
|
|
166
|
+
# BuiltinType (str, int, bool, float, dict, list, etc.)
|
|
167
|
+
if hasattr(expr, "sym_name") and hasattr(expr, "name") {
|
|
168
|
+
return expr.sym_name;
|
|
169
|
+
}
|
|
170
|
+
# AtomTrailer: base[params] — e.g., dict[str, str], list[str]
|
|
171
|
+
if type(expr).__name__ == "AtomTrailer" and hasattr(expr, "kid") {
|
|
172
|
+
kids = expr.kid;
|
|
173
|
+
if len(kids) >= 2 {
|
|
174
|
+
base = _expr_to_type_str(kids[0]);
|
|
175
|
+
# Collect all type-bearing nodes from remaining kids (skip punctuation tokens)
|
|
176
|
+
type_params: list = [];
|
|
177
|
+
for k in kids[1:] {
|
|
178
|
+
_collect_type_params(k, type_params);
|
|
179
|
+
}
|
|
180
|
+
if type_params {
|
|
181
|
+
return f"{base}[{', '.join(type_params)}]";
|
|
182
|
+
}
|
|
183
|
+
return base;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
# Name reference
|
|
187
|
+
if hasattr(expr, "sym_name") {
|
|
188
|
+
return expr.sym_name;
|
|
189
|
+
}
|
|
190
|
+
if hasattr(expr, "value") {
|
|
191
|
+
return str(expr.value);
|
|
192
|
+
}
|
|
193
|
+
# Fallback
|
|
194
|
+
s = str(expr);
|
|
195
|
+
if len(s) < 40 {
|
|
196
|
+
return s;
|
|
197
|
+
}
|
|
198
|
+
return "Any";
|
|
199
|
+
} except Exception {
|
|
200
|
+
return "Any";
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _expr_name(expr: Any) -> str {
|
|
206
|
+
try {
|
|
207
|
+
if hasattr(expr, "sym_name") {
|
|
208
|
+
return expr.sym_name;
|
|
209
|
+
}
|
|
210
|
+
if hasattr(expr, "value") and hasattr(expr.value, "sym_name") {
|
|
211
|
+
return expr.value.sym_name;
|
|
212
|
+
}
|
|
213
|
+
if hasattr(expr, "target") and hasattr(expr.target, "sym_name") {
|
|
214
|
+
return expr.target.sym_name;
|
|
215
|
+
}
|
|
216
|
+
# Fallback: try string representation, clean it up
|
|
217
|
+
s = str(expr).strip();
|
|
218
|
+
if s and len(s) < 50 {
|
|
219
|
+
return s;
|
|
220
|
+
}
|
|
221
|
+
return "";
|
|
222
|
+
} except Exception {
|
|
223
|
+
return "";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _extract_import(imp: Any) -> dict | None {
|
|
229
|
+
try {
|
|
230
|
+
from_loc = "";
|
|
231
|
+
if imp.from_loc and hasattr(imp.from_loc, "path") and imp.from_loc.path {
|
|
232
|
+
parts: list = [];
|
|
233
|
+
for p in imp.from_loc.path {
|
|
234
|
+
if hasattr(p, "sym_name") {
|
|
235
|
+
parts.append(p.sym_name);
|
|
236
|
+
} elif hasattr(p, "value") {
|
|
237
|
+
parts.append(str(p.value));
|
|
238
|
+
} else {
|
|
239
|
+
parts.append(str(p));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
from_loc = ".".join(parts);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
items: list = [];
|
|
246
|
+
if imp.items {
|
|
247
|
+
for item in imp.items {
|
|
248
|
+
if hasattr(item, "name") {
|
|
249
|
+
iname = item.name.sym_name
|
|
250
|
+
if hasattr(item.name, "sym_name")
|
|
251
|
+
else str(item.name);
|
|
252
|
+
items.append(iname);
|
|
253
|
+
} elif hasattr(item, "path") and item.path {
|
|
254
|
+
parts2: list = [];
|
|
255
|
+
for p in item.path {
|
|
256
|
+
parts2.append(p.sym_name if hasattr(p, "sym_name") else str(p));
|
|
257
|
+
}
|
|
258
|
+
items.append(".".join(parts2));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if not from_loc and not items {
|
|
264
|
+
return None;
|
|
265
|
+
}
|
|
266
|
+
return {"from": from_loc, "items": items};
|
|
267
|
+
} except Exception {
|
|
268
|
+
return None;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------------------
|
|
274
|
+
# Directory scanning
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
def _find_jac_files(directory: str) -> list {
|
|
277
|
+
skip_dirs = {"__pycache__","node_modules",".git",".jac","__jac_gen__"};
|
|
278
|
+
result: list = [];
|
|
279
|
+
for (dirpath, dirnames, filenames) in os.walk(directory) {
|
|
280
|
+
dirnames[:] = [
|
|
281
|
+
d
|
|
282
|
+
for d in dirnames
|
|
283
|
+
if d not in skip_dirs and not d.startswith(".")
|
|
284
|
+
];
|
|
285
|
+
for fname in sorted(filenames) {
|
|
286
|
+
if fname.endswith(".jac") {
|
|
287
|
+
result.append(os.path.join(dirpath, fname));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _analyze_project(directory: str) -> dict {
|
|
296
|
+
files = _find_jac_files(directory);
|
|
297
|
+
all_archetypes: list = [];
|
|
298
|
+
all_imports: dict = {};
|
|
299
|
+
exports: dict = {};
|
|
300
|
+
failed: list = [];
|
|
301
|
+
|
|
302
|
+
for fpath in files {
|
|
303
|
+
rel = os.path.relpath(fpath, directory);
|
|
304
|
+
parsed = _parse_file(fpath);
|
|
305
|
+
if parsed is None {
|
|
306
|
+
failed.append(rel);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Collect archetypes
|
|
311
|
+
for arch in parsed["archetypes"] {
|
|
312
|
+
arch["rel_file"] = rel;
|
|
313
|
+
all_archetypes.append(arch);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Collect imports per file
|
|
317
|
+
if parsed["imports"] {
|
|
318
|
+
all_imports[rel] = parsed["imports"];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Build export map (symbols defined per file)
|
|
322
|
+
defined: list = [a["name"] for a in parsed["archetypes"]];
|
|
323
|
+
if defined {
|
|
324
|
+
exports[rel] = defined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
"archetypes": all_archetypes,
|
|
330
|
+
"imports": all_imports,
|
|
331
|
+
"exports": exports,
|
|
332
|
+
"failed": failed
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Formatting
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
def _format_analysis(analysis: dict, directory: str) -> str {
|
|
341
|
+
sections: list = [];
|
|
342
|
+
archetypes = analysis["archetypes"];
|
|
343
|
+
|
|
344
|
+
# Group by kind
|
|
345
|
+
nodes = [
|
|
346
|
+
a
|
|
347
|
+
for a in archetypes
|
|
348
|
+
if a["kind"] == "node"
|
|
349
|
+
];
|
|
350
|
+
walkers = [
|
|
351
|
+
a
|
|
352
|
+
for a in archetypes
|
|
353
|
+
if a["kind"] == "walker"
|
|
354
|
+
];
|
|
355
|
+
edges = [
|
|
356
|
+
a
|
|
357
|
+
for a in archetypes
|
|
358
|
+
if a["kind"] == "edge"
|
|
359
|
+
];
|
|
360
|
+
objects = [
|
|
361
|
+
a
|
|
362
|
+
for a in archetypes
|
|
363
|
+
if a["kind"] == "obj"
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
# Client components: check if file is .cl.jac
|
|
367
|
+
client_comps = [
|
|
368
|
+
a
|
|
369
|
+
for a in archetypes
|
|
370
|
+
if a["rel_file"].endswith(".cl.jac")
|
|
371
|
+
];
|
|
372
|
+
server_nodes = [
|
|
373
|
+
a
|
|
374
|
+
for a in nodes
|
|
375
|
+
if not a["rel_file"].endswith(".cl.jac")
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
# Nodes section
|
|
379
|
+
if server_nodes {
|
|
380
|
+
lines: list = ["Nodes:"];
|
|
381
|
+
for n in server_nodes {
|
|
382
|
+
fields_str = ", ".join(n["fields"][:8]) if n["fields"] else "no fields";
|
|
383
|
+
abilities_str = "";
|
|
384
|
+
if n["abilities"] {
|
|
385
|
+
abilities_str = " | abilities: " + ", ".join(n["abilities"][:5]);
|
|
386
|
+
}
|
|
387
|
+
lines.append(
|
|
388
|
+
f" - {n['name']} ({n['rel_file']}:{n['line']}) — {fields_str}{abilities_str}"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
sections.append("\n".join(lines));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
# Walkers section
|
|
395
|
+
if walkers {
|
|
396
|
+
lines2: list = ["Walkers:"];
|
|
397
|
+
for w in walkers {
|
|
398
|
+
visits_str = "";
|
|
399
|
+
if w["entry_points"] {
|
|
400
|
+
visit_names = [ep["node_type"] for ep in w["entry_points"]];
|
|
401
|
+
visits_str = " — visits: " + " → ".join(visit_names);
|
|
402
|
+
}
|
|
403
|
+
lines2.append(f" - {w['name']} ({w['rel_file']}:{w['line']}){visits_str}");
|
|
404
|
+
}
|
|
405
|
+
sections.append("\n".join(lines2));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Edges section
|
|
409
|
+
if edges {
|
|
410
|
+
lines3: list = ["Edges:"];
|
|
411
|
+
for e in edges {
|
|
412
|
+
fields_str3 = ", ".join(e["fields"][:5]) if e["fields"] else "no fields";
|
|
413
|
+
lines3.append(
|
|
414
|
+
f" - {e['name']} ({e['rel_file']}:{e['line']}) — {fields_str3}"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
sections.append("\n".join(lines3));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Client components section
|
|
421
|
+
if client_comps {
|
|
422
|
+
lines4: list = ["Client Components (.cl.jac):"];
|
|
423
|
+
for c in client_comps {
|
|
424
|
+
state_str = ", ".join(c["fields"][:5]) if c["fields"] else "no state";
|
|
425
|
+
lines4.append(
|
|
426
|
+
f" - {c['name']} ({c['rel_file']}:{c['line']}) — state: {state_str}"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
sections.append("\n".join(lines4));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# Objects section (only if not too many)
|
|
433
|
+
if objects and len(objects) <= 10 {
|
|
434
|
+
lines5: list = ["Objects:"];
|
|
435
|
+
for o in objects {
|
|
436
|
+
fields_str5 = ", ".join(o["fields"][:6]) if o["fields"] else "no fields";
|
|
437
|
+
lines5.append(
|
|
438
|
+
f" - {o['name']} ({o['rel_file']}:{o['line']}) — {fields_str5}"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
sections.append("\n".join(lines5));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# Import map
|
|
445
|
+
all_imports = analysis["imports"];
|
|
446
|
+
exports = analysis["exports"];
|
|
447
|
+
if all_imports {
|
|
448
|
+
lines6: list = ["Import Map:"];
|
|
449
|
+
for (file, imps) in sorted(all_imports.items()) {
|
|
450
|
+
imp_strs: list = [];
|
|
451
|
+
for imp in imps {
|
|
452
|
+
if imp["from"] and imp["items"] {
|
|
453
|
+
imp_strs.append(f"{', '.join(imp['items'])} from {imp['from']}");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if imp_strs {
|
|
457
|
+
lines6.append(f" {file}: imports [{'; '.join(imp_strs[:5])}]");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if exports {
|
|
461
|
+
lines6.append("");
|
|
462
|
+
for (file, syms) in sorted(exports.items()) {
|
|
463
|
+
lines6.append(f" {file}: exports [{', '.join(syms[:10])}]");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
sections.append("\n".join(lines6));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# Failed files
|
|
470
|
+
if analysis["failed"] {
|
|
471
|
+
sections.append(f"Parse errors: {', '.join(analysis['failed'][:5])}");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if not sections {
|
|
475
|
+
return "No .jac files found in directory.";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return "\n\n".join(sections);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# ---------------------------------------------------------------------------
|
|
483
|
+
# Public tool implementations
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
impl analyze_project(directory: str) -> str {
|
|
486
|
+
if not directory {
|
|
487
|
+
return "Error: directory is required.";
|
|
488
|
+
}
|
|
489
|
+
if not os.path.isdir(directory) {
|
|
490
|
+
return f"Error: directory not found: {directory}";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
analysis = _get_analysis(directory);
|
|
495
|
+
return _format_analysis(analysis, directory);
|
|
496
|
+
} except Exception as e {
|
|
497
|
+
sys.stderr.write(f"[jac_analyzer] Analysis failed: {e}\n");
|
|
498
|
+
return f"Analysis failed: {e}";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
impl find_symbol(name: str, directory: str = "") -> str {
|
|
504
|
+
if not name {
|
|
505
|
+
return "Error: symbol name is required.";
|
|
506
|
+
}
|
|
507
|
+
if not directory or not os.path.isdir(directory) {
|
|
508
|
+
return f"Error: valid directory is required (got: '{directory}').";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
analysis = _get_analysis(directory);
|
|
513
|
+
} except Exception as e {
|
|
514
|
+
return f"Analysis failed: {e}";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# Search archetypes for the symbol
|
|
518
|
+
matches: list = [];
|
|
519
|
+
usages: list = [];
|
|
520
|
+
|
|
521
|
+
for arch in analysis["archetypes"] {
|
|
522
|
+
if arch["name"] == name {
|
|
523
|
+
matches.append(arch);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# Search imports for usages
|
|
528
|
+
for (file, imps) in analysis["imports"].items() {
|
|
529
|
+
for imp in imps {
|
|
530
|
+
if name in imp.get("items", []) {
|
|
531
|
+
usages.append(
|
|
532
|
+
{"file": file, "type": "import", "from": imp.get("from", "")}
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
# Search walker entry points for node type usages
|
|
539
|
+
for arch in analysis["archetypes"] {
|
|
540
|
+
for ep in arch.get("entry_points", []) {
|
|
541
|
+
if ep["node_type"] == name {
|
|
542
|
+
usages.append(
|
|
543
|
+
{
|
|
544
|
+
"file": arch["rel_file"],
|
|
545
|
+
"type": f"walker entry ({arch['name']}.{ep['ability']})"
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if not matches and not usages {
|
|
553
|
+
return f"Symbol '{name}' not found in the project.";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
parts: list = [];
|
|
557
|
+
|
|
558
|
+
# Definition info
|
|
559
|
+
for m in matches {
|
|
560
|
+
fields_str = ", ".join(m["fields"][:8]) if m["fields"] else "no fields";
|
|
561
|
+
abilities_str = ", ".join(m["abilities"][:5]) if m["abilities"] else "none";
|
|
562
|
+
parts.append(
|
|
563
|
+
f"Defined: {m['rel_file']}:{m['line']} ({m['kind']})\n"
|
|
564
|
+
f" Fields: {fields_str}\n"
|
|
565
|
+
f" Abilities: {abilities_str}"
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
# Suggest import statement
|
|
569
|
+
# Find which module-relative path defines this
|
|
570
|
+
for (file, syms) in analysis["exports"].items() {
|
|
571
|
+
if name in syms and file == m["rel_file"] {
|
|
572
|
+
mod_path = file.replace("/", ".").replace(".jac", "").replace(".cl", "");
|
|
573
|
+
parts.append(f" Import: import from {mod_path} {'{' + name + '}'}");
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
# Usage info
|
|
580
|
+
if usages {
|
|
581
|
+
usage_lines: list = ["Used by:"];
|
|
582
|
+
for u in usages {
|
|
583
|
+
if u["type"] == "import" {
|
|
584
|
+
usage_lines.append(f" - {u['file']} (import from {u['from']})");
|
|
585
|
+
} else {
|
|
586
|
+
usage_lines.append(f" - {u['file']} ({u['type']})");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
parts.append("\n".join(usage_lines));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return "\n".join(parts);
|
|
593
|
+
}
|