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.
Files changed (63) hide show
  1. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/PKG-INFO +11 -12
  2. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/README.md +6 -6
  3. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/pyproject.toml +56 -8
  4. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/__init__.py +1 -1
  5. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/bridge.py +1 -1
  6. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/centrality.py +1 -1
  7. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/framework_detector.py +1 -1
  8. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_hooks.py +1 -1
  9. pycode_kg-0.22.0/src/pycode_kg/layout3d.py +84 -0
  10. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg.py +1 -1
  11. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/snapshots.py +1 -1
  12. pycode_kg-0.21.2/src/pycode_kg/layout3d.py +0 -476
  13. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/LICENSE +0 -0
  14. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/.DS_Store +0 -0
  15. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/__main__.py +0 -0
  16. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/__init__.py +0 -0
  17. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/analysis/hybrid_rank.py +0 -0
  18. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/app.py +0 -0
  19. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/architecture.py +0 -0
  20. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/build_pycodekg_sqlite.py +0 -0
  21. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/__init__.py +0 -0
  22. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_analyze.py +0 -0
  23. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_architecture.py +0 -0
  24. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_bridges.py +0 -0
  25. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build.py +0 -0
  26. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_build_full.py +0 -0
  27. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_centrality.py +0 -0
  28. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_explain.py +0 -0
  29. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_framework_nodes.py +0 -0
  30. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_init.py +0 -0
  31. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_mcp.py +0 -0
  32. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_model.py +0 -0
  33. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_query.py +0 -0
  34. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_snapshot.py +0 -0
  35. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/cmd_viz.py +0 -0
  36. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/main.py +0 -0
  37. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/cli/options.py +0 -0
  38. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/config.py +0 -0
  39. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/explain.py +0 -0
  40. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/graph.py +0 -0
  41. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/graph_html.py +0 -0
  42. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/index.py +0 -0
  43. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/kg.py +0 -0
  44. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/mcp_server.py +0 -0
  45. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/__init__.py +0 -0
  46. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/base.py +0 -0
  47. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/extractor.py +0 -0
  48. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/module/types.py +0 -0
  49. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_query.py +0 -0
  50. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_snippet_packer.py +0 -0
  51. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_thorough_analysis.py +0 -0
  52. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz.py +0 -0
  53. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/pycodekg_viz3d.py +0 -0
  54. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/__init__.py +0 -0
  55. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/cli_rank.py +0 -0
  56. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/ranking/coderank.py +0 -0
  57. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/sql/004_add_centrality_table.sql +0 -0
  58. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/store.py +0 -0
  59. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/theme.py +0 -0
  60. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/utils.py +0 -0
  61. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/visitor.py +0 -0
  62. {pycode_kg-0.21.2 → pycode_kg-0.22.0}/src/pycode_kg/viz3d.py +0 -0
  63. {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.21.2
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,sqlite-vec,viz] (>=0.9.0)
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[jupyter] (>=0.44.0) ; extra == "all"
49
- Requires-Dist: pyvista[jupyter] (>=0.44.0) ; extra == "viz3d"
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
  [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue.svg)](https://www.python.org/)
75
74
  [![License: Elastic-2.0](https://img.shields.io/badge/License-Elastic%202.0-blue.svg)](https://www.elastic.co/licensing/elastic-license)
76
- [![Version](https://img.shields.io/badge/version-0.21.2-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
75
+ [![Version](https://img.shields.io/badge/version-0.22.0-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
77
76
  [![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)
78
77
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
79
78
  [![DOI](https://zenodo.org/badge/1202379010.svg)](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.19.0.md](docs/analysis_v0.19.0.md), produced (of course) by `pycodekg analyze` against this very repo.
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
  [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
272
271
 
273
- > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.21.2) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19834777
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.21.2},
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.19834777},
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 July 2026*
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
  [![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.21.2-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
8
+ [![Version](https://img.shields.io/badge/version-0.22.0-blue.svg)](https://github.com/Flux-Frontiers/pycode_kg/releases)
9
9
  [![CI](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml/badge.svg)](https://github.com/Flux-Frontiers/pycode_kg/actions/workflows/ci.yml)
10
10
  [![Poetry](https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json)](https://python-poetry.org/)
11
11
  [![DOI](https://zenodo.org/badge/1202379010.svg)](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.19.0.md](docs/analysis_v0.19.0.md), produced (of course) by `pycodekg analyze` against this very repo.
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
  [![DOI](https://zenodo.org/badge/1202379010.svg)](https://zenodo.org/badge/latestdoi/1202379010)
204
204
 
205
- > Suchanek, E. G. (2026). *PyCodeKG: A Knowledge Graph for Python Codebases* (Version 0.21.2) [Software]. Flux-Frontiers. https://doi.org/10.5281/zenodo.19834777
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.21.2},
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.19834777},
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 July 2026*
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-07-29
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.21.2"
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
- "transformers>=5.5.0,<6",
108
- "kgmodule-utils[semantic,sqlite-vec,viz]>=0.9.0",
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[jupyter]>=0.44.0",
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[jupyter]>=0.44.0",
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.21.2"
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-07-28 22:44:43
7
+ Last Revision: 2026-08-03 11:37:06
8
8
  License: Elastic 2.0
9
9
  """
10
10
 
@@ -12,7 +12,7 @@ Public API:
12
12
  - :func:`aggregate_module_scores` — roll node scores up to module level.
13
13
 
14
14
  Author: Eric G. Suchanek, PhD
15
- Last Revision: 2026-07-28 22:44:43
15
+ Last Revision: 2026-08-03 11:37:06
16
16
  """
17
17
 
18
18
  from __future__ import annotations
@@ -3,7 +3,7 @@ Framework Detector for PyCodeKG.
3
3
  Identifies repo-defining abstractions using centrality and cross-module signals.
4
4
 
5
5
  Author: Eric G. Suchanek, PhD
6
- Last Revision: 2026-07-28 22:44:43
6
+ Last Revision: 2026-08-03 11:37:06
7
7
  License: Elastic 2.0
8
8
  """
9
9
 
@@ -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-07-29 15:21:32
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-07-28 22:44:43
26
+ Last Revision: 2026-08-03 11:37:06
27
27
  """
28
28
 
29
29
  from __future__ import annotations
@@ -35,7 +35,7 @@ Usage
35
35
  >>> prev = mgr.get_previous(tree_hash)
36
36
 
37
37
  Author: Eric G. Suchanek, PhD
38
- Last Revision: 2026-07-28
38
+ Last Revision: 2026-08-03
39
39
 
40
40
  License: Elastic 2.0
41
41
  """
@@ -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