pycode-kg 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pycode_kg/.DS_Store +0 -0
- pycode_kg/__init__.py +91 -0
- pycode_kg/__main__.py +11 -0
- pycode_kg/analysis/__init__.py +15 -0
- pycode_kg/analysis/bridge.py +108 -0
- pycode_kg/analysis/centrality.py +412 -0
- pycode_kg/analysis/framework_detector.py +103 -0
- pycode_kg/analysis/hybrid_rank.py +53 -0
- pycode_kg/app.py +1335 -0
- pycode_kg/architecture.py +624 -0
- pycode_kg/build_pycodekg_lancedb.py +15 -0
- pycode_kg/build_pycodekg_sqlite.py +15 -0
- pycode_kg/cli/__init__.py +29 -0
- pycode_kg/cli/cmd_analyze.py +96 -0
- pycode_kg/cli/cmd_architecture.py +150 -0
- pycode_kg/cli/cmd_bridges.py +26 -0
- pycode_kg/cli/cmd_build.py +154 -0
- pycode_kg/cli/cmd_build_full.py +242 -0
- pycode_kg/cli/cmd_centrality.py +131 -0
- pycode_kg/cli/cmd_explain.py +180 -0
- pycode_kg/cli/cmd_framework_nodes.py +18 -0
- pycode_kg/cli/cmd_hooks.py +137 -0
- pycode_kg/cli/cmd_init.py +312 -0
- pycode_kg/cli/cmd_mcp.py +71 -0
- pycode_kg/cli/cmd_model.py +53 -0
- pycode_kg/cli/cmd_query.py +211 -0
- pycode_kg/cli/cmd_snapshot.py +421 -0
- pycode_kg/cli/cmd_viz.py +180 -0
- pycode_kg/cli/main.py +23 -0
- pycode_kg/cli/options.py +63 -0
- pycode_kg/config.py +78 -0
- pycode_kg/graph.py +125 -0
- pycode_kg/index.py +542 -0
- pycode_kg/kg.py +220 -0
- pycode_kg/layout3d.py +470 -0
- pycode_kg/mcp/bridge_tools.py +19 -0
- pycode_kg/mcp/framework_tools.py +18 -0
- pycode_kg/mcp_server.py +1965 -0
- pycode_kg/module/__init__.py +83 -0
- pycode_kg/module/base.py +720 -0
- pycode_kg/module/extractor.py +276 -0
- pycode_kg/module/types.py +532 -0
- pycode_kg/pycodekg.py +543 -0
- pycode_kg/pycodekg_query.py +1 -0
- pycode_kg/pycodekg_snippet_packer.py +1 -0
- pycode_kg/pycodekg_thorough_analysis.py +2751 -0
- pycode_kg/pycodekg_viz.py +1 -0
- pycode_kg/pycodekg_viz3d.py +1 -0
- pycode_kg/ranking/__init__.py +1 -0
- pycode_kg/ranking/cli_rank.py +92 -0
- pycode_kg/ranking/coderank.py +555 -0
- pycode_kg/snapshots.py +612 -0
- pycode_kg/sql/004_add_centrality_table.sql +12 -0
- pycode_kg/store.py +766 -0
- pycode_kg/utils.py +39 -0
- pycode_kg/visitor.py +413 -0
- pycode_kg/viz3d.py +1353 -0
- pycode_kg/viz3d_timeline.py +364 -0
- pycode_kg-0.16.0.dist-info/METADATA +305 -0
- pycode_kg-0.16.0.dist-info/RECORD +63 -0
- pycode_kg-0.16.0.dist-info/WHEEL +4 -0
- pycode_kg-0.16.0.dist-info/entry_points.txt +19 -0
- pycode_kg-0.16.0.dist-info/licenses/LICENSE +94 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""
|
|
2
|
+
architecture.py — Coherent Architectural Analysis
|
|
3
|
+
|
|
4
|
+
Provides intelligent analysis of codebase architecture, producing humanreadable descriptions of:
|
|
5
|
+
- Module layers and their purposes
|
|
6
|
+
- Key architectural components (classes, functions)
|
|
7
|
+
- Inter-module dependencies and coupling
|
|
8
|
+
- Data flow patterns
|
|
9
|
+
- Critical paths and integration points
|
|
10
|
+
- Complexity hotspots and risk factors
|
|
11
|
+
- Health signals (docstring coverage, circular dependencies, issues)
|
|
12
|
+
|
|
13
|
+
Outputs both Markdown (human-readable) and JSON (machine-ingestion) formats,
|
|
14
|
+
with timestamp/version provenance stamping.
|
|
15
|
+
|
|
16
|
+
Integrates thorough analysis results for richer architectural insights suitable
|
|
17
|
+
for infographic generation and decision-making.
|
|
18
|
+
|
|
19
|
+
Usage
|
|
20
|
+
-----
|
|
21
|
+
>>> from pycode_kg.architecture import ArchitectureAnalyzer
|
|
22
|
+
>>> analyzer = ArchitectureAnalyzer(store, repo_root, version="0.5.1")
|
|
23
|
+
>>> analyzer.incorporate_thorough_analysis(analysis_results)
|
|
24
|
+
>>> arch_md = analyzer.analyze_to_markdown()
|
|
25
|
+
>>> arch_json = analyzer.analyze_to_json()
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import asdict, dataclass, field
|
|
32
|
+
from datetime import UTC, datetime
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from pycode_kg.store import GraphStore
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ModuleLayer:
|
|
41
|
+
"""Represents a logical layer or module group."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
description: str
|
|
45
|
+
modules: list[str] # module paths in this layer
|
|
46
|
+
responsibilities: list[str] # high-level responsibilities
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ComponentNode:
|
|
51
|
+
"""A significant architectural component."""
|
|
52
|
+
|
|
53
|
+
node_id: str
|
|
54
|
+
name: str
|
|
55
|
+
kind: str # class, function, module
|
|
56
|
+
description: str
|
|
57
|
+
file: str
|
|
58
|
+
lineno: int
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ArchitectureGraph:
|
|
63
|
+
"""Structured representation of architecture with provenance."""
|
|
64
|
+
|
|
65
|
+
title: str
|
|
66
|
+
description: str
|
|
67
|
+
layers: list[ModuleLayer]
|
|
68
|
+
key_components: list[ComponentNode]
|
|
69
|
+
critical_paths: list[dict] # important call chains
|
|
70
|
+
coupling_summary: dict[str, Any] # module dependencies and strength
|
|
71
|
+
# Metadata for provenance
|
|
72
|
+
generated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
73
|
+
version: str = "unknown"
|
|
74
|
+
commit: str = "unknown"
|
|
75
|
+
# Thorough analysis integration
|
|
76
|
+
complexity_hotspots: list[dict] = field(default_factory=list)
|
|
77
|
+
health_signals: dict[str, Any] = field(default_factory=dict)
|
|
78
|
+
issues: list[str] = field(default_factory=list)
|
|
79
|
+
strengths: list[str] = field(default_factory=list)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class ArchitectureAnalyzer:
|
|
83
|
+
"""Analyzes codebase architecture and produces coherent descriptions."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self, store: GraphStore, repo_root: Path, version: str = "unknown", commit: str = "unknown"
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Initialize architecture analyzer.
|
|
90
|
+
|
|
91
|
+
:param store: GraphStore instance for querying graph.
|
|
92
|
+
:param repo_root: Root directory of repository.
|
|
93
|
+
:param version: Version string for provenance stamping.
|
|
94
|
+
:param commit: Commit hash for provenance stamping.
|
|
95
|
+
"""
|
|
96
|
+
self.store = store
|
|
97
|
+
self.repo_root = Path(repo_root)
|
|
98
|
+
self.version = version
|
|
99
|
+
self.commit = commit
|
|
100
|
+
self.thorough_analysis: dict[str, Any] = {}
|
|
101
|
+
|
|
102
|
+
def incorporate_thorough_analysis(self, analysis_results: dict[str, Any]) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Incorporate results from thorough analysis into architecture.
|
|
105
|
+
|
|
106
|
+
Enriches the architecture description with complexity hotspots,
|
|
107
|
+
issues, strengths, and health signals from the PyCodeKG analyzer.
|
|
108
|
+
|
|
109
|
+
:param analysis_results: Output from PyCodeKGAnalyzer.run_analysis()
|
|
110
|
+
"""
|
|
111
|
+
self.thorough_analysis = analysis_results
|
|
112
|
+
|
|
113
|
+
def analyze_to_markdown(self) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Analyze architecture and produce Markdown description.
|
|
116
|
+
|
|
117
|
+
Returns a comprehensive architectural overview in Markdown format,
|
|
118
|
+
suitable for documentation, infographics, and human consumption.
|
|
119
|
+
Includes provenance metadata and thorough analysis integration.
|
|
120
|
+
|
|
121
|
+
:return: Markdown string describing the architecture.
|
|
122
|
+
"""
|
|
123
|
+
arch = self._build_architecture_graph()
|
|
124
|
+
|
|
125
|
+
lines = []
|
|
126
|
+
# Header with provenance
|
|
127
|
+
lines.append(f"# {arch.title}")
|
|
128
|
+
lines.append("")
|
|
129
|
+
lines.append(
|
|
130
|
+
f"> **Generated:** {arch.generated_at} | **Version:** {arch.version} | **Commit:** {arch.commit}"
|
|
131
|
+
)
|
|
132
|
+
lines.append("")
|
|
133
|
+
lines.append(arch.description)
|
|
134
|
+
lines.append("")
|
|
135
|
+
|
|
136
|
+
# Layers section
|
|
137
|
+
lines.append("## Architectural Layers")
|
|
138
|
+
lines.append("")
|
|
139
|
+
for layer in arch.layers:
|
|
140
|
+
lines.append(f"### {layer.name}")
|
|
141
|
+
lines.append(layer.description)
|
|
142
|
+
lines.append("")
|
|
143
|
+
if layer.responsibilities:
|
|
144
|
+
lines.append("**Responsibilities:**")
|
|
145
|
+
for resp in layer.responsibilities:
|
|
146
|
+
lines.append(f"- {resp}")
|
|
147
|
+
lines.append("")
|
|
148
|
+
if layer.modules:
|
|
149
|
+
lines.append("**Modules:**")
|
|
150
|
+
for mod in layer.modules:
|
|
151
|
+
lines.append(f"- `{mod}`")
|
|
152
|
+
lines.append("")
|
|
153
|
+
|
|
154
|
+
# Key components
|
|
155
|
+
if arch.key_components:
|
|
156
|
+
lines.append("## Key Components")
|
|
157
|
+
lines.append("")
|
|
158
|
+
for comp in arch.key_components:
|
|
159
|
+
lines.append(f"### {comp.name}")
|
|
160
|
+
lines.append(f"**Type:** `{comp.kind}` | **File:** `{comp.file}:{comp.lineno}`")
|
|
161
|
+
lines.append("")
|
|
162
|
+
lines.append(comp.description)
|
|
163
|
+
lines.append("")
|
|
164
|
+
|
|
165
|
+
# Critical paths
|
|
166
|
+
if arch.critical_paths:
|
|
167
|
+
lines.append("## Critical Paths")
|
|
168
|
+
lines.append("")
|
|
169
|
+
for i, path in enumerate(arch.critical_paths, 1):
|
|
170
|
+
lines.append(f"### Path {i}: {path.get('name', 'Key Workflow')}")
|
|
171
|
+
lines.append(path.get("description", ""))
|
|
172
|
+
lines.append("")
|
|
173
|
+
if "steps" in path:
|
|
174
|
+
for step in path["steps"]:
|
|
175
|
+
lines.append(f"- {step}")
|
|
176
|
+
lines.append("")
|
|
177
|
+
|
|
178
|
+
# Coupling analysis
|
|
179
|
+
if arch.coupling_summary:
|
|
180
|
+
lines.append("## Dependency & Coupling Analysis")
|
|
181
|
+
lines.append("")
|
|
182
|
+
lines.append("### Module Dependencies")
|
|
183
|
+
for mod, deps in arch.coupling_summary.get("dependencies", {}).items():
|
|
184
|
+
if deps:
|
|
185
|
+
lines.append(f"**{mod}**")
|
|
186
|
+
for dep in deps:
|
|
187
|
+
lines.append(f"- imports: {dep}")
|
|
188
|
+
lines.append("")
|
|
189
|
+
|
|
190
|
+
# Complexity hotspots with risk context
|
|
191
|
+
if arch.complexity_hotspots:
|
|
192
|
+
lines.append("## Complexity Hotspots & Risk Areas")
|
|
193
|
+
lines.append("")
|
|
194
|
+
lines.append(
|
|
195
|
+
"These functions have high complexity or connectivity. "
|
|
196
|
+
"Changes require careful testing and impact assessment."
|
|
197
|
+
)
|
|
198
|
+
lines.append("")
|
|
199
|
+
|
|
200
|
+
# Group by type for clarity
|
|
201
|
+
hubs = [h for h in arch.complexity_hotspots if h.get("type") == "integration_hub"]
|
|
202
|
+
coordinators = [h for h in arch.complexity_hotspots if h.get("type") == "coordinator"]
|
|
203
|
+
|
|
204
|
+
if hubs:
|
|
205
|
+
lines.append("### Integration Hubs (High Fan-In)")
|
|
206
|
+
lines.append("Heavily called functions. Changes impact many dependents.")
|
|
207
|
+
lines.append("")
|
|
208
|
+
for i, hotspot in enumerate(hubs[:5], 1):
|
|
209
|
+
name = hotspot.get("name", "unknown")
|
|
210
|
+
fan_in = hotspot.get("fan_in", 0)
|
|
211
|
+
fan_out = hotspot.get("fan_out", 0)
|
|
212
|
+
risk = hotspot.get("risk_level", "low")
|
|
213
|
+
desc = hotspot.get("description", "")
|
|
214
|
+
lines.append(f"**{i}. {name}**")
|
|
215
|
+
lines.append(f" Risk: {risk.upper()} | Callers: {fan_in} | Calls: {fan_out}")
|
|
216
|
+
if desc:
|
|
217
|
+
lines.append(f" {desc}")
|
|
218
|
+
lines.append("")
|
|
219
|
+
|
|
220
|
+
if coordinators:
|
|
221
|
+
lines.append("### Coordinators (High Fan-Out)")
|
|
222
|
+
lines.append("Complex orchestration logic. Refactoring candidates.")
|
|
223
|
+
lines.append("")
|
|
224
|
+
for i, hotspot in enumerate(coordinators[:5], 1):
|
|
225
|
+
name = hotspot.get("name", "unknown")
|
|
226
|
+
fan_in = hotspot.get("fan_in", 0)
|
|
227
|
+
fan_out = hotspot.get("fan_out", 0)
|
|
228
|
+
risk = hotspot.get("risk_level", "low")
|
|
229
|
+
desc = hotspot.get("description", "")
|
|
230
|
+
lines.append(f"**{i}. {name}**")
|
|
231
|
+
lines.append(f" Risk: {risk.upper()} | Callers: {fan_in} | Calls: {fan_out}")
|
|
232
|
+
if desc:
|
|
233
|
+
lines.append(f" {desc}")
|
|
234
|
+
lines.append("")
|
|
235
|
+
|
|
236
|
+
# Health signals
|
|
237
|
+
if arch.health_signals:
|
|
238
|
+
lines.append("## Health & Quality Signals")
|
|
239
|
+
lines.append("")
|
|
240
|
+
for signal, value in arch.health_signals.items():
|
|
241
|
+
if isinstance(value, float):
|
|
242
|
+
lines.append(
|
|
243
|
+
f"- **{signal}:** {value:.1%}"
|
|
244
|
+
if signal.lower().endswith("coverage")
|
|
245
|
+
else f"- **{signal}:** {value}"
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
lines.append(f"- **{signal}:** {value}")
|
|
249
|
+
lines.append("")
|
|
250
|
+
|
|
251
|
+
# Issues and strengths
|
|
252
|
+
if arch.issues or arch.strengths:
|
|
253
|
+
if arch.issues:
|
|
254
|
+
lines.append("## Issues & Risks")
|
|
255
|
+
lines.append("")
|
|
256
|
+
for issue in arch.issues[:10]:
|
|
257
|
+
lines.append(f"- {issue}")
|
|
258
|
+
lines.append("")
|
|
259
|
+
|
|
260
|
+
if arch.strengths:
|
|
261
|
+
lines.append("## Strengths")
|
|
262
|
+
lines.append("")
|
|
263
|
+
for strength in arch.strengths[:10]:
|
|
264
|
+
lines.append(f"- {strength}")
|
|
265
|
+
lines.append("")
|
|
266
|
+
|
|
267
|
+
return "\n".join(lines)
|
|
268
|
+
|
|
269
|
+
def analyze_to_json(self) -> str:
|
|
270
|
+
"""
|
|
271
|
+
Analyze architecture and produce JSON description.
|
|
272
|
+
|
|
273
|
+
Returns a structured JSON representation of the architecture suitable
|
|
274
|
+
for machine ingestion, tool integration, infographic generation,
|
|
275
|
+
and programmatic processing. Includes full provenance and metadata.
|
|
276
|
+
|
|
277
|
+
:return: JSON string describing the architecture.
|
|
278
|
+
"""
|
|
279
|
+
arch = self._build_architecture_graph()
|
|
280
|
+
|
|
281
|
+
# Build risk assessment summary
|
|
282
|
+
risk_summary = {
|
|
283
|
+
"total_hotspots": len(arch.complexity_hotspots),
|
|
284
|
+
"high_risk_count": sum(
|
|
285
|
+
1 for h in arch.complexity_hotspots if h.get("risk_level") == "high"
|
|
286
|
+
),
|
|
287
|
+
"integration_hubs": sum(
|
|
288
|
+
1 for h in arch.complexity_hotspots if h.get("type") == "integration_hub"
|
|
289
|
+
),
|
|
290
|
+
"coordinators": sum(
|
|
291
|
+
1 for h in arch.complexity_hotspots if h.get("type") == "coordinator"
|
|
292
|
+
),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
data = {
|
|
296
|
+
"metadata": {
|
|
297
|
+
"generated_at": arch.generated_at,
|
|
298
|
+
"version": arch.version,
|
|
299
|
+
"commit": arch.commit,
|
|
300
|
+
"title": arch.title,
|
|
301
|
+
},
|
|
302
|
+
"description": arch.description,
|
|
303
|
+
"layers": [asdict(layer) for layer in arch.layers],
|
|
304
|
+
"key_components": [asdict(comp) for comp in arch.key_components],
|
|
305
|
+
"critical_paths": arch.critical_paths,
|
|
306
|
+
"risk_assessment": risk_summary,
|
|
307
|
+
"complexity_hotspots": arch.complexity_hotspots,
|
|
308
|
+
"coupling": arch.coupling_summary,
|
|
309
|
+
"health_signals": arch.health_signals,
|
|
310
|
+
"issues": arch.issues,
|
|
311
|
+
"strengths": arch.strengths,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return json.dumps(data, indent=2, default=str)
|
|
315
|
+
|
|
316
|
+
def _build_architecture_graph(self) -> ArchitectureGraph:
|
|
317
|
+
"""
|
|
318
|
+
Build internal architecture representation by analyzing the graph.
|
|
319
|
+
|
|
320
|
+
Combines code-level analysis with thorough analysis results for
|
|
321
|
+
rich, infographic-ready descriptions.
|
|
322
|
+
|
|
323
|
+
:return: ArchitectureGraph with layers, components, paths, coupling.
|
|
324
|
+
"""
|
|
325
|
+
# Detect layers from module hierarchy
|
|
326
|
+
layers = self._detect_layers()
|
|
327
|
+
|
|
328
|
+
# Find key components (high fan-in, central classes, entry points)
|
|
329
|
+
key_components = self._identify_key_components()
|
|
330
|
+
|
|
331
|
+
# Trace critical paths (important call chains)
|
|
332
|
+
critical_paths = self._trace_critical_paths()
|
|
333
|
+
|
|
334
|
+
# Analyze module coupling
|
|
335
|
+
coupling = self._analyze_coupling()
|
|
336
|
+
|
|
337
|
+
# Extract data from thorough analysis if available
|
|
338
|
+
complexity_hotspots = self._extract_complexity_hotspots()
|
|
339
|
+
health_signals = self._extract_health_signals()
|
|
340
|
+
issues = self.thorough_analysis.get("issues", [])
|
|
341
|
+
strengths = self.thorough_analysis.get("strengths", [])
|
|
342
|
+
|
|
343
|
+
return ArchitectureGraph(
|
|
344
|
+
title=self._infer_project_title(),
|
|
345
|
+
description=self._generate_architecture_summary(layers),
|
|
346
|
+
version=self.version,
|
|
347
|
+
commit=self.commit,
|
|
348
|
+
layers=layers,
|
|
349
|
+
key_components=key_components,
|
|
350
|
+
critical_paths=critical_paths,
|
|
351
|
+
coupling_summary=coupling,
|
|
352
|
+
complexity_hotspots=complexity_hotspots,
|
|
353
|
+
health_signals=health_signals,
|
|
354
|
+
issues=issues,
|
|
355
|
+
strengths=strengths,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def _detect_layers(self) -> list[ModuleLayer]:
|
|
359
|
+
"""
|
|
360
|
+
Detect logical layers from module organization.
|
|
361
|
+
|
|
362
|
+
Common patterns:
|
|
363
|
+
- cli/ → Command-line layer
|
|
364
|
+
- api/ → API layer
|
|
365
|
+
- core/ → Core business logic
|
|
366
|
+
- store/ → Data persistence
|
|
367
|
+
- models/ → Data models
|
|
368
|
+
|
|
369
|
+
:return: List of detected layers.
|
|
370
|
+
"""
|
|
371
|
+
# Get all modules from graph
|
|
372
|
+
all_nodes = self.store.query_nodes(kinds=["module"])
|
|
373
|
+
modules = [n["module_path"] for n in all_nodes if n.get("module_path")]
|
|
374
|
+
|
|
375
|
+
layers = []
|
|
376
|
+
layer_map: dict[str, list[str]] = {}
|
|
377
|
+
|
|
378
|
+
# Organize by prefix path
|
|
379
|
+
for mod in sorted(modules):
|
|
380
|
+
parts = mod.split("/")
|
|
381
|
+
if len(parts) > 1:
|
|
382
|
+
layer_name = parts[0]
|
|
383
|
+
if layer_name not in layer_map:
|
|
384
|
+
layer_map[layer_name] = []
|
|
385
|
+
layer_map[layer_name].append(mod)
|
|
386
|
+
|
|
387
|
+
# Create layer definitions based on detected structure
|
|
388
|
+
layer_descriptions = {
|
|
389
|
+
"cli": (
|
|
390
|
+
"Command-line Interface",
|
|
391
|
+
["Parse and handle CLI commands", "Route to subcommands"],
|
|
392
|
+
),
|
|
393
|
+
"api": ("API Layer", ["Expose REST/RPC endpoints", "Handle request/response"]),
|
|
394
|
+
"core": (
|
|
395
|
+
"Core Business Logic",
|
|
396
|
+
["Implement core functionality", "Orchestrate workflows"],
|
|
397
|
+
),
|
|
398
|
+
"store": ("Data Storage & Persistence", ["Manage graph data", "Query knowledge graph"]),
|
|
399
|
+
"index": (
|
|
400
|
+
"Semantic Search & Indexing",
|
|
401
|
+
["Embed and index vectors", "Semantic similarity search"],
|
|
402
|
+
),
|
|
403
|
+
"models": ("Data Models", ["Define core entities", "Provide type definitions"]),
|
|
404
|
+
"graph": ("Graph Construction", ["Extract code structure", "Build AST-based graph"]),
|
|
405
|
+
"visitor": ("AST Visitor & Analysis", ["Traverse AST", "Track scopes and symbols"]),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for layer_name, modules_list in sorted(layer_map.items()):
|
|
409
|
+
desc, resps = layer_descriptions.get(
|
|
410
|
+
layer_name, (f"{layer_name.capitalize()} Layer", ["Component responsibility"])
|
|
411
|
+
)
|
|
412
|
+
layers.append(
|
|
413
|
+
ModuleLayer(
|
|
414
|
+
name=desc,
|
|
415
|
+
description=f"Handles {layer_name} concerns.",
|
|
416
|
+
modules=modules_list,
|
|
417
|
+
responsibilities=resps,
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
return layers
|
|
422
|
+
|
|
423
|
+
def _identify_key_components(self) -> list[ComponentNode]:
|
|
424
|
+
"""
|
|
425
|
+
Identify key architectural components (high fan-in, central classes).
|
|
426
|
+
|
|
427
|
+
:return: List of important components.
|
|
428
|
+
"""
|
|
429
|
+
components = []
|
|
430
|
+
|
|
431
|
+
# Get classes from the graph
|
|
432
|
+
classes = self.store.query_nodes(kinds=["class"])
|
|
433
|
+
|
|
434
|
+
for cls_node in classes[:5]: # Top 5 classes
|
|
435
|
+
node_id = cls_node["id"]
|
|
436
|
+
docstring = cls_node.get("docstring", "Core component")
|
|
437
|
+
desc = docstring.split("\n")[0] if docstring else "Core component"
|
|
438
|
+
|
|
439
|
+
components.append(
|
|
440
|
+
ComponentNode(
|
|
441
|
+
node_id=node_id,
|
|
442
|
+
name=cls_node["name"],
|
|
443
|
+
kind="class",
|
|
444
|
+
description=desc,
|
|
445
|
+
file=cls_node.get("module_path", "unknown"),
|
|
446
|
+
lineno=cls_node.get("lineno", 0),
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
return components
|
|
451
|
+
|
|
452
|
+
def _trace_critical_paths(self) -> list[dict]:
|
|
453
|
+
"""
|
|
454
|
+
Identify critical execution paths (important call chains).
|
|
455
|
+
|
|
456
|
+
:return: List of critical paths with descriptions.
|
|
457
|
+
"""
|
|
458
|
+
return [
|
|
459
|
+
{
|
|
460
|
+
"name": "Graph Query Pipeline",
|
|
461
|
+
"description": "Semantic search → graph expansion → snippet packing",
|
|
462
|
+
"steps": [
|
|
463
|
+
"Semantic search finds seed nodes via LanceDB",
|
|
464
|
+
"Graph expansion traverses CALLS, CONTAINS, IMPORTS edges",
|
|
465
|
+
"Snippet pack materializes source code with context",
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "AST Extraction & Graph Building",
|
|
470
|
+
"description": "Repository scanning → code analysis → graph storage",
|
|
471
|
+
"steps": [
|
|
472
|
+
"CodeGraph walks repo and extracts Python files",
|
|
473
|
+
"PyCodeKGVisitor traverses AST, collects nodes and edges",
|
|
474
|
+
"GraphStore persists in SQLite with symbol resolution",
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
def _analyze_coupling(self) -> dict[str, Any]:
|
|
480
|
+
"""
|
|
481
|
+
Analyze module dependencies and coupling strength.
|
|
482
|
+
|
|
483
|
+
:return: Dict with dependency analysis.
|
|
484
|
+
"""
|
|
485
|
+
# Query IMPORTS edges from the graph store directly
|
|
486
|
+
# We'll use the internal SQLite connection
|
|
487
|
+
rows = self.store.con.execute(
|
|
488
|
+
"SELECT src, dst FROM edges WHERE rel = 'IMPORTS' LIMIT 100"
|
|
489
|
+
).fetchall()
|
|
490
|
+
|
|
491
|
+
dependencies: dict[str, list[str]] = {}
|
|
492
|
+
for src, dst in rows:
|
|
493
|
+
if src and dst:
|
|
494
|
+
src_mod = src.split(":")[1] if ":" in src else src
|
|
495
|
+
dst_mod = dst.split(":")[1] if ":" in dst else dst
|
|
496
|
+
if src_mod not in dependencies:
|
|
497
|
+
dependencies[src_mod] = []
|
|
498
|
+
if dst_mod not in dependencies[src_mod]:
|
|
499
|
+
dependencies[src_mod].append(dst_mod)
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
"dependencies": dependencies,
|
|
503
|
+
"total_import_edges": len(rows),
|
|
504
|
+
"summary": "Module dependencies tracked via IMPORTS edges in knowledge graph",
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
def _extract_complexity_hotspots(self) -> list[dict]:
|
|
508
|
+
"""Extract top complexity hotspots from thorough analysis with risk context."""
|
|
509
|
+
hotspots = []
|
|
510
|
+
|
|
511
|
+
# High fan-in functions (heavily called = change impact risk)
|
|
512
|
+
high_fanin = self.thorough_analysis.get("high_fan_in_functions", [])
|
|
513
|
+
for func in high_fanin[:10]:
|
|
514
|
+
fan_in = func.get("fan_in", 0)
|
|
515
|
+
fan_out = func.get("fan_out", 0)
|
|
516
|
+
risk = func.get("risk_level", "low")
|
|
517
|
+
hotspots.append(
|
|
518
|
+
{
|
|
519
|
+
"name": func.get("name", "unknown"),
|
|
520
|
+
"fan_in": fan_in,
|
|
521
|
+
"fan_out": fan_out,
|
|
522
|
+
"risk_level": risk,
|
|
523
|
+
"type": "integration_hub",
|
|
524
|
+
"description": (
|
|
525
|
+
f"Called by {fan_in} other functions. Changes propagate broadly. "
|
|
526
|
+
f"Requires careful testing and backward compatibility."
|
|
527
|
+
),
|
|
528
|
+
"impact": "high" if fan_in > 15 else "medium",
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# High fan-out functions (many calls = coordination burden)
|
|
533
|
+
high_fanout = self.thorough_analysis.get("high_fan_out_functions", [])
|
|
534
|
+
for func in high_fanout[:10]:
|
|
535
|
+
fan_in = func.get("fan_in", 0)
|
|
536
|
+
fan_out = func.get("fan_out", 0)
|
|
537
|
+
risk = func.get("risk_level", "low")
|
|
538
|
+
hotspots.append(
|
|
539
|
+
{
|
|
540
|
+
"name": func.get("name", "unknown"),
|
|
541
|
+
"fan_in": fan_in,
|
|
542
|
+
"fan_out": fan_out,
|
|
543
|
+
"risk_level": risk,
|
|
544
|
+
"type": "coordinator",
|
|
545
|
+
"description": (
|
|
546
|
+
f"Calls {fan_out} other functions. Complex orchestration logic. "
|
|
547
|
+
f"Refactoring candidate: consider breaking into smaller steps."
|
|
548
|
+
),
|
|
549
|
+
"impact": "high" if fan_out > 20 else "medium",
|
|
550
|
+
}
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
# Sort by impact and risk, return top hotspots
|
|
554
|
+
return sorted(
|
|
555
|
+
hotspots, key=lambda h: (h.get("risk_level") == "high", h.get("impact") == "high")
|
|
556
|
+
)[:15]
|
|
557
|
+
|
|
558
|
+
def _extract_health_signals(self) -> dict[str, Any]:
|
|
559
|
+
"""Extract comprehensive health and quality signals from thorough analysis."""
|
|
560
|
+
signals: dict[str, Any] = {}
|
|
561
|
+
|
|
562
|
+
# Docstring coverage (primary quality metric)
|
|
563
|
+
coverage = self.thorough_analysis.get("docstring_coverage", {})
|
|
564
|
+
if coverage:
|
|
565
|
+
total_cov = coverage.get("total", 0.0)
|
|
566
|
+
signals["Docstring Coverage"] = total_cov
|
|
567
|
+
# Add color/severity
|
|
568
|
+
if total_cov < 0.6:
|
|
569
|
+
signals["Documentation Status"] = "Below Target"
|
|
570
|
+
elif total_cov < 0.85:
|
|
571
|
+
signals["Documentation Status"] = "Good Progress"
|
|
572
|
+
else:
|
|
573
|
+
signals["Documentation Status"] = "Well Documented"
|
|
574
|
+
|
|
575
|
+
# Circular dependencies (structural health)
|
|
576
|
+
circular = self.thorough_analysis.get("circular_dependencies", [])
|
|
577
|
+
circular_count = len(circular)
|
|
578
|
+
signals["Circular Dependencies"] = circular_count
|
|
579
|
+
if circular_count > 0:
|
|
580
|
+
signals["Coupling Health"] = "Has cycles"
|
|
581
|
+
else:
|
|
582
|
+
signals["Coupling Health"] = "Acyclic"
|
|
583
|
+
|
|
584
|
+
# Orphaned functions (dead code)
|
|
585
|
+
orphaned = self.thorough_analysis.get("orphaned_functions", [])
|
|
586
|
+
orphaned_count = len(orphaned)
|
|
587
|
+
signals["Orphaned Functions"] = orphaned_count
|
|
588
|
+
if orphaned_count > 5:
|
|
589
|
+
signals["Dead Code Status"] = "Cleanup needed"
|
|
590
|
+
elif orphaned_count > 0:
|
|
591
|
+
signals["Dead Code Status"] = "Minor cleanup"
|
|
592
|
+
else:
|
|
593
|
+
signals["Dead Code Status"] = "Clean"
|
|
594
|
+
|
|
595
|
+
# High-level counts if available
|
|
596
|
+
stats = self.thorough_analysis.get("statistics", {})
|
|
597
|
+
if stats:
|
|
598
|
+
total_funcs = stats.get("total_functions", 0)
|
|
599
|
+
if total_funcs > 0:
|
|
600
|
+
signals["Function Count"] = total_funcs
|
|
601
|
+
|
|
602
|
+
return signals
|
|
603
|
+
|
|
604
|
+
def _infer_project_title(self) -> str:
|
|
605
|
+
"""Infer project title from repo structure."""
|
|
606
|
+
readme = self.repo_root / "README.md"
|
|
607
|
+
if readme.exists():
|
|
608
|
+
try:
|
|
609
|
+
with open(readme) as f:
|
|
610
|
+
for line in f:
|
|
611
|
+
if line.startswith("#"):
|
|
612
|
+
return line.lstrip("#").strip()
|
|
613
|
+
except OSError:
|
|
614
|
+
pass
|
|
615
|
+
return "PyCodeKG Architecture"
|
|
616
|
+
|
|
617
|
+
def _generate_architecture_summary(self, layers: list[ModuleLayer]) -> str:
|
|
618
|
+
"""Generate high-level architecture summary."""
|
|
619
|
+
layer_names = [layer.name for layer in layers]
|
|
620
|
+
return (
|
|
621
|
+
f"This codebase is organized into {len(layers)} architectural layers: "
|
|
622
|
+
f"{', '.join(layer_names)}. "
|
|
623
|
+
f"The architecture supports semantic code search via knowledge graph indexing and querying."
|
|
624
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Legacy module — functionality moved to CLI.
|
|
3
|
+
|
|
4
|
+
This module is deprecated. Use the CLI command instead::
|
|
5
|
+
|
|
6
|
+
poetry run pycodekg build-lancedb --repo /path/to/repo
|
|
7
|
+
|
|
8
|
+
Or import from the CLI module directly::
|
|
9
|
+
|
|
10
|
+
from pycode_kg.cli.cmd_build import build_lancedb
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pycode_kg.cli.cmd_build import build_lancedb
|
|
14
|
+
|
|
15
|
+
__all__ = ["build_lancedb"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Legacy module — functionality moved to CLI.
|
|
3
|
+
|
|
4
|
+
This module is deprecated. Use the CLI command instead::
|
|
5
|
+
|
|
6
|
+
poetry run pycodekg build-sqlite --repo /path/to/repo
|
|
7
|
+
|
|
8
|
+
Or import from the CLI module directly::
|
|
9
|
+
|
|
10
|
+
from pycode_kg.cli.cmd_build import build_sqlite
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pycode_kg.cli.cmd_build import build_sqlite
|
|
14
|
+
|
|
15
|
+
__all__ = ["build_sqlite"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pycode_kg.cli — Click-based CLI entry points.
|
|
3
|
+
|
|
4
|
+
Public API
|
|
5
|
+
----------
|
|
6
|
+
The root Click group is importable from either location::
|
|
7
|
+
|
|
8
|
+
from pycode_kg.cli import cli
|
|
9
|
+
from pycode_kg.cli.main import cli
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pycode_kg.cli import ( # noqa: F401 — registers snapshot (save, list, show, diff)
|
|
13
|
+
cmd_analyze, # noqa: F401 — registers analyze
|
|
14
|
+
cmd_architecture, # noqa: F401 — registers architecture
|
|
15
|
+
cmd_build, # noqa: F401 — registers build-sqlite, build-lancedb
|
|
16
|
+
cmd_build_full, # noqa: F401 — registers build (full pipeline), update (incremental upsert)
|
|
17
|
+
cmd_centrality, # noqa: F401 — registers centrality
|
|
18
|
+
cmd_explain, # noqa: F401 — registers explain
|
|
19
|
+
cmd_hooks, # noqa: F401 — registers install-hooks
|
|
20
|
+
cmd_init, # noqa: F401 — registers init
|
|
21
|
+
cmd_mcp, # noqa: F401 — registers mcp
|
|
22
|
+
cmd_model, # noqa: F401 — registers download-model
|
|
23
|
+
cmd_query, # noqa: F401 — registers query, pack
|
|
24
|
+
cmd_snapshot,
|
|
25
|
+
cmd_viz, # noqa: F401 — registers viz, viz3d, viz-timeline
|
|
26
|
+
)
|
|
27
|
+
from pycode_kg.cli.main import cli
|
|
28
|
+
|
|
29
|
+
__all__ = ["cli"]
|