@titan-design/code-graph 0.1.1 → 0.3.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 CHANGED
@@ -1,5 +1,19 @@
1
+ import {
2
+ aggregateChurnWindows,
3
+ authorLinesByPath,
4
+ computeChangeCoupling,
5
+ computeOwnership,
6
+ couplingFor,
7
+ detectGitToplevel,
8
+ entriesWithin,
9
+ loadChurnEntries,
10
+ loadFileFirstSeen,
11
+ runGit,
12
+ summarizeOwnership
13
+ } from "./chunk-PFI5XMUG.js";
14
+
1
15
  // src/store.ts
2
- import { openDatabase, runMigrations } from "@titan-design/store-sqlite";
16
+ import { openDatabase, quoteIdent, runMigrations } from "@titan-design/store-sqlite";
3
17
 
4
18
  // src/schema.ts
5
19
  import { kitMigration } from "@titan-design/store-sqlite";
@@ -48,11 +62,13 @@ var DOMAIN_DDL = `
48
62
  PRIMARY KEY (snapshot_id, file_id)
49
63
  );
50
64
  `;
65
+ var SNAPSHOT_SCOPED_TABLES = ["node", "edge", "metric", "id_alias", "file_fingerprint"];
51
66
  var MIGRATIONS = [
52
67
  kitMigration(1, { snapshot: KIT.snapshot, entitySnap: KIT.entitySnap, cacheBlob: KIT.cacheBlob }),
53
68
  { version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
54
69
  { version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
55
70
  ];
71
+ var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.version));
56
72
 
57
73
  // src/rows.ts
58
74
  function rowToSnapshot(row) {
@@ -107,15 +123,81 @@ function rowToFingerprint(row) {
107
123
  };
108
124
  }
109
125
 
126
+ // src/store-reads.ts
127
+ var EDGE_COLS = "src_id, dst_id, kind, attrs";
128
+ var AGGREGATE_SELECT = `SELECT m.name AS name, n.kind AS node_kind, COUNT(m.value) AS count,
129
+ SUM(m.value) AS sum, MIN(m.value) AS min, MAX(m.value) AS max
130
+ FROM metric m CROSS JOIN node n ON n.snapshot_id = m.snapshot_id AND n.id = m.node_id`;
131
+ var TOP_SELECT = `SELECT m.node_id, m.value, m.unit, n.kind, n.name, n.role
132
+ FROM metric m JOIN node n ON n.snapshot_id = m.snapshot_id AND n.id = m.node_id
133
+ WHERE m.snapshot_id = ? AND m.name = ?`;
134
+ var TOP_ORDER = "ORDER BY m.value DESC LIMIT ?";
135
+ function prepareTargetedStatements(db) {
136
+ return {
137
+ listMetricsForNode: db.prepare(
138
+ "SELECT node_id, name, value, unit FROM metric WHERE snapshot_id = ? AND node_id = ?"
139
+ ),
140
+ // The second arm skips self-loops so an edge from a node to itself comes back once.
141
+ listEdgesTouching: db.prepare(
142
+ `SELECT ${EDGE_COLS} FROM edge WHERE snapshot_id = @snapshotId AND src_id = @nodeId
143
+ UNION ALL
144
+ SELECT ${EDGE_COLS} FROM edge WHERE snapshot_id = @snapshotId AND dst_id = @nodeId AND src_id <> @nodeId`
145
+ ),
146
+ aggregateMetric: db.prepare(
147
+ `${AGGREGATE_SELECT} WHERE m.snapshot_id = ? AND m.name = ? GROUP BY n.kind ORDER BY n.kind`
148
+ ),
149
+ aggregateMetrics: db.prepare(
150
+ `${AGGREGATE_SELECT} WHERE m.snapshot_id = ? GROUP BY m.name, n.kind ORDER BY m.name, n.kind`
151
+ ),
152
+ listMetricNames: db.prepare("SELECT DISTINCT name FROM metric WHERE snapshot_id = ? ORDER BY name"),
153
+ topByMetric: db.prepare(`${TOP_SELECT} ${TOP_ORDER}`),
154
+ topByMetricOfKind: db.prepare(`${TOP_SELECT} AND n.kind = ? ${TOP_ORDER}`)
155
+ };
156
+ }
157
+ function readMetricsForNode(stmts, snapshotId, nodeId) {
158
+ return stmts.listMetricsForNode.all(snapshotId, nodeId).map(rowToMetric);
159
+ }
160
+ function readEdgesTouching(stmts, snapshotId, nodeId) {
161
+ return stmts.listEdgesTouching.all({ snapshotId, nodeId }).map(rowToEdge);
162
+ }
163
+ function readMetricNames(stmts, snapshotId) {
164
+ return stmts.listMetricNames.all(snapshotId).map((r) => r.name);
165
+ }
166
+ function readTopByMetric(stmts, opts) {
167
+ const limit = opts.limit ?? 20;
168
+ const rows = opts.kind === void 0 ? stmts.topByMetric.all(opts.snapshotId, opts.metric, limit) : stmts.topByMetricOfKind.all(opts.snapshotId, opts.metric, opts.kind, limit);
169
+ return rows.map((r) => ({
170
+ nodeId: r.node_id,
171
+ name: r.name,
172
+ kind: r.kind,
173
+ role: r.role,
174
+ value: r.value,
175
+ unit: r.unit
176
+ }));
177
+ }
178
+ function readMetricAggregates(stmts, snapshotId, name) {
179
+ const rows = name === void 0 ? stmts.aggregateMetrics.all(snapshotId) : stmts.aggregateMetric.all(snapshotId, name);
180
+ return rows.map((r) => ({
181
+ name: r.name,
182
+ nodeKind: r.node_kind,
183
+ count: r.count,
184
+ sum: r.sum,
185
+ min: r.min,
186
+ max: r.max
187
+ }));
188
+ }
189
+
110
190
  // src/store.ts
111
191
  var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
