pycode-kg 0.28.0__tar.gz → 0.28.2__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.
Files changed (62) hide show
  1. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/PKG-INFO +9 -9
  2. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/README.md +4 -4
  3. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/pyproject.toml +5 -5
  4. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/__init__.py +1 -1
  5. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/pycodekg_thorough_analysis.py +93 -1
  6. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/report.py +29 -0
  7. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/LICENSE +0 -0
  8. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/.DS_Store +0 -0
  9. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/__main__.py +0 -0
  10. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/analysis/__init__.py +0 -0
  11. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/analysis/bridge.py +0 -0
  12. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/analysis/centrality.py +0 -0
  13. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/analysis/framework_detector.py +0 -0
  14. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/app.py +0 -0
  15. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/architecture.py +0 -0
  16. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
  17. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/__init__.py +0 -0
  18. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_analyze.py +0 -0
  19. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_architecture.py +0 -0
  20. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_bridges.py +0 -0
  21. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_build.py +0 -0
  22. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_build_full.py +0 -0
  23. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_centrality.py +0 -0
  24. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_explain.py +0 -0
  25. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
  26. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_hooks.py +0 -0
  27. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_init.py +0 -0
  28. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_mcp.py +0 -0
  29. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_model.py +0 -0
  30. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_query.py +0 -0
  31. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_quilt.py +0 -0
  32. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_snapshot.py +0 -0
  33. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/cmd_viz.py +0 -0
  34. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/main.py +0 -0
  35. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/cli/options.py +0 -0
  36. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/config.py +0 -0
  37. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/explain.py +0 -0
  38. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/graph.py +0 -0
  39. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/graph_html.py +0 -0
  40. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/index.py +0 -0
  41. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/kg.py +0 -0
  42. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/layout3d.py +0 -0
  43. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/mcp_server.py +0 -0
  44. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/module/__init__.py +0 -0
  45. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/module/base.py +0 -0
  46. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/module/extractor.py +0 -0
  47. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/module/types.py +0 -0
  48. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/pycodekg.py +0 -0
  49. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/ranking/__init__.py +0 -0
  50. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/ranking/cli_rank.py +0 -0
  51. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/ranking/coderank.py +0 -0
  52. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/render.py +0 -0
  53. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/resolution.py +0 -0
  54. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/scene3d.py +0 -0
  55. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/snapshots.py +0 -0
  56. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
  57. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/store.py +0 -0
  58. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/theme.py +0 -0
  59. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/utils.py +0 -0
  60. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/visitor.py +0 -0
  61. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/viz3d.py +0 -0
  62. {pycode_kg-0.28.0 → pycode_kg-0.28.2}/src/pycode_kg/viz3d_timeline.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycode-kg
3
- Version: 0.28.0
3
+ Version: 0.28.2
4
4
  Summary: A tool to build a searchable knowledge graph from Python repositories
5
5
  License-Expression: Elastic-2.0
6
6
  License-File: LICENSE
@@ -21,8 +21,8 @@ Provides-Extra: viz3d
21
21
  Requires-Dist: PyQt5 (>=5.15.0) ; extra == "all"
22
22
  Requires-Dist: PyQt5 (>=5.15.0) ; extra == "viz3d"
23
23
  Requires-Dist: click (>=8.1.0,<9)
24
- Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.23.0)
25
- Requires-Dist: kgmodule-utils[viz3d-qt,viz3d-render] (>=0.23.0) ; extra == "viz3d"
24
+ Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.27.0)
25
+ Requires-Dist: kgmodule-utils[viz3d-qt,viz3d-render] (>=0.27.0) ; extra == "viz3d"
26
26
  Requires-Dist: markdown (>=3.6) ; extra == "all"
27
27
  Requires-Dist: markdown (>=3.6) ; extra == "viz3d"
28
28
  Requires-Dist: mcp (>=1.3.0,<2)
@@ -38,8 +38,8 @@ Requires-Dist: pyvis (>=0.3.2) ; extra == "viz"
38
38
  Requires-Dist: pyvista (>=0.44.0) ; extra == "all"
