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/bridge.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ Module Connectivity Centrality for TypeScriptKG.
3
+ Measures module interaction complexity: how many unique modules each module calls/imports.
4
+ For well-modularized codebases, identifies orchestrator and hub modules.
5
+
6
+ Ported from PyCodeKG's ``analysis/bridge.py`` and adapted to the TS/JS
7
+ module vocabulary (path-based module names).
8
+
9
+ Author: Eric G. Suchanek, PhD
10
+ License: Elastic 2.0
11
+ """
12
+
13
+ import sqlite3
14
+ from collections import defaultdict
15
+
16
+ from tscode_kg.centrality import CentralityRecord, StructuralImportanceRanker
17
+
18
+
19
+ def compute_bridge_centrality(
20
+ kind: str = "module",
21
+ include_imports: bool = True,
22
+ top: int = 25,
23
+ db_path: str = "tscodekg.sqlite",
24
+ ) -> list[tuple[str, float]]:
25
+ """
26
+ Compute module connectivity: unique module interactions per module.
27
+
28
+ For well-modularized codebases with strong module boundaries, connectivity
29
+ identifies which modules are hubs (calling many others) or widely depended upon
30
+ (called by many modules).
31
+
32
+ Replaces betweenness centrality which is meaningless when inter-module edges are zero.
33
+
34
+ :param kind: Node kind (default 'module', currently unused but kept for compatibility)
35
+ :param include_imports: Whether to include IMPORTS in connectivity (default True)
36
+ :param top: Number of top modules to return (default 25)
37
+ :param db_path: Path to SQLite database
38
+ :return: List of (module_path, connectivity_score) tuples
39
+ """
40
+ with sqlite3.connect(db_path) as con:
41
+ rows = con.execute(
42
+ """
43
+ SELECT src.module_path, dst.module_path, rel
44
+ FROM edges
45
+ JOIN nodes AS src ON edges.src = src.id
46
+ JOIN nodes AS dst ON edges.dst = dst.id
47
+ WHERE rel IN ('CALLS', 'IMPORTS')
48
+ AND src.module_path IS NOT NULL
49
+ AND dst.module_path IS NOT NULL
50
+ """
51
+ ).fetchall()
52
+
53
+ # Compute unique modules called + unique modules calling this module
54
+ outbound: dict[str, set[str]] = defaultdict(set) # modules this module calls
55
+ inbound: dict[str, set[str]] = defaultdict(set) # modules that call this module
56
+ call_counts: dict[str, int] = defaultdict(int) # total call frequency
57
+
58
+ for src_mod, dst_mod, rel in rows:
59
+ if not src_mod or not dst_mod:
60
+ continue
61
+ if rel == "IMPORTS" and not include_imports:
62
+ continue
63
+
64
+ # Record outbound: src_mod calls/imports dst_mod
65
+ outbound[src_mod].add(dst_mod)
66
+ # Record inbound: dst_mod is called/imported by src_mod
67
+ inbound[dst_mod].add(src_mod)
68
+ call_counts[src_mod] += 1
69
+
70
+ # Collect all modules
71
+ all_modules = set(outbound.keys()) | set(inbound.keys())
72
+
73
+ # Compute connectivity score: unique modules touched (fan-out + fan-in)
74
+ # Higher score = more coupled with other modules
75
+ scores: dict[str, float] = {}
76
+ for mod in all_modules:
77
+ unique_outbound = len(outbound[mod])
78
+ unique_inbound = len(inbound[mod])
79
+ total_calls = call_counts[mod]
80
+
81
+ # Normalize: average of outbound and inbound diversity + call frequency
82
+ # Scale to [0, 1]: assume typical module touches ~15 others
83
+ connectivity_score = (
84
+ (unique_outbound + unique_inbound) / 30.0 # diversity (60%)
85
+ + min(total_calls / 50.0, 1.0) * 0.4 # frequency (40%)
86
+ ) / 1.4 # normalize to roughly [0, 1]
87
+ scores[mod] = min(connectivity_score, 1.0)
88
+
89
+ # Persist scores
90
+ records = [
91
+ CentralityRecord(
92
+ node_id=mod,
93
+ kind="module",
94
+ name=mod.split("/")[-1],
95
+ module_path=mod,
96
+ score=score,
97
+ rank=idx + 1,
98
+ inbound_count=len(inbound.get(mod, set())),
99
+ cross_module_inbound=len(inbound.get(mod, set())), # all are cross-module
100
+ rel_breakdown={
101
+ "calls_to_modules": len(outbound.get(mod, set())),
102
+ "called_by_modules": len(inbound.get(mod, set())),
103
+ },
104
+ top_contributors=[],
105
+ )
106
+ for idx, (mod, score) in enumerate(sorted(scores.items(), key=lambda x: x[1], reverse=True))
107
+ ]
108
+
109
+ if records:
110
+ StructuralImportanceRanker(db_path).write_scores(records, metric="module_connectivity")
111
+
112
+ # Return top modules by connectivity
113
+ ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
114
+ return ranked[:top]
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env python3
2
+ """Structural centrality analysis for TypeScriptKG.
3
+
4
+ Implements Structural Importance Ranking (SIR): a deterministic weighted
5
+ PageRank over the TypeScriptKG graph. Edge weights are tuned per relation
6
+ type (CALLS > INHERITS/IMPLEMENTS > IMPORTS > CONTAINS) and amplified for
7
+ cross-module links, giving a stable, interpretable importance score for every
8
+ module, class, interface, function, and method in the indexed codebase.
9
+
10
+ Public API:
11
+ - :class:`StructuralImportanceRanker` — compute and persist SIR scores.
12
+ - :func:`aggregate_module_scores` — roll node scores up to module level.
13
+
14
+ Author: Eric G. Suchanek, PhD
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import sqlite3
21
+ from collections import defaultdict
22
+ from dataclasses import dataclass, field
23
+ from datetime import UTC, datetime
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ _ALLOWED_KINDS = {"module", "class", "interface", "function", "method"}
28
+ _STRUCTURAL_RELS = ("CALLS", "INHERITS", "IMPLEMENTS", "EXTENDS", "IMPORTS", "CONTAINS")
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class CentralityConfig:
33
+ """Configuration for Structural Importance Ranking.
34
+
35
+ :param damping: PageRank damping factor.
36
+ :param max_iter: Maximum PageRank iterations.
37
+ :param tol: Convergence tolerance.
38
+ :param rel_weights: Per-relation weights.
39
+ :param cross_module_boost: Weight multiplier for cross-module edges.
40
+ :param private_penalty: Multiplier for private symbols.
41
+ """
42
+
43
+ damping: float = 0.85
44
+ max_iter: int = 100
45
+ tol: float = 1e-10
46
+ rel_weights: dict[str, float] = field(
47
+ default_factory=lambda: {
48
+ "CALLS": 1.0,
49
+ "INHERITS": 0.8,
50
+ "IMPLEMENTS": 0.8,
51
+ "EXTENDS": 0.8,
52
+ "IMPORTS": 0.45,
53
+ "CONTAINS": 0.15,
54
+ }
55
+ )
56
+ cross_module_boost: float = 1.5
57
+ private_penalty: float = 0.85
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class CentralityRecord:
62
+ """Centrality result for a single node.
63
+
64
+ :param node_id: Stable node identifier.
65
+ :param kind: TypeScriptKG node kind.
66
+ :param name: Node name.
67
+ :param module_path: Module path from the store.
68
+ :param score: Final importance score.
69
+ :param rank: Rank among all returned nodes.
70
+ :param inbound_count: Number of inbound effective edges.
71
+ :param cross_module_inbound: Number of inbound cross-module edges.
72
+ :param rel_breakdown: Inbound counts by relation type.
73
+ :param top_contributors: Top inbound contributor summaries.
74
+ """
75
+
76
+ node_id: str
77
+ kind: str
78
+ name: str
79
+ module_path: str | None
80
+ score: float
81
+ rank: int
82
+ inbound_count: int
83
+ cross_module_inbound: int
84
+ rel_breakdown: dict[str, int]
85
+ top_contributors: list[dict[str, Any]]
86
+
87
+
88
+ @dataclass(slots=True)
89
+ class _NodeInfo:
90
+ node_id: str
91
+ kind: str
92
+ name: str
93
+ module_path: str | None
94
+
95
+
96
+ @dataclass(slots=True)
97
+ class _EffectiveEdge:
98
+ src: str
99
+ dst: str
100
+ rel: str
101
+ weight: float
102
+ same_module: bool
103
+
104
+
105
+ class StructuralImportanceRanker:
106
+ """Compute Structural Importance Ranking (SIR) for a TypeScriptKG SQLite database.
107
+
108
+ Loads all real nodes (excluding sym-stub intermediates) and structural
109
+ edges, resolves cross-module symbol stubs, then runs a weighted PageRank
110
+ where each relation type contributes a distinct edge weight and
111
+ cross-module edges receive an additional boost. Private symbols are
112
+ penalized post-convergence. Scores are normalized to sum to 1.0.
113
+ """
114
+
115
+ def __init__(self, db_path: str | Path, config: CentralityConfig | None = None) -> None:
116
+ """Bind the ranker to a SQLite graph and (optional) tuning config.
117
+
118
+ :param db_path: Path to the TypeScriptKG SQLite knowledge graph.
119
+ :param config: Override default relation weights, damping, iteration
120
+ cap, tolerance, cross-module boost, or private-symbol penalty.
121
+ Pass ``None`` for the package defaults (see ``CentralityConfig``).
122
+ """
123
+ self.db_path = Path(db_path)
124
+ self.config = config or CentralityConfig()
125
+
126
+ def compute(
127
+ self,
128
+ *,
129
+ kinds: set[str] | None = None,
130
+ top: int | None = None,
131
+ ) -> list[CentralityRecord]:
132
+ """Compute SIR scores for all nodes in the graph.
133
+
134
+ :param kinds: Restrict results to a subset of node kinds
135
+ (``'module'``, ``'class'``, ``'interface'``, ``'function'``,
136
+ ``'method'``). When ``None``, all kinds are returned.
137
+ :param top: Cap the number of returned records after filtering.
138
+ :return: Records sorted descending by normalized importance score,
139
+ each annotated with rank, inbound-edge counts, and top
140
+ contributing callers.
141
+ """
142
+ node_map = self._load_nodes()
143
+ effective_edges = self._load_effective_edges(node_map)
144
+ scores = self._pagerank(node_map, effective_edges)
145
+ records = self._assemble_records(node_map, effective_edges, scores)
146
+
147
+ if kinds:
148
+ records = [r for r in records if r.kind in kinds]
149
+ if top is not None:
150
+ records = records[:top]
151
+ return records
152
+
153
+ def write_scores(
154
+ self,
155
+ records: list[CentralityRecord],
156
+ *,
157
+ metric: str = "sir_pagerank",
158
+ ) -> int:
159
+ """Persist SIR scores into the ``centrality_scores`` table.
160
+
161
+ Upserts on ``(node_id, metric)`` so repeated runs overwrite stale
162
+ scores without accumulating duplicate rows.
163
+
164
+ :param records: Ranked records from :meth:`compute`.
165
+ :param metric: Label for the metric column (default ``'sir_pagerank'``).
166
+ :return: Number of rows written.
167
+ """
168
+ rows = []
169
+ computed_at = datetime.now(UTC).isoformat()
170
+ params = json.dumps(
171
+ {
172
+ "damping": self.config.damping,
173
+ "max_iter": self.config.max_iter,
174
+ "tol": self.config.tol,
175
+ "rel_weights": self.config.rel_weights,
176
+ "cross_module_boost": self.config.cross_module_boost,
177
+ "private_penalty": self.config.private_penalty,
178
+ },
179
+ sort_keys=True,
180
+ )
181
+ for rec in records:
182
+ rows.append((rec.node_id, metric, rec.score, rec.rank, computed_at, params))
183
+
184
+ with sqlite3.connect(self.db_path) as con:
185
+ con.execute(
186
+ """
187
+ CREATE TABLE IF NOT EXISTS centrality_scores (
188
+ node_id TEXT NOT NULL,
189
+ metric TEXT NOT NULL,
190
+ score REAL NOT NULL,
191
+ rank INTEGER,
192
+ computed_at TEXT NOT NULL,
193
+ params_json TEXT NOT NULL,
194
+ PRIMARY KEY (node_id, metric)
195
+ )
196
+ """
197
+ )
198
+ con.executemany(
199
+ """
200
+ INSERT INTO centrality_scores
201
+ (node_id, metric, score, rank, computed_at, params_json)
202
+ VALUES (?, ?, ?, ?, ?, ?)
203
+ ON CONFLICT(node_id, metric) DO UPDATE SET
204
+ score = excluded.score,
205
+ rank = excluded.rank,
206
+ computed_at = excluded.computed_at,
207
+ params_json = excluded.params_json
208
+ """,
209
+ rows,
210
+ )
211
+ con.commit()
212
+ return len(rows)
213
+
214
+ def _load_nodes(self) -> dict[str, _NodeInfo]:
215
+ """Load real graph nodes (modules, classes, interfaces, functions, methods) from SQLite, keyed by node id. ``sym:`` stubs are excluded."""
216
+ with sqlite3.connect(self.db_path) as con:
217
+ rows = con.execute(
218
+ """
219
+ SELECT id, kind, name, module_path
220
+ FROM nodes
221
+ WHERE kind IN ('module', 'class', 'interface', 'function', 'method')
222
+ """
223
+ ).fetchall()
224
+ return {
225
+ row[0]: _NodeInfo(node_id=row[0], kind=row[1], name=row[2], module_path=row[3])
226
+ for row in rows
227
+ }
228
+
229
+ def _load_effective_edges(
230
+ self,
231
+ node_map: dict[str, _NodeInfo],
232
+ ) -> list[_EffectiveEdge]:
233
+ """Load structural edges (CALLS / INHERITS / IMPLEMENTS / EXTENDS / IMPORTS / CONTAINS) and rewrite ``sym:`` targets through ``RESOLVES_TO``.
234
+
235
+ Returns deduplicated edges weighted by relation type, with cross-module
236
+ edges receiving an additional boost from ``config.cross_module_boost``.
237
+ """
238
+ with sqlite3.connect(self.db_path) as con:
239
+ structural = con.execute(
240
+ """
241
+ SELECT src, rel, dst
242
+ FROM edges
243
+ WHERE rel IN ('CALLS', 'INHERITS', 'IMPLEMENTS', 'EXTENDS', 'IMPORTS', 'CONTAINS')
244
+ """
245
+ ).fetchall()
246
+ resolves = con.execute(
247
+ """
248
+ SELECT src, dst
249
+ FROM edges
250
+ WHERE rel = 'RESOLVES_TO'
251
+ """
252
+ ).fetchall()
253
+
254
+ resolve_map: dict[str, list[str]] = defaultdict(list)
255
+ for sym_id, dst in resolves:
256
+ if dst in node_map:
257
+ resolve_map[sym_id].append(dst)
258
+
259
+ dedup: set[tuple[str, str, str]] = set()
260
+ effective: list[_EffectiveEdge] = []
261
+
262
+ for src, rel, dst in structural:
263
+ if src not in node_map:
264
+ continue
265
+ targets: list[str]
266
+ if dst in node_map:
267
+ targets = [dst]
268
+ else:
269
+ targets = resolve_map.get(dst, [])
270
+ for target in targets:
271
+ if target not in node_map:
272
+ continue
273
+ key = (src, rel, target)
274
+ if key in dedup:
275
+ continue
276
+ dedup.add(key)
277
+ weight = float(self.config.rel_weights[rel])
278
+ same_module = node_map[src].module_path == node_map[target].module_path
279
+ if not same_module:
280
+ weight *= self.config.cross_module_boost
281
+ effective.append(
282
+ _EffectiveEdge(
283
+ src=src,
284
+ dst=target,
285
+ rel=rel,
286
+ weight=weight,
287
+ same_module=same_module,
288
+ )
289
+ )
290
+ return effective
291
+
292
+ def _pagerank(
293
+ self,
294
+ node_map: dict[str, _NodeInfo],
295
+ effective_edges: list[_EffectiveEdge],
296
+ ) -> dict[str, float]:
297
+ """Run weighted PageRank on the resolved edge set until convergence.
298
+
299
+ Iterates up to ``config.max_iter`` with damping ``config.damping``,
300
+ redistributing dangling-node mass uniformly. After convergence, names
301
+ starting with ``_`` are scaled by ``config.private_penalty`` and the
302
+ result is normalized so all scores sum to ``1.0``.
303
+ """
304
+ nodes = list(node_map)
305
+ n = len(nodes)
306
+ if n == 0:
307
+ return {}
308
+
309
+ out_weight: dict[str, float] = defaultdict(float)
310
+ incoming: dict[str, list[tuple[str, float]]] = defaultdict(list)
311
+ for edge in effective_edges:
312
+ out_weight[edge.src] += edge.weight
313
+ incoming[edge.dst].append((edge.src, edge.weight))
314
+
315
+ pr = {node_id: 1.0 / n for node_id in nodes}
316
+ damping = self.config.damping
317
+
318
+ for _ in range(self.config.max_iter):
319
+ base = (1.0 - damping) / n
320
+ dangling_mass = sum(pr[node_id] for node_id in nodes if out_weight[node_id] == 0.0)
321
+ dangling_share = damping * dangling_mass / n
322
+ new_pr: dict[str, float] = {}
323
+ delta = 0.0
324
+
325
+ for node_id in nodes:
326
+ score = base + dangling_share
327
+ for src, weight in incoming.get(node_id, []):
328
+ denom = out_weight[src]
329
+ if denom > 0.0:
330
+ score += damping * pr[src] * (weight / denom)
331
+ new_pr[node_id] = score
332
+ delta += abs(score - pr[node_id])
333
+
334
+ pr = new_pr
335
+ if delta < self.config.tol:
336
+ break
337
+
338
+ for node_id, info in node_map.items():
339
+ if info.name.startswith("_"):
340
+ pr[node_id] *= self.config.private_penalty
341
+
342
+ total = sum(pr.values())
343
+ if total > 0.0:
344
+ pr = {node_id: score / total for node_id, score in pr.items()}
345
+ return pr
346
+
347
+ def _assemble_records(
348
+ self,
349
+ node_map: dict[str, _NodeInfo],
350
+ effective_edges: list[_EffectiveEdge],
351
+ scores: dict[str, float],
352
+ ) -> list[CentralityRecord]:
353
+ """Combine PageRank scores with inbound counts, per-relation breakdowns, and top contributing callers into rank-ordered ``CentralityRecord`` outputs."""
354
+ inbound_counts: dict[str, int] = defaultdict(int)
355
+ cross_counts: dict[str, int] = defaultdict(int)
356
+ rel_breakdown: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
357
+ out_weight: dict[str, float] = defaultdict(float)
358
+ incoming_edges: dict[str, list[_EffectiveEdge]] = defaultdict(list)
359
+
360
+ for edge in effective_edges:
361
+ out_weight[edge.src] += edge.weight
362
+ inbound_counts[edge.dst] += 1
363
+ rel_breakdown[edge.dst][edge.rel] += 1
364
+ if not edge.same_module:
365
+ cross_counts[edge.dst] += 1
366
+ incoming_edges[edge.dst].append(edge)
367
+
368
+ ranked_ids = sorted(scores, key=lambda node_id: scores[node_id], reverse=True)
369
+ records: list[CentralityRecord] = []
370
+ for rank, node_id in enumerate(ranked_ids, start=1):
371
+ info = node_map[node_id]
372
+ contributors: list[dict[str, Any]] = []
373
+ for edge in incoming_edges.get(node_id, []):
374
+ denom = out_weight[edge.src]
375
+ contrib = 0.0 if denom == 0.0 else scores[edge.src] * (edge.weight / denom)
376
+ src_info = node_map.get(edge.src)
377
+ contributors.append(
378
+ {
379
+ "src": edge.src,
380
+ "src_name": src_info.name if src_info else edge.src,
381
+ "rel": edge.rel,
382
+ "same_module": edge.same_module,
383
+ "contribution": contrib,
384
+ }
385
+ )
386
+ contributors.sort(key=lambda item: item["contribution"], reverse=True)
387
+ records.append(
388
+ CentralityRecord(
389
+ node_id=node_id,
390
+ kind=info.kind,
391
+ name=info.name,
392
+ module_path=info.module_path,
393
+ score=scores[node_id],
394
+ rank=rank,
395
+ inbound_count=inbound_counts[node_id],
396
+ cross_module_inbound=cross_counts[node_id],
397
+ rel_breakdown=dict(sorted(rel_breakdown[node_id].items())),
398
+ top_contributors=contributors[:5],
399
+ )
400
+ )
401
+ return records
402
+
403
+
404
+ def aggregate_module_scores(records: list[CentralityRecord]) -> list[dict[str, Any]]:
405
+ """Roll up node-level SIR scores into per-module totals.
406
+
407
+ Each node's score is weighted by kind (class/interface × 1.2, all others
408
+ × 1.0) before accumulation, so modules that export important types rank
409
+ higher than those containing only utility functions of similar raw score.
410
+
411
+ :param records: Node-level centrality records from
412
+ :meth:`StructuralImportanceRanker.compute`.
413
+ :return: List of ``{module_path, score, rank, member_count}`` dicts,
414
+ sorted descending by aggregated score.
415
+ """
416
+ kind_weight = {"function": 1.0, "method": 1.0, "class": 1.2, "interface": 1.2, "module": 1.0}
417
+ totals: dict[str, float] = defaultdict(float)
418
+ counts: dict[str, int] = defaultdict(int)
419
+
420
+ for rec in records:
421
+ module_key = rec.module_path or "<unknown>"
422
+ totals[module_key] += rec.score * kind_weight.get(rec.kind, 1.0)
423
+ counts[module_key] += 1
424
+
425
+ ranked = sorted(totals.items(), key=lambda item: item[1], reverse=True)
426
+ return [
427
+ {
428
+ "module_path": module_path,
429
+ "score": score,
430
+ "rank": rank,
431
+ "member_count": counts[module_path],
432
+ }
433
+ for rank, (module_path, score) in enumerate(ranked, start=1)
434
+ ]
@@ -0,0 +1 @@
1
+ """TypeScriptKG CLI package."""
@@ -0,0 +1,69 @@
1
+ """
2
+ cli/cmd_analyze.py — tscodekg analyze command: thorough repository analysis.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import click
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command("analyze")
16
+ @click.argument("repo_root", default=".", required=False)
17
+ @click.option(
18
+ "--db",
19
+ default=None,
20
+ type=click.Path(),
21
+ help="SQLite knowledge graph path (default: <repo>/.tscodekg/graph.sqlite).",
22
+ )
23
+ @click.option(
24
+ "--vectors",
25
+ default=None,
26
+ type=click.Path(),
27
+ help="sqlite-vec vector store path (default: <repo>/.tscodekg/vectors.sqlite).",
28
+ )
29
+ @click.option(
30
+ "--report",
31
+ "-o",
32
+ "report_path",
33
+ default=None,
34
+ type=click.Path(),
35
+ help="Markdown report output path (omit to print to stdout).",
36
+ )
37
+ @click.option(
38
+ "--write-centrality",
39
+ is_flag=True,
40
+ help="Persist SIR centrality scores to the centrality_scores table in the SQLite graph.",
41
+ )
42
+ def analyze(
43
+ repo_root: str,
44
+ db: str | None,
45
+ vectors: str | None,
46
+ report_path: str | None,
47
+ write_centrality: bool,
48
+ ) -> None:
49
+ """Run a thorough analysis of a TypeScript/JavaScript repository.
50
+
51
+ Analyzes fan-in/fan-out, module coupling, CodeRank, SIR centrality,
52
+ JSDoc coverage, class/interface hierarchy, and other health signals.
53
+ Outputs a Markdown report.
54
+ """
55
+ from tscode_kg.analysis import TSCodeKGAnalyzer # pylint: disable=import-outside-toplevel
56
+ from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
57
+
58
+ kg = TypeScriptKG(
59
+ repo_root=Path(repo_root).resolve(),
60
+ db_path=db,
61
+ vectors_path=vectors,
62
+ )
63
+ analyzer = TSCodeKGAnalyzer(kg, console=console)
64
+ analyzer.run_analysis(report_path=report_path, persist_centrality=write_centrality)
65
+
66
+ if report_path:
67
+ console.print(f"[green]Report written to {report_path}[/green]")
68
+ else:
69
+ console.print(analyzer.to_markdown())
@@ -0,0 +1,38 @@
1
+ """
2
+ cli/cmd_bridges.py — tscodekg bridges command.
3
+
4
+ Module connectivity (bridge centrality) over the TypeScriptKG graph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+
14
+ @click.command("bridges")
15
+ @click.option(
16
+ "--db",
17
+ type=click.Path(path_type=Path, dir_okay=False),
18
+ default=Path(".tscodekg/graph.sqlite"),
19
+ show_default=True,
20
+ help="Path to the TypeScriptKG SQLite graph.",
21
+ )
22
+ @click.option("--top", type=int, default=25, show_default=True, help="Number of top modules.")
23
+ @click.option("--no-imports", is_flag=True, help="Ignore IMPORTS edges.")
24
+ def bridges(db: Path, top: int, no_imports: bool) -> None:
25
+ """Show top bridge modules by connectivity score."""
26
+ from tscode_kg.bridge import ( # pylint: disable=import-outside-toplevel
27
+ compute_bridge_centrality,
28
+ )
29
+
30
+ ranked = compute_bridge_centrality(
31
+ kind="module",
32
+ include_imports=not no_imports,
33
+ top=top,
34
+ db_path=str(db),
35
+ )
36
+ click.echo(f"Top {top} bridge modules:")
37
+ for mod, score in ranked:
38
+ click.echo(f"{mod:50s} {score:.5f}")