112
192
  var CodeGraphStore = class {
113
193
  constructor(db) {
114
194
  this.db = db;
115
195
  this.statements = prepareStatements(db);
196
+ this.targeted = prepareTargetedStatements(db);
116
197
  }
117
198
  db;
118
199
  statements;
200
+ targeted;
119
201
  createSnapshot(input) {
120
202
  const result = this.statements.insertSnapshot.run({
121
203
  ref: input.ref,
@@ -164,6 +246,21 @@ var CodeGraphStore = class {
164
246
  }
165
247
  })();
166
248
  }
249
+ /**
250
+ * Replace every metric of one `name` for a snapshot in a single transaction —
251
+ * for an overlay such as coverage, which is re-ingested wholesale and must not
252
+ * accumulate stale rows across runs.
253
+ */
254
+ replaceMetricsByName(snapshotId, name, metrics) {
255
+ const del = this.statements.deleteMetricsByName;
256
+ const insert = this.statements.insertMetric;
257
+ this.db.transaction(() => {
258
+ del.run(snapshotId, name);
259
+ for (const m of metrics) {
260
+ insert.run({ snapshotId, nodeId: m.nodeId, name: m.name, value: m.value, unit: m.unit ?? null });
261
+ }
262
+ })();
263
+ }
167
264
  insertAliases(snapshotId, aliases) {
168
265
  const stmt = this.statements.insertAlias;
169
266
  this.db.transaction(() => {
@@ -218,12 +315,53 @@ var CodeGraphStore = class {
218
315
  listMetrics(snapshotId) {
219
316
  return this.statements.listMetrics.all(snapshotId).map(rowToMetric);
220
317
  }
318
+ /** Every metric on one node, from the primary key instead of a whole-snapshot read. */
319
+ listMetricsForNode(snapshotId, nodeId) {
320
+ return readMetricsForNode(this.targeted, snapshotId, nodeId);
321
+ }
322
+ /** Distinct metric names stored for a snapshot, ascending. */
323
+ listMetricNames(snapshotId) {
324
+ return readMetricNames(this.targeted, snapshotId);
325
+ }
326
+ /** The highest-valued nodes for one metric, descending; `limit` defaults to 20. */
327
+ topByMetric(opts) {
328
+ return readTopByMetric(this.targeted, opts);
329
+ }
330
+ /** Edges into or out of one node; `references` edges are excluded unless asked, as in {@link listEdges}. */
331
+ listEdgesTouching(snapshotId, nodeId, opts) {
332
+ const edges = readEdgesTouching(this.targeted, snapshotId, nodeId);
333
+ return opts?.includeReferences ? edges : edges.filter((e) => e.kind !== "references");
334
+ }
335
+ /** Count, sum, min, and max per metric name and node kind; pass `name` for one metric. */
336
+ aggregateMetrics(snapshotId, opts) {
337
+ return readMetricAggregates(this.targeted, snapshotId, opts?.name);
338
+ }
221
339
  listAliases(snapshotId) {
222
340
  return this.statements.listAliases.all(snapshotId).map(rowToAlias);
223
341
  }
224
342
  listFingerprints(snapshotId) {
225
343
  return this.statements.listFingerprints.all(snapshotId).map(rowToFingerprint);
226
344
  }
345
+ /** Drop whole snapshots. The domain tables carry no foreign key, so each is cleared by hand. */
346
+ deleteSnapshots(ids) {
347
+ if (ids.length === 0) return;
348
+ this.db.transaction(() => {
349
+ for (const id of ids) {
350
+ for (const stmt of this.statements.deleteBySnapshot) stmt.run(id);
351
+ }
352
+ })();
353
+ }
354
+ vacuum() {
355
+ this.db.exec("VACUUM");
356
+ }
357
+ countRowsByTable(tables) {
358
+ const out = {};
359
+ for (const table of tables) {
360
+ const row = this.db.prepare(`SELECT COUNT(*) AS n FROM ${quoteIdent(table)}`).get();
361
+ out[table] = row.n;
362
+ }
363
+ return out;
364
+ }
227
365
  close() {
228
366
  this.db.close();
229
367
  }
@@ -266,130 +404,54 @@ function prepareStatements(db) {
266
404
  listAliases: db.prepare("SELECT old_id, new_id, reason FROM id_alias WHERE snapshot_id = ?"),
267
405
  listFingerprints: db.prepare(
268
406
  "SELECT file_id, content_hash, structural_hash FROM file_fingerprint WHERE snapshot_id = ?"
407
+ ),
408
+ deleteMetricsByName: db.prepare("DELETE FROM metric WHERE snapshot_id = ? AND name = ?"),
409
+ deleteBySnapshot: [...SNAPSHOT_SCOPED_TABLES, "snapshot"].map(
410
+ (table) => db.prepare(`DELETE FROM ${quoteIdent(table)} WHERE ${table === "snapshot" ? "id" : "snapshot_id"} = ?`)
269
411
  )
270
412
  };
271
413
  }
272
414
  function openCodeGraph(dbPath) {
273
- const db = openDatabase(dbPath);
415
+ const db = openDatabase(dbPath, { schemaVersion: SCHEMA_VERSION });
274
416
  runMigrations(db, MIGRATIONS);
275
417
  return new CodeGraphStore(db);
276
418
  }
277
419
 
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;
420
+ // src/prune.ts
421
+ var CASCADE_TABLES = SNAPSHOT_SCOPED_TABLES;
422
+ function planPrune(store, options = {}) {
423
+ const keepCount = options.keep ?? 10;
424
+ const keepRefs = new Set(options.keepRefs ?? []);
425
+ const all = store.listSnapshots({ limit: 1e6 });
426
+ const keep = [];
427
+ const remove = [];
428
+ for (let i = 0; i < all.length; i++) {
429
+ const snap = all[i];
430
+ if (i < keepCount || keepRefs.has(snap.ref)) {
431
+ keep.push(snap);
432
+ } else {
433
+ remove.push(snap);
386
434
  }
387
435
  }
388
- return null;
436
+ return { keep, remove };
437
+ }
438
+ function runPrune(store, options = {}) {
439
+ const plan = planPrune(store, options);
440
+ const tables = ["snapshot", ...CASCADE_TABLES];
441
+ const rowsBefore = store.countRowsByTable(tables);
442
+ store.deleteSnapshots(plan.remove.map((s) => s.id));
443
+ if (options.vacuum) store.vacuum();
444
+ const rowsAfter = store.countRowsByTable(tables);
445
+ return { plan, rowsBefore, rowsAfter, vacuumed: !!options.vacuum };
389
446
  }
390
447
 
448
+ // src/indexer.ts
449
+ import { realpathSync } from "fs";
450
+ import * as path9 from "path";
451
+ import { parseFile } from "@titan-design/code-parser";
452
+
391
453
  // src/extractors/ts-morph-extractor.ts
392
- import * as path5 from "path";
454
+ import * as path4 from "path";
393
455
  import { existsSync } from "fs";
394
456
  import {
395
457
  Project,
@@ -399,16 +461,17 @@ import {
399
461
  } from "ts-morph";
400
462
 
401
463
  // src/extractors/file-nodes.ts
402
- import * as path3 from "path";
464
+ import * as path2 from "path";
465
+ import { getLanguageFromPath } from "@titan-design/code-parser";
403
466
 
404
467
  // src/extractors/ids.ts
405
- import * as path2 from "path";
468
+ import * as path from "path";
406
469
  var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
407
470
  function toPosix(p) {
408
- return p.split(path2.sep).join("/");
471
+ return p.split(path.sep).join("/");
409
472
  }
410
473
  function repoRelative(repoRoot, absPath) {
411
- const rel = path2.relative(repoRoot, absPath);
474
+ const rel = path.relative(repoRoot, absPath);
412
475
  return toPosix(rel);
413
476
  }
414
477
  function fileId(repoRoot, absPath) {
@@ -427,8 +490,8 @@ function packageId(name) {
427
490
  return name;
428
491
  }
429
492
  var SYMBOL_ID_SEP = "#";
430
- function symbolId(fileId2, exportName) {
431
- return `${fileId2}${SYMBOL_ID_SEP}${exportName}`;
493
+ function symbolId(fileId2, qualifiedName) {
494
+ return `${fileId2}${SYMBOL_ID_SEP}${qualifiedName}`;
432
495
  }
433
496
  function parseSymbolId(id) {
434
497
  const idx = id.indexOf(SYMBOL_ID_SEP);
@@ -457,8 +520,8 @@ function buildFileModuleNodes(repoRoot, absPath) {
457
520
  const parentId = parentModuleId(mId) ?? void 0;
458
521
  const language = getLanguageFromPath(absPath) ?? "typescript";
459
522
  return [
460
- { id: fId, kind: "file", name: path3.basename(fId), parentId: mId, language },
461
- { id: mId, kind: "module", name: path3.basename(mId), parentId, language }
523
+ { id: fId, kind: "file", name: path2.basename(fId), parentId: mId, language },
524
+ { id: mId, kind: "module", name: path2.basename(mId), parentId, language }
462
525
  ];
463
526
  }
464
527
 
@@ -534,6 +597,65 @@ function isPropertyNamePosition(id) {
534
597
  return false;
535
598
  }
536
599
 
600
+ // src/scope-path.ts
601
+ var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
602
+ "class_declaration",
603
+ "abstract_class_declaration",
604
+ "class",
605
+ "function_declaration",
606
+ "generator_function_declaration",
607
+ "method_definition",
608
+ "internal_module"
609
+ ]);
610
+ var TS_BOUND_SCOPES = /* @__PURE__ */ new Set([
611
+ "class",
612
+ "arrow_function",
613
+ "function_expression",
614
+ "generator_function",
615
+ "object"
616
+ ]);
617
+ var PY_NAMED_SCOPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
618
+ var ANONYMOUS_SCOPE = "<anonymous>";
619
+ function qualify(scope, name) {
620
+ return scope ? `${scope}.${name}` : name;
621
+ }
622
+ function bindingName(node) {
623
+ const parent = node.parent;
624
+ if (!parent) return null;
625
+ if (parent.type === "variable_declarator" || parent.type === "public_field_definition") {
626
+ return parent.childForFieldName("name")?.text ?? null;
627
+ }
628
+ if (parent.type === "pair") return parent.childForFieldName("key")?.text ?? null;
629
+ if (parent.type === "export_statement") return "default";
630
+ return null;
631
+ }
632
+ function scopeSegment(node, python) {
633
+ if (python) {
634
+ return PY_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text ?? null : null;
635
+ }
636
+ const own = TS_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text : void 0;
637
+ if (own) return own;
638
+ if (!TS_BOUND_SCOPES.has(node.type)) return null;
639
+ return bindingName(node) ?? ANONYMOUS_SCOPE;
640
+ }
641
+ function enter(scope, segment) {
642
+ if (!segment) return scope;
643
+ if (segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`))) {
644
+ return scope;
645
+ }
646
+ return qualify(scope, segment);
647
+ }
648
+ function walkScopes(root, python, visit) {
649
+ const step2 = (node, scope) => {
650
+ visit(node, scope);
651
+ const inner = enter(scope, scopeSegment(node, python));
652
+ for (const child of node.children) {
653
+ if (child) step2(child, inner);
654
+ }
655
+ };
656
+ step2(root, "");
657
+ }
658
+
537
659
  // src/declared-names.ts
538
660
  var TS_DECL_TYPES = /* @__PURE__ */ new Set([
539
661
  "function_declaration",
@@ -541,22 +663,24 @@ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
541
663
  "class_declaration"
542
664
  ]);
543
665
  var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
666
+ function collectDeclarations(file) {
667
+ const python = file.language === "python";
668
+ const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
669
+ const out = [];
670
+ walkScopes(file.tree.rootNode, python, (node, scope) => {
671
+ const named = declaredNodeAt(node, declTypes);
672
+ if (!named) return;
673
+ out.push({
674
+ name: named.name,
675
+ qualifiedName: qualify(scope, named.name),
676
+ span: { startLine: named.node.startPosition.row + 1, endLine: named.node.endPosition.row + 1 }
677
+ });
678
+ });
679
+ return out;
680
+ }
544
681
  function collectDeclaredSpans(file) {
545
- const declTypes = file.language === "python" ? PY_DECL_TYPES : TS_DECL_TYPES;
546
682
  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);
683
+ for (const d of collectDeclarations(file)) spans.set(d.qualifiedName, d.span);
560
684
  return spans;
561
685
  }
562
686
  function collectDeclaredNames(file) {
@@ -754,7 +878,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
754
878
  }
755
879
 
756
880
  // src/extractors/module-resolution.ts
757
- import * as path4 from "path";
881
+ import * as path3 from "path";
758
882
  var RESOLVABLE_EXTS = [
759
883
  ".ts",
760
884
  ".tsx",
@@ -776,20 +900,20 @@ function* relativeResolutionCandidates(base) {
776
900
  const stem = base.slice(0, base.length - jsExt[0].length);
777
901
  for (const ext of RESOLVABLE_EXTS) yield stem + ext;
778
902
  }
779
- for (const ext of RESOLVABLE_EXTS) yield path4.join(base, "index" + ext);
903
+ for (const ext of RESOLVABLE_EXTS) yield path3.join(base, "index" + ext);
780
904
  }
781
905
  function resolveRelativeAbs(fs3, fromAbs, specifier) {
782
906
  if (!isRelativeSpecifier(specifier)) return null;
783
- const base = path4.resolve(path4.dirname(fromAbs), specifier);
907
+ const base = path3.resolve(path3.dirname(fromAbs), specifier);
784
908
  for (const candidate of relativeResolutionCandidates(base)) {
785
909
  if (fs3.fileExistsSync(candidate)) return candidate;
786
910
  }
787
911
  return null;
788
912
  }
789
913
  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;
914
+ const relative4 = path3.relative(repoRoot, abs);
915
+ if (relative4.startsWith("..") || path3.isAbsolute(relative4)) return null;
916
+ if (relative4.split(path3.sep).includes("node_modules")) return null;
793
917
  return fileId(repoRoot, abs);
794
918
  }
795
919
 
@@ -1028,7 +1152,7 @@ function remapDistToSrc(abs) {
1028
1152
  const base = m[1];
1029
1153
  const sub = m[2];
1030
1154
  for (const ext of [".ts", ".tsx"]) {
1031
- const candidate = path5.join(base, "src", sub + ext);
1155
+ const candidate = path4.join(base, "src", sub + ext);
1032
1156
  if (existsSync(candidate)) return candidate;
1033
1157
  }
1034
1158
  return abs;
@@ -1038,11 +1162,11 @@ function isTypeScriptFile(file) {
1038
1162
  }
1039
1163
 
1040
1164
  // src/extractors/python-extractor.ts
1041
- import * as path6 from "path";
1165
+ import * as path5 from "path";
1042
1166
  import { existsSync as existsSync2 } from "fs";
1043
1167
  function* candidates(base) {
1044
1168
  yield `${base}.py`;
1045
- yield path6.join(base, "__init__.py");
1169
+ yield path5.join(base, "__init__.py");
1046
1170
  }
1047
1171
  var PythonGraphExtractor = class {
1048
1172
  constructor(repoRoot) {
@@ -1079,10 +1203,10 @@ var PythonGraphExtractor = class {
1079
1203
  resolveDotted(fromAbs, specifier) {
1080
1204
  const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
1081
1205
  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];
1206
+ const relativeBase = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1207
+ const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
1084
1208
  for (const root of roots) {
1085
- for (const candidate of candidates(path6.join(root, ...parts))) {
1209
+ for (const candidate of candidates(path5.join(root, ...parts))) {
1086
1210
  const id = inRepoFileId(this.repoRoot, candidate);
1087
1211
  if (id !== null && existsSync2(candidate)) return id;
1088
1212
  }
@@ -1090,6 +1214,9 @@ var PythonGraphExtractor = class {
1090
1214
  return null;
1091
1215
  }
1092
1216
  };
1217
+ function isPublicName(qualifiedName) {
1218
+ return !qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1).startsWith("_");
1219
+ }
1093
1220
  function symbolNodes(fId, file) {
1094
1221
  return [...collectDeclaredSpans(file)].map(([name, span]) => ({
1095
1222
  id: symbolId(fId, name),
@@ -1097,7 +1224,7 @@ function symbolNodes(fId, file) {
1097
1224
  name,
1098
1225
  parentId: fId,
1099
1226
  language: "python",
1100
- attrs: { exported: !name.startsWith("_"), startLine: span.startLine, endLine: span.endLine }
1227
+ attrs: { exported: isPublicName(name), startLine: span.startLine, endLine: span.endLine }
1101
1228
  }));
1102
1229
  }
1103
1230
  function importSpecifiers(root) {
@@ -1130,16 +1257,15 @@ var LanguageExtractor = class {
1130
1257
  };
1131
1258
 
1132
1259
  // src/git-renames.ts
1133
- import { execFileSync } from "child_process";
1134
- import * as path7 from "path";
1260
+ import * as path6 from "path";
1135
1261
  function detectGitHead(repoRoot) {
1136
1262
  return runGit(repoRoot, ["rev-parse", "HEAD"]);
1137
1263
  }
1138
1264
  function isInsideGitRepo(repoRoot) {
1139
1265
  return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
1140
1266
  }
1141
- function detectGitToplevel(cwd) {
1142
- return runGit(cwd, ["rev-parse", "--show-toplevel"]);
1267
+ function resolveGitRef(repoRoot, ref) {
1268
+ return runGit(repoRoot, ["rev-parse", ref]);
1143
1269
  }
1144
1270
  function detectRenames(options) {
1145
1271
  const threshold = options.similarityThreshold ?? 80;
@@ -1172,12 +1298,12 @@ function parseRenameOutput(text) {
1172
1298
  return out;
1173
1299
  }
1174
1300
  function classifyRename(oldPath, newPath) {
1175
- return path7.posix.dirname(oldPath) === path7.posix.dirname(newPath) ? "rename" : "move";
1301
+ return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
1176
1302
  }
1177
1303
  function renameToAliases(repoRoot, pair) {
1178
1304
  const reason = classifyRename(pair.oldPath, pair.newPath);
1179
- const oldAbs = path7.join(repoRoot, pair.oldPath);
1180
- const newAbs = path7.join(repoRoot, pair.newPath);
1305
+ const oldAbs = path6.join(repoRoot, pair.oldPath);
1306
+ const newAbs = path6.join(repoRoot, pair.newPath);
1181
1307
  return [
1182
1308
  { oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
1183
1309
  { oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
@@ -1195,29 +1321,164 @@ function buildAliases(repoRoot, pairs) {
1195
1321
  }
1196
1322
  return out;
1197
1323
  }
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;
1324
+
1325
+ // src/identity/lineage.ts
1326
+ var ALIAS_BASE_ATTR = "aliasBase";
1327
+ var DEFAULT_MAX_HOPS = 1e4;
1328
+ function recordedBase(snap) {
1329
+ if (!(ALIAS_BASE_ATTR in snap.attrs)) return void 0;
1330
+ const base = snap.attrs[ALIAS_BASE_ATTR];
1331
+ return typeof base === "number" && base < snap.id ? base : null;
1332
+ }
1333
+ function buildLineage(snapshots) {
1334
+ const out = /* @__PURE__ */ new Map();
1335
+ let lastWithCommit = null;
1336
+ for (const snap of [...snapshots].sort((a, b) => a.id - b.id)) {
1337
+ const recorded = recordedBase(snap);
1338
+ out.set(snap.id, recorded === void 0 ? lastWithCommit : recorded);
1339
+ if (snap.commitHash) lastWithCommit = snap.id;
1340
+ }
1341
+ return out;
1204
1342
  }
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;
1343
+ function ancestry(lineage, snapshotId, maxHops = DEFAULT_MAX_HOPS) {
1344
+ const out = [];
1345
+ let current = snapshotId;
1346
+ while (current !== null && current !== void 0 && out.length <= maxHops) {
1347
+ out.push(current);
1348
+ current = lineage.get(current);
1349
+ }
1350
+ return out;
1351
+ }
1352
+ function lineagePath(lineage, fromId, toId, maxHops = DEFAULT_MAX_HOPS) {
1353
+ const up = ancestry(lineage, fromId, maxHops);
1354
+ const down = ancestry(lineage, toId, maxHops);
1355
+ const upIndex = new Map(up.map((id, i) => [id, i]));
1356
+ const meet = down.findIndex((id) => upIndex.has(id));
1357
+ if (meet < 0) return null;
1358
+ const backward = up.slice(0, upIndex.get(down[meet])).map((id) => step(id, "backward"));
1359
+ const forward = down.slice(0, meet).reverse().map((id) => step(id, "forward"));
1360
+ return [...backward, ...forward];
1361
+ }
1362
+ function rootPath(lineage, toId, maxHops = DEFAULT_MAX_HOPS) {
1363
+ return ancestry(lineage, toId, maxHops).reverse().map((id) => step(id, "forward"));
1364
+ }
1365
+ function step(snapshotId, direction) {
1366
+ return { snapshotId, direction };
1367
+ }
1368
+
1369
+ // src/identity/alias-chain.ts
1370
+ function stepMap(aliases, direction) {
1371
+ const byId = /* @__PURE__ */ new Map();
1372
+ const sorted = [...aliases].sort((a, b) => compare(a.oldId, b.oldId) || compare(a.newId, b.newId));
1373
+ for (const alias of sorted) {
1374
+ const key = direction === "forward" ? alias.oldId : alias.newId;
1375
+ if (!byId.has(key)) byId.set(key, alias);
1376
+ }
1377
+ return { direction, byId };
1378
+ }
1379
+ function compare(a, b) {
1380
+ return a < b ? -1 : a > b ? 1 : 0;
1381
+ }
1382
+ function overallReason(hops) {
1383
+ if (hops.length === 0) return void 0;
1384
+ return hops.some((h) => h.reason === "move") ? "move" : hops[hops.length - 1].reason;
1385
+ }
1386
+ function walk(requestedId, maps) {
1387
+ let id = requestedId;
1388
+ const hops = [];
1389
+ for (const { direction, byId } of maps) {
1390
+ const alias = byId.get(id);
1391
+ if (!alias) continue;
1392
+ hops.push(alias);
1393
+ id = direction === "forward" ? alias.newId : alias.oldId;
1394
+ }
1395
+ return { requestedId, id, hops, reason: overallReason(hops) };
1396
+ }
1397
+ function chainSteps(input) {
1398
+ if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
1399
+ const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1400
+ if (path11) return { steps: path11, connected: true };
1401
+ return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
1402
+ }
1403
+ function createAliasChain(input) {
1404
+ const { steps, connected } = chainSteps(input);
1405
+ const maps = steps.map((s) => stepMap(input.loadAliases(s.snapshotId), s.direction));
1406
+ const cache = /* @__PURE__ */ new Map();
1407
+ const trace = (id) => {
1408
+ let hit = cache.get(id);
1409
+ if (!hit) cache.set(id, hit = walk(id, maps));
1410
+ return hit;
1411
+ };
1412
+ return { fromSnapshotId: input.from, toSnapshotId: input.to, connected, steps, resolve: (id) => trace(id).id, trace };
1413
+ }
1414
+
1415
+ // src/identity/store-identity.ts
1416
+ function allSnapshots(store) {
1417
+ return store.db.prepare("SELECT * FROM snapshot ORDER BY id").all().map(rowToSnapshot);
1418
+ }
1419
+ function loadLineage(store) {
1420
+ return buildLineage(allSnapshots(store));
1421
+ }
1422
+ function aliasChain(store, fromSnapshotId, toSnapshotId, options = {}) {
1423
+ return createAliasChain({
1424
+ lineage: loadLineage(store),
1425
+ loadAliases: (id) => store.listAliases(id),
1426
+ from: fromSnapshotId,
1427
+ to: toSnapshotId,
1428
+ maxHops: options.maxHops
1429
+ });
1430
+ }
1431
+ function resolveAlias(store, id, toSnapshotId, options = {}) {
1432
+ return aliasChain(store, options.fromSnapshotId ?? null, toSnapshotId, options).trace(id);
1433
+ }
1434
+ function priorSnapshotForRef(store, ref, options = {}) {
1435
+ const before = options.before ?? Number.POSITIVE_INFINITY;
1436
+ const candidates2 = allSnapshots(store).filter((s) => s.id < before).reverse();
1437
+ const commit = options.repoRoot ? resolveGitRef(options.repoRoot, `${ref}^{commit}`) : null;
1438
+ const byCommit = commit ? candidates2.find((s) => s.commitHash === commit) : void 0;
1439
+ return byCommit ?? candidates2.find((s) => s.ref === ref) ?? null;
1440
+ }
1441
+ function aliasBaseFor(store, ref) {
1442
+ const committed = allSnapshots(store).filter((s) => s.commitHash).reverse();
1443
+ return committed.find((s) => s.ref === ref) ?? committed[0] ?? null;
1444
+ }
1445
+
1446
+ // src/identity/index-aliases.ts
1447
+ var SYMBOLS_OF_FILE = `SELECT id FROM node
1448
+ WHERE snapshot_id = ? AND id >= ? AND id < ? AND kind = 'symbol'`;
1449
+ function symbolIdsOf(store, snapshotId, fileId2) {
1450
+ const rows = store.db.prepare(SYMBOLS_OF_FILE).all(snapshotId, `${fileId2}#`, `${fileId2}$`);
1451
+ return rows.map((r) => r.id);
1452
+ }
1453
+ function movedSymbolAliases(store, baseSnapshotId, fileAliases, nodes) {
1454
+ const out = [];
1455
+ for (const alias of fileAliases) {
1456
+ if (nodes.get(alias.newId)?.kind !== "file") continue;
1457
+ for (const oldId of symbolIdsOf(store, baseSnapshotId, alias.oldId)) {
1458
+ const newId = alias.newId + SYMBOL_ID_SEP + oldId.slice(alias.oldId.length + 1);
1459
+ if (nodes.get(newId)?.kind === "symbol") out.push({ oldId, newId, reason: alias.reason });
1460
+ }
1215
1461
  }
1462
+ return out;
1463
+ }
1464
+ function renameAliases(input, baseCommit) {
1465
+ if (input.detectRenames === false || !isInsideGitRepo(input.rootDir)) return [];
1466
+ const target = input.commitHash ?? detectGitHead(input.rootDir) ?? void 0;
1467
+ if (target === baseCommit) return [];
1468
+ const pairs = detectRenames({ repoRoot: input.rootDir, fromCommit: baseCommit, toCommit: target });
1469
+ return buildAliases(input.idRoot, pairs);
1470
+ }
1471
+ function computeAliasBridge(store, input) {
1472
+ const base = aliasBaseFor(store, input.ref);
1473
+ if (!base?.commitHash) return { baseSnapshotId: null, aliases: [] };
1474
+ const fileAliases = renameAliases(input, base.commitHash);
1475
+ const symbols = movedSymbolAliases(store, base.id, fileAliases, input.nodes);
1476
+ return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
1216
1477
  }
1217
1478
 
1218
1479
  // src/generated.ts
1219
1480
  import { readFileSync } from "fs";
1220
- import * as path8 from "path";
1481
+ import * as path7 from "path";
1221
1482
  var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
1222
1483
  var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
1223
1484
  function isGeneratedByHeuristic(id) {
@@ -1229,7 +1490,7 @@ function isGeneratedFile(id, patterns) {
1229
1490
  }
1230
1491
  function loadGeneratedPatterns(rootDir) {
1231
1492
  try {
1232
- const content = readFileSync(path8.join(rootDir, ".gitattributes"), "utf-8");
1493
+ const content = readFileSync(path7.join(rootDir, ".gitattributes"), "utf-8");
1233
1494
  return parseGeneratedPatterns(content);
1234
1495
  } catch {
1235
1496
  return [];
@@ -1332,25 +1593,26 @@ function annotateRoles(nodes, options) {
1332
1593
 
1333
1594
  // src/file-walk.ts
1334
1595
  import * as fs from "fs/promises";
1335
- import * as path9 from "path";
1336
- async function walkSourceFiles(rootDirs, languages2) {
1596
+ import * as path8 from "path";
1597
+ import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
1598
+ async function walkSourceFiles(rootDirs, languages) {
1337
1599
  const seen = /* @__PURE__ */ new Set();
1338
1600
  const out = [];
1339
1601
  for (const rootDir of rootDirs) {
1340
- await collectFiles(rootDir, rootDir, languages2, seen, out);
1602
+ await collectFiles(rootDir, rootDir, languages, seen, out);
1341
1603
  }
1342
1604
  return out;
1343
1605
  }
1344
- async function collectFiles(dir, rootDir, languages2, seen, out) {
1606
+ async function collectFiles(dir, rootDir, languages, seen, out) {
1345
1607
  const entries = await fs.readdir(dir, { withFileTypes: true });
1346
1608
  for (const entry of entries) {
1347
- const full = path9.join(dir, entry.name);
1609
+ const full = path8.join(dir, entry.name);
1348
1610
  if (entry.isDirectory()) {
1349
1611
  if (isExcludedDir(entry.name)) continue;
1350
- await collectFiles(full, rootDir, languages2, seen, out);
1612
+ await collectFiles(full, rootDir, languages, seen, out);
1351
1613
  } else if (entry.isFile() && !seen.has(full)) {
1352
- const relative4 = path9.relative(rootDir, full);
1353
- if (shouldIncludeFile(relative4, [...languages2])) {
1614
+ const relative4 = path8.relative(rootDir, full);
1615
+ if (shouldIncludeFile(relative4, [...languages])) {
1354
1616
  seen.add(full);
1355
1617
  out.push(full);
1356
1618
  }
@@ -1421,6 +1683,7 @@ function resolveBarrelEdges(nodes, edges) {
1421
1683
  // src/incremental.ts
1422
1684
  import { createHash } from "crypto";
1423
1685
  import * as fs2 from "fs/promises";
1686
+ import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
1424
1687
 
1425
1688
  // src/cognitive-complexity.ts
1426
1689
  function cognitiveComplexityOf(body, language) {
@@ -1812,21 +2075,16 @@ function countLoc(content) {
1812
2075
  function analyzeFunctions(file) {
1813
2076
  const stats = [];
1814
2077
  const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
1815
- const visit = (node) => {
2078
+ walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
1816
2079
  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);
2080
+ if (!fn) return;
2081
+ stats.push({
2082
+ name: fn.name === null ? null : qualify(scope, fn.name),
2083
+ cyclomatic: cyclomaticOf(fn.body, file.language),
2084
+ cognitive: cognitiveComplexityOf(fn.body, file.language),
2085
+ nestingDepth: nestingDepthOf(fn.body, file.language, 0)
2086
+ });
2087
+ });
1830
2088
  return stats;
1831
2089
  }
1832
2090
  function functionAt(node, fnTypes) {
@@ -1872,6 +2130,259 @@ function cyclomaticOf(body, language) {
1872
2130
  return complexity;
1873
2131
  }
1874
2132
 
2133
+ // src/analysis/dead-code.ts
2134
+ var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
2135
+ "unreachable_statements",
2136
+ "unused_locals",
2137
+ "unused_params"
2138
+ ]);
2139
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
2140
+ "function_declaration",
2141
+ "function_expression",
2142
+ "arrow_function",
2143
+ "method_definition",
2144
+ "generator_function_declaration",
2145
+ "generator_function"
2146
+ ]);
2147
+ var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
2148
+ var TERMINALS = /* @__PURE__ */ new Set([
2149
+ "return_statement",
2150
+ "throw_statement",
2151
+ "break_statement",
2152
+ "continue_statement"
2153
+ ]);
2154
+ function computeDeadCodeMetrics(files, fileIdOf) {
2155
+ const out = [];
2156
+ for (const file of files) {
2157
+ if (file.language !== "typescript" && file.language !== "tsx") continue;
2158
+ const id = fileIdOf(file.filePath);
2159
+ const unreachable = countUnreachable(file.tree.rootNode);
2160
+ if (unreachable > 0) {
2161
+ out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
2162
+ }
2163
+ const { locals, params } = countUnusedBindings(file.tree.rootNode);
2164
+ if (locals > 0) {
2165
+ out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
2166
+ }
2167
+ if (params > 0) {
2168
+ out.push({ nodeId: id, name: "unused_params", value: params, unit: "count" });
2169
+ }
2170
+ }
2171
+ return out;
2172
+ }
2173
+ function countUnreachable(root) {
2174
+ let count = 0;
2175
+ const visit = (node) => {
2176
+ if (node.type === "statement_block") count += unreachableInBlock(node);
2177
+ for (const child of node.namedChildren) {
2178
+ if (child) visit(child);
2179
+ }
2180
+ };
2181
+ visit(root);
2182
+ return count;
2183
+ }
2184
+ function unreachableInBlock(block) {
2185
+ let seenTerminal = false;
2186
+ let count = 0;
2187
+ for (const child of block.namedChildren) {
2188
+ if (!child) continue;
2189
+ if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
2190
+ count++;
2191
+ }
2192
+ if (TERMINALS.has(child.type)) seenTerminal = true;
2193
+ }
2194
+ return count;
2195
+ }
2196
+ function countUnusedBindings(root) {
2197
+ let locals = 0;
2198
+ let params = 0;
2199
+ const visit = (node) => {
2200
+ if (FUNCTION_TYPES.has(node.type)) {
2201
+ const r = analyzeFunction(node);
2202
+ locals += r.locals;
2203
+ params += r.params;
2204
+ }
2205
+ for (const child of node.namedChildren) {
2206
+ if (child) visit(child);
2207
+ }
2208
+ };
2209
+ visit(root);
2210
+ return { locals, params };
2211
+ }
2212
+ function analyzeFunction(fn) {
2213
+ const declIds = /* @__PURE__ */ new Set();
2214
+ const nameCount = /* @__PURE__ */ new Map();
2215
+ collectBindingDecls(fn, declIds, nameCount);
2216
+ const refs = /* @__PURE__ */ new Map();
2217
+ countReferences(fn, declIds, refs);
2218
+ const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
2219
+ const paramNames = ownParamNames(fn);
2220
+ let params = 0;
2221
+ for (let i = paramNames.length - 1; i >= 0; i--) {
2222
+ const name = paramNames[i] ?? null;
2223
+ if (name !== null && isUnused(name)) params++;
2224
+ else break;
2225
+ }
2226
+ let locals = 0;
2227
+ for (const name of ownLocalNames(fn)) {
2228
+ if (isUnused(name)) locals++;
2229
+ }
2230
+ return { locals, params };
2231
+ }
2232
+ function ownParamNames(fn) {
2233
+ const params = fn.childForFieldName("parameters");
2234
+ if (!params) return [];
2235
+ const out = [];
2236
+ for (const p of params.namedChildren) {
2237
+ if (!p || !PARAM_TYPES.has(p.type)) continue;
2238
+ const first = p.namedChild(0);
2239
+ out.push(first?.type === "identifier" ? first.text : null);
2240
+ }
2241
+ return out;
2242
+ }
2243
+ function ownLocalNames(fn) {
2244
+ const body = fn.childForFieldName("body");
2245
+ if (!body) return [];
2246
+ const out = [];
2247
+ const walk2 = (node) => {
2248
+ if (FUNCTION_TYPES.has(node.type)) return;
2249
+ if (node.type === "variable_declarator") {
2250
+ const name = node.childForFieldName("name");
2251
+ if (name?.type === "identifier") out.push(name.text);
2252
+ }
2253
+ for (const child of node.namedChildren) {
2254
+ if (child) walk2(child);
2255
+ }
2256
+ };
2257
+ walk2(body);
2258
+ return out;
2259
+ }
2260
+ function collectBindingDecls(fn, ids, nameCount) {
2261
+ const mark = (n) => {
2262
+ if (n?.type !== "identifier") return;
2263
+ ids.add(n.id);
2264
+ nameCount.set(n.text, (nameCount.get(n.text) ?? 0) + 1);
2265
+ };
2266
+ const walk2 = (node) => {
2267
+ if (PARAM_TYPES.has(node.type)) mark(node.namedChild(0));
2268
+ else if (node.type === "variable_declarator") mark(node.childForFieldName("name"));
2269
+ for (const child of node.namedChildren) {
2270
+ if (child) walk2(child);
2271
+ }
2272
+ };
2273
+ walk2(fn);
2274
+ }
2275
+ function countReferences(fn, declIds, out) {
2276
+ const walk2 = (node) => {
2277
+ if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
2278
+ out.set(node.text, (out.get(node.text) ?? 0) + 1);
2279
+ }
2280
+ for (const child of node.namedChildren) {
2281
+ if (child) walk2(child);
2282
+ }
2283
+ };
2284
+ walk2(fn);
2285
+ }
2286
+
2287
+ // src/analysis/growth-risk.ts
2288
+ var GROWTH_RISK_METRIC_NAMES = /* @__PURE__ */ new Set([
2289
+ "loop_depth",
2290
+ "recursive_functions",
2291
+ "search_in_loop"
2292
+ ]);
2293
+ var TS_LOOP_TYPES = /* @__PURE__ */ new Set([
2294
+ "for_statement",
2295
+ "for_in_statement",
2296
+ "while_statement",
2297
+ "do_statement"
2298
+ ]);
2299
+ var PY_LOOP_TYPES = /* @__PURE__ */ new Set(["for_statement", "while_statement"]);
2300
+ var NAMED_FN_TYPES = /* @__PURE__ */ new Set([
2301
+ "function_declaration",
2302
+ "generator_function_declaration",
2303
+ "method_definition",
2304
+ "function_definition"
2305
+ ]);
2306
+ var SEARCH_METHODS = /* @__PURE__ */ new Set([
2307
+ "includes",
2308
+ "indexOf",
2309
+ "lastIndexOf",
2310
+ "find",
2311
+ "findIndex",
2312
+ "some",
2313
+ "every",
2314
+ "filter"
2315
+ ]);
2316
+ function computeGrowthRiskMetrics(files, fileIdOf) {
2317
+ const out = [];
2318
+ for (const file of files) {
2319
+ const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
2320
+ const root = file.tree.rootNode;
2321
+ const id = fileIdOf(file.filePath);
2322
+ const depth = maxLoopDepth(root, loopTypes, 0);
2323
+ if (depth >= 2) {
2324
+ out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
2325
+ }
2326
+ const recursion = countDirectRecursion(root);
2327
+ if (recursion > 0) {
2328
+ out.push({ nodeId: id, name: "recursive_functions", value: recursion, unit: "count" });
2329
+ }
2330
+ const searches = countSearchInLoop(root, loopTypes, false);
2331
+ if (searches > 0) {
2332
+ out.push({ nodeId: id, name: "search_in_loop", value: searches, unit: "count" });
2333
+ }
2334
+ }
2335
+ return out;
2336
+ }
2337
+ function countDirectRecursion(root) {
2338
+ let count = 0;
2339
+ const visit = (node) => {
2340
+ if (NAMED_FN_TYPES.has(node.type)) {
2341
+ const name = node.childForFieldName("name")?.text;
2342
+ const body = node.childForFieldName("body");
2343
+ if (name && body && callsName(body, name)) count++;
2344
+ }
2345
+ for (const child of node.namedChildren) {
2346
+ if (child) visit(child);
2347
+ }
2348
+ };
2349
+ visit(root);
2350
+ return count;
2351
+ }
2352
+ function callsName(node, name) {
2353
+ if (node.type === "call_expression") {
2354
+ const fn = node.childForFieldName("function");
2355
+ if (fn?.type === "identifier" && fn.text === name) return true;
2356
+ }
2357
+ for (const child of node.namedChildren) {
2358
+ if (child && callsName(child, name)) return true;
2359
+ }
2360
+ return false;
2361
+ }
2362
+ function countSearchInLoop(node, loopTypes, inLoop) {
2363
+ let count = 0;
2364
+ if (inLoop && node.type === "call_expression") {
2365
+ const fn = node.childForFieldName("function");
2366
+ const prop = fn?.type === "member_expression" ? fn.childForFieldName("property") : null;
2367
+ if (prop && SEARCH_METHODS.has(prop.text)) count++;
2368
+ }
2369
+ const childInLoop = inLoop || loopTypes.has(node.type);
2370
+ for (const child of node.namedChildren) {
2371
+ if (child) count += countSearchInLoop(child, loopTypes, childInLoop);
2372
+ }
2373
+ return count;
2374
+ }
2375
+ function maxLoopDepth(node, loopTypes, depth) {
2376
+ let max = depth;
2377
+ for (const child of node.namedChildren) {
2378
+ if (!child) continue;
2379
+ const next = loopTypes.has(child.type) ? depth + 1 : depth;
2380
+ const childMax = maxLoopDepth(child, loopTypes, next);
2381
+ if (childMax > max) max = childMax;
2382
+ }
2383
+ return max;
2384
+ }
2385
+
1875
2386
  // src/reconstruct.ts
1876
2387
  function reconstructFragment(repoRoot, absPath, fileId2, basis) {
1877
2388
  const nodes = buildFileModuleNodes(repoRoot, absPath);
@@ -1922,7 +2433,7 @@ function structuralSignature(file) {
1922
2433
  return hashContent(parts.join(""));
1923
2434
  }
1924
2435
  function sourceLanguage(filePath) {
1925
- const language = getLanguageFromPath(filePath);
2436
+ const language = getLanguageFromPath2(filePath);
1926
2437
  return language === "typescript" || language === "python" ? language : null;
1927
2438
  }
1928
2439
  async function readSourceFiles(filePaths) {
@@ -2019,7 +2530,8 @@ function loadReuseBasis(db, indexVersion) {
2019
2530
  }
2020
2531
  const sourceMetricsByFile = /* @__PURE__ */ new Map();
2021
2532
  for (const m of db.listMetrics(snap.id)) {
2022
- if (!SOURCE_METRIC_NAMES.has(m.name)) continue;
2533
+ if (!SOURCE_METRIC_NAMES.has(m.name) && !DEAD_CODE_METRIC_NAMES.has(m.name) && !GROWTH_RISK_METRIC_NAMES.has(m.name))
2534
+ continue;
2023
2535
  const node = nodesById.get(m.nodeId);
2024
2536
  const fileKey = node?.kind === "symbol" && node.parentId ? node.parentId : m.nodeId;
2025
2537
  const bucket = sourceMetricsByFile.get(fileKey);
@@ -2163,12 +2675,205 @@ function computeMetrics(nodes, edges) {
2163
2675
  return out;
2164
2676
  }
2165
2677
 
2166
- // src/index-metrics.ts
2167
- function symbolNamesByFile(nodes) {
2168
- const out = /* @__PURE__ */ new Map();
2678
+ // src/analysis/test-linker.ts
2679
+ var DEFAULT_MIN_CO_EDIT_COUNT = 2;
2680
+ var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
2681
+ var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
2682
+ function pathCandidates(testId) {
2683
+ const out = /* @__PURE__ */ new Set();
2684
+ const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
2685
+ if (deInfixed !== testId) out.add(deInfixed);
2686
+ const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
2687
+ if (deDir !== testId) out.add(deDir);
2688
+ return [...out];
2689
+ }
2690
+ function splitFileIds(nodes) {
2691
+ const testIds = [];
2692
+ const nonTestIds = /* @__PURE__ */ new Set();
2169
2693
  for (const n of nodes) {
2170
- if (n.kind !== "symbol" || !n.parentId) continue;
2171
- const bucket = out.get(n.parentId);
2694
+ if (n.kind !== "file") continue;
2695
+ if (n.role === "test") testIds.push(n.id);
2696
+ else nonTestIds.add(n.id);
2697
+ }
2698
+ return { testIds, nonTestIds };
2699
+ }
2700
+ function linkTestsToSources(nodes, coEditPairs, options = {}) {
2701
+ const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
2702
+ const { testIds, nonTestIds } = splitFileIds(nodes);
2703
+ const links = [];
2704
+ for (const testId of testIds) {
2705
+ const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
2706
+ if (matched.length > 0) {
2707
+ for (const sourceId of matched) {
2708
+ links.push({ testId, sourceId, method: "path" });
2709
+ }
2710
+ continue;
2711
+ }
2712
+ const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
2713
+ if (partner) {
2714
+ links.push({ testId, sourceId: partner.partner, method: "coedit" });
2715
+ }
2716
+ }
2717
+ return links;
2718
+ }
2719
+ function testCoverageCountMetrics(links) {
2720
+ const testsBySource = groupTestsBySource(links);
2721
+ const out = [];
2722
+ for (const [sourceId, tests] of testsBySource) {
2723
+ out.push({
2724
+ nodeId: sourceId,
2725
+ name: "linked_test_count",
2726
+ value: tests.size,
2727
+ unit: "count"
2728
+ });
2729
+ }
2730
+ return out;
2731
+ }
2732
+ function groupTestsBySource(links) {
2733
+ const out = /* @__PURE__ */ new Map();
2734
+ for (const l of links) {
2735
+ let set = out.get(l.sourceId);
2736
+ if (!set) {
2737
+ set = /* @__PURE__ */ new Set();
2738
+ out.set(l.sourceId, set);
2739
+ }
2740
+ set.add(l.testId);
2741
+ }
2742
+ return out;
2743
+ }
2744
+
2745
+ // src/history-recency.ts
2746
+ var DAY_SECONDS = 86400;
2747
+ function windowSuffix(window) {
2748
+ return window === "lifetime" ? "lifetime" : `${window}d`;
2749
+ }
2750
+ function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
2751
+ const out = [];
2752
+ const ageEmitted = /* @__PURE__ */ new Set();
2753
+ for (const [windowDays, ids] of churnedIdsByWindow) {
2754
+ const suffix = windowSuffix(windowDays);
2755
+ for (const id of ids) {
2756
+ const seen = firstSeen.get(id);
2757
+ let recency = 1;
2758
+ if (seen !== void 0) {
2759
+ const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
2760
+ if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
2761
+ if (!ageEmitted.has(id)) {
2762
+ out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
2763
+ ageEmitted.add(id);
2764
+ }
2765
+ }
2766
+ out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
2767
+ }
2768
+ }
2769
+ return out;
2770
+ }
2771
+ function round3(n) {
2772
+ return Math.round(n * 1e3) / 1e3;
2773
+ }
2774
+
2775
+ // src/history-metrics.ts
2776
+ var DEFAULT_CHURN_WINDOWS = [30, 90, 180];
2777
+ function resolveChurnWindows(requested, primaryWindow, includeLifetime) {
2778
+ const base = requested && requested.length > 0 ? requested : DEFAULT_CHURN_WINDOWS;
2779
+ const finite = [.../* @__PURE__ */ new Set([primaryWindow, ...base])].filter((w) => w > 0).sort((a, b) => a - b);
2780
+ return includeLifetime ? [...finite, "lifetime"] : finite;
2781
+ }
2782
+ function loadHistoryMetrics(nodes, idRoot, options = {}) {
2783
+ const knownPaths = collectFileIds(nodes);
2784
+ const primaryWindow = options.churnWindowDays ?? 30;
2785
+ const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
2786
+ const wide = loadChurnEntries({ repoRoot: idRoot, windowDays: windows[windows.length - 1] });
2787
+ if (wide === null) return null;
2788
+ const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
2789
+ const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
2790
+ const primaryEntries = windows.at(-1) === primaryWindow ? wide : entriesWithin(wide, primaryWindow, nowEpoch);
2791
+ const metrics = [
2792
+ ...churnMetrics(churnByWindow),
2793
+ ...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
2794
+ ...recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch)
2795
+ ];
2796
+ if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
2797
+ return { metrics, primaryEntries };
2798
+ }
2799
+ function collectFileIds(nodes) {
2800
+ const out = /* @__PURE__ */ new Set();
2801
+ for (const n of nodes) {
2802
+ if (n.kind === "file") out.add(n.id);
2803
+ }
2804
+ return out;
2805
+ }
2806
+ function churnMetrics(churnByWindow) {
2807
+ const out = [];
2808
+ for (const [window, byPath] of churnByWindow) {
2809
+ const suffix = windowSuffix(window);
2810
+ for (const [nodeId, churn] of byPath) {
2811
+ out.push(
2812
+ { nodeId, name: `churn_${suffix}`, value: churn.lines, unit: "lines" },
2813
+ { nodeId, name: `churn_${suffix}_commits`, value: churn.commits, unit: "count" },
2814
+ { nodeId, name: `churn_${suffix}_authors`, value: churn.authors, unit: "count" }
2815
+ );
2816
+ }
2817
+ }
2818
+ return out;
2819
+ }
2820
+ function ownershipMetrics(entries, window, knownPaths) {
2821
+ const suffix = windowSuffix(window);
2822
+ const out = [];
2823
+ for (const [nodeId, owner] of computeOwnership(entries, { knownPaths })) {
2824
+ out.push(
2825
+ { nodeId, name: `bus_factor_${suffix}`, value: owner.busFactor, unit: "count" },
2826
+ { nodeId, name: `top_author_share_${suffix}`, value: round3(owner.topAuthorShare), unit: "ratio" }
2827
+ );
2828
+ }
2829
+ return out;
2830
+ }
2831
+ function computeTestCoverageOwnership(entries, links, options = {}) {
2832
+ const suffix = windowSuffix(options.windowDays ?? 30);
2833
+ const testsBySource = groupTestsBySource(links);
2834
+ const testIds = /* @__PURE__ */ new Set();
2835
+ for (const tests of testsBySource.values()) {
2836
+ for (const t of tests) testIds.add(t);
2837
+ }
2838
+ const linesByTest = authorLinesByPath(entries, testIds);
2839
+ const out = [];
2840
+ for (const [nodeId, tests] of testsBySource) {
2841
+ const summary = summarizeOwnership(mergeAuthorChurn(tests, linesByTest), options.busFactorThreshold);
2842
+ if (summary === null) continue;
2843
+ out.push(
2844
+ { nodeId, name: `test_bus_factor_${suffix}`, value: summary.busFactor, unit: "count" },
2845
+ { nodeId, name: `test_top_author_share_${suffix}`, value: round3(summary.topAuthorShare), unit: "ratio" }
2846
+ );
2847
+ }
2848
+ return out;
2849
+ }
2850
+ function mergeAuthorChurn(tests, linesByTest) {
2851
+ const byAuthor = /* @__PURE__ */ new Map();
2852
+ for (const testId of tests) {
2853
+ const fileAuthors = linesByTest.get(testId);
2854
+ if (!fileAuthors) continue;
2855
+ for (const [author, lines] of fileAuthors) {
2856
+ byAuthor.set(author, (byAuthor.get(author) ?? 0) + lines);
2857
+ }
2858
+ }
2859
+ return byAuthor;
2860
+ }
2861
+ function recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch) {
2862
+ const churnedByWindow = /* @__PURE__ */ new Map();
2863
+ for (const [window, byPath] of churnByWindow) {
2864
+ if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
2865
+ }
2866
+ if (churnedByWindow.size === 0) return [];
2867
+ const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
2868
+ return computeRecencyWindows(firstSeen, churnedByWindow, nowEpoch);
2869
+ }
2870
+
2871
+ // src/index-metrics.ts
2872
+ function symbolNamesByFile(nodes) {
2873
+ const out = /* @__PURE__ */ new Map();
2874
+ for (const n of nodes) {
2875
+ if (n.kind !== "symbol" || !n.parentId) continue;
2876
+ const bucket = out.get(n.parentId);
2172
2877
  if (bucket) bucket.add(n.name);
2173
2878
  else out.set(n.parentId, /* @__PURE__ */ new Set([n.name]));
2174
2879
  }
@@ -2176,6 +2881,7 @@ function symbolNamesByFile(nodes) {
2176
2881
  }
2177
2882
  function buildIndexerMetrics(input) {
2178
2883
  const nodeList = [...input.nodes.values()];
2884
+ const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
2179
2885
  return [
2180
2886
  ...computeMetrics(nodeList, [...input.edges.values()]),
2181
2887
  ...computeSourceMetrics(
@@ -2183,9 +2889,23 @@ function buildIndexerMetrics(input) {
2183
2889
  (p) => fileId(input.idRoot, p),
2184
2890
  symbolNamesByFile(nodeList)
2185
2891
  ),
2186
- ...input.reusedSourceMetrics
2892
+ ...computeDeadCodeMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
2893
+ ...computeGrowthRiskMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
2894
+ ...input.reusedSourceMetrics,
2895
+ ...history?.metrics ?? [],
2896
+ ...computeTestCoverage(nodeList, history?.primaryEntries ?? null, input.history?.churnWindowDays)
2187
2897
  ];
2188
2898
  }
2899
+ function computeTestCoverage(nodes, entries, windowDays) {
2900
+ const coEditPairs = entries ? computeChangeCoupling(entries, { knownPaths: collectFileIds(nodes) }).pairs : [];
2901
+ const links = linkTestsToSources(nodes, coEditPairs);
2902
+ if (links.length === 0) return [];
2903
+ const out = testCoverageCountMetrics(links);
2904
+ if (entries) {
2905
+ out.push(...computeTestCoverageOwnership(entries, links, { windowDays }));
2906
+ }
2907
+ return out;
2908
+ }
2189
2909
 
2190
2910
  // src/merge.ts
2191
2911
  function edgeKey(edge) {
@@ -2204,8 +2924,45 @@ function mergeFragments(fragments) {
2204
2924
  return { nodes, edges };
2205
2925
  }
2206
2926
 
2927
+ // src/symbol-aliases.ts
2928
+ var QUALIFIED_SYMBOLS_SINCE = "0.14.0";
2929
+ function versionParts(version) {
2930
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
2931
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
2932
+ }
2933
+ function predatesQualifiedSymbols(indexVersion) {
2934
+ const have = versionParts(indexVersion);
2935
+ if (!have) return true;
2936
+ const since = versionParts(QUALIFIED_SYMBOLS_SINCE);
2937
+ for (let i = 0; i < 3; i++) {
2938
+ if (have[i] !== since[i]) return have[i] < since[i];
2939
+ }
2940
+ return false;
2941
+ }
2942
+ function qualifiedSymbolAliases(files, nodeIds) {
2943
+ const out = [];
2944
+ for (const { fileId: fileId2, file } of files) {
2945
+ for (const [name, qualified] of qualifiedByName(file)) {
2946
+ const oldId = symbolId(fileId2, name);
2947
+ if (qualified.size !== 1 || nodeIds.has(oldId)) continue;
2948
+ const newId = symbolId(fileId2, [...qualified][0]);
2949
+ if (nodeIds.has(newId)) out.push({ oldId, newId, reason: "requalify" });
2950
+ }
2951
+ }
2952
+ return out;
2953
+ }
2954
+ function qualifiedByName(file) {
2955
+ const out = /* @__PURE__ */ new Map();
2956
+ for (const d of collectDeclarations(file)) {
2957
+ const bucket = out.get(d.name);
2958
+ if (bucket) bucket.add(d.qualifiedName);
2959
+ else out.set(d.name, /* @__PURE__ */ new Set([d.qualifiedName]));
2960
+ }
2961
+ return out;
2962
+ }
2963
+
2207
2964
  // src/indexer.ts
2208
- var INDEX_VERSION = "0.11.0";
2965
+ var INDEX_VERSION = "0.15.0";
2209
2966
  var LANGUAGES = ["typescript", "python"];
2210
2967
  function canonicalizePath(p) {
2211
2968
  try {
@@ -2216,32 +2973,28 @@ function canonicalizePath(p) {
2216
2973
  }
2217
2974
  function normalizePaths(paths) {
2218
2975
  if (paths.length === 0) throw new Error("indexPaths requires at least one path");
2219
- return [...new Set(paths.map((p) => path10.resolve(p)))];
2976
+ return [...new Set(paths.map((p) => path9.resolve(p)))];
2220
2977
  }
2221
2978
  function countByKind(items) {
2222
2979
  const out = {};
2223
2980
  for (const item of items) out[item.kind] = (out[item.kind] ?? 0) + 1;
2224
2981
  return out;
2225
2982
  }
2226
- function persist(store, ref, commitHash, accumulator, aliases, metrics) {
2227
- const snapshotId = store.createSnapshot({ ref, commitHash, indexVersion: INDEX_VERSION });
2983
+ function persist(store, snapshot, accumulator, aliases, metrics) {
2984
+ const { ref, commitHash, aliasBase } = snapshot;
2985
+ const attrs = { [ALIAS_BASE_ATTR]: aliasBase };
2986
+ const snapshotId = store.createSnapshot({ ref, commitHash, indexVersion: INDEX_VERSION, attrs });
2228
2987
  store.insertNodes(snapshotId, [...accumulator.nodes.values()]);
2229
2988
  store.insertEdges(snapshotId, [...accumulator.edges.values()]);
2230
2989
  if (aliases.length > 0) store.insertAliases(snapshotId, aliases);
2231
2990
  if (metrics.length > 0) store.insertMetrics(snapshotId, metrics);
2232
2991
  return snapshotId;
2233
2992
  }
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 }));
2993
+ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
2994
+ const prior = store.listSnapshots({ limit: 1 })[0];
2995
+ if (!prior || !predatesQualifiedSymbols(prior.indexVersion)) return [];
2996
+ const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
2997
+ return qualifiedSymbolAliases(files, nodeIds);
2245
2998
  }
2246
2999
  function resolveRoots(paths) {
2247
3000
  const raw = normalizePaths(paths);
@@ -2256,6 +3009,13 @@ async function parseAll(toParse) {
2256
3009
  }
2257
3010
  return parsed;
2258
3011
  }
3012
+ function historyOptions(options) {
3013
+ return {
3014
+ churnWindowDays: options.churnWindowDays,
3015
+ churnWindows: options.churnWindows,
3016
+ includeLifetime: options.lifetime === true
3017
+ };
3018
+ }
2259
3019
  async function indexPaths(store, options) {
2260
3020
  const { rootDirs, idRoot } = resolveRoots(options.paths);
2261
3021
  const readFiles = await readSourceFiles(await walkSourceFiles(rootDirs, LANGUAGES));
@@ -2283,12 +3043,19 @@ async function indexPaths(store, options) {
2283
3043
  edges: accumulator.edges,
2284
3044
  parsedFiles: [...parsedByPath.values()],
2285
3045
  reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
2286
- idRoot
3046
+ idRoot,
3047
+ history: options.computeChurn === false ? void 0 : historyOptions(options)
2287
3048
  });
2288
3049
  const rootDir = rootDirs[0];
2289
- const aliases = resolveAliases(rootDir, idRoot, options, store);
3050
+ const ref = options.ref ?? "wd";
3051
+ const bridge = computeAliasBridge(store, { ...options, rootDir, idRoot, ref, nodes: accumulator.nodes });
3052
+ const aliases = [
3053
+ ...bridge.aliases,
3054
+ ...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
3055
+ ];
2290
3056
  const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2291
- const snapshotId = persist(store, options.ref ?? "wd", commitHash, accumulator, aliases, metrics);
3057
+ const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
3058
+ const snapshotId = persist(store, snapshot, accumulator, aliases, metrics);
2292
3059
  store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
2293
3060
  return {
2294
3061
  snapshotId,
@@ -2305,6 +3072,146 @@ async function indexPaths(store, options) {
2305
3072
  };
2306
3073
  }
2307
3074
 
3075
+ // src/index.ts
3076
+ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
3077
+
3078
+ // src/extractors/deep-ast.ts
3079
+ import { readFileSync as readFileSync2 } from "fs";
3080
+ import * as path10 from "path";
3081
+ import {
3082
+ Node as Node4,
3083
+ Project as Project2,
3084
+ ScriptTarget as ScriptTarget2
3085
+ } from "ts-morph";
3086
+ function computeDeepAst(input) {
3087
+ const sf = loadSourceFile(input.absPath);
3088
+ if (!sf) return null;
3089
+ const target = input.symbolName ? `${input.filePath}#${input.symbolName}` : input.filePath;
3090
+ return input.symbolName ? symbolDeepAst(sf, input.symbolName, target) : fileDeepAst(sf, target);
3091
+ }
3092
+ function loadSourceFile(absPath) {
3093
+ try {
3094
+ const text = readFileSync2(absPath, "utf8");
3095
+ const project = new Project2({
3096
+ useInMemoryFileSystem: true,
3097
+ compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
3098
+ });
3099
+ return project.createSourceFile(`f${path10.extname(absPath) || ".ts"}`, text);
3100
+ } catch {
3101
+ return null;
3102
+ }
3103
+ }
3104
+ function symbolDeepAst(sf, name, target) {
3105
+ const decl = lookupDeclaration(sf, name);
3106
+ if (!decl) {
3107
+ return { ...empty(target, "symbol"), note: "declaration not found in source" };
3108
+ }
3109
+ const text = declarationText(decl);
3110
+ return {
3111
+ target,
3112
+ kind: "symbol",
3113
+ declarationKind: decl.getKindName(),
3114
+ signature: text.signature ?? null,
3115
+ purpose: text.purpose ?? null,
3116
+ params: paramsOf(decl),
3117
+ returnType: returnTypeOf(decl),
3118
+ members: membersOf(decl)
3119
+ };
3120
+ }
3121
+ function fileDeepAst(sf, target) {
3122
+ return {
3123
+ ...empty(target, "file"),
3124
+ declarationKind: "SourceFile",
3125
+ members: exportedDeclarations(sf)
3126
+ };
3127
+ }
3128
+ function empty(target, kind) {
3129
+ return {
3130
+ target,
3131
+ kind,
3132
+ declarationKind: null,
3133
+ signature: null,
3134
+ purpose: null,
3135
+ params: [],
3136
+ returnType: null,
3137
+ members: []
3138
+ };
3139
+ }
3140
+ function callableOf(decl) {
3141
+ if (Node4.isFunctionDeclaration(decl) || Node4.isMethodDeclaration(decl)) return decl;
3142
+ if (Node4.isVariableDeclaration(decl)) {
3143
+ const init = decl.getInitializer();
3144
+ if (init && (Node4.isArrowFunction(init) || Node4.isFunctionExpression(init))) return init;
3145
+ }
3146
+ return null;
3147
+ }
3148
+ function paramsOf(decl) {
3149
+ const callable = callableOf(decl);
3150
+ if (!callable) return [];
3151
+ return callable.getParameters().map((p) => ({
3152
+ name: p.getName(),
3153
+ type: p.getTypeNode()?.getText() ?? null
3154
+ }));
3155
+ }
3156
+ function returnTypeOf(decl) {
3157
+ return callableOf(decl)?.getReturnTypeNode()?.getText() ?? null;
3158
+ }
3159
+ function membersOf(decl) {
3160
+ if (Node4.isClassDeclaration(decl)) return classMembers(decl);
3161
+ if (Node4.isInterfaceDeclaration(decl)) return interfaceMembers(decl);
3162
+ return [];
3163
+ }
3164
+ function classMembers(cls) {
3165
+ return cls.getMembers().map((m) => ({
3166
+ name: memberName(m),
3167
+ memberKind: m.getKindName(),
3168
+ signature: memberSignature(m),
3169
+ isStatic: Node4.isStaticable(m) ? m.isStatic() : false
3170
+ }));
3171
+ }
3172
+ function interfaceMembers(iface) {
3173
+ return iface.getMembers().map((m) => ({
3174
+ name: memberName(m),
3175
+ memberKind: m.getKindName(),
3176
+ signature: memberSignature(m),
3177
+ isStatic: false
3178
+ }));
3179
+ }
3180
+ function exportedDeclarations(sf) {
3181
+ const out = [];
3182
+ for (const [name, decls] of sf.getExportedDeclarations()) {
3183
+ const decl = decls[0];
3184
+ if (!decl) continue;
3185
+ out.push({
3186
+ name,
3187
+ memberKind: decl.getKindName(),
3188
+ signature: declarationText(decl).signature ?? memberSignature(decl),
3189
+ isStatic: false
3190
+ });
3191
+ }
3192
+ return out;
3193
+ }
3194
+ function memberName(m) {
3195
+ if (Node4.isConstructorDeclaration(m)) return "constructor";
3196
+ return Node4.hasName(m) ? m.getName() : "";
3197
+ }
3198
+ function memberSignature(m) {
3199
+ if (Node4.isMethodDeclaration(m) || Node4.isMethodSignature(m) || Node4.isConstructorDeclaration(m)) {
3200
+ const params = m.getParameters().map((p) => oneLine2(p.getText())).join(", ");
3201
+ const ret = m.getReturnTypeNode()?.getText();
3202
+ const name = Node4.isConstructorDeclaration(m) ? "constructor" : memberName(m);
3203
+ return oneLine2(`${name}(${params})${ret ? `: ${ret}` : ""}`);
3204
+ }
3205
+ if (Node4.isPropertyDeclaration(m) || Node4.isPropertySignature(m)) {
3206
+ const type = m.getTypeNode()?.getText();
3207
+ return oneLine2(`${memberName(m)}${type ? `: ${type}` : ""}`);
3208
+ }
3209
+ return null;
3210
+ }
3211
+ function oneLine2(s) {
3212
+ return s.replace(/\s+/g, " ").trim();
3213
+ }
3214
+
2308
3215
  // src/aliases.ts
2309
3216
  var METRIC_ALIASES = /* @__PURE__ */ new Map([
2310
3217
  ["lines", "loc"],
@@ -2334,6 +3241,9 @@ var EDGE_KIND_ALIASES = /* @__PURE__ */ new Map([
2334
3241
  ["depends_on", "depends-on"],
2335
3242
  ["dependson", "depends-on"]
2336
3243
  ]);
3244
+ function deprecatedTarget(map, name) {
3245
+ return map.get(name);
3246
+ }
2337
3247
  function canonicalMetricName(name) {
2338
3248
  return METRIC_ALIASES.get(name) ?? name;
2339
3249
  }
@@ -2343,71 +3253,1912 @@ function canonicalRole(name) {
2343
3253
  function canonicalEdgeKind(name) {
2344
3254
  return EDGE_KIND_ALIASES.get(name) ?? name;
2345
3255
  }
3256
+ function metricAliasTarget(name) {
3257
+ return deprecatedTarget(METRIC_ALIASES, name);
3258
+ }
3259
+ function roleAliasTarget(name) {
3260
+ return deprecatedTarget(ROLE_ALIASES, name);
3261
+ }
2346
3262
 
2347
- // src/read.ts
2348
- function listNodes(store, snapshotId, opts) {
2349
- return store.listNodes(snapshotId, opts);
3263
+ // src/analysis/coverage.ts
3264
+ var COVERAGE_METRIC_NAME = "coverage_pct";
3265
+ function isFileCoverage(v) {
3266
+ return typeof v === "object" && v !== null && typeof v.fnMap === "object" && typeof v.f === "object";
3267
+ }
3268
+ function innermostContaining(symbols, line) {
3269
+ let best = null;
3270
+ for (const s of symbols) {
3271
+ if (s.startLine <= line && line <= s.endLine) {
3272
+ if (!best || s.endLine - s.startLine < best.endLine - best.startLine) best = s;
3273
+ }
3274
+ }
3275
+ return best;
2350
3276
  }
2351
- function listEdges(store, snapshotId, opts) {
2352
- return store.listEdges(snapshotId, opts);
3277
+ function attributeCoverage(coverage, fileIdOf, symbolsByFile) {
3278
+ const out = [];
3279
+ for (const [absPath, fc] of Object.entries(coverage)) {
3280
+ if (!isFileCoverage(fc)) continue;
3281
+ const fileId2 = fileIdOf(absPath);
3282
+ if (!fileId2) continue;
3283
+ const fnIds = Object.keys(fc.fnMap);
3284
+ if (fnIds.length === 0) continue;
3285
+ const covered = fnIds.filter((k) => (fc.f[k] ?? 0) > 0).length;
3286
+ out.push(pctMetric(fileId2, covered, fnIds.length));
3287
+ out.push(...symbolCoverage(fc, fnIds, symbolsByFile.get(fileId2) ?? []));
3288
+ }
3289
+ return out;
2353
3290
  }
2354
- function listMetrics(store, snapshotId) {
2355
- return store.listMetrics(snapshotId);
3291
+ function symbolCoverage(fc, fnIds, symbols) {
3292
+ const perSymbol = /* @__PURE__ */ new Map();
3293
+ for (const k of fnIds) {
3294
+ const sym = innermostContaining(symbols, fc.fnMap[k].loc.start.line);
3295
+ if (!sym) continue;
3296
+ const acc = perSymbol.get(sym.id) ?? { hit: 0, total: 0 };
3297
+ acc.total++;
3298
+ if ((fc.f[k] ?? 0) > 0) acc.hit++;
3299
+ perSymbol.set(sym.id, acc);
3300
+ }
3301
+ return [...perSymbol].map(([id, { hit, total }]) => pctMetric(id, hit, total));
3302
+ }
3303
+ function pctMetric(nodeId, covered, total) {
3304
+ return {
3305
+ nodeId,
3306
+ name: COVERAGE_METRIC_NAME,
3307
+ value: Math.round(100 * covered / total),
3308
+ unit: "percent"
3309
+ };
3310
+ }
3311
+
3312
+ // src/analysis/pagerank.ts
3313
+ var DEFAULT_DAMPING = 0.85;
3314
+ var DEFAULT_TOLERANCE = 1e-6;
3315
+ var DEFAULT_MAX_ITERS = 100;
3316
+ var DEFAULT_EDGE_WEIGHTS = {
3317
+ imports: 1,
3318
+ "re-exports": 0.5,
3319
+ calls: 1.5,
3320
+ extends: 1,
3321
+ implements: 1,
3322
+ references: 1,
3323
+ "depends-on": 1
3324
+ };
3325
+ function getEdgeWeight(kind, overrides) {
3326
+ return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
3327
+ }
3328
+ function computePageRank(nodes, edges, options = {}) {
3329
+ const n = nodes.length;
3330
+ if (n === 0) return { rows: [], iterations: 0, converged: true };
3331
+ const damping = options.damping ?? DEFAULT_DAMPING;
3332
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
3333
+ const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
3334
+ const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
3335
+ const idx = buildIdIndex(nodes);
3336
+ const adj = buildAdjacency(idx, edges, weights, n);
3337
+ const pers = buildPersonalization(idx, n, options.personalization);
3338
+ const { rank, iterations, converged } = powerIterate(
3339
+ pers,
3340
+ adj,
3341
+ damping,
3342
+ tolerance,
3343
+ maxIters
3344
+ );
3345
+ return { rows: rankToRows(nodes, rank), iterations, converged };
3346
+ }
3347
+ function buildIdIndex(nodes) {
3348
+ const idx = /* @__PURE__ */ new Map();
3349
+ for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
3350
+ return idx;
3351
+ }
3352
+ function buildAdjacency(idx, edges, weights, n) {
3353
+ const outNeighbors = Array.from(
3354
+ { length: n },
3355
+ () => []
3356
+ );
3357
+ const outWeightSum = new Array(n).fill(0);
3358
+ for (const e of edges) {
3359
+ const si = idx.get(e.srcId);
3360
+ const di = idx.get(e.dstId);
3361
+ if (si === void 0 || di === void 0) continue;
3362
+ const w = weights[e.kind] ?? 1;
3363
+ if (w <= 0) continue;
3364
+ outNeighbors[si].push({ to: di, w });
3365
+ outWeightSum[si] += w;
3366
+ }
3367
+ return { outNeighbors, outWeightSum };
3368
+ }
3369
+ function powerIterate(pers, adj, damping, tolerance, maxIters) {
3370
+ const n = pers.length;
3371
+ let rank = pers.slice();
3372
+ let next = new Array(n).fill(0);
3373
+ let iterations = 0;
3374
+ let converged = false;
3375
+ for (let iter = 0; iter < maxIters; iter++) {
3376
+ iterations = iter + 1;
3377
+ const dangling = sumDangling(rank, adj.outWeightSum);
3378
+ const teleport = 1 - damping + damping * dangling;
3379
+ seedTeleport(next, pers, teleport);
3380
+ distributeRank(next, rank, adj, damping);
3381
+ const diff = l1Diff(rank, next);
3382
+ [rank, next] = [next, rank];
3383
+ next.fill(0);
3384
+ if (diff < tolerance) {
3385
+ converged = true;
3386
+ break;
3387
+ }
3388
+ }
3389
+ return { rank, iterations, converged };
3390
+ }
3391
+ function sumDangling(rank, outWeightSum) {
3392
+ let total = 0;
3393
+ for (let i = 0; i < rank.length; i++) {
3394
+ if (outWeightSum[i] === 0) total += rank[i];
3395
+ }
3396
+ return total;
3397
+ }
3398
+ function seedTeleport(next, pers, teleport) {
3399
+ for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
3400
+ }
3401
+ function distributeRank(next, rank, adj, damping) {
3402
+ for (let i = 0; i < rank.length; i++) {
3403
+ const s = adj.outWeightSum[i];
3404
+ if (s === 0 || rank[i] === 0) continue;
3405
+ const factor = damping * rank[i] / s;
3406
+ const list = adj.outNeighbors[i];
3407
+ for (let k = 0; k < list.length; k++) {
3408
+ next[list[k].to] += factor * list[k].w;
3409
+ }
3410
+ }
3411
+ }
3412
+ function l1Diff(a, b) {
3413
+ let diff = 0;
3414
+ for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
3415
+ return diff;
3416
+ }
3417
+ function rankToRows(nodes, rank) {
3418
+ return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
3419
+ }
3420
+ function compareRows(a, b) {
3421
+ if (b.score !== a.score) return b.score - a.score;
3422
+ return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
3423
+ }
3424
+ function buildPersonalization(idx, n, pers) {
3425
+ if (!pers || pers.size === 0) return uniformVector(n);
3426
+ const out = new Array(n).fill(0);
3427
+ const total = applySeedWeights(out, idx, pers);
3428
+ if (total === 0) return uniformVector(n);
3429
+ for (let i = 0; i < n; i++) out[i] /= total;
3430
+ return out;
3431
+ }
3432
+ function uniformVector(n) {
3433
+ return new Array(n).fill(1 / n);
3434
+ }
3435
+ function applySeedWeights(out, idx, pers) {
3436
+ let total = 0;
3437
+ for (const [id, w] of pers) {
3438
+ if (!isValidWeight(w)) continue;
3439
+ const i = idx.get(id);
3440
+ if (i === void 0) continue;
3441
+ out[i] = w;
3442
+ total += w;
3443
+ }
3444
+ return total;
3445
+ }
3446
+ function isValidWeight(w) {
3447
+ return Number.isFinite(w) && w > 0;
3448
+ }
3449
+
3450
+ // src/analysis/relevance.ts
3451
+ function computeRelevance(nodes, edges, seedIds, options = {}) {
3452
+ const seeds = seedIds.filter((id) => id.length > 0);
3453
+ if (seeds.length === 0) return /* @__PURE__ */ new Map();
3454
+ const personalization = new Map(seeds.map((id) => [id, 1]));
3455
+ const result = computePageRank(nodes, symmetrize(edges), {
3456
+ personalization,
3457
+ damping: options.damping,
3458
+ edgeWeights: options.edgeWeights
3459
+ });
3460
+ const out = /* @__PURE__ */ new Map();
3461
+ for (const row of result.rows) out.set(row.nodeId, row.score);
3462
+ return out;
3463
+ }
3464
+ function symmetrize(edges) {
3465
+ const out = [];
3466
+ for (const e of edges) {
3467
+ out.push(e);
3468
+ out.push({ ...e, srcId: e.dstId, dstId: e.srcId });
3469
+ }
3470
+ return out;
3471
+ }
3472
+
3473
+ // src/analysis/symbol-coupling.ts
3474
+ var DEFAULT_MIN_CO_IMPORTS = 2;
3475
+ var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
3476
+ function computeSymbolConsumers(edges) {
3477
+ const bySymbol = /* @__PURE__ */ new Map();
3478
+ for (const e of edges) {
3479
+ let set = bySymbol.get(e.dstId);
3480
+ if (!set) {
3481
+ set = /* @__PURE__ */ new Set();
3482
+ bySymbol.set(e.dstId, set);
3483
+ }
3484
+ set.add(e.srcId);
3485
+ }
3486
+ const out = [];
3487
+ for (const [id, consumers] of bySymbol) {
3488
+ const parsed = parseSymbolId(id);
3489
+ if (!parsed) continue;
3490
+ out.push({
3491
+ symbolId: id,
3492
+ fileId: parsed.fileId,
3493
+ name: parsed.name,
3494
+ consumers: [...consumers].sort()
3495
+ });
3496
+ }
3497
+ return out.sort(compareConsumers);
3498
+ }
3499
+ function compareConsumers(a, b) {
3500
+ if (b.consumers.length !== a.consumers.length) {
3501
+ return b.consumers.length - a.consumers.length;
3502
+ }
3503
+ return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
3504
+ }
3505
+ function computeSymbolCoupling(edges, options = {}) {
3506
+ const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
3507
+ const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
3508
+ const bySource = groupSymbolsBySource(edges);
3509
+ const counts = /* @__PURE__ */ new Map();
3510
+ for (const symbols of bySource.values()) {
3511
+ if (symbols.length < 2 || symbols.length > largeThreshold) continue;
3512
+ accumulateSymbolPairs(symbols, counts);
3513
+ }
3514
+ return finalizePairs(counts, minCoImports);
3515
+ }
3516
+ function groupSymbolsBySource(edges) {
3517
+ const perSource = /* @__PURE__ */ new Map();
3518
+ for (const e of edges) {
3519
+ if (!parseSymbolId(e.dstId)) continue;
3520
+ let set = perSource.get(e.srcId);
3521
+ if (!set) {
3522
+ set = /* @__PURE__ */ new Set();
3523
+ perSource.set(e.srcId, set);
3524
+ }
3525
+ set.add(e.dstId);
3526
+ }
3527
+ const out = /* @__PURE__ */ new Map();
3528
+ for (const [src, set] of perSource) out.set(src, [...set].sort());
3529
+ return out;
3530
+ }
3531
+ function accumulateSymbolPairs(symbols, counts) {
3532
+ for (let i = 0; i < symbols.length; i++) {
3533
+ for (let j = i + 1; j < symbols.length; j++) {
3534
+ const key = `${symbols[i]} ${symbols[j]}`;
3535
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3536
+ }
3537
+ }
3538
+ }
3539
+ function finalizePairs(counts, minCoImports) {
3540
+ const out = [];
3541
+ for (const [key, coImports] of counts) {
3542
+ if (coImports < minCoImports) continue;
3543
+ const [aId, bId] = key.split(" ");
3544
+ const a = parseSymbolId(aId);
3545
+ const b = parseSymbolId(bId);
3546
+ out.push({
3547
+ aId,
3548
+ aFile: a.fileId,
3549
+ aName: a.name,
3550
+ bId,
3551
+ bFile: b.fileId,
3552
+ bName: b.name,
3553
+ coImports,
3554
+ crossFile: a.fileId !== b.fileId
3555
+ });
3556
+ }
3557
+ return out.sort(comparePairs);
3558
+ }
3559
+ function comparePairs(a, b) {
3560
+ if (b.coImports !== a.coImports) return b.coImports - a.coImports;
3561
+ if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
3562
+ if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
3563
+ return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
3564
+ }
3565
+
3566
+ // src/analysis/partition-quality.ts
3567
+ var FOUNDATION_INSTABILITY_MAX = 0.3;
3568
+ var TOP_INSTABILITY_MIN = 0.9;
3569
+ var WEAK_COHESION_MAX = 0.5;
3570
+ var TIGHT_INTENSITY_MIN = 0.6;
3571
+ var MODERATE_INTENSITY_MIN = 0.3;
3572
+ function computePartitionQuality(input) {
3573
+ const resolveBarrels = input.resolveBarrels ?? false;
3574
+ const pkgByFile = invertBuckets(input.fileByPackage);
3575
+ const barrelTargets = resolveBarrels ? buildBarrelTargetMap(input.nodes, input.edges) : /* @__PURE__ */ new Map();
3576
+ const resolvedPairs = buildResolvedPairs(
3577
+ input.edges,
3578
+ pkgByFile,
3579
+ barrelTargets,
3580
+ resolveBarrels
3581
+ );
3582
+ const counts = countByPackage(resolvedPairs);
3583
+ const totalEdges = resolvedPairs.length;
3584
+ const filesByPkg = filesPerPackage(input.fileByPackage);
3585
+ const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
3586
+ const perPackage = input.packages.map(
3587
+ (p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
3588
+ );
3589
+ const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
3590
+ const modularityQ = computeModularity(perPackage, totalEdges);
3591
+ const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
3592
+ return { modularityQ, totalEdges, perPackage, pairCoupling, flagsCount };
3593
+ }
3594
+ function invertBuckets(fileByPackage) {
3595
+ const out = /* @__PURE__ */ new Map();
3596
+ for (const [pkgId, files] of fileByPackage) {
3597
+ if (pkgId === "") continue;
3598
+ for (const f of files) out.set(f, pkgId);
3599
+ }
3600
+ return out;
3601
+ }
3602
+ function filesPerPackage(fileByPackage) {
3603
+ const out = /* @__PURE__ */ new Map();
3604
+ for (const [pkgId, files] of fileByPackage) {
3605
+ out.set(pkgId, files.length);
3606
+ }
3607
+ return out;
3608
+ }
3609
+ function buildBarrelTargetMap(nodes, edges) {
3610
+ const barrelIds = new Set(
3611
+ nodes.filter((n) => n.role === "barrel").map((n) => n.id)
3612
+ );
3613
+ if (barrelIds.size === 0) return /* @__PURE__ */ new Map();
3614
+ const directReExports = /* @__PURE__ */ new Map();
3615
+ for (const e of edges) {
3616
+ if (!barrelIds.has(e.srcId)) continue;
3617
+ if (e.kind !== "re-exports") continue;
3618
+ let list = directReExports.get(e.srcId);
3619
+ if (!list) {
3620
+ list = [];
3621
+ directReExports.set(e.srcId, list);
3622
+ }
3623
+ list.push(e.dstId);
3624
+ }
3625
+ const resolved = /* @__PURE__ */ new Map();
3626
+ const resolve4 = (barrel, visiting) => {
3627
+ if (resolved.has(barrel)) return resolved.get(barrel);
3628
+ if (visiting.has(barrel)) return [];
3629
+ visiting.add(barrel);
3630
+ const out = [];
3631
+ for (const target of directReExports.get(barrel) ?? []) {
3632
+ if (barrelIds.has(target)) {
3633
+ for (const inner of resolve4(target, visiting)) out.push(inner);
3634
+ } else {
3635
+ out.push(target);
3636
+ }
3637
+ }
3638
+ visiting.delete(barrel);
3639
+ resolved.set(barrel, out);
3640
+ return out;
3641
+ };
3642
+ for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
3643
+ return resolved;
3644
+ }
3645
+ function buildResolvedPairs(edges, pkgByFile, barrelTargets, resolveBarrels) {
3646
+ const out = [];
3647
+ for (const e of edges) {
3648
+ if (resolveBarrels && e.kind === "re-exports") continue;
3649
+ const fromPkg = pkgByFile.get(e.srcId);
3650
+ if (!fromPkg) continue;
3651
+ const targets = barrelTargets.get(e.dstId) ?? [e.dstId];
3652
+ for (const dst of targets) {
3653
+ const toPkg = pkgByFile.get(dst);
3654
+ if (toPkg) out.push({ from: fromPkg, to: toPkg });
3655
+ }
3656
+ }
3657
+ return out;
3658
+ }
3659
+ function countByPackage(pairs) {
3660
+ const out = /* @__PURE__ */ new Map();
3661
+ const get = (id) => {
3662
+ let c = out.get(id);
3663
+ if (!c) {
3664
+ c = { internal: 0, outgoing: 0, incoming: 0 };
3665
+ out.set(id, c);
3666
+ }
3667
+ return c;
3668
+ };
3669
+ for (const p of pairs) {
3670
+ if (p.from === p.to) {
3671
+ get(p.from).internal += 1;
3672
+ } else {
3673
+ get(p.from).outgoing += 1;
3674
+ get(p.to).incoming += 1;
3675
+ }
3676
+ }
3677
+ return out;
3678
+ }
3679
+ function buildPackageStats(pkgId, counts, filesByPkg, abstractByPkg) {
3680
+ const c = counts.get(pkgId) ?? { internal: 0, outgoing: 0, incoming: 0 };
3681
+ const cohesion = ratioOrZero(c.internal, c.internal + c.outgoing);
3682
+ const instability = ratioOrZero(c.outgoing, c.outgoing + c.incoming);
3683
+ const fileCount = filesByPkg.get(pkgId) ?? 0;
3684
+ const abstractness = ratioOrZero(abstractByPkg.get(pkgId) ?? 0, fileCount);
3685
+ const layer = classifyLayer(instability);
3686
+ const flags = [];
3687
+ if (layer !== "top" && cohesion < WEAK_COHESION_MAX && c.internal + c.outgoing > 0) {
3688
+ flags.push("weak-boundary");
3689
+ }
3690
+ return {
3691
+ pkgId,
3692
+ fileCount,
3693
+ internalEdges: c.internal,
3694
+ outgoingEdges: c.outgoing,
3695
+ incomingEdges: c.incoming,
3696
+ cohesion,
3697
+ instability,
3698
+ abstractness,
3699
+ layer,
3700
+ flags
3701
+ };
3702
+ }
3703
+ function countAbstractFiles(nodes, pkgByFile) {
3704
+ const out = /* @__PURE__ */ new Map();
3705
+ for (const n of nodes) {
3706
+ if (n.kind !== "file" || n.role !== "types") continue;
3707
+ const pkg = pkgByFile.get(n.id);
3708
+ if (pkg) out.set(pkg, (out.get(pkg) ?? 0) + 1);
3709
+ }
3710
+ return out;
3711
+ }
3712
+ function ratioOrZero(num, denom) {
3713
+ return denom === 0 ? 0 : num / denom;
3714
+ }
3715
+ function classifyLayer(instability) {
3716
+ if (instability <= FOUNDATION_INSTABILITY_MAX) return "foundation";
3717
+ if (instability >= TOP_INSTABILITY_MIN) return "top";
3718
+ return "middle";
3719
+ }
3720
+ function buildPairCoupling(pairs, filesByPkg) {
3721
+ const counts = /* @__PURE__ */ new Map();
3722
+ for (const p of pairs) {
3723
+ if (p.from === p.to) continue;
3724
+ const key = JSON.stringify([p.from, p.to]);
3725
+ const entry = counts.get(key);
3726
+ if (entry) entry.edges += 1;
3727
+ else counts.set(key, { from: p.from, to: p.to, edges: 1 });
3728
+ }
3729
+ const out = [];
3730
+ for (const { from, to, edges } of counts.values()) {
3731
+ const filesFrom = filesByPkg.get(from) ?? 0;
3732
+ const intensity = filesFrom === 0 ? 0 : edges / filesFrom;
3733
+ const flag = classifyPairIntensity(intensity);
3734
+ out.push({ from, to, edges, intensity, flag });
3735
+ }
3736
+ return out.sort(comparePairs2);
3737
+ }
3738
+ function classifyPairIntensity(intensity) {
3739
+ if (intensity >= TIGHT_INTENSITY_MIN) return "tight";
3740
+ if (intensity >= MODERATE_INTENSITY_MIN) return "moderate";
3741
+ return "none";
3742
+ }
3743
+ function comparePairs2(a, b) {
3744
+ if (a.from !== b.from) return a.from < b.from ? -1 : 1;
3745
+ return a.to < b.to ? -1 : 1;
3746
+ }
3747
+ function computeModularity(perPackage, totalEdges) {
3748
+ if (totalEdges === 0) return 0;
3749
+ let Q = 0;
3750
+ for (const p of perPackage) {
3751
+ const e_cc = p.internalEdges / totalEdges;
3752
+ const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
3753
+ const a_c = touching / (2 * totalEdges);
3754
+ Q += e_cc - a_c * a_c;
3755
+ }
3756
+ return Q;
3757
+ }
3758
+
3759
+ // src/analysis/snapshot.ts
3760
+ function snapshotPageRank(store, snapshotId, options = {}) {
3761
+ return computePageRank(store.listNodes(snapshotId), store.listEdges(snapshotId), options);
3762
+ }
3763
+ function snapshotRelevance(store, snapshotId, seedIds, options = {}) {
3764
+ return computeRelevance(store.listNodes(snapshotId), store.listEdges(snapshotId), seedIds, options);
3765
+ }
3766
+ function snapshotReferenceEdges(store, snapshotId) {
3767
+ return store.listEdges(snapshotId, { includeReferences: true }).filter((e) => e.kind === "references").map((e) => ({ srcId: e.srcId, dstId: e.dstId }));
3768
+ }
3769
+ function snapshotSymbolConsumers(store, snapshotId) {
3770
+ return computeSymbolConsumers(snapshotReferenceEdges(store, snapshotId));
3771
+ }
3772
+ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
3773
+ return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
3774
+ }
3775
+
3776
+ // src/read.ts
3777
+ function listNodes(store, snapshotId, opts) {
3778
+ return store.listNodes(snapshotId, opts);
3779
+ }
3780
+ function listEdges(store, snapshotId, opts) {
3781
+ return store.listEdges(snapshotId, opts);
3782
+ }
3783
+ function listMetrics(store, snapshotId) {
3784
+ return store.listMetrics(snapshotId);
3785
+ }
3786
+ function listMetricsForNode(store, snapshotId, nodeId) {
3787
+ return store.listMetricsForNode(snapshotId, nodeId);
3788
+ }
3789
+ function listEdgesTouching(store, snapshotId, nodeId, opts) {
3790
+ return store.listEdgesTouching(snapshotId, nodeId, opts);
3791
+ }
3792
+ function aggregateMetrics(store, snapshotId, opts) {
3793
+ return store.aggregateMetrics(snapshotId, opts);
3794
+ }
3795
+
3796
+ // src/catalogue/entries.ts
3797
+ var FILE = ["file"];
3798
+ var SYMBOL = ["symbol"];
3799
+ var STRUCTURAL = ["file", "module", "external"];
3800
+ var DEGREE = [
3801
+ {
3802
+ name: "fan_in",
3803
+ unit: "count",
3804
+ appliesTo: STRUCTURAL,
3805
+ rollup: "none",
3806
+ direction: "neutral",
3807
+ absent: "zero",
3808
+ source: "degree",
3809
+ description: "Import and re-export edges into the node. Summing over a group would count its internal edges."
3810
+ },
3811
+ {
3812
+ name: "fan_out",
3813
+ unit: "count",
3814
+ appliesTo: STRUCTURAL,
3815
+ rollup: "none",
3816
+ direction: "higher-worse",
3817
+ absent: "zero",
3818
+ source: "degree",
3819
+ description: "Import and re-export edges out of the node. Summing over a group would count its internal edges."
3820
+ },
3821
+ {
3822
+ name: "instability",
3823
+ unit: "ratio",
3824
+ appliesTo: STRUCTURAL,
3825
+ rollup: "none",
3826
+ direction: "neutral",
3827
+ absent: "exclude",
3828
+ source: "degree",
3829
+ description: "fan_out / (fan_in + fan_out); written only when the node has an import edge."
3830
+ },
3831
+ {
3832
+ name: "utilization",
3833
+ unit: "count",
3834
+ appliesTo: [...STRUCTURAL, "symbol"],
3835
+ rollup: "none",
3836
+ direction: "neutral",
3837
+ absent: "zero",
3838
+ source: "degree",
3839
+ description: "Inbound edges weighted by reference count over the barrel-resolved graph: how heavily a node is used."
3840
+ }
3841
+ ];
3842
+ var SOURCE = [
3843
+ {
3844
+ name: "loc",
3845
+ unit: "lines",
3846
+ appliesTo: FILE,
3847
+ rollup: "sum",
3848
+ direction: "higher-worse",
3849
+ absent: "zero",
3850
+ source: "source-metrics",
3851
+ description: "Non-blank lines in the file."
3852
+ },
3853
+ {
3854
+ name: "function_count",
3855
+ unit: "count",
3856
+ appliesTo: FILE,
3857
+ rollup: "sum",
3858
+ direction: "neutral",
3859
+ absent: "zero",
3860
+ source: "source-metrics",
3861
+ description: "Functions, methods, and arrow functions declared in the file."
3862
+ },
3863
+ {
3864
+ name: "cyclomatic_max",
3865
+ unit: "count",
3866
+ appliesTo: FILE,
3867
+ rollup: "max",
3868
+ direction: "higher-worse",
3869
+ absent: "exclude",
3870
+ source: "source-metrics",
3871
+ description: "Highest cyclomatic complexity of any function in the file."
3872
+ },
3873
+ {
3874
+ name: "cyclomatic_sum",
3875
+ unit: "count",
3876
+ appliesTo: FILE,
3877
+ rollup: "sum",
3878
+ direction: "higher-worse",
3879
+ absent: "zero",
3880
+ source: "source-metrics",
3881
+ description: "Cyclomatic complexity summed over the file's functions."
3882
+ },
3883
+ {
3884
+ name: "cognitive_max",
3885
+ unit: "count",
3886
+ appliesTo: FILE,
3887
+ rollup: "max",
3888
+ direction: "higher-worse",
3889
+ absent: "exclude",
3890
+ source: "source-metrics",
3891
+ description: "Highest cognitive complexity of any function in the file."
3892
+ },
3893
+ {
3894
+ name: "cognitive_sum",
3895
+ unit: "count",
3896
+ appliesTo: FILE,
3897
+ rollup: "sum",
3898
+ direction: "higher-worse",
3899
+ absent: "zero",
3900
+ source: "source-metrics",
3901
+ description: "Cognitive complexity summed over the file's functions."
3902
+ },
3903
+ {
3904
+ name: "max_nesting_depth",
3905
+ unit: "count",
3906
+ appliesTo: FILE,
3907
+ rollup: "max",
3908
+ direction: "higher-worse",
3909
+ absent: "exclude",
3910
+ source: "source-metrics",
3911
+ description: "Deepest block nesting inside any function in the file."
3912
+ },
3913
+ {
3914
+ name: "symbol_cognitive",
3915
+ unit: "count",
3916
+ appliesTo: SYMBOL,
3917
+ rollup: "max",
3918
+ direction: "higher-worse",
3919
+ absent: "exclude",
3920
+ source: "source-metrics",
3921
+ description: "Cognitive complexity of the function a symbol names; the max when several functions share the name."
3922
+ },
3923
+ {
3924
+ name: "symbol_cyclomatic",
3925
+ unit: "count",
3926
+ appliesTo: SYMBOL,
3927
+ rollup: "max",
3928
+ direction: "higher-worse",
3929
+ absent: "exclude",
3930
+ source: "source-metrics",
3931
+ description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
3932
+ },
3933
+ {
3934
+ name: "class_count",
3935
+ unit: "count",
3936
+ appliesTo: FILE,
3937
+ rollup: "sum",
3938
+ direction: "neutral",
3939
+ absent: "zero",
3940
+ source: "lcom",
3941
+ description: "Classes declared in the file; written only when there is one."
3942
+ },
3943
+ {
3944
+ name: "lcom4_max",
3945
+ unit: "count",
3946
+ appliesTo: FILE,
3947
+ rollup: "max",
3948
+ direction: "higher-worse",
3949
+ absent: "exclude",
3950
+ source: "lcom",
3951
+ description: "Highest LCOM4 of any class in the file; above 1 means the class could split."
3952
+ }
3953
+ ];
3954
+ var SMELLS = [
3955
+ {
3956
+ name: "unreachable_statements",
3957
+ unit: "count",
3958
+ appliesTo: FILE,
3959
+ rollup: "sum",
3960
+ direction: "higher-worse",
3961
+ absent: "zero",
3962
+ source: "dead-code",
3963
+ description: "Statements after an unconditional return, throw, break, or continue. TypeScript only."
3964
+ },
3965
+ {
3966
+ name: "unused_locals",
3967
+ unit: "count",
3968
+ appliesTo: FILE,
3969
+ rollup: "sum",
3970
+ direction: "higher-worse",
3971
+ absent: "zero",
3972
+ source: "dead-code",
3973
+ description: "Local bindings never read. TypeScript only."
3974
+ },
3975
+ {
3976
+ name: "unused_params",
3977
+ unit: "count",
3978
+ appliesTo: FILE,
3979
+ rollup: "sum",
3980
+ direction: "higher-worse",
3981
+ absent: "zero",
3982
+ source: "dead-code",
3983
+ description: "Trailing parameters never read. TypeScript only."
3984
+ },
3985
+ {
3986
+ name: "loop_depth",
3987
+ unit: "count",
3988
+ appliesTo: FILE,
3989
+ rollup: "max",
3990
+ direction: "higher-worse",
3991
+ absent: "zero",
3992
+ source: "growth-risk",
3993
+ description: "Deepest lexical loop nesting; written at 2 or more."
3994
+ },
3995
+ {
3996
+ name: "recursive_functions",
3997
+ unit: "count",
3998
+ appliesTo: FILE,
3999
+ rollup: "sum",
4000
+ direction: "higher-worse",
4001
+ absent: "zero",
4002
+ source: "growth-risk",
4003
+ description: "Functions that call themselves by name. TypeScript only."
4004
+ },
4005
+ {
4006
+ name: "search_in_loop",
4007
+ unit: "count",
4008
+ appliesTo: FILE,
4009
+ rollup: "sum",
4010
+ direction: "higher-worse",
4011
+ absent: "zero",
4012
+ source: "growth-risk",
4013
+ description: "Linear searches such as .includes or .find inside a loop. TypeScript only."
4014
+ }
4015
+ ];
4016
+ var HISTORY = [
4017
+ {
4018
+ name: "churn_{w}",
4019
+ unit: "lines",
4020
+ appliesTo: FILE,
4021
+ rollup: "sum",
4022
+ direction: "neutral",
4023
+ absent: "zero",
4024
+ source: "history",
4025
+ windowed: true,
4026
+ description: "Lines added plus deleted in the {w} window."
4027
+ },
4028
+ {
4029
+ name: "churn_{w}_commits",
4030
+ unit: "count",
4031
+ appliesTo: FILE,
4032
+ rollup: "none",
4033
+ direction: "neutral",
4034
+ absent: "zero",
4035
+ source: "history",
4036
+ windowed: true,
4037
+ description: "Commits touching the file in the {w} window. A group's distinct commits are not the sum."
4038
+ },
4039
+ {
4040
+ name: "churn_{w}_authors",
4041
+ unit: "count",
4042
+ appliesTo: FILE,
4043
+ rollup: "none",
4044
+ direction: "neutral",
4045
+ absent: "zero",
4046
+ source: "history",
4047
+ windowed: true,
4048
+ description: "Distinct authors of the file's commits in the {w} window. Not additive."
4049
+ },
4050
+ {
4051
+ name: "recency_{w}",
4052
+ unit: "ratio",
4053
+ appliesTo: FILE,
4054
+ rollup: "none",
4055
+ direction: "neutral",
4056
+ absent: "exclude",
4057
+ source: "history",
4058
+ windowed: true,
4059
+ description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime. A hotspot discount."
4060
+ },
4061
+ {
4062
+ name: "file_age_days",
4063
+ unit: "days",
4064
+ appliesTo: FILE,
4065
+ rollup: "max",
4066
+ direction: "neutral",
4067
+ absent: "exclude",
4068
+ source: "history",
4069
+ description: "Days since the file first appeared in git history."
4070
+ },
4071
+ {
4072
+ name: "bus_factor_{w}",
4073
+ unit: "count",
4074
+ appliesTo: FILE,
4075
+ rollup: "none",
4076
+ direction: "lower-worse",
4077
+ absent: "exclude",
4078
+ source: "history",
4079
+ windowed: true,
4080
+ description: "Fewest authors covering half the file's churn in the {w} window."
4081
+ },
4082
+ {
4083
+ name: "top_author_share_{w}",
4084
+ unit: "ratio",
4085
+ appliesTo: FILE,
4086
+ rollup: "none",
4087
+ direction: "higher-worse",
4088
+ absent: "exclude",
4089
+ source: "history",
4090
+ windowed: true,
4091
+ description: "Share of the file's churn in the {w} window written by its top author."
4092
+ }
4093
+ ];
4094
+ var TESTS = [
4095
+ {
4096
+ name: "linked_test_count",
4097
+ unit: "count",
4098
+ appliesTo: FILE,
4099
+ rollup: "none",
4100
+ direction: "lower-worse",
4101
+ absent: "zero",
4102
+ source: "test-linker",
4103
+ description: "Test files linked to this source. One test can link to several sources, so it does not sum."
4104
+ },
4105
+ {
4106
+ name: "test_bus_factor_{w}",
4107
+ unit: "count",
4108
+ appliesTo: FILE,
4109
+ rollup: "none",
4110
+ direction: "lower-worse",
4111
+ absent: "exclude",
4112
+ source: "test-linker",
4113
+ windowed: true,
4114
+ description: "Bus factor of the churn on this source's linked tests in the {w} window."
4115
+ },
4116
+ {
4117
+ name: "test_top_author_share_{w}",
4118
+ unit: "ratio",
4119
+ appliesTo: FILE,
4120
+ rollup: "none",
4121
+ direction: "higher-worse",
4122
+ absent: "exclude",
4123
+ source: "test-linker",
4124
+ windowed: true,
4125
+ description: "Top author's share of the churn on this source's linked tests in the {w} window."
4126
+ },
4127
+ {
4128
+ name: "coverage_pct",
4129
+ unit: "percent",
4130
+ appliesTo: ["file", "symbol"],
4131
+ rollup: "none",
4132
+ direction: "lower-worse",
4133
+ absent: "exclude",
4134
+ source: "coverage",
4135
+ description: "Functions covered over functions total, from an ingested Istanbul report. Never written by the indexer."
4136
+ }
4137
+ ];
4138
+ var METRIC_CATALOGUE = [...DEGREE, ...SOURCE, ...SMELLS, ...HISTORY, ...TESTS];
4139
+
4140
+ // src/catalogue/describe.ts
4141
+ var WINDOW_TOKEN = "{w}";
4142
+ var WINDOW_PATTERN = "(\\d+d|lifetime)";
4143
+ var BY_NAME = new Map(METRIC_CATALOGUE.filter((d) => !d.windowed).map((d) => [d.name, d]));
4144
+ var WINDOWED = METRIC_CATALOGUE.filter((d) => d.windowed).map((template) => ({
4145
+ pattern: new RegExp(`^${template.name.replace(WINDOW_TOKEN, WINDOW_PATTERN)}$`),
4146
+ template
4147
+ }));
4148
+ function resolveWindow(template, window) {
4149
+ const resolved = {
4150
+ ...template,
4151
+ name: template.name.replace(WINDOW_TOKEN, window),
4152
+ description: template.description.replace(WINDOW_TOKEN, window),
4153
+ window
4154
+ };
4155
+ delete resolved.windowed;
4156
+ return resolved;
4157
+ }
4158
+ function describeMetric(name) {
4159
+ const exact = BY_NAME.get(name);
4160
+ if (exact) return exact;
4161
+ for (const { pattern, template } of WINDOWED) {
4162
+ const match = pattern.exec(name);
4163
+ if (match) return resolveWindow(template, match[1]);
4164
+ }
4165
+ return null;
4166
+ }
4167
+ function describeMetrics(names) {
4168
+ const described = [];
4169
+ const unknown = [];
4170
+ for (const name of new Set(names)) {
4171
+ const descriptor = describeMetric(name);
4172
+ if (descriptor) described.push(descriptor);
4173
+ else unknown.push(name);
4174
+ }
4175
+ return { described, unknown };
4176
+ }
4177
+
4178
+ // src/check/context.ts
4179
+ function buildRuleContext(store, snapshotId) {
4180
+ const nodes = store.listNodes(snapshotId);
4181
+ const edges = store.listEdges(snapshotId);
4182
+ const nodesById = /* @__PURE__ */ new Map();
4183
+ for (const n of nodes) nodesById.set(n.id, n);
4184
+ return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
4185
+ }
4186
+ function indexMetrics(metrics) {
4187
+ const metricsByNode = /* @__PURE__ */ new Map();
4188
+ for (const m of metrics) {
4189
+ if (m.value === null) continue;
4190
+ let inner = metricsByNode.get(m.nodeId);
4191
+ if (!inner) {
4192
+ inner = /* @__PURE__ */ new Map();
4193
+ metricsByNode.set(m.nodeId, inner);
4194
+ }
4195
+ inner.set(m.name, m.value);
4196
+ }
4197
+ return metricsByNode;
4198
+ }
4199
+
4200
+ // src/check/patterns.ts
4201
+ function patternToRegex(pattern) {
4202
+ if (!pattern.includes("*")) {
4203
+ return new RegExp(escapeRegex(pattern));
4204
+ }
4205
+ let out = "";
4206
+ for (let i = 0; i < pattern.length; i++) {
4207
+ const c = pattern[i];
4208
+ if (c === "*" && pattern[i + 1] === "*") {
4209
+ out += ".*";
4210
+ i++;
4211
+ } else if (c === "*") {
4212
+ out += "[^/]*";
4213
+ } else {
4214
+ out += escapeRegex(c);
4215
+ }
4216
+ }
4217
+ return new RegExp(`^${out}$`);
4218
+ }
4219
+ function compilePatterns(patterns) {
4220
+ if (!patterns) return [];
4221
+ return patterns.map(patternToRegex);
4222
+ }
4223
+ function matchesAny(value, patterns) {
4224
+ for (const rx of patterns) {
4225
+ if (rx.test(value)) return true;
4226
+ }
4227
+ return false;
4228
+ }
4229
+ function escapeRegex(s) {
4230
+ return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
4231
+ }
4232
+
4233
+ // src/check/rule-helpers.ts
4234
+ function severityOf(rule) {
4235
+ return rule.severity ?? "error";
4236
+ }
4237
+ function formatNumber(n) {
4238
+ if (Number.isInteger(n)) return String(n);
4239
+ return n.toFixed(3).replace(/\.?0+$/, "");
4240
+ }
4241
+ function isImportEdge(edge) {
4242
+ return edge.kind === "imports" || edge.kind === "re-exports";
4243
+ }
4244
+ function longestPrefixMatcher(prefixes) {
4245
+ const byLength = [...prefixes].sort((a, b) => b.length - a.length);
4246
+ return (id) => {
4247
+ for (const p of byLength) {
4248
+ if (id === p || id.startsWith(`${p}/`)) return p;
4249
+ }
4250
+ return null;
4251
+ };
4252
+ }
4253
+
4254
+ // src/check/import-rules.ts
4255
+ function layerIndex(layers) {
4256
+ const packageLayer = /* @__PURE__ */ new Map();
4257
+ for (let i = 0; i < layers.length; i++) {
4258
+ for (const pkg of layers[i]) packageLayer.set(pkg, i);
4259
+ }
4260
+ return packageLayer;
4261
+ }
4262
+ function runLayeredDepsRule(rule, ctx) {
4263
+ const packageLayer = layerIndex(rule.layers);
4264
+ const packageOf = longestPrefixMatcher([...packageLayer.keys()]);
4265
+ const out = [];
4266
+ for (const edge of ctx.edges) {
4267
+ if (!isImportEdge(edge)) continue;
4268
+ const srcPkg = packageOf(edge.srcId);
4269
+ const dstPkg = packageOf(edge.dstId);
4270
+ if (srcPkg === null || dstPkg === null) continue;
4271
+ const srcLayer = packageLayer.get(srcPkg);
4272
+ const dstLayer = packageLayer.get(dstPkg);
4273
+ if (srcLayer >= dstLayer) continue;
4274
+ out.push({
4275
+ ruleId: rule.id,
4276
+ severity: severityOf(rule),
4277
+ nodeId: edge.srcId,
4278
+ destinationId: edge.dstId,
4279
+ message: `${srcPkg} (layer ${srcLayer}) imports ${dstPkg} (layer ${dstLayer})`
4280
+ });
4281
+ }
4282
+ return out;
4283
+ }
4284
+ function runForbidImportRule(rule, ctx) {
4285
+ const fromRx = compilePatterns([rule.from]);
4286
+ const toRx = compilePatterns([rule.to]);
4287
+ const out = [];
4288
+ for (const edge of ctx.edges) {
4289
+ if (!isImportEdge(edge)) continue;
4290
+ if (!matchesAny(edge.srcId, fromRx)) continue;
4291
+ if (!matchesAny(edge.dstId, toRx)) continue;
4292
+ out.push({
4293
+ ruleId: rule.id,
4294
+ severity: severityOf(rule),
4295
+ nodeId: edge.srcId,
4296
+ destinationId: edge.dstId,
4297
+ message: `${edge.srcId} imports ${edge.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
4298
+ });
4299
+ }
4300
+ return out;
4301
+ }
4302
+ function importersByDestination(edges) {
4303
+ const importersByDst = /* @__PURE__ */ new Map();
4304
+ for (const edge of edges) {
4305
+ if (!isImportEdge(edge)) continue;
4306
+ let list = importersByDst.get(edge.dstId);
4307
+ if (!list) {
4308
+ list = [];
4309
+ importersByDst.set(edge.dstId, list);
4310
+ }
4311
+ list.push(edge.srcId);
4312
+ }
4313
+ return importersByDst;
4314
+ }
4315
+ function runNoInternalOnlyBarrelsRule(rule, ctx) {
4316
+ const packageOf = longestPrefixMatcher(rule.packageRoots);
4317
+ const excluders = compilePatterns(rule.exclude);
4318
+ const importersByDst = importersByDestination(ctx.edges);
4319
+ const out = [];
4320
+ for (const node of ctx.nodes) {
4321
+ if (node.kind !== "file") continue;
4322
+ if (node.role !== "barrel") continue;
4323
+ if (matchesAny(node.id, excluders)) continue;
4324
+ const barrelPkg = packageOf(node.id);
4325
+ if (barrelPkg === null) continue;
4326
+ const importers = importersByDst.get(node.id) ?? [];
4327
+ const externalCount = importers.reduce((n, src) => packageOf(src) !== barrelPkg ? n + 1 : n, 0);
4328
+ if (externalCount > 0) continue;
4329
+ out.push({
4330
+ ruleId: rule.id,
4331
+ severity: severityOf(rule),
4332
+ nodeId: node.id,
4333
+ message: barrelMessage(barrelPkg, importers.length)
4334
+ });
4335
+ }
4336
+ return out;
4337
+ }
4338
+ function barrelMessage(barrelPkg, importerCount) {
4339
+ return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
4340
+ }
4341
+
4342
+ // src/check/metric-rules.ts
4343
+ function nodeFilter(rule) {
4344
+ return {
4345
+ kind: rule.kind,
4346
+ excluders: compilePatterns(rule.exclude),
4347
+ excludedRoles: new Set(rule.excludeRoles ?? [])
4348
+ };
4349
+ }
4350
+ function passesFilter(node, filter) {
4351
+ if (filter.kind && node.kind !== filter.kind) return false;
4352
+ if (matchesAny(node.id, filter.excluders)) return false;
4353
+ if (node.role && filter.excludedRoles.has(node.role)) return false;
4354
+ return true;
4355
+ }
4356
+ function runMetricMaxRule(rule, ctx) {
4357
+ const filter = nodeFilter(rule);
4358
+ const out = [];
4359
+ for (const node of ctx.nodes) {
4360
+ if (!passesFilter(node, filter)) continue;
4361
+ const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4362
+ if (value === void 0) continue;
4363
+ if (value <= rule.max) continue;
4364
+ out.push({
4365
+ ruleId: rule.id,
4366
+ severity: severityOf(rule),
4367
+ nodeId: node.id,
4368
+ metric: rule.metric,
4369
+ value,
4370
+ threshold: rule.max,
4371
+ message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
4372
+ });
4373
+ }
4374
+ return out;
4375
+ }
4376
+ function runMetricMinRule(rule, ctx) {
4377
+ const filter = nodeFilter(rule);
4378
+ const out = [];
4379
+ for (const node of ctx.nodes) {
4380
+ if (!passesFilter(node, filter)) continue;
4381
+ const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
4382
+ if (value === void 0) continue;
4383
+ if (value >= rule.min) continue;
4384
+ out.push({
4385
+ ruleId: rule.id,
4386
+ severity: severityOf(rule),
4387
+ nodeId: node.id,
4388
+ metric: rule.metric,
4389
+ value,
4390
+ threshold: rule.min,
4391
+ message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
4392
+ });
4393
+ }
4394
+ return out;
4395
+ }
4396
+ function runMetricProductMaxRule(rule, ctx) {
4397
+ const filter = nodeFilter(rule);
4398
+ const out = [];
4399
+ for (const node of ctx.nodes) {
4400
+ if (!passesFilter(node, filter)) continue;
4401
+ const inner = ctx.metricsByNode.get(node.id);
4402
+ if (!inner) continue;
4403
+ const components = collectComponents(rule.metrics, inner);
4404
+ if (!components) continue;
4405
+ const product = components.reduce((a, b) => a * b, 1);
4406
+ if (product <= rule.max) continue;
4407
+ out.push(productViolation(rule, node.id, components, product));
4408
+ }
4409
+ return out;
4410
+ }
4411
+ function productViolation(rule, nodeId, components, product) {
4412
+ const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
4413
+ return {
4414
+ ruleId: rule.id,
4415
+ severity: severityOf(rule),
4416
+ nodeId,
4417
+ metric: rule.metrics.join(" * "),
4418
+ value: product,
4419
+ threshold: rule.max,
4420
+ message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`
4421
+ };
4422
+ }
4423
+ function collectComponents(metrics, values) {
4424
+ const out = [];
4425
+ for (const m of metrics) {
4426
+ const v = values.get(m);
4427
+ if (v === void 0) return null;
4428
+ out.push(v);
4429
+ }
4430
+ return out;
4431
+ }
4432
+
4433
+ // src/check/rules.ts
4434
+ function runRule(rule, ctx) {
4435
+ switch (rule.type) {
4436
+ case "metric-max":
4437
+ return runMetricMaxRule(rule, ctx);
4438
+ case "metric-min":
4439
+ return runMetricMinRule(rule, ctx);
4440
+ case "metric-product-max":
4441
+ return runMetricProductMaxRule(rule, ctx);
4442
+ case "forbid-import":
4443
+ return runForbidImportRule(rule, ctx);
4444
+ case "layered-deps":
4445
+ return runLayeredDepsRule(rule, ctx);
4446
+ case "no-internal-only-barrels":
4447
+ return runNoInternalOnlyBarrelsRule(rule, ctx);
4448
+ }
4449
+ }
4450
+
4451
+ // src/check/check.ts
4452
+ function runChecks(store, options) {
4453
+ const ctx = buildRuleContext(store, options.snapshotId);
4454
+ const violations = [];
4455
+ for (const rule of options.rules) {
4456
+ violations.push(...runRule(rule, ctx));
4457
+ }
4458
+ if (options.baselineSnapshotId) {
4459
+ const baselineKeys = collectBaselineKeys(store, options.baselineSnapshotId, options.snapshotId, options.rules);
4460
+ for (const v of violations) {
4461
+ if (baselineKeys.has(violationKey(v))) v.isCarryover = true;
4462
+ }
4463
+ }
4464
+ const counts = countByOriginAndSeverity(violations);
4465
+ return {
4466
+ snapshotId: options.snapshotId,
4467
+ baselineSnapshotId: options.baselineSnapshotId,
4468
+ rulesEvaluated: options.rules.length,
4469
+ nodesEvaluated: ctx.nodes.length,
4470
+ violations,
4471
+ ...counts,
4472
+ passed: counts.newErrors === 0
4473
+ };
4474
+ }
4475
+ function snapshotViolations(store, snapshotId, rules) {
4476
+ const ctx = buildRuleContext(store, snapshotId);
4477
+ return rules.flatMap((rule) => runRule(rule, ctx));
4478
+ }
4479
+ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
4480
+ const ctx = buildRuleContext(store, baselineSnapshotId);
4481
+ const chain = aliasChain(store, baselineSnapshotId, snapshotId);
4482
+ const keys = /* @__PURE__ */ new Set();
4483
+ for (const rule of rules) {
4484
+ for (const v of runRule(rule, ctx)) keys.add(rebasedViolationKey(v, chain.resolve));
4485
+ }
4486
+ return keys;
4487
+ }
4488
+ function violationKey(v) {
4489
+ return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
4490
+ }
4491
+ function rebasedViolationKey(v, resolve4) {
4492
+ const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
4493
+ return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
4494
+ }
4495
+ function countByOriginAndSeverity(violations) {
4496
+ const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
4497
+ for (const v of violations) {
4498
+ const isError = v.severity === "error";
4499
+ if (v.isCarryover) {
4500
+ if (isError) counts.carryoverErrors++;
4501
+ else counts.carryoverWarnings++;
4502
+ } else {
4503
+ if (isError) counts.newErrors++;
4504
+ else counts.newWarnings++;
4505
+ }
4506
+ }
4507
+ return counts;
4508
+ }
4509
+
4510
+ // src/check/validate.ts
4511
+ function validateRules(input, options = {}) {
4512
+ if (!input || typeof input !== "object") {
4513
+ throw new Error("rules file must be an object with a `rules` array");
4514
+ }
4515
+ const obj = input;
4516
+ if (!Array.isArray(obj.rules)) {
4517
+ throw new Error("rules file must have a `rules` array");
4518
+ }
4519
+ const warn = options.onWarn ?? (() => {
4520
+ });
4521
+ return obj.rules.map((r, i) => validateRule(r, i, warn));
4522
+ }
4523
+ function healMetricName(name, ruleId, warn) {
4524
+ const target = metricAliasTarget(name);
4525
+ if (target) {
4526
+ warn(`${ruleId}: metric "${name}" is deprecated \u2014 using "${target}"`);
4527
+ return target;
4528
+ }
4529
+ return name;
4530
+ }
4531
+ function validateRule(raw, index, warn) {
4532
+ if (!raw || typeof raw !== "object") {
4533
+ throw new Error(`rule[${index}] must be an object`);
4534
+ }
4535
+ const r = raw;
4536
+ if (typeof r.id !== "string" || !r.id) {
4537
+ throw new Error(`rule[${index}] missing string id`);
4538
+ }
4539
+ if (typeof r.type !== "string") {
4540
+ throw new Error(`rule[${index}] (${r.id}) missing string type`);
4541
+ }
4542
+ switch (r.type) {
4543
+ case "metric-max":
4544
+ return assertMetricMax(r, warn);
4545
+ case "metric-min":
4546
+ return assertMetricMin(r, warn);
4547
+ case "metric-product-max":
4548
+ return assertMetricProductMax(r, warn);
4549
+ case "forbid-import":
4550
+ return assertForbidImport(r);
4551
+ case "layered-deps":
4552
+ return assertLayeredDeps(r);
4553
+ case "no-internal-only-barrels":
4554
+ return assertNoInternalOnlyBarrels(r);
4555
+ default:
4556
+ throw new Error(`rule[${index}] (${r.id}) unknown type "${r.type}"`);
4557
+ }
4558
+ }
4559
+ function assertNoInternalOnlyBarrels(r) {
4560
+ if (!Array.isArray(r.packageRoots) || r.packageRoots.length === 0) {
4561
+ throw new Error(
4562
+ `${r.id}: packageRoots must be a non-empty array of path-prefix strings`
4563
+ );
4564
+ }
4565
+ for (const p of r.packageRoots) {
4566
+ if (typeof p !== "string" || !p) {
4567
+ throw new Error(`${r.id}: each packageRoots entry must be a non-empty string`);
4568
+ }
4569
+ }
4570
+ return {
4571
+ type: "no-internal-only-barrels",
4572
+ id: r.id,
4573
+ packageRoots: r.packageRoots,
4574
+ severity: r.severity,
4575
+ exclude: parseStringArray(r.exclude)
4576
+ };
4577
+ }
4578
+ function assertLayeredDeps(r) {
4579
+ if (!Array.isArray(r.layers) || r.layers.length < 2) {
4580
+ throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
4581
+ }
4582
+ for (const layer of r.layers) {
4583
+ if (!Array.isArray(layer) || !layer.every((p) => typeof p === "string")) {
4584
+ throw new Error(`${r.id}: each layer must be a string[]`);
4585
+ }
4586
+ if (layer.length === 0) {
4587
+ throw new Error(`${r.id}: empty layers not allowed`);
4588
+ }
4589
+ }
4590
+ const seen = /* @__PURE__ */ new Set();
4591
+ for (const layer of r.layers) {
4592
+ for (const pkg of layer) {
4593
+ if (seen.has(pkg)) {
4594
+ throw new Error(`${r.id}: package "${pkg}" appears in more than one layer`);
4595
+ }
4596
+ seen.add(pkg);
4597
+ }
4598
+ }
4599
+ return {
4600
+ type: "layered-deps",
4601
+ id: r.id,
4602
+ layers: r.layers,
4603
+ severity: r.severity
4604
+ };
4605
+ }
4606
+ function assertMetricProductMax(r, warn) {
4607
+ if (!Array.isArray(r.metrics) || r.metrics.length < 2) {
4608
+ throw new Error(`${r.id}: metrics must be an array of 2+ metric names`);
4609
+ }
4610
+ if (!r.metrics.every((m) => typeof m === "string")) {
4611
+ throw new Error(`${r.id}: metrics must be strings`);
4612
+ }
4613
+ if (typeof r.max !== "number") {
4614
+ throw new Error(`${r.id}: max must be a number`);
4615
+ }
4616
+ const ruleId = r.id;
4617
+ return {
4618
+ type: "metric-product-max",
4619
+ id: ruleId,
4620
+ metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
4621
+ max: r.max,
4622
+ kind: r.kind,
4623
+ severity: r.severity,
4624
+ exclude: parseStringArray(r.exclude),
4625
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
4626
+ };
4627
+ }
4628
+ function assertMetricMax(r, warn) {
4629
+ if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
4630
+ if (typeof r.max !== "number") throw new Error(`${r.id}: max must be a number`);
4631
+ const ruleId = r.id;
4632
+ return {
4633
+ type: "metric-max",
4634
+ id: ruleId,
4635
+ metric: healMetricName(r.metric, ruleId, warn),
4636
+ max: r.max,
4637
+ kind: r.kind,
4638
+ severity: r.severity,
4639
+ exclude: parseStringArray(r.exclude),
4640
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
4641
+ };
4642
+ }
4643
+ function assertMetricMin(r, warn) {
4644
+ if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
4645
+ if (typeof r.min !== "number") throw new Error(`${r.id}: min must be a number`);
4646
+ const ruleId = r.id;
4647
+ return {
4648
+ type: "metric-min",
4649
+ id: ruleId,
4650
+ metric: healMetricName(r.metric, ruleId, warn),
4651
+ min: r.min,
4652
+ kind: r.kind,
4653
+ severity: r.severity,
4654
+ exclude: parseStringArray(r.exclude),
4655
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
4656
+ };
4657
+ }
4658
+ function assertForbidImport(r) {
4659
+ if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
4660
+ if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
4661
+ return {
4662
+ type: "forbid-import",
4663
+ id: r.id,
4664
+ from: r.from,
4665
+ to: r.to,
4666
+ severity: r.severity
4667
+ };
4668
+ }
4669
+ function parseStringArray(value) {
4670
+ return Array.isArray(value) ? value : void 0;
4671
+ }
4672
+ var ROLE_VALUES = /* @__PURE__ */ new Set([
4673
+ "test",
4674
+ "fixture",
4675
+ "barrel",
4676
+ "types",
4677
+ "config",
4678
+ "entry",
4679
+ "source"
4680
+ ]);
4681
+ function parseRoleArray(ruleId, value, warn) {
4682
+ if (value === void 0) return void 0;
4683
+ if (!Array.isArray(value)) {
4684
+ throw new Error(`${ruleId}: excludeRoles must be an array`);
4685
+ }
4686
+ return value.map((entry) => healRole(entry, ruleId, warn));
4687
+ }
4688
+ function healRole(entry, ruleId, warn) {
4689
+ if (typeof entry === "string" && ROLE_VALUES.has(entry)) {
4690
+ return entry;
4691
+ }
4692
+ if (typeof entry === "string") {
4693
+ const target = roleAliasTarget(entry);
4694
+ if (target) {
4695
+ warn(`${ruleId}: role "${entry}" is deprecated \u2014 using "${target}"`);
4696
+ return target;
4697
+ }
4698
+ }
4699
+ throw new Error(
4700
+ `${ruleId}: unknown role "${entry}" \u2014 valid: ${[...ROLE_VALUES].join(", ")}`
4701
+ );
4702
+ }
4703
+
4704
+ // src/check/run.ts
4705
+ import { readFile as readFile2 } from "fs/promises";
4706
+ function resolveSnapshot(store, spec) {
4707
+ if (typeof spec === "number" || /^\d+$/.test(spec)) {
4708
+ const snap2 = store.getSnapshot(Number(spec));
4709
+ if (!snap2) throw new Error(`no snapshot with id ${spec}`);
4710
+ return snap2;
4711
+ }
4712
+ const snap = store.getLatestSnapshotByRef(spec);
4713
+ if (!snap) throw new Error(`no snapshot found for ref "${spec}"`);
4714
+ return snap;
4715
+ }
4716
+ function checkSnapshot(store, options) {
4717
+ const snapshot = resolveSnapshot(store, options.snapshot);
4718
+ const baselineSnapshot = options.baseline !== void 0 ? resolveSnapshot(store, options.baseline) : void 0;
4719
+ const result = runChecks(store, {
4720
+ snapshotId: snapshot.id,
4721
+ rules: options.rules,
4722
+ baselineSnapshotId: baselineSnapshot?.id
4723
+ });
4724
+ return { snapshot, baselineSnapshot, result };
4725
+ }
4726
+ async function loadCheckRules(path11, options = {}) {
4727
+ let raw;
4728
+ try {
4729
+ raw = await readFile2(path11, "utf8");
4730
+ } catch (err) {
4731
+ throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
4732
+ }
4733
+ let parsed;
4734
+ try {
4735
+ parsed = JSON.parse(raw);
4736
+ } catch (err) {
4737
+ throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
4738
+ }
4739
+ return validateRules(parsed, options);
4740
+ }
4741
+ function errorMessage(err) {
4742
+ return err instanceof Error ? err.message : String(err);
4743
+ }
4744
+
4745
+ // src/diff/diff.ts
4746
+ function resolveId(id, aliases) {
4747
+ return aliases.resolve(id);
4748
+ }
4749
+ function edgeKey2(srcId, dstId, kind) {
4750
+ return JSON.stringify([srcId, dstId, canonicalEdgeKind(kind)]);
4751
+ }
4752
+ function metricKey(nodeId, name) {
4753
+ return JSON.stringify([nodeId, canonicalMetricName(name)]);
4754
+ }
4755
+ function indexEdges(edges) {
4756
+ const m = /* @__PURE__ */ new Map();
4757
+ for (const e of edges) m.set(edgeKey2(e.srcId, e.dstId, e.kind), e);
4758
+ return m;
4759
+ }
4760
+ function diffNodes(fromNodes, toNodes, aliases) {
4761
+ const toIndex = new Map(toNodes.map((n) => [n.id, n]));
4762
+ const matchedToIds = /* @__PURE__ */ new Set();
4763
+ const removed = [];
4764
+ const renamed = [];
4765
+ for (const fromNode of fromNodes) {
4766
+ const newId = resolveId(fromNode.id, aliases);
4767
+ const toNode = toIndex.get(newId);
4768
+ if (!toNode) {
4769
+ removed.push(fromNode);
4770
+ continue;
4771
+ }
4772
+ matchedToIds.add(newId);
4773
+ if (newId !== fromNode.id) {
4774
+ renamed.push({ oldId: fromNode.id, newId, reason: aliases.trace(fromNode.id).reason, node: toNode });
4775
+ }
4776
+ }
4777
+ const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
4778
+ return { added, removed, renamed, commonNewIds: matchedToIds };
4779
+ }
4780
+ function diffEdges(fromEdges, toEdges, aliases) {
4781
+ const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
4782
+ const fromKeys = new Set(fromEdges.map(remappedKey));
4783
+ const toIndex = indexEdges(toEdges);
4784
+ const added = [];
4785
+ for (const [k, e] of toIndex) {
4786
+ if (!fromKeys.has(k)) added.push(e);
4787
+ }
4788
+ const removed = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
4789
+ return { added, removed };
4790
+ }
4791
+ function indexFromMetrics(metrics, aliases) {
4792
+ const fromIndex = /* @__PURE__ */ new Map();
4793
+ for (const m of metrics) {
4794
+ const newId = resolveId(m.nodeId, aliases);
4795
+ fromIndex.set(metricKey(newId, m.name), { metric: m, nodeId: newId });
4796
+ }
4797
+ return fromIndex;
4798
+ }
4799
+ function deltaOf(before, after) {
4800
+ if (before === null || after === null) return null;
4801
+ return after - before;
4802
+ }
4803
+ function metricDelta(nodeId, name, before, after) {
4804
+ return { nodeId, name: canonicalMetricName(name), before, after, delta: deltaOf(before, after) };
4805
+ }
4806
+ function diffMetrics(fromMetrics, toMetrics, aliases, commonNewIds) {
4807
+ const fromIndex = indexFromMetrics(fromMetrics, aliases);
4808
+ const toIndex = new Map(toMetrics.map((m) => [metricKey(m.nodeId, m.name), m]));
4809
+ const out = [];
4810
+ const seen = /* @__PURE__ */ new Set();
4811
+ for (const [key, { metric: before, nodeId }] of fromIndex) {
4812
+ if (!commonNewIds.has(nodeId)) continue;
4813
+ seen.add(key);
4814
+ const afterVal = toIndex.get(key)?.value ?? null;
4815
+ if (before.value !== afterVal) out.push(metricDelta(nodeId, before.name, before.value, afterVal));
4816
+ }
4817
+ for (const [key, after] of toIndex) {
4818
+ if (seen.has(key) || !commonNewIds.has(after.nodeId)) continue;
4819
+ out.push(metricDelta(after.nodeId, after.name, null, after.value));
4820
+ }
4821
+ return out;
4822
+ }
4823
+ function diffSnapshots(store, options) {
4824
+ const { fromSnapshotId, toSnapshotId } = options;
4825
+ const aliases = aliasChain(store, fromSnapshotId, toSnapshotId);
4826
+ const nodeDiff = diffNodes(store.listNodes(fromSnapshotId), store.listNodes(toSnapshotId), aliases);
4827
+ const edgeDiff = diffEdges(store.listEdges(fromSnapshotId), store.listEdges(toSnapshotId), aliases);
4828
+ const metricDeltas = diffMetrics(
4829
+ store.listMetrics(fromSnapshotId),
4830
+ store.listMetrics(toSnapshotId),
4831
+ aliases,
4832
+ nodeDiff.commonNewIds
4833
+ );
4834
+ return {
4835
+ summary: summarize(options, nodeDiff, edgeDiff, metricDeltas),
4836
+ addedNodes: nodeDiff.added,
4837
+ removedNodes: nodeDiff.removed,
4838
+ renamedNodes: nodeDiff.renamed,
4839
+ addedEdges: edgeDiff.added,
4840
+ removedEdges: edgeDiff.removed,
4841
+ metricDeltas
4842
+ };
4843
+ }
4844
+ function summarize({ fromSnapshotId, toSnapshotId }, nodeDiff, edgeDiff, metricDeltas) {
4845
+ return {
4846
+ fromSnapshotId,
4847
+ toSnapshotId,
4848
+ addedNodes: nodeDiff.added.length,
4849
+ removedNodes: nodeDiff.removed.length,
4850
+ renamedNodes: nodeDiff.renamed.length,
4851
+ unchangedNodes: nodeDiff.commonNewIds.size - nodeDiff.renamed.length,
4852
+ addedEdges: edgeDiff.added.length,
4853
+ removedEdges: edgeDiff.removed.length,
4854
+ metricChanges: metricDeltas.length
4855
+ };
4856
+ }
4857
+
4858
+ // src/diff/check-diff.ts
4859
+ function diffCheckResults(store, options) {
4860
+ const from = runChecks(store, { snapshotId: options.fromSnapshotId, rules: options.rules });
4861
+ const to = runChecks(store, { snapshotId: options.toSnapshotId, rules: options.rules });
4862
+ const chain = aliasChain(store, options.fromSnapshotId, options.toSnapshotId);
4863
+ return {
4864
+ fromSnapshotId: options.fromSnapshotId,
4865
+ toSnapshotId: options.toSnapshotId,
4866
+ rulesEvaluated: options.rules.length,
4867
+ ...bucketViolations(indexByKey(from.violations, chain.resolve), indexByKey(to.violations))
4868
+ };
4869
+ }
4870
+ function bucketViolations(fromByKey, toByKey) {
4871
+ const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
4872
+ for (const [key, toV] of toByKey) {
4873
+ const fromV = fromByKey.get(key);
4874
+ if (!fromV) {
4875
+ buckets.newViolations.push(toV);
4876
+ continue;
4877
+ }
4878
+ const delta = computeDelta(fromV, toV);
4879
+ const entry = { from: fromV, to: toV, delta };
4880
+ buckets.unchanged.push(entry);
4881
+ if (delta !== null && delta > 0) buckets.worsened.push(entry);
4882
+ else if (delta !== null && delta < 0) buckets.improved.push(entry);
4883
+ }
4884
+ for (const [key, fromV] of fromByKey) {
4885
+ if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
4886
+ }
4887
+ return buckets;
4888
+ }
4889
+ function indexByKey(violations, resolve4) {
4890
+ const out = /* @__PURE__ */ new Map();
4891
+ for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
4892
+ return out;
4893
+ }
4894
+ function computeDelta(from, to) {
4895
+ if (from.value === void 0 || to.value === void 0) return null;
4896
+ return to.value - from.value;
4897
+ }
4898
+
4899
+ // src/embeddings/corpus.ts
4900
+ import { contentHashOf } from "@titan-design/store-sqlite";
4901
+ function buildEmbedText(signature, purpose) {
4902
+ return purpose ? `${signature} -- ${purpose}` : signature;
4903
+ }
4904
+ function hashEmbedText(text) {
4905
+ return contentHashOf(text);
4906
+ }
4907
+ var TEST_OR_FIXTURE_PATH = /\.test\.|\.spec\.|__tests__|\/fixtures\//;
4908
+ var EXCLUDED_FILE_ROLES = /* @__PURE__ */ new Set(["test", "generated"]);
4909
+ function listEmbeddableSymbols(store, snapshotId) {
4910
+ const nodes = store.listNodes(snapshotId, { includeSymbols: true });
4911
+ const fileRoles = /* @__PURE__ */ new Map();
4912
+ for (const n of nodes) if (n.kind === "file") fileRoles.set(n.id, n.role ?? null);
4913
+ const out = [];
4914
+ for (const n of nodes) {
4915
+ const symbol = toEmbeddable(n, fileRoles);
4916
+ if (symbol) out.push(symbol);
4917
+ }
4918
+ return out;
4919
+ }
4920
+ function toEmbeddable(node, fileRoles) {
4921
+ if (node.kind !== "symbol") return null;
4922
+ const attrs = node.attrs ?? {};
4923
+ if (attrs.exported !== true || !attrs.signature) return null;
4924
+ const file = node.parentId ?? node.id.split("#")[0] ?? node.id;
4925
+ if (TEST_OR_FIXTURE_PATH.test(file)) return null;
4926
+ const role = fileRoles.get(file);
4927
+ if (role && EXCLUDED_FILE_ROLES.has(role)) return null;
4928
+ const text = buildEmbedText(attrs.signature, attrs.purpose);
4929
+ return {
4930
+ id: node.id,
4931
+ name: node.name,
4932
+ file,
4933
+ signature: attrs.signature,
4934
+ purpose: attrs.purpose,
4935
+ text,
4936
+ textHash: hashEmbedText(text)
4937
+ };
4938
+ }
4939
+
4940
+ // src/embeddings/cache.ts
4941
+ import { fromFloat32Buffer, normalize, toFloat32Buffer } from "@titan-design/embed";
4942
+ import { CacheBlobTable } from "@titan-design/store-sqlite";
4943
+ var SYMBOL_EMBEDDING_NAMESPACE = "code-graph/symbol-embedding";
4944
+ var EMBED_BATCH_SIZE = 64;
4945
+ function symbolVectorCache(store) {
4946
+ return new CacheBlobTable(store.db, { name: KIT.cacheBlob });
4947
+ }
4948
+ function loadVectorsByHash(store, model, hashes) {
4949
+ const cache = symbolVectorCache(store);
4950
+ const found = /* @__PURE__ */ new Map();
4951
+ for (const contentHash of new Set(hashes)) {
4952
+ const hit = cache.get({ namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash });
4953
+ if (hit) found.set(contentHash, fromFloat32Buffer(hit.value));
4954
+ }
4955
+ return found;
4956
+ }
4957
+ function storeVectors(store, model, rows) {
4958
+ const cache = symbolVectorCache(store);
4959
+ store.db.transaction(() => {
4960
+ for (const r of rows) {
4961
+ const key = { namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash: r.textHash };
4962
+ cache.put(key, toFloat32Buffer(r.vector), { dims: r.vector.length });
4963
+ }
4964
+ })();
4965
+ }
4966
+ async function embedTextsCached(store, embedder, texts) {
4967
+ const hashes = texts.map((t) => hashEmbedText(t));
4968
+ const byHash = loadVectorsByHash(store, embedder.model, hashes);
4969
+ const missing = /* @__PURE__ */ new Map();
4970
+ texts.forEach((text, i) => {
4971
+ if (!byHash.has(hashes[i])) missing.set(hashes[i], text);
4972
+ });
4973
+ const pending = [...missing.entries()];
4974
+ for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
4975
+ const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
4976
+ storeVectors(store, embedder.model, rows);
4977
+ for (const r of rows) byHash.set(r.textHash, r.vector);
4978
+ }
4979
+ return { byHash, newHashes: new Set(missing.keys()) };
4980
+ }
4981
+ async function embedBatch(embedder, batch) {
4982
+ const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
4983
+ if (vectors.length !== batch.length) {
4984
+ throw new Error(`Embedder returned ${vectors.length} vectors for ${batch.length} texts`);
4985
+ }
4986
+ return batch.map(([textHash], j) => ({ textHash, vector: normalize(vectors[j]) }));
4987
+ }
4988
+
4989
+ // src/embeddings/embeddings.ts
4990
+ import { BruteForceVectorIndex, vectorRetriever } from "@titan-design/retrieval";
4991
+ async function embedSnapshot(store, snapshotId, embedder) {
4992
+ const symbols = listEmbeddableSymbols(store, snapshotId);
4993
+ const { newHashes } = await embedTextsCached(store, embedder, symbols.map((s) => s.text));
4994
+ return {
4995
+ model: embedder.model,
4996
+ symbols: symbols.length,
4997
+ withPurpose: symbols.filter((s) => s.purpose).length,
4998
+ embedded: symbols.length,
4999
+ newlyEmbedded: newHashes.size,
5000
+ reused: symbols.filter((s) => !newHashes.has(s.textHash)).length
5001
+ };
5002
+ }
5003
+ async function tryEmbedSnapshot(store, snapshotId, embedder) {
5004
+ try {
5005
+ return { ok: true, result: await embedSnapshot(store, snapshotId, embedder) };
5006
+ } catch (err) {
5007
+ return { ok: false, model: embedder.model, error: err instanceof Error ? err.message : String(err) };
5008
+ }
5009
+ }
5010
+ async function findSimilarCapability(store, snapshotId, query, embedder, opts = {}) {
5011
+ const symbols = listEmbeddableSymbols(store, snapshotId);
5012
+ const index = buildSymbolIndex(store, embedder.model, symbols);
5013
+ if (index.size === 0) {
5014
+ throw new Error(
5015
+ `No capability embeddings stored for snapshot ${snapshotId} (model ${embedder.model}); run embedSnapshot first.`
5016
+ );
5017
+ }
5018
+ const retriever = vectorRetriever(embedder, index);
5019
+ const hits = await retriever.retrieve(query, { limit: opts.limit ?? 10 });
5020
+ const byId = new Map(symbols.map((s) => [s.id, s]));
5021
+ return {
5022
+ query,
5023
+ model: embedder.model,
5024
+ coverage: { symbols: symbols.length, embedded: index.size, withPurpose: symbols.filter((s) => s.purpose).length },
5025
+ candidates: hits.map((h) => toCandidate(byId.get(h.id), h.score ?? 0))
5026
+ };
5027
+ }
5028
+ function buildSymbolIndex(store, model, symbols) {
5029
+ const vectors = loadVectorsByHash(store, model, symbols.map((s) => s.textHash));
5030
+ const index = new BruteForceVectorIndex();
5031
+ for (const s of symbols) {
5032
+ const vector = vectors.get(s.textHash);
5033
+ if (vector) index.add(s.id, vector);
5034
+ }
5035
+ return index;
5036
+ }
5037
+ function toCandidate(s, score) {
5038
+ return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
2356
5039
  }
2357
5040
  export {
5041
+ ALIAS_BASE_ATTR,
2358
5042
  ALL_ROLES,
5043
+ COVERAGE_METRIC_NAME,
2359
5044
  CodeGraphStore,
5045
+ DEAD_CODE_METRIC_NAMES,
5046
+ DEFAULT_CHURN_WINDOWS,
2360
5047
  DOMAIN_DDL,
5048
+ GROWTH_RISK_METRIC_NAMES,
2361
5049
  INDEX_VERSION,
2362
5050
  KIT,
2363
5051
  LanguageExtractor,
5052
+ METRIC_CATALOGUE,
2364
5053
  MIGRATIONS,
2365
5054
  PythonGraphExtractor,
5055
+ SCHEMA_VERSION,
5056
+ SNAPSHOT_SCOPED_TABLES,
2366
5057
  SOURCE_METRIC_NAMES,
5058
+ SYMBOL_EMBEDDING_NAMESPACE,
2367
5059
  SYMBOL_ID_SEP,
2368
5060
  TsMorphGraphExtractor,
5061
+ aggregateMetrics,
5062
+ aliasChain,
2369
5063
  annotateRoles,
5064
+ attributeCoverage,
2370
5065
  buildAliases,
5066
+ buildEmbedText,
2371
5067
  buildFileModuleNodes,
2372
5068
  buildIndexerMetrics,
5069
+ buildLineage,
2373
5070
  canonicalEdgeKind,
2374
5071
  canonicalMetricName,
2375
5072
  canonicalRole,
5073
+ checkSnapshot,
2376
5074
  classifyRole,
2377
5075
  collectDeclaredNames,
2378
5076
  collectDeclaredSpans,
5077
+ compilePatterns,
5078
+ computeDeadCodeMetrics,
5079
+ computeDeepAst,
5080
+ computeGrowthRiskMetrics,
2379
5081
  computeMetrics,
5082
+ computePageRank,
5083
+ computePartitionQuality,
5084
+ computeRecencyWindows,
5085
+ computeRelevance,
2380
5086
  computeRoleHints,
2381
5087
  computeSourceMetrics,
5088
+ computeSymbolConsumers,
5089
+ computeSymbolCoupling,
5090
+ computeTestCoverageOwnership,
5091
+ createAliasChain,
5092
+ describeMetric,
5093
+ describeMetrics,
2382
5094
  detectGitHead,
2383
5095
  detectGitToplevel,
2384
5096
  detectRenames,
5097
+ diffCheckResults,
5098
+ diffSnapshots,
2385
5099
  edgeWeight,
5100
+ embedSnapshot,
5101
+ embedTextsCached,
2386
5102
  externalId,
2387
5103
  fileId,
2388
- getLanguageFromPath,
5104
+ findSimilarCapability,
5105
+ getEdgeWeight,
5106
+ getLanguageFromPath3 as getLanguageFromPath,
2389
5107
  getSupportedLanguages,
5108
+ groupTestsBySource,
2390
5109
  hashContent,
5110
+ hashEmbedText,
2391
5111
  indexPaths,
5112
+ invertBuckets,
2392
5113
  isGeneratedByHeuristic,
2393
5114
  isGeneratedFile,
2394
5115
  isInsideGitRepo,
5116
+ lineagePath,
5117
+ linkTestsToSources,
2395
5118
  listEdges,
5119
+ listEdgesTouching,
5120
+ listEmbeddableSymbols,
2396
5121
  listMetrics,
5122
+ listMetricsForNode,
2397
5123
  listNodes,
5124
+ loadCheckRules,
2398
5125
  loadGeneratedPatterns,
5126
+ loadHistoryMetrics,
5127
+ loadLineage,
5128
+ matchesAny,
2399
5129
  moduleId,
2400
5130
  openCodeGraph,
2401
5131
  packageId,
2402
5132
  parentModuleId,
2403
- parseFile,
5133
+ parseFile2 as parseFile,
2404
5134
  parseSymbolId,
5135
+ patternToRegex,
5136
+ planPrune,
5137
+ priorSnapshotForRef,
2405
5138
  pruneDanglingReferences,
2406
5139
  readSourceFiles,
5140
+ rebasedViolationKey,
5141
+ resolveAlias,
2407
5142
  resolveBarrelEdges,
2408
- shouldIncludeFile,
5143
+ resolveChurnWindows,
5144
+ resolveGitRef,
5145
+ resolveSnapshot,
5146
+ runChecks,
5147
+ runPrune,
5148
+ shouldIncludeFile2 as shouldIncludeFile,
5149
+ snapshotPageRank,
5150
+ snapshotReferenceEdges,
5151
+ snapshotRelevance,
5152
+ snapshotSymbolConsumers,
5153
+ snapshotSymbolCoupling,
5154
+ snapshotViolations,
2409
5155
  structuralSignature,
2410
5156
  symbolId,
2411
- walkSourceFiles
5157
+ testCoverageCountMetrics,
5158
+ tryEmbedSnapshot,
5159
+ validateRules,
5160
+ violationKey,
5161
+ walkSourceFiles,
5162
+ windowSuffix
2412
5163
  };
2413
5164
  //# sourceMappingURL=index.js.map