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.
- codebeacon/__init__.py +1 -0
- codebeacon/__main__.py +3 -0
- codebeacon/cache.py +136 -0
- codebeacon/cli.py +391 -0
- codebeacon/common/__init__.py +0 -0
- codebeacon/common/filters.py +170 -0
- codebeacon/common/symbols.py +121 -0
- codebeacon/common/types.py +98 -0
- codebeacon/config.py +144 -0
- codebeacon/contextmap/__init__.py +0 -0
- codebeacon/contextmap/generator.py +602 -0
- codebeacon/discover/__init__.py +0 -0
- codebeacon/discover/detector.py +388 -0
- codebeacon/discover/scanner.py +192 -0
- codebeacon/export/__init__.py +0 -0
- codebeacon/export/mcp.py +515 -0
- codebeacon/export/obsidian.py +812 -0
- codebeacon/extract/__init__.py +22 -0
- codebeacon/extract/base.py +372 -0
- codebeacon/extract/components.py +357 -0
- codebeacon/extract/dependencies.py +140 -0
- codebeacon/extract/entities.py +575 -0
- codebeacon/extract/queries/README.md +116 -0
- codebeacon/extract/queries/actix.scm +115 -0
- codebeacon/extract/queries/angular.scm +155 -0
- codebeacon/extract/queries/aspnet.scm +159 -0
- codebeacon/extract/queries/django.scm +122 -0
- codebeacon/extract/queries/express.scm +124 -0
- codebeacon/extract/queries/fastapi.scm +152 -0
- codebeacon/extract/queries/flask.scm +120 -0
- codebeacon/extract/queries/gin.scm +142 -0
- codebeacon/extract/queries/ktor.scm +144 -0
- codebeacon/extract/queries/laravel.scm +172 -0
- codebeacon/extract/queries/nestjs.scm +183 -0
- codebeacon/extract/queries/rails.scm +114 -0
- codebeacon/extract/queries/react.scm +111 -0
- codebeacon/extract/queries/spring_boot.scm +204 -0
- codebeacon/extract/queries/svelte.scm +73 -0
- codebeacon/extract/queries/vapor.scm +130 -0
- codebeacon/extract/queries/vue.scm +123 -0
- codebeacon/extract/routes.py +910 -0
- codebeacon/extract/semantic.py +280 -0
- codebeacon/extract/services.py +597 -0
- codebeacon/graph/__init__.py +1 -0
- codebeacon/graph/analyze.py +281 -0
- codebeacon/graph/build.py +320 -0
- codebeacon/graph/cluster.py +160 -0
- codebeacon/graph/enrich.py +206 -0
- codebeacon/skill/SKILL.md +127 -0
- codebeacon/wave.py +292 -0
- codebeacon/wiki/__init__.py +0 -0
- codebeacon/wiki/generator.py +376 -0
- codebeacon/wiki/index.py +95 -0
- codebeacon/wiki/templates.py +467 -0
- codebeacon-0.1.2.dist-info/METADATA +319 -0
- codebeacon-0.1.2.dist-info/RECORD +59 -0
- codebeacon-0.1.2.dist-info/WHEEL +4 -0
- codebeacon-0.1.2.dist-info/entry_points.txt +2 -0
- codebeacon-0.1.2.dist-info/licenses/LICENSE +21 -0
codebeacon/wiki/index.py
ADDED
|
@@ -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"
|