codebeacon 0.1.2__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 (59) hide show
  1. codebeacon/__init__.py +1 -0
  2. codebeacon/__main__.py +3 -0
  3. codebeacon/cache.py +136 -0
  4. codebeacon/cli.py +391 -0
  5. codebeacon/common/__init__.py +0 -0
  6. codebeacon/common/filters.py +170 -0
  7. codebeacon/common/symbols.py +121 -0
  8. codebeacon/common/types.py +98 -0
  9. codebeacon/config.py +144 -0
  10. codebeacon/contextmap/__init__.py +0 -0
  11. codebeacon/contextmap/generator.py +602 -0
  12. codebeacon/discover/__init__.py +0 -0
  13. codebeacon/discover/detector.py +388 -0
  14. codebeacon/discover/scanner.py +192 -0
  15. codebeacon/export/__init__.py +0 -0
  16. codebeacon/export/mcp.py +515 -0
  17. codebeacon/export/obsidian.py +812 -0
  18. codebeacon/extract/__init__.py +22 -0
  19. codebeacon/extract/base.py +372 -0
  20. codebeacon/extract/components.py +357 -0
  21. codebeacon/extract/dependencies.py +140 -0
  22. codebeacon/extract/entities.py +575 -0
  23. codebeacon/extract/queries/README.md +116 -0
  24. codebeacon/extract/queries/actix.scm +115 -0
  25. codebeacon/extract/queries/angular.scm +155 -0
  26. codebeacon/extract/queries/aspnet.scm +159 -0
  27. codebeacon/extract/queries/django.scm +122 -0
  28. codebeacon/extract/queries/express.scm +124 -0
  29. codebeacon/extract/queries/fastapi.scm +152 -0
  30. codebeacon/extract/queries/flask.scm +120 -0
  31. codebeacon/extract/queries/gin.scm +142 -0
  32. codebeacon/extract/queries/ktor.scm +144 -0
  33. codebeacon/extract/queries/laravel.scm +172 -0
  34. codebeacon/extract/queries/nestjs.scm +183 -0
  35. codebeacon/extract/queries/rails.scm +114 -0
  36. codebeacon/extract/queries/react.scm +111 -0
  37. codebeacon/extract/queries/spring_boot.scm +204 -0
  38. codebeacon/extract/queries/svelte.scm +73 -0
  39. codebeacon/extract/queries/vapor.scm +130 -0
  40. codebeacon/extract/queries/vue.scm +123 -0
  41. codebeacon/extract/routes.py +910 -0
  42. codebeacon/extract/semantic.py +280 -0
  43. codebeacon/extract/services.py +597 -0
  44. codebeacon/graph/__init__.py +1 -0
  45. codebeacon/graph/analyze.py +281 -0
  46. codebeacon/graph/build.py +320 -0
  47. codebeacon/graph/cluster.py +160 -0
  48. codebeacon/graph/enrich.py +206 -0
  49. codebeacon/skill/SKILL.md +127 -0
  50. codebeacon/wave.py +292 -0
  51. codebeacon/wiki/__init__.py +0 -0
  52. codebeacon/wiki/generator.py +376 -0
  53. codebeacon/wiki/index.py +95 -0
  54. codebeacon/wiki/templates.py +467 -0
  55. codebeacon-0.1.2.dist-info/METADATA +319 -0
  56. codebeacon-0.1.2.dist-info/RECORD +59 -0
  57. codebeacon-0.1.2.dist-info/WHEEL +4 -0
  58. codebeacon-0.1.2.dist-info/entry_points.txt +2 -0
  59. codebeacon-0.1.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,95 @@
