codebeacon 0.1.6__tar.gz → 0.1.8__tar.gz
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-0.1.6 → codebeacon-0.1.8}/PKG-INFO +1 -1
- codebeacon-0.1.8/codebeacon/__init__.py +1 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/cli.py +12 -7
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/contextmap/generator.py +24 -28
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/graph/analyze.py +129 -35
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/graph/enrich.py +91 -4
- {codebeacon-0.1.6 → codebeacon-0.1.8}/pyproject.toml +1 -1
- codebeacon-0.1.6/codebeacon/__init__.py +0 -1
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.cursorrules +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.github/CODEOWNERS +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.github/dependabot.yml +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.github/workflows/ci.yml +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.github/workflows/release.yml +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/.gitignore +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/AGENTS.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/CLAUDE.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/LICENSE +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.de.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.es.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.fr.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.ja.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.ko.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.pt-BR.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/README.zh-CN.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/__main__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/cache.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/common/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/common/filters.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/common/symbols.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/common/types.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/config.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/contextmap/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/discover/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/discover/detector.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/discover/scanner.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/export/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/export/mcp.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/export/obsidian.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/base.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/components.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/dependencies.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/entities.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/README.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/actix.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/angular.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/aspnet.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/django.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/express.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/fastapi.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/flask.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/gin.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/ktor.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/laravel.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/nestjs.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/rails.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/react.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/spring_boot.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/svelte.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/tauri.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/vapor.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/queries/vue.scm +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/routes.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/semantic.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/extract/services.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/graph/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/graph/build.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/graph/cluster.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/skill/SKILL.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/wave.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/wiki/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/wiki/generator.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/wiki/index.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon/wiki/templates.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/codebeacon.yaml.example +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/docs/TRANSLATION_STATUS.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/public-plan.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/skill/SKILL.md +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/skill/install.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/__init__.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/conftest.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/actix/main.rs +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/angular/app.component.ts +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/aspnet/UserController.cs +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/django/views.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/express/userRouter.js +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/fastapi/main.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/flask/app.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/gin/main.go +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/ktor/UserRoutes.kt +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/laravel/UserController.php +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/nestjs/user.controller.ts +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/rails/users_controller.rb +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/react/UserPage.tsx +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/spring_boot/UserController.java +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/sveltekit/+page.svelte +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/vapor/routes.swift +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/fixtures/vue/UserList.vue +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_discover.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_entities.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_filters.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_graph.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_resolve.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_routes.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_services.py +0 -0
- {codebeacon-0.1.6 → codebeacon-0.1.8}/tests/test_wiki.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebeacon
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: Source code AST analysis tool for AI context generation — unified multi-framework knowledge graph
|
|
5
5
|
Project-URL: Homepage, https://github.com/codebeacon/codebeacon
|
|
6
6
|
Project-URL: Repository, https://github.com/codebeacon/codebeacon
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.8"
|
|
@@ -127,7 +127,7 @@ def _run_pipeline(projects, output_dir: str, args) -> int:
|
|
|
127
127
|
from codebeacon.cache import Cache
|
|
128
128
|
from codebeacon.wave import auto_wave
|
|
129
129
|
from codebeacon.graph.build import build_graph
|
|
130
|
-
from codebeacon.graph.enrich import enrich_http_api, enrich_shared_db
|
|
130
|
+
from codebeacon.graph.enrich import enrich_http_api, enrich_shared_db, enrich_ipc_invoke
|
|
131
131
|
from codebeacon.graph.cluster import cluster, apply_communities, score_all
|
|
132
132
|
|
|
133
133
|
cache = Cache(output_dir)
|
|
@@ -176,8 +176,13 @@ def _run_pipeline(projects, output_dir: str, args) -> int:
|
|
|
176
176
|
# Enrichment
|
|
177
177
|
api_edges = enrich_http_api(G)
|
|
178
178
|
db_edges = enrich_shared_db(G)
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
ipc_edges = enrich_ipc_invoke(G)
|
|
180
|
+
enriched_parts = []
|
|
181
|
+
if api_edges: enriched_parts.append(f"+{api_edges} calls_api")
|
|
182
|
+
if db_edges: enriched_parts.append(f"+{db_edges} shares_db_entity")
|
|
183
|
+
if ipc_edges: enriched_parts.append(f"+{ipc_edges} invokes_command")
|
|
184
|
+
if enriched_parts:
|
|
185
|
+
print(f" Enriched: {', '.join(enriched_parts)} edges")
|
|
181
186
|
|
|
182
187
|
# Community detection
|
|
183
188
|
print(" Detecting communities ...")
|
|
@@ -188,7 +193,7 @@ def _run_pipeline(projects, output_dir: str, args) -> int:
|
|
|
188
193
|
print(f" {n_communities} communities detected")
|
|
189
194
|
|
|
190
195
|
# Analysis
|
|
191
|
-
report = analyze(G, communities, cohesion)
|
|
196
|
+
report = analyze(G, communities, cohesion, project_paths={p.name: p.path for p in projects})
|
|
192
197
|
|
|
193
198
|
# Save outputs
|
|
194
199
|
import networkx.readwrite.json_graph as nxjson
|
|
@@ -249,7 +254,7 @@ def _run_deep_dive_pipeline(projects, workspace_output_dir: str, args) -> int:
|
|
|
249
254
|
from pathlib import Path
|
|
250
255
|
from codebeacon.graph.analyze import analyze, report_to_markdown
|
|
251
256
|
from codebeacon.graph.build import build_graph
|
|
252
|
-
from codebeacon.graph.enrich import enrich_http_api, enrich_shared_db
|
|
257
|
+
from codebeacon.graph.enrich import enrich_http_api, enrich_shared_db, enrich_ipc_invoke
|
|
253
258
|
from codebeacon.graph.cluster import cluster, apply_communities, score_all
|
|
254
259
|
from codebeacon.wiki.generator import generate_wiki
|
|
255
260
|
from codebeacon.export.obsidian import generate_obsidian_vault
|
|
@@ -364,7 +369,7 @@ def _run_deep_dive_pipeline(projects, workspace_output_dir: str, args) -> int:
|
|
|
364
369
|
n_communities = len(set(communities.values())) if communities else 0
|
|
365
370
|
print(f" {n_communities} communities")
|
|
366
371
|
|
|
367
|
-
report = analyze(G, communities, cohesion)
|
|
372
|
+
report = analyze(G, communities, cohesion, project_paths={project.name: project.path})
|
|
368
373
|
|
|
369
374
|
beacon_path = Path(proj_output_dir) / "beacon.json"
|
|
370
375
|
beacon_path.write_text(
|
|
@@ -418,7 +423,7 @@ def _run_deep_dive_pipeline(projects, workspace_output_dir: str, args) -> int:
|
|
|
418
423
|
n_communities_all = len(set(communities_all.values())) if communities_all else 0
|
|
419
424
|
print(f" {n_communities_all} communities detected")
|
|
420
425
|
|
|
421
|
-
report_all = analyze(G_all, communities_all, cohesion_all)
|
|
426
|
+
report_all = analyze(G_all, communities_all, cohesion_all, project_paths={p.name: p.path for p in projects})
|
|
422
427
|
|
|
423
428
|
beacon_path = workspace_path / "beacon.json"
|
|
424
429
|
beacon_path.write_text(
|
|
@@ -273,40 +273,42 @@ def _build_content(
|
|
|
273
273
|
lines = [
|
|
274
274
|
"# CLAUDE.md",
|
|
275
275
|
"",
|
|
276
|
-
"##
|
|
276
|
+
"## Lookup strategy",
|
|
277
277
|
"",
|
|
278
|
-
">
|
|
279
|
-
">",
|
|
280
|
-
"> Skipping these steps and reading source files directly is a rule violation.",
|
|
278
|
+
"> This repo ships a pre-built index in `.codebeacon/`. Check it first —",
|
|
279
|
+
"> most \"where is X\" questions resolve in one read without a full-repo search.",
|
|
281
280
|
"",
|
|
282
281
|
]
|
|
283
282
|
elif tool == "cursor":
|
|
284
283
|
lines = [
|
|
285
284
|
"# Project Context",
|
|
286
285
|
"",
|
|
287
|
-
"## Lookup
|
|
286
|
+
"## Lookup strategy",
|
|
288
287
|
"",
|
|
289
|
-
">
|
|
288
|
+
"> A pre-built index lives in `.codebeacon/`. Use the 3-step lookup below",
|
|
289
|
+
"> before reaching for Glob or Grep.",
|
|
290
290
|
"",
|
|
291
291
|
]
|
|
292
292
|
else: # agents
|
|
293
293
|
lines = [
|
|
294
294
|
"# AGENTS.md",
|
|
295
295
|
"",
|
|
296
|
-
"##
|
|
296
|
+
"## Lookup strategy",
|
|
297
297
|
"",
|
|
298
|
-
">
|
|
298
|
+
"> A pre-built index lives in `.codebeacon/`. Follow the 3-step lookup below",
|
|
299
|
+
"> so parallel agents converge on the same answer.",
|
|
299
300
|
"",
|
|
300
301
|
]
|
|
301
302
|
|
|
302
303
|
# ── Step 1: wiki ──
|
|
303
304
|
lines += [
|
|
304
|
-
"### Step 1
|
|
305
|
+
"### Step 1 — codebeacon wiki",
|
|
306
|
+
"Routes, controllers, services, entities.",
|
|
305
307
|
"```",
|
|
306
|
-
f"{codebeacon_dir}/wiki/index.md ←
|
|
307
|
-
f"{codebeacon_dir}/wiki/{{project}}/controllers/{{Name}}.md ←
|
|
308
|
-
f"{codebeacon_dir}/wiki/{{project}}/services/{{Name}}.md ←
|
|
309
|
-
f"{codebeacon_dir}/wiki/{{project}}/entities/{{Name}}.md ←
|
|
308
|
+
f"{codebeacon_dir}/wiki/index.md ← global index",
|
|
309
|
+
f"{codebeacon_dir}/wiki/{{project}}/controllers/{{Name}}.md ← controller logic",
|
|
310
|
+
f"{codebeacon_dir}/wiki/{{project}}/services/{{Name}}.md ← service methods",
|
|
311
|
+
f"{codebeacon_dir}/wiki/{{project}}/entities/{{Name}}.md ← data models",
|
|
310
312
|
f"{codebeacon_dir}/wiki/routes.md ← all API routes across projects",
|
|
311
313
|
"```",
|
|
312
314
|
"",
|
|
@@ -314,11 +316,9 @@ def _build_content(
|
|
|
314
316
|
|
|
315
317
|
# ── Step 2: obsidian ──
|
|
316
318
|
lines += [
|
|
317
|
-
"### Step 2
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"",
|
|
321
|
-
f"Look up by class name — replace `{{project}}` with the relevant folder:",
|
|
319
|
+
"### Step 2 — obsidian notes",
|
|
320
|
+
"Class-level detail (methods, fields, incoming/outgoing edges) the wiki omits.",
|
|
321
|
+
"Look up by class name — replace `{project}` with the relevant folder:",
|
|
322
322
|
"```",
|
|
323
323
|
f"{obsidian_path}/{{project}}/{{ClassName}}.md",
|
|
324
324
|
"```",
|
|
@@ -351,21 +351,17 @@ def _build_content(
|
|
|
351
351
|
|
|
352
352
|
# ── Step 3 ──
|
|
353
353
|
lines += [
|
|
354
|
-
"### Step 3
|
|
355
|
-
"
|
|
356
|
-
"No directory exploration, no Glob scans, no broad Grep searches.",
|
|
354
|
+
"### Step 3 — source file",
|
|
355
|
+
"Open the paths surfaced by Steps 1–2.",
|
|
357
356
|
"",
|
|
358
357
|
]
|
|
359
358
|
|
|
360
359
|
if tool == "claude":
|
|
361
360
|
lines += [
|
|
362
|
-
"###
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"- **DO NOT Read source files directly without checking Steps 1-2 first**",
|
|
367
|
-
"",
|
|
368
|
-
"Proceed to Step 3 only when Steps 1-2 are insufficient.",
|
|
361
|
+
"### Fall back to direct search",
|
|
362
|
+
"Reach for Glob, Grep, or the Explore agent when the index does not cover",
|
|
363
|
+
"what you need — new files, deep implementation details, or cross-cutting",
|
|
364
|
+
"searches that span projects.",
|
|
369
365
|
"",
|
|
370
366
|
]
|
|
371
367
|
|
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
These metrics help users understand their codebase structure at a glance.
|
|
4
4
|
|
|
5
5
|
Public API:
|
|
6
|
-
god_nodes(G, top_n, min_degree)
|
|
7
|
-
surprising_connections(G, communities)
|
|
8
|
-
hub_files(G, top_n)
|
|
9
|
-
analyze(G, communities, cohesion_scores
|
|
10
|
-
|
|
6
|
+
god_nodes(G, top_n, min_degree, project_paths) → list[GodNode]
|
|
7
|
+
surprising_connections(G, communities) → list[SurprisingConnection]
|
|
8
|
+
hub_files(G, top_n) → list[HubFile]
|
|
9
|
+
analyze(G, communities, cohesion_scores,
|
|
10
|
+
project_paths) → GraphReport
|
|
11
|
+
report_to_markdown(report) → str
|
|
11
12
|
"""
|
|
12
13
|
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
16
|
+
import os
|
|
17
|
+
from collections import defaultdict
|
|
15
18
|
from dataclasses import dataclass, field
|
|
16
19
|
from typing import Optional
|
|
17
20
|
|
|
@@ -22,15 +25,15 @@ import networkx as nx
|
|
|
22
25
|
|
|
23
26
|
@dataclass
|
|
24
27
|
class GodNode:
|
|
25
|
-
"""A
|
|
26
|
-
|
|
27
|
-
label: str
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
"""A directory with unusually high cross-boundary coupling."""
|
|
29
|
+
folder_path: str # relative path within project: "lib/utils" or "src-tauri/src"
|
|
30
|
+
label: str # folder name: "utils"
|
|
31
|
+
project: str # owning project: "desktop"
|
|
32
|
+
child_count: int # number of nodes inside this folder
|
|
33
|
+
in_degree: int # external → folder edges
|
|
34
|
+
out_degree: int # folder → external edges
|
|
35
|
+
degree: int # total cross-boundary edges
|
|
36
|
+
centrality: float # degree / (total_nodes - child_count)
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
@dataclass
|
|
@@ -70,37 +73,123 @@ class GraphReport:
|
|
|
70
73
|
|
|
71
74
|
# ── Analysis functions ────────────────────────────────────────────────────────
|
|
72
75
|
|
|
76
|
+
def _infer_project_paths(G: nx.DiGraph) -> dict[str, str]:
|
|
77
|
+
"""Infer project root paths from source_file attributes in the graph.
|
|
78
|
+
|
|
79
|
+
Groups nodes by their ``project`` attribute, then finds the common path
|
|
80
|
+
prefix of all source_file directories within each project.
|
|
81
|
+
"""
|
|
82
|
+
project_dirs: dict[str, list[str]] = defaultdict(list)
|
|
83
|
+
for _node_id, data in G.nodes(data=True):
|
|
84
|
+
sf = data.get("source_file", "")
|
|
85
|
+
proj = data.get("project", "")
|
|
86
|
+
if sf and proj:
|
|
87
|
+
project_dirs[proj].append(os.path.dirname(os.path.abspath(sf)))
|
|
88
|
+
|
|
89
|
+
result: dict[str, str] = {}
|
|
90
|
+
for proj, dirs in project_dirs.items():
|
|
91
|
+
if dirs:
|
|
92
|
+
result[proj] = os.path.commonpath(dirs)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
73
96
|
def god_nodes(
|
|
74
97
|
G: nx.DiGraph,
|
|
75
98
|
top_n: int = 20,
|
|
76
99
|
min_degree: int = 5,
|
|
100
|
+
project_paths: Optional[dict[str, str]] = None,
|
|
77
101
|
) -> list[GodNode]:
|
|
78
|
-
"""Find
|
|
102
|
+
"""Find directories with the highest cross-boundary coupling.
|
|
103
|
+
|
|
104
|
+
Counts only edges that cross folder boundaries (cross-boundary edges).
|
|
105
|
+
Intra-folder edges are ignored, so a single large wrapper file can no
|
|
106
|
+
longer dominate solely because of its high node-level degree.
|
|
79
107
|
|
|
80
108
|
Args:
|
|
81
109
|
G: the knowledge graph
|
|
82
|
-
top_n: return at most this many
|
|
83
|
-
min_degree: minimum
|
|
110
|
+
top_n: return at most this many folders
|
|
111
|
+
min_degree: minimum cross-boundary edge count to qualify
|
|
112
|
+
project_paths: optional dict mapping project name → absolute project
|
|
113
|
+
root path. When None, paths are inferred automatically
|
|
114
|
+
from source_file attributes via ``_infer_project_paths``.
|
|
84
115
|
|
|
85
116
|
Returns:
|
|
86
|
-
List of GodNode sorted by degree descending.
|
|
117
|
+
List of GodNode (folder-level) sorted by degree descending.
|
|
87
118
|
"""
|
|
88
|
-
|
|
119
|
+
if project_paths is None:
|
|
120
|
+
project_paths = _infer_project_paths(G)
|
|
121
|
+
|
|
122
|
+
total_nodes = G.number_of_nodes()
|
|
123
|
+
|
|
124
|
+
# Step 1: build node → (folder_key, folder_path, project) mapping.
|
|
125
|
+
# folder_key uses "{project}/{rel}" for cross-project uniqueness.
|
|
126
|
+
# folder_path stores only the relative portion shown in the report.
|
|
127
|
+
node_folder_key: dict[str, str] = {}
|
|
128
|
+
key_to_rel: dict[str, str] = {}
|
|
129
|
+
key_to_project: dict[str, str] = {}
|
|
89
130
|
|
|
90
|
-
results: list[GodNode] = []
|
|
91
131
|
for node_id, data in G.nodes(data=True):
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
sf = data.get("source_file", "")
|
|
133
|
+
proj = data.get("project", "")
|
|
134
|
+
if not sf:
|
|
135
|
+
continue
|
|
136
|
+
dirname = os.path.dirname(os.path.abspath(sf))
|
|
137
|
+
if proj and proj in project_paths:
|
|
138
|
+
try:
|
|
139
|
+
rel = os.path.relpath(dirname, project_paths[proj])
|
|
140
|
+
except ValueError:
|
|
141
|
+
rel = dirname
|
|
142
|
+
# Skip nodes whose source lives outside the project root
|
|
143
|
+
if rel.startswith(".."):
|
|
144
|
+
rel = dirname
|
|
145
|
+
else:
|
|
146
|
+
rel = dirname
|
|
147
|
+
key = f"{proj}/{rel}" if proj else rel
|
|
148
|
+
node_folder_key[node_id] = key
|
|
149
|
+
key_to_rel[key] = rel
|
|
150
|
+
key_to_project[key] = proj
|
|
151
|
+
|
|
152
|
+
# Step 2: count cross-boundary edges in a single pass.
|
|
153
|
+
folder_in: dict[str, int] = defaultdict(int)
|
|
154
|
+
folder_out: dict[str, int] = defaultdict(int)
|
|
155
|
+
folder_children: dict[str, set] = defaultdict(set)
|
|
156
|
+
|
|
157
|
+
for node_id in G.nodes():
|
|
158
|
+
fk = node_folder_key.get(node_id)
|
|
159
|
+
if fk:
|
|
160
|
+
folder_children[fk].add(node_id)
|
|
161
|
+
|
|
162
|
+
for src, tgt in G.edges():
|
|
163
|
+
src_key = node_folder_key.get(src)
|
|
164
|
+
tgt_key = node_folder_key.get(tgt)
|
|
165
|
+
if src_key is None or tgt_key is None:
|
|
94
166
|
continue
|
|
167
|
+
if src_key != tgt_key:
|
|
168
|
+
folder_out[src_key] += 1
|
|
169
|
+
folder_in[tgt_key] += 1
|
|
170
|
+
|
|
171
|
+
# Step 3: filter, build GodNode list, sort.
|
|
172
|
+
results: list[GodNode] = []
|
|
173
|
+
for folder_key in folder_children:
|
|
174
|
+
in_d = folder_in.get(folder_key, 0)
|
|
175
|
+
out_d = folder_out.get(folder_key, 0)
|
|
176
|
+
degree = in_d + out_d
|
|
177
|
+
if degree < min_degree:
|
|
178
|
+
continue
|
|
179
|
+
child_count = len(folder_children[folder_key])
|
|
180
|
+
centrality = degree / max(1, total_nodes - child_count)
|
|
181
|
+
rel = key_to_rel.get(folder_key, folder_key)
|
|
182
|
+
proj = key_to_project.get(folder_key, "")
|
|
183
|
+
label = os.path.basename(rel) if rel not in (".", "") else "(root)"
|
|
95
184
|
results.append(GodNode(
|
|
96
|
-
|
|
97
|
-
label=
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
185
|
+
folder_path=rel,
|
|
186
|
+
label=label,
|
|
187
|
+
project=proj,
|
|
188
|
+
child_count=child_count,
|
|
189
|
+
in_degree=in_d,
|
|
190
|
+
out_degree=out_d,
|
|
191
|
+
degree=degree,
|
|
192
|
+
centrality=centrality,
|
|
104
193
|
))
|
|
105
194
|
|
|
106
195
|
results.sort(key=lambda n: n.degree, reverse=True)
|
|
@@ -207,6 +296,7 @@ def analyze(
|
|
|
207
296
|
G: nx.DiGraph,
|
|
208
297
|
communities: Optional[dict[str, int]] = None,
|
|
209
298
|
cohesion_scores: Optional[dict[int, float]] = None,
|
|
299
|
+
project_paths: Optional[dict[str, str]] = None,
|
|
210
300
|
) -> GraphReport:
|
|
211
301
|
"""Run all analyses and return a unified GraphReport.
|
|
212
302
|
|
|
@@ -214,6 +304,8 @@ def analyze(
|
|
|
214
304
|
G: built knowledge graph (output of build.py + optional enrich.py)
|
|
215
305
|
communities: optional community mapping from cluster.py
|
|
216
306
|
cohesion_scores: optional per-community cohesion scores from cluster.score_all()
|
|
307
|
+
project_paths: optional dict mapping project name → absolute project root path.
|
|
308
|
+
When None, paths are inferred automatically from the graph.
|
|
217
309
|
"""
|
|
218
310
|
report = GraphReport(
|
|
219
311
|
node_count=G.number_of_nodes(),
|
|
@@ -224,7 +316,7 @@ def analyze(
|
|
|
224
316
|
isolated_nodes=sum(1 for n in G.nodes() if G.degree(n) == 0),
|
|
225
317
|
)
|
|
226
318
|
|
|
227
|
-
report.god_nodes = god_nodes(G)
|
|
319
|
+
report.god_nodes = god_nodes(G, project_paths=project_paths)
|
|
228
320
|
report.hub_files = hub_files(G)
|
|
229
321
|
|
|
230
322
|
if communities:
|
|
@@ -248,12 +340,14 @@ def report_to_markdown(report: GraphReport) -> str:
|
|
|
248
340
|
]
|
|
249
341
|
|
|
250
342
|
if report.god_nodes:
|
|
251
|
-
lines += ["## God Nodes (High
|
|
252
|
-
lines.append(
|
|
253
|
-
|
|
343
|
+
lines += ["## God Nodes (High-Coupling Directories)", ""]
|
|
344
|
+
lines.append(
|
|
345
|
+
f"{'Folder':<44} {'Project':<12} {'Cross-Edges':>11} {'Children':>8} {'Centrality':>10}"
|
|
346
|
+
)
|
|
347
|
+
lines.append("-" * 89)
|
|
254
348
|
for gn in report.god_nodes[:10]:
|
|
255
349
|
lines.append(
|
|
256
|
-
f"{gn.
|
|
350
|
+
f"{gn.folder_path:<44} {gn.project:<12} {gn.degree:>11} {gn.child_count:>8} {gn.centrality:>10.4f}"
|
|
257
351
|
)
|
|
258
352
|
lines.append("")
|
|
259
353
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""Graph enrichment: HTTP
|
|
1
|
+
"""Graph enrichment: HTTP/IPC cross-service edges + shared DB entity edges.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
1. enrich_http_api()
|
|
5
|
-
2. enrich_shared_db()
|
|
3
|
+
Three enrichment passes run AFTER the base graph is built by build.py:
|
|
4
|
+
1. enrich_http_api() — frontend URL calls → backend routes (calls_api edges)
|
|
5
|
+
2. enrich_shared_db() — same DAO/Entity across services (shares_db_entity edges)
|
|
6
|
+
3. enrich_ipc_invoke() — frontend invoke("cmd") → IPC command routes (invokes_command edges)
|
|
7
|
+
Covers Tauri, Electron ipcRenderer, and any invoke()-pattern IPC framework.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -191,6 +193,91 @@ def enrich_shared_db(G: nx.DiGraph) -> int:
|
|
|
191
193
|
return added
|
|
192
194
|
|
|
193
195
|
|
|
196
|
+
# ── IPC invoke enrichment (Tauri, Electron, etc.) ────────────────────────────
|
|
197
|
+
|
|
198
|
+
# Regexes for IPC invoke patterns across desktop/hybrid frameworks:
|
|
199
|
+
# Tauri: invoke("cmd") invoke<T>("cmd")
|
|
200
|
+
# Electron: ipcRenderer.invoke("cmd") ipcRenderer.send("cmd")
|
|
201
|
+
_IPC_INVOKE_RES = [
|
|
202
|
+
re.compile(r"""invoke\s*(?:<[^>]*>)?\s*\(\s*["'](\w+)["']"""),
|
|
203
|
+
re.compile(r"""ipcRenderer\.(?:invoke|send)\s*\(\s*["']([^"']+)["']"""),
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _extract_ipc_commands(source_file: str) -> list[str]:
|
|
208
|
+
"""Extract IPC invoke/send command names from a frontend source file."""
|
|
209
|
+
try:
|
|
210
|
+
content = Path(source_file).read_text(encoding="utf-8", errors="replace")
|
|
211
|
+
except OSError:
|
|
212
|
+
return []
|
|
213
|
+
commands: set[str] = set()
|
|
214
|
+
for pat in _IPC_INVOKE_RES:
|
|
215
|
+
for m in pat.finditer(content):
|
|
216
|
+
commands.add(m.group(1))
|
|
217
|
+
return list(commands)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def enrich_ipc_invoke(G: nx.DiGraph) -> int:
|
|
221
|
+
"""Add invokes_command edges: frontend invoke("cmd") → backend IPC command route.
|
|
222
|
+
|
|
223
|
+
Framework-agnostic — works with any route whose method is INVOKE,
|
|
224
|
+
regardless of backend framework (Tauri, Electron, etc.).
|
|
225
|
+
|
|
226
|
+
Strategy:
|
|
227
|
+
- Collect all 'route' nodes where method == "INVOKE"
|
|
228
|
+
- Extract the command name from the route handler
|
|
229
|
+
- For each frontend component, scan for invoke()/ipcRenderer.invoke() calls
|
|
230
|
+
- Match command names across projects
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Number of new invokes_command edges added.
|
|
234
|
+
"""
|
|
235
|
+
added = 0
|
|
236
|
+
|
|
237
|
+
# Build command lookup: handler_name → (node_id, project)
|
|
238
|
+
cmd_map: dict[str, tuple[str, str]] = {}
|
|
239
|
+
for node_id, data in G.nodes(data=True):
|
|
240
|
+
if data.get("type") != "route":
|
|
241
|
+
continue
|
|
242
|
+
method = data.get("method", "")
|
|
243
|
+
if method != "INVOKE":
|
|
244
|
+
continue
|
|
245
|
+
handler = data.get("label", "").split(" ")[0] # "handler [INVOKE /...]" → "handler"
|
|
246
|
+
if handler:
|
|
247
|
+
cmd_map[handler] = (node_id, data.get("project", ""))
|
|
248
|
+
|
|
249
|
+
if not cmd_map:
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
# Find frontend component nodes and scan for IPC calls
|
|
253
|
+
for node_id, data in G.nodes(data=True):
|
|
254
|
+
if data.get("type") != "component":
|
|
255
|
+
continue
|
|
256
|
+
src_proj = data.get("project", "")
|
|
257
|
+
src_file = data.get("source_file", "")
|
|
258
|
+
if not src_file:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
commands = _extract_ipc_commands(src_file)
|
|
262
|
+
for cmd in commands:
|
|
263
|
+
if cmd not in cmd_map:
|
|
264
|
+
continue
|
|
265
|
+
target_id, target_proj = cmd_map[cmd]
|
|
266
|
+
if target_proj == src_proj:
|
|
267
|
+
continue
|
|
268
|
+
if not G.has_edge(node_id, target_id):
|
|
269
|
+
G.add_edge(
|
|
270
|
+
node_id, target_id,
|
|
271
|
+
relation="invokes_command",
|
|
272
|
+
confidence="EXTRACTED",
|
|
273
|
+
confidence_score=1.0,
|
|
274
|
+
source_file=src_file,
|
|
275
|
+
)
|
|
276
|
+
added += 1
|
|
277
|
+
|
|
278
|
+
return added
|
|
279
|
+
|
|
280
|
+
|
|
194
281
|
# ── URL / path utilities ──────────────────────────────────────────────────────
|
|
195
282
|
|
|
196
283
|
def _normalize_path(path: str) -> str:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codebeacon"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
description = "Source code AST analysis tool for AI context generation — unified multi-framework knowledge graph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|