pycode-kg 0.27.1__tar.gz → 0.28.1__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.27.1 → pycode_kg-0.28.1}/PKG-INFO +12 -12
  2. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/README.md +6 -6
  3. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/pyproject.toml +7 -28
  4. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/__init__.py +1 -1
  5. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_hooks.py +9 -2
  6. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/mcp_server.py +19 -0
  7. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/pycodekg_thorough_analysis.py +33 -1
  8. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/report.py +29 -0
  9. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/LICENSE +0 -0
  10. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/.DS_Store +0 -0
  11. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/__main__.py +0 -0
  12. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/analysis/__init__.py +0 -0
  13. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/analysis/bridge.py +0 -0
  14. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/analysis/centrality.py +0 -0
  15. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/analysis/framework_detector.py +0 -0
  16. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/app.py +0 -0
  17. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/architecture.py +0 -0
  18. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
  19. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/__init__.py +0 -0
  20. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_analyze.py +0 -0
  21. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_architecture.py +0 -0
  22. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_bridges.py +0 -0
  23. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_build.py +0 -0
  24. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_build_full.py +0 -0
  25. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_centrality.py +0 -0
  26. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_explain.py +0 -0
  27. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
  28. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_init.py +0 -0
  29. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_mcp.py +0 -0
  30. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_model.py +0 -0
  31. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_query.py +0 -0
  32. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_quilt.py +0 -0
  33. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_snapshot.py +0 -0
  34. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/cmd_viz.py +0 -0
  35. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/main.py +0 -0
  36. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/cli/options.py +0 -0
  37. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/config.py +0 -0
  38. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/explain.py +0 -0
  39. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/graph.py +0 -0
  40. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/graph_html.py +0 -0
  41. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/index.py +0 -0
  42. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/kg.py +0 -0
  43. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/layout3d.py +0 -0
  44. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/module/__init__.py +0 -0
  45. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/module/base.py +0 -0
  46. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/module/extractor.py +0 -0
  47. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/module/types.py +0 -0
  48. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/pycodekg.py +0 -0
  49. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/ranking/__init__.py +0 -0
  50. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/ranking/cli_rank.py +0 -0
  51. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/ranking/coderank.py +0 -0
  52. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/render.py +0 -0
  53. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/resolution.py +0 -0
  54. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/scene3d.py +0 -0
  55. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/snapshots.py +0 -0
  56. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
  57. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/store.py +0 -0
  58. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/theme.py +0 -0
  59. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/utils.py +0 -0
  60. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/visitor.py +0 -0
  61. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/src/pycode_kg/viz3d.py +0 -0
  62. {pycode_kg-0.27.1 → pycode_kg-0.28.1}/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.27.1
3
+ Version: 0.28.1
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,11 +21,11 @@ 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.22.0)
25
- Requires-Dist: kgmodule-utils[viz3d-qt,viz3d-render] (>=0.22.0) ; extra == "viz3d"
24
+ Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.23.0)
25
+ Requires-Dist: kgmodule-utils[viz3d-qt,viz3d-render] (>=0.23.0) ; extra == "viz3d"
26
26
  Requires-Dist: markdown (>=3.6) ; extra == "all"
27
27
  Requires-Dist: markdown (>=3.6) ; extra == "viz3d"
28
- Requires-Dist: mcp (>=1.0.0,<2)
28
+ Requires-Dist: mcp (>=1.3.0,<2)
29
29
  Requires-Dist: networkx (>=3.0)
30
30
  Requires-Dist: numpy (>=1.24.0)
31
31
  Requires-Dist: pandas (>=2.0.0)
