pycode-kg 0.16.0__py3-none-any.whl
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/.DS_Store +0 -0
- pycode_kg/__init__.py +91 -0
- pycode_kg/__main__.py +11 -0
- pycode_kg/analysis/__init__.py +15 -0
- pycode_kg/analysis/bridge.py +108 -0
- pycode_kg/analysis/centrality.py +412 -0
- pycode_kg/analysis/framework_detector.py +103 -0
- pycode_kg/analysis/hybrid_rank.py +53 -0
- pycode_kg/app.py +1335 -0
- pycode_kg/architecture.py +624 -0
- pycode_kg/build_pycodekg_lancedb.py +15 -0
- pycode_kg/build_pycodekg_sqlite.py +15 -0
- pycode_kg/cli/__init__.py +29 -0
- pycode_kg/cli/cmd_analyze.py +96 -0
- pycode_kg/cli/cmd_architecture.py +150 -0
- pycode_kg/cli/cmd_bridges.py +26 -0
- pycode_kg/cli/cmd_build.py +154 -0
- pycode_kg/cli/cmd_build_full.py +242 -0
- pycode_kg/cli/cmd_centrality.py +131 -0
- pycode_kg/cli/cmd_explain.py +180 -0
- pycode_kg/cli/cmd_framework_nodes.py +18 -0
- pycode_kg/cli/cmd_hooks.py +137 -0
- pycode_kg/cli/cmd_init.py +312 -0
- pycode_kg/cli/cmd_mcp.py +71 -0
- pycode_kg/cli/cmd_model.py +53 -0
- pycode_kg/cli/cmd_query.py +211 -0
- pycode_kg/cli/cmd_snapshot.py +421 -0
- pycode_kg/cli/cmd_viz.py +180 -0
- pycode_kg/cli/main.py +23 -0
- pycode_kg/cli/options.py +63 -0
- pycode_kg/config.py +78 -0
- pycode_kg/graph.py +125 -0
- pycode_kg/index.py +542 -0
- pycode_kg/kg.py +220 -0
- pycode_kg/layout3d.py +470 -0
- pycode_kg/mcp/bridge_tools.py +19 -0
- pycode_kg/mcp/framework_tools.py +18 -0
- pycode_kg/mcp_server.py +1965 -0
- pycode_kg/module/__init__.py +83 -0
- pycode_kg/module/base.py +720 -0
- pycode_kg/module/extractor.py +276 -0
- pycode_kg/module/types.py +532 -0
- pycode_kg/pycodekg.py +543 -0
- pycode_kg/pycodekg_query.py +1 -0
- pycode_kg/pycodekg_snippet_packer.py +1 -0
- pycode_kg/pycodekg_thorough_analysis.py +2751 -0
- pycode_kg/pycodekg_viz.py +1 -0
- pycode_kg/pycodekg_viz3d.py +1 -0
- pycode_kg/ranking/__init__.py +1 -0
- pycode_kg/ranking/cli_rank.py +92 -0
- pycode_kg/ranking/coderank.py +555 -0
- pycode_kg/snapshots.py +612 -0
- pycode_kg/sql/004_add_centrality_table.sql +12 -0
- pycode_kg/store.py +766 -0
- pycode_kg/utils.py +39 -0
- pycode_kg/visitor.py +413 -0
- pycode_kg/viz3d.py +1353 -0
- pycode_kg/viz3d_timeline.py +364 -0
- pycode_kg-0.16.0.dist-info/METADATA +305 -0
- pycode_kg-0.16.0.dist-info/RECORD +63 -0
- pycode_kg-0.16.0.dist-info/WHEEL +4 -0
- pycode_kg-0.16.0.dist-info/entry_points.txt +19 -0
- pycode_kg-0.16.0.dist-info/licenses/LICENSE +94 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
module/extractor.py
|
|
4
|
+
|
|
5
|
+
KGExtractor — abstract extraction protocol for any knowledge graph domain.
|
|
6
|
+
|
|
7
|
+
NodeSpec and EdgeSpec are the canonical intermediate types between
|
|
8
|
+
domain-specific source parsing and the generic GraphStore / SemanticIndex
|
|
9
|
+
infrastructure provided by KGModule.
|
|
10
|
+
|
|
11
|
+
Also provides PyCodeKGExtractor — the Python-AST-backed implementation that
|
|
12
|
+
drives the existing PyCodeKG build pipeline.
|
|
13
|
+
|
|
14
|
+
Author: Eric G. Suchanek, PhD
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Intermediate node / edge representations
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class NodeSpec:
|
|
32
|
+
"""Specification for a single graph node emitted by a KGExtractor.
|
|
33
|
+
|
|
34
|
+
This is the canonical intermediate type between domain-specific source
|
|
35
|
+
parsing and the generic GraphStore persistence layer.
|
|
36
|
+
|
|
37
|
+
:param node_id: Stable unique ID (e.g. ``'fn:src/foo.py:bar'``).
|
|
38
|
+
:param kind: Domain node type (``'function'``, ``'gene'``, ``'statute'``, …).
|
|
39
|
+
:param name: Human-readable short name.
|
|
40
|
+
:param qualname: Fully-qualified name within its source (may equal ``name``).
|
|
41
|
+
:param source_path: Repo-relative path to source file or document.
|
|
42
|
+
:param lineno: 1-based start line (source-code KGs); ``None`` for doc/domain KGs.
|
|
43
|
+
:param end_lineno: 1-based end line; ``None`` if not applicable.
|
|
44
|
+
:param docstring: Docstring, description, or excerpt used for embedding.
|
|
45
|
+
:param metadata: Domain-specific extension data (serialized as JSON in the store).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
node_id: str
|
|
49
|
+
kind: str
|
|
50
|
+
name: str
|
|
51
|
+
qualname: str
|
|
52
|
+
source_path: str
|
|
53
|
+
lineno: int | None = None
|
|
54
|
+
end_lineno: int | None = None
|
|
55
|
+
docstring: str = ""
|
|
56
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class EdgeSpec:
|
|
61
|
+
"""Specification for a single directed graph edge emitted by a KGExtractor.
|
|
62
|
+
|
|
63
|
+
:param source_id: ``node_id`` of the source node.
|
|
64
|
+
:param target_id: ``node_id`` of the target node (may be an unresolved stub ID).
|
|
65
|
+
:param relation: Edge type (``'CALLS'``, ``'IMPORTS'``, ``'CONTAINS'``, …).
|
|
66
|
+
:param weight: Edge weight for PageRank (default 1.0).
|
|
67
|
+
:param metadata: Domain-specific edge data (serialized as evidence JSON in the store).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
source_id: str
|
|
71
|
+
target_id: str
|
|
72
|
+
relation: str
|
|
73
|
+
weight: float = 1.0
|
|
74
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# KGExtractor — abstract domain extraction protocol
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class KGExtractor(ABC):
|
|
83
|
+
"""Abstract extraction protocol for a knowledge graph domain.
|
|
84
|
+
|
|
85
|
+
Subclass this to teach :class:`~pycode_kg.module.base.KGModule` how to
|
|
86
|
+
traverse your source and emit :class:`NodeSpec` / :class:`EdgeSpec`
|
|
87
|
+
objects. The base infrastructure (:class:`~pycode_kg.store.GraphStore`,
|
|
88
|
+
:class:`~pycode_kg.index.SemanticIndex`, snapshot management, MCP server,
|
|
89
|
+
CLI) is provided by :class:`~pycode_kg.module.base.KGModule` — you only
|
|
90
|
+
implement the domain-specific parsing.
|
|
91
|
+
|
|
92
|
+
Minimal subclass::
|
|
93
|
+
|
|
94
|
+
class MyExtractor(KGExtractor):
|
|
95
|
+
def node_kinds(self): return ["entity", "relation"]
|
|
96
|
+
def edge_kinds(self): return ["LINKED_TO"]
|
|
97
|
+
def extract(self):
|
|
98
|
+
for item in my_parser(self.repo_path):
|
|
99
|
+
yield NodeSpec(node_id=item.id, kind="entity", ...)
|
|
100
|
+
|
|
101
|
+
:param repo_path: Absolute path to the repository or corpus root.
|
|
102
|
+
:param config: Domain-specific configuration dict.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, repo_path: Path, config: dict[str, Any] | None = None) -> None:
|
|
106
|
+
"""Initialise the extractor.
|
|
107
|
+
|
|
108
|
+
:param repo_path: Path to the repository or corpus root.
|
|
109
|
+
:param config: Optional domain-specific configuration dict.
|
|
110
|
+
"""
|
|
111
|
+
self.repo_path = Path(repo_path).resolve()
|
|
112
|
+
self.config = config or {}
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def node_kinds(self) -> list[str]:
|
|
116
|
+
"""Return the canonical list of node kinds this extractor emits.
|
|
117
|
+
|
|
118
|
+
Example: ``['module', 'class', 'function', 'method']``
|
|
119
|
+
|
|
120
|
+
Used to parameterize the LanceDB ``index_kinds`` and the MCP
|
|
121
|
+
``graph_stats`` tool enum.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def edge_kinds(self) -> list[str]:
|
|
126
|
+
"""Return the canonical list of edge relation types this extractor emits.
|
|
127
|
+
|
|
128
|
+
Example: ``['CALLS', 'IMPORTS', 'CONTAINS', 'INHERITS']``
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def extract(self) -> Iterator[NodeSpec | EdgeSpec]:
|
|
133
|
+
"""Traverse the source and yield nodes and edges.
|
|
134
|
+
|
|
135
|
+
May yield :class:`NodeSpec` and :class:`EdgeSpec` objects in any
|
|
136
|
+
order. The build pipeline separates them into two lists before
|
|
137
|
+
calling :meth:`~pycode_kg.store.GraphStore.write`.
|
|
138
|
+
|
|
139
|
+
Implementations should be deterministic: the same source should
|
|
140
|
+
produce the same stream on every call.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def meaningful_node_kinds(self) -> list[str]:
|
|
144
|
+
"""Return the subset of ``node_kinds()`` that are indexed semantically.
|
|
145
|
+
|
|
146
|
+
Default: all of :meth:`node_kinds`. Override to exclude structural
|
|
147
|
+
stubs (e.g., unresolved import placeholders, synthetic wrapper nodes)
|
|
148
|
+
from the LanceDB index and from coverage metrics.
|
|
149
|
+
|
|
150
|
+
:return: List of node kind strings to index and measure.
|
|
151
|
+
"""
|
|
152
|
+
return self.node_kinds()
|
|
153
|
+
|
|
154
|
+
def coverage_metric(self, nodes: list[NodeSpec]) -> float:
|
|
155
|
+
"""Compute a coverage quality metric for snapshots (0.0–1.0).
|
|
156
|
+
|
|
157
|
+
Default: fraction of meaningful nodes that have a non-empty docstring.
|
|
158
|
+
Override for domain-appropriate metrics (e.g., pathway annotation
|
|
159
|
+
completeness, legal cross-reference density).
|
|
160
|
+
|
|
161
|
+
:param nodes: All extracted :class:`NodeSpec` objects.
|
|
162
|
+
:return: Coverage score in ``[0.0, 1.0]``.
|
|
163
|
+
"""
|
|
164
|
+
meaningful = [n for n in nodes if n.kind in self.meaningful_node_kinds()]
|
|
165
|
+
if not meaningful:
|
|
166
|
+
return 0.0
|
|
167
|
+
covered = sum(1 for n in meaningful if n.docstring.strip())
|
|
168
|
+
return covered / len(meaningful)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# PyCodeKGExtractor — Python-AST-backed extractor
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PyCodeKGExtractor(KGExtractor):
|
|
177
|
+
"""KGExtractor backed by CodeGraph (Python AST analysis).
|
|
178
|
+
|
|
179
|
+
Wraps :class:`~pycode_kg.graph.CodeGraph` and converts its :class:`Node`
|
|
180
|
+
and :class:`Edge` v0 primitives to :class:`NodeSpec` / :class:`EdgeSpec`
|
|
181
|
+
for the generic :class:`~pycode_kg.module.base.KGModule` build pipeline.
|
|
182
|
+
|
|
183
|
+
This is the extractor used by :class:`~pycode_kg.kg.PyCodeKG` and is the
|
|
184
|
+
reference implementation for source-code KG extractors.
|
|
185
|
+
|
|
186
|
+
:param repo_path: Path to the Python repository root.
|
|
187
|
+
:param include: Set of top-level directory names to include (all if empty).
|
|
188
|
+
:param exclude: Set of directory names to exclude at every depth.
|
|
189
|
+
:param config: Optional config dict (forwarded to super).
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
repo_path: Path,
|
|
195
|
+
*,
|
|
196
|
+
include: set[str] | None = None,
|
|
197
|
+
exclude: set[str] | None = None,
|
|
198
|
+
config: dict[str, Any] | None = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Initialise the extractor for a Python repository.
|
|
201
|
+
|
|
202
|
+
:param repo_path: Path to the Python repository root.
|
|
203
|
+
:param include: Top-level directory names to include (empty = all).
|
|
204
|
+
:param exclude: Directory names to exclude at every walk depth.
|
|
205
|
+
:param config: Optional domain-specific configuration dict.
|
|
206
|
+
"""
|
|
207
|
+
super().__init__(repo_path, config)
|
|
208
|
+
self._include: set[str] = include or set()
|
|
209
|
+
self._exclude: set[str] = exclude or set()
|
|
210
|
+
|
|
211
|
+
def node_kinds(self) -> list[str]:
|
|
212
|
+
"""Return the Python AST node kinds.
|
|
213
|
+
|
|
214
|
+
:return: ``['module', 'class', 'function', 'method', 'symbol']``
|
|
215
|
+
"""
|
|
216
|
+
return ["module", "class", "function", "method", "symbol"]
|
|
217
|
+
|
|
218
|
+
def edge_kinds(self) -> list[str]:
|
|
219
|
+
"""Return the Python AST edge relation types.
|
|
220
|
+
|
|
221
|
+
:return: List of all relation strings emitted by the Python extractor.
|
|
222
|
+
"""
|
|
223
|
+
return [
|
|
224
|
+
"CONTAINS",
|
|
225
|
+
"CALLS",
|
|
226
|
+
"IMPORTS",
|
|
227
|
+
"INHERITS",
|
|
228
|
+
"READS",
|
|
229
|
+
"WRITES",
|
|
230
|
+
"ATTR_ACCESS",
|
|
231
|
+
"DEPENDS_ON",
|
|
232
|
+
"RESOLVES_TO",
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
def meaningful_node_kinds(self) -> list[str]:
|
|
236
|
+
"""Return the node kinds indexed by LanceDB and counted in coverage.
|
|
237
|
+
|
|
238
|
+
Excludes ``'symbol'`` (unresolved import stubs).
|
|
239
|
+
|
|
240
|
+
:return: ``['module', 'class', 'function', 'method']``
|
|
241
|
+
"""
|
|
242
|
+
return ["module", "class", "function", "method"]
|
|
243
|
+
|
|
244
|
+
def extract(self) -> Iterator[NodeSpec | EdgeSpec]:
|
|
245
|
+
"""Run Python AST extraction and yield NodeSpec / EdgeSpec objects.
|
|
246
|
+
|
|
247
|
+
Delegates to :class:`~pycode_kg.graph.CodeGraph` for the AST walk, then
|
|
248
|
+
converts the v0-locked :class:`~pycode_kg.pycodekg.Node` and
|
|
249
|
+
:class:`~pycode_kg.pycodekg.Edge` primitives to the generic spec types.
|
|
250
|
+
|
|
251
|
+
:return: Iterator of :class:`NodeSpec` and :class:`EdgeSpec` objects.
|
|
252
|
+
"""
|
|
253
|
+
from pycode_kg.graph import CodeGraph # pylint: disable=import-outside-toplevel
|
|
254
|
+
|
|
255
|
+
graph = CodeGraph(self.repo_path, include=self._include, exclude=self._exclude)
|
|
256
|
+
nodes, edges = graph.result()
|
|
257
|
+
|
|
258
|
+
for n in nodes:
|
|
259
|
+
yield NodeSpec(
|
|
260
|
+
node_id=n.id,
|
|
261
|
+
kind=n.kind,
|
|
262
|
+
name=n.name,
|
|
263
|
+
qualname=n.qualname or "",
|
|
264
|
+
source_path=n.module_path or "",
|
|
265
|
+
lineno=n.lineno,
|
|
266
|
+
end_lineno=n.end_lineno,
|
|
267
|
+
docstring=n.docstring or "",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
for e in edges:
|
|
271
|
+
yield EdgeSpec(
|
|
272
|
+
source_id=e.src,
|
|
273
|
+
target_id=e.dst,
|
|
274
|
+
relation=e.rel,
|
|
275
|
+
metadata={"evidence": e.evidence} if e.evidence else {},
|
|
276
|
+
)
|