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.
Files changed (85) hide show
  1. jac_coder/__init__.jac +0 -0
  2. jac_coder/api.jac +82 -0
  3. jac_coder/cli_entry.py +25 -0
  4. jac_coder/config.jac +36 -0
  5. jac_coder/context.jac +17 -0
  6. jac_coder/data/examples/ai_agent.md +90 -0
  7. jac_coder/data/examples/blog_app.md +386 -0
  8. jac_coder/data/examples/core_patterns.md +321 -0
  9. jac_coder/data/examples/todo_app.md +321 -0
  10. jac_coder/data/reference/ai.md +131 -0
  11. jac_coder/data/reference/backend.md +215 -0
  12. jac_coder/data/reference/frontend.md +271 -0
  13. jac_coder/data/reference/osp.md +229 -0
  14. jac_coder/data/reference/pitfalls.md +141 -0
  15. jac_coder/data/reference/syntax.md +159 -0
  16. jac_coder/data/rules/core_jac.md +559 -0
  17. jac_coder/data/rules/fullstack.md +362 -0
  18. jac_coder/data/rules/workflow.md +88 -0
  19. jac_coder/events.jac +110 -0
  20. jac_coder/impl/api.impl.jac +399 -0
  21. jac_coder/impl/config.impl.jac +163 -0
  22. jac_coder/impl/context.impl.jac +117 -0
  23. jac_coder/impl/mcp_manager.impl.jac +380 -0
  24. jac_coder/impl/memory.impl.jac +247 -0
  25. jac_coder/impl/nodes.impl.jac +259 -0
  26. jac_coder/impl/permission.impl.jac +62 -0
  27. jac_coder/impl/walkers.impl.jac +298 -0
  28. jac_coder/mcp_manager.jac +35 -0
  29. jac_coder/memory.jac +15 -0
  30. jac_coder/nodes.jac +306 -0
  31. jac_coder/permission.jac +19 -0
  32. jac_coder/serve_entry.jac +30 -0
  33. jac_coder/server.jac +324 -0
  34. jac_coder/tool/__init__.jac +17 -0
  35. jac_coder/tool/checked.jac +10 -0
  36. jac_coder/tool/delegation.jac +23 -0
  37. jac_coder/tool/filesystem.jac +25 -0
  38. jac_coder/tool/git.jac +18 -0
  39. jac_coder/tool/guarded.jac +23 -0
  40. jac_coder/tool/impl/checked.impl.jac +38 -0
  41. jac_coder/tool/impl/delegation.impl.jac +157 -0
  42. jac_coder/tool/impl/filesystem.impl.jac +781 -0
  43. jac_coder/tool/impl/git.impl.jac +115 -0
  44. jac_coder/tool/impl/guarded.impl.jac +72 -0
  45. jac_coder/tool/impl/jac_analyzer.impl.jac +593 -0
  46. jac_coder/tool/impl/jac_docs.impl.jac +136 -0
  47. jac_coder/tool/impl/jac_tools.impl.jac +79 -0
  48. jac_coder/tool/impl/mcp.impl.jac +32 -0
  49. jac_coder/tool/impl/preview.impl.jac +233 -0
  50. jac_coder/tool/impl/question.impl.jac +29 -0
  51. jac_coder/tool/impl/scaffold.impl.jac +231 -0
  52. jac_coder/tool/impl/search.impl.jac +85 -0
  53. jac_coder/tool/impl/shell.impl.jac +89 -0
  54. jac_coder/tool/impl/task.impl.jac +12 -0
  55. jac_coder/tool/impl/think.impl.jac +4 -0
  56. jac_coder/tool/impl/todo.impl.jac +58 -0
  57. jac_coder/tool/impl/validate.impl.jac +236 -0
  58. jac_coder/tool/impl/web.impl.jac +91 -0
  59. jac_coder/tool/jac_analyzer.jac +21 -0
  60. jac_coder/tool/jac_docs.jac +9 -0
  61. jac_coder/tool/jac_tools.jac +11 -0
  62. jac_coder/tool/mcp.jac +17 -0
  63. jac_coder/tool/preview.jac +31 -0
  64. jac_coder/tool/question.jac +7 -0
  65. jac_coder/tool/scaffold.jac +10 -0
  66. jac_coder/tool/search.jac +14 -0
  67. jac_coder/tool/shell.jac +12 -0
  68. jac_coder/tool/task.jac +9 -0
  69. jac_coder/tool/think.jac +5 -0
  70. jac_coder/tool/todo.jac +12 -0
  71. jac_coder/tool/validate.jac +11 -0
  72. jac_coder/tool/vision.jac +17 -0
  73. jac_coder/tool/web.jac +10 -0
  74. jac_coder/util/__init__.jac +18 -0
  75. jac_coder/util/colors.jac +20 -0
  76. jac_coder/util/impl/sandbox.impl.jac +38 -0
  77. jac_coder/util/impl/tool_output.impl.jac +208 -0
  78. jac_coder/util/sandbox.jac +8 -0
  79. jac_coder/util/tool_output.jac +29 -0
  80. jac_coder/walkers.jac +67 -0
  81. jac_coder-0.1.0.dist-info/METADATA +9 -0
  82. jac_coder-0.1.0.dist-info/RECORD +85 -0
  83. jac_coder-0.1.0.dist-info/WHEEL +5 -0
  84. jac_coder-0.1.0.dist-info/entry_points.txt +3 -0
  85. 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
+ }