@@ -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.14.1) ; extra == "all"
42
- Requires-Dist: quiltwright (>=0.14.1) ; extra == "viz3d"
41
+ Requires-Dist: quiltwright (>=0.15.0) ; extra == "all"
42
+ Requires-Dist: quiltwright (>=0.15.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,10 +60,10 @@ 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.27.1-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
63
+ [![Version](https://img.shields.io/badge/version-0.28.1-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
- [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
66
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
67
67
 
68
68
  # PyCodeKG — A Knowledge Graph for Python Codebases
69
69
 
@@ -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.27.1.md](docs/analysis_v0.27.1.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.1.md](docs/analysis_v0.28.1.md), produced (of course) by `pycodekg analyze` against this very repo.
259
259
 
260
260
  ---
261
261
 
@@ -280,15 +280,15 @@ The MCP server, the CLI, and the Streamlit app are thin wrappers over the same s
280
280
 
281
281
  If you use PyCodeKG in your research or project, please cite it:
282
282
 
283
- [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
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.27.1) [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.1) [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.27.1},
291
+ version = {0.28.1},
292
292
  year = {2026},
293
293
  publisher = {Flux-Frontiers},
294
294
  url = {https://github.com/Flux-Frontiers/pycode_kg},
@@ -5,10 +5,10 @@
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.27.1-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
8
+ [![Version](https://img.shields.io/badge/version-0.28.1-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
- [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
11
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19737993-blue.svg)](https://doi.org/10.5281/zenodo.19737993)
12
12
 
13
13
  # PyCodeKG — A Knowledge Graph for Python Codebases
14
14
 
@@ -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.27.1.md](docs/analysis_v0.27.1.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.1.md](docs/analysis_v0.28.1.md), produced (of course) by `pycodekg analyze` against this very repo.
204
204
 
205
205
  ---
206
206
 
@@ -225,15 +225,15 @@ The MCP server, the CLI, and the Streamlit app are thin wrappers over the same s
225
225
 
226
226
  If you use PyCodeKG in your research or project, please cite it:
227
227
 
228
- [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
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.27.1) [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.1) [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.27.1},
236
+ version = {0.28.1},
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.27.1"
73
+ version = "0.28.1"
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"
@@ -96,7 +96,7 @@ dependencies = [
96
96
  # FastMCP from mcp, so an unbounded floor lets a clean install pull 2.x and
97
97
  # break `pycodekg-mcp` at import time. Lift this only alongside a port to
98
98
  # the standalone fastmcp package.
99
- "mcp>=1.0.0,<2",
99
+ "mcp>=1.3.0,<2",
100
100
  "networkx>=3.0",
101
101
  "numpy>=1.24.0",
102
102
  "pandas>=2.0.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.22.0",
172
+ "kgmodule-utils[semantic,viz3d]>=0.23.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.22.0",
197
+ "kgmodule-utils[viz3d-render,viz3d-qt]>=0.23.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.14.1",
207
+ "quiltwright>=0.15.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.14.1",
232
+ "quiltwright>=0.15.0",
233
233
  "streamlit>=1.56.0",
234
234
  "trame-vtk>=2.0.0",
235
235
  ]
@@ -249,30 +249,9 @@ pdoc = ">=14.0.0"
249
249
  pre-commit = ">=4.5.1"
250
250
  pytest = ">=9.0.3"
251
251
  pytest-cov = ">=5.0.0"
252
- ruff = ">=0.4.0,<0.16"
252
+ ruff = ">=0.15,<0.16"
253
253
  ty = ">=0.0.41"
254
254
 
255
- # Maintainer tooling, not a dependency of this package. `.mcp.json` serves a
256
- # `dockg` MCP server from `.venv/bin/dockg`, so the CLI has to be in this repo's
257
- # environment — but nothing under src/ imports doc_kg, and declaring it as a real
258
- # dependency would put it in the published wheel for every consumer.
259
- #
260
- # A Poetry group is the right shape: it is locked and installable but never
261
- # written into wheel metadata, so `pip install pycode-kg` is unaffected.
262
- #
263
- # poetry install --with kg # gets the dockg CLI into .venv/bin
264
- # poetry install # default — the group is optional, skipped
265
- #
266
- # `kg` is the fleet-wide name for this group so there is one command to remember;
267
- # contents are per-repo and minimal, covering only what that repo actually
268
- # invokes. Safe to lock because doc-kg keeps its own pycode-kg in a group too, so
269
- # the published metadata on either side has no cycle for the resolver to chase.
270
- [tool.poetry.group.kg]
271
- optional = true
272
-
273
- [tool.poetry.group.kg.dependencies]
274
- doc-kg = ">=0.22.0"
275
-
276
255
  [project.urls]
277
256
  Homepage = "https://github.com/Flux-Frontiers/pycode_kg"
278
257
  Repository = "https://github.com/Flux-Frontiers/pycode_kg"
@@ -36,7 +36,7 @@ Author: Eric G. Suchanek, PhD
36
36
  License: Elastic 2.0
37
37
  """
38
38
 
39
- __version__ = "0.27.1"
39
+ __version__ = "0.28.1"
40
40
  __author__ = "Eric G. Suchanek, PhD"
41
41
 
42
42
  # Low-level primitives (locked v0 contract)
@@ -105,10 +105,17 @@ DEFAULT_BRANCH="${DEFAULT_BRANCH#origin/}"
105
105
  [ "$BRANCH" != "${DEFAULT_BRANCH:-main}" ] && exit 0
106
106
 
107
107
  # Rebuild the local index to keep it in sync with staged content.
108
- "$REPO_ROOT/.venv/bin/pycodekg" build --repo "$REPO_ROOT" || exit 1
108
+ # pycodekg is a tool this repo runs, not a dependency it declares: the fleet
109
+ # installs it once, globally (uv tool install pycode-kg), so PATH is the normal
110
+ # case. A .venv copy is honoured if present, for a checkout that still has one.
111
+ PYCODEKG="$(command -v pycodekg 2>/dev/null || true)"
112
+ [ -n "$PYCODEKG" ] || PYCODEKG="$REPO_ROOT/.venv/bin/pycodekg"
113
+ [ -x "$PYCODEKG" ] || { echo "[pycodekg] not found on PATH or in .venv; uv tool install pycode-kg" >&2; exit 1; }
114
+
115
+ "$PYCODEKG" build --repo "$REPO_ROOT" || exit 1
109
116
 
110
117
  # Snapshot PyCodeKG (version auto-detected from installed package).
111
- "$REPO_ROOT/.venv/bin/pycodekg" snapshot save \\
118
+ "$PYCODEKG" snapshot save \\
112
119
  --repo . \\
113
120
  --tree-hash "$TREE_HASH" \\
114
121
  --branch "$BRANCH" \\
@@ -142,6 +142,8 @@ from __future__ import annotations
142
142
  import argparse
143
143
  import json
144
144
  import sys
145
+ from collections.abc import AsyncIterator
146
+ from contextlib import asynccontextmanager
145
147
  from pathlib import Path
146
148
 
147
149
  from mcp.server.fastmcp import FastMCP
@@ -266,6 +268,22 @@ def _snapshot_freshness(snapshot_total_nodes: int) -> dict:
266
268
  # MCP server
267
269
  # ---------------------------------------------------------------------------
268
270
 
271
+
272
+ @asynccontextmanager
273
+ async def _lifespan(_server: FastMCP) -> AsyncIterator[None]:
274
+ """Close the graph's SQLite connection(s) when the server shuts down.
275
+
276
+ ``main()`` sets the module-level ``_kg`` before ``mcp.run()`` calls into
277
+ this, and both the stdio and SSE transports route through the same
278
+ underlying ``Server.run()``, so this fires on either one.
279
+ """
280
+ try:
281
+ yield
282
+ finally:
283
+ if _kg is not None:
284
+ _kg.close()
285
+
286
+
269
287
  mcp = FastMCP(
270
288
  "pycodekg",
271
289
  instructions=(
@@ -396,6 +414,7 @@ mcp = FastMCP(
396
414
  "- **Structure-aware query**: query_ranked(q='database connection', mode='hybrid')\n"
397
415
  "- **PPR query**: query_ranked(q='...', mode='ppr') → explain_rank(node_id, q='...')"
398
416
  ),
417
+ lifespan=_lifespan,
399
418
  )
400
419
 
401
420
 
@@ -43,6 +43,7 @@ from collections.abc import Callable
43
43
  from dataclasses import asdict, dataclass
44
44
  from pathlib import Path
45
45
 
46
+ from kg_utils.vector_backend import VectorStoreNotFoundError
46
47
  from rich.columns import Columns
47
48
  from rich.console import Console
48
49
  from rich.panel import Panel
@@ -444,6 +445,8 @@ class PyCodeKGAnalyzer:
444
445
  self.concern_analysis: list[dict] = [] # populated by _analyze_concerns
445
446
  # Single-line phase output: each phase fn writes its summary here
446
447
  self._phase_result: str = ""
448
+ # Phases that raised; rendered in the report (see _run_phase)
449
+ self.phase_failures: list[dict] = []
447
450
 
448
451
  # ── total phase count (update if phases are added/removed) ────────────────
449
452
  _TOTAL_PHASES = 15
@@ -455,13 +458,39 @@ class PyCodeKGAnalyzer:
455
458
  instead of printing directly, keeping the terminal output to one line
456
459
  per phase.
457
460
 
461
+ A phase that cannot run must not take the others and the report with
462
+ it. The concrete case is ``pycodekg build-sqlite``, a supported command
463
+ that builds the graph without the vector index: the fan-out and
464
+ concern-ranking phases seed on a semantic query and raise
465
+ ``VectorStoreNotFoundError``, while every other phase reads the graph
466
+ only. The failure is recorded in :attr:`phase_failures` and rendered in
467
+ the report, so a degraded run names its missing sections instead of
468
+ quietly omitting them.
469
+
458
470
  :param num: Phase number (1-based).
459
471
  :param name: Human-readable phase label.
460
472
  :param fn: Zero-argument callable to execute.
461
473
  """
462
474
  self._phase_result = ""
463
475
  t0 = time.monotonic()
464
- fn()
476
+ try:
477
+ fn()
478
+ except Exception as exc: # noqa: BLE001 -- see docstring
479
+ elapsed = time.monotonic() - t0
480
+ self.phase_failures.append(
481
+ {
482
+ "phase": num,
483
+ "name": name,
484
+ "error": str(exc),
485
+ "missing_index": isinstance(exc, VectorStoreNotFoundError),
486
+ }
487
+ )
488
+ logger.warning("Phase %d (%s) failed: %s", num, name, exc)
489
+ self.console.print(
490
+ f" [cyan]▶ Phase {num:2d}/{self._TOTAL_PHASES}:[/cyan]"
491
+ f" {name} [yellow]skipped: {exc}[/yellow] [green]({elapsed:.1f}s)[/green]"
492
+ )
493
+ return
465
494
  elapsed = time.monotonic() - t0
466
495
  result = f" {self._phase_result}" if self._phase_result else ""
467
496
  self.console.print(
@@ -2505,6 +2534,9 @@ class PyCodeKGAnalyzer:
2505
2534
  "coderank_top_nodes": self.coderank_top_nodes,
2506
2535
  # Option D: concern-based hybrid ranking
2507
2536
  "concern_analysis": self.concern_analysis,
2537
+ # An empty section above may mean a phase did not run, not that
2538
+ # the codebase has nothing to show.
2539
+ "phase_failures": self.phase_failures,
2508
2540
  }
2509
2541
 
2510
2542
  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