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/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
+ )