39
39
  Requires-Dist: pyvistaqt (>=0.11.0) ; extra == "all"
40
40
  Requires-Dist: pyvistaqt (>=0.11.0) ; extra == "viz3d"
41
- Requires-Dist: quiltwright (>=0.15.0) ; extra == "all"
42
- Requires-Dist: quiltwright (>=0.15.0) ; extra == "viz3d"
41
+ Requires-Dist: quiltwright (>=0.16.0) ; extra == "all"
42
+ Requires-Dist: quiltwright (>=0.16.0) ; extra == "viz3d"
43
43
  Requires-Dist: rich (>=14.3.3,<15)
44
44
  Requires-Dist: sentence-transformers (>=5.4.1)
45
45
  Requires-Dist: sqlite-vec (==0.1.9)
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
60
60
 
61
61
  [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
62
62
  [![License: Elastic-2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](https://www.elastic.co/licensing/elastic-license)
63
- [![Version](https://img.shields.io/badge/version-0.28.0-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
63
+ [![Version](https://img.shields.io/badge/version-0.28.2-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
64
64
  [![CI](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml/badge.svg)](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
65
65
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
66
66
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
@@ -255,7 +255,7 @@ src/pycode_kg/
255
255
  └── viz3d_timeline.py # Metric history timeline
256
256
  ```
257
257
 
258
- The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.28.0.md](docs/analysis_v0.28.0.md), produced (of course) by `pycodekg analyze` against this very repo.
258
+ The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.28.2.md](docs/analysis_v0.28.2.md), produced (of course) by `pycodekg analyze` against this very repo.
259
259
 
260
260
  ---
261
261
 
@@ -282,13 +282,13 @@ If you use PyCodeKG in your research or project, please cite it:
282
282
 
283
283
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
284
284
 
285
- > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.28.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
285
+ > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.28.2) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
286
286
 
287
287
  ```bibtex
288
288
  @software{suchanek_pycode_kg,
289
289
  author = {Suchanek, Eric G.},
290
290
  title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
291
- version = {0.28.0},
291
+ version = {0.28.2},
292
292
  year = {2026},
293
293
  publisher = {Flux-Frontiers},
294
294
  url = {https://github.com/Flux-Frontiers/pycode_kg},
@@ -5,7 +5,7 @@
5
5
 
6
6
  [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
7
7
  [![License: Elastic-2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](https://www.elastic.co/licensing/elastic-license)
8
- [![Version](https://img.shields.io/badge/version-0.28.0-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
8
+ [![Version](https://img.shields.io/badge/version-0.28.2-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
9
9
  [![CI](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml/badge.svg)](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
10
10
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
11
11
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
@@ -200,7 +200,7 @@ src/pycode_kg/
200
200
  └── viz3d_timeline.py # Metric history timeline
201
201
  ```
202
202
 
203
- The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.28.0.md](docs/analysis_v0.28.0.md), produced (of course) by `pycodekg analyze` against this very repo.
203
+ The MCP server, the CLI, and the Streamlit app are thin wrappers over the same store + index + ranking core — there is exactly one code path for each capability. The latest architectural deep-dive is in [docs/analysis_v0.28.2.md](docs/analysis_v0.28.2.md), produced (of course) by `pycodekg analyze` against this very repo.
204
204
 
205
205
  ---
206
206
 
@@ -227,13 +227,13 @@ If you use PyCodeKG in your research or project, please cite it:
227
227
 
228
228
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
229
229
 
230
- > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.28.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
230
+ > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.28.2) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
231
231
 
232
232
  ```bibtex
233
233
  @software{suchanek_pycode_kg,
234
234
  author = {Suchanek, Eric G.},
235
235
  title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
236
- version = {0.28.0},
236
+ version = {0.28.2},
237
237
  year = {2026},
238
238
  publisher = {Flux-Frontiers},
239
239
  url = {https://github.com/Flux-Frontiers/pycode_kg},
@@ -70,7 +70,7 @@ torch = [
70
70
  # ---------------------------------------------------------------------------
71
71
  [project]
72
72
  name = "pycode-kg"
73
- version = "0.28.0"
73
+ version = "0.28.2"
74
74
  description = "A tool to build a searchable knowledge graph from Python repositories"
75
75
  readme = "README.md"
76
76
  license = "Elastic-2.0"
@@ -169,7 +169,7 @@ dependencies = [
169
169
  # and the _domain_metrics() hook, none of which exist before 0.20.0.
170
170
  # Against 0.19.x the manager silently reports itself as "kg-utils" and
171
171
  # drops module_node_counts entirely.
172
- "kgmodule-utils[semantic,viz3d]>=0.23.0",
172
+ "kgmodule-utils[semantic,viz3d]>=0.27.0",
173
173
  ]
174
174
 
175
175
  [project.optional-dependencies]
@@ -194,7 +194,7 @@ viz3d = [
194
194
  # the four-step build/render/write/cast itself. It subsumes [viz3d-render]
195
195
  # (it declares pyvista too), but both are named so the dependency reads as
196
196
  # what it is rather than as a side effect.
197
- "kgmodule-utils[viz3d-render,viz3d-qt]>=0.23.0",
197
+ "kgmodule-utils[viz3d-render,viz3d-qt]>=0.27.0",
198
198
  "pyvistaqt>=0.11.0",
199
199
  "trame-vtk>=2.0.0",
200
200
  # Looking Glass quilts. 0.4.0 supplies depth_report() and widened
@@ -204,7 +204,7 @@ viz3d = [
204
204
  # 0.14.1 is what kgmodule-utils[viz3d-qt] >=0.22.0 needs transitively, for
205
205
  # resolve_view_cone; declared here so this file states the real floor
206
206
  # rather than leaving it to the resolver to discover.
207
- "quiltwright>=0.15.0",
207
+ "quiltwright>=0.16.0",
208
208
  ]
209
209
 
210
210
  # Cross-KG sibling packages (doc-kg, pycode-kg, agent-kg, kg-rag) are NOT
@@ -229,7 +229,7 @@ all = [
229
229
  "pyvis>=0.3.2",
230
230
  "pyvista>=0.44.0",
231
231
  "pyvistaqt>=0.11.0",
232
- "quiltwright>=0.15.0",
232
+ "quiltwright>=0.16.0",
233
233
  "streamlit>=1.56.0",
234
234
  "trame-vtk>=2.0.0",
235
235
  ]
@@ -36,7 +36,7 @@ Author: Eric G. Suchanek, PhD
36
36
  License: Elastic 2.0
37
37
  """
38
38
 
39
- __version__ = "0.28.0"
39
+ __version__ = "0.28.2"
40
40
  __author__ = "Eric G. Suchanek, PhD"
41
41
 
42
42
  # Low-level primitives (locked v0 contract)
@@ -27,6 +27,7 @@ Author: Eric G. Suchanek, PhD
27
27
  License: Elastic 2.0
28
28
  """
29
29
 
30
+ import ast
30
31
  import datetime
31
32
  import importlib
32
33
  import inspect
@@ -43,6 +44,7 @@ from collections.abc import Callable
43
44
  from dataclasses import asdict, dataclass
44
45
  from pathlib import Path
45
46
 
47
+ from kg_utils.vector_backend import VectorStoreNotFoundError
46
48
  from rich.columns import Columns
47
49
  from rich.console import Console
48
50
  from rich.panel import Panel
@@ -135,6 +137,28 @@ _PROPERTY_DECORATOR_RE = re.compile(
135
137
  r"@(?:functools\.)?(?:cached_)?property\b|@\w[\w.]*\.(?:setter|getter|deleter)\b"
136
138
  )
137
139
 
140
+ # Route-registration decorator attributes on a web-framework app or router:
141
+ # ``@app.get(...)``, ``@router.post(...)``, ``@app.middleware("http")``
142
+ # (FastAPI, Starlette) and ``@app.route(...)`` (Flask). The framework calls
143
+ # the handler, so the graph never sees a CALLS edge to it.
144
+ _ROUTE_DECORATOR_ATTRS = frozenset(
145
+ {
146
+ "get",
147
+ "post",
148
+ "put",
149
+ "patch",
150
+ "delete",
151
+ "head",
152
+ "options",
153
+ "api_route",
154
+ "route",
155
+ "middleware",
156
+ "websocket",
157
+ "exception_handler",
158
+ "on_event",
159
+ }
160
+ )
161
+
138
162
  # Builtin container/str method names live in pycode_kg.resolution, where the
139
163
  # post-build hook prunes their bogus RESOLVES_TO edges at the source. The
140
164
  # analyzer still guards its own reads (chain tracing, fan-out) so it stays
@@ -355,6 +379,33 @@ def _has_property_decorator(source_lines: list[str], def_lineno: int) -> bool:
355
379
  return False
356
380
 
357
381
 
382
+ def _has_route_decorator(source: str, def_lineno: int) -> bool:
383
+ """Check whether the ``def`` at *def_lineno* is registered as a web route.
384
+
385
+ Matches a decorator of the form ``@<obj>.<attr>`` or ``@<obj>.<attr>(...)``
386
+ with ``attr`` in :data:`_ROUTE_DECORATOR_ATTRS`. The module is parsed
387
+ rather than scanned line by line because route decorators often span
388
+ several lines (``@app.post(\n "/x",\n response_model=M,\n)``).
389
+
390
+ :param source: Full module source text.
391
+ :param def_lineno: 1-based line number of the ``def`` statement.
392
+ :return: True when a route-registration decorator precedes the definition;
393
+ False when it does not, or the source does not parse.
394
+ """
395
+ try:
396
+ tree = ast.parse(source)
397
+ except SyntaxError:
398
+ return False
399
+ for node in ast.walk(tree):
400
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.lineno == def_lineno:
401
+ for dec in node.decorator_list:
402
+ target = dec.func if isinstance(dec, ast.Call) else dec
403
+ if isinstance(target, ast.Attribute) and target.attr in _ROUTE_DECORATOR_ATTRS:
404
+ return True
405
+ return False
406
+ return False
407
+
408
+
358
409
  def _dotted_module(module_path: str) -> str:
359
410
  """Convert a repo-relative module path to its dotted import path.
360
411
 
@@ -444,6 +495,8 @@ class PyCodeKGAnalyzer:
444
495
  self.concern_analysis: list[dict] = [] # populated by _analyze_concerns
445
496
  # Single-line phase output: each phase fn writes its summary here
446
497
  self._phase_result: str = ""
498
+ # Phases that raised; rendered in the report (see _run_phase)
499
+ self.phase_failures: list[dict] = []
447
500
 
448
501
  # ── total phase count (update if phases are added/removed) ────────────────
449
502
  _TOTAL_PHASES = 15
@@ -455,13 +508,39 @@ class PyCodeKGAnalyzer:
455
508
  instead of printing directly, keeping the terminal output to one line
456
509
  per phase.
457
510
 
511
+ A phase that cannot run must not take the others and the report with
512
+ it. The concrete case is ``pycodekg build-sqlite``, a supported command
513
+ that builds the graph without the vector index: the fan-out and
514
+ concern-ranking phases seed on a semantic query and raise
515
+ ``VectorStoreNotFoundError``, while every other phase reads the graph
516
+ only. The failure is recorded in :attr:`phase_failures` and rendered in
517
+ the report, so a degraded run names its missing sections instead of
518
+ quietly omitting them.
519
+
458
520
  :param num: Phase number (1-based).
459
521
  :param name: Human-readable phase label.
460
522
  :param fn: Zero-argument callable to execute.
461
523
  """
462
524
  self._phase_result = ""
463
525
  t0 = time.monotonic()
464
- fn()
526
+ try:
527
+ fn()
528
+ except Exception as exc: # noqa: BLE001 -- see docstring
529
+ elapsed = time.monotonic() - t0
530
+ self.phase_failures.append(
531
+ {
532
+ "phase": num,
533
+ "name": name,
534
+ "error": str(exc),
535
+ "missing_index": isinstance(exc, VectorStoreNotFoundError),
536
+ }
537
+ )
538
+ logger.warning("Phase %d (%s) failed: %s", num, name, exc)
539
+ self.console.print(
540
+ f" [cyan]▶ Phase {num:2d}/{self._TOTAL_PHASES}:[/cyan]"
541
+ f" {name} [yellow]skipped: {exc}[/yellow] [green]({elapsed:.1f}s)[/green]"
542
+ )
543
+ return
465
544
  elapsed = time.monotonic() - t0
466
545
  result = f" {self._phase_result}" if self._phase_result else ""
467
546
  self.console.print(
@@ -899,6 +978,8 @@ class PyCodeKGAnalyzer:
899
978
  - Property-family methods — accessed as attributes, not calls
900
979
  - MCP tools in ``mcp_server.py`` — dispatched by the MCP router
901
980
  - Click handlers in CLI modules — dispatched by Click
981
+ - Web route handlers (``@app.get``, ``@router.post``,
982
+ ``@app.middleware``, ...) -- dispatched by the web framework
902
983
  - ``visit_*`` on ``ast.NodeVisitor`` subclasses — ``getattr`` dispatch
903
984
  - Overrides of SDK protocol methods on classes with external bases —
904
985
  invoked by the kg_utils pipeline
@@ -985,6 +1066,14 @@ class PyCodeKGAnalyzer:
985
1066
  if len(re.findall(rf"\b{re.escape(name)}\b", source)) > 1:
986
1067
  return True
987
1068
 
1069
+ # Web route handlers: registered by decorator and called by the
1070
+ # framework's router, so no CALLS edge reaches them.
1071
+ if kind in ("function", "method"):
1072
+ source = self._module_source(module)
1073
+ lineno = node.get("lineno")
1074
+ if source and lineno and _has_route_decorator(source, lineno):
1075
+ return True
1076
+
988
1077
  return False
989
1078
 
990
1079
  def _is_unverifiable_override(self, node: dict, ctx: dict) -> bool:
@@ -2505,6 +2594,9 @@ class PyCodeKGAnalyzer:
2505
2594
  "coderank_top_nodes": self.coderank_top_nodes,
2506
2595
  # Option D: concern-based hybrid ranking
2507
2596
  "concern_analysis": self.concern_analysis,
2597
+ # An empty section above may mean a phase did not run, not that
2598
+ # the codebase has nothing to show.
2599
+ "phase_failures": self.phase_failures,
2508
2600
  }
2509
2601
 
2510
2602
  def to_markdown(self, *, metadata: str = "", elapsed_seconds: float | None = None) -> str:
@@ -124,6 +124,35 @@ def render_markdown(analyzer, *, metadata: str = "", elapsed_seconds: float | No
124
124
  out += [f"# {repo_name} Analysis", "", f"**Generated:** {generated}"]
125
125
  rule()
126
126
 
127
+ # ── Incomplete Analysis ──────────────────────────────────────────────
128
+ # A phase that could not run leaves its section empty. Said plainly, that
129
+ # is a missing build step; left unsaid, it reads as a finding about the code.
130
+ failures = analyzer.phase_failures
131
+ if failures:
132
+ out += [
133
+ "## Incomplete Analysis",
134
+ "",
135
+ f"{len(failures)} of {analyzer._TOTAL_PHASES} phases could not run. "
136
+ "The sections they produce are missing from this report -- that is a "
137
+ "gap in the analysis, not a finding about the code.",
138
+ "",
139
+ ]
140
+ out += md_table(
141
+ ["Phase", "Name", "Reason"],
142
+ [(f["phase"], f["name"], f"`{f['error']}`") for f in failures],
143
+ aligns="rll",
144
+ )
145
+ if any(f["missing_index"] for f in failures):
146
+ out += [
147
+ "",
148
+ "> The semantic index is missing. Only the fan-out and concern-based "
149
+ "ranking phases need it; every other phase reads the SQLite graph "
150
+ "directly, which is why the rest of this report is complete. Run "
151
+ "`pycodekg build-index` (or `pycodekg build`) to build it, then "
152
+ "re-run the analysis.",
153
+ ]
154
+ rule()
155
+
127
156
  # ── Executive Summary ────────────────────────────────────────────────
128
157
  out += [
129
158
  "## Executive Summary",
File without changes