pycode-kg 0.23.1__tar.gz → 0.24.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.
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/PKG-INFO +9 -9
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/README.md +4 -4
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/pyproject.toml +36 -13
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/__init__.py +1 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/analysis/bridge.py +0 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/analysis/centrality.py +0 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/analysis/framework_detector.py +0 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/architecture.py +2 -2
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_hooks.py +52 -11
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_init.py +4 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_quilt.py +0 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_snapshot.py +4 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/module/extractor.py +1 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/pycodekg.py +103 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/pycodekg_thorough_analysis.py +356 -79
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/report.py +53 -2
- pycode_kg-0.24.1/src/pycode_kg/resolution.py +211 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/scene3d.py +5 -51
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/snapshots.py +16 -1
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/viz3d.py +45 -47
- pycode_kg-0.23.1/src/pycode_kg/resolution.py +0 -95
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/LICENSE +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/.DS_Store +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/__main__.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/analysis/__init__.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/app.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/__init__.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_analyze.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_architecture.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_bridges.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_build.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_build_full.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_centrality.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_explain.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_mcp.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_model.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_query.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/cmd_viz.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/main.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/cli/options.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/config.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/explain.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/graph.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/graph_html.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/index.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/kg.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/layout3d.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/mcp_server.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/module/__init__.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/module/base.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/module/types.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/ranking/__init__.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/ranking/cli_rank.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/ranking/coderank.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/render.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/store.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/theme.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/utils.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.1}/src/pycode_kg/visitor.py +0 -0
- {pycode_kg-0.23.1 → pycode_kg-0.24.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.
|
|
3
|
+
Version: 0.24.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,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.
|
|
25
|
-
Requires-Dist: kgmodule-utils[viz3d-render] (>=0.
|
|
24
|
+
Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.18.1)
|
|
25
|
+
Requires-Dist: kgmodule-utils[viz3d-qt,viz3d-render] (>=0.18.1) ; 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.0.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.
|
|
42
|
-
Requires-Dist: quiltwright (>=0.
|
|
41
|
+
Requires-Dist: quiltwright (>=0.8.0) ; extra == "all"
|
|
42
|
+
Requires-Dist: quiltwright (>=0.8.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
|
[](https://www.python.org/)
|
|
62
62
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
63
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
64
64
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
65
65
|
[](https://python-poetry.org/)
|
|
66
66
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -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.
|
|
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.24.1.md](docs/analysis_v0.24.1.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
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
284
284
|
|
|
285
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
285
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.24.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.
|
|
291
|
+
version = {0.24.1},
|
|
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
|
[](https://www.python.org/)
|
|
7
7
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
8
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
9
9
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
10
10
|
[](https://python-poetry.org/)
|
|
11
11
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -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.
|
|
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.24.1.md](docs/analysis_v0.24.1.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
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
229
229
|
|
|
230
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
230
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.24.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.
|
|
236
|
+
version = {0.24.1},
|
|
237
237
|
year = {2026},
|
|
238
238
|
publisher = {Flux-Frontiers},
|
|
239
239
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# pyproject.toml — PyCodeKG package configuration (PEP 621)
|
|
2
2
|
#
|
|
3
3
|
# Author: Eric G. Suchanek, PhD
|
|
4
|
-
# Last Revision: 2026-08-15
|
|
5
4
|
#
|
|
6
5
|
# Build system : Poetry 2.x with PEP 621 [project] table
|
|
7
6
|
#
|
|
@@ -71,7 +70,7 @@ torch = [
|
|
|
71
70
|
# ---------------------------------------------------------------------------
|
|
72
71
|
[project]
|
|
73
72
|
name = "pycode-kg"
|
|
74
|
-
version = "0.
|
|
73
|
+
version = "0.24.1"
|
|
75
74
|
description = "A tool to build a searchable knowledge graph from Python repositories"
|
|
76
75
|
readme = "README.md"
|
|
77
76
|
license = "Elastic-2.0"
|
|
@@ -137,12 +136,26 @@ dependencies = [
|
|
|
137
136
|
# Its only dependency is numpy, which `[semantic]` already pulls, so it adds
|
|
138
137
|
# nothing to the install — it just declares what layout3d actually imports.
|
|
139
138
|
# Unlike `[viz]`, there is no pyvis to leak into a bare install.
|
|
140
|
-
# Floor is 0.13.
|
|
141
|
-
# stops
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
|
|
139
|
+
# Floor is 0.13.x rather than 0.12.1 because that is where SnapshotManager
|
|
140
|
+
# stops writing absolute paths into snapshot JSON. This package ships
|
|
141
|
+
# `pycodekg snapshot save`, and those snapshots get committed to the
|
|
142
|
+
# user's repo — against an older kgmodule-utils the command writes their
|
|
143
|
+
# home directory and username into version control.
|
|
144
|
+
#
|
|
145
|
+
# 0.13.2 specifically, never 0.13.1: that release carried the path fix but
|
|
146
|
+
# also made `repo_root` a read-only property, which breaks every subclass
|
|
147
|
+
# that assigns it. 0.13.2 keeps the fix and restores the plain attribute.
|
|
148
|
+
#
|
|
149
|
+
# 0.15.0 because the `viz3d` extra below needs kg_utils.viz3d.qt, and this
|
|
150
|
+
# repo's own rule is one floor per package — a lower floor here would leave
|
|
151
|
+
# a reader working out which constraint actually binds. Nothing in the core
|
|
152
|
+
# install needs 0.15.0 functionally; it is a superset of 0.14.0.
|
|
153
|
+
#
|
|
154
|
+
# 0.18.1 because resolve_symbols() only reads the receiver_class metadata
|
|
155
|
+
# this repo's visitor writes as of 0.24.1 -- against 0.18.0 the stubs are
|
|
156
|
+
# tagged correctly but the tag is silently ignored, so resolution stays on
|
|
157
|
+
# the old untyped trailing-name match with no error to say so.
|
|
158
|
+
"kgmodule-utils[semantic,viz3d]>=0.18.1",
|
|
146
159
|
]
|
|
147
160
|
|
|
148
161
|
[project.optional-dependencies]
|
|
@@ -159,12 +172,22 @@ viz3d = [
|
|
|
159
172
|
# and smooth_paths live there. The main dependency above stays on plain
|
|
160
173
|
# [viz3d] (numpy only) so the nine repos that take pycode-kg do not acquire
|
|
161
174
|
# VTK for a layout import; rendering stays opt-in, here.
|
|
162
|
-
|
|
175
|
+
# Same floor as the core declaration above — one package, one floor, so a
|
|
176
|
+
# reader does not have to work out which constraint actually binds.
|
|
177
|
+
# [viz3d-qt] (new in 0.15.0) adds kg_utils.viz3d.qt — the Qt machinery a
|
|
178
|
+
# viewer needs to cast: the render lifecycle and cast_scene_to_looking_glass,
|
|
179
|
+
# which this module's "Cast to LG" button now drives instead of open-coding
|
|
180
|
+
# the four-step build/render/write/cast itself. It subsumes [viz3d-render]
|
|
181
|
+
# (it declares pyvista too), but both are named so the dependency reads as
|
|
182
|
+
# what it is rather than as a side effect.
|
|
183
|
+
"kgmodule-utils[viz3d-render,viz3d-qt]>=0.18.1",
|
|
163
184
|
"pyvistaqt>=0.11.0",
|
|
164
185
|
"trame-vtk>=2.0.0",
|
|
165
186
|
# Looking Glass quilts. 0.4.0 supplies depth_report() and widened
|
|
166
|
-
# requires-python to <3.14, which retires the old marker gate.
|
|
167
|
-
|
|
187
|
+
# requires-python to <3.14, which retires the old marker gate. 0.6.0 adds
|
|
188
|
+
# QuiltSpec.scaled(), which replaces the tile-grid rounding this repo used
|
|
189
|
+
# to open-code, and save_and_cast_quilt() underneath the SDK cast helper.
|
|
190
|
+
"quiltwright>=0.8.0",
|
|
168
191
|
]
|
|
169
192
|
|
|
170
193
|
# Cross-KG sibling packages (doc-kg, pycode-kg, agent-kg, kg-rag) are NOT
|
|
@@ -189,7 +212,7 @@ all = [
|
|
|
189
212
|
"pyvis>=0.3.2",
|
|
190
213
|
"pyvista>=0.44.0",
|
|
191
214
|
"pyvistaqt>=0.11.0",
|
|
192
|
-
"quiltwright>=0.
|
|
215
|
+
"quiltwright>=0.8.0",
|
|
193
216
|
"streamlit>=1.56.0",
|
|
194
217
|
"trame-vtk>=2.0.0",
|
|
195
218
|
]
|
|
@@ -231,7 +254,7 @@ ty = ">=0.0.41"
|
|
|
231
254
|
optional = true
|
|
232
255
|
|
|
233
256
|
[tool.poetry.group.kg.dependencies]
|
|
234
|
-
doc-kg = ">=0.
|
|
257
|
+
doc-kg = ">=0.22.0"
|
|
235
258
|
|
|
236
259
|
[project.urls]
|
|
237
260
|
Homepage = "https://github.com/Flux-Frontiers/pycode_kg"
|
|
@@ -610,11 +610,11 @@ class ArchitectureAnalyzer:
|
|
|
610
610
|
readme = self.repo_root / "README.md"
|
|
611
611
|
if readme.exists():
|
|
612
612
|
try:
|
|
613
|
-
with open(readme) as f:
|
|
613
|
+
with open(readme, encoding="utf-8") as f:
|
|
614
614
|
for line in f:
|
|
615
615
|
if line.startswith("#"):
|
|
616
616
|
return line.lstrip("#").strip()
|
|
617
|
-
except OSError:
|
|
617
|
+
except (OSError, UnicodeDecodeError):
|
|
618
618
|
pass
|
|
619
619
|
return "PyCodeKG Architecture"
|
|
620
620
|
|
|
@@ -6,7 +6,6 @@ CLI command for installing PyCodeKG git hooks:
|
|
|
6
6
|
install-hooks — install the pre-commit snapshot hook into .git/hooks/
|
|
7
7
|
|
|
8
8
|
Author: Eric G. Suchanek, PhD
|
|
9
|
-
Last Revision: 2026-08-14 22:51:58
|
|
10
9
|
|
|
11
10
|
Author: Eric G. Suchanek, PhD
|
|
12
11
|
|
|
@@ -29,10 +28,33 @@ from pycode_kg.cli.main import cli
|
|
|
29
28
|
|
|
30
29
|
_PRE_COMMIT_HOOK = """\
|
|
31
30
|
#!/usr/bin/env bash
|
|
32
|
-
# PyCodeKG pre-commit hook — runs quality checks
|
|
33
|
-
#
|
|
31
|
+
# PyCodeKG pre-commit hook — runs quality checks. The index rebuild and metrics
|
|
32
|
+
# snapshot are opt-in and OFF by default; see "Why snapshots are off" below.
|
|
34
33
|
# Installed by: pycodekg install-hooks
|
|
35
|
-
#
|
|
34
|
+
#
|
|
35
|
+
# PYCODEKG_SNAPSHOT=1 git commit ... opt in to a per-commit snapshot
|
|
36
|
+
# PYCODEKG_SKIP_SNAPSHOT=1 git commit ... force snapshots off (wins)
|
|
37
|
+
#
|
|
38
|
+
# Note that PYCODEKG_SKIP_SNAPSHOT no longer skips the quality checks. It used
|
|
39
|
+
# to short-circuit the whole hook, so a variable named "skip snapshot" also
|
|
40
|
+
# silently skipped ruff, ty and pytest. It now gates only what it names.
|
|
41
|
+
#
|
|
42
|
+
# Why snapshots are off by default (2026-08-18)
|
|
43
|
+
# ---------------------------------------------
|
|
44
|
+
# A per-commit snapshot records `git write-tree` and is then itself staged into
|
|
45
|
+
# that same commit. Staging changes the index, so the recorded hash can never
|
|
46
|
+
# equal the tree it claims to describe — and manifest.json carries a
|
|
47
|
+
# `last_update` timestamp, so the `git add` is never a no-op. The drift is
|
|
48
|
+
# guaranteed by construction, not caused by formatting, and reordering does not
|
|
49
|
+
# fix it: the `git add` lands after the hash either way.
|
|
50
|
+
#
|
|
51
|
+
# An audit of 605 snapshots across 29 fleet manifests found 63 (10.4%) keyed to
|
|
52
|
+
# a tree any commit actually has. `snapshot diff` between adjacent entries has
|
|
53
|
+
# therefore been comparing states that never existed.
|
|
54
|
+
#
|
|
55
|
+
# The fix is to snapshot at release, keyed on the tag rather than on an
|
|
56
|
+
# ephemeral pre-commit tree. See kgrag_priv/docs/SNAPSHOT_STRATEGY.md. Until
|
|
57
|
+
# that lands, this hook runs quality checks only.
|
|
36
58
|
#
|
|
37
59
|
# Order matters, and it is deliberately checks-then-index:
|
|
38
60
|
#
|
|
@@ -46,8 +68,6 @@ _PRE_COMMIT_HOOK = """\
|
|
|
46
68
|
# commit that ruff/ty/pytest is about to reject.
|
|
47
69
|
set -euo pipefail
|
|
48
70
|
|
|
49
|
-
[ "${PYCODEKG_SKIP_SNAPSHOT:-0}" = "1" ] && exit 0
|
|
50
|
-
|
|
51
71
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
52
72
|
|
|
53
73
|
cd "$REPO_ROOT"
|
|
@@ -63,12 +83,27 @@ elif command -v pre-commit &>/dev/null; then
|
|
|
63
83
|
pre-commit run || exit 1
|
|
64
84
|
fi
|
|
65
85
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
# Opt-in index rebuild + snapshot. Everything below is skipped unless
|
|
88
|
+
# PYCODEKG_SNAPSHOT=1 is set, and is skipped regardless if
|
|
89
|
+
# PYCODEKG_SKIP_SNAPSHOT=1.
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
[ "${PYCODEKG_SNAPSHOT:-0}" = "1" ] || exit 0
|
|
92
|
+
[ "${PYCODEKG_SKIP_SNAPSHOT:-0}" = "1" ] && exit 0
|
|
93
|
+
|
|
94
|
+
# Captured after the checks so nothing further modifies the working tree. Note
|
|
95
|
+
# the caveat above: this still cannot match the committed tree, because the
|
|
96
|
+
# `git add` below changes the index after this point.
|
|
69
97
|
TREE_HASH=$(git write-tree)
|
|
70
98
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
71
99
|
|
|
100
|
+
# Snapshots are default-branch history. On any other branch, stop here —
|
|
101
|
+
# feature-branch commits (and the PRs/CI built from them) stay free of
|
|
102
|
+
# generated snapshot files, and skip the 60-90s rebuild along the way.
|
|
103
|
+
DEFAULT_BRANCH=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)
|
|
104
|
+
DEFAULT_BRANCH="${DEFAULT_BRANCH#origin/}"
|
|
105
|
+
[ "$BRANCH" != "${DEFAULT_BRANCH:-main}" ] && exit 0
|
|
106
|
+
|
|
72
107
|
# Rebuild the local index to keep it in sync with staged content.
|
|
73
108
|
"$REPO_ROOT/.venv/bin/pycodekg" build --repo "$REPO_ROOT" || exit 1
|
|
74
109
|
|
|
@@ -104,11 +139,14 @@ exit 0
|
|
|
104
139
|
def install_hooks(repo: str, force: bool) -> None:
|
|
105
140
|
"""Install the PyCodeKG pre-commit git hook.
|
|
106
141
|
|
|
107
|
-
After installation, before each commit:
|
|
142
|
+
After installation, before each commit on the default branch:
|
|
108
143
|
1. Rebuilds the local PyCodeKG index (full wipe)
|
|
109
144
|
2. Captures a metrics snapshot, keyed by tree hash
|
|
110
145
|
3. Stages the snapshot directory
|
|
111
146
|
|
|
147
|
+
On any other branch the quality checks still run but the rebuild and
|
|
148
|
+
snapshot are skipped, so feature-branch commits carry no generated files.
|
|
149
|
+
|
|
112
150
|
This keeps the index in sync and ensures snapshots reflect the state of
|
|
113
151
|
the knowledge graph at commit time. The hook only ever touches its own
|
|
114
152
|
repository — sibling KG repos install their own hooks.
|
|
@@ -137,5 +175,8 @@ def install_hooks(repo: str, force: bool) -> None:
|
|
|
137
175
|
hook_path.chmod(mode)
|
|
138
176
|
|
|
139
177
|
click.echo(f"OK Installed pre-commit hook: {hook_path}")
|
|
140
|
-
click.echo("
|
|
178
|
+
click.echo(" Quality checks run on every commit.")
|
|
179
|
+
click.echo(" Snapshots are OFF by default - see kgrag_priv/docs/SNAPSHOT_STRATEGY.md.")
|
|
180
|
+
click.echo(" Opt in with: PYCODEKG_SNAPSHOT=1 git commit ...")
|
|
181
|
+
click.echo(" Force off: PYCODEKG_SKIP_SNAPSHOT=1 git commit ...")
|
|
141
182
|
click.echo(" Run 'pycodekg build' first if you haven't built the graph yet.")
|
|
@@ -253,6 +253,8 @@ def init(
|
|
|
253
253
|
|
|
254
254
|
docstring_cov = analysis.get("docstring_coverage", {})
|
|
255
255
|
coverage = docstring_cov.get("coverage_pct", 0.0) / 100.0 if docstring_cov else 0.0
|
|
256
|
+
coverage_documented = docstring_cov.get("with_doc", 0)
|
|
257
|
+
coverage_total = docstring_cov.get("total", 0)
|
|
256
258
|
critical_issues = len(analysis.get("issues", []))
|
|
257
259
|
|
|
258
260
|
fn_metrics = analysis.get("function_metrics", {})
|
|
@@ -276,6 +278,8 @@ def init(
|
|
|
276
278
|
branch=branch,
|
|
277
279
|
graph_stats_dict=stats,
|
|
278
280
|
coverage=coverage,
|
|
281
|
+
coverage_documented=coverage_documented,
|
|
282
|
+
coverage_total=coverage_total,
|
|
279
283
|
critical_issues=critical_issues,
|
|
280
284
|
complexity_median=complexity_median,
|
|
281
285
|
hotspots=hotspots,
|
|
@@ -111,6 +111,8 @@ def save_snapshot(
|
|
|
111
111
|
# Extract metrics from analysis results
|
|
112
112
|
docstring_cov = analysis.get("docstring_coverage", {})
|
|
113
113
|
coverage = docstring_cov.get("coverage_pct", 0.0) / 100.0 if docstring_cov else 0.0
|
|
114
|
+
coverage_documented = docstring_cov.get("with_doc", 0)
|
|
115
|
+
coverage_total = docstring_cov.get("total", 0)
|
|
114
116
|
issue_strings = analysis.get("issues", [])
|
|
115
117
|
critical_issues = len(issue_strings)
|
|
116
118
|
|
|
@@ -140,6 +142,8 @@ def save_snapshot(
|
|
|
140
142
|
branch=branch,
|
|
141
143
|
graph_stats_dict=stats,
|
|
142
144
|
coverage=coverage,
|
|
145
|
+
coverage_documented=coverage_documented,
|
|
146
|
+
coverage_total=coverage_total,
|
|
143
147
|
critical_issues=critical_issues,
|
|
144
148
|
complexity_median=complexity_median,
|
|
145
149
|
hotspots=hotspots,
|
|
@@ -23,7 +23,6 @@ No persistence, no embeddings, no LLMs—just pure AST extraction. Integration w
|
|
|
23
23
|
vector databases and semantic search happens downstream.
|
|
24
24
|
|
|
25
25
|
Author: Eric G. Suchanek, PhD
|
|
26
|
-
Last Revision: 2026-08-15 00:46:18
|
|
27
26
|
|
|
28
27
|
License: Elastic 2.0
|
|
29
28
|
"""
|
|
@@ -62,6 +61,11 @@ class Node:
|
|
|
62
61
|
:param lineno: Starting line number
|
|
63
62
|
:param end_lineno: Ending line number (if available)
|
|
64
63
|
:param docstring: Extracted docstring (may be None)
|
|
64
|
+
:param receiver_class: For a dotted call stub (``kind == "symbol"``), the
|
|
65
|
+
class name resolved from the receiver's parameter or local variable
|
|
66
|
+
annotation, if any — e.g. ``"Plotter"`` for a call site
|
|
67
|
+
``plotter.render()`` where ``plotter: pv.Plotter``. ``None`` when the
|
|
68
|
+
receiver has no known annotation, or the node isn't a call stub.
|
|
65
69
|
"""
|
|
66
70
|
|
|
67
71
|
id: str
|
|
@@ -72,6 +76,7 @@ class Node:
|
|
|
72
76
|
lineno: int | None
|
|
73
77
|
end_lineno: int | None
|
|
74
78
|
docstring: str | None
|
|
79
|
+
receiver_class: str | None = None
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
@dataclass(frozen=True)
|
|
@@ -212,6 +217,91 @@ def _owner_id(
|
|
|
212
217
|
return module_locals[module].get(fn.name)
|
|
213
218
|
|
|
214
219
|
|
|
220
|
+
def _own_scope_nodes(node: ast.AST) -> Iterable[ast.AST]:
|
|
221
|
+
"""Yield descendants of *node*, not descending into nested function/class scopes.
|
|
222
|
+
|
|
223
|
+
A nested ``def``/``class``/``lambda`` introduces its own name binding for
|
|
224
|
+
any local it declares, so an ``AnnAssign`` inside one does not describe a
|
|
225
|
+
name in *node*'s own scope.
|
|
226
|
+
|
|
227
|
+
:param node: Root AST node (typically a function body).
|
|
228
|
+
:return: Iterator over descendants in *node*'s own scope.
|
|
229
|
+
"""
|
|
230
|
+
for child in ast.iter_child_nodes(node):
|
|
231
|
+
if isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda | ast.ClassDef):
|
|
232
|
+
continue
|
|
233
|
+
yield child
|
|
234
|
+
yield from _own_scope_nodes(child)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _annotation_class_name(expr: ast.AST) -> str | None:
|
|
238
|
+
"""Extract a class name from a type annotation, unwrapping ``Optional`` shapes.
|
|
239
|
+
|
|
240
|
+
``X | None`` (PEP 604) and ``Optional[X]``/``typing.Optional[X]`` are the
|
|
241
|
+
common "this local starts as ``None``, gets assigned later" shape --
|
|
242
|
+
exactly the pattern a loop-accumulated match object or lazily-built
|
|
243
|
+
instance tends to carry. Neither is a class name on its own, so both are
|
|
244
|
+
unwrapped to the non-``None`` member before falling back to
|
|
245
|
+
:func:`expr_to_name`, which does not know about either.
|
|
246
|
+
|
|
247
|
+
:param expr: The annotation expression.
|
|
248
|
+
:return: Dotted class name, or ``None`` if it can't be determined.
|
|
249
|
+
"""
|
|
250
|
+
if isinstance(expr, ast.BinOp) and isinstance(expr.op, ast.BitOr):
|
|
251
|
+
for side in (expr.left, expr.right):
|
|
252
|
+
if isinstance(side, ast.Constant) and side.value is None:
|
|
253
|
+
continue
|
|
254
|
+
resolved = _annotation_class_name(side)
|
|
255
|
+
if resolved:
|
|
256
|
+
return resolved
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
if isinstance(expr, ast.Subscript):
|
|
260
|
+
base = expr_to_name(expr.value)
|
|
261
|
+
if base and base.rsplit(".", 1)[-1] == "Optional":
|
|
262
|
+
return _annotation_class_name(expr.slice)
|
|
263
|
+
return expr_to_name(expr)
|
|
264
|
+
|
|
265
|
+
if isinstance(expr, ast.Constant) and expr.value is None:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
return expr_to_name(expr)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _receiver_annotation(
|
|
272
|
+
fn: ast.FunctionDef | ast.AsyncFunctionDef, receiver_name: str
|
|
273
|
+
) -> str | None:
|
|
274
|
+
"""Resolve a call receiver's class name from its parameter or local annotation.
|
|
275
|
+
|
|
276
|
+
Looks at *fn*'s own parameters, then any ``AnnAssign`` in *fn*'s own body
|
|
277
|
+
(see :func:`_own_scope_nodes`), for a name matching *receiver_name*. The
|
|
278
|
+
first annotation found wins -- no reassignment or narrowing is tracked,
|
|
279
|
+
matching the conservative stance the rest of this module takes toward
|
|
280
|
+
ambiguity.
|
|
281
|
+
|
|
282
|
+
:param fn: Enclosing function or async function definition.
|
|
283
|
+
:param receiver_name: Bare name of the call's receiver, e.g. ``"plotter"``.
|
|
284
|
+
:return: The annotation's last dotted segment (e.g. ``"Plotter"`` for a
|
|
285
|
+
``pv.Plotter`` annotation, or ``"Match"`` for ``re.Match | None``), or
|
|
286
|
+
``None`` if *receiver_name* has no known annotation.
|
|
287
|
+
"""
|
|
288
|
+
for a in (*fn.args.posonlyargs, *fn.args.args, *fn.args.kwonlyargs):
|
|
289
|
+
if a.arg == receiver_name and a.annotation is not None:
|
|
290
|
+
cls = _annotation_class_name(a.annotation)
|
|
291
|
+
return cls.rsplit(".", 1)[-1] if cls else None
|
|
292
|
+
|
|
293
|
+
for stmt in _own_scope_nodes(fn):
|
|
294
|
+
if (
|
|
295
|
+
isinstance(stmt, ast.AnnAssign)
|
|
296
|
+
and isinstance(stmt.target, ast.Name)
|
|
297
|
+
and stmt.target.id == receiver_name
|
|
298
|
+
):
|
|
299
|
+
cls = _annotation_class_name(stmt.annotation)
|
|
300
|
+
return cls.rsplit(".", 1)[-1] if cls else None
|
|
301
|
+
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
215
305
|
# ============================================================================
|
|
216
306
|
# Core extraction logic
|
|
217
307
|
# ============================================================================
|
|
@@ -467,6 +557,8 @@ def extract_repo(
|
|
|
467
557
|
if not callee:
|
|
468
558
|
continue
|
|
469
559
|
|
|
560
|
+
receiver_class: str | None = None
|
|
561
|
+
|
|
470
562
|
# resolution rules (LOCKED)
|
|
471
563
|
if callee in module_locals[module]:
|
|
472
564
|
dst_id = module_locals[module][callee]
|
|
@@ -481,6 +573,15 @@ def extract_repo(
|
|
|
481
573
|
dst_id = module_class_methods[module].get(meth) or f"sym:{callee}"
|
|
482
574
|
else:
|
|
483
575
|
dst_id = f"sym:{callee}"
|
|
576
|
+
# A plain local variable/parameter receiver, not self/cls and
|
|
577
|
+
# not a module-level import alias -- the one case an
|
|
578
|
+
# annotation can disambiguate. self./cls. never reach here.
|
|
579
|
+
if (
|
|
580
|
+
isinstance(n.func, ast.Attribute)
|
|
581
|
+
and isinstance(n.func.value, ast.Name)
|
|
582
|
+
and n.func.value.id not in ("self", "cls")
|
|
583
|
+
):
|
|
584
|
+
receiver_class = _receiver_annotation(fn, n.func.value.id)
|
|
484
585
|
|
|
485
586
|
if dst_id.startswith("sym:"):
|
|
486
587
|
nodes.setdefault(
|
|
@@ -494,6 +595,7 @@ def extract_repo(
|
|
|
494
595
|
None,
|
|
495
596
|
None,
|
|
496
597
|
None,
|
|
598
|
+
receiver_class,
|
|
497
599
|
),
|
|
498
600
|
)
|
|
499
601
|
|