1
+ """Index file generators: global index.md, overview.md, routes.md, cross-project/connections.md.
2
+
3
+ Public API:
4
+ generate_index(wiki_dir, project_summary, routes_by_project, cross_edges, total_stats)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from codebeacon.wiki import templates
13
+
14
+
15
+ def generate_index(
16
+ wiki_dir: Path,
17
+ project_summary: list[dict[str, Any]],
18
+ routes_by_project: dict[str, list[dict[str, Any]]],
19
+ cross_edges: list[dict[str, Any]],
20
+ total_stats: dict[str, int],
21
+ ) -> None:
22
+ """Write all global index files into wiki_dir.
23
+
24
+ Args:
25
+ wiki_dir: root wiki output directory (.codebeacon/wiki/)
26
+ project_summary: list of {name, framework, route_count, service_count, entity_count, component_count}
27
+ routes_by_project: project_name → list of route dicts
28
+ cross_edges: list of cross-project edge dicts {source, target, relation, source_project, target_project}
29
+ total_stats: aggregate {nodes, edges, communities, routes, services, entities, components}
30
+ """
31
+ wiki_dir.mkdir(parents=True, exist_ok=True)
32
+
33
+ # index.md (short ~200 tokens global index)
34
+ content = templates.global_index(
35
+ projects=project_summary,
36
+ total_stats=total_stats,
37
+ )
38
+ _write(wiki_dir / "index.md", content)
39
+
40
+ # overview.md
41
+ cross_connections = [
42
+ {"source": f"{e['source_project']}/{e['source']}", "target": f"{e['target_project']}/{e['target']}", "relation": e["relation"]}
43
+ for e in cross_edges
44
+ ]
45
+ content = templates.platform_overview(
46
+ projects=project_summary,
47
+ cross_connections=cross_connections,
48
+ total_stats=total_stats,
49
+ )
50
+ _write(wiki_dir / "overview.md", content)
51
+
52
+ # routes.md (all projects)
53
+ content = templates.routes_summary(routes_by_project)
54
+ _write(wiki_dir / "routes.md", content)
55
+
56
+ # cross-project/connections.md
57
+ _write_cross_project(wiki_dir / "cross-project" / "connections.md", cross_edges)
58
+
59
+
60
+ def _write_cross_project(path: Path, cross_edges: list[dict[str, Any]]) -> None:
61
+ """Write cross-project/connections.md."""
62
+ lines = [
63
+ "# Cross-Project Connections",
64
+ "",
65
+ "Edges that cross service/project boundaries, extracted from the knowledge graph.",
66
+ "",
67
+ ]
68
+
69
+ if not cross_edges:
70
+ lines.append("_No cross-project connections detected._")
71
+ else:
72
+ # Group by relation type
73
+ by_relation: dict[str, list[dict]] = {}
74
+ for e in cross_edges:
75
+ rel = e.get("relation", "unknown")
76
+ by_relation.setdefault(rel, []).append(e)
77
+
78
+ for relation in sorted(by_relation.keys()):
79
+ edges = by_relation[relation]
80
+ lines += [f"## `{relation}`", ""]
81
+ for e in edges[:50]: # cap per relation
82
+ src_proj = e.get("source_project", "")
83
+ tgt_proj = e.get("target_project", "")
84
+ src = e.get("source", "")
85
+ tgt = e.get("target", "")
86
+ lines.append(f"- `{src_proj}/{src}` → `{tgt_proj}/{tgt}`")
87
+ lines.append("")
88
+
89
+ path.parent.mkdir(parents=True, exist_ok=True)
90
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
91
+
92
+
93
+ def _write(path: Path, content: str) -> None:
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ path.write_text(content, encoding="utf-8")
@@ -0,0 +1,467 @@
1
+ """Markdown templates for codebeacon wiki articles.
2
+
3
+ Each function takes structured data and returns a markdown string.
4
+ No file I/O here — callers decide where to write.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+
12
+ # ── Helpers ───────────────────────────────────────────────────────────────────
13
+
14
+ def _rel_link(label: str, project: str) -> str:
15
+ """Produce a relative markdown link (same project)."""
16
+ safe = label.replace(" ", "%20")
17
+ return f"[{label}](./{safe}.md)"
18
+
19
+
20
+ def _back_link(project_name: str) -> str:
21
+ return f"_Back to [{project_name}/index.md](./index.md)_"
22
+
23
+
24
+ # ── Controller article ────────────────────────────────────────────────────────
25
+
26
+ def controller_article(
27
+ label: str,
28
+ routes: list[dict[str, Any]],
29
+ source_file: str,
30
+ called_by: list[str],
31
+ calls: list[str],
32
+ project_name: str,
33
+ ) -> str:
34
+ """Wiki article for a controller / router handler class.
35
+
36
+ Args:
37
+ label: class name (e.g. "UserController")
38
+ routes: list of dicts with keys: method, path, handler, line
39
+ source_file: relative source path
40
+ called_by: list of node labels that call this controller
41
+ calls: list of node labels this controller calls/injects
42
+ project_name: owning project name
43
+ """
44
+ lines = [
45
+ f"# {label}",
46
+ "",
47
+ f"> **Navigation aid.** Route list and file locations extracted via AST."
48
+ f" Read the source files listed below before implementing or modifying this subsystem.",
49
+ "",
50
+ ]
51
+
52
+ if routes:
53
+ lines += [f"The {label} subsystem handles **{len(routes)} route(s)**.", "", "## Routes", ""]
54
+ for r in sorted(routes, key=lambda x: x.get("path", "")):
55
+ method = r.get("method", "GET")
56
+ path = r.get("path", "")
57
+ handler = r.get("handler", "")
58
+ tags = r.get("tags", [])
59
+ tag_str = f" [{', '.join(tags)}]" if tags else ""
60
+ lines.append(f"- `{method}` `{path}`{tag_str}")
61
+ lines.append(f" `{source_file}`")
62
+ else:
63
+ lines += ["## Routes", "", "_No routes extracted._"]
64
+
65
+ lines += ["", "## Source Files", "", "Read these before implementing or modifying this subsystem:"]
66
+ lines.append(f"- `{source_file}`")
67
+
68
+ if calls:
69
+ lines += ["", "## Calls / Injects", ""]
70
+ for name in sorted(set(calls)):
71
+ lines.append(f"- {_rel_link(name, project_name)}")
72
+
73
+ if called_by:
74
+ lines += ["", "## Called By", ""]
75
+ for name in sorted(set(called_by)):
76
+ lines.append(f"- {_rel_link(name, project_name)}")
77
+
78
+ lines += ["", "---", _back_link(project_name)]
79
+ return "\n".join(lines) + "\n"
80
+
81
+
82
+ # ── Service article ───────────────────────────────────────────────────────────
83
+
84
+ def service_article(
85
+ label: str,
86
+ methods: list[str],
87
+ dependencies: list[str],
88
+ source_file: str,
89
+ called_by: list[str],
90
+ calls: list[str],
91
+ related_entities: list[str],
92
+ annotations: list[str],
93
+ project_name: str,
94
+ ) -> str:
95
+ """Wiki article for a service / component class.
96
+
97
+ This is the ★ core article type — it includes method signatures, DI
98
+ dependencies, call graph edges, and related entities.
99
+
100
+ Args:
101
+ label: class name
102
+ methods: list of method name strings
103
+ dependencies: injected type names (may be unresolved)
104
+ source_file: relative source path
105
+ called_by: node labels with incoming call/inject edges
106
+ calls: node labels with outgoing call/inject edges
107
+ related_entities: entity node labels imported/used by this service
108
+ annotations: framework annotations (e.g. @Service, @Injectable)
109
+ project_name: owning project name
110
+ """
111
+ ann_str = f" `{'` `'.join(annotations)}`" if annotations else ""
112
+ lines = [
113
+ f"# {label}",
114
+ "",
115
+ f"**Type:** Service{ann_str}",
116
+ f"**Source:** `{source_file}`",
117
+ "",
118
+ ]
119
+
120
+ if methods:
121
+ lines += ["## Methods", ""]
122
+ for m in methods:
123
+ lines.append(f"- `{m}()`")
124
+ lines.append("")
125
+
126
+ if dependencies:
127
+ lines += ["## DI Dependencies", ""]
128
+ for dep in sorted(set(dependencies)):
129
+ lines.append(f"- {_rel_link(dep, project_name)}")
130
+ lines.append("")
131
+
132
+ if calls:
133
+ lines += ["## Calls", ""]
134
+ for name in sorted(set(calls)):
135
+ lines.append(f"- {_rel_link(name, project_name)}")
136
+ lines.append("")
137
+
138
+ if called_by:
139
+ lines += ["## Called By", ""]
140
+ for name in sorted(set(called_by)):
141
+ lines.append(f"- {_rel_link(name, project_name)}")
142
+ lines.append("")
143
+
144
+ if related_entities:
145
+ lines += ["## Related Entities", ""]
146
+ for ent in sorted(set(related_entities)):
147
+ lines.append(f"- {_rel_link(ent, project_name)}")
148
+ lines.append("")
149
+
150
+ lines += ["---", _back_link(project_name)]
151
+ return "\n".join(lines) + "\n"
152
+
153
+
154
+ # ── Entity article ────────────────────────────────────────────────────────────
155
+
156
+ def entity_article(
157
+ label: str,
158
+ table_name: str,
159
+ fields: list[dict[str, Any]],
160
+ relations: list[dict[str, Any]],
161
+ source_file: str,
162
+ used_by: list[str],
163
+ framework: str,
164
+ project_name: str,
165
+ ) -> str:
166
+ """Wiki article for an ORM entity / model.
167
+
168
+ Args:
169
+ label: entity class name
170
+ table_name: database table name (may be empty)
171
+ fields: list of dicts: {name, type, annotations}
172
+ relations: list of dicts: {type, target}
173
+ source_file: relative source path
174
+ used_by: node labels referencing this entity
175
+ framework: ORM framework (jpa, django-orm, sqlalchemy, …)
176
+ project_name: owning project name
177
+ """
178
+ table_line = f"**Table:** `{table_name}` " if table_name else ""
179
+ lines = [
180
+ f"# {label}",
181
+ "",
182
+ f"**Type:** Entity ({framework}) ",
183
+ f"{table_line}**Source:** `{source_file}`",
184
+ "",
185
+ ]
186
+
187
+ if fields:
188
+ lines += ["## Fields", ""]
189
+ for f in fields:
190
+ name = f.get("name", "")
191
+ ftype = f.get("type", "")
192
+ anns = f.get("annotations", [])
193
+ ann_str = f" `{'` `'.join(anns)}`" if anns else ""
194
+ lines.append(f"- `{ftype} {name}`{ann_str}")
195
+ lines.append("")
196
+
197
+ if relations:
198
+ lines += ["## Relations", ""]
199
+ for r in relations:
200
+ rtype = r.get("type", "")
201
+ target = r.get("target", "")
202
+ lines.append(f"- `{rtype}` → {_rel_link(target, project_name)}")
203
+ lines.append("")
204
+
205
+ if used_by:
206
+ lines += ["## Used By", ""]
207
+ for name in sorted(set(used_by)):
208
+ lines.append(f"- {_rel_link(name, project_name)}")
209
+ lines.append("")
210
+
211
+ lines += ["---", _back_link(project_name)]
212
+ return "\n".join(lines) + "\n"
213
+
214
+
215
+ # ── Component article ─────────────────────────────────────────────────────────
216
+
217
+ def component_article(
218
+ label: str,
219
+ props: list[str],
220
+ hooks: list[str],
221
+ imports: list[str],
222
+ is_page: bool,
223
+ route_path: str,
224
+ source_file: str,
225
+ framework: str,
226
+ project_name: str,
227
+ ) -> str:
228
+ """Wiki article for a frontend component (React/Vue/Svelte/Angular).
229
+
230
+ Args:
231
+ label: component name
232
+ props: prop names
233
+ hooks: used hooks/composables
234
+ imports: imported component names
235
+ is_page: True if this is a page-level route component
236
+ route_path: derived route path for page components
237
+ source_file: relative source path
238
+ framework: "react", "vue", "svelte", "angular"
239
+ project_name: owning project name
240
+ """
241
+ kind = "Page Component" if is_page else "Component"
242
+ lines = [
243
+ f"# {label}",
244
+ "",
245
+ f"**Type:** {kind} ({framework}) ",
246
+ f"**Source:** `{source_file}`",
247
+ ]
248
+
249
+ if is_page and route_path:
250
+ lines.append(f"**Route:** `{route_path}`")
251
+
252
+ lines.append("")
253
+
254
+ if props:
255
+ lines += ["## Props", ""]
256
+ for p in props:
257
+ lines.append(f"- `{p}`")
258
+ lines.append("")
259
+
260
+ if hooks:
261
+ lines += ["## Hooks / Composables", ""]
262
+ for h in hooks:
263
+ lines.append(f"- `{h}`")
264
+ lines.append("")
265
+
266
+ if imports:
267
+ lines += ["## Imports", ""]
268
+ for name in sorted(set(imports)):
269
+ lines.append(f"- {_rel_link(name, project_name)}")
270
+ lines.append("")
271
+
272
+ lines += ["---", _back_link(project_name)]
273
+ return "\n".join(lines) + "\n"
274
+
275
+
276
+ # ── Routes summary ────────────────────────────────────────────────────────────
277
+
278
+ def routes_summary(
279
+ routes_by_project: dict[str, list[dict[str, Any]]],
280
+ ) -> str:
281
+ """Full routes.md — all routes across all projects in a table.
282
+
283
+ Args:
284
+ routes_by_project: project_name → list of route dicts
285
+ Each route dict: {method, path, handler, source_file, framework}
286
+ """
287
+ lines = [
288
+ "# Routes Summary",
289
+ "",
290
+ "All API routes extracted across all projects.",
291
+ "",
292
+ ]
293
+
294
+ for project_name, routes in sorted(routes_by_project.items()):
295
+ if not routes:
296
+ continue
297
+ lines += [f"## {project_name}", "", f"| Method | Path | Handler | File |", "| --- | --- | --- | --- |"]
298
+ for r in sorted(routes, key=lambda x: (x.get("path", ""), x.get("method", ""))):
299
+ method = r.get("method", "")
300
+ path = r.get("path", "")
301
+ handler = r.get("handler", "")
302
+ sf = r.get("source_file", "")
303
+ lines.append(f"| `{method}` | `{path}` | `{handler}` | `{sf}` |")
304
+ lines.append("")
305
+
306
+ return "\n".join(lines) + "\n"
307
+
308
+
309
+ # ── Project index ─────────────────────────────────────────────────────────────
310
+
311
+ def project_index(
312
+ project_name: str,
313
+ framework: str,
314
+ stats: dict[str, int],
315
+ controllers: list[str],
316
+ services: list[str],
317
+ entities: list[str],
318
+ components: list[str],
319
+ ) -> str:
320
+ """Per-project index.md.
321
+
322
+ Args:
323
+ project_name: project name
324
+ framework: detected framework
325
+ stats: {routes, services, entities, components, ...}
326
+ controllers: list of controller names
327
+ services: list of service names
328
+ entities: list of entity names
329
+ components: list of component names
330
+ """
331
+ lines = [
332
+ f"# {project_name}",
333
+ "",
334
+ f"**Framework:** {framework}",
335
+ f"**Routes:** {stats.get('routes', 0)} ",
336
+ f"**Services:** {stats.get('services', 0)} ",
337
+ f"**Entities:** {stats.get('entities', 0)} ",
338
+ f"**Components:** {stats.get('components', 0)}",
339
+ "",
340
+ ]
341
+
342
+ if controllers:
343
+ lines += ["## Controllers", ""]
344
+ for name in sorted(controllers):
345
+ lines.append(f"- [controllers/{name}](./controllers/{name}.md)")
346
+ lines.append("")
347
+
348
+ if services:
349
+ lines += ["## Services", ""]
350
+ for name in sorted(services):
351
+ lines.append(f"- [services/{name}](./services/{name}.md)")
352
+ lines.append("")
353
+
354
+ if entities:
355
+ lines += ["## Entities", ""]
356
+ for name in sorted(entities):
357
+ lines.append(f"- [entities/{name}](./entities/{name}.md)")
358
+ lines.append("")
359
+
360
+ if components:
361
+ lines += ["## Components", ""]
362
+ for name in sorted(components):
363
+ lines.append(f"- [components/{name}](./components/{name}.md)")
364
+ lines.append("")
365
+
366
+ lines += ["---", "_Back to [index.md](../index.md)_"]
367
+ return "\n".join(lines) + "\n"
368
+
369
+
370
+ # ── Platform overview ─────────────────────────────────────────────────────────
371
+
372
+ def platform_overview(
373
+ projects: list[dict[str, Any]],
374
+ cross_connections: list[dict[str, Any]],
375
+ total_stats: dict[str, int],
376
+ ) -> str:
377
+ """Platform-wide overview.md.
378
+
379
+ Args:
380
+ projects: list of dicts: {name, framework, route_count, service_count, entity_count}
381
+ cross_connections: list of dicts: {source, target, relation}
382
+ total_stats: {nodes, edges, communities, routes, services, entities, components}
383
+ """
384
+ lines = [
385
+ "# Platform Overview",
386
+ "",
387
+ "## Statistics",
388
+ "",
389
+ f"| Metric | Count |",
390
+ "| --- | --- |",
391
+ f"| Graph nodes | {total_stats.get('nodes', 0)} |",
392
+ f"| Graph edges | {total_stats.get('edges', 0)} |",
393
+ f"| Communities | {total_stats.get('communities', 0)} |",
394
+ f"| Routes | {total_stats.get('routes', 0)} |",
395
+ f"| Services | {total_stats.get('services', 0)} |",
396
+ f"| Entities | {total_stats.get('entities', 0)} |",
397
+ f"| Components | {total_stats.get('components', 0)} |",
398
+ "",
399
+ ]
400
+
401
+ if projects:
402
+ lines += [
403
+ "## Projects",
404
+ "",
405
+ "| Project | Framework | Routes | Services | Entities |",
406
+ "| --- | --- | --- | --- | --- |",
407
+ ]
408
+ for p in sorted(projects, key=lambda x: x.get("name", "")):
409
+ name = p.get("name", "")
410
+ fw = p.get("framework", "")
411
+ lines.append(
412
+ f"| [{name}](./{name}/index.md) | {fw}"
413
+ f" | {p.get('route_count', 0)}"
414
+ f" | {p.get('service_count', 0)}"
415
+ f" | {p.get('entity_count', 0)} |"
416
+ )
417
+ lines.append("")
418
+
419
+ if cross_connections:
420
+ lines += ["## Cross-Project Connections", ""]
421
+ for cc in cross_connections[:30]:
422
+ src = cc.get("source", "")
423
+ tgt = cc.get("target", "")
424
+ rel = cc.get("relation", "")
425
+ lines.append(f"- `{src}` →[{rel}]→ `{tgt}`")
426
+ lines.append("")
427
+
428
+ return "\n".join(lines) + "\n"
429
+
430
+
431
+ # ── Global index (short, ~200 tokens) ────────────────────────────────────────
432
+
433
+ def global_index(
434
+ projects: list[dict[str, Any]],
435
+ total_stats: dict[str, int],
436
+ ) -> str:
437
+ """Root index.md — kept short for quick loading in AI context.
438
+
439
+ Args:
440
+ projects: list of dicts: {name, framework}
441
+ total_stats: aggregate statistics
442
+ """
443
+ lines = [
444
+ "# CodeBeacon Wiki",
445
+ "",
446
+ f"**{total_stats.get('nodes', 0)} nodes · "
447
+ f"{total_stats.get('edges', 0)} edges · "
448
+ f"{total_stats.get('communities', 0)} communities**",
449
+ "",
450
+ "## Projects",
451
+ "",
452
+ ]
453
+ for p in sorted(projects, key=lambda x: x.get("name", "")):
454
+ name = p.get("name", "")
455
+ fw = p.get("framework", "")
456
+ lines.append(f"- [{name}](./{name}/index.md) — {fw}")
457
+
458
+ lines += [
459
+ "",
460
+ "## Quick Links",
461
+ "",
462
+ "- [Platform Overview](./overview.md)",
463
+ "- [All Routes](./routes.md)",
464
+ "- [Cross-Project Connections](./cross-project/connections.md)",
465
+ ]
466
+
467
+ return "\n".join(lines) + "\n"