@titan-design/code-graph 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,2413 @@
1
+ // src/store.ts
2
+ import { openDatabase, runMigrations } from "@titan-design/store-sqlite";
3
+
4
+ // src/schema.ts
5
+ import { kitMigration } from "@titan-design/store-sqlite";
6
+ var KIT = { snapshot: "snapshot", entitySnap: "node", cacheBlob: "blob_cache" };
7
+ var KIT_EXTENSIONS = `
8
+ ALTER TABLE node ADD COLUMN language TEXT;
9
+ ALTER TABLE node ADD COLUMN role TEXT;
10
+ ALTER TABLE snapshot ADD COLUMN commit_hash TEXT;
11
+ ALTER TABLE snapshot ADD COLUMN index_version TEXT NOT NULL DEFAULT '';
12
+ `;
13
+ var DOMAIN_DDL = `
14
+ CREATE TABLE IF NOT EXISTS edge (
15
+ snapshot_id INTEGER NOT NULL,
16
+ src_id TEXT NOT NULL,
17
+ dst_id TEXT NOT NULL,
18
+ kind TEXT NOT NULL,
19
+ attrs TEXT,
20
+ PRIMARY KEY (snapshot_id, src_id, dst_id, kind)
21
+ );
22
+ CREATE INDEX IF NOT EXISTS idx_edge_dst ON edge(snapshot_id, dst_id);
23
+ CREATE INDEX IF NOT EXISTS idx_edge_kind ON edge(snapshot_id, kind);
24
+
25
+ CREATE TABLE IF NOT EXISTS metric (
26
+ snapshot_id INTEGER NOT NULL,
27
+ node_id TEXT NOT NULL,
28
+ name TEXT NOT NULL,
29
+ value REAL,
30
+ unit TEXT,
31
+ PRIMARY KEY (snapshot_id, node_id, name)
32
+ );
33
+ CREATE INDEX IF NOT EXISTS idx_metric_name ON metric(snapshot_id, name);
34
+
35
+ CREATE TABLE IF NOT EXISTS id_alias (
36
+ snapshot_id INTEGER NOT NULL,
37
+ old_id TEXT NOT NULL,
38
+ new_id TEXT NOT NULL,
39
+ reason TEXT NOT NULL,
40
+ PRIMARY KEY (snapshot_id, old_id, new_id)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS file_fingerprint (
44
+ snapshot_id INTEGER NOT NULL,
45
+ file_id TEXT NOT NULL,
46
+ content_hash TEXT NOT NULL,
47
+ structural_hash TEXT,
48
+ PRIMARY KEY (snapshot_id, file_id)
49
+ );
50
+ `;
51
+ var MIGRATIONS = [
52
+ kitMigration(1, { snapshot: KIT.snapshot, entitySnap: KIT.entitySnap, cacheBlob: KIT.cacheBlob }),
53
+ { version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
54
+ { version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
55
+ ];
56
+
57
+ // src/rows.ts
58
+ function rowToSnapshot(row) {
59
+ return {
60
+ id: row.id,
61
+ ref: row.ref,
62
+ commitHash: row.commit_hash,
63
+ takenAt: row.taken_at,
64
+ indexVersion: row.index_version,
65
+ attrs: JSON.parse(row.attrs)
66
+ };
67
+ }
68
+ function rowToNode(row) {
69
+ return {
70
+ id: row.id,
71
+ kind: row.kind,
72
+ name: row.name,
73
+ parentId: row.parent_id ?? void 0,
74
+ language: row.language ?? void 0,
75
+ role: row.role ?? void 0,
76
+ attrs: JSON.parse(row.attrs)
77
+ };
78
+ }
79
+ function rowToEdge(row) {
80
+ return {
81
+ srcId: row.src_id,
82
+ dstId: row.dst_id,
83
+ kind: row.kind,
84
+ attrs: JSON.parse(row.attrs)
85
+ };
86
+ }
87
+ function rowToMetric(row) {
88
+ return {
89
+ nodeId: row.node_id,
90
+ name: row.name,
91
+ value: row.value,
92
+ unit: row.unit ?? void 0
93
+ };
94
+ }
95
+ function rowToAlias(row) {
96
+ return {
97
+ oldId: row.old_id,
98
+ newId: row.new_id,
99
+ reason: row.reason
100
+ };
101
+ }
102
+ function rowToFingerprint(row) {
103
+ return {
104
+ fileId: row.file_id,
105
+ contentHash: row.content_hash,
106
+ structuralHash: row.structural_hash ?? void 0
107
+ };
108
+ }
109
+
110
+ // src/store.ts
111
+ var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
112
+ var CodeGraphStore = class {
113
+ constructor(db) {
114
+ this.db = db;
115
+ this.statements = prepareStatements(db);
116
+ }
117
+ db;
118
+ statements;
119
+ createSnapshot(input) {
120
+ const result = this.statements.insertSnapshot.run({
121
+ ref: input.ref,
122
+ commitHash: input.commitHash ?? null,
123
+ indexVersion: input.indexVersion,
124
+ attrs: JSON.stringify(input.attrs ?? {})
125
+ });
126
+ return Number(result.lastInsertRowid);
127
+ }
128
+ insertNodes(snapshotId, nodes) {
129
+ const stmt = this.statements.insertNode;
130
+ this.db.transaction(() => {
131
+ for (const n of nodes) {
132
+ stmt.run({
133
+ snapshotId,
134
+ id: n.id,
135
+ kind: n.kind,
136
+ name: n.name,
137
+ parentId: n.parentId ?? null,
138
+ language: n.language ?? null,
139
+ role: n.role ?? null,
140
+ attrs: JSON.stringify(n.attrs ?? {})
141
+ });
142
+ }
143
+ })();
144
+ }
145
+ insertEdges(snapshotId, edges) {
146
+ const stmt = this.statements.insertEdge;
147
+ this.db.transaction(() => {
148
+ for (const e of edges) {
149
+ stmt.run({
150
+ snapshotId,
151
+ srcId: e.srcId,
152
+ dstId: e.dstId,
153
+ kind: e.kind,
154
+ attrs: JSON.stringify(e.attrs ?? {})
155
+ });
156
+ }
157
+ })();
158
+ }
159
+ insertMetrics(snapshotId, metrics) {
160
+ const stmt = this.statements.insertMetric;
161
+ this.db.transaction(() => {
162
+ for (const m of metrics) {
163
+ stmt.run({ snapshotId, nodeId: m.nodeId, name: m.name, value: m.value, unit: m.unit ?? null });
164
+ }
165
+ })();
166
+ }
167
+ insertAliases(snapshotId, aliases) {
168
+ const stmt = this.statements.insertAlias;
169
+ this.db.transaction(() => {
170
+ for (const a of aliases) {
171
+ stmt.run({ snapshotId, oldId: a.oldId, newId: a.newId, reason: a.reason });
172
+ }
173
+ })();
174
+ }
175
+ insertFingerprints(snapshotId, fingerprints) {
176
+ const stmt = this.statements.insertFingerprint;
177
+ this.db.transaction(() => {
178
+ for (const f of fingerprints) {
179
+ stmt.run({
180
+ snapshotId,
181
+ fileId: f.fileId,
182
+ contentHash: f.contentHash,
183
+ structuralHash: f.structuralHash ?? null
184
+ });
185
+ }
186
+ })();
187
+ }
188
+ getSnapshot(id) {
189
+ const row = this.statements.getSnapshot.get(id);
190
+ return row ? rowToSnapshot(row) : null;
191
+ }
192
+ listSnapshots(opts = {}) {
193
+ const limit = opts.limit ?? 50;
194
+ const rows = opts.ref ? this.statements.listSnapshotsByRef.all(opts.ref, limit) : this.statements.listSnapshots.all(limit);
195
+ return rows.map(rowToSnapshot);
196
+ }
197
+ getLatestSnapshotByRef(ref) {
198
+ return this.listSnapshots({ ref, limit: 1 })[0] ?? null;
199
+ }
200
+ getNode(snapshotId, id) {
201
+ const row = this.statements.getNode.get(snapshotId, id);
202
+ return row ? rowToNode(row) : null;
203
+ }
204
+ /**
205
+ * File-level structural graph by default: the per-symbol layer is excluded so
206
+ * consumers that reason about module structure see the graph they expect. Pass
207
+ * `includeSymbols` for the symbol layer (the reuse basis needs it).
208
+ */
209
+ listNodes(snapshotId, opts) {
210
+ const nodes = this.statements.listNodes.all(snapshotId).map(rowToNode);
211
+ return opts?.includeSymbols ? nodes : nodes.filter((n) => n.kind !== "symbol");
212
+ }
213
+ /** See {@link listNodes}: `references` edges are the symbol layer, excluded by default. */
214
+ listEdges(snapshotId, opts) {
215
+ const edges = this.statements.listEdges.all(snapshotId).map(rowToEdge);
216
+ return opts?.includeReferences ? edges : edges.filter((e) => e.kind !== "references");
217
+ }
218
+ listMetrics(snapshotId) {
219
+ return this.statements.listMetrics.all(snapshotId).map(rowToMetric);
220
+ }
221
+ listAliases(snapshotId) {
222
+ return this.statements.listAliases.all(snapshotId).map(rowToAlias);
223
+ }
224
+ listFingerprints(snapshotId) {
225
+ return this.statements.listFingerprints.all(snapshotId).map(rowToFingerprint);
226
+ }
227
+ close() {
228
+ this.db.close();
229
+ }
230
+ };
231
+ function prepareStatements(db) {
232
+ return {
233
+ insertSnapshot: db.prepare(
234
+ `INSERT INTO snapshot (ref, commit_hash, index_version, attrs)
235
+ VALUES (@ref, @commitHash, @indexVersion, @attrs)`
236
+ ),
237
+ insertNode: db.prepare(
238
+ `INSERT OR REPLACE INTO node (snapshot_id, id, kind, name, parent_id, language, role, attrs)
239
+ VALUES (@snapshotId, @id, @kind, @name, @parentId, @language, @role, @attrs)`
240
+ ),
241
+ insertEdge: db.prepare(
242
+ `INSERT OR REPLACE INTO edge (snapshot_id, src_id, dst_id, kind, attrs)
243
+ VALUES (@snapshotId, @srcId, @dstId, @kind, @attrs)`
244
+ ),
245
+ insertMetric: db.prepare(
246
+ `INSERT OR REPLACE INTO metric (snapshot_id, node_id, name, value, unit)
247
+ VALUES (@snapshotId, @nodeId, @name, @value, @unit)`
248
+ ),
249
+ insertAlias: db.prepare(
250
+ `INSERT OR REPLACE INTO id_alias (snapshot_id, old_id, new_id, reason)
251
+ VALUES (@snapshotId, @oldId, @newId, @reason)`
252
+ ),
253
+ insertFingerprint: db.prepare(
254
+ `INSERT OR REPLACE INTO file_fingerprint (snapshot_id, file_id, content_hash, structural_hash)
255
+ VALUES (@snapshotId, @fileId, @contentHash, @structuralHash)`
256
+ ),
257
+ getSnapshot: db.prepare("SELECT * FROM snapshot WHERE id = ?"),
258
+ listSnapshots: db.prepare("SELECT * FROM snapshot ORDER BY taken_at DESC, id DESC LIMIT ?"),
259
+ listSnapshotsByRef: db.prepare(
260
+ "SELECT * FROM snapshot WHERE ref = ? ORDER BY taken_at DESC, id DESC LIMIT ?"
261
+ ),
262
+ getNode: db.prepare(`SELECT ${NODE_COLS} FROM node WHERE snapshot_id = ? AND id = ?`),
263
+ listNodes: db.prepare(`SELECT ${NODE_COLS} FROM node WHERE snapshot_id = ?`),
264
+ listEdges: db.prepare("SELECT src_id, dst_id, kind, attrs FROM edge WHERE snapshot_id = ?"),
265
+ listMetrics: db.prepare("SELECT node_id, name, value, unit FROM metric WHERE snapshot_id = ?"),
266
+ listAliases: db.prepare("SELECT old_id, new_id, reason FROM id_alias WHERE snapshot_id = ?"),
267
+ listFingerprints: db.prepare(
268
+ "SELECT file_id, content_hash, structural_hash FROM file_fingerprint WHERE snapshot_id = ?"
269
+ )
270
+ };
271
+ }
272
+ function openCodeGraph(dbPath) {
273
+ const db = openDatabase(dbPath);
274
+ runMigrations(db, MIGRATIONS);
275
+ return new CodeGraphStore(db);
276
+ }
277
+
278
+ // src/indexer.ts
279
+ import { realpathSync } from "fs";
280
+ import * as path10 from "path";
281
+
282
+ // src/parser/parser.ts
283
+ import { Parser, Language } from "web-tree-sitter";
284
+ import { createRequire } from "module";
285
+ var require2 = createRequire(import.meta.url);
286
+ var initialized = false;
287
+ var parsers = /* @__PURE__ */ new Map();
288
+ var languages = /* @__PURE__ */ new Map();
289
+ function resolveWasmPath(language) {
290
+ switch (language) {
291
+ case "typescript":
292
+ return require2.resolve("tree-sitter-typescript/tree-sitter-typescript.wasm");
293
+ case "tsx":
294
+ return require2.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
295
+ case "python":
296
+ return require2.resolve("tree-sitter-python/tree-sitter-python.wasm");
297
+ default:
298
+ throw new Error(`Unsupported language: ${language}`);
299
+ }
300
+ }
301
+ async function ensureInitialized() {
302
+ if (initialized) return;
303
+ await Parser.init();
304
+ initialized = true;
305
+ }
306
+ async function getLanguage(language) {
307
+ const cached = languages.get(language);
308
+ if (cached) return cached;
309
+ const wasmPath = resolveWasmPath(language);
310
+ const lang = await Language.load(wasmPath);
311
+ languages.set(language, lang);
312
+ return lang;
313
+ }
314
+ async function getParser(language) {
315
+ const cached = parsers.get(language);
316
+ if (cached) return cached;
317
+ resolveWasmPath(language);
318
+ await ensureInitialized();
319
+ const lang = await getLanguage(language);
320
+ const parser = new Parser();
321
+ parser.setLanguage(lang);
322
+ parsers.set(language, parser);
323
+ return parser;
324
+ }
325
+ async function parseFile(content, filePath, language) {
326
+ const parser = await getParser(language);
327
+ const tree = parser.parse(content);
328
+ if (!tree) {
329
+ throw new Error(`Failed to parse ${filePath}`);
330
+ }
331
+ return { tree, content, filePath, language };
332
+ }
333
+ function getSupportedLanguages() {
334
+ return ["typescript", "tsx", "python"];
335
+ }
336
+
337
+ // src/parser/file-filter.ts
338
+ import * as path from "path";
339
+ var EXCLUDED_DIRS = [
340
+ "node_modules",
341
+ "vendor",
342
+ "dist",
343
+ "build",
344
+ ".next",
345
+ "__generated__",
346
+ ".git",
347
+ ".claude",
348
+ "coverage"
349
+ ];
350
+ var EXCLUDED_PATTERNS = [
351
+ /\.min\.[jt]sx?$/,
352
+ /\.d\.ts$/,
353
+ /\.map$/,
354
+ /lock\.(json|yaml)$/,
355
+ /pnpm-lock\.yaml$/,
356
+ /package-lock\.json$/,
357
+ /yarn\.lock$/
358
+ ];
359
+ var LANGUAGE_EXTENSIONS = {
360
+ typescript: [".ts", ".tsx"],
361
+ javascript: [".js", ".jsx"],
362
+ python: [".py"]
363
+ };
364
+ function isExcludedDir(dirName) {
365
+ return EXCLUDED_DIRS.includes(dirName);
366
+ }
367
+ function shouldIncludeFile(filePath, languages2) {
368
+ const segments = filePath.split("/");
369
+ if (segments.some((s) => EXCLUDED_DIRS.includes(s))) {
370
+ return false;
371
+ }
372
+ if (EXCLUDED_PATTERNS.some((p) => p.test(filePath))) {
373
+ return false;
374
+ }
375
+ const ext = path.extname(filePath);
376
+ const allowedExtensions = languages2.flatMap(
377
+ (lang) => LANGUAGE_EXTENSIONS[lang] ?? []
378
+ );
379
+ return allowedExtensions.includes(ext);
380
+ }
381
+ function getLanguageFromPath(filePath) {
382
+ const ext = path.extname(filePath);
383
+ for (const [language, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
384
+ if (extensions.includes(ext)) {
385
+ return language;
386
+ }
387
+ }
388
+ return null;
389
+ }
390
+
391
+ // src/extractors/ts-morph-extractor.ts
392
+ import * as path5 from "path";
393
+ import { existsSync } from "fs";
394
+ import {
395
+ Project,
396
+ ScriptTarget,
397
+ ModuleKind,
398
+ ModuleResolutionKind
399
+ } from "ts-morph";
400
+
401
+ // src/extractors/file-nodes.ts
402
+ import * as path3 from "path";
403
+
404
+ // src/extractors/ids.ts
405
+ import * as path2 from "path";
406
+ var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
407
+ function toPosix(p) {
408
+ return p.split(path2.sep).join("/");
409
+ }
410
+ function repoRelative(repoRoot, absPath) {
411
+ const rel = path2.relative(repoRoot, absPath);
412
+ return toPosix(rel);
413
+ }
414
+ function fileId(repoRoot, absPath) {
415
+ return repoRelative(repoRoot, absPath);
416
+ }
417
+ function moduleId(repoRoot, absPath) {
418
+ const rel = repoRelative(repoRoot, absPath);
419
+ return rel.replace(MODULE_EXT_RE, "");
420
+ }
421
+ function parentModuleId(id) {
422
+ const idx = id.lastIndexOf("/");
423
+ if (idx < 0) return null;
424
+ return id.slice(0, idx);
425
+ }
426
+ function packageId(name) {
427
+ return name;
428
+ }
429
+ var SYMBOL_ID_SEP = "#";
430
+ function symbolId(fileId2, exportName) {
431
+ return `${fileId2}${SYMBOL_ID_SEP}${exportName}`;
432
+ }
433
+ function parseSymbolId(id) {
434
+ const idx = id.indexOf(SYMBOL_ID_SEP);
435
+ if (idx < 0) return null;
436
+ return { fileId: id.slice(0, idx), name: id.slice(idx + 1) };
437
+ }
438
+ function externalId(specifier) {
439
+ if (specifier.startsWith("node:")) {
440
+ return specifier;
441
+ }
442
+ return `npm:${bareName(specifier)}`;
443
+ }
444
+ function bareName(specifier) {
445
+ if (specifier.startsWith("@")) {
446
+ const parts = specifier.split("/");
447
+ return parts.slice(0, 2).join("/");
448
+ }
449
+ const slash = specifier.indexOf("/");
450
+ return slash < 0 ? specifier : specifier.slice(0, slash);
451
+ }
452
+
453
+ // src/extractors/file-nodes.ts
454
+ function buildFileModuleNodes(repoRoot, absPath) {
455
+ const fId = fileId(repoRoot, absPath);
456
+ const mId = moduleId(repoRoot, absPath);
457
+ const parentId = parentModuleId(mId) ?? void 0;
458
+ const language = getLanguageFromPath(absPath) ?? "typescript";
459
+ return [
460
+ { id: fId, kind: "file", name: path3.basename(fId), parentId: mId, language },
461
+ { id: mId, kind: "module", name: path3.basename(mId), parentId, language }
462
+ ];
463
+ }
464
+
465
+ // src/extractors/reference-weight.ts
466
+ import {
467
+ Node,
468
+ SyntaxKind
469
+ } from "ts-morph";
470
+ function addWeightedEdge(agg, srcId, dstId, kind, specifier, weight) {
471
+ const key = JSON.stringify([kind, dstId]);
472
+ const existing = agg.get(key);
473
+ if (existing) {
474
+ existing.attrs.weight += weight;
475
+ return;
476
+ }
477
+ agg.set(key, { srcId, dstId, kind, attrs: { specifier, weight } });
478
+ }
479
+ function importWeight(decl, usage) {
480
+ const names = importBindingNames(decl);
481
+ if (names.length === 0) return 1;
482
+ const used = names.reduce((sum, n) => sum + (usage.get(n) ?? 0), 0);
483
+ return Math.max(used, 1);
484
+ }
485
+ function namedImportBindings(decl) {
486
+ const out = [];
487
+ const def = decl.getDefaultImport();
488
+ if (def) out.push({ importedName: "default", localName: def.getText() });
489
+ for (const spec of decl.getNamedImports()) {
490
+ out.push({
491
+ importedName: spec.getNameNode().getText(),
492
+ localName: (spec.getAliasNode() ?? spec.getNameNode()).getText()
493
+ });
494
+ }
495
+ return out;
496
+ }
497
+ function bindingWeight(binding, usage) {
498
+ return Math.max(usage.get(binding.localName) ?? 0, 1);
499
+ }
500
+ function importBindingNames(decl) {
501
+ const names = [];
502
+ const def = decl.getDefaultImport();
503
+ if (def) names.push(def.getText());
504
+ const ns = decl.getNamespaceImport();
505
+ if (ns) names.push(ns.getText());
506
+ for (const spec of decl.getNamedImports()) {
507
+ names.push((spec.getAliasNode() ?? spec.getNameNode()).getText());
508
+ }
509
+ return names;
510
+ }
511
+ function reExportWeight(decl) {
512
+ if (decl.isNamespaceExport()) return 1;
513
+ return Math.max(decl.getNamedExports().length, 1);
514
+ }
515
+ function buildLocalUsageCounts(sourceFile) {
516
+ const counts = /* @__PURE__ */ new Map();
517
+ for (const id of sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)) {
518
+ if (id.getFirstAncestorByKind(SyntaxKind.ImportDeclaration)) continue;
519
+ if (id.getFirstAncestorByKind(SyntaxKind.ExportDeclaration)) continue;
520
+ if (isPropertyNamePosition(id)) continue;
521
+ const text = id.getText();
522
+ counts.set(text, (counts.get(text) ?? 0) + 1);
523
+ }
524
+ return counts;
525
+ }
526
+ function isPropertyNamePosition(id) {
527
+ const parent = id.getParent();
528
+ if (Node.isPropertyAccessExpression(parent) && parent.getNameNode() === id) {
529
+ return true;
530
+ }
531
+ if (Node.isPropertyAssignment(parent) && parent.getNameNode() === id) {
532
+ return true;
533
+ }
534
+ return false;
535
+ }
536
+
537
+ // src/declared-names.ts
538
+ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
539
+ "function_declaration",
540
+ "method_definition",
541
+ "class_declaration"
542
+ ]);
543
+ var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
544
+ function collectDeclaredSpans(file) {
545
+ const declTypes = file.language === "python" ? PY_DECL_TYPES : TS_DECL_TYPES;
546
+ const spans = /* @__PURE__ */ new Map();
547
+ const visit = (node) => {
548
+ const named = declaredNodeAt(node, declTypes);
549
+ if (named) {
550
+ spans.set(named.name, {
551
+ startLine: named.node.startPosition.row + 1,
552
+ endLine: named.node.endPosition.row + 1
553
+ });
554
+ }
555
+ for (const child of node.children) {
556
+ if (child) visit(child);
557
+ }
558
+ };
559
+ visit(file.tree.rootNode);
560
+ return spans;
561
+ }
562
+ function collectDeclaredNames(file) {
563
+ return new Set(collectDeclaredSpans(file).keys());
564
+ }
565
+ function declaredNodeAt(node, declTypes) {
566
+ if (declTypes.has(node.type)) {
567
+ const name = node.childForFieldName("name")?.text;
568
+ return name ? { name, node } : null;
569
+ }
570
+ if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
571
+ const name = node.parent.childForFieldName("name")?.text;
572
+ return name ? { name, node } : null;
573
+ }
574
+ return null;
575
+ }
576
+
577
+ // src/extractors/symbol-signature.ts
578
+ import {
579
+ Node as Node2
580
+ } from "ts-morph";
581
+ var MAX_SIGNATURE = 200;
582
+ var MAX_PURPOSE = 240;
583
+ function declarationText(decl) {
584
+ const out = {};
585
+ const signature = signatureOf(decl);
586
+ if (signature) out.signature = signature;
587
+ const purpose = purposeOf(decl);
588
+ if (purpose) out.purpose = purpose;
589
+ return out;
590
+ }
591
+ function lookupDeclaration(sf, name) {
592
+ return sf.getFunction(name) ?? sf.getClass(name) ?? sf.getInterface(name) ?? sf.getTypeAlias(name) ?? sf.getEnum(name) ?? sf.getVariableDeclaration(name);
593
+ }
594
+ function signatureOf(decl) {
595
+ if (Node2.isFunctionDeclaration(decl) || Node2.isMethodDeclaration(decl)) {
596
+ return callableSignature(decl, decl.getName() ?? "");
597
+ }
598
+ if (Node2.isVariableDeclaration(decl)) return variableSignature(decl);
599
+ if (Node2.isClassDeclaration(decl)) return clamp(`class ${decl.getName() ?? ""}`.trim());
600
+ if (Node2.isInterfaceDeclaration(decl)) return clamp(`interface ${decl.getName()}`);
601
+ if (Node2.isTypeAliasDeclaration(decl)) {
602
+ return clamp(`type ${decl.getName()} = ${oneLine(decl.getTypeNode()?.getText() ?? "")}`);
603
+ }
604
+ if (Node2.isEnumDeclaration(decl)) return clamp(`enum ${decl.getName()}`);
605
+ return void 0;
606
+ }
607
+ function variableSignature(decl) {
608
+ const init = decl.getInitializer();
609
+ if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
610
+ return callableSignature(init, decl.getName());
611
+ }
612
+ const typeNode = decl.getTypeNode();
613
+ const type = typeNode ? oneLine(typeNode.getText()) : cleanType(() => decl.getType().getText());
614
+ return type ? clamp(`${decl.getName()}: ${type}`) : void 0;
615
+ }
616
+ function callableSignature(decl, name) {
617
+ const params = decl.getParameters().map((p) => oneLine(p.getText())).join(", ");
618
+ const ret = returnType(decl);
619
+ return clamp(`${name}(${params})${ret ? `: ${ret}` : ""}`);
620
+ }
621
+ function returnType(decl) {
622
+ const node = decl.getReturnTypeNode();
623
+ if (node) return oneLine(node.getText());
624
+ return cleanType(() => decl.getReturnType().getText());
625
+ }
626
+ function cleanType(getText) {
627
+ let text;
628
+ try {
629
+ text = getText();
630
+ } catch {
631
+ return void 0;
632
+ }
633
+ if (!text || text.includes("import(")) return void 0;
634
+ return oneLine(text);
635
+ }
636
+ function purposeOf(decl) {
637
+ const docs = jsDocsFor(decl);
638
+ if (!docs || docs.length === 0) return void 0;
639
+ const desc = docs[docs.length - 1].getDescription().trim();
640
+ return desc ? clamp(oneLine(desc), MAX_PURPOSE) : void 0;
641
+ }
642
+ function jsDocsFor(decl) {
643
+ if (Node2.isJSDocable(decl)) return decl.getJsDocs();
644
+ if (Node2.isVariableDeclaration(decl)) return decl.getVariableStatement()?.getJsDocs();
645
+ return void 0;
646
+ }
647
+ function oneLine(s) {
648
+ return s.replace(/\s+/g, " ").trim();
649
+ }
650
+ function clamp(s, max = MAX_SIGNATURE) {
651
+ return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
652
+ }
653
+
654
+ // src/extractors/symbol-nodes.ts
655
+ function makeSymbolNode(fileId2, name, exported, span, text) {
656
+ const attrs = { exported };
657
+ if (span) {
658
+ attrs.startLine = span.startLine;
659
+ attrs.endLine = span.endLine;
660
+ }
661
+ if (text.signature) attrs.signature = text.signature;
662
+ if (text.purpose) attrs.purpose = text.purpose;
663
+ return {
664
+ id: symbolId(fileId2, name),
665
+ kind: "symbol",
666
+ name,
667
+ parentId: fileId2,
668
+ language: "typescript",
669
+ attrs
670
+ };
671
+ }
672
+ function buildSymbolNodes(repoRoot, sourceFile, file) {
673
+ const fId = fileId(repoRoot, sourceFile.getFilePath());
674
+ const spans = collectDeclaredSpans(file);
675
+ const out = [];
676
+ const exported = /* @__PURE__ */ new Set();
677
+ for (const [name, decls] of sourceFile.getExportedDeclarations()) {
678
+ const own = decls.find((d) => d.getSourceFile() === sourceFile);
679
+ if (!own) continue;
680
+ exported.add(name);
681
+ out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own)));
682
+ }
683
+ for (const [name, span] of spans) {
684
+ if (exported.has(name)) continue;
685
+ const decl = lookupDeclaration(sourceFile, name);
686
+ out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}));
687
+ }
688
+ return out;
689
+ }
690
+
691
+ // src/extractors/dynamic-imports.ts
692
+ import {
693
+ Node as Node3,
694
+ SyntaxKind as SyntaxKind2
695
+ } from "ts-morph";
696
+ function dynamicImportSpecifiers(sourceFile) {
697
+ const out = [];
698
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression)) {
699
+ if (call.getExpression().getKind() !== SyntaxKind2.ImportKeyword) continue;
700
+ const arg = call.getArguments()[0];
701
+ if (arg && arg.getKind() === SyntaxKind2.StringLiteral) {
702
+ out.push(arg.asKindOrThrow(SyntaxKind2.StringLiteral).getLiteralText());
703
+ }
704
+ }
705
+ return out;
706
+ }
707
+ function dynamicImportBindings(sourceFile) {
708
+ const out = [];
709
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression)) {
710
+ if (call.getExpression().getKind() !== SyntaxKind2.ImportKeyword) continue;
711
+ const arg = call.getArguments()[0];
712
+ if (!arg || arg.getKind() !== SyntaxKind2.StringLiteral) continue;
713
+ const pattern = destructuredTargetOf(call);
714
+ if (!pattern) continue;
715
+ const specifier = arg.asKindOrThrow(SyntaxKind2.StringLiteral).getLiteralText();
716
+ for (const importedName of destructuredNames(pattern)) {
717
+ out.push({ specifier, importedName });
718
+ }
719
+ }
720
+ return out;
721
+ }
722
+ function destructuredTargetOf(call) {
723
+ const parent = call.getParent();
724
+ if (!parent) return void 0;
725
+ const init = Node3.isAwaitExpression(parent) ? parent : call;
726
+ const decl = init.getParent();
727
+ if (!decl || !Node3.isVariableDeclaration(decl)) return void 0;
728
+ if (decl.getInitializer() !== init) return void 0;
729
+ const nameNode = decl.getNameNode();
730
+ return Node3.isObjectBindingPattern(nameNode) ? nameNode : void 0;
731
+ }
732
+ function destructuredNames(pattern) {
733
+ const names = [];
734
+ for (const el of pattern.getElements()) {
735
+ if (el.getDotDotDotToken()) continue;
736
+ const nameNode = el.getPropertyNameNode() ?? el.getNameNode();
737
+ names.push(nameNode.getText());
738
+ }
739
+ return names;
740
+ }
741
+ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifier) {
742
+ for (const binding of dynamicImportBindings(sourceFile)) {
743
+ const targetFileId2 = resolveSpecifier(binding.specifier);
744
+ if (!targetFileId2) continue;
745
+ addWeightedEdge(
746
+ agg,
747
+ srcFileId,
748
+ symbolId(targetFileId2, binding.importedName),
749
+ "references",
750
+ binding.specifier,
751
+ 1
752
+ );
753
+ }
754
+ }
755
+
756
+ // src/extractors/module-resolution.ts
757
+ import * as path4 from "path";
758
+ var RESOLVABLE_EXTS = [
759
+ ".ts",
760
+ ".tsx",
761
+ ".mts",
762
+ ".cts",
763
+ ".js",
764
+ ".jsx",
765
+ ".mjs",
766
+ ".cjs"
767
+ ];
768
+ function isRelativeSpecifier(specifier) {
769
+ return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
770
+ }
771
+ function* relativeResolutionCandidates(base) {
772
+ yield base;
773
+ for (const ext of RESOLVABLE_EXTS) yield base + ext;
774
+ const jsExt = /\.(?:jsx?|mjs|cjs)$/.exec(base);
775
+ if (jsExt) {
776
+ const stem = base.slice(0, base.length - jsExt[0].length);
777
+ for (const ext of RESOLVABLE_EXTS) yield stem + ext;
778
+ }
779
+ for (const ext of RESOLVABLE_EXTS) yield path4.join(base, "index" + ext);
780
+ }
781
+ function resolveRelativeAbs(fs3, fromAbs, specifier) {
782
+ if (!isRelativeSpecifier(specifier)) return null;
783
+ const base = path4.resolve(path4.dirname(fromAbs), specifier);
784
+ for (const candidate of relativeResolutionCandidates(base)) {
785
+ if (fs3.fileExistsSync(candidate)) return candidate;
786
+ }
787
+ return null;
788
+ }
789
+ function inRepoFileId(repoRoot, abs) {
790
+ const relative4 = path4.relative(repoRoot, abs);
791
+ if (relative4.startsWith("..") || path4.isAbsolute(relative4)) return null;
792
+ if (relative4.split(path4.sep).includes("node_modules")) return null;
793
+ return fileId(repoRoot, abs);
794
+ }
795
+
796
+ // src/extractors/reexport-resolve.ts
797
+ function resolveReExportOrigin(ctx, importerAbs, specifier, name) {
798
+ const fs3 = ctx.project.getFileSystem();
799
+ const targetAbs = resolveRelativeAbs(fs3, importerAbs, specifier);
800
+ return targetAbs ? walkReExports(ctx, targetAbs, name, /* @__PURE__ */ new Set()) : null;
801
+ }
802
+ function walkReExports(ctx, fileAbs, name, visited) {
803
+ if (visited.has(fileAbs)) return null;
804
+ visited.add(fileAbs);
805
+ const sf = ctx.project.getSourceFile(fileAbs) ?? ctx.project.addSourceFileAtPathIfExists(fileAbs);
806
+ if (!sf) return null;
807
+ if (declaresExportLocally(sf, name)) {
808
+ const id = inRepoFileId(ctx.repoRoot, fileAbs);
809
+ return id ? symbolId(id, name) : null;
810
+ }
811
+ const fs3 = ctx.project.getFileSystem();
812
+ for (const ed of sf.getExportDeclarations()) {
813
+ const spec = ed.getModuleSpecifierValue();
814
+ if (!spec) continue;
815
+ const inward = reExportInwardName(ed, name);
816
+ if (inward === null) continue;
817
+ const targetAbs = resolveRelativeAbs(fs3, fileAbs, spec);
818
+ if (!targetAbs) continue;
819
+ const origin = walkReExports(ctx, targetAbs, inward, visited);
820
+ if (origin) return origin;
821
+ }
822
+ return null;
823
+ }
824
+ function declaresExportLocally(sf, name) {
825
+ const decls = sf.getExportedDeclarations().get(name);
826
+ return !!decls && decls.some((d) => d.getSourceFile() === sf);
827
+ }
828
+ function reExportInwardName(ed, name) {
829
+ const named = ed.getNamedExports();
830
+ if (named.length > 0) {
831
+ for (const spec of named) {
832
+ const outward = spec.getAliasNode()?.getText() ?? spec.getName();
833
+ if (outward === name) return spec.getName();
834
+ }
835
+ return null;
836
+ }
837
+ if (ed.getNamespaceExport()) return null;
838
+ return name;
839
+ }
840
+
841
+ // src/extractors/ts-morph-extractor.ts
842
+ var TsMorphGraphExtractor = class {
843
+ name = "ts-morph-graph";
844
+ repoRoot;
845
+ tsConfigPath;
846
+ project;
847
+ constructor(options) {
848
+ this.repoRoot = options.repoRoot;
849
+ this.tsConfigPath = options.tsConfigPath;
850
+ this.project = options.project;
851
+ }
852
+ extract(file) {
853
+ if (!isTypeScriptFile(file)) return [];
854
+ const project = this.ensureProject();
855
+ const sourceFile = this.loadSourceFile(project, file);
856
+ const nodes = this.buildFileAndModuleNodes(sourceFile);
857
+ const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
858
+ const { edges, externalNodes } = this.collectEdges(sourceFile);
859
+ return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges }];
860
+ }
861
+ ensureProject() {
862
+ if (this.project) return this.project;
863
+ this.project = this.tsConfigPath ? new Project({ tsConfigFilePath: this.tsConfigPath }) : new Project({
864
+ compilerOptions: {
865
+ allowJs: true,
866
+ target: ScriptTarget.ESNext,
867
+ module: ModuleKind.ESNext,
868
+ moduleResolution: ModuleResolutionKind.NodeNext
869
+ }
870
+ });
871
+ return this.project;
872
+ }
873
+ loadSourceFile(project, file) {
874
+ const existing = project.getSourceFile(file.filePath);
875
+ if (existing) {
876
+ if (existing.getFullText() !== file.content) {
877
+ existing.replaceWithText(file.content);
878
+ }
879
+ return existing;
880
+ }
881
+ return project.createSourceFile(file.filePath, file.content, {
882
+ overwrite: true
883
+ });
884
+ }
885
+ buildFileAndModuleNodes(sourceFile) {
886
+ return buildFileModuleNodes(this.repoRoot, sourceFile.getFilePath());
887
+ }
888
+ collectEdges(sourceFile) {
889
+ const c = {
890
+ srcAbs: sourceFile.getFilePath(),
891
+ srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
892
+ usage: buildLocalUsageCounts(sourceFile),
893
+ agg: /* @__PURE__ */ new Map(),
894
+ externalNodes: [],
895
+ seenExternals: /* @__PURE__ */ new Set()
896
+ };
897
+ for (const decl of sourceFile.getImportDeclarations()) {
898
+ this.recordImportEdge(c, decl);
899
+ }
900
+ for (const decl of sourceFile.getExportDeclarations()) {
901
+ this.recordReExportEdge(c, decl);
902
+ }
903
+ for (const specifier of dynamicImportSpecifiers(sourceFile)) {
904
+ const dstId = this.resolveRelativeInternal(c.srcAbs, specifier);
905
+ if (dstId) addWeightedEdge(c.agg, c.srcFileId, dstId, "imports", specifier, 1);
906
+ }
907
+ recordDynamicSymbolRefEdges(
908
+ sourceFile,
909
+ c.srcFileId,
910
+ c.agg,
911
+ (s) => this.resolveRelativeInternal(c.srcAbs, s)
912
+ );
913
+ return { edges: [...c.agg.values()], externalNodes: c.externalNodes };
914
+ }
915
+ recordImportEdge(c, decl) {
916
+ const specifier = decl.getModuleSpecifierValue();
917
+ const dstId = this.resolveTarget(c, specifier, decl);
918
+ if (dstId) {
919
+ addWeightedEdge(c.agg, c.srcFileId, dstId, "imports", specifier, importWeight(decl, c.usage));
920
+ }
921
+ this.recordSymbolReferences(c, decl);
922
+ }
923
+ /**
924
+ * Split an import into per-export `references` edges targeting `symbol` nodes,
925
+ * so per-symbol utilization (which exports are hot) falls out of the same
926
+ * inbound-weight sum that powers file utilization (C-53). Extracted *forward*
927
+ * from the importing file — source-local, exactly like the file-level import
928
+ * weight — which sidesteps the reuse-breaking reverse `findReferences` query:
929
+ * an unchanged file's outbound reference edges are carried forward verbatim.
930
+ */
931
+ recordSymbolReferences(c, decl) {
932
+ const specifier = decl.getModuleSpecifierValue();
933
+ for (const binding of namedImportBindings(decl)) {
934
+ const dstId = this.resolveSymbolTarget(c, decl, binding.importedName);
935
+ if (!dstId) continue;
936
+ addWeightedEdge(
937
+ c.agg,
938
+ c.srcFileId,
939
+ dstId,
940
+ "references",
941
+ specifier,
942
+ bindingWeight(binding, c.usage)
943
+ );
944
+ }
945
+ }
946
+ /**
947
+ * Resolve an imported name to the id of the `symbol` node for the export that
948
+ * actually *declares* it — following re-exports through barrels via ts-morph's
949
+ * own `getExportedDeclarations`, so `import { x } from "./barrel"` credits the
950
+ * origin file's `x`, not the barrel. When ts-morph can't resolve the specifier
951
+ * (e.g. an extensionless relative import), falls back to a filesystem-based
952
+ * re-export walk that still sees through barrel hops (C-70). Returns null for
953
+ * external / unresolved targets.
954
+ * A name that resolves to no local declaration (an aliased re-export the
955
+ * origin doesn't export under this name) yields a dangling edge, pruned after
956
+ * assembly (`pruneDanglingReferences`).
957
+ */
958
+ resolveSymbolTarget(c, decl, importedName) {
959
+ const targetSf = decl.getModuleSpecifierSourceFile();
960
+ if (targetSf) {
961
+ const decls = targetSf.getExportedDeclarations().get(importedName);
962
+ const originSf = decls && decls.length > 0 ? decls[0].getSourceFile() : targetSf;
963
+ const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath()));
964
+ return originId ? symbolId(originId, importedName) : null;
965
+ }
966
+ return resolveReExportOrigin(
967
+ { project: this.ensureProject(), repoRoot: this.repoRoot },
968
+ c.srcAbs,
969
+ decl.getModuleSpecifierValue(),
970
+ importedName
971
+ );
972
+ }
973
+ recordReExportEdge(c, decl) {
974
+ if (!decl.hasModuleSpecifier()) return;
975
+ const specifier = decl.getModuleSpecifierValue();
976
+ if (!specifier) return;
977
+ const dstId = this.resolveTarget(c, specifier, decl);
978
+ if (dstId) {
979
+ addWeightedEdge(c.agg, c.srcFileId, dstId, "re-exports", specifier, reExportWeight(decl));
980
+ }
981
+ }
982
+ /**
983
+ * Resolve an import/export specifier to a graph node id (an in-repo file id or
984
+ * an `external` node, creating the external node on first sight), or `null`
985
+ * when it should not produce an edge.
986
+ */
987
+ resolveTarget(c, specifier, decl) {
988
+ const internal = this.resolveInternal(decl.getModuleSpecifierSourceFile()) ?? this.resolveRelativeInternal(c.srcAbs, specifier);
989
+ if (internal) return internal;
990
+ if (isRelativeSpecifier(specifier)) {
991
+ return null;
992
+ }
993
+ const extId = externalId(specifier);
994
+ if (!c.seenExternals.has(extId)) {
995
+ c.seenExternals.add(extId);
996
+ c.externalNodes.push({ id: extId, kind: "external", name: specifier });
997
+ }
998
+ return extId;
999
+ }
1000
+ resolveInternal(target) {
1001
+ if (!target) return null;
1002
+ return this.inRepoFileId(remapDistToSrc(target.getFilePath()));
1003
+ }
1004
+ /**
1005
+ * Resolve a relative specifier ts-morph failed to link by walking the
1006
+ * filesystem from the importing file. ts-morph's NodeNext resolution only
1007
+ * links relative imports that carry an explicit `.js` extension, so
1008
+ * extensionless bundler-style imports (`../types`, common in code outside the
1009
+ * tsconfig project such as `dashboard/`) resolve to nothing and would
1010
+ * otherwise fall through to the `npm:` external bucket (C-44). Resolves
1011
+ * through the ts-morph filesystem host so it honours in-memory test fixtures.
1012
+ */
1013
+ resolveRelativeInternal(srcAbs, specifier) {
1014
+ const abs = resolveRelativeAbs(
1015
+ this.ensureProject().getFileSystem(),
1016
+ srcAbs,
1017
+ specifier
1018
+ );
1019
+ return abs ? inRepoFileId(this.repoRoot, abs) : null;
1020
+ }
1021
+ inRepoFileId(abs) {
1022
+ return inRepoFileId(this.repoRoot, abs);
1023
+ }
1024
+ };
1025
+ function remapDistToSrc(abs) {
1026
+ const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
1027
+ if (!m) return abs;
1028
+ const base = m[1];
1029
+ const sub = m[2];
1030
+ for (const ext of [".ts", ".tsx"]) {
1031
+ const candidate = path5.join(base, "src", sub + ext);
1032
+ if (existsSync(candidate)) return candidate;
1033
+ }
1034
+ return abs;
1035
+ }
1036
+ function isTypeScriptFile(file) {
1037
+ return file.language === "typescript" || file.language === "tsx";
1038
+ }
1039
+
1040
+ // src/extractors/python-extractor.ts
1041
+ import * as path6 from "path";
1042
+ import { existsSync as existsSync2 } from "fs";
1043
+ function* candidates(base) {
1044
+ yield `${base}.py`;
1045
+ yield path6.join(base, "__init__.py");
1046
+ }
1047
+ var PythonGraphExtractor = class {
1048
+ constructor(repoRoot) {
1049
+ this.repoRoot = repoRoot;
1050
+ }
1051
+ repoRoot;
1052
+ name = "python-graph";
1053
+ extract(file) {
1054
+ if (file.language !== "python") return [];
1055
+ const fId = fileId(this.repoRoot, file.filePath);
1056
+ const nodes = buildFileModuleNodes(this.repoRoot, file.filePath);
1057
+ nodes.push(...symbolNodes(fId, file));
1058
+ const { edges, externals } = this.collectImports(fId, file);
1059
+ return [{ nodes: [...nodes, ...externals], edges }];
1060
+ }
1061
+ collectImports(srcId, file) {
1062
+ const edges = /* @__PURE__ */ new Map();
1063
+ const externals = [];
1064
+ for (const specifier of importSpecifiers(file.tree.rootNode)) {
1065
+ const internal = this.resolveDotted(file.filePath, specifier);
1066
+ const dstId = internal ?? externalId(specifier);
1067
+ if (!internal && !edges.has(dstId)) {
1068
+ externals.push({ id: dstId, kind: "external", name: specifier });
1069
+ }
1070
+ edges.set(dstId, { srcId, dstId, kind: "imports", attrs: { specifier, weight: 1 } });
1071
+ }
1072
+ return { edges: [...edges.values()], externals };
1073
+ }
1074
+ /**
1075
+ * Resolve `a.b.c` (or a leading-dot relative import) to an in-repo file id by
1076
+ * walking the dotted path from the importing file's package and then from the
1077
+ * repo root, which covers both intra-package and root-relative layouts.
1078
+ */
1079
+ resolveDotted(fromAbs, specifier) {
1080
+ const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
1081
+ const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
1082
+ const relativeBase = path6.resolve(path6.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1083
+ const roots = leadingDots > 0 ? [relativeBase] : [path6.dirname(fromAbs), this.repoRoot];
1084
+ for (const root of roots) {
1085
+ for (const candidate of candidates(path6.join(root, ...parts))) {
1086
+ const id = inRepoFileId(this.repoRoot, candidate);
1087
+ if (id !== null && existsSync2(candidate)) return id;
1088
+ }
1089
+ }
1090
+ return null;
1091
+ }
1092
+ };
1093
+ function symbolNodes(fId, file) {
1094
+ return [...collectDeclaredSpans(file)].map(([name, span]) => ({
1095
+ id: symbolId(fId, name),
1096
+ kind: "symbol",
1097
+ name,
1098
+ parentId: fId,
1099
+ language: "python",
1100
+ attrs: { exported: !name.startsWith("_"), startLine: span.startLine, endLine: span.endLine }
1101
+ }));
1102
+ }
1103
+ function importSpecifiers(root) {
1104
+ const out = [];
1105
+ const visit = (node) => {
1106
+ if (node.type === "import_statement" || node.type === "import_from_statement") {
1107
+ const name = node.childForFieldName("module_name");
1108
+ if (name) out.push(name.text);
1109
+ else for (const child of node.namedChildren) if (child?.type === "dotted_name") out.push(child.text);
1110
+ }
1111
+ for (const child of node.namedChildren) if (child) visit(child);
1112
+ };
1113
+ visit(root);
1114
+ return out;
1115
+ }
1116
+
1117
+ // src/extractors/dispatch.ts
1118
+ var LanguageExtractor = class {
1119
+ name = "language-graph";
1120
+ delegates;
1121
+ constructor(options) {
1122
+ this.delegates = [
1123
+ new TsMorphGraphExtractor({ repoRoot: options.repoRoot, tsConfigPath: options.tsConfigPath }),
1124
+ new PythonGraphExtractor(options.repoRoot)
1125
+ ];
1126
+ }
1127
+ extract(file) {
1128
+ return this.delegates.flatMap((delegate) => delegate.extract(file));
1129
+ }
1130
+ };
1131
+
1132
+ // src/git-renames.ts
1133
+ import { execFileSync } from "child_process";
1134
+ import * as path7 from "path";
1135
+ function detectGitHead(repoRoot) {
1136
+ return runGit(repoRoot, ["rev-parse", "HEAD"]);
1137
+ }
1138
+ function isInsideGitRepo(repoRoot) {
1139
+ return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
1140
+ }
1141
+ function detectGitToplevel(cwd) {
1142
+ return runGit(cwd, ["rev-parse", "--show-toplevel"]);
1143
+ }
1144
+ function detectRenames(options) {
1145
+ const threshold = options.similarityThreshold ?? 80;
1146
+ const args = [
1147
+ "diff",
1148
+ `--find-renames=${threshold}%`,
1149
+ "--name-status",
1150
+ options.fromCommit
1151
+ ];
1152
+ if (options.toCommit) args.push(options.toCommit);
1153
+ const out = runGit(options.repoRoot, args);
1154
+ if (out === null) return [];
1155
+ return parseRenameOutput(out);
1156
+ }
1157
+ function parseRenameOutput(text) {
1158
+ const out = [];
1159
+ for (const rawLine of text.split("\n")) {
1160
+ const line = rawLine.trimEnd();
1161
+ if (!line) continue;
1162
+ const parts = line.split(" ");
1163
+ const status = parts[0] ?? "";
1164
+ if (!status.startsWith("R") || parts.length < 3) continue;
1165
+ const similarity = parseInt(status.slice(1), 10);
1166
+ out.push({
1167
+ oldPath: parts[1],
1168
+ newPath: parts[2],
1169
+ similarity: Number.isFinite(similarity) ? similarity : 100
1170
+ });
1171
+ }
1172
+ return out;
1173
+ }
1174
+ function classifyRename(oldPath, newPath) {
1175
+ return path7.posix.dirname(oldPath) === path7.posix.dirname(newPath) ? "rename" : "move";
1176
+ }
1177
+ function renameToAliases(repoRoot, pair) {
1178
+ const reason = classifyRename(pair.oldPath, pair.newPath);
1179
+ const oldAbs = path7.join(repoRoot, pair.oldPath);
1180
+ const newAbs = path7.join(repoRoot, pair.newPath);
1181
+ return [
1182
+ { oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
1183
+ { oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
1184
+ ];
1185
+ }
1186
+ function buildAliases(repoRoot, pairs) {
1187
+ const seen = /* @__PURE__ */ new Set();
1188
+ const out = [];
1189
+ for (const pair of pairs) {
1190
+ for (const alias of renameToAliases(repoRoot, pair)) {
1191
+ if (seen.has(alias.oldId)) continue;
1192
+ seen.add(alias.oldId);
1193
+ out.push(alias);
1194
+ }
1195
+ }
1196
+ return out;
1197
+ }
1198
+ function discoveryEnv() {
1199
+ const env = { ...process.env };
1200
+ delete env.GIT_DIR;
1201
+ delete env.GIT_INDEX_FILE;
1202
+ delete env.GIT_WORK_TREE;
1203
+ return env;
1204
+ }
1205
+ function runGit(cwd, args) {
1206
+ try {
1207
+ return execFileSync("git", [...args], {
1208
+ cwd,
1209
+ encoding: "utf-8",
1210
+ stdio: ["ignore", "pipe", "ignore"],
1211
+ env: discoveryEnv()
1212
+ }).trim();
1213
+ } catch {
1214
+ return null;
1215
+ }
1216
+ }
1217
+
1218
+ // src/generated.ts
1219
+ import { readFileSync } from "fs";
1220
+ import * as path8 from "path";
1221
+ var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
1222
+ var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
1223
+ function isGeneratedByHeuristic(id) {
1224
+ return HEURISTIC_FILE_RE.test(id) || HEURISTIC_DIR_RE.test(id);
1225
+ }
1226
+ function isGeneratedFile(id, patterns) {
1227
+ for (const rx of patterns) if (rx.test(id)) return true;
1228
+ return isGeneratedByHeuristic(id);
1229
+ }
1230
+ function loadGeneratedPatterns(rootDir) {
1231
+ try {
1232
+ const content = readFileSync(path8.join(rootDir, ".gitattributes"), "utf-8");
1233
+ return parseGeneratedPatterns(content);
1234
+ } catch {
1235
+ return [];
1236
+ }
1237
+ }
1238
+ function parseGeneratedPatterns(content) {
1239
+ const out = [];
1240
+ for (const raw of content.split("\n")) {
1241
+ const line = raw.trim();
1242
+ if (!line || line.startsWith("#")) continue;
1243
+ const parsed = /^("(?:[^"\\]|\\.)*"|\S+)\s*(.*)$/.exec(line);
1244
+ if (!parsed) continue;
1245
+ const pattern = unquote(parsed[1]);
1246
+ if (generatedAttr(parsed[2].split(/\s+/).filter(Boolean)) === true) {
1247
+ out.push(gitattributesPatternToRegex(pattern));
1248
+ }
1249
+ }
1250
+ return out;
1251
+ }
1252
+ function generatedAttr(attrs) {
1253
+ let value;
1254
+ for (const a of attrs) {
1255
+ if (a === "linguist-generated" || a === "linguist-generated=true") value = true;
1256
+ else if (a === "-linguist-generated" || a === "linguist-generated=false") value = false;
1257
+ }
1258
+ return value;
1259
+ }
1260
+ function unquote(token) {
1261
+ return token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1).replace(/\\(.)/g, "$1") : token;
1262
+ }
1263
+ function gitattributesPatternToRegex(pattern) {
1264
+ let p = pattern;
1265
+ const isDir = p.endsWith("/");
1266
+ if (isDir) p = p.slice(0, -1);
1267
+ let body = "";
1268
+ for (let i = 0; i < p.length; i++) {
1269
+ const c = p[i];
1270
+ if (c === "*" && p[i + 1] === "*") {
1271
+ body += ".*";
1272
+ i++;
1273
+ } else if (c === "*") body += "[^/]*";
1274
+ else if (c === "?") body += "[^/]";
1275
+ else body += c.replace(/[.+(){}|^$\\[\]]/, "\\$&");
1276
+ }
1277
+ const prefix = p.includes("/") ? "^" : "(?:^|/)";
1278
+ return new RegExp(prefix + body + (isDir ? "(?:/|$)" : "$"));
1279
+ }
1280
+
1281
+ // src/roles.ts
1282
+ var TEST_RE = /(?:^|\/)(?:__tests__\/|tests?\/)|\.(?:test|spec)(?:\.[a-z]+)?$/;
1283
+ var FIXTURE_RE = /(?:^|\/)fixtures(?:\/|$)/;
1284
+ var SCRIPT_RE = /(?:^|\/)(?:scripts|archive)(?:\/|$)/;
1285
+ var BARREL_RE = /(?:^|\/)index(?:\.[a-z]+)?$/;
1286
+ var TYPES_RE = /(?:^|\/)(?:[a-z][\w-]*\.)?types(?:\.[a-z]+)?$/i;
1287
+ var CONFIG_RE = /\.config(?:\.[a-z]+)?$/;
1288
+ var ALL_ROLES = [
1289
+ "test",
1290
+ "fixture",
1291
+ "barrel",
1292
+ "types",
1293
+ "config",
1294
+ "script",
1295
+ "entry",
1296
+ "generated",
1297
+ "source"
1298
+ ];
1299
+ function classifyRole(id, hints) {
1300
+ if (hints?.isGenerated) return "generated";
1301
+ if (TEST_RE.test(id)) return "test";
1302
+ if (FIXTURE_RE.test(id)) return "fixture";
1303
+ if (SCRIPT_RE.test(id)) return "script";
1304
+ if (hints?.hasShebang) return "entry";
1305
+ if (BARREL_RE.test(id)) return "barrel";
1306
+ if (TYPES_RE.test(id)) return "types";
1307
+ if (CONFIG_RE.test(id)) return "config";
1308
+ return "source";
1309
+ }
1310
+ function computeRoleHints(readFiles, idRoot, toId) {
1311
+ const patterns = loadGeneratedPatterns(idRoot);
1312
+ const shebangIds = /* @__PURE__ */ new Set();
1313
+ const generatedIds = /* @__PURE__ */ new Set();
1314
+ for (const rf of readFiles) {
1315
+ const id = toId(idRoot, rf.filePath);
1316
+ if (rf.content.startsWith("#!")) shebangIds.add(id);
1317
+ if (isGeneratedFile(id, patterns)) generatedIds.add(id);
1318
+ }
1319
+ return { shebangIds, generatedIds };
1320
+ }
1321
+ function annotateRoles(nodes, options) {
1322
+ return nodes.map(
1323
+ (n) => n.kind === "file" || n.kind === "module" ? {
1324
+ ...n,
1325
+ role: n.role ?? classifyRole(n.id, {
1326
+ hasShebang: options?.shebangIds?.has(n.id) ?? false,
1327
+ isGenerated: options?.generatedIds?.has(n.id) ?? false
1328
+ })
1329
+ } : n
1330
+ );
1331
+ }
1332
+
1333
+ // src/file-walk.ts
1334
+ import * as fs from "fs/promises";
1335
+ import * as path9 from "path";
1336
+ async function walkSourceFiles(rootDirs, languages2) {
1337
+ const seen = /* @__PURE__ */ new Set();
1338
+ const out = [];
1339
+ for (const rootDir of rootDirs) {
1340
+ await collectFiles(rootDir, rootDir, languages2, seen, out);
1341
+ }
1342
+ return out;
1343
+ }
1344
+ async function collectFiles(dir, rootDir, languages2, seen, out) {
1345
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1346
+ for (const entry of entries) {
1347
+ const full = path9.join(dir, entry.name);
1348
+ if (entry.isDirectory()) {
1349
+ if (isExcludedDir(entry.name)) continue;
1350
+ await collectFiles(full, rootDir, languages2, seen, out);
1351
+ } else if (entry.isFile() && !seen.has(full)) {
1352
+ const relative4 = path9.relative(rootDir, full);
1353
+ if (shouldIncludeFile(relative4, [...languages2])) {
1354
+ seen.add(full);
1355
+ out.push(full);
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+
1361
+ // src/barrel-resolve.ts
1362
+ function edgeWeight(e) {
1363
+ const w = e.attrs?.weight;
1364
+ return typeof w === "number" && w > 0 ? w : 1;
1365
+ }
1366
+ function pruneDanglingReferences(nodes, edges) {
1367
+ for (const [key, edge] of edges) {
1368
+ if (edge.kind === "references" && !nodes.has(edge.dstId)) edges.delete(key);
1369
+ }
1370
+ }
1371
+ function resolveBarrelEdges(nodes, edges) {
1372
+ const barrels = /* @__PURE__ */ new Set();
1373
+ for (const n of nodes) if (n.role === "barrel") barrels.add(n.id);
1374
+ if (barrels.size === 0) return [...edges];
1375
+ const forwards = /* @__PURE__ */ new Map();
1376
+ for (const e of edges) {
1377
+ if (!barrels.has(e.srcId) || e.kind === "references") continue;
1378
+ const arr = forwards.get(e.srcId) ?? [];
1379
+ arr.push({ dst: e.dstId, weight: edgeWeight(e) });
1380
+ forwards.set(e.srcId, arr);
1381
+ }
1382
+ const cache = /* @__PURE__ */ new Map();
1383
+ function resolve4(id, seen) {
1384
+ if (!barrels.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
1385
+ const cached = cache.get(id);
1386
+ if (cached) return cached;
1387
+ const targets = forwards.get(id);
1388
+ const total = targets?.reduce((s, t) => s + t.weight, 0) ?? 0;
1389
+ if (!targets || total === 0 || seen.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
1390
+ const out = /* @__PURE__ */ new Map();
1391
+ const nextSeen = new Set(seen).add(id);
1392
+ for (const t of targets) {
1393
+ const share = t.weight / total;
1394
+ for (const [real, frac] of resolve4(t.dst, nextSeen)) {
1395
+ out.set(real, (out.get(real) ?? 0) + share * frac);
1396
+ }
1397
+ }
1398
+ cache.set(id, out);
1399
+ return out;
1400
+ }
1401
+ const resolved = [];
1402
+ for (const e of edges) {
1403
+ if (barrels.has(e.srcId) && e.kind !== "references") continue;
1404
+ if (!barrels.has(e.dstId)) {
1405
+ resolved.push(e);
1406
+ continue;
1407
+ }
1408
+ const w = edgeWeight(e);
1409
+ for (const [real, frac] of resolve4(e.dstId, /* @__PURE__ */ new Set())) {
1410
+ if (real === e.srcId) continue;
1411
+ resolved.push({
1412
+ ...e,
1413
+ dstId: real,
1414
+ attrs: { ...e.attrs, weight: w * frac }
1415
+ });
1416
+ }
1417
+ }
1418
+ return resolved;
1419
+ }
1420
+
1421
+ // src/incremental.ts
1422
+ import { createHash } from "crypto";
1423
+ import * as fs2 from "fs/promises";
1424
+
1425
+ // src/cognitive-complexity.ts
1426
+ function cognitiveComplexityOf(body, language) {
1427
+ const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
1428
+ return scoreNode(body, 0, handler);
1429
+ }
1430
+ var TS_STRUCTURAL = /* @__PURE__ */ new Set([
1431
+ "if_statement",
1432
+ "for_statement",
1433
+ "for_in_statement",
1434
+ "while_statement",
1435
+ "do_statement",
1436
+ "switch_statement",
1437
+ "catch_clause",
1438
+ "ternary_expression"
1439
+ ]);
1440
+ var PY_STRUCTURAL = /* @__PURE__ */ new Set([
1441
+ "if_statement",
1442
+ "for_statement",
1443
+ "while_statement",
1444
+ "except_clause",
1445
+ "conditional_expression"
1446
+ // elif_clause is intentionally NOT here — it's a flat continuation of an
1447
+ // existing if (counted in flatIncrement). Including it would charge both
1448
+ // a structural and a flat +1.
1449
+ ]);
1450
+ var TS_NESTING = /* @__PURE__ */ new Set([
1451
+ "if_statement",
1452
+ "for_statement",
1453
+ "for_in_statement",
1454
+ "while_statement",
1455
+ "do_statement",
1456
+ "switch_statement",
1457
+ "catch_clause",
1458
+ "function_declaration",
1459
+ "method_definition",
1460
+ "arrow_function"
1461
+ ]);
1462
+ var PY_NESTING = /* @__PURE__ */ new Set([
1463
+ "if_statement",
1464
+ "for_statement",
1465
+ "while_statement",
1466
+ "except_clause",
1467
+ "function_definition",
1468
+ "lambda"
1469
+ ]);
1470
+ var TS_HANDLER = {
1471
+ structuralIncrement(node, nesting) {
1472
+ return TS_STRUCTURAL.has(node.type) ? 1 + nesting : 0;
1473
+ },
1474
+ isNesting(node) {
1475
+ return TS_NESTING.has(node.type);
1476
+ },
1477
+ flatIncrement(node) {
1478
+ if (node.type === "else_clause") return 1;
1479
+ if (node.type === "binary_expression" && isLogicalOp(node) && !isInnerOfSameOpChain(node, "binary_expression")) {
1480
+ return 1;
1481
+ }
1482
+ return 0;
1483
+ }
1484
+ };
1485
+ var PY_HANDLER = {
1486
+ structuralIncrement(node, nesting) {
1487
+ return PY_STRUCTURAL.has(node.type) ? 1 + nesting : 0;
1488
+ },
1489
+ isNesting(node) {
1490
+ return PY_NESTING.has(node.type);
1491
+ },
1492
+ flatIncrement(node) {
1493
+ if (node.type === "elif_clause" || node.type === "else_clause") return 1;
1494
+ if (node.type === "boolean_operator" && !isInnerOfSameOpChain(node, "boolean_operator")) {
1495
+ return 1;
1496
+ }
1497
+ return 0;
1498
+ }
1499
+ };
1500
+ function isLogicalOp(node) {
1501
+ const op = node.childForFieldName("operator");
1502
+ return !!op && (op.text === "&&" || op.text === "||");
1503
+ }
1504
+ function isInnerOfSameOpChain(node, parentType) {
1505
+ const parent = node.parent;
1506
+ if (!parent || parent.type !== parentType) return false;
1507
+ const op = node.childForFieldName("operator")?.text;
1508
+ const parentOp = parent.childForFieldName("operator")?.text;
1509
+ return !!op && op === parentOp;
1510
+ }
1511
+ function isElseIf(node) {
1512
+ if (node.type !== "if_statement" && node.type !== "elif_clause") return false;
1513
+ const parent = node.parent;
1514
+ return !!parent && parent.type === "else_clause";
1515
+ }
1516
+ function scoreNode(node, nesting, handler) {
1517
+ const elseIf = isElseIf(node);
1518
+ const structural = elseIf ? 0 : handler.structuralIncrement(node, nesting);
1519
+ const flat = handler.flatIncrement(node);
1520
+ const childNesting = !elseIf && handler.isNesting(node) ? nesting + 1 : nesting;
1521
+ let score = structural + flat;
1522
+ for (const child of node.namedChildren) {
1523
+ if (child) score += scoreNode(child, childNesting, handler);
1524
+ }
1525
+ return score;
1526
+ }
1527
+
1528
+ // src/lcom.ts
1529
+ function computeLcomMetrics(file, fileId2) {
1530
+ const classes = collectClasses(file);
1531
+ if (classes.length === 0) return [];
1532
+ const out = [
1533
+ {
1534
+ nodeId: fileId2,
1535
+ name: "class_count",
1536
+ value: classes.length,
1537
+ unit: "count"
1538
+ }
1539
+ ];
1540
+ const scores = classes.map((c) => lcom4OfClass(c, file.language)).filter((s) => s !== null);
1541
+ if (scores.length > 0) {
1542
+ out.push({
1543
+ nodeId: fileId2,
1544
+ name: "lcom4_max",
1545
+ value: Math.max(...scores),
1546
+ unit: "count"
1547
+ });
1548
+ }
1549
+ return out;
1550
+ }
1551
+ function collectClasses(file) {
1552
+ const isClass = file.language === "python" ? (n) => n.type === "class_definition" : (n) => n.type === "class_declaration" || n.type === "class_expression";
1553
+ const out = [];
1554
+ const visit = (node) => {
1555
+ if (isClass(node)) out.push(node);
1556
+ for (const child of node.namedChildren) {
1557
+ if (child) visit(child);
1558
+ }
1559
+ };
1560
+ visit(file.tree.rootNode);
1561
+ return out;
1562
+ }
1563
+ function lcom4OfClass(classNode, language) {
1564
+ const methods = collectMethods(classNode, language);
1565
+ if (methods.length === 0) return null;
1566
+ if (methods.length === 1) return 1;
1567
+ return countComponents(methods);
1568
+ }
1569
+ function collectMethods(classNode, language) {
1570
+ return language === "python" ? collectPythonMethods(classNode) : collectTsMethods(classNode);
1571
+ }
1572
+ function collectTsMethods(classNode) {
1573
+ const body = classNode.childForFieldName("body");
1574
+ if (!body) return [];
1575
+ const out = [];
1576
+ for (const child of body.namedChildren) {
1577
+ if (!child || child.type !== "method_definition") continue;
1578
+ if (hasStaticModifier(child)) continue;
1579
+ const name = methodName(child);
1580
+ if (!name || name === "constructor") continue;
1581
+ out.push({ name, usedNames: thisRefs(child, "this") });
1582
+ }
1583
+ return out;
1584
+ }
1585
+ function collectPythonMethods(classNode) {
1586
+ const body = classNode.childForFieldName("body");
1587
+ if (!body) return [];
1588
+ const out = [];
1589
+ for (const child of body.namedChildren) {
1590
+ if (!child || child.type !== "function_definition") continue;
1591
+ const name = child.childForFieldName("name")?.text;
1592
+ if (!name || name === "__init__") continue;
1593
+ if (isPythonStaticOrClassMethod(child)) continue;
1594
+ out.push({ name, usedNames: thisRefs(child, "self") });
1595
+ }
1596
+ return out;
1597
+ }
1598
+ function methodName(methodNode) {
1599
+ const nameNode = methodNode.childForFieldName("name");
1600
+ return nameNode?.text ?? null;
1601
+ }
1602
+ function hasStaticModifier(methodNode) {
1603
+ for (const child of methodNode.children) {
1604
+ if (child?.type === "static") return true;
1605
+ }
1606
+ return false;
1607
+ }
1608
+ function isPythonStaticOrClassMethod(funcNode) {
1609
+ let prev = funcNode.previousNamedSibling;
1610
+ while (prev && prev.type === "decorator") {
1611
+ const text = prev.text;
1612
+ if (text.includes("@staticmethod") || text.includes("@classmethod")) {
1613
+ return true;
1614
+ }
1615
+ prev = prev.previousNamedSibling;
1616
+ }
1617
+ return false;
1618
+ }
1619
+ function thisRefs(methodNode, selfKeyword) {
1620
+ const out = /* @__PURE__ */ new Set();
1621
+ const visit = (n) => {
1622
+ collectThisRef(n, selfKeyword, out);
1623
+ for (const child of n.namedChildren) {
1624
+ if (child) visit(child);
1625
+ }
1626
+ };
1627
+ visit(methodNode);
1628
+ return out;
1629
+ }
1630
+ function collectThisRef(n, selfKeyword, out) {
1631
+ if (n.type === "member_expression" || n.type === "attribute") {
1632
+ const obj = n.childForFieldName("object");
1633
+ if (!obj) return;
1634
+ if (matchesSelf(obj, selfKeyword)) {
1635
+ const prop = n.childForFieldName("property") ?? n.childForFieldName("attribute");
1636
+ if (prop) out.add(prop.text);
1637
+ }
1638
+ }
1639
+ }
1640
+ function matchesSelf(node, selfKeyword) {
1641
+ if (selfKeyword === "this") return node.type === "this";
1642
+ return node.type === "identifier" && node.text === selfKeyword;
1643
+ }
1644
+ function countComponents(methods) {
1645
+ const n = methods.length;
1646
+ const parent = Array.from({ length: n }, (_, i) => i);
1647
+ const find = (i) => {
1648
+ while (parent[i] !== i) {
1649
+ parent[i] = parent[parent[i]];
1650
+ i = parent[i];
1651
+ }
1652
+ return i;
1653
+ };
1654
+ const union = (a, b) => {
1655
+ const ra = find(a);
1656
+ const rb = find(b);
1657
+ if (ra !== rb) parent[ra] = rb;
1658
+ };
1659
+ for (let i = 0; i < n; i++) {
1660
+ for (let j = i + 1; j < n; j++) {
1661
+ if (areLinked(methods[i], methods[j])) union(i, j);
1662
+ }
1663
+ }
1664
+ const roots = /* @__PURE__ */ new Set();
1665
+ for (let i = 0; i < n; i++) roots.add(find(i));
1666
+ return roots.size;
1667
+ }
1668
+ function areLinked(a, b) {
1669
+ if (a.usedNames.has(b.name)) return true;
1670
+ if (b.usedNames.has(a.name)) return true;
1671
+ for (const name of a.usedNames) {
1672
+ if (b.usedNames.has(name)) return true;
1673
+ }
1674
+ return false;
1675
+ }
1676
+
1677
+ // src/source-metrics.ts
1678
+ var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
1679
+ "function_declaration",
1680
+ "method_definition"
1681
+ ]);
1682
+ var PY_FUNCTION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
1683
+ var TS_NESTING_TYPES = /* @__PURE__ */ new Set([
1684
+ "if_statement",
1685
+ "for_statement",
1686
+ "for_in_statement",
1687
+ "while_statement",
1688
+ "do_statement",
1689
+ "switch_statement",
1690
+ "try_statement"
1691
+ ]);
1692
+ var PY_NESTING_TYPES = /* @__PURE__ */ new Set([
1693
+ "if_statement",
1694
+ "for_statement",
1695
+ "while_statement",
1696
+ "try_statement"
1697
+ ]);
1698
+ var TS_BRANCH_TYPES = /* @__PURE__ */ new Set([
1699
+ "if_statement",
1700
+ "for_statement",
1701
+ "for_in_statement",
1702
+ "while_statement",
1703
+ "do_statement",
1704
+ "switch_case",
1705
+ "catch_clause",
1706
+ "ternary_expression"
1707
+ ]);
1708
+ var PY_BRANCH_TYPES = /* @__PURE__ */ new Set([
1709
+ "if_statement",
1710
+ "elif_clause",
1711
+ "for_statement",
1712
+ "while_statement",
1713
+ "except_clause",
1714
+ "conditional_expression"
1715
+ ]);
1716
+ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
1717
+ "loc",
1718
+ "function_count",
1719
+ "cyclomatic_max",
1720
+ "cyclomatic_sum",
1721
+ "cognitive_max",
1722
+ "cognitive_sum",
1723
+ "max_nesting_depth",
1724
+ "class_count",
1725
+ "lcom4_max",
1726
+ // Per-symbol complexity (C-58; C-64 extends it to non-exported symbols), keyed
1727
+ // to `symbol` node ids. Source-local like the file-level metrics above, so an
1728
+ // unchanged file carries them forward — but their nodeId is `<fileId>#<name>`,
1729
+ // so the reuse basis buckets them under the symbol's parent file (incremental.ts).
1730
+ "symbol_cognitive",
1731
+ "symbol_cyclomatic"
1732
+ ]);
1733
+ var EMPTY_NAMES = /* @__PURE__ */ new Set();
1734
+ function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
1735
+ const out = [];
1736
+ for (const file of files) {
1737
+ const id = fileIdOf(file.filePath);
1738
+ out.push(...metricsForFile(id, file, symbolNamesByFile2.get(id) ?? EMPTY_NAMES));
1739
+ }
1740
+ return out;
1741
+ }
1742
+ function metricsForFile(nodeId, file, symbolNames) {
1743
+ const out = [];
1744
+ const loc = countLoc(file.content);
1745
+ out.push({ nodeId, name: "loc", value: loc, unit: "lines" });
1746
+ const stats = analyzeFunctions(file);
1747
+ out.push({
1748
+ nodeId,
1749
+ name: "function_count",
1750
+ value: stats.length,
1751
+ unit: "count"
1752
+ });
1753
+ if (stats.length > 0) {
1754
+ out.push({
1755
+ nodeId,
1756
+ name: "cyclomatic_max",
1757
+ value: Math.max(...stats.map((s) => s.cyclomatic)),
1758
+ unit: "count"
1759
+ });
1760
+ out.push({
1761
+ nodeId,
1762
+ name: "cyclomatic_sum",
1763
+ value: stats.reduce((acc, s) => acc + s.cyclomatic, 0),
1764
+ unit: "count"
1765
+ });
1766
+ out.push({
1767
+ nodeId,
1768
+ name: "cognitive_max",
1769
+ value: Math.max(...stats.map((s) => s.cognitive)),
1770
+ unit: "count"
1771
+ });
1772
+ out.push({
1773
+ nodeId,
1774
+ name: "cognitive_sum",
1775
+ value: stats.reduce((acc, s) => acc + s.cognitive, 0),
1776
+ unit: "count"
1777
+ });
1778
+ out.push({
1779
+ nodeId,
1780
+ name: "max_nesting_depth",
1781
+ value: Math.max(...stats.map((s) => s.nestingDepth)),
1782
+ unit: "count"
1783
+ });
1784
+ }
1785
+ out.push(...symbolComplexityMetrics(nodeId, stats, symbolNames));
1786
+ out.push(...computeLcomMetrics(file, nodeId));
1787
+ return out;
1788
+ }
1789
+ function symbolComplexityMetrics(fileId2, stats, symbolNames) {
1790
+ if (symbolNames.size === 0) return [];
1791
+ const byName = /* @__PURE__ */ new Map();
1792
+ for (const s of stats) {
1793
+ if (!s.name || !symbolNames.has(s.name)) continue;
1794
+ const prev = byName.get(s.name);
1795
+ if (!prev) byName.set(s.name, { cognitive: s.cognitive, cyclomatic: s.cyclomatic });
1796
+ else {
1797
+ prev.cognitive = Math.max(prev.cognitive, s.cognitive);
1798
+ prev.cyclomatic = Math.max(prev.cyclomatic, s.cyclomatic);
1799
+ }
1800
+ }
1801
+ const out = [];
1802
+ for (const [name, m] of byName) {
1803
+ const sid = symbolId(fileId2, name);
1804
+ out.push({ nodeId: sid, name: "symbol_cognitive", value: m.cognitive, unit: "count" });
1805
+ out.push({ nodeId: sid, name: "symbol_cyclomatic", value: m.cyclomatic, unit: "count" });
1806
+ }
1807
+ return out;
1808
+ }
1809
+ function countLoc(content) {
1810
+ return content.split("\n").filter((l) => l.trim() !== "").length;
1811
+ }
1812
+ function analyzeFunctions(file) {
1813
+ const stats = [];
1814
+ const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
1815
+ const visit = (node) => {
1816
+ const fn = functionAt(node, fnTypes);
1817
+ if (fn) {
1818
+ stats.push({
1819
+ name: fn.name,
1820
+ cyclomatic: cyclomaticOf(fn.body, file.language),
1821
+ cognitive: cognitiveComplexityOf(fn.body, file.language),
1822
+ nestingDepth: nestingDepthOf(fn.body, file.language, 0)
1823
+ });
1824
+ }
1825
+ for (const child of node.children) {
1826
+ if (child) visit(child);
1827
+ }
1828
+ };
1829
+ visit(file.tree.rootNode);
1830
+ return stats;
1831
+ }
1832
+ function functionAt(node, fnTypes) {
1833
+ if (fnTypes.has(node.type)) {
1834
+ const body = node.childForFieldName("body");
1835
+ return body ? { name: node.childForFieldName("name")?.text ?? null, body } : null;
1836
+ }
1837
+ if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
1838
+ const body = node.childForFieldName("body");
1839
+ if (!body) return null;
1840
+ return { name: node.parent.childForFieldName("name")?.text ?? null, body };
1841
+ }
1842
+ return null;
1843
+ }
1844
+ function nestingDepthOf(node, language, depth) {
1845
+ const nestingTypes = language === "python" ? PY_NESTING_TYPES : TS_NESTING_TYPES;
1846
+ let maxDepth = depth;
1847
+ for (const child of node.namedChildren) {
1848
+ if (!child) continue;
1849
+ const next = nestingTypes.has(child.type) ? depth + 1 : depth;
1850
+ const childMax = nestingDepthOf(child, language, next);
1851
+ if (childMax > maxDepth) maxDepth = childMax;
1852
+ }
1853
+ return maxDepth;
1854
+ }
1855
+ function cyclomaticOf(body, language) {
1856
+ const branchTypes = language === "python" ? PY_BRANCH_TYPES : TS_BRANCH_TYPES;
1857
+ let complexity = 1;
1858
+ const visit = (node) => {
1859
+ if (branchTypes.has(node.type)) complexity++;
1860
+ if (node.type === "binary_expression") {
1861
+ const op = node.childForFieldName("operator");
1862
+ if (op && (op.text === "&&" || op.text === "||")) complexity++;
1863
+ }
1864
+ if (language === "python" && node.type === "boolean_operator") {
1865
+ complexity++;
1866
+ }
1867
+ for (const child of node.namedChildren) {
1868
+ if (child) visit(child);
1869
+ }
1870
+ };
1871
+ visit(body);
1872
+ return complexity;
1873
+ }
1874
+
1875
+ // src/reconstruct.ts
1876
+ function reconstructFragment(repoRoot, absPath, fileId2, basis) {
1877
+ const nodes = buildFileModuleNodes(repoRoot, absPath);
1878
+ nodes.push(...basis.symbolsByFile.get(fileId2) ?? []);
1879
+ const edges = basis.edgesBySrc.get(fileId2) ?? [];
1880
+ const seenExternals = /* @__PURE__ */ new Set();
1881
+ for (const edge of edges) {
1882
+ const dst = basis.nodesById.get(edge.dstId);
1883
+ if (dst?.kind === "external" && !seenExternals.has(dst.id)) {
1884
+ seenExternals.add(dst.id);
1885
+ nodes.push({ id: dst.id, kind: "external", name: dst.name });
1886
+ }
1887
+ }
1888
+ return { nodes, edges };
1889
+ }
1890
+ function reconstructCosmetic(repoRoot, absPath, fileId2, basis, parsed) {
1891
+ const base = reconstructFragment(repoRoot, absPath, fileId2, basis);
1892
+ const spans = collectDeclaredSpans(parsed);
1893
+ const nodes = base.nodes.map(
1894
+ (n) => n.kind === "symbol" ? withRefreshedSpan(n, spans) : n
1895
+ );
1896
+ return { nodes, edges: base.edges };
1897
+ }
1898
+ function withRefreshedSpan(node, spans) {
1899
+ const attrs = node.attrs;
1900
+ if (!attrs || attrs.startLine === void 0) return node;
1901
+ const span = spans.get(node.name);
1902
+ if (!span) return node;
1903
+ return {
1904
+ ...node,
1905
+ attrs: { ...attrs, startLine: span.startLine, endLine: span.endLine }
1906
+ };
1907
+ }
1908
+
1909
+ // src/incremental.ts
1910
+ function hashContent(content) {
1911
+ return createHash("sha256").update(content, "utf8").digest("hex");
1912
+ }
1913
+ function structuralSignature(file) {
1914
+ const parts = [];
1915
+ const visit = (node) => {
1916
+ if (node.type === "comment") return;
1917
+ parts.push(node.type);
1918
+ if (node.childCount === 0) parts.push(node.text);
1919
+ for (const child of node.children) if (child) visit(child);
1920
+ };
1921
+ visit(file.tree.rootNode);
1922
+ return hashContent(parts.join(""));
1923
+ }
1924
+ function sourceLanguage(filePath) {
1925
+ const language = getLanguageFromPath(filePath);
1926
+ return language === "typescript" || language === "python" ? language : null;
1927
+ }
1928
+ async function readSourceFiles(filePaths) {
1929
+ const out = [];
1930
+ for (const filePath of filePaths) {
1931
+ const language = sourceLanguage(filePath);
1932
+ if (language === null) continue;
1933
+ const content = await fs2.readFile(filePath, "utf-8");
1934
+ out.push({ filePath, language, content, hash: hashContent(content) });
1935
+ }
1936
+ return out;
1937
+ }
1938
+ function classifyForReuse(readFiles, idRoot, reuse, affected = /* @__PURE__ */ new Set()) {
1939
+ const toParse = [];
1940
+ const reusedFileIds = [];
1941
+ for (const rf of readFiles) {
1942
+ const id = fileId(idRoot, rf.filePath);
1943
+ if (reuse && reuse.fingerprints.get(id) === rf.hash && !affected.has(id)) {
1944
+ reusedFileIds.push(id);
1945
+ } else {
1946
+ toParse.push(rf);
1947
+ }
1948
+ }
1949
+ return { toParse, reusedFileIds };
1950
+ }
1951
+ function classifyParsed(parsedByPath, idRoot, reuse, reusedFileIds, affected = /* @__PURE__ */ new Set()) {
1952
+ const cosmeticFileIds = /* @__PURE__ */ new Set();
1953
+ const structuralByFileId = /* @__PURE__ */ new Map();
1954
+ for (const [filePath, parsed] of parsedByPath) {
1955
+ const id = fileId(idRoot, filePath);
1956
+ const sig = structuralSignature(parsed);
1957
+ structuralByFileId.set(id, sig);
1958
+ if (reuse && !affected.has(id) && reuse.structuralHashes.get(id) === sig) {
1959
+ cosmeticFileIds.add(id);
1960
+ }
1961
+ }
1962
+ if (reuse) {
1963
+ for (const id of reusedFileIds) {
1964
+ const sig = reuse.structuralHashes.get(id);
1965
+ if (sig !== void 0) structuralByFileId.set(id, sig);
1966
+ }
1967
+ }
1968
+ return { cosmeticFileIds, structuralByFileId };
1969
+ }
1970
+ function assembleFragments(input) {
1971
+ const fragments = [];
1972
+ for (const rf of input.readFiles) {
1973
+ const parsed = input.parsedByPath.get(rf.filePath);
1974
+ const id = fileId(input.idRoot, rf.filePath);
1975
+ if (parsed && input.reuse && input.cosmeticFileIds.has(id)) {
1976
+ fragments.push(
1977
+ reconstructCosmetic(input.idRoot, rf.filePath, id, input.reuse, parsed)
1978
+ );
1979
+ } else if (parsed) {
1980
+ fragments.push(...input.extractor.extract(parsed));
1981
+ } else if (input.reuse) {
1982
+ fragments.push(
1983
+ reconstructFragment(input.idRoot, rf.filePath, id, input.reuse)
1984
+ );
1985
+ }
1986
+ }
1987
+ return fragments;
1988
+ }
1989
+ function buildFingerprints(readFiles, idRoot, structuralByFileId) {
1990
+ return readFiles.map((rf) => {
1991
+ const id = fileId(idRoot, rf.filePath);
1992
+ return {
1993
+ fileId: id,
1994
+ contentHash: rf.hash,
1995
+ structuralHash: structuralByFileId.get(id)
1996
+ };
1997
+ });
1998
+ }
1999
+ function loadReuseBasis(db, indexVersion) {
2000
+ for (const snap of db.listSnapshots({ limit: 50 })) {
2001
+ if (snap.indexVersion !== indexVersion) continue;
2002
+ const fingerprints = db.listFingerprints(snap.id);
2003
+ if (fingerprints.length === 0) continue;
2004
+ const nodesById = /* @__PURE__ */ new Map();
2005
+ const symbolsByFile = /* @__PURE__ */ new Map();
2006
+ for (const n of db.listNodes(snap.id, { includeSymbols: true })) {
2007
+ nodesById.set(n.id, n);
2008
+ if (n.kind === "symbol" && n.parentId) {
2009
+ const bucket = symbolsByFile.get(n.parentId);
2010
+ if (bucket) bucket.push(n);
2011
+ else symbolsByFile.set(n.parentId, [n]);
2012
+ }
2013
+ }
2014
+ const edgesBySrc = /* @__PURE__ */ new Map();
2015
+ for (const e of db.listEdges(snap.id, { includeReferences: true })) {
2016
+ const bucket = edgesBySrc.get(e.srcId);
2017
+ if (bucket) bucket.push(e);
2018
+ else edgesBySrc.set(e.srcId, [e]);
2019
+ }
2020
+ const sourceMetricsByFile = /* @__PURE__ */ new Map();
2021
+ for (const m of db.listMetrics(snap.id)) {
2022
+ if (!SOURCE_METRIC_NAMES.has(m.name)) continue;
2023
+ const node = nodesById.get(m.nodeId);
2024
+ const fileKey = node?.kind === "symbol" && node.parentId ? node.parentId : m.nodeId;
2025
+ const bucket = sourceMetricsByFile.get(fileKey);
2026
+ if (bucket) bucket.push(m);
2027
+ else sourceMetricsByFile.set(fileKey, [m]);
2028
+ }
2029
+ const structuralHashes = /* @__PURE__ */ new Map();
2030
+ for (const f of fingerprints) {
2031
+ if (f.structuralHash) structuralHashes.set(f.fileId, f.structuralHash);
2032
+ }
2033
+ return {
2034
+ snapshotId: snap.id,
2035
+ fingerprints: new Map(fingerprints.map((f) => [f.fileId, f.contentHash])),
2036
+ structuralHashes,
2037
+ nodesById,
2038
+ edgesBySrc,
2039
+ sourceMetricsByFile,
2040
+ symbolsByFile
2041
+ };
2042
+ }
2043
+ return null;
2044
+ }
2045
+
2046
+ // src/reuse-delta.ts
2047
+ function dirOf(id) {
2048
+ const i = id.lastIndexOf("/");
2049
+ return i < 0 ? "" : id.slice(0, i);
2050
+ }
2051
+ function normalizePosix(p) {
2052
+ const out = [];
2053
+ for (const seg of p.split("/")) {
2054
+ if (seg === "" || seg === ".") continue;
2055
+ if (seg === "..") out.pop();
2056
+ else out.push(seg);
2057
+ }
2058
+ return out.join("/");
2059
+ }
2060
+ var JS_EXT_RE = /\.(?:jsx?|mjs|cjs)$/;
2061
+ function* candidateIds(base) {
2062
+ yield base;
2063
+ for (const ext of RESOLVABLE_EXTS) yield base + ext;
2064
+ const jsExt = JS_EXT_RE.exec(base);
2065
+ if (jsExt) {
2066
+ const stem = base.slice(0, base.length - jsExt[0].length);
2067
+ for (const ext of RESOLVABLE_EXTS) yield stem + ext;
2068
+ }
2069
+ for (const ext of RESOLVABLE_EXTS) yield `${base}/index${ext}`;
2070
+ }
2071
+ function isRelative(specifier) {
2072
+ return specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
2073
+ }
2074
+ function resolveRelativeId(srcFileId, specifier, fileIds) {
2075
+ const base = normalizePosix(`${dirOf(srcFileId)}/${specifier}`);
2076
+ for (const candidate of candidateIds(base)) {
2077
+ if (fileIds.has(candidate)) return candidate;
2078
+ }
2079
+ return null;
2080
+ }
2081
+ function targetFileId(edge) {
2082
+ return parseSymbolId(edge.dstId)?.fileId ?? edge.dstId;
2083
+ }
2084
+ function computeDeltaAffected(basis, currentFileIds) {
2085
+ const affected = /* @__PURE__ */ new Set();
2086
+ const removed = /* @__PURE__ */ new Set();
2087
+ for (const id of basis.fingerprints.keys()) {
2088
+ if (!currentFileIds.has(id)) removed.add(id);
2089
+ }
2090
+ const added = currentFileIds.size !== basis.fingerprints.size || removed.size > 0;
2091
+ if (!added) return affected;
2092
+ for (const [srcId, edges] of basis.edgesBySrc) {
2093
+ if (!currentFileIds.has(srcId)) continue;
2094
+ for (const edge of edges) {
2095
+ if (isEdgeAffected(edge, srcId, removed, currentFileIds)) {
2096
+ affected.add(srcId);
2097
+ break;
2098
+ }
2099
+ }
2100
+ }
2101
+ return affected;
2102
+ }
2103
+ function isEdgeAffected(edge, srcId, removed, currentFileIds) {
2104
+ if (removed.has(targetFileId(edge))) return true;
2105
+ const specifier = edge.attrs?.specifier;
2106
+ if (!specifier || !isRelative(specifier)) return false;
2107
+ return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge);
2108
+ }
2109
+
2110
+ // src/metrics.ts
2111
+ function isStructuralEdge(e) {
2112
+ return e.kind === "imports" || e.kind === "re-exports";
2113
+ }
2114
+ function countDegrees(nodes, edges) {
2115
+ const fanIn = /* @__PURE__ */ new Map();
2116
+ const fanOut = /* @__PURE__ */ new Map();
2117
+ for (const n of nodes) {
2118
+ fanIn.set(n.id, 0);
2119
+ fanOut.set(n.id, 0);
2120
+ }
2121
+ for (const e of edges) {
2122
+ if (!isStructuralEdge(e)) continue;
2123
+ if (fanOut.has(e.srcId)) {
2124
+ fanOut.set(e.srcId, fanOut.get(e.srcId) + 1);
2125
+ }
2126
+ if (fanIn.has(e.dstId)) {
2127
+ fanIn.set(e.dstId, fanIn.get(e.dstId) + 1);
2128
+ }
2129
+ }
2130
+ return { fanIn, fanOut };
2131
+ }
2132
+ function weightedInbound(nodes, edges) {
2133
+ const util = /* @__PURE__ */ new Map();
2134
+ for (const n of nodes) util.set(n.id, 0);
2135
+ for (const e of resolveBarrelEdges(nodes, edges)) {
2136
+ if (util.has(e.dstId)) util.set(e.dstId, util.get(e.dstId) + edgeWeight(e));
2137
+ }
2138
+ return util;
2139
+ }
2140
+ function computeMetrics(nodes, edges) {
2141
+ const { fanIn, fanOut } = countDegrees(nodes, edges);
2142
+ const utilization = weightedInbound(nodes, edges);
2143
+ const out = [];
2144
+ for (const n of nodes) {
2145
+ if (n.kind === "symbol") {
2146
+ out.push({ nodeId: n.id, name: "utilization", value: utilization.get(n.id) ?? 0, unit: "count" });
2147
+ continue;
2148
+ }
2149
+ const ci = fanIn.get(n.id) ?? 0;
2150
+ const co = fanOut.get(n.id) ?? 0;
2151
+ out.push({ nodeId: n.id, name: "fan_in", value: ci, unit: "count" });
2152
+ out.push({ nodeId: n.id, name: "fan_out", value: co, unit: "count" });
2153
+ out.push({ nodeId: n.id, name: "utilization", value: utilization.get(n.id) ?? 0, unit: "count" });
2154
+ if (ci + co > 0) {
2155
+ out.push({
2156
+ nodeId: n.id,
2157
+ name: "instability",
2158
+ value: co / (ci + co),
2159
+ unit: "ratio"
2160
+ });
2161
+ }
2162
+ }
2163
+ return out;
2164
+ }
2165
+
2166
+ // src/index-metrics.ts
2167
+ function symbolNamesByFile(nodes) {
2168
+ const out = /* @__PURE__ */ new Map();
2169
+ for (const n of nodes) {
2170
+ if (n.kind !== "symbol" || !n.parentId) continue;
2171
+ const bucket = out.get(n.parentId);
2172
+ if (bucket) bucket.add(n.name);
2173
+ else out.set(n.parentId, /* @__PURE__ */ new Set([n.name]));
2174
+ }
2175
+ return out;
2176
+ }
2177
+ function buildIndexerMetrics(input) {
2178
+ const nodeList = [...input.nodes.values()];
2179
+ return [
2180
+ ...computeMetrics(nodeList, [...input.edges.values()]),
2181
+ ...computeSourceMetrics(
2182
+ input.parsedFiles,
2183
+ (p) => fileId(input.idRoot, p),
2184
+ symbolNamesByFile(nodeList)
2185
+ ),
2186
+ ...input.reusedSourceMetrics
2187
+ ];
2188
+ }
2189
+
2190
+ // src/merge.ts
2191
+ function edgeKey(edge) {
2192
+ return JSON.stringify([edge.srcId, edge.dstId, edge.kind]);
2193
+ }
2194
+ function mergeFragments(fragments) {
2195
+ const nodes = /* @__PURE__ */ new Map();
2196
+ const edges = /* @__PURE__ */ new Map();
2197
+ for (const fragment of fragments) {
2198
+ for (const node of fragment.nodes) if (!nodes.has(node.id)) nodes.set(node.id, node);
2199
+ for (const edge of fragment.edges) {
2200
+ const key = edgeKey(edge);
2201
+ if (!edges.has(key)) edges.set(key, edge);
2202
+ }
2203
+ }
2204
+ return { nodes, edges };
2205
+ }
2206
+
2207
+ // src/indexer.ts
2208
+ var INDEX_VERSION = "0.11.0";
2209
+ var LANGUAGES = ["typescript", "python"];
2210
+ function canonicalizePath(p) {
2211
+ try {
2212
+ return realpathSync(p);
2213
+ } catch {
2214
+ return p;
2215
+ }
2216
+ }
2217
+ function normalizePaths(paths) {
2218
+ if (paths.length === 0) throw new Error("indexPaths requires at least one path");
2219
+ return [...new Set(paths.map((p) => path10.resolve(p)))];
2220
+ }
2221
+ function countByKind(items) {
2222
+ const out = {};
2223
+ for (const item of items) out[item.kind] = (out[item.kind] ?? 0) + 1;
2224
+ return out;
2225
+ }
2226
+ function persist(store, ref, commitHash, accumulator, aliases, metrics) {
2227
+ const snapshotId = store.createSnapshot({ ref, commitHash, indexVersion: INDEX_VERSION });
2228
+ store.insertNodes(snapshotId, [...accumulator.nodes.values()]);
2229
+ store.insertEdges(snapshotId, [...accumulator.edges.values()]);
2230
+ if (aliases.length > 0) store.insertAliases(snapshotId, aliases);
2231
+ if (metrics.length > 0) store.insertMetrics(snapshotId, metrics);
2232
+ return snapshotId;
2233
+ }
2234
+ function findPriorCommit(store) {
2235
+ return store.listSnapshots({ limit: 50 }).find((s) => s.commitHash)?.commitHash ?? null;
2236
+ }
2237
+ function resolveAliases(rootDir, idRoot, options, store) {
2238
+ if (options.detectRenames === false) return [];
2239
+ if (!isInsideGitRepo(rootDir)) return [];
2240
+ const priorCommit = findPriorCommit(store);
2241
+ if (!priorCommit) return [];
2242
+ const target = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2243
+ if (target === priorCommit) return [];
2244
+ return buildAliases(idRoot, detectRenames({ repoRoot: rootDir, fromCommit: priorCommit, toCommit: target }));
2245
+ }
2246
+ function resolveRoots(paths) {
2247
+ const raw = normalizePaths(paths);
2248
+ const gitToplevel = detectGitToplevel(raw[0]);
2249
+ const rootDirs = gitToplevel !== null ? raw.map(canonicalizePath) : raw;
2250
+ return { rootDirs, idRoot: gitToplevel ?? rootDirs[0] };
2251
+ }
2252
+ async function parseAll(toParse) {
2253
+ const parsed = /* @__PURE__ */ new Map();
2254
+ for (const rf of toParse) {
2255
+ parsed.set(rf.filePath, await parseFile(rf.content, rf.filePath, rf.language));
2256
+ }
2257
+ return parsed;
2258
+ }
2259
+ async function indexPaths(store, options) {
2260
+ const { rootDirs, idRoot } = resolveRoots(options.paths);
2261
+ const readFiles = await readSourceFiles(await walkSourceFiles(rootDirs, LANGUAGES));
2262
+ const reuse = options.incremental !== false ? loadReuseBasis(store, INDEX_VERSION) : null;
2263
+ const currentFileIds = new Set(readFiles.map((rf) => fileId(idRoot, rf.filePath)));
2264
+ const affected = reuse ? computeDeltaAffected(reuse, currentFileIds) : /* @__PURE__ */ new Set();
2265
+ const { toParse, reusedFileIds } = classifyForReuse(readFiles, idRoot, reuse, affected);
2266
+ const parsedByPath = await parseAll(toParse);
2267
+ const classified = classifyParsed(parsedByPath, idRoot, reuse, reusedFileIds, affected);
2268
+ const accumulator = mergeFragments(
2269
+ assembleFragments({
2270
+ readFiles,
2271
+ idRoot,
2272
+ parsedByPath,
2273
+ reuse,
2274
+ cosmeticFileIds: classified.cosmeticFileIds,
2275
+ extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig })
2276
+ })
2277
+ );
2278
+ const annotated = annotateRoles([...accumulator.nodes.values()], computeRoleHints(readFiles, idRoot, fileId));
2279
+ accumulator.nodes = new Map(annotated.map((n) => [n.id, n]));
2280
+ pruneDanglingReferences(accumulator.nodes, accumulator.edges);
2281
+ const metrics = options.computeMetrics === false ? [] : buildIndexerMetrics({
2282
+ nodes: accumulator.nodes,
2283
+ edges: accumulator.edges,
2284
+ parsedFiles: [...parsedByPath.values()],
2285
+ reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
2286
+ idRoot
2287
+ });
2288
+ const rootDir = rootDirs[0];
2289
+ const aliases = resolveAliases(rootDir, idRoot, options, store);
2290
+ const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2291
+ const snapshotId = persist(store, options.ref ?? "wd", commitHash, accumulator, aliases, metrics);
2292
+ store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
2293
+ return {
2294
+ snapshotId,
2295
+ files: readFiles.length,
2296
+ nodes: accumulator.nodes.size,
2297
+ edges: accumulator.edges.size,
2298
+ aliases: aliases.length,
2299
+ metrics: metrics.length,
2300
+ reused: reusedFileIds.length,
2301
+ reparsed: toParse.length,
2302
+ cosmetic: classified.cosmeticFileIds.size,
2303
+ nodesByKind: countByKind(accumulator.nodes.values()),
2304
+ edgesByKind: countByKind(accumulator.edges.values())
2305
+ };
2306
+ }
2307
+
2308
+ // src/aliases.ts
2309
+ var METRIC_ALIASES = /* @__PURE__ */ new Map([
2310
+ ["lines", "loc"],
2311
+ ["lines_of_code", "loc"],
2312
+ ["fan-in", "fan_in"],
2313
+ ["fanin", "fan_in"],
2314
+ ["fan-out", "fan_out"],
2315
+ ["fanout", "fan_out"],
2316
+ ["nesting_depth", "max_nesting_depth"],
2317
+ ["max-nesting-depth", "max_nesting_depth"],
2318
+ ["lcom4", "lcom4_max"]
2319
+ ]);
2320
+ var ROLE_ALIASES = /* @__PURE__ */ new Map([
2321
+ ["tests", "test"],
2322
+ ["spec", "test"],
2323
+ ["specs", "test"],
2324
+ ["fixtures", "fixture"],
2325
+ ["barrels", "barrel"]
2326
+ ]);
2327
+ var EDGE_KIND_ALIASES = /* @__PURE__ */ new Map([
2328
+ ["import", "imports"],
2329
+ ["call", "calls"],
2330
+ ["reference", "references"],
2331
+ ["re_exports", "re-exports"],
2332
+ ["reexports", "re-exports"],
2333
+ ["re-export", "re-exports"],
2334
+ ["depends_on", "depends-on"],
2335
+ ["dependson", "depends-on"]
2336
+ ]);
2337
+ function canonicalMetricName(name) {
2338
+ return METRIC_ALIASES.get(name) ?? name;
2339
+ }
2340
+ function canonicalRole(name) {
2341
+ return ROLE_ALIASES.get(name) ?? name;
2342
+ }
2343
+ function canonicalEdgeKind(name) {
2344
+ return EDGE_KIND_ALIASES.get(name) ?? name;
2345
+ }
2346
+
2347
+ // src/read.ts
2348
+ function listNodes(store, snapshotId, opts) {
2349
+ return store.listNodes(snapshotId, opts);
2350
+ }
2351
+ function listEdges(store, snapshotId, opts) {
2352
+ return store.listEdges(snapshotId, opts);
2353
+ }
2354
+ function listMetrics(store, snapshotId) {
2355
+ return store.listMetrics(snapshotId);
2356
+ }
2357
+ export {
2358
+ ALL_ROLES,
2359
+ CodeGraphStore,
2360
+ DOMAIN_DDL,
2361
+ INDEX_VERSION,
2362
+ KIT,
2363
+ LanguageExtractor,
2364
+ MIGRATIONS,
2365
+ PythonGraphExtractor,
2366
+ SOURCE_METRIC_NAMES,
2367
+ SYMBOL_ID_SEP,
2368
+ TsMorphGraphExtractor,
2369
+ annotateRoles,
2370
+ buildAliases,
2371
+ buildFileModuleNodes,
2372
+ buildIndexerMetrics,
2373
+ canonicalEdgeKind,
2374
+ canonicalMetricName,
2375
+ canonicalRole,
2376
+ classifyRole,
2377
+ collectDeclaredNames,
2378
+ collectDeclaredSpans,
2379
+ computeMetrics,
2380
+ computeRoleHints,
2381
+ computeSourceMetrics,
2382
+ detectGitHead,
2383
+ detectGitToplevel,
2384
+ detectRenames,
2385
+ edgeWeight,
2386
+ externalId,
2387
+ fileId,
2388
+ getLanguageFromPath,
2389
+ getSupportedLanguages,
2390
+ hashContent,
2391
+ indexPaths,
2392
+ isGeneratedByHeuristic,
2393
+ isGeneratedFile,
2394
+ isInsideGitRepo,
2395
+ listEdges,
2396
+ listMetrics,
2397
+ listNodes,
2398
+ loadGeneratedPatterns,
2399
+ moduleId,
2400
+ openCodeGraph,
2401
+ packageId,
2402
+ parentModuleId,
2403
+ parseFile,
2404
+ parseSymbolId,
2405
+ pruneDanglingReferences,
2406
+ readSourceFiles,
2407
+ resolveBarrelEdges,
2408
+ shouldIncludeFile,
2409
+ structuralSignature,
2410
+ symbolId,
2411
+ walkSourceFiles
2412
+ };
2413
+ //# sourceMappingURL=index.js.map