pycode-kg 0.21.2__tar.gz → 0.22.0__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.21.2 → pycode_kg-0.22.0}/PKG-INFO +11 -12
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/README.md +6 -6
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/pyproject.toml +56 -8
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/__init__.py +1 -1
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/bridge.py +1 -1
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/centrality.py +1 -1
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/framework_detector.py +1 -1
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_hooks.py +1 -1
- pycode_kg-0.22.0/src/pycode_kg/layout3d.py +84 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg.py +1 -1
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/snapshots.py +1 -1
- pycode_kg-0.21.2/src/pycode_kg/layout3d.py +0 -476
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/LICENSE +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/.DS_Store +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/__main__.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/__init__.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/hybrid_rank.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/app.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/architecture.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/__init__.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_analyze.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_architecture.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_bridges.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build_full.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_centrality.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_explain.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_init.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_mcp.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_model.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_query.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_snapshot.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_viz.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/main.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/options.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/config.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/explain.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/graph.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/graph_html.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/index.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/kg.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/mcp_server.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/__init__.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/base.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/extractor.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/types.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_query.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_snippet_packer.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_thorough_analysis.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz3d.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/__init__.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/cli_rank.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/coderank.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/store.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/theme.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/utils.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/visitor.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/viz3d.py +0 -0
- {pycode_kg-0.21.2 → pycode_kg-0.22.0}/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.22.0
|
|
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
|
|
@@ -24,7 +24,7 @@ Requires-Dist: PyQt5 (>=5.15.0) ; extra == "viz3d"
|
|
|
24
24
|
Requires-Dist: click (>=8.1.0,<9)
|
|
25
25
|
Requires-Dist: detect-secrets (>=1.5.0) ; extra == "all"
|
|
26
26
|
Requires-Dist: detect-secrets (>=1.5.0) ; extra == "dev"
|
|
27
|
-
Requires-Dist: kgmodule-utils[semantic,
|
|
27
|
+
Requires-Dist: kgmodule-utils[semantic,viz3d] (>=0.11.0)
|
|
28
28
|
Requires-Dist: markdown (>=3.6) ; extra == "all"
|
|
29
29
|
Requires-Dist: markdown (>=3.6) ; extra == "viz3d"
|
|
30
30
|
Requires-Dist: mcp (>=1.0.0,<2)
|
|
@@ -45,21 +45,20 @@ Requires-Dist: pytest-cov (>=5.0.0) ; extra == "all"
|
|
|
45
45
|
Requires-Dist: pytest-cov (>=5.0.0) ; extra == "dev"
|
|
46
46
|
Requires-Dist: pyvis (>=0.3.2) ; extra == "all"
|
|
47
47
|
Requires-Dist: pyvis (>=0.3.2) ; extra == "viz"
|
|
48
|
-
Requires-Dist: pyvista
|
|
49
|
-
Requires-Dist: pyvista
|
|
48
|
+
Requires-Dist: pyvista (>=0.44.0) ; extra == "all"
|
|
49
|
+
Requires-Dist: pyvista (>=0.44.0) ; extra == "viz3d"
|
|
50
50
|
Requires-Dist: pyvistaqt (>=0.11.0) ; extra == "all"
|
|
51
51
|
Requires-Dist: pyvistaqt (>=0.11.0) ; extra == "viz3d"
|
|
52
52
|
Requires-Dist: rich (>=14.3.3,<15)
|
|
53
53
|
Requires-Dist: ruff (>=0.4.0) ; extra == "all"
|
|
54
54
|
Requires-Dist: ruff (>=0.4.0) ; extra == "dev"
|
|
55
|
-
Requires-Dist: safetensors (>=0.5.0)
|
|
56
55
|
Requires-Dist: sentence-transformers (>=5.4.1)
|
|
56
|
+
Requires-Dist: sqlite-vec (==0.1.9)
|
|
57
57
|
Requires-Dist: streamlit (>=1.56.0) ; extra == "all"
|
|
58
58
|
Requires-Dist: streamlit (>=1.56.0) ; extra == "viz"
|
|
59
59
|
Requires-Dist: torch (>=2.5.1)
|
|
60
60
|
Requires-Dist: trame-vtk (>=2.0.0) ; extra == "all"
|
|
61
61
|
Requires-Dist: trame-vtk (>=2.0.0) ; extra == "viz3d"
|
|
62
|
-
Requires-Dist: transformers (>=5.5.0,<6)
|
|
63
62
|
Requires-Dist: ty (>=0.0.41) ; extra == "all"
|
|
64
63
|
Requires-Dist: ty (>=0.0.41) ; extra == "dev"
|
|
65
64
|
Project-URL: Homepage, https://github.com/Flux-Frontiers/pycode_kg
|
|
@@ -73,7 +72,7 @@ Description-Content-Type: text/markdown
|
|
|
73
72
|
|
|
74
73
|
[](https://www.python.org/)
|
|
75
74
|
[](https://www.elastic.co/licensing/elastic-license)
|
|
76
|
-
[](https://github.com/Flux-Frontiers/pycode_kg/releases)
|
|
77
76
|
[](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
|
|
78
77
|
[](https://python-poetry.org/)
|
|
79
78
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
@@ -244,7 +243,7 @@ src/pycode_kg/
|
|
|
244
243
|
└── viz3d_timeline.py # Metric history timeline
|
|
245
244
|
```
|
|
246
245
|
|
|
247
|
-
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.
|
|
246
|
+
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.22.0.md](docs/analysis_v0.22.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
248
247
|
|
|
249
248
|
---
|
|
250
249
|
|
|
@@ -270,17 +269,17 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
270
269
|
|
|
271
270
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
272
271
|
|
|
273
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
272
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.22.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
274
273
|
|
|
275
274
|
```bibtex
|
|
276
275
|
@software{suchanek_pycode_kg,
|
|
277
276
|
author = {Suchanek, Eric G.},
|
|
278
277
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
279
|
-
version = {0.
|
|
278
|
+
version = {0.22.0},
|
|
280
279
|
year = {2026},
|
|
281
280
|
publisher = {Flux-Frontiers},
|
|
282
281
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
283
|
-
doi = {10.5281/zenodo.
|
|
282
|
+
doi = {10.5281/zenodo.19737993},
|
|
284
283
|
}
|
|
285
284
|
```
|
|
286
285
|
|
|
@@ -300,5 +299,5 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
300
299
|
|
|
301
300
|
---
|
|
302
301
|
|
|
303
|
-
*Built for Python developers and AI agents that work alongside them — egs · Last updated
|
|
302
|
+
*Built for Python developers and AI agents that work alongside them — egs · Last updated August 2026*
|
|
304
303
|
|
|
@@ -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)
|
|
@@ -176,7 +176,7 @@ src/pycode_kg/
|
|
|
176
176
|
└── viz3d_timeline.py # Metric history timeline
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
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.
|
|
179
|
+
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.22.0.md](docs/analysis_v0.22.0.md), produced (of course) by `pycodekg analyze` against this very repo.
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
@@ -202,17 +202,17 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
202
202
|
|
|
203
203
|
[](https://zenodo.org/badge/latestdoi/1202379010)
|
|
204
204
|
|
|
205
|
-
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.
|
|
205
|
+
> Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.22.0) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19737993
|
|
206
206
|
|
|
207
207
|
```bibtex
|
|
208
208
|
@software{suchanek_pycode_kg,
|
|
209
209
|
author = {Suchanek, Eric G.},
|
|
210
210
|
title = {{PyCodeKG}: A Knowledge Graph for Python Codebases},
|
|
211
|
-
version = {0.
|
|
211
|
+
version = {0.22.0},
|
|
212
212
|
year = {2026},
|
|
213
213
|
publisher = {Flux-Frontiers},
|
|
214
214
|
url = {https://github.com/Flux-Frontiers/pycode_kg},
|
|
215
|
-
doi = {10.5281/zenodo.
|
|
215
|
+
doi = {10.5281/zenodo.19737993},
|
|
216
216
|
}
|
|
217
217
|
```
|
|
218
218
|
|
|
@@ -232,4 +232,4 @@ If you use PyCodeKG in your research or project, please cite it:
|
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
235
|
-
*Built for Python developers and AI agents that work alongside them — egs · Last updated
|
|
235
|
+
*Built for Python developers and AI agents that work alongside them — egs · Last updated August 2026*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pyproject.toml — PyCodeKG package configuration (PEP 621)
|
|
2
2
|
#
|
|
3
3
|
# Author: Eric G. Suchanek, PhD
|
|
4
|
-
# Last Revision: 2026-
|
|
4
|
+
# Last Revision: 2026-08-11
|
|
5
5
|
#
|
|
6
6
|
# Build system : Poetry 2.x with PEP 621 [project] table
|
|
7
7
|
#
|
|
@@ -70,7 +70,7 @@ torch = [
|
|
|
70
70
|
# ---------------------------------------------------------------------------
|
|
71
71
|
[project]
|
|
72
72
|
name = "pycode-kg"
|
|
73
|
-
version = "0.
|
|
73
|
+
version = "0.22.0"
|
|
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"
|
|
@@ -101,11 +101,32 @@ dependencies = [
|
|
|
101
101
|
"numpy>=1.24.0",
|
|
102
102
|
"pandas>=2.0.0",
|
|
103
103
|
"rich>=14.3.3,<15",
|
|
104
|
-
"safetensors>=0.5.0",
|
|
105
104
|
"sentence-transformers>=5.4.1",
|
|
105
|
+
# Core, not optional: kg.py hard-codes vector_backend="sqlite-vec", so every
|
|
106
|
+
# build/query path goes through kg_utils.vector_backend.SqliteVecBackend,
|
|
107
|
+
# which imports sqlite_vec at connect time. Exact pin — sqlite-vec is pre-1.0
|
|
108
|
+
# and breaking minors ship under 0.x; kgmodule-utils pins the same ==0.1.9.
|
|
109
|
+
# Declared here rather than pulled via kgmodule-utils[sqlite-vec] so the
|
|
110
|
+
# requirement is visible in this project's own metadata (doc-kg 0.20.0 does
|
|
111
|
+
# the same).
|
|
112
|
+
"sqlite-vec==0.1.9",
|
|
113
|
+
# Kept direct even though `[semantic]` carries the same `>=2.5.1`: the
|
|
114
|
+
# `[tool.poetry.dependencies]` block above routes linux torch to the CPU-only
|
|
115
|
+
# index, and that enrichment only applies to a dependency this project
|
|
116
|
+
# declares itself. Dropping it here would pull the ~3.4G CUDA wheel back in.
|
|
106
117
|
"torch>=2.5.1",
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
# `[semantic]` is safe again as of kgmodule-utils 0.10.0, which moved
|
|
119
|
+
# `lancedb` out into its own `[lancedb]` extra. It now carries numpy, rich,
|
|
120
|
+
# sentence-transformers, sqlite-vec, torch and transformers under the same
|
|
121
|
+
# constraints used here, so `transformers` needs no direct entry.
|
|
122
|
+
# `[viz]` stays off: kg_utils.viz imports pyvis lazily, so it would force
|
|
123
|
+
# pyvis into every bare install when it belongs in this project's own `viz`
|
|
124
|
+
# extra.
|
|
125
|
+
# `[viz3d]` is the shared 3-D layout engine behind `pycode_kg.layout3d`.
|
|
126
|
+
# Its only dependency is numpy, which `[semantic]` already pulls, so it adds
|
|
127
|
+
# nothing to the install — it just declares what layout3d actually imports.
|
|
128
|
+
# Unlike `[viz]`, there is no pyvis to leak into a bare install.
|
|
129
|
+
"kgmodule-utils[semantic,viz3d]>=0.11.0",
|
|
109
130
|
]
|
|
110
131
|
|
|
111
132
|
[project.optional-dependencies]
|
|
@@ -127,7 +148,7 @@ viz3d = [
|
|
|
127
148
|
"markdown>=3.6",
|
|
128
149
|
"param>=2.0.0",
|
|
129
150
|
"PyQt5>=5.15.0",
|
|
130
|
-
"pyvista
|
|
151
|
+
"pyvista>=0.44.0",
|
|
131
152
|
"pyvistaqt>=0.11.0",
|
|
132
153
|
"trame-vtk>=2.0.0",
|
|
133
154
|
]
|
|
@@ -149,7 +170,7 @@ all = [
|
|
|
149
170
|
"PyQt5>=5.15.0",
|
|
150
171
|
"pytest>=8.0.0",
|
|
151
172
|
"pytest-cov>=5.0.0",
|
|
152
|
-
"pyvista
|
|
173
|
+
"pyvista>=0.44.0",
|
|
153
174
|
"pyvistaqt>=0.11.0",
|
|
154
175
|
"pyvis>=0.3.2",
|
|
155
176
|
"ruff>=0.4.0",
|
|
@@ -158,6 +179,27 @@ all = [
|
|
|
158
179
|
"ty>=0.0.41",
|
|
159
180
|
]
|
|
160
181
|
|
|
182
|
+
# Maintainer tooling, not a dependency of this package. `.mcp.json` serves a
|
|
183
|
+
# `dockg` MCP server from `.venv/bin/dockg`, so the CLI has to be in this repo's
|
|
184
|
+
# environment — but nothing under src/ imports doc_kg, and declaring it as a real
|
|
185
|
+
# dependency would put it in the published wheel for every consumer.
|
|
186
|
+
#
|
|
187
|
+
# A Poetry group is the right shape: it is locked and installable but never
|
|
188
|
+
# written into wheel metadata, so `pip install pycode-kg` is unaffected.
|
|
189
|
+
#
|
|
190
|
+
# poetry install --with kg # gets the dockg CLI into .venv/bin
|
|
191
|
+
# poetry install # default — the group is optional, skipped
|
|
192
|
+
#
|
|
193
|
+
# `kg` is the fleet-wide name for this group so there is one command to remember;
|
|
194
|
+
# contents are per-repo and minimal, covering only what that repo actually
|
|
195
|
+
# invokes. Safe to lock because doc-kg keeps its own pycode-kg in a group too, so
|
|
196
|
+
# the published metadata on either side has no cycle for the resolver to chase.
|
|
197
|
+
[tool.poetry.group.kg]
|
|
198
|
+
optional = true
|
|
199
|
+
|
|
200
|
+
[tool.poetry.group.kg.dependencies]
|
|
201
|
+
doc-kg = ">=0.21.1"
|
|
202
|
+
|
|
161
203
|
[project.urls]
|
|
162
204
|
Homepage = "https://github.com/Flux-Frontiers/pycode_kg"
|
|
163
205
|
Repository = "https://github.com/Flux-Frontiers/pycode_kg"
|
|
@@ -190,7 +232,13 @@ target-version = "py312"
|
|
|
190
232
|
# Vendored agent scratch dirs (Claude Code skills, Codex) — not project code.
|
|
191
233
|
# The pre-commit hook's exclude can't cover these (it runs with
|
|
192
234
|
# pass_filenames: false), so the exclusion must live here.
|
|
193
|
-
extend-exclude = [".claude", ".agents", ".codex"]
|
|
235
|
+
extend-exclude = [".claude", ".agents", ".codex", "*.md"]
|
|
236
|
+
# ruff 0.16 formats Python code blocks inside Markdown as stable behaviour
|
|
237
|
+
# (0.15 gates it behind preview), so `ruff format --check .` starts rewriting
|
|
238
|
+
# prose docs the moment the lock resolves 0.16 — a failure that looks unrelated
|
|
239
|
+
# to whatever change triggered the relock. Doc snippets are illustrative and
|
|
240
|
+
# often deliberately aligned, so keep them out of the formatter's reach.
|
|
241
|
+
# doc_kg and ftree_kg carry the same exclusion.
|
|
194
242
|
|
|
195
243
|
[tool.ruff.lint]
|
|
196
244
|
# B023, BLE001, PLC0415 replace the retired pylint hook's cell-var-from-loop,
|
|
@@ -32,7 +32,7 @@ KGModule SDK (build new domain KGs)::
|
|
|
32
32
|
from pycode_kg import KGModule, KGExtractor, PyCodeKGExtractor, NodeSpec, EdgeSpec
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
__version__ = "0.
|
|
35
|
+
__version__ = "0.22.0"
|
|
36
36
|
__author__ = "Eric G. Suchanek, PhD"
|
|
37
37
|
|
|
38
38
|
# Low-level primitives (locked v0 contract)
|
|
@@ -4,7 +4,7 @@ Measures module interaction complexity: how many unique modules each module call
|
|
|
4
4
|
For well-modularized codebases, identifies orchestrator and hub modules.
|
|
5
5
|
|
|
6
6
|
Author: Eric G. Suchanek, PhD
|
|
7
|
-
Last Revision: 2026-
|
|
7
|
+
Last Revision: 2026-08-03 11:37:06
|
|
8
8
|
License: Elastic 2.0
|
|
9
9
|
"""
|
|
10
10
|
|
|
@@ -6,7 +6,7 @@ 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-
|
|
9
|
+
Last Revision: 2026-08-03 11:37:06
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
PyCodeKG's 3-D layout bindings over the shared layout engine.
|
|
4
|
+
|
|
5
|
+
The engine itself lives in :mod:`kg_utils.viz3d`, shared with every other KG
|
|
6
|
+
module — GutenbergKG was already importing it from here, paying a full
|
|
7
|
+
``pycode-kg`` dependency for five symbols that have nothing to do with parsing
|
|
8
|
+
Python. What stays here is the part that is genuinely specific to a *code*
|
|
9
|
+
graph: which node kinds occupy which Z level, which comes from
|
|
10
|
+
:mod:`pycode_kg.theme`.
|
|
11
|
+
|
|
12
|
+
Everything is re-exported so existing callers do not need to know where the
|
|
13
|
+
implementation moved.
|
|
14
|
+
|
|
15
|
+
The Fibonacci utilities (``fibonacci_sphere``, ``fibonacci_annulus``) are
|
|
16
|
+
adapted from *repo_vis* ``pkg_visualizer/utility.py``
|
|
17
|
+
(Eric G. Suchanek, PhD — https://github.com/Suchanek/repo_vis).
|
|
18
|
+
|
|
19
|
+
Author: Eric G. Suchanek, PhD
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from kg_utils.viz3d import (
|
|
25
|
+
AlliumLayout,
|
|
26
|
+
Layout3D,
|
|
27
|
+
LayoutEdge,
|
|
28
|
+
LayoutNode,
|
|
29
|
+
fibonacci_annulus,
|
|
30
|
+
fibonacci_sphere,
|
|
31
|
+
golden_spiral_2d,
|
|
32
|
+
)
|
|
33
|
+
from kg_utils.viz3d import FunnelLayout as _SharedFunnelLayout
|
|
34
|
+
|
|
35
|
+
from pycode_kg import theme
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AlliumLayout",
|
|
39
|
+
"FunnelLayout",
|
|
40
|
+
"Layout3D",
|
|
41
|
+
"LayoutEdge",
|
|
42
|
+
"LayoutNode",
|
|
43
|
+
"fibonacci_annulus",
|
|
44
|
+
"fibonacci_sphere",
|
|
45
|
+
"golden_spiral_2d",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FunnelLayout(_SharedFunnelLayout):
|
|
50
|
+
"""Funnel layout stratified by *code* node kind.
|
|
51
|
+
|
|
52
|
+
The shared engine takes the kind→Z-level mapping as an argument, because
|
|
53
|
+
only the domain knows its own hierarchy — modules at the bottom, then
|
|
54
|
+
classes, then functions and methods, with symbol stubs on top is a fact
|
|
55
|
+
about Python code, not about knowledge graphs. This subclass supplies
|
|
56
|
+
that mapping from :mod:`pycode_kg.theme`, so the layout engine, the 3-D
|
|
57
|
+
viewer and the 2-D explorer keep sharing one definition.
|
|
58
|
+
|
|
59
|
+
Unknown kinds land on the symbol level, matching
|
|
60
|
+
:func:`pycode_kg.theme.resolve_kind`, which collapses anything the palette
|
|
61
|
+
does not recognise to ``symbol``.
|
|
62
|
+
|
|
63
|
+
:param layer_gap: Vertical distance between adjacent layers.
|
|
64
|
+
:param node_spacing: Spacing multiplier — larger spreads layers out more.
|
|
65
|
+
:param kwargs: Forwarded to :class:`kg_utils.viz3d.FunnelLayout`; the
|
|
66
|
+
theme-derived defaults are only applied to keys left unset.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
layer_gap: float = 12.0,
|
|
72
|
+
node_spacing: float = 2.0,
|
|
73
|
+
**kwargs,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Initialise layout parameters, defaulting the maps from the theme.
|
|
76
|
+
|
|
77
|
+
:param layer_gap: Vertical separation between layers.
|
|
78
|
+
:param node_spacing: Controls minimum gap between node surfaces.
|
|
79
|
+
:param kwargs: Overrides forwarded to the shared layout.
|
|
80
|
+
"""
|
|
81
|
+
kwargs.setdefault("zlevels", theme.KIND_ZLEVEL)
|
|
82
|
+
kwargs.setdefault("level_sizes", theme.LEVEL_NODE_SIZE)
|
|
83
|
+
kwargs.setdefault("default_level", theme.KIND_ZLEVEL[theme.UNKNOWN_KIND])
|
|
84
|
+
super().__init__(layer_gap=layer_gap, node_spacing=node_spacing, **kwargs)
|
|
@@ -23,7 +23,7 @@ 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-
|
|
26
|
+
Last Revision: 2026-08-03 11:37:06
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
from __future__ import annotations
|
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
layout3d.py — Pluggable 3-D layout engine for the PyCodeKG knowledge graph.
|
|
4
|
-
|
|
5
|
-
Provides an abstract :class:`Layout3D` base class and two concrete
|
|
6
|
-
implementations:
|
|
7
|
-
|
|
8
|
-
- :class:`AlliumLayout`: Each module is rendered as a Giant Allium plant
|
|
9
|
-
(a vertical stem with a Fibonacci-sphere "head" of classes and functions).
|
|
10
|
-
Modules are arranged in a Fibonacci annulus in the XY plane.
|
|
11
|
-
|
|
12
|
-
- :class:`FunnelLayout`: Node kind determines the Z level (modules at
|
|
13
|
-
the bottom, classes above, functions/methods at the top). XY positions
|
|
14
|
-
are spread via a golden-angle spiral within each layer.
|
|
15
|
-
|
|
16
|
-
The Fibonacci utilities (``fibonacci_sphere``, ``fibonacci_annulus``) are
|
|
17
|
-
adapted from *repo_vis* ``pkg_visualizer/utility.py``
|
|
18
|
-
(Eric G. Suchanek, PhD — https://github.com/Suchanek/repo_vis).
|
|
19
|
-
|
|
20
|
-
Author: Eric G. Suchanek, PhD
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from __future__ import annotations
|
|
24
|
-
|
|
25
|
-
from abc import ABC, abstractmethod
|
|
26
|
-
from dataclasses import dataclass
|
|
27
|
-
|
|
28
|
-
import numpy as np
|
|
29
|
-
|
|
30
|
-
from pycode_kg import theme
|
|
31
|
-
|
|
32
|
-
# ---------------------------------------------------------------------------
|
|
33
|
-
# Fibonacci spatial utilities (adapted from repo_vis/pkg_visualizer/utility.py)
|
|
34
|
-
# ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def fibonacci_sphere(
|
|
38
|
-
samples: int,
|
|
39
|
-
radius: float = 1.0,
|
|
40
|
-
center: np.ndarray | None = None,
|
|
41
|
-
) -> list[np.ndarray]:
|
|
42
|
-
"""
|
|
43
|
-
Distribute *samples* points uniformly on a sphere using the Fibonacci spiral.
|
|
44
|
-
|
|
45
|
-
Adapted from ``utility.fibonacci_sphere`` in *repo_vis*.
|
|
46
|
-
|
|
47
|
-
:param samples: Number of points to generate.
|
|
48
|
-
:param radius: Sphere radius.
|
|
49
|
-
:param center: Centre of the sphere (default: origin).
|
|
50
|
-
:return: List of 3-D coordinate arrays.
|
|
51
|
-
"""
|
|
52
|
-
if center is None:
|
|
53
|
-
center = np.zeros(3)
|
|
54
|
-
if samples <= 0:
|
|
55
|
-
return []
|
|
56
|
-
if samples == 1:
|
|
57
|
-
return [center + radius * np.array([0.0, 0.0, 1.0])]
|
|
58
|
-
|
|
59
|
-
phi = np.pi * (3.0 - np.sqrt(5.0)) # golden angle in radians
|
|
60
|
-
points: list[np.ndarray] = []
|
|
61
|
-
for i in range(samples):
|
|
62
|
-
y = 1.0 - (i / float(samples - 1)) * 2.0
|
|
63
|
-
r_at_y = np.sqrt(max(0.0, 1.0 - y * y))
|
|
64
|
-
theta = phi * i
|
|
65
|
-
x = np.cos(theta) * r_at_y
|
|
66
|
-
z = np.sin(theta) * r_at_y
|
|
67
|
-
points.append(center + radius * np.array([x, y, z]))
|
|
68
|
-
return points
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def fibonacci_annulus(
|
|
72
|
-
samples: int,
|
|
73
|
-
inner_radius: float = 1.0,
|
|
74
|
-
outer_radius: float = 2.0,
|
|
75
|
-
center: np.ndarray | None = None,
|
|
76
|
-
z_thickness: float = 0.2,
|
|
77
|
-
) -> list[np.ndarray]:
|
|
78
|
-
"""
|
|
79
|
-
Distribute *samples* points in a flat annular ring in the XY plane.
|
|
80
|
-
|
|
81
|
-
A small Z jitter (``z_thickness``) adds visual depth when non-zero.
|
|
82
|
-
Adapted from ``utility.fibonacci_annulus`` in *repo_vis*.
|
|
83
|
-
|
|
84
|
-
:param samples: Number of points to generate.
|
|
85
|
-
:param inner_radius: Inner radius of the annulus.
|
|
86
|
-
:param outer_radius: Outer radius of the annulus.
|
|
87
|
-
:param center: Centre of the annulus (default: origin).
|
|
88
|
-
:param z_thickness: Half-range of Z jitter applied to each point.
|
|
89
|
-
:return: List of 3-D coordinate arrays.
|
|
90
|
-
"""
|
|
91
|
-
if center is None:
|
|
92
|
-
center = np.zeros(3)
|
|
93
|
-
if samples <= 0:
|
|
94
|
-
return []
|
|
95
|
-
if samples == 1:
|
|
96
|
-
mid = (inner_radius + outer_radius) / 2.0
|
|
97
|
-
return [center + np.array([mid, 0.0, 0.0])]
|
|
98
|
-
|
|
99
|
-
phi = np.pi * (3.0 - np.sqrt(5.0))
|
|
100
|
-
r_range = outer_radius - inner_radius
|
|
101
|
-
r_step = r_range / max(samples - 1, 1)
|
|
102
|
-
rng = np.random.default_rng(42) # deterministic jitter seed
|
|
103
|
-
|
|
104
|
-
points: list[np.ndarray] = []
|
|
105
|
-
for i in range(samples):
|
|
106
|
-
r = inner_radius + i * r_step
|
|
107
|
-
theta = phi * i
|
|
108
|
-
x = np.cos(theta) * r
|
|
109
|
-
y = np.sin(theta) * r
|
|
110
|
-
z = (rng.random() * 2.0 - 1.0) * z_thickness
|
|
111
|
-
points.append(center + np.array([x, y, z]))
|
|
112
|
-
return points
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _golden_spiral_2d(
|
|
116
|
-
samples: int,
|
|
117
|
-
radius: float = 1.0,
|
|
118
|
-
center: np.ndarray | None = None,
|
|
119
|
-
z: float = 0.0,
|
|
120
|
-
) -> list[np.ndarray]:
|
|
121
|
-
"""
|
|
122
|
-
Place *samples* points in the XY plane using a golden-angle disc spiral.
|
|
123
|
-
|
|
124
|
-
:param samples: Number of points.
|
|
125
|
-
:param radius: Outer radius of the disc.
|
|
126
|
-
:param center: XY centre (Z component ignored; overridden by *z*).
|
|
127
|
-
:param z: Fixed Z coordinate for all output points.
|
|
128
|
-
:return: List of 3-D coordinate arrays.
|
|
129
|
-
"""
|
|
130
|
-
if center is None:
|
|
131
|
-
center = np.zeros(3)
|
|
132
|
-
if samples <= 0:
|
|
133
|
-
return []
|
|
134
|
-
|
|
135
|
-
phi = np.pi * (3.0 - np.sqrt(5.0))
|
|
136
|
-
points: list[np.ndarray] = []
|
|
137
|
-
for i in range(samples):
|
|
138
|
-
r = radius * np.sqrt(i / max(samples - 1, 1))
|
|
139
|
-
theta = phi * i
|
|
140
|
-
x = r * np.cos(theta)
|
|
141
|
-
y = r * np.sin(theta)
|
|
142
|
-
points.append(center + np.array([x, y, z]))
|
|
143
|
-
return points
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# ---------------------------------------------------------------------------
|
|
147
|
-
# Data transfer objects
|
|
148
|
-
# ---------------------------------------------------------------------------
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@dataclass
|
|
152
|
-
class LayoutNode:
|
|
153
|
-
"""
|
|
154
|
-
Thin wrapper around a node dict from :class:`~pycode_kg.store.GraphStore`.
|
|
155
|
-
|
|
156
|
-
:param id: Stable node identifier (e.g. ``mod:src/foo.py``).
|
|
157
|
-
:param kind: Node kind — ``module``, ``class``, ``function``, ``method``,
|
|
158
|
-
or ``symbol``.
|
|
159
|
-
:param name: Short name of the node.
|
|
160
|
-
:param module_path: Source module path (may be ``None`` for symbol stubs).
|
|
161
|
-
:param docstring: Raw docstring text (may be ``None``).
|
|
162
|
-
:param lineno: First source line number (may be ``None``).
|
|
163
|
-
:param end_lineno: Last source line number (may be ``None``).
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
id: str
|
|
167
|
-
kind: str
|
|
168
|
-
name: str
|
|
169
|
-
module_path: str | None = None
|
|
170
|
-
docstring: str | None = None
|
|
171
|
-
lineno: int | None = None
|
|
172
|
-
end_lineno: int | None = None
|
|
173
|
-
|
|
174
|
-
@classmethod
|
|
175
|
-
def from_dict(cls, d: dict) -> LayoutNode:
|
|
176
|
-
"""Construct from a GraphStore node dict.
|
|
177
|
-
|
|
178
|
-
:param d: Dict with keys ``id``, ``kind``, ``name``, etc.
|
|
179
|
-
:return: New :class:`LayoutNode`.
|
|
180
|
-
"""
|
|
181
|
-
return cls(
|
|
182
|
-
id=d["id"],
|
|
183
|
-
kind=d["kind"],
|
|
184
|
-
name=d["name"],
|
|
185
|
-
module_path=d.get("module_path"),
|
|
186
|
-
docstring=d.get("docstring"),
|
|
187
|
-
lineno=d.get("lineno"),
|
|
188
|
-
end_lineno=d.get("end_lineno"),
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
@property
|
|
192
|
-
def line_count(self) -> int:
|
|
193
|
-
"""Approximate source size in lines (0 if unknown).
|
|
194
|
-
|
|
195
|
-
:return: ``end_lineno - lineno`` or 0.
|
|
196
|
-
"""
|
|
197
|
-
if self.lineno and self.end_lineno:
|
|
198
|
-
return max(0, self.end_lineno - self.lineno)
|
|
199
|
-
return 0
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
@dataclass
|
|
203
|
-
class LayoutEdge:
|
|
204
|
-
"""
|
|
205
|
-
Thin wrapper around an edge dict from :class:`~pycode_kg.store.GraphStore`.
|
|
206
|
-
|
|
207
|
-
:param src: Source node ID.
|
|
208
|
-
:param rel: Relation type — ``CONTAINS``, ``CALLS``, ``IMPORTS``,
|
|
209
|
-
``INHERITS``.
|
|
210
|
-
:param dst: Destination node ID.
|
|
211
|
-
"""
|
|
212
|
-
|
|
213
|
-
src: str
|
|
214
|
-
rel: str
|
|
215
|
-
dst: str
|
|
216
|
-
|
|
217
|
-
@classmethod
|
|
218
|
-
def from_dict(cls, d: dict) -> LayoutEdge:
|
|
219
|
-
"""Construct from a GraphStore edge dict.
|
|
220
|
-
|
|
221
|
-
:param d: Dict with keys ``src``, ``rel``, ``dst``.
|
|
222
|
-
:return: New :class:`LayoutEdge`.
|
|
223
|
-
"""
|
|
224
|
-
return cls(src=d["src"], rel=d["rel"], dst=d["dst"])
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
# ---------------------------------------------------------------------------
|
|
228
|
-
# Abstract base
|
|
229
|
-
# ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class Layout3D(ABC):
|
|
233
|
-
"""
|
|
234
|
-
Abstract base class for 3-D graph layout strategies.
|
|
235
|
-
|
|
236
|
-
Subclasses implement :meth:`compute` to assign a 3-D position to every
|
|
237
|
-
node, returning a ``{node_id: np.ndarray([x, y, z])}`` mapping that the
|
|
238
|
-
:class:`~pycode_kg.viz3d.KGViz3D` renderer consumes.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
@abstractmethod
|
|
242
|
-
def compute(
|
|
243
|
-
self,
|
|
244
|
-
nodes: list[LayoutNode],
|
|
245
|
-
edges: list[LayoutEdge],
|
|
246
|
-
) -> dict[str, np.ndarray]:
|
|
247
|
-
"""
|
|
248
|
-
Compute 3-D positions for all *nodes*.
|
|
249
|
-
|
|
250
|
-
:param nodes: All nodes in the graph.
|
|
251
|
-
:param edges: All edges in the graph (used to derive hierarchy).
|
|
252
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
253
|
-
"""
|
|
254
|
-
...
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
# ---------------------------------------------------------------------------
|
|
258
|
-
# AlliumLayout
|
|
259
|
-
# ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
class AlliumLayout(Layout3D):
|
|
263
|
-
"""
|
|
264
|
-
Allium-plant layout: each module is visualised as a Giant Allium flower.
|
|
265
|
-
|
|
266
|
-
Spatial structure:
|
|
267
|
-
|
|
268
|
-
- **Stem base** — module node sits at XY position in a Fibonacci annulus
|
|
269
|
-
at ``Z = 0``.
|
|
270
|
-
- **Head** — classes and top-level functions are distributed on a
|
|
271
|
-
Fibonacci sphere centred at the stem apex (``Z = stem_height``).
|
|
272
|
-
Head radius scales with ``sqrt(n_children)``.
|
|
273
|
-
- **Florets** — methods orbit their parent class on a smaller Fibonacci
|
|
274
|
-
sphere, slightly above the head.
|
|
275
|
-
- **Orphans** — nodes with no CONTAINS parent cluster on a small sphere
|
|
276
|
-
at the origin.
|
|
277
|
-
|
|
278
|
-
Multiple module-alliums are arranged in a Fibonacci annulus in the XY
|
|
279
|
-
plane so they are evenly spaced regardless of count.
|
|
280
|
-
|
|
281
|
-
Inspired by :class:`GiantAllium` in *repo_vis/pkg_visualizer/plants3d.py*.
|
|
282
|
-
|
|
283
|
-
:param stem_height: Height of each allium stem (Z offset of the head).
|
|
284
|
-
:param base_head_radius: Minimum radius for the Fibonacci sphere head.
|
|
285
|
-
:param method_orbit_radius: Base radius for method sub-spheres.
|
|
286
|
-
:param annulus_inner_radius: Inner radius of the module placement ring.
|
|
287
|
-
:param annulus_outer_radius: Minimum outer radius (auto-scaled for large graphs).
|
|
288
|
-
"""
|
|
289
|
-
|
|
290
|
-
def __init__(
|
|
291
|
-
self,
|
|
292
|
-
stem_height: float = 8.0,
|
|
293
|
-
base_head_radius: float = 2.0,
|
|
294
|
-
method_orbit_radius: float = 0.8,
|
|
295
|
-
annulus_inner_radius: float = 8.0,
|
|
296
|
-
annulus_outer_radius: float = 20.0,
|
|
297
|
-
) -> None:
|
|
298
|
-
"""Initialise layout parameters.
|
|
299
|
-
|
|
300
|
-
:param stem_height: Vertical height of each allium stem.
|
|
301
|
-
:param base_head_radius: Minimum allium head sphere radius.
|
|
302
|
-
:param method_orbit_radius: Base orbit radius for methods.
|
|
303
|
-
:param annulus_inner_radius: Inner radius for module ring placement.
|
|
304
|
-
:param annulus_outer_radius: Minimum outer radius for module ring.
|
|
305
|
-
"""
|
|
306
|
-
self.stem_height = stem_height
|
|
307
|
-
self.base_head_radius = base_head_radius
|
|
308
|
-
self.method_orbit_radius = method_orbit_radius
|
|
309
|
-
self.annulus_inner_radius = annulus_inner_radius
|
|
310
|
-
self.annulus_outer_radius = annulus_outer_radius
|
|
311
|
-
|
|
312
|
-
def compute(
|
|
313
|
-
self,
|
|
314
|
-
nodes: list[LayoutNode],
|
|
315
|
-
edges: list[LayoutEdge],
|
|
316
|
-
) -> dict[str, np.ndarray]:
|
|
317
|
-
"""
|
|
318
|
-
Compute allium-plant 3-D positions for all nodes.
|
|
319
|
-
|
|
320
|
-
:param nodes: All nodes in the graph.
|
|
321
|
-
:param edges: All edges (``CONTAINS`` used to derive hierarchy).
|
|
322
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
323
|
-
"""
|
|
324
|
-
# Build CONTAINS hierarchy: child_id -> parent_id, parent_id -> [child_ids]
|
|
325
|
-
parent: dict[str, str] = {}
|
|
326
|
-
children: dict[str, list[str]] = {}
|
|
327
|
-
for e in edges:
|
|
328
|
-
if e.rel == "CONTAINS":
|
|
329
|
-
parent[e.dst] = e.src
|
|
330
|
-
children.setdefault(e.src, []).append(e.dst)
|
|
331
|
-
|
|
332
|
-
node_by_id: dict[str, LayoutNode] = {n.id: n for n in nodes}
|
|
333
|
-
positions: dict[str, np.ndarray] = {}
|
|
334
|
-
|
|
335
|
-
# Module nodes form the allium stems
|
|
336
|
-
modules = [n for n in nodes if n.kind == "module"]
|
|
337
|
-
if not modules:
|
|
338
|
-
# Fallback: treat nodes without a CONTAINS parent as pseudo-modules
|
|
339
|
-
modules = [n for n in nodes if n.id not in parent]
|
|
340
|
-
|
|
341
|
-
n_mods = len(modules)
|
|
342
|
-
inner = self.annulus_inner_radius
|
|
343
|
-
# Scale outer radius so stems don't crowd each other
|
|
344
|
-
outer = max(self.annulus_outer_radius, inner + np.sqrt(n_mods) * 4.0)
|
|
345
|
-
|
|
346
|
-
mod_positions = fibonacci_annulus(
|
|
347
|
-
n_mods,
|
|
348
|
-
inner_radius=inner,
|
|
349
|
-
outer_radius=outer,
|
|
350
|
-
center=np.zeros(3),
|
|
351
|
-
z_thickness=0.0, # flat ring — alliums stand vertically
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
for mod_node, mod_pos in zip(modules, mod_positions):
|
|
355
|
-
positions[mod_node.id] = np.array(mod_pos)
|
|
356
|
-
stem_apex = np.array([mod_pos[0], mod_pos[1], self.stem_height])
|
|
357
|
-
|
|
358
|
-
# Direct children (classes, top-level functions)
|
|
359
|
-
direct_ids = children.get(mod_node.id, [])
|
|
360
|
-
direct = [node_by_id[cid] for cid in direct_ids if cid in node_by_id]
|
|
361
|
-
n_direct = len(direct)
|
|
362
|
-
if not direct:
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
# Head radius scales with child count
|
|
366
|
-
head_r = self.base_head_radius + np.sqrt(n_direct) * 0.4
|
|
367
|
-
head_positions = fibonacci_sphere(n_direct, radius=head_r, center=stem_apex)
|
|
368
|
-
|
|
369
|
-
for child, child_pos in zip(direct, head_positions):
|
|
370
|
-
positions[child.id] = np.array(child_pos)
|
|
371
|
-
|
|
372
|
-
# Grandchildren (methods) orbit their parent class
|
|
373
|
-
grand_ids = children.get(child.id, [])
|
|
374
|
-
grand = [node_by_id[gid] for gid in grand_ids if gid in node_by_id]
|
|
375
|
-
n_grand = len(grand)
|
|
376
|
-
if not grand:
|
|
377
|
-
continue
|
|
378
|
-
|
|
379
|
-
method_r = self.method_orbit_radius + np.sqrt(n_grand) * 0.15
|
|
380
|
-
method_positions = fibonacci_sphere(
|
|
381
|
-
n_grand, radius=method_r, center=np.array(child_pos)
|
|
382
|
-
)
|
|
383
|
-
for gc, gc_pos in zip(grand, method_positions):
|
|
384
|
-
positions[gc.id] = np.array(gc_pos)
|
|
385
|
-
|
|
386
|
-
# Orphan nodes: anything not yet placed (symbols, unrooted nodes)
|
|
387
|
-
orphans = [n for n in nodes if n.id not in positions]
|
|
388
|
-
if orphans:
|
|
389
|
-
orphan_r = 3.0
|
|
390
|
-
orphan_positions = fibonacci_sphere(
|
|
391
|
-
len(orphans), radius=orphan_r, center=np.array([0.0, 0.0, orphan_r])
|
|
392
|
-
)
|
|
393
|
-
for n, pos in zip(orphans, orphan_positions):
|
|
394
|
-
positions[n.id] = np.array(pos)
|
|
395
|
-
|
|
396
|
-
return positions
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
# ---------------------------------------------------------------------------
|
|
400
|
-
# FunnelLayout
|
|
401
|
-
# ---------------------------------------------------------------------------
|
|
402
|
-
|
|
403
|
-
# Z levels and per-level radii come from pycode_kg.theme so the layout engine,
|
|
404
|
-
# the 3-D viewer and the 2-D explorer share one definition.
|
|
405
|
-
_KIND_ZLEVEL: dict[str, int] = dict(theme.KIND_ZLEVEL)
|
|
406
|
-
_LEVEL_NODE_SIZE: dict[int, float] = dict(theme.LEVEL_NODE_SIZE)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
class FunnelLayout(Layout3D):
|
|
410
|
-
"""
|
|
411
|
-
Stratified layout: node *kind* determines the Z layer; XY positions use a
|
|
412
|
-
golden-angle disc spiral within each layer.
|
|
413
|
-
|
|
414
|
-
Layers (bottom to top):
|
|
415
|
-
|
|
416
|
-
- **Z = 0** — modules
|
|
417
|
-
- **Z = layer_gap** — classes
|
|
418
|
-
- **Z = 2 × layer_gap** — functions and methods
|
|
419
|
-
- **Z = 3 × layer_gap** — symbol stubs
|
|
420
|
-
|
|
421
|
-
Cross-cutting edges (``CALLS``, ``IMPORTS``, ``INHERITS``) arc between
|
|
422
|
-
layers, making structural coupling immediately visible from any angle.
|
|
423
|
-
|
|
424
|
-
Disc radius is derived algorithmically:
|
|
425
|
-
``r = node_spacing * node_size * sqrt(n)``
|
|
426
|
-
so the layout scales correctly for repos of any size without hand-tuning.
|
|
427
|
-
|
|
428
|
-
:param layer_gap: Vertical distance between adjacent layers.
|
|
429
|
-
:param node_spacing: Spacing multiplier — larger spreads layers out more.
|
|
430
|
-
"""
|
|
431
|
-
|
|
432
|
-
def __init__(
|
|
433
|
-
self,
|
|
434
|
-
layer_gap: float = 12.0,
|
|
435
|
-
node_spacing: float = 2.0,
|
|
436
|
-
) -> None:
|
|
437
|
-
"""Initialise layout parameters.
|
|
438
|
-
|
|
439
|
-
:param layer_gap: Vertical separation between layers.
|
|
440
|
-
:param node_spacing: Controls minimum gap between node surfaces.
|
|
441
|
-
"""
|
|
442
|
-
self.layer_gap = layer_gap
|
|
443
|
-
self.node_spacing = node_spacing
|
|
444
|
-
|
|
445
|
-
def compute(
|
|
446
|
-
self,
|
|
447
|
-
nodes: list[LayoutNode],
|
|
448
|
-
edges: list[LayoutEdge],
|
|
449
|
-
) -> dict[str, np.ndarray]:
|
|
450
|
-
"""
|
|
451
|
-
Compute funnel 3-D positions for all nodes.
|
|
452
|
-
|
|
453
|
-
:param nodes: All nodes in the graph.
|
|
454
|
-
:param edges: Unused by this layout (present for API compatibility).
|
|
455
|
-
:return: Mapping from node ID to ``[x, y, z]`` position.
|
|
456
|
-
"""
|
|
457
|
-
# Group nodes by Z layer
|
|
458
|
-
layers: dict[int, list[LayoutNode]] = {}
|
|
459
|
-
for n in nodes:
|
|
460
|
-
level = _KIND_ZLEVEL.get(n.kind, 3)
|
|
461
|
-
layers.setdefault(level, []).append(n)
|
|
462
|
-
|
|
463
|
-
positions: dict[str, np.ndarray] = {}
|
|
464
|
-
|
|
465
|
-
for level, layer_nodes in layers.items():
|
|
466
|
-
z = level * self.layer_gap
|
|
467
|
-
node_size = _LEVEL_NODE_SIZE.get(level, 0.7)
|
|
468
|
-
# Derived radius: scales with sqrt(n) and node size so no manual
|
|
469
|
-
# tuning is needed as the repo grows
|
|
470
|
-
r = self.node_spacing * node_size * np.sqrt(len(layer_nodes))
|
|
471
|
-
r = max(r, 4.0)
|
|
472
|
-
pts = _golden_spiral_2d(len(layer_nodes), radius=r, z=z)
|
|
473
|
-
for n, pt in zip(layer_nodes, pts):
|
|
474
|
-
positions[n.id] = pt
|
|
475
|
-
|
|
476
|
-
return positions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|