codebeacon 0.1.7__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.7 → codebeacon-0.1.8}/PKG-INFO +1 -1
- codebeacon-0.1.8/codebeacon/__init__.py +1 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/cli.py +12 -7
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/graph/analyze.py +129 -35
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/graph/enrich.py +91 -4
- {codebeacon-0.1.7 → codebeacon-0.1.8}/pyproject.toml +1 -1
- codebeacon-0.1.7/codebeacon/__init__.py +0 -1
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.cursorrules +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.github/CODEOWNERS +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.github/dependabot.yml +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.github/workflows/ci.yml +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.github/workflows/release.yml +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/.gitignore +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/AGENTS.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/CLAUDE.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/LICENSE +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.de.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.es.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.fr.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.ja.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.ko.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.pt-BR.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/README.zh-CN.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/__main__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/cache.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/common/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/common/filters.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/common/symbols.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/common/types.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/config.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/contextmap/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/contextmap/generator.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/discover/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/discover/detector.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/discover/scanner.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/export/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/export/mcp.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/export/obsidian.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/base.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/components.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/dependencies.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/entities.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/README.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/actix.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/angular.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/aspnet.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/django.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/express.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/fastapi.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/flask.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/gin.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/ktor.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/laravel.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/nestjs.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/rails.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/react.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/spring_boot.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/svelte.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/tauri.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/vapor.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/queries/vue.scm +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/routes.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/semantic.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/extract/services.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/graph/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/graph/build.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/graph/cluster.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/skill/SKILL.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/wave.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/wiki/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/wiki/generator.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/wiki/index.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon/wiki/templates.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/codebeacon.yaml.example +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/docs/TRANSLATION_STATUS.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/public-plan.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/skill/SKILL.md +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/skill/install.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/__init__.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/conftest.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/actix/main.rs +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/angular/app.component.ts +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/aspnet/UserController.cs +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/django/views.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/express/userRouter.js +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/fastapi/main.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/flask/app.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/gin/main.go +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/ktor/UserRoutes.kt +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/laravel/UserController.php +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/nestjs/user.controller.ts +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/rails/users_controller.rb +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/react/UserPage.tsx +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/spring_boot/UserController.java +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/sveltekit/+page.svelte +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/vapor/routes.swift +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/fixtures/vue/UserList.vue +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_discover.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_entities.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_filters.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_graph.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_resolve.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_routes.py +0 -0
- {codebeacon-0.1.7 → codebeacon-0.1.8}/tests/test_services.py +0 -0
- {codebeacon-0.1.7 → 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(
|
|
@@ -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.7"
|
|
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
|
|
File without changes
|