@titan-design/code-graph 0.1.0 → 0.2.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,3 +1,17 @@
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
16
  import { openDatabase, runMigrations } from "@titan-design/store-sqlite";
3
17
 
@@ -53,6 +67,7 @@ var MIGRATIONS = [
53
67
  { version: 2, name: "code graph columns", up: (db) => db.exec(KIT_EXTENSIONS) },
54
68
  { version: 3, name: "code graph tables", up: (db) => db.exec(DOMAIN_DDL) }
55
69
  ];
70
+ var SCHEMA_VERSION = Math.max(...MIGRATIONS.map((m) => m.version));
56
71
 
57
72
  // src/rows.ts
58
73
  function rowToSnapshot(row) {
@@ -270,126 +285,18 @@ function prepareStatements(db) {
270
285
  };
271
286
  }
272
287
  function openCodeGraph(dbPath) {
273
- const db = openDatabase(dbPath);
288
+ const db = openDatabase(dbPath, { schemaVersion: SCHEMA_VERSION });
274
289
  runMigrations(db, MIGRATIONS);
275
290
  return new CodeGraphStore(db);
276
291
  }
277
292
 
278
293
  // src/indexer.ts
279
294
  import { realpathSync } from "fs";
280
- import * as path10 from "path";
281
-
282
- // src/parser/parser.ts
283
- import { Parser, Language } from "web-tree-sitter";
284
- import { createRequire } from "module";
285
- var require2 = createRequire(import.meta.url);
286
- var initialized = false;
287
- var parsers = /* @__PURE__ */ new Map();
288
- var languages = /* @__PURE__ */ new Map();
289
- function resolveWasmPath(language) {
290
- switch (language) {
291
- case "typescript":
292
- return require2.resolve("tree-sitter-typescript/tree-sitter-typescript.wasm");
293
- case "tsx":
294
- return require2.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
295
- case "python":
296
- return require2.resolve("tree-sitter-python/tree-sitter-python.wasm");
297
- default:
298
- throw new Error(`Unsupported language: ${language}`);
299
- }
300
- }
301
- async function ensureInitialized() {
302
- if (initialized) return;
303
- await Parser.init();
304
- initialized = true;
305
- }
306
- async function getLanguage(language) {
307
- const cached = languages.get(language);
308
- if (cached) return cached;
309
- const wasmPath = resolveWasmPath(language);
310
- const lang = await Language.load(wasmPath);
311
- languages.set(language, lang);
312
- return lang;
313
- }
314
- async function getParser(language) {
315
- const cached = parsers.get(language);
316
- if (cached) return cached;
317
- resolveWasmPath(language);
318
- await ensureInitialized();
319
- const lang = await getLanguage(language);
320
- const parser = new Parser();
321
- parser.setLanguage(lang);
322
- parsers.set(language, parser);
323
- return parser;
324
- }
325
- async function parseFile(content, filePath, language) {
326
- const parser = await getParser(language);
327
- const tree = parser.parse(content);
328
- if (!tree) {
329
- throw new Error(`Failed to parse ${filePath}`);
330
- }
331
- return { tree, content, filePath, language };
332
- }
333
- function getSupportedLanguages() {
334
- return ["typescript", "tsx", "python"];
335
- }
336
-
337
- // src/parser/file-filter.ts
338
- import * as path from "path";
339
- var EXCLUDED_DIRS = [
340
- "node_modules",
341
- "vendor",
342
- "dist",
343
- "build",
344
- ".next",
345
- "__generated__",
346
- ".git",
347
- ".claude",
348
- "coverage"
349
- ];
350
- var EXCLUDED_PATTERNS = [
351
- /\.min\.[jt]sx?$/,
352
- /\.d\.ts$/,
353
- /\.map$/,
354
- /lock\.(json|yaml)$/,
355
- /pnpm-lock\.yaml$/,
356
- /package-lock\.json$/,
357
- /yarn\.lock$/
358
- ];
359
- var LANGUAGE_EXTENSIONS = {
360
- typescript: [".ts", ".tsx"],
361
- javascript: [".js", ".jsx"],
362
- python: [".py"]
363
- };
364
- function isExcludedDir(dirName) {
365
- return EXCLUDED_DIRS.includes(dirName);
366
- }
367
- function shouldIncludeFile(filePath, languages2) {
368
- const segments = filePath.split("/");
369
- if (segments.some((s) => EXCLUDED_DIRS.includes(s))) {
370
- return false;
371
- }
372
- if (EXCLUDED_PATTERNS.some((p) => p.test(filePath))) {
373
- return false;
374
- }
375
- const ext = path.extname(filePath);
376
- const allowedExtensions = languages2.flatMap(
377
- (lang) => LANGUAGE_EXTENSIONS[lang] ?? []
378
- );
379
- return allowedExtensions.includes(ext);
380
- }
381
- function getLanguageFromPath(filePath) {
382
- const ext = path.extname(filePath);
383
- for (const [language, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
384
- if (extensions.includes(ext)) {
385
- return language;
386
- }
387
- }
388
- return null;
389
- }
295
+ import * as path9 from "path";
296
+ import { parseFile } from "@titan-design/code-parser";
390
297
 
391
298
  // src/extractors/ts-morph-extractor.ts
392
- import * as path5 from "path";
299
+ import * as path4 from "path";
393
300
  import { existsSync } from "fs";
394
301
  import {
395
302
  Project,
@@ -399,16 +306,17 @@ import {
399
306
  } from "ts-morph";
400
307
 
401
308
  // src/extractors/file-nodes.ts
402
- import * as path3 from "path";
309
+ import * as path2 from "path";
310
+ import { getLanguageFromPath } from "@titan-design/code-parser";
403
311
 
404
312
  // src/extractors/ids.ts
405
- import * as path2 from "path";
313
+ import * as path from "path";
406
314
  var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
407
315
  function toPosix(p) {
408
- return p.split(path2.sep).join("/");
316
+ return p.split(path.sep).join("/");
409
317
  }
410
318
  function repoRelative(repoRoot, absPath) {
411
- const rel = path2.relative(repoRoot, absPath);
319
+ const rel = path.relative(repoRoot, absPath);
412
320
  return toPosix(rel);
413
321
  }
414
322
  function fileId(repoRoot, absPath) {
@@ -427,8 +335,8 @@ function packageId(name) {
427
335
  return name;
428
336
  }
429
337
  var SYMBOL_ID_SEP = "#";
430
- function symbolId(fileId2, exportName) {
431
- return `${fileId2}${SYMBOL_ID_SEP}${exportName}`;
338
+ function symbolId(fileId2, qualifiedName) {
339
+ return `${fileId2}${SYMBOL_ID_SEP}${qualifiedName}`;
432
340
  }
433
341
  function parseSymbolId(id) {
434
342
  const idx = id.indexOf(SYMBOL_ID_SEP);
@@ -457,8 +365,8 @@ function buildFileModuleNodes(repoRoot, absPath) {
457
365
  const parentId = parentModuleId(mId) ?? void 0;
458
366
  const language = getLanguageFromPath(absPath) ?? "typescript";
459
367
  return [
460
- { id: fId, kind: "file", name: path3.basename(fId), parentId: mId, language },
461
- { id: mId, kind: "module", name: path3.basename(mId), parentId, language }
368
+ { id: fId, kind: "file", name: path2.basename(fId), parentId: mId, language },
369
+ { id: mId, kind: "module", name: path2.basename(mId), parentId, language }
462
370
  ];
463
371
  }
464
372
 
@@ -534,6 +442,65 @@ function isPropertyNamePosition(id) {
534
442
  return false;
535
443
  }
536
444
 
445
+ // src/scope-path.ts
446
+ var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
447
+ "class_declaration",
448
+ "abstract_class_declaration",
449
+ "class",
450
+ "function_declaration",
451
+ "generator_function_declaration",
452
+ "method_definition",
453
+ "internal_module"
454
+ ]);
455
+ var TS_BOUND_SCOPES = /* @__PURE__ */ new Set([
456
+ "class",
457
+ "arrow_function",
458
+ "function_expression",
459
+ "generator_function",
460
+ "object"
461
+ ]);
462
+ var PY_NAMED_SCOPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
463
+ var ANONYMOUS_SCOPE = "<anonymous>";
464
+ function qualify(scope, name) {
465
+ return scope ? `${scope}.${name}` : name;
466
+ }
467
+ function bindingName(node) {
468
+ const parent = node.parent;
469
+ if (!parent) return null;
470
+ if (parent.type === "variable_declarator" || parent.type === "public_field_definition") {
471
+ return parent.childForFieldName("name")?.text ?? null;
472
+ }
473
+ if (parent.type === "pair") return parent.childForFieldName("key")?.text ?? null;
474
+ if (parent.type === "export_statement") return "default";
475
+ return null;
476
+ }
477
+ function scopeSegment(node, python) {
478
+ if (python) {
479
+ return PY_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text ?? null : null;
480
+ }
481
+ const own = TS_NAMED_SCOPES.has(node.type) ? node.childForFieldName("name")?.text : void 0;
482
+ if (own) return own;
483
+ if (!TS_BOUND_SCOPES.has(node.type)) return null;
484
+ return bindingName(node) ?? ANONYMOUS_SCOPE;
485
+ }
486
+ function enter(scope, segment) {
487
+ if (!segment) return scope;
488
+ if (segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`))) {
489
+ return scope;
490
+ }
491
+ return qualify(scope, segment);
492
+ }
493
+ function walkScopes(root, python, visit) {
494
+ const step = (node, scope) => {
495
+ visit(node, scope);
496
+ const inner = enter(scope, scopeSegment(node, python));
497
+ for (const child of node.children) {
498
+ if (child) step(child, inner);
499
+ }
500
+ };
501
+ step(root, "");
502
+ }
503
+
537
504
  // src/declared-names.ts
538
505
  var TS_DECL_TYPES = /* @__PURE__ */ new Set([
539
506
  "function_declaration",
@@ -541,22 +508,24 @@ var TS_DECL_TYPES = /* @__PURE__ */ new Set([
541
508
  "class_declaration"
542
509
  ]);
543
510
  var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
511
+ function collectDeclarations(file) {
512
+ const python = file.language === "python";
513
+ const declTypes = python ? PY_DECL_TYPES : TS_DECL_TYPES;
514
+ const out = [];
515
+ walkScopes(file.tree.rootNode, python, (node, scope) => {
516
+ const named = declaredNodeAt(node, declTypes);
517
+ if (!named) return;
518
+ out.push({
519
+ name: named.name,
520
+ qualifiedName: qualify(scope, named.name),
521
+ span: { startLine: named.node.startPosition.row + 1, endLine: named.node.endPosition.row + 1 }
522
+ });
523
+ });
524
+ return out;
525
+ }
544
526
  function collectDeclaredSpans(file) {
545
- const declTypes = file.language === "python" ? PY_DECL_TYPES : TS_DECL_TYPES;
546
527
  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);
528
+ for (const d of collectDeclarations(file)) spans.set(d.qualifiedName, d.span);
560
529
  return spans;
561
530
  }
562
531
  function collectDeclaredNames(file) {
@@ -754,7 +723,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
754
723
  }
755
724
 
756
725
  // src/extractors/module-resolution.ts
757
- import * as path4 from "path";
726
+ import * as path3 from "path";
758
727
  var RESOLVABLE_EXTS = [
759
728
  ".ts",
760
729
  ".tsx",
@@ -776,20 +745,20 @@ function* relativeResolutionCandidates(base) {
776
745
  const stem = base.slice(0, base.length - jsExt[0].length);
777
746
  for (const ext of RESOLVABLE_EXTS) yield stem + ext;
778
747
  }
779
- for (const ext of RESOLVABLE_EXTS) yield path4.join(base, "index" + ext);
748
+ for (const ext of RESOLVABLE_EXTS) yield path3.join(base, "index" + ext);
780
749
  }
781
750
  function resolveRelativeAbs(fs3, fromAbs, specifier) {
782
751
  if (!isRelativeSpecifier(specifier)) return null;
783
- const base = path4.resolve(path4.dirname(fromAbs), specifier);
752
+ const base = path3.resolve(path3.dirname(fromAbs), specifier);
784
753
  for (const candidate of relativeResolutionCandidates(base)) {
785
754
  if (fs3.fileExistsSync(candidate)) return candidate;
786
755
  }
787
756
  return null;
788
757
  }
789
758
  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;
759
+ const relative4 = path3.relative(repoRoot, abs);
760
+ if (relative4.startsWith("..") || path3.isAbsolute(relative4)) return null;
761
+ if (relative4.split(path3.sep).includes("node_modules")) return null;
793
762
  return fileId(repoRoot, abs);
794
763
  }
795
764
 
@@ -1028,7 +997,7 @@ function remapDistToSrc(abs) {
1028
997
  const base = m[1];
1029
998
  const sub = m[2];
1030
999
  for (const ext of [".ts", ".tsx"]) {
1031
- const candidate = path5.join(base, "src", sub + ext);
1000
+ const candidate = path4.join(base, "src", sub + ext);
1032
1001
  if (existsSync(candidate)) return candidate;
1033
1002
  }
1034
1003
  return abs;
@@ -1038,11 +1007,11 @@ function isTypeScriptFile(file) {
1038
1007
  }
1039
1008
 
1040
1009
  // src/extractors/python-extractor.ts
1041
- import * as path6 from "path";
1010
+ import * as path5 from "path";
1042
1011
  import { existsSync as existsSync2 } from "fs";
1043
1012
  function* candidates(base) {
1044
1013
  yield `${base}.py`;
1045
- yield path6.join(base, "__init__.py");
1014
+ yield path5.join(base, "__init__.py");
1046
1015
  }
1047
1016
  var PythonGraphExtractor = class {
1048
1017
  constructor(repoRoot) {
@@ -1079,10 +1048,10 @@ var PythonGraphExtractor = class {
1079
1048
  resolveDotted(fromAbs, specifier) {
1080
1049
  const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
1081
1050
  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];
1051
+ const relativeBase = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1052
+ const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
1084
1053
  for (const root of roots) {
1085
- for (const candidate of candidates(path6.join(root, ...parts))) {
1054
+ for (const candidate of candidates(path5.join(root, ...parts))) {
1086
1055
  const id = inRepoFileId(this.repoRoot, candidate);
1087
1056
  if (id !== null && existsSync2(candidate)) return id;
1088
1057
  }
@@ -1090,6 +1059,9 @@ var PythonGraphExtractor = class {
1090
1059
  return null;
1091
1060
  }
1092
1061
  };
1062
+ function isPublicName(qualifiedName) {
1063
+ return !qualifiedName.slice(qualifiedName.lastIndexOf(".") + 1).startsWith("_");
1064
+ }
1093
1065
  function symbolNodes(fId, file) {
1094
1066
  return [...collectDeclaredSpans(file)].map(([name, span]) => ({
1095
1067
  id: symbolId(fId, name),
@@ -1097,7 +1069,7 @@ function symbolNodes(fId, file) {
1097
1069
  name,
1098
1070
  parentId: fId,
1099
1071
  language: "python",
1100
- attrs: { exported: !name.startsWith("_"), startLine: span.startLine, endLine: span.endLine }
1072
+ attrs: { exported: isPublicName(name), startLine: span.startLine, endLine: span.endLine }
1101
1073
  }));
1102
1074
  }
1103
1075
  function importSpecifiers(root) {
@@ -1130,17 +1102,13 @@ var LanguageExtractor = class {
1130
1102
  };
1131
1103
 
1132
1104
  // src/git-renames.ts
1133
- import { execFileSync } from "child_process";
1134
- import * as path7 from "path";
1105
+ import * as path6 from "path";
1135
1106
  function detectGitHead(repoRoot) {
1136
1107
  return runGit(repoRoot, ["rev-parse", "HEAD"]);
1137
1108
  }
1138
1109
  function isInsideGitRepo(repoRoot) {
1139
1110
  return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]) === "true";
1140
1111
  }
1141
- function detectGitToplevel(cwd) {
1142
- return runGit(cwd, ["rev-parse", "--show-toplevel"]);
1143
- }
1144
1112
  function detectRenames(options) {
1145
1113
  const threshold = options.similarityThreshold ?? 80;
1146
1114
  const args = [
@@ -1172,12 +1140,12 @@ function parseRenameOutput(text) {
1172
1140
  return out;
1173
1141
  }
1174
1142
  function classifyRename(oldPath, newPath) {
1175
- return path7.posix.dirname(oldPath) === path7.posix.dirname(newPath) ? "rename" : "move";
1143
+ return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
1176
1144
  }
1177
1145
  function renameToAliases(repoRoot, pair) {
1178
1146
  const reason = classifyRename(pair.oldPath, pair.newPath);
1179
- const oldAbs = path7.join(repoRoot, pair.oldPath);
1180
- const newAbs = path7.join(repoRoot, pair.newPath);
1147
+ const oldAbs = path6.join(repoRoot, pair.oldPath);
1148
+ const newAbs = path6.join(repoRoot, pair.newPath);
1181
1149
  return [
1182
1150
  { oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
1183
1151
  { oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
@@ -1195,29 +1163,10 @@ function buildAliases(repoRoot, pairs) {
1195
1163
  }
1196
1164
  return out;
1197
1165
  }
1198
- function discoveryEnv() {
1199
- const env = { ...process.env };
1200
- delete env.GIT_DIR;
1201
- delete env.GIT_INDEX_FILE;
1202
- delete env.GIT_WORK_TREE;
1203
- return env;
1204
- }
1205
- function runGit(cwd, args) {
1206
- try {
1207
- return execFileSync("git", [...args], {
1208
- cwd,
1209
- encoding: "utf-8",
1210
- stdio: ["ignore", "pipe", "ignore"],
1211
- env: discoveryEnv()
1212
- }).trim();
1213
- } catch {
1214
- return null;
1215
- }
1216
- }
1217
1166
 
1218
1167
  // src/generated.ts
1219
1168
  import { readFileSync } from "fs";
1220
- import * as path8 from "path";
1169
+ import * as path7 from "path";
1221
1170
  var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
1222
1171
  var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
1223
1172
  function isGeneratedByHeuristic(id) {
@@ -1229,7 +1178,7 @@ function isGeneratedFile(id, patterns) {
1229
1178
  }
1230
1179
  function loadGeneratedPatterns(rootDir) {
1231
1180
  try {
1232
- const content = readFileSync(path8.join(rootDir, ".gitattributes"), "utf-8");
1181
+ const content = readFileSync(path7.join(rootDir, ".gitattributes"), "utf-8");
1233
1182
  return parseGeneratedPatterns(content);
1234
1183
  } catch {
1235
1184
  return [];
@@ -1332,25 +1281,26 @@ function annotateRoles(nodes, options) {
1332
1281
 
1333
1282
  // src/file-walk.ts
1334
1283
  import * as fs from "fs/promises";
1335
- import * as path9 from "path";
1336
- async function walkSourceFiles(rootDirs, languages2) {
1284
+ import * as path8 from "path";
1285
+ import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
1286
+ async function walkSourceFiles(rootDirs, languages) {
1337
1287
  const seen = /* @__PURE__ */ new Set();
1338
1288
  const out = [];
1339
1289
  for (const rootDir of rootDirs) {
1340
- await collectFiles(rootDir, rootDir, languages2, seen, out);
1290
+ await collectFiles(rootDir, rootDir, languages, seen, out);
1341
1291
  }
1342
1292
  return out;
1343
1293
  }
1344
- async function collectFiles(dir, rootDir, languages2, seen, out) {
1294
+ async function collectFiles(dir, rootDir, languages, seen, out) {
1345
1295
  const entries = await fs.readdir(dir, { withFileTypes: true });
1346
1296
  for (const entry of entries) {
1347
- const full = path9.join(dir, entry.name);
1297
+ const full = path8.join(dir, entry.name);
1348
1298
  if (entry.isDirectory()) {
1349
1299
  if (isExcludedDir(entry.name)) continue;
1350
- await collectFiles(full, rootDir, languages2, seen, out);
1300
+ await collectFiles(full, rootDir, languages, seen, out);
1351
1301
  } else if (entry.isFile() && !seen.has(full)) {
1352
- const relative4 = path9.relative(rootDir, full);
1353
- if (shouldIncludeFile(relative4, [...languages2])) {
1302
+ const relative4 = path8.relative(rootDir, full);
1303
+ if (shouldIncludeFile(relative4, [...languages])) {
1354
1304
  seen.add(full);
1355
1305
  out.push(full);
1356
1306
  }
@@ -1421,6 +1371,7 @@ function resolveBarrelEdges(nodes, edges) {
1421
1371
  // src/incremental.ts
1422
1372
  import { createHash } from "crypto";
1423
1373
  import * as fs2 from "fs/promises";
1374
+ import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
1424
1375
 
1425
1376
  // src/cognitive-complexity.ts
1426
1377
  function cognitiveComplexityOf(body, language) {
@@ -1812,21 +1763,16 @@ function countLoc(content) {
1812
1763
  function analyzeFunctions(file) {
1813
1764
  const stats = [];
1814
1765
  const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
1815
- const visit = (node) => {
1766
+ walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
1816
1767
  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);
1768
+ if (!fn) return;
1769
+ stats.push({
1770
+ name: fn.name === null ? null : qualify(scope, fn.name),
1771
+ cyclomatic: cyclomaticOf(fn.body, file.language),
1772
+ cognitive: cognitiveComplexityOf(fn.body, file.language),
1773
+ nestingDepth: nestingDepthOf(fn.body, file.language, 0)
1774
+ });
1775
+ });
1830
1776
  return stats;
1831
1777
  }
1832
1778
  function functionAt(node, fnTypes) {
@@ -1872,6 +1818,259 @@ function cyclomaticOf(body, language) {
1872
1818
  return complexity;
1873
1819
  }
1874
1820
 
1821
+ // src/analysis/dead-code.ts
1822
+ var DEAD_CODE_METRIC_NAMES = /* @__PURE__ */ new Set([
1823
+ "unreachable_statements",
1824
+ "unused_locals",
1825
+ "unused_params"
1826
+ ]);
1827
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
1828
+ "function_declaration",
1829
+ "function_expression",
1830
+ "arrow_function",
1831
+ "method_definition",
1832
+ "generator_function_declaration",
1833
+ "generator_function"
1834
+ ]);
1835
+ var PARAM_TYPES = /* @__PURE__ */ new Set(["required_parameter", "optional_parameter"]);
1836
+ var TERMINALS = /* @__PURE__ */ new Set([
1837
+ "return_statement",
1838
+ "throw_statement",
1839
+ "break_statement",
1840
+ "continue_statement"
1841
+ ]);
1842
+ function computeDeadCodeMetrics(files, fileIdOf) {
1843
+ const out = [];
1844
+ for (const file of files) {
1845
+ if (file.language !== "typescript" && file.language !== "tsx") continue;
1846
+ const id = fileIdOf(file.filePath);
1847
+ const unreachable = countUnreachable(file.tree.rootNode);
1848
+ if (unreachable > 0) {
1849
+ out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
1850
+ }
1851
+ const { locals, params } = countUnusedBindings(file.tree.rootNode);
1852
+ if (locals > 0) {
1853
+ out.push({ nodeId: id, name: "unused_locals", value: locals, unit: "count" });
1854
+ }
1855
+ if (params > 0) {
1856
+ out.push({ nodeId: id, name: "unused_params", value: params, unit: "count" });
1857
+ }
1858
+ }
1859
+ return out;
1860
+ }
1861
+ function countUnreachable(root) {
1862
+ let count = 0;
1863
+ const visit = (node) => {
1864
+ if (node.type === "statement_block") count += unreachableInBlock(node);
1865
+ for (const child of node.namedChildren) {
1866
+ if (child) visit(child);
1867
+ }
1868
+ };
1869
+ visit(root);
1870
+ return count;
1871
+ }
1872
+ function unreachableInBlock(block) {
1873
+ let seenTerminal = false;
1874
+ let count = 0;
1875
+ for (const child of block.namedChildren) {
1876
+ if (!child) continue;
1877
+ if (seenTerminal && child.type !== "comment" && child.type !== "function_declaration") {
1878
+ count++;
1879
+ }
1880
+ if (TERMINALS.has(child.type)) seenTerminal = true;
1881
+ }
1882
+ return count;
1883
+ }
1884
+ function countUnusedBindings(root) {
1885
+ let locals = 0;
1886
+ let params = 0;
1887
+ const visit = (node) => {
1888
+ if (FUNCTION_TYPES.has(node.type)) {
1889
+ const r = analyzeFunction(node);
1890
+ locals += r.locals;
1891
+ params += r.params;
1892
+ }
1893
+ for (const child of node.namedChildren) {
1894
+ if (child) visit(child);
1895
+ }
1896
+ };
1897
+ visit(root);
1898
+ return { locals, params };
1899
+ }
1900
+ function analyzeFunction(fn) {
1901
+ const declIds = /* @__PURE__ */ new Set();
1902
+ const nameCount = /* @__PURE__ */ new Map();
1903
+ collectBindingDecls(fn, declIds, nameCount);
1904
+ const refs = /* @__PURE__ */ new Map();
1905
+ countReferences(fn, declIds, refs);
1906
+ const isUnused = (name) => !name.startsWith("_") && (nameCount.get(name) ?? 0) <= 1 && (refs.get(name) ?? 0) === 0;
1907
+ const paramNames = ownParamNames(fn);
1908
+ let params = 0;
1909
+ for (let i = paramNames.length - 1; i >= 0; i--) {
1910
+ const name = paramNames[i] ?? null;
1911
+ if (name !== null && isUnused(name)) params++;
1912
+ else break;
1913
+ }
1914
+ let locals = 0;
1915
+ for (const name of ownLocalNames(fn)) {
1916
+ if (isUnused(name)) locals++;
1917
+ }
1918
+ return { locals, params };
1919
+ }
1920
+ function ownParamNames(fn) {
1921
+ const params = fn.childForFieldName("parameters");
1922
+ if (!params) return [];
1923
+ const out = [];
1924
+ for (const p of params.namedChildren) {
1925
+ if (!p || !PARAM_TYPES.has(p.type)) continue;
1926
+ const first = p.namedChild(0);
1927
+ out.push(first?.type === "identifier" ? first.text : null);
1928
+ }
1929
+ return out;
1930
+ }
1931
+ function ownLocalNames(fn) {
1932
+ const body = fn.childForFieldName("body");
1933
+ if (!body) return [];
1934
+ const out = [];
1935
+ const walk = (node) => {
1936
+ if (FUNCTION_TYPES.has(node.type)) return;
1937
+ if (node.type === "variable_declarator") {
1938
+ const name = node.childForFieldName("name");
1939
+ if (name?.type === "identifier") out.push(name.text);
1940
+ }
1941
+ for (const child of node.namedChildren) {
1942
+ if (child) walk(child);
1943
+ }
1944
+ };
1945
+ walk(body);
1946
+ return out;
1947
+ }
1948
+ function collectBindingDecls(fn, ids, nameCount) {
1949
+ const mark = (n) => {
1950
+ if (n?.type !== "identifier") return;
1951
+ ids.add(n.id);
1952
+ nameCount.set(n.text, (nameCount.get(n.text) ?? 0) + 1);
1953
+ };
1954
+ const walk = (node) => {
1955
+ if (PARAM_TYPES.has(node.type)) mark(node.namedChild(0));
1956
+ else if (node.type === "variable_declarator") mark(node.childForFieldName("name"));
1957
+ for (const child of node.namedChildren) {
1958
+ if (child) walk(child);
1959
+ }
1960
+ };
1961
+ walk(fn);
1962
+ }
1963
+ function countReferences(fn, declIds, out) {
1964
+ const walk = (node) => {
1965
+ if ((node.type === "identifier" || node.type === "shorthand_property_identifier") && !declIds.has(node.id)) {
1966
+ out.set(node.text, (out.get(node.text) ?? 0) + 1);
1967
+ }
1968
+ for (const child of node.namedChildren) {
1969
+ if (child) walk(child);
1970
+ }
1971
+ };
1972
+ walk(fn);
1973
+ }
1974
+
1975
+ // src/analysis/growth-risk.ts
1976
+ var GROWTH_RISK_METRIC_NAMES = /* @__PURE__ */ new Set([
1977
+ "loop_depth",
1978
+ "recursive_functions",
1979
+ "search_in_loop"
1980
+ ]);
1981
+ var TS_LOOP_TYPES = /* @__PURE__ */ new Set([
1982
+ "for_statement",
1983
+ "for_in_statement",
1984
+ "while_statement",
1985
+ "do_statement"
1986
+ ]);
1987
+ var PY_LOOP_TYPES = /* @__PURE__ */ new Set(["for_statement", "while_statement"]);
1988
+ var NAMED_FN_TYPES = /* @__PURE__ */ new Set([
1989
+ "function_declaration",
1990
+ "generator_function_declaration",
1991
+ "method_definition",
1992
+ "function_definition"
1993
+ ]);
1994
+ var SEARCH_METHODS = /* @__PURE__ */ new Set([
1995
+ "includes",
1996
+ "indexOf",
1997
+ "lastIndexOf",
1998
+ "find",
1999
+ "findIndex",
2000
+ "some",
2001
+ "every",
2002
+ "filter"
2003
+ ]);
2004
+ function computeGrowthRiskMetrics(files, fileIdOf) {
2005
+ const out = [];
2006
+ for (const file of files) {
2007
+ const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
2008
+ const root = file.tree.rootNode;
2009
+ const id = fileIdOf(file.filePath);
2010
+ const depth = maxLoopDepth(root, loopTypes, 0);
2011
+ if (depth >= 2) {
2012
+ out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
2013
+ }
2014
+ const recursion = countDirectRecursion(root);
2015
+ if (recursion > 0) {
2016
+ out.push({ nodeId: id, name: "recursive_functions", value: recursion, unit: "count" });
2017
+ }
2018
+ const searches = countSearchInLoop(root, loopTypes, false);
2019
+ if (searches > 0) {
2020
+ out.push({ nodeId: id, name: "search_in_loop", value: searches, unit: "count" });
2021
+ }
2022
+ }
2023
+ return out;
2024
+ }
2025
+ function countDirectRecursion(root) {
2026
+ let count = 0;
2027
+ const visit = (node) => {
2028
+ if (NAMED_FN_TYPES.has(node.type)) {
2029
+ const name = node.childForFieldName("name")?.text;
2030
+ const body = node.childForFieldName("body");
2031
+ if (name && body && callsName(body, name)) count++;
2032
+ }
2033
+ for (const child of node.namedChildren) {
2034
+ if (child) visit(child);
2035
+ }
2036
+ };
2037
+ visit(root);
2038
+ return count;
2039
+ }
2040
+ function callsName(node, name) {
2041
+ if (node.type === "call_expression") {
2042
+ const fn = node.childForFieldName("function");
2043
+ if (fn?.type === "identifier" && fn.text === name) return true;
2044
+ }
2045
+ for (const child of node.namedChildren) {
2046
+ if (child && callsName(child, name)) return true;
2047
+ }
2048
+ return false;
2049
+ }
2050
+ function countSearchInLoop(node, loopTypes, inLoop) {
2051
+ let count = 0;
2052
+ if (inLoop && node.type === "call_expression") {
2053
+ const fn = node.childForFieldName("function");
2054
+ const prop = fn?.type === "member_expression" ? fn.childForFieldName("property") : null;
2055
+ if (prop && SEARCH_METHODS.has(prop.text)) count++;
2056
+ }
2057
+ const childInLoop = inLoop || loopTypes.has(node.type);
2058
+ for (const child of node.namedChildren) {
2059
+ if (child) count += countSearchInLoop(child, loopTypes, childInLoop);
2060
+ }
2061
+ return count;
2062
+ }
2063
+ function maxLoopDepth(node, loopTypes, depth) {
2064
+ let max = depth;
2065
+ for (const child of node.namedChildren) {
2066
+ if (!child) continue;
2067
+ const next = loopTypes.has(child.type) ? depth + 1 : depth;
2068
+ const childMax = maxLoopDepth(child, loopTypes, next);
2069
+ if (childMax > max) max = childMax;
2070
+ }
2071
+ return max;
2072
+ }
2073
+
1875
2074
  // src/reconstruct.ts
1876
2075
  function reconstructFragment(repoRoot, absPath, fileId2, basis) {
1877
2076
  const nodes = buildFileModuleNodes(repoRoot, absPath);
@@ -1922,7 +2121,7 @@ function structuralSignature(file) {
1922
2121
  return hashContent(parts.join(""));
1923
2122
  }
1924
2123
  function sourceLanguage(filePath) {
1925
- const language = getLanguageFromPath(filePath);
2124
+ const language = getLanguageFromPath2(filePath);
1926
2125
  return language === "typescript" || language === "python" ? language : null;
1927
2126
  }
1928
2127
  async function readSourceFiles(filePaths) {
@@ -2019,7 +2218,8 @@ function loadReuseBasis(db, indexVersion) {
2019
2218
  }
2020
2219
  const sourceMetricsByFile = /* @__PURE__ */ new Map();
2021
2220
  for (const m of db.listMetrics(snap.id)) {
2022
- if (!SOURCE_METRIC_NAMES.has(m.name)) continue;
2221
+ if (!SOURCE_METRIC_NAMES.has(m.name) && !DEAD_CODE_METRIC_NAMES.has(m.name) && !GROWTH_RISK_METRIC_NAMES.has(m.name))
2222
+ continue;
2023
2223
  const node = nodesById.get(m.nodeId);
2024
2224
  const fileKey = node?.kind === "symbol" && node.parentId ? node.parentId : m.nodeId;
2025
2225
  const bucket = sourceMetricsByFile.get(fileKey);
@@ -2163,6 +2363,199 @@ function computeMetrics(nodes, edges) {
2163
2363
  return out;
2164
2364
  }
2165
2365
 
2366
+ // src/analysis/test-linker.ts
2367
+ var DEFAULT_MIN_CO_EDIT_COUNT = 2;
2368
+ var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
2369
+ var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
2370
+ function pathCandidates(testId) {
2371
+ const out = /* @__PURE__ */ new Set();
2372
+ const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
2373
+ if (deInfixed !== testId) out.add(deInfixed);
2374
+ const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
2375
+ if (deDir !== testId) out.add(deDir);
2376
+ return [...out];
2377
+ }
2378
+ function splitFileIds(nodes) {
2379
+ const testIds = [];
2380
+ const nonTestIds = /* @__PURE__ */ new Set();
2381
+ for (const n of nodes) {
2382
+ if (n.kind !== "file") continue;
2383
+ if (n.role === "test") testIds.push(n.id);
2384
+ else nonTestIds.add(n.id);
2385
+ }
2386
+ return { testIds, nonTestIds };
2387
+ }
2388
+ function linkTestsToSources(nodes, coEditPairs, options = {}) {
2389
+ const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
2390
+ const { testIds, nonTestIds } = splitFileIds(nodes);
2391
+ const links = [];
2392
+ for (const testId of testIds) {
2393
+ const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
2394
+ if (matched.length > 0) {
2395
+ for (const sourceId of matched) {
2396
+ links.push({ testId, sourceId, method: "path" });
2397
+ }
2398
+ continue;
2399
+ }
2400
+ const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
2401
+ if (partner) {
2402
+ links.push({ testId, sourceId: partner.partner, method: "coedit" });
2403
+ }
2404
+ }
2405
+ return links;
2406
+ }
2407
+ function testCoverageCountMetrics(links) {
2408
+ const testsBySource = groupTestsBySource(links);
2409
+ const out = [];
2410
+ for (const [sourceId, tests] of testsBySource) {
2411
+ out.push({
2412
+ nodeId: sourceId,
2413
+ name: "linked_test_count",
2414
+ value: tests.size,
2415
+ unit: "count"
2416
+ });
2417
+ }
2418
+ return out;
2419
+ }
2420
+ function groupTestsBySource(links) {
2421
+ const out = /* @__PURE__ */ new Map();
2422
+ for (const l of links) {
2423
+ let set = out.get(l.sourceId);
2424
+ if (!set) {
2425
+ set = /* @__PURE__ */ new Set();
2426
+ out.set(l.sourceId, set);
2427
+ }
2428
+ set.add(l.testId);
2429
+ }
2430
+ return out;
2431
+ }
2432
+
2433
+ // src/history-recency.ts
2434
+ var DAY_SECONDS = 86400;
2435
+ function windowSuffix(window) {
2436
+ return window === "lifetime" ? "lifetime" : `${window}d`;
2437
+ }
2438
+ function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
2439
+ const out = [];
2440
+ const ageEmitted = /* @__PURE__ */ new Set();
2441
+ for (const [windowDays, ids] of churnedIdsByWindow) {
2442
+ const suffix = windowSuffix(windowDays);
2443
+ for (const id of ids) {
2444
+ const seen = firstSeen.get(id);
2445
+ let recency = 1;
2446
+ if (seen !== void 0) {
2447
+ const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
2448
+ if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
2449
+ if (!ageEmitted.has(id)) {
2450
+ out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
2451
+ ageEmitted.add(id);
2452
+ }
2453
+ }
2454
+ out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
2455
+ }
2456
+ }
2457
+ return out;
2458
+ }
2459
+ function round3(n) {
2460
+ return Math.round(n * 1e3) / 1e3;
2461
+ }
2462
+
2463
+ // src/history-metrics.ts
2464
+ var DEFAULT_CHURN_WINDOWS = [30, 90, 180];
2465
+ function resolveChurnWindows(requested, primaryWindow, includeLifetime) {
2466
+ const base = requested && requested.length > 0 ? requested : DEFAULT_CHURN_WINDOWS;
2467
+ const finite = [.../* @__PURE__ */ new Set([primaryWindow, ...base])].filter((w) => w > 0).sort((a, b) => a - b);
2468
+ return includeLifetime ? [...finite, "lifetime"] : finite;
2469
+ }
2470
+ function loadHistoryMetrics(nodes, idRoot, options = {}) {
2471
+ const knownPaths = collectFileIds(nodes);
2472
+ const primaryWindow = options.churnWindowDays ?? 30;
2473
+ const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
2474
+ const wide = loadChurnEntries({ repoRoot: idRoot, windowDays: windows[windows.length - 1] });
2475
+ if (wide === null) return null;
2476
+ const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
2477
+ const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
2478
+ const primaryEntries = windows.at(-1) === primaryWindow ? wide : entriesWithin(wide, primaryWindow, nowEpoch);
2479
+ const metrics = [
2480
+ ...churnMetrics(churnByWindow),
2481
+ ...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
2482
+ ...recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch)
2483
+ ];
2484
+ if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
2485
+ return { metrics, primaryEntries };
2486
+ }
2487
+ function collectFileIds(nodes) {
2488
+ const out = /* @__PURE__ */ new Set();
2489
+ for (const n of nodes) {
2490
+ if (n.kind === "file") out.add(n.id);
2491
+ }
2492
+ return out;
2493
+ }
2494
+ function churnMetrics(churnByWindow) {
2495
+ const out = [];
2496
+ for (const [window, byPath] of churnByWindow) {
2497
+ const suffix = windowSuffix(window);
2498
+ for (const [nodeId, churn] of byPath) {
2499
+ out.push(
2500
+ { nodeId, name: `churn_${suffix}`, value: churn.lines, unit: "lines" },
2501
+ { nodeId, name: `churn_${suffix}_commits`, value: churn.commits, unit: "count" },
2502
+ { nodeId, name: `churn_${suffix}_authors`, value: churn.authors, unit: "count" }
2503
+ );
2504
+ }
2505
+ }
2506
+ return out;
2507
+ }
2508
+ function ownershipMetrics(entries, window, knownPaths) {
2509
+ const suffix = windowSuffix(window);
2510
+ const out = [];
2511
+ for (const [nodeId, owner] of computeOwnership(entries, { knownPaths })) {
2512
+ out.push(
2513
+ { nodeId, name: `bus_factor_${suffix}`, value: owner.busFactor, unit: "count" },
2514
+ { nodeId, name: `top_author_share_${suffix}`, value: round3(owner.topAuthorShare), unit: "ratio" }
2515
+ );
2516
+ }
2517
+ return out;
2518
+ }
2519
+ function computeTestCoverageOwnership(entries, links, options = {}) {
2520
+ const suffix = windowSuffix(options.windowDays ?? 30);
2521
+ const testsBySource = groupTestsBySource(links);
2522
+ const testIds = /* @__PURE__ */ new Set();
2523
+ for (const tests of testsBySource.values()) {
2524
+ for (const t of tests) testIds.add(t);
2525
+ }
2526
+ const linesByTest = authorLinesByPath(entries, testIds);
2527
+ const out = [];
2528
+ for (const [nodeId, tests] of testsBySource) {
2529
+ const summary = summarizeOwnership(mergeAuthorChurn(tests, linesByTest), options.busFactorThreshold);
2530
+ if (summary === null) continue;
2531
+ out.push(
2532
+ { nodeId, name: `test_bus_factor_${suffix}`, value: summary.busFactor, unit: "count" },
2533
+ { nodeId, name: `test_top_author_share_${suffix}`, value: round3(summary.topAuthorShare), unit: "ratio" }
2534
+ );
2535
+ }
2536
+ return out;
2537
+ }
2538
+ function mergeAuthorChurn(tests, linesByTest) {
2539
+ const byAuthor = /* @__PURE__ */ new Map();
2540
+ for (const testId of tests) {
2541
+ const fileAuthors = linesByTest.get(testId);
2542
+ if (!fileAuthors) continue;
2543
+ for (const [author, lines] of fileAuthors) {
2544
+ byAuthor.set(author, (byAuthor.get(author) ?? 0) + lines);
2545
+ }
2546
+ }
2547
+ return byAuthor;
2548
+ }
2549
+ function recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch) {
2550
+ const churnedByWindow = /* @__PURE__ */ new Map();
2551
+ for (const [window, byPath] of churnByWindow) {
2552
+ if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
2553
+ }
2554
+ if (churnedByWindow.size === 0) return [];
2555
+ const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
2556
+ return computeRecencyWindows(firstSeen, churnedByWindow, nowEpoch);
2557
+ }
2558
+
2166
2559
  // src/index-metrics.ts
2167
2560
  function symbolNamesByFile(nodes) {
2168
2561
  const out = /* @__PURE__ */ new Map();
@@ -2176,6 +2569,7 @@ function symbolNamesByFile(nodes) {
2176
2569
  }
2177
2570
  function buildIndexerMetrics(input) {
2178
2571
  const nodeList = [...input.nodes.values()];
2572
+ const history = input.history ? loadHistoryMetrics(nodeList, input.idRoot, input.history) : null;
2179
2573
  return [
2180
2574
  ...computeMetrics(nodeList, [...input.edges.values()]),
2181
2575
  ...computeSourceMetrics(
@@ -2183,9 +2577,23 @@ function buildIndexerMetrics(input) {
2183
2577
  (p) => fileId(input.idRoot, p),
2184
2578
  symbolNamesByFile(nodeList)
2185
2579
  ),
2186
- ...input.reusedSourceMetrics
2580
+ ...computeDeadCodeMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
2581
+ ...computeGrowthRiskMetrics(input.parsedFiles, (p) => fileId(input.idRoot, p)),
2582
+ ...input.reusedSourceMetrics,
2583
+ ...history?.metrics ?? [],
2584
+ ...computeTestCoverage(nodeList, history?.primaryEntries ?? null, input.history?.churnWindowDays)
2187
2585
  ];
2188
2586
  }
2587
+ function computeTestCoverage(nodes, entries, windowDays) {
2588
+ const coEditPairs = entries ? computeChangeCoupling(entries, { knownPaths: collectFileIds(nodes) }).pairs : [];
2589
+ const links = linkTestsToSources(nodes, coEditPairs);
2590
+ if (links.length === 0) return [];
2591
+ const out = testCoverageCountMetrics(links);
2592
+ if (entries) {
2593
+ out.push(...computeTestCoverageOwnership(entries, links, { windowDays }));
2594
+ }
2595
+ return out;
2596
+ }
2189
2597
 
2190
2598
  // src/merge.ts
2191
2599
  function edgeKey(edge) {
@@ -2204,19 +2612,56 @@ function mergeFragments(fragments) {
2204
2612
  return { nodes, edges };
2205
2613
  }
2206
2614
 
2207
- // src/indexer.ts
2208
- var INDEX_VERSION = "0.11.0";
2209
- var LANGUAGES = ["typescript", "python"];
2210
- function canonicalizePath(p) {
2211
- try {
2212
- return realpathSync(p);
2213
- } catch {
2214
- return p;
2615
+ // src/symbol-aliases.ts
2616
+ var QUALIFIED_SYMBOLS_SINCE = "0.14.0";
2617
+ function versionParts(version) {
2618
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
2619
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
2620
+ }
2621
+ function predatesQualifiedSymbols(indexVersion) {
2622
+ const have = versionParts(indexVersion);
2623
+ if (!have) return true;
2624
+ const since = versionParts(QUALIFIED_SYMBOLS_SINCE);
2625
+ for (let i = 0; i < 3; i++) {
2626
+ if (have[i] !== since[i]) return have[i] < since[i];
2215
2627
  }
2628
+ return false;
2216
2629
  }
2217
- function normalizePaths(paths) {
2630
+ function qualifiedSymbolAliases(files, nodeIds) {
2631
+ const out = [];
2632
+ for (const { fileId: fileId2, file } of files) {
2633
+ for (const [name, qualified] of qualifiedByName(file)) {
2634
+ const oldId = symbolId(fileId2, name);
2635
+ if (qualified.size !== 1 || nodeIds.has(oldId)) continue;
2636
+ const newId = symbolId(fileId2, [...qualified][0]);
2637
+ if (nodeIds.has(newId)) out.push({ oldId, newId, reason: "requalify" });
2638
+ }
2639
+ }
2640
+ return out;
2641
+ }
2642
+ function qualifiedByName(file) {
2643
+ const out = /* @__PURE__ */ new Map();
2644
+ for (const d of collectDeclarations(file)) {
2645
+ const bucket = out.get(d.name);
2646
+ if (bucket) bucket.add(d.qualifiedName);
2647
+ else out.set(d.name, /* @__PURE__ */ new Set([d.qualifiedName]));
2648
+ }
2649
+ return out;
2650
+ }
2651
+
2652
+ // src/indexer.ts
2653
+ var INDEX_VERSION = "0.14.0";
2654
+ var LANGUAGES = ["typescript", "python"];
2655
+ function canonicalizePath(p) {
2656
+ try {
2657
+ return realpathSync(p);
2658
+ } catch {
2659
+ return p;
2660
+ }
2661
+ }
2662
+ function normalizePaths(paths) {
2218
2663
  if (paths.length === 0) throw new Error("indexPaths requires at least one path");
2219
- return [...new Set(paths.map((p) => path10.resolve(p)))];
2664
+ return [...new Set(paths.map((p) => path9.resolve(p)))];
2220
2665
  }
2221
2666
  function countByKind(items) {
2222
2667
  const out = {};
@@ -2243,6 +2688,12 @@ function resolveAliases(rootDir, idRoot, options, store) {
2243
2688
  if (target === priorCommit) return [];
2244
2689
  return buildAliases(idRoot, detectRenames({ repoRoot: rootDir, fromCommit: priorCommit, toCommit: target }));
2245
2690
  }
2691
+ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
2692
+ const prior = store.listSnapshots({ limit: 1 })[0];
2693
+ if (!prior || !predatesQualifiedSymbols(prior.indexVersion)) return [];
2694
+ const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
2695
+ return qualifiedSymbolAliases(files, nodeIds);
2696
+ }
2246
2697
  function resolveRoots(paths) {
2247
2698
  const raw = normalizePaths(paths);
2248
2699
  const gitToplevel = detectGitToplevel(raw[0]);
@@ -2256,6 +2707,13 @@ async function parseAll(toParse) {
2256
2707
  }
2257
2708
  return parsed;
2258
2709
  }
2710
+ function historyOptions(options) {
2711
+ return {
2712
+ churnWindowDays: options.churnWindowDays,
2713
+ churnWindows: options.churnWindows,
2714
+ includeLifetime: options.lifetime === true
2715
+ };
2716
+ }
2259
2717
  async function indexPaths(store, options) {
2260
2718
  const { rootDirs, idRoot } = resolveRoots(options.paths);
2261
2719
  const readFiles = await readSourceFiles(await walkSourceFiles(rootDirs, LANGUAGES));
@@ -2283,10 +2741,14 @@ async function indexPaths(store, options) {
2283
2741
  edges: accumulator.edges,
2284
2742
  parsedFiles: [...parsedByPath.values()],
2285
2743
  reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
2286
- idRoot
2744
+ idRoot,
2745
+ history: options.computeChurn === false ? void 0 : historyOptions(options)
2287
2746
  });
2288
2747
  const rootDir = rootDirs[0];
2289
- const aliases = resolveAliases(rootDir, idRoot, options, store);
2748
+ const aliases = [
2749
+ ...resolveAliases(rootDir, idRoot, options, store),
2750
+ ...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
2751
+ ];
2290
2752
  const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
2291
2753
  const snapshotId = persist(store, options.ref ?? "wd", commitHash, accumulator, aliases, metrics);
2292
2754
  store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
@@ -2305,6 +2767,9 @@ async function indexPaths(store, options) {
2305
2767
  };
2306
2768
  }
2307
2769
 
2770
+ // src/index.ts
2771
+ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
2772
+
2308
2773
  // src/aliases.ts
2309
2774
  var METRIC_ALIASES = /* @__PURE__ */ new Map([
2310
2775
  ["lines", "loc"],
@@ -2334,6 +2799,9 @@ var EDGE_KIND_ALIASES = /* @__PURE__ */ new Map([
2334
2799
  ["depends_on", "depends-on"],
2335
2800
  ["dependson", "depends-on"]
2336
2801
  ]);
2802
+ function deprecatedTarget(map, name) {
2803
+ return map.get(name);
2804
+ }
2337
2805
  function canonicalMetricName(name) {
2338
2806
  return METRIC_ALIASES.get(name) ?? name;
2339
2807
  }
@@ -2343,6 +2811,332 @@ function canonicalRole(name) {
2343
2811
  function canonicalEdgeKind(name) {
2344
2812
  return EDGE_KIND_ALIASES.get(name) ?? name;
2345
2813
  }
2814
+ function metricAliasTarget(name) {
2815
+ return deprecatedTarget(METRIC_ALIASES, name);
2816
+ }
2817
+ function roleAliasTarget(name) {
2818
+ return deprecatedTarget(ROLE_ALIASES, name);
2819
+ }
2820
+
2821
+ // src/analysis/coverage.ts
2822
+ var COVERAGE_METRIC_NAME = "coverage_pct";
2823
+ function isFileCoverage(v) {
2824
+ return typeof v === "object" && v !== null && typeof v.fnMap === "object" && typeof v.f === "object";
2825
+ }
2826
+ function innermostContaining(symbols, line) {
2827
+ let best = null;
2828
+ for (const s of symbols) {
2829
+ if (s.startLine <= line && line <= s.endLine) {
2830
+ if (!best || s.endLine - s.startLine < best.endLine - best.startLine) best = s;
2831
+ }
2832
+ }
2833
+ return best;
2834
+ }
2835
+ function attributeCoverage(coverage, fileIdOf, symbolsByFile) {
2836
+ const out = [];
2837
+ for (const [absPath, fc] of Object.entries(coverage)) {
2838
+ if (!isFileCoverage(fc)) continue;
2839
+ const fileId2 = fileIdOf(absPath);
2840
+ if (!fileId2) continue;
2841
+ const fnIds = Object.keys(fc.fnMap);
2842
+ if (fnIds.length === 0) continue;
2843
+ const covered = fnIds.filter((k) => (fc.f[k] ?? 0) > 0).length;
2844
+ out.push(pctMetric(fileId2, covered, fnIds.length));
2845
+ out.push(...symbolCoverage(fc, fnIds, symbolsByFile.get(fileId2) ?? []));
2846
+ }
2847
+ return out;
2848
+ }
2849
+ function symbolCoverage(fc, fnIds, symbols) {
2850
+ const perSymbol = /* @__PURE__ */ new Map();
2851
+ for (const k of fnIds) {
2852
+ const sym = innermostContaining(symbols, fc.fnMap[k].loc.start.line);
2853
+ if (!sym) continue;
2854
+ const acc = perSymbol.get(sym.id) ?? { hit: 0, total: 0 };
2855
+ acc.total++;
2856
+ if ((fc.f[k] ?? 0) > 0) acc.hit++;
2857
+ perSymbol.set(sym.id, acc);
2858
+ }
2859
+ return [...perSymbol].map(([id, { hit, total }]) => pctMetric(id, hit, total));
2860
+ }
2861
+ function pctMetric(nodeId, covered, total) {
2862
+ return {
2863
+ nodeId,
2864
+ name: COVERAGE_METRIC_NAME,
2865
+ value: Math.round(100 * covered / total),
2866
+ unit: "percent"
2867
+ };
2868
+ }
2869
+
2870
+ // src/analysis/pagerank.ts
2871
+ var DEFAULT_DAMPING = 0.85;
2872
+ var DEFAULT_TOLERANCE = 1e-6;
2873
+ var DEFAULT_MAX_ITERS = 100;
2874
+ var DEFAULT_EDGE_WEIGHTS = {
2875
+ imports: 1,
2876
+ "re-exports": 0.5,
2877
+ calls: 1.5,
2878
+ extends: 1,
2879
+ implements: 1,
2880
+ references: 1,
2881
+ "depends-on": 1
2882
+ };
2883
+ function getEdgeWeight(kind, overrides) {
2884
+ return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
2885
+ }
2886
+ function computePageRank(nodes, edges, options = {}) {
2887
+ const n = nodes.length;
2888
+ if (n === 0) return { rows: [], iterations: 0, converged: true };
2889
+ const damping = options.damping ?? DEFAULT_DAMPING;
2890
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
2891
+ const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
2892
+ const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
2893
+ const idx = buildIdIndex(nodes);
2894
+ const adj = buildAdjacency(idx, edges, weights, n);
2895
+ const pers = buildPersonalization(idx, n, options.personalization);
2896
+ const { rank, iterations, converged } = powerIterate(
2897
+ pers,
2898
+ adj,
2899
+ damping,
2900
+ tolerance,
2901
+ maxIters
2902
+ );
2903
+ return { rows: rankToRows(nodes, rank), iterations, converged };
2904
+ }
2905
+ function buildIdIndex(nodes) {
2906
+ const idx = /* @__PURE__ */ new Map();
2907
+ for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
2908
+ return idx;
2909
+ }
2910
+ function buildAdjacency(idx, edges, weights, n) {
2911
+ const outNeighbors = Array.from(
2912
+ { length: n },
2913
+ () => []
2914
+ );
2915
+ const outWeightSum = new Array(n).fill(0);
2916
+ for (const e of edges) {
2917
+ const si = idx.get(e.srcId);
2918
+ const di = idx.get(e.dstId);
2919
+ if (si === void 0 || di === void 0) continue;
2920
+ const w = weights[e.kind] ?? 1;
2921
+ if (w <= 0) continue;
2922
+ outNeighbors[si].push({ to: di, w });
2923
+ outWeightSum[si] += w;
2924
+ }
2925
+ return { outNeighbors, outWeightSum };
2926
+ }
2927
+ function powerIterate(pers, adj, damping, tolerance, maxIters) {
2928
+ const n = pers.length;
2929
+ let rank = pers.slice();
2930
+ let next = new Array(n).fill(0);
2931
+ let iterations = 0;
2932
+ let converged = false;
2933
+ for (let iter = 0; iter < maxIters; iter++) {
2934
+ iterations = iter + 1;
2935
+ const dangling = sumDangling(rank, adj.outWeightSum);
2936
+ const teleport = 1 - damping + damping * dangling;
2937
+ seedTeleport(next, pers, teleport);
2938
+ distributeRank(next, rank, adj, damping);
2939
+ const diff = l1Diff(rank, next);
2940
+ [rank, next] = [next, rank];
2941
+ next.fill(0);
2942
+ if (diff < tolerance) {
2943
+ converged = true;
2944
+ break;
2945
+ }
2946
+ }
2947
+ return { rank, iterations, converged };
2948
+ }
2949
+ function sumDangling(rank, outWeightSum) {
2950
+ let total = 0;
2951
+ for (let i = 0; i < rank.length; i++) {
2952
+ if (outWeightSum[i] === 0) total += rank[i];
2953
+ }
2954
+ return total;
2955
+ }
2956
+ function seedTeleport(next, pers, teleport) {
2957
+ for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
2958
+ }
2959
+ function distributeRank(next, rank, adj, damping) {
2960
+ for (let i = 0; i < rank.length; i++) {
2961
+ const s = adj.outWeightSum[i];
2962
+ if (s === 0 || rank[i] === 0) continue;
2963
+ const factor = damping * rank[i] / s;
2964
+ const list = adj.outNeighbors[i];
2965
+ for (let k = 0; k < list.length; k++) {
2966
+ next[list[k].to] += factor * list[k].w;
2967
+ }
2968
+ }
2969
+ }
2970
+ function l1Diff(a, b) {
2971
+ let diff = 0;
2972
+ for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
2973
+ return diff;
2974
+ }
2975
+ function rankToRows(nodes, rank) {
2976
+ return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
2977
+ }
2978
+ function compareRows(a, b) {
2979
+ if (b.score !== a.score) return b.score - a.score;
2980
+ return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
2981
+ }
2982
+ function buildPersonalization(idx, n, pers) {
2983
+ if (!pers || pers.size === 0) return uniformVector(n);
2984
+ const out = new Array(n).fill(0);
2985
+ const total = applySeedWeights(out, idx, pers);
2986
+ if (total === 0) return uniformVector(n);
2987
+ for (let i = 0; i < n; i++) out[i] /= total;
2988
+ return out;
2989
+ }
2990
+ function uniformVector(n) {
2991
+ return new Array(n).fill(1 / n);
2992
+ }
2993
+ function applySeedWeights(out, idx, pers) {
2994
+ let total = 0;
2995
+ for (const [id, w] of pers) {
2996
+ if (!isValidWeight(w)) continue;
2997
+ const i = idx.get(id);
2998
+ if (i === void 0) continue;
2999
+ out[i] = w;
3000
+ total += w;
3001
+ }
3002
+ return total;
3003
+ }
3004
+ function isValidWeight(w) {
3005
+ return Number.isFinite(w) && w > 0;
3006
+ }
3007
+
3008
+ // src/analysis/relevance.ts
3009
+ function computeRelevance(nodes, edges, seedIds, options = {}) {
3010
+ const seeds = seedIds.filter((id) => id.length > 0);
3011
+ if (seeds.length === 0) return /* @__PURE__ */ new Map();
3012
+ const personalization = new Map(seeds.map((id) => [id, 1]));
3013
+ const result = computePageRank(nodes, symmetrize(edges), {
3014
+ personalization,
3015
+ damping: options.damping,
3016
+ edgeWeights: options.edgeWeights
3017
+ });
3018
+ const out = /* @__PURE__ */ new Map();
3019
+ for (const row of result.rows) out.set(row.nodeId, row.score);
3020
+ return out;
3021
+ }
3022
+ function symmetrize(edges) {
3023
+ const out = [];
3024
+ for (const e of edges) {
3025
+ out.push(e);
3026
+ out.push({ ...e, srcId: e.dstId, dstId: e.srcId });
3027
+ }
3028
+ return out;
3029
+ }
3030
+
3031
+ // src/analysis/symbol-coupling.ts
3032
+ var DEFAULT_MIN_CO_IMPORTS = 2;
3033
+ var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
3034
+ function computeSymbolConsumers(edges) {
3035
+ const bySymbol = /* @__PURE__ */ new Map();
3036
+ for (const e of edges) {
3037
+ let set = bySymbol.get(e.dstId);
3038
+ if (!set) {
3039
+ set = /* @__PURE__ */ new Set();
3040
+ bySymbol.set(e.dstId, set);
3041
+ }
3042
+ set.add(e.srcId);
3043
+ }
3044
+ const out = [];
3045
+ for (const [id, consumers] of bySymbol) {
3046
+ const parsed = parseSymbolId(id);
3047
+ if (!parsed) continue;
3048
+ out.push({
3049
+ symbolId: id,
3050
+ fileId: parsed.fileId,
3051
+ name: parsed.name,
3052
+ consumers: [...consumers].sort()
3053
+ });
3054
+ }
3055
+ return out.sort(compareConsumers);
3056
+ }
3057
+ function compareConsumers(a, b) {
3058
+ if (b.consumers.length !== a.consumers.length) {
3059
+ return b.consumers.length - a.consumers.length;
3060
+ }
3061
+ return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
3062
+ }
3063
+ function computeSymbolCoupling(edges, options = {}) {
3064
+ const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
3065
+ const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
3066
+ const bySource = groupSymbolsBySource(edges);
3067
+ const counts = /* @__PURE__ */ new Map();
3068
+ for (const symbols of bySource.values()) {
3069
+ if (symbols.length < 2 || symbols.length > largeThreshold) continue;
3070
+ accumulateSymbolPairs(symbols, counts);
3071
+ }
3072
+ return finalizePairs(counts, minCoImports);
3073
+ }
3074
+ function groupSymbolsBySource(edges) {
3075
+ const perSource = /* @__PURE__ */ new Map();
3076
+ for (const e of edges) {
3077
+ if (!parseSymbolId(e.dstId)) continue;
3078
+ let set = perSource.get(e.srcId);
3079
+ if (!set) {
3080
+ set = /* @__PURE__ */ new Set();
3081
+ perSource.set(e.srcId, set);
3082
+ }
3083
+ set.add(e.dstId);
3084
+ }
3085
+ const out = /* @__PURE__ */ new Map();
3086
+ for (const [src, set] of perSource) out.set(src, [...set].sort());
3087
+ return out;
3088
+ }
3089
+ function accumulateSymbolPairs(symbols, counts) {
3090
+ for (let i = 0; i < symbols.length; i++) {
3091
+ for (let j = i + 1; j < symbols.length; j++) {
3092
+ const key = `${symbols[i]} ${symbols[j]}`;
3093
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3094
+ }
3095
+ }
3096
+ }
3097
+ function finalizePairs(counts, minCoImports) {
3098
+ const out = [];
3099
+ for (const [key, coImports] of counts) {
3100
+ if (coImports < minCoImports) continue;
3101
+ const [aId, bId] = key.split(" ");
3102
+ const a = parseSymbolId(aId);
3103
+ const b = parseSymbolId(bId);
3104
+ out.push({
3105
+ aId,
3106
+ aFile: a.fileId,
3107
+ aName: a.name,
3108
+ bId,
3109
+ bFile: b.fileId,
3110
+ bName: b.name,
3111
+ coImports,
3112
+ crossFile: a.fileId !== b.fileId
3113
+ });
3114
+ }
3115
+ return out.sort(comparePairs);
3116
+ }
3117
+ function comparePairs(a, b) {
3118
+ if (b.coImports !== a.coImports) return b.coImports - a.coImports;
3119
+ if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
3120
+ if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
3121
+ return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
3122
+ }
3123
+
3124
+ // src/analysis/snapshot.ts
3125
+ function snapshotPageRank(store, snapshotId, options = {}) {
3126
+ return computePageRank(store.listNodes(snapshotId), store.listEdges(snapshotId), options);
3127
+ }
3128
+ function snapshotRelevance(store, snapshotId, seedIds, options = {}) {
3129
+ return computeRelevance(store.listNodes(snapshotId), store.listEdges(snapshotId), seedIds, options);
3130
+ }
3131
+ function snapshotReferenceEdges(store, snapshotId) {
3132
+ return store.listEdges(snapshotId, { includeReferences: true }).filter((e) => e.kind === "references").map((e) => ({ srcId: e.srcId, dstId: e.dstId }));
3133
+ }
3134
+ function snapshotSymbolConsumers(store, snapshotId) {
3135
+ return computeSymbolConsumers(snapshotReferenceEdges(store, snapshotId));
3136
+ }
3137
+ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
3138
+ return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
3139
+ }
2346
3140
 
2347
3141
  // src/read.ts
2348
3142
  function listNodes(store, snapshotId, opts) {
@@ -2354,60 +3148,954 @@ function listEdges(store, snapshotId, opts) {
2354
3148
  function listMetrics(store, snapshotId) {
2355
3149
  return store.listMetrics(snapshotId);
2356
3150
  }
3151
+
3152
+ // src/check/context.ts
3153
+ function buildRuleContext(store, snapshotId) {
3154
+ const nodes = store.listNodes(snapshotId);
3155
+ const edges = store.listEdges(snapshotId);
3156
+ const nodesById = /* @__PURE__ */ new Map();
3157
+ for (const n of nodes) nodesById.set(n.id, n);
3158
+ return { nodes, nodesById, metricsByNode: indexMetrics(store.listMetrics(snapshotId)), edges };
3159
+ }
3160
+ function indexMetrics(metrics) {
3161
+ const metricsByNode = /* @__PURE__ */ new Map();
3162
+ for (const m of metrics) {
3163
+ if (m.value === null) continue;
3164
+ let inner = metricsByNode.get(m.nodeId);
3165
+ if (!inner) {
3166
+ inner = /* @__PURE__ */ new Map();
3167
+ metricsByNode.set(m.nodeId, inner);
3168
+ }
3169
+ inner.set(m.name, m.value);
3170
+ }
3171
+ return metricsByNode;
3172
+ }
3173
+
3174
+ // src/check/patterns.ts
3175
+ function patternToRegex(pattern) {
3176
+ if (!pattern.includes("*")) {
3177
+ return new RegExp(escapeRegex(pattern));
3178
+ }
3179
+ let out = "";
3180
+ for (let i = 0; i < pattern.length; i++) {
3181
+ const c = pattern[i];
3182
+ if (c === "*" && pattern[i + 1] === "*") {
3183
+ out += ".*";
3184
+ i++;
3185
+ } else if (c === "*") {
3186
+ out += "[^/]*";
3187
+ } else {
3188
+ out += escapeRegex(c);
3189
+ }
3190
+ }
3191
+ return new RegExp(`^${out}$`);
3192
+ }
3193
+ function compilePatterns(patterns) {
3194
+ if (!patterns) return [];
3195
+ return patterns.map(patternToRegex);
3196
+ }
3197
+ function matchesAny(value, patterns) {
3198
+ for (const rx of patterns) {
3199
+ if (rx.test(value)) return true;
3200
+ }
3201
+ return false;
3202
+ }
3203
+ function escapeRegex(s) {
3204
+ return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
3205
+ }
3206
+
3207
+ // src/check/rule-helpers.ts
3208
+ function severityOf(rule) {
3209
+ return rule.severity ?? "error";
3210
+ }
3211
+ function formatNumber(n) {
3212
+ if (Number.isInteger(n)) return String(n);
3213
+ return n.toFixed(3).replace(/\.?0+$/, "");
3214
+ }
3215
+ function isImportEdge(edge) {
3216
+ return edge.kind === "imports" || edge.kind === "re-exports";
3217
+ }
3218
+ function longestPrefixMatcher(prefixes) {
3219
+ const byLength = [...prefixes].sort((a, b) => b.length - a.length);
3220
+ return (id) => {
3221
+ for (const p of byLength) {
3222
+ if (id === p || id.startsWith(`${p}/`)) return p;
3223
+ }
3224
+ return null;
3225
+ };
3226
+ }
3227
+
3228
+ // src/check/import-rules.ts
3229
+ function layerIndex(layers) {
3230
+ const packageLayer = /* @__PURE__ */ new Map();
3231
+ for (let i = 0; i < layers.length; i++) {
3232
+ for (const pkg of layers[i]) packageLayer.set(pkg, i);
3233
+ }
3234
+ return packageLayer;
3235
+ }
3236
+ function runLayeredDepsRule(rule, ctx) {
3237
+ const packageLayer = layerIndex(rule.layers);
3238
+ const packageOf = longestPrefixMatcher([...packageLayer.keys()]);
3239
+ const out = [];
3240
+ for (const edge of ctx.edges) {
3241
+ if (!isImportEdge(edge)) continue;
3242
+ const srcPkg = packageOf(edge.srcId);
3243
+ const dstPkg = packageOf(edge.dstId);
3244
+ if (srcPkg === null || dstPkg === null) continue;
3245
+ const srcLayer = packageLayer.get(srcPkg);
3246
+ const dstLayer = packageLayer.get(dstPkg);
3247
+ if (srcLayer >= dstLayer) continue;
3248
+ out.push({
3249
+ ruleId: rule.id,
3250
+ severity: severityOf(rule),
3251
+ nodeId: edge.srcId,
3252
+ destinationId: edge.dstId,
3253
+ message: `${srcPkg} (layer ${srcLayer}) imports ${dstPkg} (layer ${dstLayer})`
3254
+ });
3255
+ }
3256
+ return out;
3257
+ }
3258
+ function runForbidImportRule(rule, ctx) {
3259
+ const fromRx = compilePatterns([rule.from]);
3260
+ const toRx = compilePatterns([rule.to]);
3261
+ const out = [];
3262
+ for (const edge of ctx.edges) {
3263
+ if (!isImportEdge(edge)) continue;
3264
+ if (!matchesAny(edge.srcId, fromRx)) continue;
3265
+ if (!matchesAny(edge.dstId, toRx)) continue;
3266
+ out.push({
3267
+ ruleId: rule.id,
3268
+ severity: severityOf(rule),
3269
+ nodeId: edge.srcId,
3270
+ destinationId: edge.dstId,
3271
+ message: `${edge.srcId} imports ${edge.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
3272
+ });
3273
+ }
3274
+ return out;
3275
+ }
3276
+ function importersByDestination(edges) {
3277
+ const importersByDst = /* @__PURE__ */ new Map();
3278
+ for (const edge of edges) {
3279
+ if (!isImportEdge(edge)) continue;
3280
+ let list = importersByDst.get(edge.dstId);
3281
+ if (!list) {
3282
+ list = [];
3283
+ importersByDst.set(edge.dstId, list);
3284
+ }
3285
+ list.push(edge.srcId);
3286
+ }
3287
+ return importersByDst;
3288
+ }
3289
+ function runNoInternalOnlyBarrelsRule(rule, ctx) {
3290
+ const packageOf = longestPrefixMatcher(rule.packageRoots);
3291
+ const excluders = compilePatterns(rule.exclude);
3292
+ const importersByDst = importersByDestination(ctx.edges);
3293
+ const out = [];
3294
+ for (const node of ctx.nodes) {
3295
+ if (node.kind !== "file") continue;
3296
+ if (node.role !== "barrel") continue;
3297
+ if (matchesAny(node.id, excluders)) continue;
3298
+ const barrelPkg = packageOf(node.id);
3299
+ if (barrelPkg === null) continue;
3300
+ const importers = importersByDst.get(node.id) ?? [];
3301
+ const externalCount = importers.reduce((n, src) => packageOf(src) !== barrelPkg ? n + 1 : n, 0);
3302
+ if (externalCount > 0) continue;
3303
+ out.push({
3304
+ ruleId: rule.id,
3305
+ severity: severityOf(rule),
3306
+ nodeId: node.id,
3307
+ message: barrelMessage(barrelPkg, importers.length)
3308
+ });
3309
+ }
3310
+ return out;
3311
+ }
3312
+ function barrelMessage(barrelPkg, importerCount) {
3313
+ return importerCount === 0 ? `barrel in package "${barrelPkg}" has no importers` : `barrel in package "${barrelPkg}" has ${importerCount} internal importer(s) but zero external`;
3314
+ }
3315
+
3316
+ // src/check/metric-rules.ts
3317
+ function nodeFilter(rule) {
3318
+ return {
3319
+ kind: rule.kind,
3320
+ excluders: compilePatterns(rule.exclude),
3321
+ excludedRoles: new Set(rule.excludeRoles ?? [])
3322
+ };
3323
+ }
3324
+ function passesFilter(node, filter) {
3325
+ if (filter.kind && node.kind !== filter.kind) return false;
3326
+ if (matchesAny(node.id, filter.excluders)) return false;
3327
+ if (node.role && filter.excludedRoles.has(node.role)) return false;
3328
+ return true;
3329
+ }
3330
+ function runMetricMaxRule(rule, ctx) {
3331
+ const filter = nodeFilter(rule);
3332
+ const out = [];
3333
+ for (const node of ctx.nodes) {
3334
+ if (!passesFilter(node, filter)) continue;
3335
+ const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
3336
+ if (value === void 0) continue;
3337
+ if (value <= rule.max) continue;
3338
+ out.push({
3339
+ ruleId: rule.id,
3340
+ severity: severityOf(rule),
3341
+ nodeId: node.id,
3342
+ metric: rule.metric,
3343
+ value,
3344
+ threshold: rule.max,
3345
+ message: `${rule.metric}=${formatNumber(value)} > ${formatNumber(rule.max)}`
3346
+ });
3347
+ }
3348
+ return out;
3349
+ }
3350
+ function runMetricMinRule(rule, ctx) {
3351
+ const filter = nodeFilter(rule);
3352
+ const out = [];
3353
+ for (const node of ctx.nodes) {
3354
+ if (!passesFilter(node, filter)) continue;
3355
+ const value = ctx.metricsByNode.get(node.id)?.get(rule.metric);
3356
+ if (value === void 0) continue;
3357
+ if (value >= rule.min) continue;
3358
+ out.push({
3359
+ ruleId: rule.id,
3360
+ severity: severityOf(rule),
3361
+ nodeId: node.id,
3362
+ metric: rule.metric,
3363
+ value,
3364
+ threshold: rule.min,
3365
+ message: `${rule.metric}=${formatNumber(value)} < ${formatNumber(rule.min)}`
3366
+ });
3367
+ }
3368
+ return out;
3369
+ }
3370
+ function runMetricProductMaxRule(rule, ctx) {
3371
+ const filter = nodeFilter(rule);
3372
+ const out = [];
3373
+ for (const node of ctx.nodes) {
3374
+ if (!passesFilter(node, filter)) continue;
3375
+ const inner = ctx.metricsByNode.get(node.id);
3376
+ if (!inner) continue;
3377
+ const components = collectComponents(rule.metrics, inner);
3378
+ if (!components) continue;
3379
+ const product = components.reduce((a, b) => a * b, 1);
3380
+ if (product <= rule.max) continue;
3381
+ out.push(productViolation(rule, node.id, components, product));
3382
+ }
3383
+ return out;
3384
+ }
3385
+ function productViolation(rule, nodeId, components, product) {
3386
+ const detail = rule.metrics.map((m, i) => `${m}=${formatNumber(components[i])}`).join(" * ");
3387
+ return {
3388
+ ruleId: rule.id,
3389
+ severity: severityOf(rule),
3390
+ nodeId,
3391
+ metric: rule.metrics.join(" * "),
3392
+ value: product,
3393
+ threshold: rule.max,
3394
+ message: `${detail} = ${formatNumber(product)} > ${formatNumber(rule.max)}`
3395
+ };
3396
+ }
3397
+ function collectComponents(metrics, values) {
3398
+ const out = [];
3399
+ for (const m of metrics) {
3400
+ const v = values.get(m);
3401
+ if (v === void 0) return null;
3402
+ out.push(v);
3403
+ }
3404
+ return out;
3405
+ }
3406
+
3407
+ // src/check/rules.ts
3408
+ function runRule(rule, ctx) {
3409
+ switch (rule.type) {
3410
+ case "metric-max":
3411
+ return runMetricMaxRule(rule, ctx);
3412
+ case "metric-min":
3413
+ return runMetricMinRule(rule, ctx);
3414
+ case "metric-product-max":
3415
+ return runMetricProductMaxRule(rule, ctx);
3416
+ case "forbid-import":
3417
+ return runForbidImportRule(rule, ctx);
3418
+ case "layered-deps":
3419
+ return runLayeredDepsRule(rule, ctx);
3420
+ case "no-internal-only-barrels":
3421
+ return runNoInternalOnlyBarrelsRule(rule, ctx);
3422
+ }
3423
+ }
3424
+
3425
+ // src/check/check.ts
3426
+ function runChecks(store, options) {
3427
+ const ctx = buildRuleContext(store, options.snapshotId);
3428
+ const violations = [];
3429
+ for (const rule of options.rules) {
3430
+ violations.push(...runRule(rule, ctx));
3431
+ }
3432
+ if (options.baselineSnapshotId) {
3433
+ const baselineKeys = collectBaselineKeys(store, options.baselineSnapshotId, options.rules);
3434
+ for (const v of violations) {
3435
+ if (baselineKeys.has(violationKey(v))) v.isCarryover = true;
3436
+ }
3437
+ }
3438
+ const counts = countByOriginAndSeverity(violations);
3439
+ return {
3440
+ snapshotId: options.snapshotId,
3441
+ baselineSnapshotId: options.baselineSnapshotId,
3442
+ rulesEvaluated: options.rules.length,
3443
+ nodesEvaluated: ctx.nodes.length,
3444
+ violations,
3445
+ ...counts,
3446
+ passed: counts.newErrors === 0
3447
+ };
3448
+ }
3449
+ function collectBaselineKeys(store, baselineSnapshotId, rules) {
3450
+ const ctx = buildRuleContext(store, baselineSnapshotId);
3451
+ const keys = /* @__PURE__ */ new Set();
3452
+ for (const rule of rules) {
3453
+ for (const v of runRule(rule, ctx)) keys.add(violationKey(v));
3454
+ }
3455
+ return keys;
3456
+ }
3457
+ function violationKey(v) {
3458
+ return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
3459
+ }
3460
+ function countByOriginAndSeverity(violations) {
3461
+ const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
3462
+ for (const v of violations) {
3463
+ const isError = v.severity === "error";
3464
+ if (v.isCarryover) {
3465
+ if (isError) counts.carryoverErrors++;
3466
+ else counts.carryoverWarnings++;
3467
+ } else {
3468
+ if (isError) counts.newErrors++;
3469
+ else counts.newWarnings++;
3470
+ }
3471
+ }
3472
+ return counts;
3473
+ }
3474
+
3475
+ // src/check/validate.ts
3476
+ function validateRules(input, options = {}) {
3477
+ if (!input || typeof input !== "object") {
3478
+ throw new Error("rules file must be an object with a `rules` array");
3479
+ }
3480
+ const obj = input;
3481
+ if (!Array.isArray(obj.rules)) {
3482
+ throw new Error("rules file must have a `rules` array");
3483
+ }
3484
+ const warn = options.onWarn ?? (() => {
3485
+ });
3486
+ return obj.rules.map((r, i) => validateRule(r, i, warn));
3487
+ }
3488
+ function healMetricName(name, ruleId, warn) {
3489
+ const target = metricAliasTarget(name);
3490
+ if (target) {
3491
+ warn(`${ruleId}: metric "${name}" is deprecated \u2014 using "${target}"`);
3492
+ return target;
3493
+ }
3494
+ return name;
3495
+ }
3496
+ function validateRule(raw, index, warn) {
3497
+ if (!raw || typeof raw !== "object") {
3498
+ throw new Error(`rule[${index}] must be an object`);
3499
+ }
3500
+ const r = raw;
3501
+ if (typeof r.id !== "string" || !r.id) {
3502
+ throw new Error(`rule[${index}] missing string id`);
3503
+ }
3504
+ if (typeof r.type !== "string") {
3505
+ throw new Error(`rule[${index}] (${r.id}) missing string type`);
3506
+ }
3507
+ switch (r.type) {
3508
+ case "metric-max":
3509
+ return assertMetricMax(r, warn);
3510
+ case "metric-min":
3511
+ return assertMetricMin(r, warn);
3512
+ case "metric-product-max":
3513
+ return assertMetricProductMax(r, warn);
3514
+ case "forbid-import":
3515
+ return assertForbidImport(r);
3516
+ case "layered-deps":
3517
+ return assertLayeredDeps(r);
3518
+ case "no-internal-only-barrels":
3519
+ return assertNoInternalOnlyBarrels(r);
3520
+ default:
3521
+ throw new Error(`rule[${index}] (${r.id}) unknown type "${r.type}"`);
3522
+ }
3523
+ }
3524
+ function assertNoInternalOnlyBarrels(r) {
3525
+ if (!Array.isArray(r.packageRoots) || r.packageRoots.length === 0) {
3526
+ throw new Error(
3527
+ `${r.id}: packageRoots must be a non-empty array of path-prefix strings`
3528
+ );
3529
+ }
3530
+ for (const p of r.packageRoots) {
3531
+ if (typeof p !== "string" || !p) {
3532
+ throw new Error(`${r.id}: each packageRoots entry must be a non-empty string`);
3533
+ }
3534
+ }
3535
+ return {
3536
+ type: "no-internal-only-barrels",
3537
+ id: r.id,
3538
+ packageRoots: r.packageRoots,
3539
+ severity: r.severity,
3540
+ exclude: parseStringArray(r.exclude)
3541
+ };
3542
+ }
3543
+ function assertLayeredDeps(r) {
3544
+ if (!Array.isArray(r.layers) || r.layers.length < 2) {
3545
+ throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
3546
+ }
3547
+ for (const layer of r.layers) {
3548
+ if (!Array.isArray(layer) || !layer.every((p) => typeof p === "string")) {
3549
+ throw new Error(`${r.id}: each layer must be a string[]`);
3550
+ }
3551
+ if (layer.length === 0) {
3552
+ throw new Error(`${r.id}: empty layers not allowed`);
3553
+ }
3554
+ }
3555
+ const seen = /* @__PURE__ */ new Set();
3556
+ for (const layer of r.layers) {
3557
+ for (const pkg of layer) {
3558
+ if (seen.has(pkg)) {
3559
+ throw new Error(`${r.id}: package "${pkg}" appears in more than one layer`);
3560
+ }
3561
+ seen.add(pkg);
3562
+ }
3563
+ }
3564
+ return {
3565
+ type: "layered-deps",
3566
+ id: r.id,
3567
+ layers: r.layers,
3568
+ severity: r.severity
3569
+ };
3570
+ }
3571
+ function assertMetricProductMax(r, warn) {
3572
+ if (!Array.isArray(r.metrics) || r.metrics.length < 2) {
3573
+ throw new Error(`${r.id}: metrics must be an array of 2+ metric names`);
3574
+ }
3575
+ if (!r.metrics.every((m) => typeof m === "string")) {
3576
+ throw new Error(`${r.id}: metrics must be strings`);
3577
+ }
3578
+ if (typeof r.max !== "number") {
3579
+ throw new Error(`${r.id}: max must be a number`);
3580
+ }
3581
+ const ruleId = r.id;
3582
+ return {
3583
+ type: "metric-product-max",
3584
+ id: ruleId,
3585
+ metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
3586
+ max: r.max,
3587
+ kind: r.kind,
3588
+ severity: r.severity,
3589
+ exclude: parseStringArray(r.exclude),
3590
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
3591
+ };
3592
+ }
3593
+ function assertMetricMax(r, warn) {
3594
+ if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
3595
+ if (typeof r.max !== "number") throw new Error(`${r.id}: max must be a number`);
3596
+ const ruleId = r.id;
3597
+ return {
3598
+ type: "metric-max",
3599
+ id: ruleId,
3600
+ metric: healMetricName(r.metric, ruleId, warn),
3601
+ max: r.max,
3602
+ kind: r.kind,
3603
+ severity: r.severity,
3604
+ exclude: parseStringArray(r.exclude),
3605
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
3606
+ };
3607
+ }
3608
+ function assertMetricMin(r, warn) {
3609
+ if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
3610
+ if (typeof r.min !== "number") throw new Error(`${r.id}: min must be a number`);
3611
+ const ruleId = r.id;
3612
+ return {
3613
+ type: "metric-min",
3614
+ id: ruleId,
3615
+ metric: healMetricName(r.metric, ruleId, warn),
3616
+ min: r.min,
3617
+ kind: r.kind,
3618
+ severity: r.severity,
3619
+ exclude: parseStringArray(r.exclude),
3620
+ excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
3621
+ };
3622
+ }
3623
+ function assertForbidImport(r) {
3624
+ if (typeof r.from !== "string") throw new Error(`${r.id}: from must be a string`);
3625
+ if (typeof r.to !== "string") throw new Error(`${r.id}: to must be a string`);
3626
+ return {
3627
+ type: "forbid-import",
3628
+ id: r.id,
3629
+ from: r.from,
3630
+ to: r.to,
3631
+ severity: r.severity
3632
+ };
3633
+ }
3634
+ function parseStringArray(value) {
3635
+ return Array.isArray(value) ? value : void 0;
3636
+ }
3637
+ var ROLE_VALUES = /* @__PURE__ */ new Set([
3638
+ "test",
3639
+ "fixture",
3640
+ "barrel",
3641
+ "types",
3642
+ "config",
3643
+ "entry",
3644
+ "source"
3645
+ ]);
3646
+ function parseRoleArray(ruleId, value, warn) {
3647
+ if (value === void 0) return void 0;
3648
+ if (!Array.isArray(value)) {
3649
+ throw new Error(`${ruleId}: excludeRoles must be an array`);
3650
+ }
3651
+ return value.map((entry) => healRole(entry, ruleId, warn));
3652
+ }
3653
+ function healRole(entry, ruleId, warn) {
3654
+ if (typeof entry === "string" && ROLE_VALUES.has(entry)) {
3655
+ return entry;
3656
+ }
3657
+ if (typeof entry === "string") {
3658
+ const target = roleAliasTarget(entry);
3659
+ if (target) {
3660
+ warn(`${ruleId}: role "${entry}" is deprecated \u2014 using "${target}"`);
3661
+ return target;
3662
+ }
3663
+ }
3664
+ throw new Error(
3665
+ `${ruleId}: unknown role "${entry}" \u2014 valid: ${[...ROLE_VALUES].join(", ")}`
3666
+ );
3667
+ }
3668
+
3669
+ // src/check/run.ts
3670
+ import { readFile as readFile2 } from "fs/promises";
3671
+ function resolveSnapshot(store, spec) {
3672
+ if (typeof spec === "number" || /^\d+$/.test(spec)) {
3673
+ const snap2 = store.getSnapshot(Number(spec));
3674
+ if (!snap2) throw new Error(`no snapshot with id ${spec}`);
3675
+ return snap2;
3676
+ }
3677
+ const snap = store.getLatestSnapshotByRef(spec);
3678
+ if (!snap) throw new Error(`no snapshot found for ref "${spec}"`);
3679
+ return snap;
3680
+ }
3681
+ function checkSnapshot(store, options) {
3682
+ const snapshot = resolveSnapshot(store, options.snapshot);
3683
+ const baselineSnapshot = options.baseline !== void 0 ? resolveSnapshot(store, options.baseline) : void 0;
3684
+ const result = runChecks(store, {
3685
+ snapshotId: snapshot.id,
3686
+ rules: options.rules,
3687
+ baselineSnapshotId: baselineSnapshot?.id
3688
+ });
3689
+ return { snapshot, baselineSnapshot, result };
3690
+ }
3691
+ async function loadCheckRules(path10, options = {}) {
3692
+ let raw;
3693
+ try {
3694
+ raw = await readFile2(path10, "utf8");
3695
+ } catch (err) {
3696
+ throw new Error(`Cannot read rules file at ${path10}: ${errorMessage(err)}`);
3697
+ }
3698
+ let parsed;
3699
+ try {
3700
+ parsed = JSON.parse(raw);
3701
+ } catch (err) {
3702
+ throw new Error(`Invalid JSON in ${path10}: ${errorMessage(err)}`);
3703
+ }
3704
+ return validateRules(parsed, options);
3705
+ }
3706
+ function errorMessage(err) {
3707
+ return err instanceof Error ? err.message : String(err);
3708
+ }
3709
+
3710
+ // src/diff/diff.ts
3711
+ function aliasMap(aliases) {
3712
+ const m = /* @__PURE__ */ new Map();
3713
+ for (const a of aliases) m.set(a.oldId, a);
3714
+ return m;
3715
+ }
3716
+ function resolveId(id, aliases) {
3717
+ return aliases.get(id)?.newId ?? id;
3718
+ }
3719
+ function edgeKey2(srcId, dstId, kind) {
3720
+ return JSON.stringify([srcId, dstId, canonicalEdgeKind(kind)]);
3721
+ }
3722
+ function metricKey(nodeId, name) {
3723
+ return JSON.stringify([nodeId, canonicalMetricName(name)]);
3724
+ }
3725
+ function indexEdges(edges) {
3726
+ const m = /* @__PURE__ */ new Map();
3727
+ for (const e of edges) m.set(edgeKey2(e.srcId, e.dstId, e.kind), e);
3728
+ return m;
3729
+ }
3730
+ function diffNodes(fromNodes, toNodes, aliases) {
3731
+ const toIndex = new Map(toNodes.map((n) => [n.id, n]));
3732
+ const matchedToIds = /* @__PURE__ */ new Set();
3733
+ const removed = [];
3734
+ const renamed = [];
3735
+ for (const fromNode of fromNodes) {
3736
+ const newId = resolveId(fromNode.id, aliases);
3737
+ const toNode = toIndex.get(newId);
3738
+ if (!toNode) {
3739
+ removed.push(fromNode);
3740
+ continue;
3741
+ }
3742
+ matchedToIds.add(newId);
3743
+ if (newId !== fromNode.id) {
3744
+ renamed.push({ oldId: fromNode.id, newId, reason: aliases.get(fromNode.id).reason, node: toNode });
3745
+ }
3746
+ }
3747
+ const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
3748
+ return { added, removed, renamed, commonNewIds: matchedToIds };
3749
+ }
3750
+ function diffEdges(fromEdges, toEdges, aliases) {
3751
+ const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
3752
+ const fromKeys = new Set(fromEdges.map(remappedKey));
3753
+ const toIndex = indexEdges(toEdges);
3754
+ const added = [];
3755
+ for (const [k, e] of toIndex) {
3756
+ if (!fromKeys.has(k)) added.push(e);
3757
+ }
3758
+ const removed = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
3759
+ return { added, removed };
3760
+ }
3761
+ function indexFromMetrics(metrics, aliases) {
3762
+ const fromIndex = /* @__PURE__ */ new Map();
3763
+ for (const m of metrics) {
3764
+ const newId = resolveId(m.nodeId, aliases);
3765
+ fromIndex.set(metricKey(newId, m.name), { metric: m, nodeId: newId });
3766
+ }
3767
+ return fromIndex;
3768
+ }
3769
+ function deltaOf(before, after) {
3770
+ if (before === null || after === null) return null;
3771
+ return after - before;
3772
+ }
3773
+ function metricDelta(nodeId, name, before, after) {
3774
+ return { nodeId, name: canonicalMetricName(name), before, after, delta: deltaOf(before, after) };
3775
+ }
3776
+ function diffMetrics(fromMetrics, toMetrics, aliases, commonNewIds) {
3777
+ const fromIndex = indexFromMetrics(fromMetrics, aliases);
3778
+ const toIndex = new Map(toMetrics.map((m) => [metricKey(m.nodeId, m.name), m]));
3779
+ const out = [];
3780
+ const seen = /* @__PURE__ */ new Set();
3781
+ for (const [key, { metric: before, nodeId }] of fromIndex) {
3782
+ if (!commonNewIds.has(nodeId)) continue;
3783
+ seen.add(key);
3784
+ const afterVal = toIndex.get(key)?.value ?? null;
3785
+ if (before.value !== afterVal) out.push(metricDelta(nodeId, before.name, before.value, afterVal));
3786
+ }
3787
+ for (const [key, after] of toIndex) {
3788
+ if (seen.has(key) || !commonNewIds.has(after.nodeId)) continue;
3789
+ out.push(metricDelta(after.nodeId, after.name, null, after.value));
3790
+ }
3791
+ return out;
3792
+ }
3793
+ function diffSnapshots(store, options) {
3794
+ const { fromSnapshotId, toSnapshotId } = options;
3795
+ const aliases = aliasMap(store.listAliases(toSnapshotId));
3796
+ const nodeDiff = diffNodes(store.listNodes(fromSnapshotId), store.listNodes(toSnapshotId), aliases);
3797
+ const edgeDiff = diffEdges(store.listEdges(fromSnapshotId), store.listEdges(toSnapshotId), aliases);
3798
+ const metricDeltas = diffMetrics(
3799
+ store.listMetrics(fromSnapshotId),
3800
+ store.listMetrics(toSnapshotId),
3801
+ aliases,
3802
+ nodeDiff.commonNewIds
3803
+ );
3804
+ return {
3805
+ summary: summarize(options, nodeDiff, edgeDiff, metricDeltas),
3806
+ addedNodes: nodeDiff.added,
3807
+ removedNodes: nodeDiff.removed,
3808
+ renamedNodes: nodeDiff.renamed,
3809
+ addedEdges: edgeDiff.added,
3810
+ removedEdges: edgeDiff.removed,
3811
+ metricDeltas
3812
+ };
3813
+ }
3814
+ function summarize({ fromSnapshotId, toSnapshotId }, nodeDiff, edgeDiff, metricDeltas) {
3815
+ return {
3816
+ fromSnapshotId,
3817
+ toSnapshotId,
3818
+ addedNodes: nodeDiff.added.length,
3819
+ removedNodes: nodeDiff.removed.length,
3820
+ renamedNodes: nodeDiff.renamed.length,
3821
+ unchangedNodes: nodeDiff.commonNewIds.size - nodeDiff.renamed.length,
3822
+ addedEdges: edgeDiff.added.length,
3823
+ removedEdges: edgeDiff.removed.length,
3824
+ metricChanges: metricDeltas.length
3825
+ };
3826
+ }
3827
+
3828
+ // src/diff/check-diff.ts
3829
+ function diffCheckResults(store, options) {
3830
+ const from = runChecks(store, { snapshotId: options.fromSnapshotId, rules: options.rules });
3831
+ const to = runChecks(store, { snapshotId: options.toSnapshotId, rules: options.rules });
3832
+ return {
3833
+ fromSnapshotId: options.fromSnapshotId,
3834
+ toSnapshotId: options.toSnapshotId,
3835
+ rulesEvaluated: options.rules.length,
3836
+ ...bucketViolations(indexByKey(from.violations), indexByKey(to.violations))
3837
+ };
3838
+ }
3839
+ function bucketViolations(fromByKey, toByKey) {
3840
+ const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
3841
+ for (const [key, toV] of toByKey) {
3842
+ const fromV = fromByKey.get(key);
3843
+ if (!fromV) {
3844
+ buckets.newViolations.push(toV);
3845
+ continue;
3846
+ }
3847
+ const delta = computeDelta(fromV, toV);
3848
+ const entry = { from: fromV, to: toV, delta };
3849
+ buckets.unchanged.push(entry);
3850
+ if (delta !== null && delta > 0) buckets.worsened.push(entry);
3851
+ else if (delta !== null && delta < 0) buckets.improved.push(entry);
3852
+ }
3853
+ for (const [key, fromV] of fromByKey) {
3854
+ if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
3855
+ }
3856
+ return buckets;
3857
+ }
3858
+ function indexByKey(violations) {
3859
+ const out = /* @__PURE__ */ new Map();
3860
+ for (const v of violations) out.set(violationKey(v), v);
3861
+ return out;
3862
+ }
3863
+ function computeDelta(from, to) {
3864
+ if (from.value === void 0 || to.value === void 0) return null;
3865
+ return to.value - from.value;
3866
+ }
3867
+
3868
+ // src/embeddings/corpus.ts
3869
+ import { contentHashOf } from "@titan-design/store-sqlite";
3870
+ function buildEmbedText(signature, purpose) {
3871
+ return purpose ? `${signature} -- ${purpose}` : signature;
3872
+ }
3873
+ function hashEmbedText(text) {
3874
+ return contentHashOf(text);
3875
+ }
3876
+ var TEST_OR_FIXTURE_PATH = /\.test\.|\.spec\.|__tests__|\/fixtures\//;
3877
+ var EXCLUDED_FILE_ROLES = /* @__PURE__ */ new Set(["test", "generated"]);
3878
+ function listEmbeddableSymbols(store, snapshotId) {
3879
+ const nodes = store.listNodes(snapshotId, { includeSymbols: true });
3880
+ const fileRoles = /* @__PURE__ */ new Map();
3881
+ for (const n of nodes) if (n.kind === "file") fileRoles.set(n.id, n.role ?? null);
3882
+ const out = [];
3883
+ for (const n of nodes) {
3884
+ const symbol = toEmbeddable(n, fileRoles);
3885
+ if (symbol) out.push(symbol);
3886
+ }
3887
+ return out;
3888
+ }
3889
+ function toEmbeddable(node, fileRoles) {
3890
+ if (node.kind !== "symbol") return null;
3891
+ const attrs = node.attrs ?? {};
3892
+ if (attrs.exported !== true || !attrs.signature) return null;
3893
+ const file = node.parentId ?? node.id.split("#")[0] ?? node.id;
3894
+ if (TEST_OR_FIXTURE_PATH.test(file)) return null;
3895
+ const role = fileRoles.get(file);
3896
+ if (role && EXCLUDED_FILE_ROLES.has(role)) return null;
3897
+ const text = buildEmbedText(attrs.signature, attrs.purpose);
3898
+ return {
3899
+ id: node.id,
3900
+ name: node.name,
3901
+ file,
3902
+ signature: attrs.signature,
3903
+ purpose: attrs.purpose,
3904
+ text,
3905
+ textHash: hashEmbedText(text)
3906
+ };
3907
+ }
3908
+
3909
+ // src/embeddings/cache.ts
3910
+ import { fromFloat32Buffer, normalize, toFloat32Buffer } from "@titan-design/embed";
3911
+ import { CacheBlobTable } from "@titan-design/store-sqlite";
3912
+ var SYMBOL_EMBEDDING_NAMESPACE = "code-graph/symbol-embedding";
3913
+ var EMBED_BATCH_SIZE = 64;
3914
+ function symbolVectorCache(store) {
3915
+ return new CacheBlobTable(store.db, { name: KIT.cacheBlob });
3916
+ }
3917
+ function loadVectorsByHash(store, model, hashes) {
3918
+ const cache = symbolVectorCache(store);
3919
+ const found = /* @__PURE__ */ new Map();
3920
+ for (const contentHash of new Set(hashes)) {
3921
+ const hit = cache.get({ namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash });
3922
+ if (hit) found.set(contentHash, fromFloat32Buffer(hit.value));
3923
+ }
3924
+ return found;
3925
+ }
3926
+ function storeVectors(store, model, rows) {
3927
+ const cache = symbolVectorCache(store);
3928
+ store.db.transaction(() => {
3929
+ for (const r of rows) {
3930
+ const key = { namespace: SYMBOL_EMBEDDING_NAMESPACE, model, contentHash: r.textHash };
3931
+ cache.put(key, toFloat32Buffer(r.vector), { dims: r.vector.length });
3932
+ }
3933
+ })();
3934
+ }
3935
+ async function embedTextsCached(store, embedder, texts) {
3936
+ const hashes = texts.map((t) => hashEmbedText(t));
3937
+ const byHash = loadVectorsByHash(store, embedder.model, hashes);
3938
+ const missing = /* @__PURE__ */ new Map();
3939
+ texts.forEach((text, i) => {
3940
+ if (!byHash.has(hashes[i])) missing.set(hashes[i], text);
3941
+ });
3942
+ const pending = [...missing.entries()];
3943
+ for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
3944
+ const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
3945
+ storeVectors(store, embedder.model, rows);
3946
+ for (const r of rows) byHash.set(r.textHash, r.vector);
3947
+ }
3948
+ return { byHash, newHashes: new Set(missing.keys()) };
3949
+ }
3950
+ async function embedBatch(embedder, batch) {
3951
+ const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
3952
+ if (vectors.length !== batch.length) {
3953
+ throw new Error(`Embedder returned ${vectors.length} vectors for ${batch.length} texts`);
3954
+ }
3955
+ return batch.map(([textHash], j) => ({ textHash, vector: normalize(vectors[j]) }));
3956
+ }
3957
+
3958
+ // src/embeddings/embeddings.ts
3959
+ import { BruteForceVectorIndex, vectorRetriever } from "@titan-design/retrieval";
3960
+ async function embedSnapshot(store, snapshotId, embedder) {
3961
+ const symbols = listEmbeddableSymbols(store, snapshotId);
3962
+ const { newHashes } = await embedTextsCached(store, embedder, symbols.map((s) => s.text));
3963
+ return {
3964
+ model: embedder.model,
3965
+ symbols: symbols.length,
3966
+ withPurpose: symbols.filter((s) => s.purpose).length,
3967
+ embedded: symbols.length,
3968
+ newlyEmbedded: newHashes.size,
3969
+ reused: symbols.filter((s) => !newHashes.has(s.textHash)).length
3970
+ };
3971
+ }
3972
+ async function tryEmbedSnapshot(store, snapshotId, embedder) {
3973
+ try {
3974
+ return { ok: true, result: await embedSnapshot(store, snapshotId, embedder) };
3975
+ } catch (err) {
3976
+ return { ok: false, model: embedder.model, error: err instanceof Error ? err.message : String(err) };
3977
+ }
3978
+ }
3979
+ async function findSimilarCapability(store, snapshotId, query, embedder, opts = {}) {
3980
+ const symbols = listEmbeddableSymbols(store, snapshotId);
3981
+ const index = buildSymbolIndex(store, embedder.model, symbols);
3982
+ if (index.size === 0) {
3983
+ throw new Error(
3984
+ `No capability embeddings stored for snapshot ${snapshotId} (model ${embedder.model}); run embedSnapshot first.`
3985
+ );
3986
+ }
3987
+ const retriever = vectorRetriever(embedder, index);
3988
+ const hits = await retriever.retrieve(query, { limit: opts.limit ?? 10 });
3989
+ const byId = new Map(symbols.map((s) => [s.id, s]));
3990
+ return {
3991
+ query,
3992
+ model: embedder.model,
3993
+ coverage: { symbols: symbols.length, embedded: index.size, withPurpose: symbols.filter((s) => s.purpose).length },
3994
+ candidates: hits.map((h) => toCandidate(byId.get(h.id), h.score ?? 0))
3995
+ };
3996
+ }
3997
+ function buildSymbolIndex(store, model, symbols) {
3998
+ const vectors = loadVectorsByHash(store, model, symbols.map((s) => s.textHash));
3999
+ const index = new BruteForceVectorIndex();
4000
+ for (const s of symbols) {
4001
+ const vector = vectors.get(s.textHash);
4002
+ if (vector) index.add(s.id, vector);
4003
+ }
4004
+ return index;
4005
+ }
4006
+ function toCandidate(s, score) {
4007
+ return { id: s.id, name: s.name, file: s.file, signature: s.signature, purpose: s.purpose, score };
4008
+ }
2357
4009
  export {
2358
4010
  ALL_ROLES,
4011
+ COVERAGE_METRIC_NAME,
2359
4012
  CodeGraphStore,
4013
+ DEAD_CODE_METRIC_NAMES,
2360
4014
  DOMAIN_DDL,
4015
+ GROWTH_RISK_METRIC_NAMES,
2361
4016
  INDEX_VERSION,
2362
4017
  KIT,
2363
4018
  LanguageExtractor,
2364
4019
  MIGRATIONS,
2365
4020
  PythonGraphExtractor,
4021
+ SCHEMA_VERSION,
2366
4022
  SOURCE_METRIC_NAMES,
4023
+ SYMBOL_EMBEDDING_NAMESPACE,
2367
4024
  SYMBOL_ID_SEP,
2368
4025
  TsMorphGraphExtractor,
2369
4026
  annotateRoles,
4027
+ attributeCoverage,
2370
4028
  buildAliases,
4029
+ buildEmbedText,
2371
4030
  buildFileModuleNodes,
2372
4031
  buildIndexerMetrics,
2373
4032
  canonicalEdgeKind,
2374
4033
  canonicalMetricName,
2375
4034
  canonicalRole,
4035
+ checkSnapshot,
2376
4036
  classifyRole,
2377
4037
  collectDeclaredNames,
2378
4038
  collectDeclaredSpans,
4039
+ computeDeadCodeMetrics,
4040
+ computeGrowthRiskMetrics,
2379
4041
  computeMetrics,
4042
+ computePageRank,
4043
+ computeRelevance,
2380
4044
  computeRoleHints,
2381
4045
  computeSourceMetrics,
4046
+ computeSymbolConsumers,
4047
+ computeSymbolCoupling,
4048
+ computeTestCoverageOwnership,
2382
4049
  detectGitHead,
2383
4050
  detectGitToplevel,
2384
4051
  detectRenames,
4052
+ diffCheckResults,
4053
+ diffSnapshots,
2385
4054
  edgeWeight,
4055
+ embedSnapshot,
4056
+ embedTextsCached,
2386
4057
  externalId,
2387
4058
  fileId,
2388
- getLanguageFromPath,
4059
+ findSimilarCapability,
4060
+ getEdgeWeight,
4061
+ getLanguageFromPath3 as getLanguageFromPath,
2389
4062
  getSupportedLanguages,
4063
+ groupTestsBySource,
2390
4064
  hashContent,
4065
+ hashEmbedText,
2391
4066
  indexPaths,
2392
4067
  isGeneratedByHeuristic,
2393
4068
  isGeneratedFile,
2394
4069
  isInsideGitRepo,
4070
+ linkTestsToSources,
2395
4071
  listEdges,
4072
+ listEmbeddableSymbols,
2396
4073
  listMetrics,
2397
4074
  listNodes,
4075
+ loadCheckRules,
2398
4076
  loadGeneratedPatterns,
2399
4077
  moduleId,
2400
4078
  openCodeGraph,
2401
4079
  packageId,
2402
4080
  parentModuleId,
2403
- parseFile,
4081
+ parseFile2 as parseFile,
2404
4082
  parseSymbolId,
2405
4083
  pruneDanglingReferences,
2406
4084
  readSourceFiles,
2407
4085
  resolveBarrelEdges,
2408
- shouldIncludeFile,
4086
+ resolveSnapshot,
4087
+ runChecks,
4088
+ shouldIncludeFile2 as shouldIncludeFile,
4089
+ snapshotPageRank,
4090
+ snapshotReferenceEdges,
4091
+ snapshotRelevance,
4092
+ snapshotSymbolConsumers,
4093
+ snapshotSymbolCoupling,
2409
4094
  structuralSignature,
2410
4095
  symbolId,
4096
+ testCoverageCountMetrics,
4097
+ tryEmbedSnapshot,
4098
+ validateRules,
2411
4099
  walkSourceFiles
2412
4100
  };
2413
4101
  //# sourceMappingURL=index.js.map