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.
@@ -0,0 +1,106 @@
1
+ """
2
+ Framework Detector for TypeScriptKG.
3
+ Identifies repo-defining abstractions using centrality and cross-module signals.
4
+
5
+ Ported from PyCodeKG's ``analysis/framework_detector.py`` with the boilerplate
6
+ exclusion list adapted to TS/JS lifecycle and protocol members.
7
+
8
+ Author: Eric G. Suchanek, PhD
9
+ License: Elastic 2.0
10
+ """
11
+
12
+ import sqlite3
13
+
14
+ # Short member names that are structurally high fan-in but architecturally trivial.
15
+ # Excluded from framework-node ranking so hubs surface over lifecycle boilerplate.
16
+ _BOILERPLATE_NAMES: frozenset[str] = frozenset(
17
+ {
18
+ "constructor",
19
+ "toString",
20
+ "valueOf",
21
+ "toJSON",
22
+ "hasOwnProperty",
23
+ "close",
24
+ "dispose",
25
+ "destroy",
26
+ "next",
27
+ "return",
28
+ "throw",
29
+ "then",
30
+ "catch",
31
+ "finally",
32
+ }
33
+ )
34
+
35
+
36
+ def detect_framework_nodes(
37
+ limit: int = 25, db_path: str = "tscodekg.sqlite"
38
+ ) -> list[tuple[str, float, str]]:
39
+ """
40
+ Detect framework-like nodes using SIR and module connectivity.
41
+
42
+ Combines Structural Importance Ranking (SIR — importance within the graph)
43
+ with module connectivity (interaction complexity) to identify modules that are
44
+ both architecturally central AND highly connected to other modules.
45
+
46
+ Framework score = 0.6 × normalized SIR + 0.4 × normalized connectivity.
47
+ High-scoring modules are critical hubs: important AND complex.
48
+
49
+ Lifecycle / protocol boilerplate (``constructor``, ``toString``, ``close``,
50
+ etc.) is excluded so the ranking surfaces meaningful orchestrators rather
51
+ than the most-called teardown methods.
52
+
53
+ :param limit: Number of top framework nodes to return (default 25).
54
+ :param db_path: Path to SQLite database (default "tscodekg.sqlite").
55
+ :return: List of (node_id, framework_score, label) tuples, sorted by score descending.
56
+ """
57
+ # Aggregate node-level SIR up to module level so we compare like-for-like
58
+ # against the module_connectivity metric. Both signals end up keyed by
59
+ # bare module path (e.g. "src/auth/middleware.ts"), which we map to "mod:..."
60
+ # IDs for output.
61
+ from tscode_kg.centrality import ( # noqa: PLC0415
62
+ StructuralImportanceRanker,
63
+ aggregate_module_scores,
64
+ )
65
+
66
+ with sqlite3.connect(db_path) as con:
67
+ connectivity = dict(
68
+ con.execute(
69
+ "SELECT node_id, score FROM centrality_scores WHERE metric = 'module_connectivity'"
70
+ )
71
+ )
72
+ module_id_by_path: dict[str, str] = dict(
73
+ con.execute("SELECT module_path, id FROM nodes WHERE kind = 'module'")
74
+ )
75
+
76
+ ranker = StructuralImportanceRanker(db_path)
77
+ sir_records = ranker.compute()
78
+ sir_by_path: dict[str, float] = {
79
+ row["module_path"]: row["score"] for row in aggregate_module_scores(sir_records)
80
+ }
81
+
82
+ def norm(d: dict) -> dict:
83
+ if not d:
84
+ return {}
85
+ vals = list(d.values())
86
+ mn, mx = min(vals), max(vals)
87
+ return {k: (v - mn) / (mx - mn) if mx > mn else 0.0 for k, v in d.items()}
88
+
89
+ nsir = norm(sir_by_path)
90
+ nconnectivity = norm(connectivity)
91
+
92
+ # Framework score: weighted sum (SIR 60% — importance, connectivity 40% — coupling).
93
+ # Restrict to module paths only — the tool advertises "Framework-like Modules",
94
+ # so methods/functions must not leak into the ranking.
95
+ framework: dict[str, float] = {}
96
+ for path in set(nsir) | set(nconnectivity):
97
+ framework[path] = 0.6 * nsir.get(path, 0.0) + 0.4 * nconnectivity.get(path, 0.0)
98
+
99
+ ranked = sorted(framework.items(), key=lambda x: x[1], reverse=True)
100
+ result: list[tuple[str, float, str]] = []
101
+ for path, score in ranked[:limit]:
102
+ node_id = module_id_by_path.get(path, f"mod:{path}")
103
+ result.append((node_id, score, path))
104
+ # _BOILERPLATE_NAMES retained for backward-compat at the import site.
105
+ _ = _BOILERPLATE_NAMES
106
+ return result
tscode_kg/kg.py ADDED
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ kg.py — TypeScriptKG: concrete KGModule for TypeScript/JavaScript codebases.
4
+
5
+ Owns the TS/JS-specific extraction layer (tree-sitter AST) and delegates all
6
+ generic infrastructure (SQLite, sqlite-vec, hybrid query, snippet packing,
7
+ snapshots) to the KGModule base class from kg_utils.pipeline.
8
+
9
+ Author: Eric G. Suchanek, PhD
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ try:
17
+ from kg_utils.extractor import KGExtractor
18
+ from kg_utils.pipeline import KGModule
19
+ from kg_utils.semantic import DEFAULT_MODEL
20
+ from kg_utils.specs import BuildStats, QueryResult, SnippetPack
21
+ from kg_utils.store import GraphStore # noqa: F401
22
+ except ImportError as _e:
23
+ raise ImportError(
24
+ "TypeScriptKG requires kgmodule-utils[semantic] for its graph infrastructure.\n"
25
+ "Install with: pip install 'tscode-kg[kg]'\n"
26
+ f"Original error: {_e}"
27
+ ) from _e
28
+
29
+ from tscode_kg.config import load_exclude_dirs, load_include_dirs
30
+ from tscode_kg.extractor import TSCodeExtractor
31
+
32
+ __all__ = [
33
+ "TypeScriptKG",
34
+ "BuildStats",
35
+ "QueryResult",
36
+ "SnippetPack",
37
+ ]
38
+
39
+ # TypeScript/JavaScript node kind priority for ranking
40
+ _TS_KIND_PRIORITY: dict[str, int] = {
41
+ "function": 0,
42
+ "method": 1,
43
+ "class": 2,
44
+ "interface": 3,
45
+ "type_alias": 4,
46
+ "enum": 5,
47
+ "namespace": 6,
48
+ "module": 7,
49
+ "symbol": 8,
50
+ }
51
+
52
+
53
+ class TypeScriptKG(KGModule):
54
+ """
55
+ Top-level orchestrator for the TypeScript/JavaScript Knowledge Graph.
56
+
57
+ Subclasses :class:`~kg_utils.pipeline.KGModule` and provides the
58
+ TypeScript/JS-specific extraction layer via :class:`~tscode_kg.extractor.TSCodeExtractor`.
59
+ All generic infrastructure — SQLite persistence, sqlite-vec indexing,
60
+ hybrid query, snippet packing — is inherited from KGModule.
61
+
62
+ Typical usage::
63
+
64
+ kg = TypeScriptKG(repo_root="/path/to/ts-repo")
65
+ stats = kg.build(wipe=True)
66
+ print(stats)
67
+
68
+ result = kg.query("authentication middleware", k=8)
69
+ pack = kg.pack("API error handling")
70
+ pack.save("context.md")
71
+
72
+ :param repo_root: Repository root directory.
73
+ :param db_path: SQLite database path (defaults to ``<repo_root>/.tscodekg/graph.sqlite``).
74
+ :param vectors_path: sqlite-vec store path (defaults to ``<repo_root>/.tscodekg/vectors.sqlite``).
75
+ :param model: Sentence-transformer model name.
76
+ :param table: sqlite-vec table name.
77
+ """
78
+
79
+ _default_dir = ".tscodekg"
80
+
81
+ def __init__(
82
+ self,
83
+ repo_root: str | Path,
84
+ db_path: str | Path | None = None,
85
+ vectors_path: str | Path | None = None,
86
+ *,
87
+ model: str = DEFAULT_MODEL,
88
+ table: str = "tscodekg_nodes",
89
+ ) -> None:
90
+ super().__init__(
91
+ repo_root,
92
+ db_path=db_path,
93
+ model=model,
94
+ table=table,
95
+ vector_backend="sqlite-vec",
96
+ )
97
+ if vectors_path is not None:
98
+ self.vectors_path = Path(vectors_path)
99
+
100
+ # ------------------------------------------------------------------
101
+ # KGModule abstract interface
102
+ # ------------------------------------------------------------------
103
+
104
+ def make_extractor(self) -> KGExtractor:
105
+ include = load_include_dirs(self.repo_root)
106
+ exclude = load_exclude_dirs(self.repo_root)
107
+ return TSCodeExtractor(self.repo_root, include=include, exclude=exclude)
108
+
109
+ def kind(self) -> str:
110
+ return "code"
111
+
112
+ def analyze(self) -> str:
113
+ """Run thorough structural analysis and return a Markdown report.
114
+
115
+ Uses :class:`~tscode_kg.analysis.TSCodeKGAnalyzer` for a full 14-phase
116
+ analysis (fan-in, fan-out, module coupling, JSDoc coverage, hierarchy, …).
117
+ Falls back to a lightweight summary when the KG has not been built yet.
118
+ """
119
+ try:
120
+ from tscode_kg.analysis import TSCodeKGAnalyzer # noqa: PLC0415
121
+
122
+ analyzer = TSCodeKGAnalyzer(self)
123
+ analyzer.run_analysis()
124
+ return analyzer.to_markdown()
125
+ except Exception as exc: # noqa: BLE001
126
+ try:
127
+ return _render_analysis(str(self.repo_root), self.store.stats())
128
+ except Exception: # noqa: BLE001
129
+ return f"# TypeScriptKG Analysis\n\nAnalysis failed: {exc}\n"
130
+
131
+ # ------------------------------------------------------------------
132
+ # TS-specific overrides
133
+ # ------------------------------------------------------------------
134
+
135
+ def _kind_priority(self, kind: str) -> int:
136
+ return _TS_KIND_PRIORITY.get(kind, 99)
137
+
138
+ # ------------------------------------------------------------------
139
+ # Repr
140
+ # ------------------------------------------------------------------
141
+
142
+ def __repr__(self) -> str:
143
+ return (
144
+ f"TypeScriptKG(repo_root={self.repo_root!r}, "
145
+ f"db_path={self.db_path!r}, "
146
+ f"vectors_path={self.vectors_path!r}, "
147
+ f"model={self.model_name!r})"
148
+ )
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # Analysis renderer
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def _render_analysis(repo_root: str, stats: dict) -> str:
157
+ """Render a Markdown analysis report from store stats."""
158
+ lines: list[str] = [
159
+ "# TypeScriptKG Analysis Report\n",
160
+ f"**Repository:** `{repo_root}`\n",
161
+ "---\n",
162
+ "## Structural Metrics\n",
163
+ f"- **Total nodes:** {stats.get('total_nodes', 0):,}",
164
+ f"- **Meaningful nodes:** {stats.get('meaningful_nodes', 0):,}",
165
+ f"- **Total edges:** {stats.get('total_edges', 0):,}",
166
+ ]
167
+
168
+ cov = stats.get("docstring_coverage")
169
+ if cov is not None:
170
+ lines.append(f"- **JSDoc coverage:** {cov:.1%} *(functions + methods)*")
171
+
172
+ node_counts: dict = stats.get("node_counts", {})
173
+ if node_counts:
174
+ lines.append("\n### Nodes by Kind\n")
175
+ lines.append("| Kind | Count |")
176
+ lines.append("|------|------:|")
177
+ for kind, count in sorted(node_counts.items(), key=lambda x: -x[1]):
178
+ lines.append(f"| {kind} | {count:,} |")
179
+
180
+ edge_counts: dict = stats.get("edge_counts", {})
181
+ if edge_counts:
182
+ lines.append("\n### Edges by Relation\n")
183
+ lines.append("| Relation | Count |")
184
+ lines.append("|----------|------:|")
185
+ for rel, count in sorted(edge_counts.items(), key=lambda x: -x[1]):
186
+ lines.append(f"| {rel} | {count:,} |")
187
+
188
+ lines.append("\n---\n")
189
+ lines.append(
190
+ "> Graph built by deterministic tree-sitter AST extraction. "
191
+ "No LLM inference used in indexing."
192
+ )
193
+ return "\n".join(lines)