tscode-kg 0.2.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.
- tscode_kg/__init__.py +42 -0
- tscode_kg/__main__.py +6 -0
- tscode_kg/analysis.py +1829 -0
- tscode_kg/app.py +1355 -0
- tscode_kg/bridge.py +114 -0
- tscode_kg/centrality.py +434 -0
- tscode_kg/cli/__init__.py +1 -0
- tscode_kg/cli/cmd_analyze.py +69 -0
- tscode_kg/cli/cmd_bridges.py +38 -0
- tscode_kg/cli/cmd_build.py +86 -0
- tscode_kg/cli/cmd_centrality.py +124 -0
- tscode_kg/cli/cmd_explain.py +58 -0
- tscode_kg/cli/cmd_framework_nodes.py +43 -0
- tscode_kg/cli/cmd_hooks.py +125 -0
- tscode_kg/cli/cmd_init.py +234 -0
- tscode_kg/cli/cmd_mcp.py +35 -0
- tscode_kg/cli/cmd_model.py +52 -0
- tscode_kg/cli/cmd_query.py +75 -0
- tscode_kg/cli/cmd_snapshot.py +431 -0
- tscode_kg/cli/cmd_viz.py +175 -0
- tscode_kg/cli/main.py +56 -0
- tscode_kg/coderank.py +564 -0
- tscode_kg/config.py +36 -0
- tscode_kg/explain.py +270 -0
- tscode_kg/extractor.py +827 -0
- tscode_kg/framework_detector.py +106 -0
- tscode_kg/kg.py +193 -0
- tscode_kg/layout3d.py +492 -0
- tscode_kg/mcp_server.py +1412 -0
- tscode_kg/snapshots.py +64 -0
- tscode_kg/viz3d.py +1457 -0
- tscode_kg/viz3d_timeline.py +369 -0
- tscode_kg-0.2.0.dist-info/METADATA +196 -0
- tscode_kg-0.2.0.dist-info/RECORD +37 -0
- tscode_kg-0.2.0.dist-info/WHEEL +4 -0
- tscode_kg-0.2.0.dist-info/entry_points.txt +15 -0
- tscode_kg-0.2.0.dist-info/licenses/LICENSE +24 -0
tscode_kg/explain.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown rendering of node explanations for TypeScriptKG.
|
|
3
|
+
|
|
4
|
+
Single source of truth for the ``explain`` output used by both:
|
|
5
|
+
|
|
6
|
+
- the CLI ``tscodekg explain`` command, and
|
|
7
|
+
- the MCP ``explain`` tool.
|
|
8
|
+
|
|
9
|
+
Centralizing the rendering here ensures the two surfaces never drift in
|
|
10
|
+
their role-labeling, formatting, threshold semantics, or markdown
|
|
11
|
+
structure. Ported from PyCodeKG's ``explain.py`` with kind labels and
|
|
12
|
+
zero-caller heuristics adapted to the TS/JS vocabulary.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
_TS_EXT_RE = re.compile(r"\.(ts|tsx|js|jsx|mts|cts|mjs|cjs)$")
|
|
21
|
+
|
|
22
|
+
# Members invoked by the JS runtime or standard protocols rather than by code.
|
|
23
|
+
_RUNTIME_MEMBER_NAMES: frozenset[str] = frozenset(
|
|
24
|
+
{"constructor", "toString", "toJSON", "valueOf", "next", "return", "throw"}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def render_explain(
|
|
29
|
+
kg: Any,
|
|
30
|
+
node_id: str,
|
|
31
|
+
*,
|
|
32
|
+
limit: int = 10,
|
|
33
|
+
snippets_hint: str = "pack_snippets()",
|
|
34
|
+
) -> str:
|
|
35
|
+
"""
|
|
36
|
+
Render a Markdown natural-language explanation of a code node.
|
|
37
|
+
|
|
38
|
+
:param kg: A :class:`TypeScriptKG`-like object exposing ``node()``,
|
|
39
|
+
``callers()``, ``stats()``, and ``_store.edges_from()``.
|
|
40
|
+
:param node_id: Stable node identifier
|
|
41
|
+
(e.g. ``fn:src/utils/helpers.ts:formatDate``).
|
|
42
|
+
:param limit: Maximum callers and callees to list. Pass 0 to list all.
|
|
43
|
+
:param snippets_hint: Closing call-to-action shown to the consumer for
|
|
44
|
+
retrieving the full source — ``"pack_snippets()"``
|
|
45
|
+
for MCP, ``"tscodekg pack"`` for CLI.
|
|
46
|
+
:return: Markdown string, or a "Node Not Found" header when the ID does
|
|
47
|
+
not exist in the knowledge graph.
|
|
48
|
+
"""
|
|
49
|
+
node = kg.node(node_id)
|
|
50
|
+
if node is None:
|
|
51
|
+
return f"# Node Not Found\n\nNode ID `{node_id}` does not exist in the knowledge graph."
|
|
52
|
+
|
|
53
|
+
out: list[str] = []
|
|
54
|
+
|
|
55
|
+
kind = node.get("kind", "unknown")
|
|
56
|
+
name = node.get("qualname") or node.get("name", "unknown")
|
|
57
|
+
out.append(f"# {kind.capitalize()}: `{name}`\n")
|
|
58
|
+
|
|
59
|
+
out.append("## Metadata\n")
|
|
60
|
+
if node.get("module_path"):
|
|
61
|
+
out.append(f"- **Module**: `{node['module_path']}`")
|
|
62
|
+
if node.get("lineno") is not None:
|
|
63
|
+
out.append(
|
|
64
|
+
f"- **Location**: line {node['lineno']}"
|
|
65
|
+
+ (f"–{node['end_lineno']}" if node.get("end_lineno") else "")
|
|
66
|
+
)
|
|
67
|
+
out.append(f"- **ID**: `{node_id}`")
|
|
68
|
+
out.append("")
|
|
69
|
+
|
|
70
|
+
docstring = (node.get("docstring") or "").strip()
|
|
71
|
+
if docstring:
|
|
72
|
+
out.append("## Documentation\n")
|
|
73
|
+
out.append(docstring)
|
|
74
|
+
out.append("")
|
|
75
|
+
|
|
76
|
+
_append_callers(out, kg, node_id, kind, limit)
|
|
77
|
+
_append_callees(out, kg, node_id, kind, limit)
|
|
78
|
+
|
|
79
|
+
out.append("## Role in Codebase\n")
|
|
80
|
+
out.append(_role_label(kg, node_id, node))
|
|
81
|
+
|
|
82
|
+
out.append("")
|
|
83
|
+
out.append("---\n")
|
|
84
|
+
out.append(f"*Use `{snippets_hint}` to retrieve the full source code.*")
|
|
85
|
+
|
|
86
|
+
return "\n".join(out)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _append_callers(out: list[str], kg: Any, node_id: str, kind: str, limit: int) -> None:
|
|
90
|
+
try:
|
|
91
|
+
caller_list = kg.callers(node_id, rel="CALLS")
|
|
92
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
93
|
+
return
|
|
94
|
+
if not caller_list:
|
|
95
|
+
return
|
|
96
|
+
out.append("## Called By (Callers)\n")
|
|
97
|
+
out.append(f"This {kind} is called by **{len(caller_list)}** other function(s):\n")
|
|
98
|
+
shown = caller_list[:limit] if limit > 0 else caller_list
|
|
99
|
+
for caller in shown:
|
|
100
|
+
cn = caller.get("qualname") or caller.get("name", "unknown")
|
|
101
|
+
cm = caller.get("module_path", "")
|
|
102
|
+
out.append(f"- `{cn}` ({cm})")
|
|
103
|
+
if limit > 0 and len(caller_list) > limit:
|
|
104
|
+
out.append(f"- ... and {len(caller_list) - limit} more")
|
|
105
|
+
out.append("")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _append_callees(out: list[str], kg: Any, node_id: str, kind: str, limit: int) -> None:
|
|
109
|
+
try:
|
|
110
|
+
store = getattr(kg, "_store", None)
|
|
111
|
+
if store is None:
|
|
112
|
+
return
|
|
113
|
+
edges = store.edges_from(node_id, rel="CALLS", limit=50)
|
|
114
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
115
|
+
return
|
|
116
|
+
if not edges:
|
|
117
|
+
return
|
|
118
|
+
callees: set[str] = set()
|
|
119
|
+
for edge in edges:
|
|
120
|
+
dst = edge.get("dst")
|
|
121
|
+
if dst is None:
|
|
122
|
+
continue
|
|
123
|
+
dst_node = kg.node(dst)
|
|
124
|
+
# Filter out symbol stubs and externals (no module_path → external package).
|
|
125
|
+
if dst_node and dst_node.get("kind") != "symbol" and dst_node.get("module_path"):
|
|
126
|
+
dn = dst_node.get("qualname") or dst_node.get("name", "unknown")
|
|
127
|
+
callees.add(f"- `{dn}`")
|
|
128
|
+
if not callees:
|
|
129
|
+
return
|
|
130
|
+
out.append("## Calls (Callees)\n")
|
|
131
|
+
out.append(f"This {kind} calls **{len(callees)}** other function(s):\n")
|
|
132
|
+
sorted_c = sorted(callees)
|
|
133
|
+
shown = sorted_c[:limit] if limit > 0 else sorted_c
|
|
134
|
+
for callee in shown:
|
|
135
|
+
out.append(callee)
|
|
136
|
+
if limit > 0 and len(callees) > limit:
|
|
137
|
+
out.append(f"- ... and {len(callees) - limit} more")
|
|
138
|
+
out.append("")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _role_label(kg: Any, node_id: str, node: dict) -> str:
|
|
142
|
+
"""Build the kind-aware role label used in ``## Role in Codebase``.
|
|
143
|
+
|
|
144
|
+
Uses caller-count thresholds relative to the codebase size (top 5% / top
|
|
145
|
+
2%), an orchestrator branch for high-fan-out coordination hubs, and
|
|
146
|
+
kind-aware nouns/verbs so a class is described as "Constructed" and an
|
|
147
|
+
interface as "Implemented" rather than as a "Utility function".
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
caller_count = len(kg.callers(node_id, rel="CALLS"))
|
|
151
|
+
|
|
152
|
+
callee_count = _count_internal_callees(kg, node_id)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
meaningful_nodes = kg.stats().get("meaningful_nodes", 100)
|
|
156
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
157
|
+
meaningful_nodes = 100
|
|
158
|
+
|
|
159
|
+
thresh_high = max(15, int(meaningful_nodes * 0.05))
|
|
160
|
+
thresh_imp = max(5, int(meaningful_nodes * 0.02))
|
|
161
|
+
thresh_orch = 8
|
|
162
|
+
|
|
163
|
+
node_kind = node.get("kind", "")
|
|
164
|
+
if node_kind == "class":
|
|
165
|
+
kind_noun, verb_past = "class", "Constructed"
|
|
166
|
+
elif node_kind == "interface":
|
|
167
|
+
kind_noun, verb_past = "interface", "Implemented/referenced"
|
|
168
|
+
elif node_kind in ("type_alias", "enum"):
|
|
169
|
+
kind_noun, verb_past = node_kind.replace("_", " "), "Referenced"
|
|
170
|
+
elif node_kind == "module":
|
|
171
|
+
kind_noun, verb_past = "module", "Imported"
|
|
172
|
+
else:
|
|
173
|
+
kind_noun, verb_past = "function", "Called"
|
|
174
|
+
|
|
175
|
+
if caller_count >= thresh_high:
|
|
176
|
+
return (
|
|
177
|
+
f"**High-value {kind_noun}**: {verb_past} {caller_count} times "
|
|
178
|
+
f"(≥{thresh_high} = top 5% of this codebase). "
|
|
179
|
+
"This is likely a core API or bottleneck. "
|
|
180
|
+
"Changes here may have wide impact."
|
|
181
|
+
)
|
|
182
|
+
if caller_count >= thresh_imp:
|
|
183
|
+
return (
|
|
184
|
+
f"**Important {kind_noun}**: {verb_past} {caller_count} times "
|
|
185
|
+
f"(≥{thresh_imp} = top 2% of this codebase). "
|
|
186
|
+
"Part of the essential infrastructure."
|
|
187
|
+
)
|
|
188
|
+
if callee_count >= thresh_orch and caller_count > 0:
|
|
189
|
+
return (
|
|
190
|
+
f"**Core orchestrator**: Called {caller_count} time(s), "
|
|
191
|
+
f"calls {callee_count} others. "
|
|
192
|
+
"Low caller count likely reflects a top-level entry point — "
|
|
193
|
+
"the high fan-out indicates a coordination hub, not a utility."
|
|
194
|
+
)
|
|
195
|
+
if caller_count > 0:
|
|
196
|
+
mod_summary = _caller_module_summary(kg, node_id)
|
|
197
|
+
utility_noun = "Supporting" if node_kind in ("class", "interface") else "Utility"
|
|
198
|
+
return (
|
|
199
|
+
f"**{utility_noun} {kind_noun}**: {verb_past} {caller_count} time(s) "
|
|
200
|
+
f"from {mod_summary}."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return _zero_caller_label(node)
|
|
204
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
205
|
+
return "Unable to determine call graph role."
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _count_internal_callees(kg: Any, node_id: str) -> int:
|
|
209
|
+
try:
|
|
210
|
+
store = getattr(kg, "_store", None)
|
|
211
|
+
if store is None:
|
|
212
|
+
return 0
|
|
213
|
+
edges = store.edges_from(node_id, rel="CALLS", limit=100)
|
|
214
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
215
|
+
return 0
|
|
216
|
+
count = 0
|
|
217
|
+
for e in edges or []:
|
|
218
|
+
dst = e.get("dst") or ""
|
|
219
|
+
if dst.startswith("sym:"):
|
|
220
|
+
continue
|
|
221
|
+
dst_node = kg.node(dst)
|
|
222
|
+
if dst_node and dst_node.get("module_path"):
|
|
223
|
+
count += 1
|
|
224
|
+
return count
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _caller_module_summary(kg: Any, node_id: str) -> str:
|
|
228
|
+
try:
|
|
229
|
+
callers_for_role = kg.callers(node_id, rel="CALLS")
|
|
230
|
+
except (AttributeError, ValueError, RuntimeError):
|
|
231
|
+
return "various callers"
|
|
232
|
+
caller_mods = sorted(
|
|
233
|
+
{
|
|
234
|
+
_TS_EXT_RE.sub("", c.get("module_path", "").split("/")[-1])
|
|
235
|
+
for c in callers_for_role
|
|
236
|
+
if c.get("module_path")
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
if not caller_mods:
|
|
240
|
+
return "various callers"
|
|
241
|
+
summary = ", ".join(f"`{m}`" for m in caller_mods[:4])
|
|
242
|
+
if len(caller_mods) > 4:
|
|
243
|
+
summary += " and more"
|
|
244
|
+
return summary
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _zero_caller_label(node: dict) -> str:
|
|
248
|
+
module = node.get("module_path", "")
|
|
249
|
+
name = node.get("name", "")
|
|
250
|
+
if name in _RUNTIME_MEMBER_NAMES:
|
|
251
|
+
return (
|
|
252
|
+
"**Protocol member**: Zero internal callers by design. "
|
|
253
|
+
"Invoked by the JavaScript runtime or standard protocols "
|
|
254
|
+
"(e.g., `constructor`, `toString`, iterator methods)."
|
|
255
|
+
)
|
|
256
|
+
if "/cli/" in module or _TS_EXT_RE.sub("", module).endswith("cli"):
|
|
257
|
+
return (
|
|
258
|
+
"**CLI entry point**: Zero internal callers by design. "
|
|
259
|
+
"Invoked by the CLI router when the user runs the command."
|
|
260
|
+
)
|
|
261
|
+
if node.get("kind") in ("interface", "type_alias", "enum"):
|
|
262
|
+
return (
|
|
263
|
+
"**Type-level declaration**: Zero call edges by design. "
|
|
264
|
+
"Referenced in type positions, which do not appear in the call graph."
|
|
265
|
+
)
|
|
266
|
+
return (
|
|
267
|
+
"**Orphaned**: Never called internally. "
|
|
268
|
+
"May be dead code, a public API, or a framework entry point "
|
|
269
|
+
"(e.g. a component or route handler invoked by a framework)."
|
|
270
|
+
)
|