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.
Files changed (63) hide show
  1. pycode_kg/.DS_Store +0 -0
  2. pycode_kg/__init__.py +91 -0
  3. pycode_kg/__main__.py +11 -0
  4. pycode_kg/analysis/__init__.py +15 -0
  5. pycode_kg/analysis/bridge.py +108 -0
  6. pycode_kg/analysis/centrality.py +412 -0
  7. pycode_kg/analysis/framework_detector.py +103 -0
  8. pycode_kg/analysis/hybrid_rank.py +53 -0
  9. pycode_kg/app.py +1335 -0
  10. pycode_kg/architecture.py +624 -0
  11. pycode_kg/build_pycodekg_lancedb.py +15 -0
  12. pycode_kg/build_pycodekg_sqlite.py +15 -0
  13. pycode_kg/cli/__init__.py +29 -0
  14. pycode_kg/cli/cmd_analyze.py +96 -0
  15. pycode_kg/cli/cmd_architecture.py +150 -0
  16. pycode_kg/cli/cmd_bridges.py +26 -0
  17. pycode_kg/cli/cmd_build.py +154 -0
  18. pycode_kg/cli/cmd_build_full.py +242 -0
  19. pycode_kg/cli/cmd_centrality.py +131 -0
  20. pycode_kg/cli/cmd_explain.py +180 -0
  21. pycode_kg/cli/cmd_framework_nodes.py +18 -0
  22. pycode_kg/cli/cmd_hooks.py +137 -0
  23. pycode_kg/cli/cmd_init.py +312 -0
  24. pycode_kg/cli/cmd_mcp.py +71 -0
  25. pycode_kg/cli/cmd_model.py +53 -0
  26. pycode_kg/cli/cmd_query.py +211 -0
  27. pycode_kg/cli/cmd_snapshot.py +421 -0
  28. pycode_kg/cli/cmd_viz.py +180 -0
  29. pycode_kg/cli/main.py +23 -0
  30. pycode_kg/cli/options.py +63 -0
  31. pycode_kg/config.py +78 -0
  32. pycode_kg/graph.py +125 -0
  33. pycode_kg/index.py +542 -0
  34. pycode_kg/kg.py +220 -0
  35. pycode_kg/layout3d.py +470 -0
  36. pycode_kg/mcp/bridge_tools.py +19 -0
  37. pycode_kg/mcp/framework_tools.py +18 -0
  38. pycode_kg/mcp_server.py +1965 -0
  39. pycode_kg/module/__init__.py +83 -0
  40. pycode_kg/module/base.py +720 -0
  41. pycode_kg/module/extractor.py +276 -0
  42. pycode_kg/module/types.py +532 -0
  43. pycode_kg/pycodekg.py +543 -0
  44. pycode_kg/pycodekg_query.py +1 -0
  45. pycode_kg/pycodekg_snippet_packer.py +1 -0
  46. pycode_kg/pycodekg_thorough_analysis.py +2751 -0
  47. pycode_kg/pycodekg_viz.py +1 -0
  48. pycode_kg/pycodekg_viz3d.py +1 -0
  49. pycode_kg/ranking/__init__.py +1 -0
  50. pycode_kg/ranking/cli_rank.py +92 -0
  51. pycode_kg/ranking/coderank.py +555 -0
  52. pycode_kg/snapshots.py +612 -0
  53. pycode_kg/sql/004_add_centrality_table.sql +12 -0
  54. pycode_kg/store.py +766 -0
  55. pycode_kg/utils.py +39 -0
  56. pycode_kg/visitor.py +413 -0
  57. pycode_kg/viz3d.py +1353 -0
  58. pycode_kg/viz3d_timeline.py +364 -0
  59. pycode_kg-0.16.0.dist-info/METADATA +305 -0
  60. pycode_kg-0.16.0.dist-info/RECORD +63 -0
  61. pycode_kg-0.16.0.dist-info/WHEEL +4 -0
  62. pycode_kg-0.16.0.dist-info/entry_points.txt +19 -0
  63. 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"]