@titan-design/code-graph 0.10.0 → 0.13.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,16 +1,78 @@
1
1
  import {
2
2
  aggregateChurnWindows,
3
3
  authorLinesByPath,
4
- computeChangeCoupling,
5
4
  computeOwnership,
6
- couplingFor,
7
5
  detectGitToplevel,
8
6
  entriesWithin,
7
+ listTreeBlobs,
9
8
  loadChurnEntries,
10
9
  loadFileFirstSeen,
10
+ readBlobs,
11
+ resolveCommit,
11
12
  runGit,
12
13
  summarizeOwnership
13
- } from "./chunk-PFI5XMUG.js";
14
+ } from "./chunk-GX563F6R.js";
15
+ import {
16
+ DEFAULT_HEALTH_WEIGHTS,
17
+ DEFAULT_MAX_PACKAGE_SIZE,
18
+ EXTERNAL_BUCKET,
19
+ SYMBOL_ID_SEP,
20
+ UNIMPORTED_ROLES,
21
+ aggregateEdges,
22
+ bucketFilesByPackage,
23
+ bucketViolations,
24
+ buildBlastRadius,
25
+ buildCentralFiles,
26
+ buildHotExports,
27
+ buildNodeMetrics,
28
+ buildReportContext,
29
+ busFactorOf,
30
+ classifyCoupling,
31
+ collectNodeMetrics,
32
+ collectSymbolUtil,
33
+ compilePatterns,
34
+ computeArch,
35
+ computeHealth,
36
+ computePageRank,
37
+ computePartitionQuality,
38
+ computeRecencyWindows,
39
+ computeReportDrift,
40
+ computeSymbolConsumers,
41
+ computeSymbolCoupling,
42
+ filteredFileIds,
43
+ getEdgeWeight,
44
+ groupTestsBySource,
45
+ hotspotScoreOf,
46
+ invertBuckets,
47
+ keepNode,
48
+ linkTestsToSources,
49
+ lookupMetric,
50
+ matchesAny,
51
+ packagesReferencedByEdges,
52
+ pairKey,
53
+ parseSymbolId,
54
+ patternToRegex,
55
+ publicApiFiles,
56
+ rebasedViolationKey,
57
+ referencedNodes,
58
+ round3,
59
+ symbolId,
60
+ testCoverageCountMetrics,
61
+ toSortedEdges,
62
+ topBusFactorRisks,
63
+ topCentralFiles,
64
+ topDeadModules,
65
+ topGrowthRisks,
66
+ topHotspots,
67
+ topTestCoverageRisks,
68
+ topUntestedRisks,
69
+ topUnusedExports,
70
+ violationKey,
71
+ windowSuffix
72
+ } from "./chunk-L5KU543A.js";
73
+ import {
74
+ computeChangeCoupling
75
+ } from "./chunk-HIVVDGWE.js";
14
76
 
15
77
  // src/store.ts
16
78
  import { openDatabase, quoteIdent, runMigrations } from "@titan-design/store-sqlite";
@@ -231,8 +293,8 @@ function readMetricAggregates(stmts, snapshotId, name) {
231
293
 
232
294
  // src/store.ts
233
295
  var NODE_COLS = "id, kind, name, parent_id, language, role, attrs";
234
- function isStructuralLayer(edge) {
235
- return edge.kind !== "references" && edge.kind !== "calls";
296
+ function isStructuralLayer(edge2) {
297
+ return edge2.kind !== "references" && edge2.kind !== "calls";
236
298
  }
237
299
  var CodeGraphStore = class {
238
300
  constructor(db) {
@@ -252,6 +314,15 @@ var CodeGraphStore = class {
252
314
  });
253
315
  return Number(result.lastInsertRowid);
254
316
  }
317
+ /**
318
+ * Run `write` in one transaction so a reader on another connection sees a new
319
+ * snapshot only once every row written for it is committed, never a snapshot
320
+ * row whose nodes or edges are still being inserted. The per-table inserts
321
+ * nest as savepoints inside it.
322
+ */
323
+ atomically(write) {
324
+ return this.db.transaction(write)();
325
+ }
255
326
  insertNodes(snapshotId, nodes) {
256
327
  const stmt = this.statements.insertNode;
257
328
  this.db.transaction(() => {
@@ -491,30 +562,80 @@ function runPrune(store, options = {}) {
491
562
  }
492
563
 
493
564
  // src/indexer.ts
494
- import { realpathSync } from "fs";
495
- import * as path8 from "path";
565
+ import { realpathSync as realpathSync2 } from "fs";
566
+ import * as path10 from "path";
496
567
  import { parseFile } from "@titan-design/code-parser";
497
568
 
498
569
  // src/extractors/ts-morph-extractor.ts
499
570
  import {
500
- Project,
571
+ Project as Project2,
501
572
  ScriptTarget,
502
573
  ModuleKind,
503
574
  ModuleResolutionKind
504
575
  } from "ts-morph";
505
576
 
577
+ // src/index-source.ts
578
+ import { existsSync, readFileSync } from "fs";
579
+ import { Project } from "ts-morph";
580
+
581
+ // src/file-walk.ts
582
+ import * as fs from "fs/promises";
583
+ import * as path from "path";
584
+ import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
585
+ async function walkSourceFiles(rootDirs, languages) {
586
+ const seen = /* @__PURE__ */ new Set();
587
+ const out = [];
588
+ for (const rootDir of rootDirs) {
589
+ await collectFiles(rootDir, rootDir, languages, seen, out);
590
+ }
591
+ return out;
592
+ }
593
+ async function collectFiles(dir, rootDir, languages, seen, out) {
594
+ const entries = await fs.readdir(dir, { withFileTypes: true });
595
+ for (const entry of entries) {
596
+ const full = path.join(dir, entry.name);
597
+ if (entry.isDirectory()) {
598
+ if (isExcludedDir(entry.name)) continue;
599
+ await collectFiles(full, rootDir, languages, seen, out);
600
+ } else if (entry.isFile() && !seen.has(full)) {
601
+ const relative5 = path.relative(rootDir, full);
602
+ if (shouldIncludeFile(relative5, [...languages])) {
603
+ seen.add(full);
604
+ out.push(full);
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ // src/index-source.ts
611
+ var realFileSystem;
612
+ function realFileSystemHost() {
613
+ realFileSystem ??= new Project().getFileSystem();
614
+ return realFileSystem;
615
+ }
616
+ function workingTreeSource() {
617
+ return {
618
+ listFiles: async (rootDirs, languages) => (await walkSourceFiles(rootDirs, languages)).sort(),
619
+ readFile: (abs) => readFileSync(abs, "utf-8"),
620
+ fileExists: existsSync,
621
+ get fileSystem() {
622
+ return realFileSystemHost();
623
+ }
624
+ };
625
+ }
626
+
506
627
  // src/extractors/file-nodes.ts
507
- import * as path2 from "path";
628
+ import * as path3 from "path";
508
629
  import { getLanguageFromPath } from "@titan-design/code-parser";
509
630
 
510
631
  // src/extractors/ids.ts
511
- import * as path from "path";
632
+ import * as path2 from "path";
512
633
  var MODULE_EXT_RE = /\.(?:tsx|ts|jsx|js|mts|cts|mjs|cjs|py)$/;
513
634
  function toPosix(p) {
514
- return p.split(path.sep).join("/");
635
+ return p.split(path2.sep).join("/");
515
636
  }
516
637
  function repoRelative(repoRoot, absPath) {
517
- const rel = path.relative(repoRoot, absPath);
638
+ const rel = path2.relative(repoRoot, absPath);
518
639
  return toPosix(rel);
519
640
  }
520
641
  function fileId(repoRoot, absPath) {
@@ -532,15 +653,6 @@ function parentModuleId(id) {
532
653
  function packageId(name) {
533
654
  return name;
534
655
  }
535
- var SYMBOL_ID_SEP = "#";
536
- function symbolId(fileId2, qualifiedName) {
537
- return `${fileId2}${SYMBOL_ID_SEP}${qualifiedName}`;
538
- }
539
- function parseSymbolId(id) {
540
- const idx = id.indexOf(SYMBOL_ID_SEP);
541
- if (idx < 0) return null;
542
- return { fileId: id.slice(0, idx), name: id.slice(idx + 1) };
543
- }
544
656
  function externalId(specifier) {
545
657
  if (specifier.startsWith("node:")) {
546
658
  return specifier;
@@ -563,8 +675,8 @@ function buildFileModuleNodes(repoRoot, absPath) {
563
675
  const parentId = parentModuleId(mId) ?? void 0;
564
676
  const language = getLanguageFromPath(absPath) ?? "typescript";
565
677
  return [
566
- { id: fId, kind: "file", name: path2.basename(fId), parentId: mId, language },
567
- { id: mId, kind: "module", name: path2.basename(mId), parentId, language }
678
+ { id: fId, kind: "file", name: path3.basename(fId), parentId: mId, language },
679
+ { id: mId, kind: "module", name: path3.basename(mId), parentId, language }
568
680
  ];
569
681
  }
570
682
 
@@ -641,20 +753,29 @@ function isPropertyNamePosition(id) {
641
753
  }
642
754
 
643
755
  // src/scope-path.ts
644
- var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
756
+ var TS_CLASS_TYPES = /* @__PURE__ */ new Set([
645
757
  "class_declaration",
646
758
  "abstract_class_declaration",
647
- "class",
759
+ "class"
760
+ ]);
761
+ var TS_FUNCTION_DECL_TYPES = /* @__PURE__ */ new Set([
648
762
  "function_declaration",
649
763
  "generator_function_declaration",
650
- "method_definition",
764
+ "method_definition"
765
+ ]);
766
+ var TS_BOUND_FUNCTION_TYPES = /* @__PURE__ */ new Set([
767
+ "arrow_function",
768
+ "function_expression",
769
+ "generator_function"
770
+ ]);
771
+ var TS_NAMED_SCOPES = /* @__PURE__ */ new Set([
772
+ ...TS_CLASS_TYPES,
773
+ ...TS_FUNCTION_DECL_TYPES,
651
774
  "internal_module"
652
775
  ]);
653
776
  var TS_BOUND_SCOPES = /* @__PURE__ */ new Set([
654
777
  "class",
655
- "arrow_function",
656
- "function_expression",
657
- "generator_function",
778
+ ...TS_BOUND_FUNCTION_TYPES,
658
779
  "object"
659
780
  ]);
660
781
  var PY_NAMED_SCOPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
@@ -701,9 +822,9 @@ function walkScopes(root, python, visit) {
701
822
 
702
823
  // src/declared-names.ts
703
824
  var TS_DECL_TYPES = /* @__PURE__ */ new Set([
704
- "function_declaration",
705
- "method_definition",
706
- "class_declaration"
825
+ ...TS_FUNCTION_DECL_TYPES,
826
+ "class_declaration",
827
+ "abstract_class_declaration"
707
828
  ]);
708
829
  var PY_DECL_TYPES = /* @__PURE__ */ new Set(["function_definition", "class_definition"]);
709
830
  function collectDeclarations(file) {
@@ -738,7 +859,7 @@ function declaredNodeAt(node, declTypes) {
738
859
  const name = node.childForFieldName("name")?.text;
739
860
  return name ? { name, node } : null;
740
861
  }
741
- if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
862
+ if (TS_BOUND_FUNCTION_TYPES.has(node.type) && node.parent?.type === "variable_declarator") {
742
863
  const name = node.parent.childForFieldName("name")?.text;
743
864
  return name ? { name, node } : null;
744
865
  }
@@ -821,8 +942,59 @@ function hasReceiver(fn) {
821
942
 
822
943
  // src/extractors/symbol-signature.ts
823
944
  import {
824
- Node as Node2
945
+ Node as Node3
825
946
  } from "ts-morph";
947
+
948
+ // src/extractors/ts-scope.ts
949
+ import { Node as Node2 } from "ts-morph";
950
+ function bindingName2(node) {
951
+ const parent = node.getParent();
952
+ if (Node2.isVariableDeclaration(parent) || Node2.isPropertyDeclaration(parent)) return parent.getName();
953
+ if (Node2.isPropertyAssignment(parent)) return parent.getNameNode().getText();
954
+ if (Node2.isExportAssignment(parent)) return "default";
955
+ return null;
956
+ }
957
+ function declaredName(node) {
958
+ const name = node.getName();
959
+ if (name) return name;
960
+ return Node2.isExportable(node) && node.isDefaultExport() ? "default" : null;
961
+ }
962
+ function scopeSegment2(node) {
963
+ if (Node2.isClassDeclaration(node) || Node2.isFunctionDeclaration(node)) return declaredName(node);
964
+ if (Node2.isMethodDeclaration(node) || Node2.isGetAccessorDeclaration(node) || Node2.isSetAccessorDeclaration(node)) {
965
+ return node.getName();
966
+ }
967
+ if (Node2.isConstructorDeclaration(node)) return "constructor";
968
+ if (Node2.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
969
+ if (Node2.isClassExpression(node) && node.getName()) return node.getName() ?? null;
970
+ const bound = Node2.isArrowFunction(node) || Node2.isFunctionExpression(node) || Node2.isClassExpression(node) || Node2.isObjectLiteralExpression(node);
971
+ return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
972
+ }
973
+ function scopeOf(node) {
974
+ const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
975
+ let scope = "";
976
+ for (const segment of segments) {
977
+ const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
978
+ if (!repeatsAnonymous) scope = qualify(scope, segment);
979
+ }
980
+ return scope;
981
+ }
982
+ function declarationQualifiedName(decl) {
983
+ if (Node2.isVariableDeclaration(decl)) {
984
+ const init = decl.getInitializer();
985
+ const callable = Node2.isArrowFunction(init) || Node2.isFunctionExpression(init);
986
+ return callable ? qualify(scopeOf(decl), decl.getName()) : null;
987
+ }
988
+ if (Node2.isArrowFunction(decl) || Node2.isFunctionExpression(decl)) {
989
+ const parent = decl.getParent();
990
+ return Node2.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
991
+ }
992
+ const isDeclaration = Node2.isFunctionDeclaration(decl) || Node2.isClassDeclaration(decl) || Node2.isMethodDeclaration(decl) || Node2.isGetAccessorDeclaration(decl) || Node2.isSetAccessorDeclaration(decl) || Node2.isConstructorDeclaration(decl);
993
+ const own = isDeclaration ? scopeSegment2(decl) : null;
994
+ return own ? qualify(scopeOf(decl), own) : null;
995
+ }
996
+
997
+ // src/extractors/symbol-signature.ts
826
998
  var MAX_SIGNATURE = 200;
827
999
  var MAX_PURPOSE = 240;
828
1000
  function declarationText(decl) {
@@ -833,31 +1005,72 @@ function declarationText(decl) {
833
1005
  if (purpose) out.purpose = purpose;
834
1006
  return out;
835
1007
  }
1008
+ function createDeclarationLookup(sf) {
1009
+ let qualified;
1010
+ return (name) => {
1011
+ const fileLevel = fileLevelDeclaration(sf, name);
1012
+ if (fileLevel) return fileLevel;
1013
+ qualified ??= qualifiedDeclarations(sf);
1014
+ return qualified.get(name);
1015
+ };
1016
+ }
836
1017
  function lookupDeclaration(sf, name) {
1018
+ return createDeclarationLookup(sf)(name);
1019
+ }
1020
+ function fileLevelDeclaration(sf, name) {
837
1021
  return sf.getFunction(name) ?? sf.getClass(name) ?? sf.getInterface(name) ?? sf.getTypeAlias(name) ?? sf.getEnum(name) ?? sf.getVariableDeclaration(name);
838
1022
  }
1023
+ function qualifiedDeclarations(sf) {
1024
+ const byName = /* @__PURE__ */ new Map();
1025
+ sf.forEachDescendant((node) => {
1026
+ const name = declarationQualifiedName(node);
1027
+ if (name === null) return;
1028
+ const held = byName.get(name);
1029
+ if (!held || outranks(node, held)) byName.set(name, node);
1030
+ });
1031
+ return byName;
1032
+ }
1033
+ function outranks(candidate, held) {
1034
+ if (Node3.isOverloadable(held) && held.isOverload()) {
1035
+ return !(Node3.isOverloadable(candidate) && candidate.isOverload());
1036
+ }
1037
+ return Node3.isGetAccessorDeclaration(candidate) && Node3.isSetAccessorDeclaration(held);
1038
+ }
839
1039
  function signatureOf(decl) {
840
- if (Node2.isFunctionDeclaration(decl) || Node2.isMethodDeclaration(decl)) {
1040
+ if (Node3.isFunctionDeclaration(decl) || Node3.isMethodDeclaration(decl)) {
841
1041
  return callableSignature(decl, decl.getName() ?? "");
842
1042
  }
843
- if (Node2.isVariableDeclaration(decl)) return variableSignature(decl);
844
- if (Node2.isClassDeclaration(decl)) return clamp(`class ${decl.getName() ?? ""}`.trim());
845
- if (Node2.isInterfaceDeclaration(decl)) return clamp(`interface ${decl.getName()}`);
846
- if (Node2.isTypeAliasDeclaration(decl)) {
1043
+ if (Node3.isGetAccessorDeclaration(decl) || Node3.isSetAccessorDeclaration(decl)) {
1044
+ return accessorSignature(decl);
1045
+ }
1046
+ if (Node3.isVariableDeclaration(decl)) return variableSignature(decl);
1047
+ if (Node3.isClassDeclaration(decl)) return clamp(`class ${decl.getName() ?? ""}`.trim());
1048
+ if (Node3.isInterfaceDeclaration(decl)) return clamp(`interface ${decl.getName()}`);
1049
+ if (Node3.isTypeAliasDeclaration(decl)) {
847
1050
  return clamp(`type ${decl.getName()} = ${oneLine(decl.getTypeNode()?.getText() ?? "")}`);
848
1051
  }
849
- if (Node2.isEnumDeclaration(decl)) return clamp(`enum ${decl.getName()}`);
1052
+ if (Node3.isEnumDeclaration(decl)) return clamp(`enum ${decl.getName()}`);
850
1053
  return void 0;
851
1054
  }
852
1055
  function variableSignature(decl) {
853
1056
  const init = decl.getInitializer();
854
- if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
1057
+ if (init && (Node3.isArrowFunction(init) || Node3.isFunctionExpression(init))) {
855
1058
  return callableSignature(init, decl.getName());
856
1059
  }
857
1060
  const typeNode = decl.getTypeNode();
858
1061
  const type = typeNode ? oneLine(typeNode.getText()) : cleanType(() => decl.getType().getText());
859
1062
  return type ? clamp(`${decl.getName()}: ${type}`) : void 0;
860
1063
  }
1064
+ function accessorSignature(decl) {
1065
+ const type = Node3.isGetAccessorDeclaration(decl) ? returnType(decl) : setterType(decl);
1066
+ return clamp(type ? `${decl.getName()}: ${type}` : decl.getName());
1067
+ }
1068
+ function setterType(decl) {
1069
+ const param = decl.getParameters()[0];
1070
+ if (!param) return void 0;
1071
+ const node = param.getTypeNode();
1072
+ return node ? oneLine(node.getText()) : cleanType(() => param.getType().getText());
1073
+ }
861
1074
  function callableSignature(decl, name) {
862
1075
  const params = decl.getParameters().map((p) => oneLine(p.getText())).join(", ");
863
1076
  const ret = returnType(decl);
@@ -885,8 +1098,8 @@ function purposeOf(decl) {
885
1098
  return desc ? clamp(oneLine(desc), MAX_PURPOSE) : void 0;
886
1099
  }
887
1100
  function jsDocsFor(decl) {
888
- if (Node2.isJSDocable(decl)) return decl.getJsDocs();
889
- if (Node2.isVariableDeclaration(decl)) return decl.getVariableStatement()?.getJsDocs();
1101
+ if (Node3.isJSDocable(decl)) return decl.getJsDocs();
1102
+ if (Node3.isVariableDeclaration(decl)) return decl.getVariableStatement()?.getJsDocs();
890
1103
  return void 0;
891
1104
  }
892
1105
  function oneLine(s) {
@@ -926,9 +1139,10 @@ function buildSymbolNodes(repoRoot, sourceFile, file) {
926
1139
  exported.add(name);
927
1140
  out.push(makeSymbolNode(fId, name, true, spans.get(name), declarationText(own), shapes.get(name)));
928
1141
  }
1142
+ const lookup = createDeclarationLookup(sourceFile);
929
1143
  for (const [name, span] of spans) {
930
1144
  if (exported.has(name)) continue;
931
- const decl = lookupDeclaration(sourceFile, name);
1145
+ const decl = lookup(name);
932
1146
  out.push(makeSymbolNode(fId, name, false, span, decl ? declarationText(decl) : {}, shapes.get(name)));
933
1147
  }
934
1148
  return out;
@@ -936,7 +1150,7 @@ function buildSymbolNodes(repoRoot, sourceFile, file) {
936
1150
 
937
1151
  // src/extractors/dynamic-imports.ts
938
1152
  import {
939
- Node as Node3,
1153
+ Node as Node4,
940
1154
  SyntaxKind as SyntaxKind2
941
1155
  } from "ts-morph";
942
1156
  function dynamicImportSpecifiers(sourceFile) {
@@ -968,12 +1182,12 @@ function dynamicImportBindings(sourceFile) {
968
1182
  function destructuredTargetOf(call) {
969
1183
  const parent = call.getParent();
970
1184
  if (!parent) return void 0;
971
- const init = Node3.isAwaitExpression(parent) ? parent : call;
1185
+ const init = Node4.isAwaitExpression(parent) ? parent : call;
972
1186
  const decl = init.getParent();
973
- if (!decl || !Node3.isVariableDeclaration(decl)) return void 0;
1187
+ if (!decl || !Node4.isVariableDeclaration(decl)) return void 0;
974
1188
  if (decl.getInitializer() !== init) return void 0;
975
1189
  const nameNode = decl.getNameNode();
976
- return Node3.isObjectBindingPattern(nameNode) ? nameNode : void 0;
1190
+ return Node4.isObjectBindingPattern(nameNode) ? nameNode : void 0;
977
1191
  }
978
1192
  function destructuredNames(pattern) {
979
1193
  const names = [];
@@ -1000,8 +1214,7 @@ function recordDynamicSymbolRefEdges(sourceFile, srcFileId, agg, resolveSpecifie
1000
1214
  }
1001
1215
 
1002
1216
  // src/extractors/module-resolution.ts
1003
- import * as path3 from "path";
1004
- import { existsSync } from "fs";
1217
+ import * as path4 from "path";
1005
1218
  var RESOLVABLE_EXTS = [
1006
1219
  ".ts",
1007
1220
  ".tsx",
@@ -1023,45 +1236,45 @@ function* relativeResolutionCandidates(base) {
1023
1236
  const stem = base.slice(0, base.length - jsExt[0].length);
1024
1237
  for (const ext of RESOLVABLE_EXTS) yield stem + ext;
1025
1238
  }
1026
- for (const ext of RESOLVABLE_EXTS) yield path3.join(base, "index" + ext);
1239
+ for (const ext of RESOLVABLE_EXTS) yield path4.join(base, "index" + ext);
1027
1240
  }
1028
- function resolveRelativeAbs(fs3, fromAbs, specifier) {
1241
+ function resolveRelativeAbs(fs2, fromAbs, specifier) {
1029
1242
  if (!isRelativeSpecifier(specifier)) return null;
1030
- const base = path3.resolve(path3.dirname(fromAbs), specifier);
1243
+ const base = path4.resolve(path4.dirname(fromAbs), specifier);
1031
1244
  for (const candidate of relativeResolutionCandidates(base)) {
1032
- if (fs3.fileExistsSync(candidate)) return candidate;
1245
+ if (fs2.fileExistsSync(candidate)) return candidate;
1033
1246
  }
1034
1247
  return null;
1035
1248
  }
1036
1249
  function inRepoFileId(repoRoot, abs) {
1037
- const relative4 = path3.relative(repoRoot, abs);
1038
- if (relative4.startsWith("..") || path3.isAbsolute(relative4)) return null;
1039
- if (relative4.split(path3.sep).includes("node_modules")) return null;
1250
+ const relative5 = path4.relative(repoRoot, abs);
1251
+ if (relative5.startsWith("..") || path4.isAbsolute(relative5)) return null;
1252
+ if (relative5.split(path4.sep).includes("node_modules")) return null;
1040
1253
  return fileId(repoRoot, abs);
1041
1254
  }
1042
- function remapDistToSrc(abs) {
1255
+ function remapDistToSrc(abs, fileExists) {
1043
1256
  const m = /^(.*)[\\/]dist[\\/](.+)\.d\.ts$/.exec(abs);
1044
1257
  if (!m) return abs;
1045
1258
  const base = m[1];
1046
1259
  const sub = m[2];
1047
1260
  for (const ext of [".ts", ".tsx"]) {
1048
- const candidate = path3.join(base, "src", sub + ext);
1049
- if (existsSync(candidate)) return candidate;
1261
+ const candidate = path4.join(base, "src", sub + ext);
1262
+ if (fileExists(candidate)) return candidate;
1050
1263
  }
1051
1264
  return abs;
1052
1265
  }
1053
1266
  function repoTypeRoots(repoRoot) {
1054
1267
  const roots = [];
1055
- for (let dir = path3.resolve(repoRoot); ; dir = path3.dirname(dir)) {
1056
- roots.push(path3.join(dir, "node_modules", "@types"));
1057
- if (path3.dirname(dir) === dir) return roots;
1268
+ for (let dir = path4.resolve(repoRoot); ; dir = path4.dirname(dir)) {
1269
+ roots.push(path4.join(dir, "node_modules", "@types"));
1270
+ if (path4.dirname(dir) === dir) return roots;
1058
1271
  }
1059
1272
  }
1060
1273
 
1061
1274
  // src/extractors/reexport-resolve.ts
1062
1275
  function resolveReExportOrigin(ctx, importerAbs, specifier, name) {
1063
- const fs3 = ctx.project.getFileSystem();
1064
- const targetAbs = resolveRelativeAbs(fs3, importerAbs, specifier);
1276
+ const fs2 = ctx.project.getFileSystem();
1277
+ const targetAbs = resolveRelativeAbs(fs2, importerAbs, specifier);
1065
1278
  return targetAbs ? walkReExports(ctx, targetAbs, name, /* @__PURE__ */ new Set()) : null;
1066
1279
  }
1067
1280
  function walkReExports(ctx, fileAbs, name, visited) {
@@ -1073,13 +1286,13 @@ function walkReExports(ctx, fileAbs, name, visited) {
1073
1286
  const id = inRepoFileId(ctx.repoRoot, fileAbs);
1074
1287
  return id ? symbolId(id, name) : null;
1075
1288
  }
1076
- const fs3 = ctx.project.getFileSystem();
1289
+ const fs2 = ctx.project.getFileSystem();
1077
1290
  for (const ed of sf.getExportDeclarations()) {
1078
1291
  const spec = ed.getModuleSpecifierValue();
1079
1292
  if (!spec) continue;
1080
1293
  const inward = reExportInwardName(ed, name);
1081
1294
  if (inward === null) continue;
1082
- const targetAbs = resolveRelativeAbs(fs3, fileAbs, spec);
1295
+ const targetAbs = resolveRelativeAbs(fs2, fileAbs, spec);
1083
1296
  if (!targetAbs) continue;
1084
1297
  const origin = walkReExports(ctx, targetAbs, inward, visited);
1085
1298
  if (origin) return origin;
@@ -1105,57 +1318,6 @@ function reExportInwardName(ed, name) {
1105
1318
 
1106
1319
  // src/extractors/ts-calls.ts
1107
1320
  import { Node as Node5, SyntaxKind as SyntaxKind3 } from "ts-morph";
1108
-
1109
- // src/extractors/ts-scope.ts
1110
- import { Node as Node4 } from "ts-morph";
1111
- function bindingName2(node) {
1112
- const parent = node.getParent();
1113
- if (Node4.isVariableDeclaration(parent) || Node4.isPropertyDeclaration(parent)) return parent.getName();
1114
- if (Node4.isPropertyAssignment(parent)) return parent.getNameNode().getText();
1115
- if (Node4.isExportAssignment(parent)) return "default";
1116
- return null;
1117
- }
1118
- function declaredName(node) {
1119
- const name = node.getName();
1120
- if (name) return name;
1121
- return Node4.isExportable(node) && node.isDefaultExport() ? "default" : null;
1122
- }
1123
- function scopeSegment2(node) {
1124
- if (Node4.isClassDeclaration(node) || Node4.isFunctionDeclaration(node)) return declaredName(node);
1125
- if (Node4.isMethodDeclaration(node) || Node4.isGetAccessorDeclaration(node) || Node4.isSetAccessorDeclaration(node)) {
1126
- return node.getName();
1127
- }
1128
- if (Node4.isConstructorDeclaration(node)) return "constructor";
1129
- if (Node4.isModuleDeclaration(node)) return node.hasNamespaceKeyword() ? node.getName() : null;
1130
- if (Node4.isClassExpression(node) && node.getName()) return node.getName() ?? null;
1131
- const bound = Node4.isArrowFunction(node) || Node4.isFunctionExpression(node) || Node4.isClassExpression(node) || Node4.isObjectLiteralExpression(node);
1132
- return bound ? bindingName2(node) ?? ANONYMOUS_SCOPE : null;
1133
- }
1134
- function scopeOf(node) {
1135
- const segments = node.getAncestors().map(scopeSegment2).filter((s) => s !== null).reverse();
1136
- let scope = "";
1137
- for (const segment of segments) {
1138
- const repeatsAnonymous = segment === ANONYMOUS_SCOPE && (scope === ANONYMOUS_SCOPE || scope.endsWith(`.${ANONYMOUS_SCOPE}`));
1139
- if (!repeatsAnonymous) scope = qualify(scope, segment);
1140
- }
1141
- return scope;
1142
- }
1143
- function declarationQualifiedName(decl) {
1144
- if (Node4.isVariableDeclaration(decl)) {
1145
- const init = decl.getInitializer();
1146
- const callable = Node4.isArrowFunction(init) || Node4.isFunctionExpression(init);
1147
- return callable ? qualify(scopeOf(decl), decl.getName()) : null;
1148
- }
1149
- if (Node4.isArrowFunction(decl) || Node4.isFunctionExpression(decl)) {
1150
- const parent = decl.getParent();
1151
- return Node4.isVariableDeclaration(parent) ? qualify(scopeOf(parent), parent.getName()) : null;
1152
- }
1153
- const isDeclaration = Node4.isFunctionDeclaration(decl) || Node4.isClassDeclaration(decl) || Node4.isMethodDeclaration(decl) || Node4.isGetAccessorDeclaration(decl) || Node4.isSetAccessorDeclaration(decl) || Node4.isConstructorDeclaration(decl);
1154
- const own = isDeclaration ? scopeSegment2(decl) : null;
1155
- return own ? qualify(scopeOf(decl), own) : null;
1156
- }
1157
-
1158
- // src/extractors/ts-calls.ts
1159
1321
  function collectTsCallEdges(sourceFile, ctx) {
1160
1322
  const agg = /* @__PURE__ */ new Map();
1161
1323
  sourceFile.forEachDescendant((node) => {
@@ -1181,7 +1343,7 @@ function resolveCallee(call, ctx) {
1181
1343
  }
1182
1344
  function declarationId(decl, ctx) {
1183
1345
  const sf = decl.getSourceFile();
1184
- const abs = remapDistToSrc(sf.getFilePath());
1346
+ const abs = remapDistToSrc(sf.getFilePath(), ctx.fileExists);
1185
1347
  if (sf.isDeclarationFile() && abs === sf.getFilePath()) return null;
1186
1348
  const fileId2 = inRepoFileId(ctx.repoRoot, abs);
1187
1349
  const name = fileId2 ? declarationQualifiedName(decl) : null;
@@ -1223,33 +1385,55 @@ function literalText(arg) {
1223
1385
  }
1224
1386
 
1225
1387
  // src/extractors/ts-morph-extractor.ts
1388
+ var RETIRE_BATCH = 100;
1226
1389
  var TsMorphGraphExtractor = class {
1227
1390
  name = "ts-morph-graph";
1228
1391
  repoRoot;
1229
1392
  tsConfigPath;
1230
1393
  project;
1394
+ ownsProject;
1395
+ source;
1396
+ retired = [];
1397
+ fileExists = (abs) => this.source.fileExists(abs);
1231
1398
  constructor(options) {
1232
1399
  this.repoRoot = options.repoRoot;
1233
1400
  this.tsConfigPath = options.tsConfigPath;
1234
1401
  this.project = options.project;
1402
+ this.ownsProject = options.project === void 0;
1403
+ this.source = options.source ?? workingTreeSource();
1235
1404
  }
1236
1405
  extract(file) {
1237
1406
  if (!isTypeScriptFile(file)) return [];
1238
1407
  const project = this.ensureProject();
1239
1408
  const sourceFile = this.loadSourceFile(project, file);
1409
+ try {
1410
+ return this.extractFrom(sourceFile, file);
1411
+ } finally {
1412
+ if (this.ownsProject) this.retire(project, sourceFile);
1413
+ }
1414
+ }
1415
+ retire(project, sourceFile) {
1416
+ this.retired.push(sourceFile);
1417
+ if (this.retired.length < RETIRE_BATCH) return;
1418
+ for (const sf of this.retired.splice(0)) project.removeSourceFile(sf);
1419
+ }
1420
+ extractFrom(sourceFile, file) {
1240
1421
  const nodes = this.buildFileAndModuleNodes(sourceFile);
1241
1422
  const symbolNodes2 = buildSymbolNodes(this.repoRoot, sourceFile, file);
1242
1423
  const { edges, externalNodes } = this.collectEdges(sourceFile);
1243
1424
  const calls = collectTsCallEdges(sourceFile, {
1244
1425
  repoRoot: this.repoRoot,
1245
1426
  srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
1246
- localSymbols: new Set(symbolNodes2.map((n) => n.name))
1427
+ localSymbols: new Set(symbolNodes2.map((n) => n.name)),
1428
+ fileExists: this.fileExists
1247
1429
  });
1248
1430
  return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
1249
1431
  }
1250
1432
  ensureProject() {
1251
1433
  if (this.project) return this.project;
1252
- this.project = this.tsConfigPath ? new Project({ tsConfigFilePath: this.tsConfigPath }) : new Project({
1434
+ const fileSystem = this.source.fileSystem;
1435
+ this.project = this.tsConfigPath ? new Project2({ tsConfigFilePath: this.tsConfigPath, fileSystem }) : new Project2({
1436
+ fileSystem,
1253
1437
  compilerOptions: {
1254
1438
  allowJs: true,
1255
1439
  target: ScriptTarget.ESNext,
@@ -1350,7 +1534,7 @@ var TsMorphGraphExtractor = class {
1350
1534
  if (targetSf) {
1351
1535
  const decls = targetSf.getExportedDeclarations().get(importedName);
1352
1536
  const originSf = decls && decls.length > 0 ? decls[0].getSourceFile() : targetSf;
1353
- const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath()));
1537
+ const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath(), this.fileExists));
1354
1538
  return originId ? symbolId(originId, importedName) : null;
1355
1539
  }
1356
1540
  return resolveReExportOrigin(
@@ -1389,7 +1573,7 @@ var TsMorphGraphExtractor = class {
1389
1573
  }
1390
1574
  resolveInternal(target) {
1391
1575
  if (!target) return null;
1392
- return this.inRepoFileId(remapDistToSrc(target.getFilePath()));
1576
+ return this.inRepoFileId(remapDistToSrc(target.getFilePath(), this.fileExists));
1393
1577
  }
1394
1578
  /**
1395
1579
  * Resolve a relative specifier ts-morph failed to link by walking the
@@ -1417,14 +1601,13 @@ function isTypeScriptFile(file) {
1417
1601
  }
1418
1602
 
1419
1603
  // src/extractors/python-extractor.ts
1420
- import * as path4 from "path";
1421
- import { existsSync as existsSync2 } from "fs";
1604
+ import * as path5 from "path";
1422
1605
 
1423
1606
  // src/extractors/python-calls.ts
1424
- function collectPythonCallEdges(file, fileId2, resolve4) {
1607
+ function collectPythonCallEdges(file, fileId2, resolve5) {
1425
1608
  const declarations = /* @__PURE__ */ new Map();
1426
1609
  forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
1427
- const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve4) };
1610
+ const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve5) };
1428
1611
  const agg = /* @__PURE__ */ new Map();
1429
1612
  walkScopes(file.tree.rootNode, true, (node, scope) => {
1430
1613
  if (node.type !== "call") return;
@@ -1488,11 +1671,11 @@ function parameterNames(def) {
1488
1671
  }
1489
1672
  return out;
1490
1673
  }
1491
- function fromImports(root, resolve4) {
1674
+ function fromImports(root, resolve5) {
1492
1675
  const out = /* @__PURE__ */ new Map();
1493
1676
  for (const stmt of root.descendantsOfType("import_from_statement")) {
1494
1677
  const moduleName = stmt?.childForFieldName("module_name")?.text;
1495
- const moduleFile = moduleName ? resolve4(moduleName) : null;
1678
+ const moduleFile = moduleName ? resolve5(moduleName) : null;
1496
1679
  if (!stmt || !moduleFile) continue;
1497
1680
  for (const imported of stmt.childrenForFieldName("name")) {
1498
1681
  const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
@@ -1534,13 +1717,15 @@ function addKeyword(site, arg) {
1534
1717
  // src/extractors/python-extractor.ts
1535
1718
  function* candidates(base) {
1536
1719
  yield `${base}.py`;
1537
- yield path4.join(base, "__init__.py");
1720
+ yield path5.join(base, "__init__.py");
1538
1721
  }
1539
1722
  var PythonGraphExtractor = class {
1540
- constructor(repoRoot) {
1723
+ constructor(repoRoot, source = workingTreeSource()) {
1541
1724
  this.repoRoot = repoRoot;
1725
+ this.source = source;
1542
1726
  }
1543
1727
  repoRoot;
1728
+ source;
1544
1729
  name = "python-graph";
1545
1730
  extract(file) {
1546
1731
  if (file.language !== "python") return [];
@@ -1572,12 +1757,12 @@ var PythonGraphExtractor = class {
1572
1757
  resolveDotted(fromAbs, specifier) {
1573
1758
  const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
1574
1759
  const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
1575
- const relativeBase = path4.resolve(path4.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1576
- const roots = leadingDots > 0 ? [relativeBase] : [path4.dirname(fromAbs), this.repoRoot];
1760
+ const relativeBase = path5.resolve(path5.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1761
+ const roots = leadingDots > 0 ? [relativeBase] : [path5.dirname(fromAbs), this.repoRoot];
1577
1762
  for (const root of roots) {
1578
- for (const candidate of candidates(path4.join(root, ...parts))) {
1763
+ for (const candidate of candidates(path5.join(root, ...parts))) {
1579
1764
  const id = inRepoFileId(this.repoRoot, candidate);
1580
- if (id !== null && existsSync2(candidate)) return id;
1765
+ if (id !== null && this.source.fileExists(candidate)) return id;
1581
1766
  }
1582
1767
  }
1583
1768
  return null;
@@ -1621,9 +1806,10 @@ var LanguageExtractor = class {
1621
1806
  name = "language-graph";
1622
1807
  delegates;
1623
1808
  constructor(options) {
1809
+ const { repoRoot, tsConfigPath, source } = options;
1624
1810
  this.delegates = [
1625
- new TsMorphGraphExtractor({ repoRoot: options.repoRoot, tsConfigPath: options.tsConfigPath }),
1626
- new PythonGraphExtractor(options.repoRoot)
1811
+ new TsMorphGraphExtractor({ repoRoot, tsConfigPath, source }),
1812
+ new PythonGraphExtractor(repoRoot, source)
1627
1813
  ];
1628
1814
  }
1629
1815
  extract(file) {
@@ -1632,7 +1818,7 @@ var LanguageExtractor = class {
1632
1818
  };
1633
1819
 
1634
1820
  // src/git-renames.ts
1635
- import * as path5 from "path";
1821
+ import * as path6 from "path";
1636
1822
  function detectGitHead(repoRoot) {
1637
1823
  return runGit(repoRoot, ["rev-parse", "HEAD"]);
1638
1824
  }
@@ -1676,12 +1862,12 @@ function parseRenameOutput(text) {
1676
1862
  return out;
1677
1863
  }
1678
1864
  function classifyRename(oldPath, newPath) {
1679
- return path5.posix.dirname(oldPath) === path5.posix.dirname(newPath) ? "rename" : "move";
1865
+ return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
1680
1866
  }
1681
1867
  function renameToAliases(repoRoot, pair) {
1682
1868
  const reason = classifyRename(pair.oldPath, pair.newPath);
1683
- const oldAbs = path5.join(repoRoot, pair.oldPath);
1684
- const newAbs = path5.join(repoRoot, pair.newPath);
1869
+ const oldAbs = path6.join(repoRoot, pair.oldPath);
1870
+ const newAbs = path6.join(repoRoot, pair.newPath);
1685
1871
  return [
1686
1872
  { oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
1687
1873
  { oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
@@ -1774,8 +1960,8 @@ function walk(requestedId, maps) {
1774
1960
  }
1775
1961
  function chainSteps(input) {
1776
1962
  if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
1777
- const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1778
- if (path11) return { steps: path11, connected: true };
1963
+ const path14 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1964
+ if (path14) return { steps: path14, connected: true };
1779
1965
  return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
1780
1966
  }
1781
1967
  function createAliasChain(input) {
@@ -1860,9 +2046,11 @@ function computeAliasBridge(store, input) {
1860
2046
  return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
1861
2047
  }
1862
2048
 
2049
+ // src/roles.ts
2050
+ import * as path8 from "path";
2051
+
1863
2052
  // src/generated.ts
1864
- import { readFileSync } from "fs";
1865
- import * as path6 from "path";
2053
+ import * as path7 from "path";
1866
2054
  var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
1867
2055
  var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
1868
2056
  function isGeneratedByHeuristic(id) {
@@ -1872,59 +2060,112 @@ function isGeneratedFile(id, patterns) {
1872
2060
  for (const rx of patterns) if (rx.test(id)) return true;
1873
2061
  return isGeneratedByHeuristic(id);
1874
2062
  }
1875
- function loadGeneratedPatterns(rootDir) {
2063
+ function loadGeneratedPatterns(rootDir, source = workingTreeSource()) {
1876
2064
  try {
1877
- const content = readFileSync(path6.join(rootDir, ".gitattributes"), "utf-8");
2065
+ const content = source.readFile(path7.join(rootDir, ".gitattributes"));
1878
2066
  return parseGeneratedPatterns(content);
1879
2067
  } catch {
1880
2068
  return [];
1881
2069
  }
1882
2070
  }
1883
2071
  function parseGeneratedPatterns(content) {
2072
+ const rules = parseGeneratedRules(content);
1884
2073
  const out = [];
2074
+ rules.forEach((rule, i) => {
2075
+ if (!rule.generated) return;
2076
+ const optOuts = rules.slice(i + 1).filter((r) => !r.generated);
2077
+ out.push(excluding(rule.rx, optOuts.map((r) => r.rx)));
2078
+ });
2079
+ return out;
2080
+ }
2081
+ function parseGeneratedRules(content) {
2082
+ const rules = [];
1885
2083
  for (const raw of content.split("\n")) {
1886
2084
  const line = raw.trim();
1887
2085
  if (!line || line.startsWith("#")) continue;
1888
2086
  const parsed = /^("(?:[^"\\]|\\.)*"|\S+)\s*(.*)$/.exec(line);
1889
2087
  if (!parsed) continue;
1890
- const pattern = unquote(parsed[1]);
1891
- if (generatedAttr(parsed[2].split(/\s+/).filter(Boolean)) === true) {
1892
- out.push(gitattributesPatternToRegex(pattern));
1893
- }
2088
+ const generated = generatedAttr(parsed[2].split(/\s+/).filter(Boolean));
2089
+ if (generated === void 0) continue;
2090
+ rules.push({ rx: gitattributesPatternToRegex(unquote(parsed[1])), generated });
1894
2091
  }
1895
- return out;
2092
+ return rules;
2093
+ }
2094
+ function excluding(rx, optOuts) {
2095
+ if (optOuts.length === 0) return rx;
2096
+ const rejected = optOuts.map((o) => `(?:${o.source})`).join("|");
2097
+ return new RegExp(`(?!${rejected})(?:${rx.source})`);
1896
2098
  }
1897
2099
  function generatedAttr(attrs) {
1898
2100
  let value;
1899
2101
  for (const a of attrs) {
1900
2102
  if (a === "linguist-generated" || a === "linguist-generated=true") value = true;
1901
- else if (a === "-linguist-generated" || a === "linguist-generated=false") value = false;
2103
+ else if (OPT_OUT_TOKENS.has(a)) value = false;
1902
2104
  }
1903
2105
  return value;
1904
2106
  }
2107
+ var OPT_OUT_TOKENS = /* @__PURE__ */ new Set([
2108
+ "-linguist-generated",
2109
+ "linguist-generated=false",
2110
+ "!linguist-generated"
2111
+ ]);
1905
2112
  function unquote(token) {
1906
2113
  return token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1).replace(/\\(.)/g, "$1") : token;
1907
2114
  }
2115
+ var NEVER_MATCHES = /(?!)/;
1908
2116
  function gitattributesPatternToRegex(pattern) {
1909
2117
  let p = pattern;
1910
- const isDir = p.endsWith("/");
1911
- if (isDir) p = p.slice(0, -1);
1912
- let body = "";
1913
- for (let i = 0; i < p.length; i++) {
1914
- const c = p[i];
1915
- if (c === "*" && p[i + 1] === "*") {
1916
- body += ".*";
1917
- i++;
1918
- } else if (c === "*") body += "[^/]*";
1919
- else if (c === "?") body += "[^/]";
1920
- else body += c.replace(/[.+(){}|^$\\[\]]/, "\\$&");
1921
- }
1922
- const prefix = p.includes("/") ? "^" : "(?:^|/)";
1923
- return new RegExp(prefix + body + (isDir ? "(?:/|$)" : "$"));
2118
+ if (p.endsWith("/")) return NEVER_MATCHES;
2119
+ const anchored = p.includes("/");
2120
+ if (p.startsWith("/")) p = p.slice(1);
2121
+ const prefix = anchored ? "^" : "^(?:.*/)?";
2122
+ return new RegExp(prefix + globBody(p) + "$");
2123
+ }
2124
+ function globBody(p) {
2125
+ const segments = p.split("/");
2126
+ return segments.map((seg, i) => {
2127
+ const last = i === segments.length - 1;
2128
+ if (seg === "**") return last ? ".+" : "(?:.*/)?";
2129
+ return globSegment(seg) + (last ? "" : "/");
2130
+ }).join("");
2131
+ }
2132
+ function globSegment(seg) {
2133
+ let out = "";
2134
+ for (const c of seg) {
2135
+ if (c === "*") out += "[^/]*";
2136
+ else if (c === "?") out += "[^/]";
2137
+ else out += c.replace(/[.+(){}|^$\\[\]]/, "\\$&");
2138
+ }
2139
+ return out;
2140
+ }
2141
+
2142
+ // src/role-globs.ts
2143
+ function parseRoleGlobs(value, label, knownRoles) {
2144
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2145
+ throw new Error(`${label}: expected an object mapping roles to glob arrays`);
2146
+ }
2147
+ const out = {};
2148
+ for (const [role, globs] of Object.entries(value)) {
2149
+ if (!knownRoles.includes(role)) {
2150
+ throw new Error(`${label}: unknown role "${role}"`);
2151
+ }
2152
+ if (!Array.isArray(globs) || !globs.every((g) => typeof g === "string")) {
2153
+ throw new Error(`${label}: "${role}" must be an array of glob strings`);
2154
+ }
2155
+ out[role] = globs;
2156
+ }
2157
+ return out;
2158
+ }
2159
+ function roleGlobMatcher(globs, order) {
2160
+ const compiled = order.flatMap(
2161
+ (role) => (globs?.[role] ?? []).map((g) => ({ role, rx: gitattributesPatternToRegex(g) }))
2162
+ );
2163
+ return (id) => compiled.find(({ rx }) => rx.test(id))?.role;
1924
2164
  }
1925
2165
 
1926
2166
  // src/roles.ts
1927
2167
  var TEST_RE = /(?:^|\/)(?:__tests__\/|tests?\/)|\.(?:test|spec)(?:\.[a-z]+)?$/;
2168
+ var STORY_RE = /\.stories(?:\.[cm]?[jt]sx?)?$|\.mdx$/;
1928
2169
  var FIXTURE_RE = /(?:^|\/)fixtures(?:\/|$)/;
1929
2170
  var SCRIPT_RE = /(?:^|\/)(?:scripts|archive)(?:\/|$)/;
1930
2171
  var BARREL_RE = /(?:^|\/)index(?:\.[a-z]+)?$/;
@@ -1933,6 +2174,8 @@ var CONFIG_RE = /\.config(?:\.[a-z]+)?$/;
1933
2174
  var ALL_ROLES = [
1934
2175
  "test",
1935
2176
  "fixture",
2177
+ "story",
2178
+ "lab",
1936
2179
  "barrel",
1937
2180
  "types",
1938
2181
  "config",
@@ -1943,7 +2186,9 @@ var ALL_ROLES = [
1943
2186
  ];
1944
2187
  function classifyRole(id, hints) {
1945
2188
  if (hints?.isGenerated) return "generated";
2189
+ if (hints?.configuredRole) return hints.configuredRole;
1946
2190
  if (TEST_RE.test(id)) return "test";
2191
+ if (STORY_RE.test(id)) return "story";
1947
2192
  if (FIXTURE_RE.test(id)) return "fixture";
1948
2193
  if (SCRIPT_RE.test(id)) return "script";
1949
2194
  if (hints?.hasShebang) return "entry";
@@ -1952,8 +2197,14 @@ function classifyRole(id, hints) {
1952
2197
  if (CONFIG_RE.test(id)) return "config";
1953
2198
  return "source";
1954
2199
  }
1955
- function computeRoleHints(readFiles, idRoot, toId) {
1956
- const patterns = loadGeneratedPatterns(idRoot);
2200
+ var ROLE_GLOBS_PATH = ".codewatch/roles.json";
2201
+ function loadRoleGlobs(rootDir, source = workingTreeSource()) {
2202
+ const file = path8.join(rootDir, ROLE_GLOBS_PATH);
2203
+ if (!source.fileExists(file)) return {};
2204
+ return parseRoleGlobs(JSON.parse(source.readFile(file)), ROLE_GLOBS_PATH, ALL_ROLES);
2205
+ }
2206
+ function computeRoleHints(readFiles, idRoot, toId, source) {
2207
+ const patterns = loadGeneratedPatterns(idRoot, source);
1957
2208
  const shebangIds = /* @__PURE__ */ new Set();
1958
2209
  const generatedIds = /* @__PURE__ */ new Set();
1959
2210
  for (const rf of readFiles) {
@@ -1961,47 +2212,320 @@ function computeRoleHints(readFiles, idRoot, toId) {
1961
2212
  if (rf.content.startsWith("#!")) shebangIds.add(id);
1962
2213
  if (isGeneratedFile(id, patterns)) generatedIds.add(id);
1963
2214
  }
1964
- return { shebangIds, generatedIds };
2215
+ return { shebangIds, generatedIds, roleGlobs: loadRoleGlobs(idRoot, source) };
1965
2216
  }
1966
2217
  function annotateRoles(nodes, options) {
2218
+ const configuredRole = roleGlobMatcher(options?.roleGlobs, ALL_ROLES);
1967
2219
  return nodes.map(
1968
2220
  (n) => n.kind === "file" || n.kind === "module" ? {
1969
2221
  ...n,
1970
2222
  role: n.role ?? classifyRole(n.id, {
1971
2223
  hasShebang: options?.shebangIds?.has(n.id) ?? false,
1972
- isGenerated: options?.generatedIds?.has(n.id) ?? false
2224
+ isGenerated: options?.generatedIds?.has(n.id) ?? false,
2225
+ configuredRole: configuredRole(n.id)
1973
2226
  })
1974
2227
  } : n
1975
2228
  );
1976
2229
  }
1977
2230
 
1978
- // src/file-walk.ts
1979
- import * as fs from "fs/promises";
1980
- import * as path7 from "path";
1981
- import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
1982
- async function walkSourceFiles(rootDirs, languages) {
2231
+ // src/git-tree-source.ts
2232
+ import { readlinkSync, realpathSync, statSync } from "fs";
2233
+ import * as path9 from "path";
2234
+ import { isExcludedDir as isExcludedDir2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
2235
+ import { Project as Project3 } from "ts-morph";
2236
+
2237
+ // src/git-tree-host.ts
2238
+ function readOnly() {
2239
+ throw new Error("a git tree source is read-only");
2240
+ }
2241
+ function overlayHost(answers, real) {
2242
+ return {
2243
+ isCaseSensitive: () => real.isCaseSensitive(),
2244
+ readDirSync: answers.readDir,
2245
+ readFileSync: (p) => answers.readFile(p),
2246
+ readFile: async (p) => answers.readFile(p),
2247
+ fileExistsSync: answers.fileExists,
2248
+ fileExists: async (p) => answers.fileExists(p),
2249
+ directoryExistsSync: answers.directoryExists,
2250
+ directoryExists: async (p) => answers.directoryExists(p),
2251
+ realpathSync: answers.realpath,
2252
+ getCurrentDirectory: () => real.getCurrentDirectory(),
2253
+ glob: () => Promise.reject(new Error("glob is not supported on a git tree source")),
2254
+ globSync: () => readOnly(),
2255
+ delete: async () => readOnly(),
2256
+ deleteSync: readOnly,
2257
+ writeFile: async () => readOnly(),
2258
+ writeFileSync: readOnly,
2259
+ mkdir: async () => readOnly(),
2260
+ mkdirSync: readOnly,
2261
+ move: async () => readOnly(),
2262
+ moveSync: readOnly,
2263
+ copy: async () => readOnly(),
2264
+ copySync: readOnly
2265
+ };
2266
+ }
2267
+
2268
+ // src/git-tree-source.ts
2269
+ var READ_BY_INDEX = /\.(?:[cm]?[jt]sx?|json|pyi?)$|(?:^|\/)\.gitattributes$/;
2270
+ var MAX_LINK_HOPS = 40;
2271
+ var emptyHost;
2272
+ function missing(abs, kind) {
2273
+ emptyHost ??= new Project3({ useInMemoryFileSystem: true }).getFileSystem();
2274
+ if (kind === "file") emptyHost.readFileSync(abs);
2275
+ else emptyHost.readDirSync(abs);
2276
+ throw new Error(`${abs} was expected to be missing`);
2277
+ }
2278
+ function linkTarget(abs) {
2279
+ try {
2280
+ return path9.resolve(path9.dirname(abs), readlinkSync(abs));
2281
+ } catch {
2282
+ return null;
2283
+ }
2284
+ }
2285
+ function relativeInside(root, abs) {
2286
+ const rel = path9.relative(root, abs);
2287
+ return rel === ".." || rel.startsWith(`..${path9.sep}`) || path9.isAbsolute(rel) ? null : rel;
2288
+ }
2289
+ function canonicalize(abs, hops = 0) {
2290
+ const rest = [];
2291
+ for (let at = path9.resolve(abs); ; at = path9.dirname(at)) {
2292
+ try {
2293
+ return path9.join(realpathSync(at), ...rest.reverse());
2294
+ } catch {
2295
+ const target = hops < MAX_LINK_HOPS ? linkTarget(at) : null;
2296
+ if (target !== null) return canonicalize(path9.join(target, ...rest.reverse()), hops + 1);
2297
+ if (path9.dirname(at) === at) return path9.resolve(abs);
2298
+ rest.push(path9.basename(at));
2299
+ }
2300
+ }
2301
+ }
2302
+ function firstExcluded(rel, includeSelf) {
2303
+ const segments = rel.split(path9.sep);
2304
+ return (includeSelf ? segments : segments.slice(0, -1)).findIndex(isExcludedDir2);
2305
+ }
2306
+ var CommitTree = class {
2307
+ constructor(root, commit) {
2308
+ this.root = root;
2309
+ for (const blob of listTreeBlobs(root, commit)) {
2310
+ const rel = blob.path.split("/").join(path9.sep);
2311
+ (blob.link ? this.links : this.oids).set(rel, blob.oid);
2312
+ this.addEntry(rel, blob.link ? "link" : "file");
2313
+ }
2314
+ }
2315
+ root;
2316
+ oids = /* @__PURE__ */ new Map();
2317
+ links = /* @__PURE__ */ new Map();
2318
+ dirs = /* @__PURE__ */ new Map();
2319
+ canonical = /* @__PURE__ */ new Map();
2320
+ contents;
2321
+ /** The one place an answer's origin is decided; every file, dir and realpath answer goes through it. */
2322
+ locate(abs, kind) {
2323
+ return this.classify(this.resolve(path9.resolve(abs), 0), kind);
2324
+ }
2325
+ classify(canonical, kind) {
2326
+ const rel = relativeInside(this.root, canonical);
2327
+ if (rel === null) return { origin: "disk", canonical, rel: null };
2328
+ const tracked = kind === "file" ? this.oids.has(rel) : this.dirs.has(rel);
2329
+ if (tracked) return { origin: "tree", canonical, rel };
2330
+ return { origin: this.ownsBuildOutput(rel, kind === "dir") ? "disk" : "missing", canonical, rel };
2331
+ }
2332
+ /**
2333
+ * Untracked build output a checkout would see on disk: under an excluded dir
2334
+ * whose parent the commit tracks, so `untracked/dist` is as absent as `untracked`.
2335
+ * A tracked symlink only lands here when its chain loops or dangles, and is then absent.
2336
+ */
2337
+ ownsBuildOutput(rel, includeSelf) {
2338
+ const at = firstExcluded(rel, includeSelf);
2339
+ if (at < 0 || this.links.has(rel)) return false;
2340
+ return this.dirs.has(rel.split(path9.sep).slice(0, at).join(path9.sep));
2341
+ }
2342
+ *files() {
2343
+ for (const rel of this.oids.keys()) yield path9.join(this.root, rel);
2344
+ }
2345
+ /** Children of a tracked dir by name, or undefined when the commit has no such dir. */
2346
+ children(rel) {
2347
+ return this.dirs.get(rel);
2348
+ }
2349
+ read(rel) {
2350
+ return this.blob(this.oids.get(rel));
2351
+ }
2352
+ /** Every link target joins the first batch: a checkout resolves links before it reads anything. */
2353
+ blob(oid) {
2354
+ this.contents ??= readBlobs(this.root, [
2355
+ ...[...this.oids].filter(([r]) => READ_BY_INDEX.test(r)).map(([, o]) => o),
2356
+ ...this.links.values()
2357
+ ]);
2358
+ if (!this.contents.has(oid)) this.contents.set(oid, readBlobs(this.root, [oid]).get(oid));
2359
+ return this.contents.get(oid);
2360
+ }
2361
+ tracks(rel) {
2362
+ return rel !== null && (this.oids.has(rel) || this.dirs.has(rel));
2363
+ }
2364
+ resolve(abs, hops) {
2365
+ const parent = path9.dirname(abs);
2366
+ return parent === abs ? abs : this.step(this.resolve(parent, hops), path9.basename(abs), hops);
2367
+ }
2368
+ /**
2369
+ * Every component of every path and link target passes through here, memoized
2370
+ * per path and hop budget: a link named many times in nested targets is followed
2371
+ * once per budget, and a chain cut short by the budget never answers for a shorter one.
2372
+ */
2373
+ step(dir, name, hops) {
2374
+ const key = `${hops}:${path9.join(dir, name)}`;
2375
+ let resolved = this.canonical.get(key);
2376
+ if (resolved === void 0) {
2377
+ resolved = this.stepOnce(dir, name, hops);
2378
+ this.canonical.set(key, resolved);
2379
+ }
2380
+ return resolved;
2381
+ }
2382
+ /** Disk is never consulted for a component the commit tracks, a symlink included. */
2383
+ stepOnce(dir, name, hops) {
2384
+ const next = path9.join(dir, name);
2385
+ const rel = relativeInside(this.root, next);
2386
+ if (this.tracks(rel)) return next;
2387
+ if (hops >= MAX_LINK_HOPS) return next;
2388
+ const linkOid = rel === null ? void 0 : this.links.get(rel);
2389
+ if (linkOid !== void 0) return this.follow(dir, this.blob(linkOid), hops + 1) ?? next;
2390
+ const target = linkTarget(next);
2391
+ return target === null ? next : this.resolve(target, hops + 1);
2392
+ }
2393
+ /**
2394
+ * A tracked link's target walked one component at a time from `dir`, as a
2395
+ * checkout walks it: `..` leaves the dir actually reached, so `gone/..` dangles
2396
+ * (null) when `gone` is absent, where lexical normalisation would drop it.
2397
+ */
2398
+ follow(dir, target, hops) {
2399
+ let at = path9.isAbsolute(target) ? path9.parse(path9.resolve(target)).root : dir;
2400
+ for (const name of target.split("/")) {
2401
+ if (name === "" || name === ".") continue;
2402
+ if (name !== "..") at = this.step(at, name, hops);
2403
+ else if (this.isDir(at)) at = path9.dirname(at);
2404
+ else return null;
2405
+ }
2406
+ return at;
2407
+ }
2408
+ isDir(canonical) {
2409
+ const { origin } = this.classify(canonical, "dir");
2410
+ if (origin !== "disk") return origin === "tree";
2411
+ return statSync(canonical, { throwIfNoEntry: false })?.isDirectory() ?? false;
2412
+ }
2413
+ addEntry(rel, kind) {
2414
+ let child = rel;
2415
+ let childKind = kind;
2416
+ for (let dir = path9.dirname(rel); child !== "."; child = dir, dir = path9.dirname(dir)) {
2417
+ const key = dir === "." ? "" : dir;
2418
+ const known = this.dirs.has(key);
2419
+ const children = this.dirs.get(key) ?? /* @__PURE__ */ new Map();
2420
+ children.set(path9.basename(child), childKind);
2421
+ this.dirs.set(key, children);
2422
+ if (known) return;
2423
+ childKind = "dir";
2424
+ }
2425
+ }
2426
+ };
2427
+ function* ingestedUnder(tree, rootDir, languages) {
2428
+ for (const abs of tree.files()) {
2429
+ const rel = relativeInside(rootDir, abs);
2430
+ if (rel === null) continue;
2431
+ if (path9.dirname(rel).split(path9.sep).some(isExcludedDir2)) continue;
2432
+ if (shouldIncludeFile2(rel, [...languages])) yield abs;
2433
+ }
2434
+ }
2435
+ function listTreeFiles(tree, rootDirs, languages) {
1983
2436
  const seen = /* @__PURE__ */ new Set();
1984
- const out = [];
1985
2437
  for (const rootDir of rootDirs) {
1986
- await collectFiles(rootDir, rootDir, languages, seen, out);
2438
+ for (const abs of ingestedUnder(tree, rootDir, languages)) seen.add(abs);
1987
2439
  }
1988
- return out;
2440
+ return [...seen].sort();
1989
2441
  }
1990
- async function collectFiles(dir, rootDir, languages, seen, out) {
1991
- const entries = await fs.readdir(dir, { withFileTypes: true });
1992
- for (const entry of entries) {
1993
- const full = path7.join(dir, entry.name);
1994
- if (entry.isDirectory()) {
1995
- if (isExcludedDir(entry.name)) continue;
1996
- await collectFiles(full, rootDir, languages, seen, out);
1997
- } else if (entry.isFile() && !seen.has(full)) {
1998
- const relative4 = path7.relative(rootDir, full);
1999
- if (shouldIncludeFile(relative4, [...languages])) {
2000
- seen.add(full);
2001
- out.push(full);
2002
- }
2003
- }
2442
+ function diskBuildDirs(real, canonical) {
2443
+ if (!real.directoryExistsSync(canonical)) return [];
2444
+ return real.readDirSync(canonical).filter((e) => e.isDirectory && isExcludedDir2(path9.basename(e.name)));
2445
+ }
2446
+ function treeEntry(at, kind, exists) {
2447
+ if (kind !== "link") return { name: at, isFile: kind === "file", isDirectory: kind === "dir", isSymlink: false };
2448
+ return { name: at, isFile: exists.file(at), isDirectory: exists.dir(at), isSymlink: true };
2449
+ }
2450
+ function readDirAt(tree, real, abs, exists) {
2451
+ const at = tree.locate(abs, "dir");
2452
+ const asAsked = (entry) => ({ ...entry, name: path9.join(path9.resolve(abs), path9.basename(entry.name)) });
2453
+ if (at.origin === "missing") return missing(abs, "dir");
2454
+ if (at.origin === "disk") return real.readDirSync(at.canonical).map(asAsked);
2455
+ const entries = /* @__PURE__ */ new Map();
2456
+ for (const [name, kind] of tree.children(at.rel)) {
2457
+ entries.set(name, treeEntry(path9.join(path9.resolve(abs), name), kind, exists));
2458
+ }
2459
+ for (const entry of diskBuildDirs(real, at.canonical)) {
2460
+ const name = path9.basename(entry.name);
2461
+ if (!entries.has(name)) entries.set(name, asAsked(entry));
2004
2462
  }
2463
+ return [...entries.values()];
2464
+ }
2465
+ function treeAnswers(tree, real) {
2466
+ const fileExists = (abs) => {
2467
+ const at = tree.locate(abs, "file");
2468
+ return at.origin === "tree" || at.origin === "disk" && real.fileExistsSync(at.canonical);
2469
+ };
2470
+ const directoryExists = (abs) => {
2471
+ const at = tree.locate(abs, "dir");
2472
+ return at.origin === "tree" || at.origin === "disk" && real.directoryExistsSync(at.canonical);
2473
+ };
2474
+ return {
2475
+ fileExists,
2476
+ readFile: (abs) => {
2477
+ const at = tree.locate(abs, "file");
2478
+ if (at.origin === "tree") return tree.read(at.rel);
2479
+ return at.origin === "disk" ? real.readFileSync(at.canonical, "utf-8") : missing(abs, "file");
2480
+ },
2481
+ directoryExists,
2482
+ readDir: (abs) => readDirAt(tree, real, abs, { file: fileExists, dir: directoryExists }),
2483
+ // Like the real host, realpath of an absent path throws.
2484
+ realpath: (abs) => fileExists(abs) || directoryExists(abs) ? tree.locate(abs, "file").canonical : missing(abs, "file")
2485
+ };
2486
+ }
2487
+ function canonicalToplevel(repoRoot) {
2488
+ const toplevel = detectGitToplevel(path9.resolve(repoRoot));
2489
+ if (toplevel === null) throw new Error(`${repoRoot} is not inside a git repository`);
2490
+ return realpathSync(toplevel);
2491
+ }
2492
+ function gitTreeSource(repoRoot, rev) {
2493
+ const canonicalRoot = canonicalToplevel(repoRoot);
2494
+ const { commit, commitEpoch } = resolveCommit(canonicalRoot, rev);
2495
+ const tree = new CommitTree(canonicalRoot, commit);
2496
+ const real = workingTreeSource().fileSystem;
2497
+ const answers = treeAnswers(tree, real);
2498
+ let fileSystem;
2499
+ return {
2500
+ listFiles: async (rootDirs, languages) => listTreeFiles(tree, rootDirs, languages),
2501
+ readFile: answers.readFile,
2502
+ // Like `existsSync` in the working-tree source, true for a dir as well as a file.
2503
+ fileExists: (abs) => answers.fileExists(abs) || answers.directoryExists(abs),
2504
+ get fileSystem() {
2505
+ fileSystem ??= overlayHost(answers, real);
2506
+ return fileSystem;
2507
+ },
2508
+ revision: { repoRoot: canonicalRoot, commit, commitEpoch }
2509
+ };
2510
+ }
2511
+ function rebaseOntoRoot(abs, repoRoot) {
2512
+ const rest = [];
2513
+ for (let at = path9.resolve(abs); path9.dirname(at) !== at; at = path9.dirname(at)) {
2514
+ if (realpathOrNull(at) === repoRoot) return path9.join(repoRoot, ...rest.reverse());
2515
+ rest.push(path9.basename(at));
2516
+ }
2517
+ return canonicalize(abs);
2518
+ }
2519
+ function realpathOrNull(abs) {
2520
+ try {
2521
+ return realpathSync(abs);
2522
+ } catch {
2523
+ return null;
2524
+ }
2525
+ }
2526
+ function rootsAtRevision(paths, revision) {
2527
+ const idRoot = realpathSync(revision.repoRoot);
2528
+ return { rootDirs: paths.map((p) => rebaseOntoRoot(p, idRoot)), idRoot };
2005
2529
  }
2006
2530
 
2007
2531
  // src/barrel-resolve.ts
@@ -2010,9 +2534,9 @@ function edgeWeight(e) {
2010
2534
  return typeof w === "number" && w > 0 ? w : 1;
2011
2535
  }
2012
2536
  function pruneDanglingReferences(nodes, edges) {
2013
- for (const [key, edge] of edges) {
2014
- const symbolLayer = edge.kind === "references" || edge.kind === "calls";
2015
- if (symbolLayer && (!nodes.has(edge.dstId) || !nodes.has(edge.srcId))) edges.delete(key);
2537
+ for (const [key, edge2] of edges) {
2538
+ const symbolLayer = edge2.kind === "references" || edge2.kind === "calls";
2539
+ if (symbolLayer && (!nodes.has(edge2.dstId) || !nodes.has(edge2.srcId))) edges.delete(key);
2016
2540
  }
2017
2541
  }
2018
2542
  function resolveBarrelEdges(nodes, edges) {
@@ -2027,7 +2551,7 @@ function resolveBarrelEdges(nodes, edges) {
2027
2551
  forwards.set(e.srcId, arr);
2028
2552
  }
2029
2553
  const cache = /* @__PURE__ */ new Map();
2030
- function resolve4(id, seen) {
2554
+ function resolve5(id, seen) {
2031
2555
  if (!barrels.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
2032
2556
  const cached = cache.get(id);
2033
2557
  if (cached) return cached;
@@ -2038,7 +2562,7 @@ function resolveBarrelEdges(nodes, edges) {
2038
2562
  const nextSeen = new Set(seen).add(id);
2039
2563
  for (const t of targets) {
2040
2564
  const share = t.weight / total;
2041
- for (const [real, frac] of resolve4(t.dst, nextSeen)) {
2565
+ for (const [real, frac] of resolve5(t.dst, nextSeen)) {
2042
2566
  out.set(real, (out.get(real) ?? 0) + share * frac);
2043
2567
  }
2044
2568
  }
@@ -2053,7 +2577,7 @@ function resolveBarrelEdges(nodes, edges) {
2053
2577
  continue;
2054
2578
  }
2055
2579
  const w = edgeWeight(e);
2056
- for (const [real, frac] of resolve4(e.dstId, /* @__PURE__ */ new Set())) {
2580
+ for (const [real, frac] of resolve5(e.dstId, /* @__PURE__ */ new Set())) {
2057
2581
  if (real === e.srcId) continue;
2058
2582
  resolved.push({
2059
2583
  ...e,
@@ -2067,7 +2591,6 @@ function resolveBarrelEdges(nodes, edges) {
2067
2591
 
2068
2592
  // src/incremental.ts
2069
2593
  import { createHash } from "crypto";
2070
- import * as fs2 from "fs/promises";
2071
2594
  import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
2072
2595
 
2073
2596
  // src/analysis/comment-lines.ts
@@ -2298,6 +2821,25 @@ function isLoggingCall(node) {
2298
2821
  return false;
2299
2822
  }
2300
2823
 
2824
+ // src/analysis/jsx-metrics.ts
2825
+ function rendersElement(node) {
2826
+ if (node.type === "jsx_self_closing_element") return true;
2827
+ if (node.type !== "jsx_element") return false;
2828
+ return node.childForFieldName("open_tag")?.childForFieldName("name") != null;
2829
+ }
2830
+ function jsxDepthOf(root, opensFunction) {
2831
+ let max = 0;
2832
+ const visit = (node, depth) => {
2833
+ const own = rendersElement(node) ? depth + 1 : depth;
2834
+ if (own > max) max = own;
2835
+ for (const child of node.namedChildren) {
2836
+ if (child && !opensFunction(child)) visit(child, own);
2837
+ }
2838
+ };
2839
+ visit(root, 0);
2840
+ return max;
2841
+ }
2842
+
2301
2843
  // src/cognitive-complexity.ts
2302
2844
  function cognitiveComplexityOf(body, language) {
2303
2845
  const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
@@ -2331,9 +2873,8 @@ var TS_NESTING = /* @__PURE__ */ new Set([
2331
2873
  "do_statement",
2332
2874
  "switch_statement",
2333
2875
  "catch_clause",
2334
- "function_declaration",
2335
- "method_definition",
2336
- "arrow_function"
2876
+ ...TS_FUNCTION_DECL_TYPES,
2877
+ ...TS_BOUND_FUNCTION_TYPES
2337
2878
  ]);
2338
2879
  var PY_NESTING = /* @__PURE__ */ new Set([
2339
2880
  "if_statement",
@@ -2425,7 +2966,7 @@ function computeLcomMetrics(file, fileId2) {
2425
2966
  return out;
2426
2967
  }
2427
2968
  function collectClasses(file) {
2428
- const isClass = file.language === "python" ? (n) => n.type === "class_definition" : (n) => n.type === "class_declaration" || n.type === "class_expression";
2969
+ const isClass = file.language === "python" ? (n) => n.type === "class_definition" : (n) => TS_CLASS_TYPES.has(n.type);
2429
2970
  const out = [];
2430
2971
  const visit = (node) => {
2431
2972
  if (isClass(node)) out.push(node);
@@ -2463,14 +3004,21 @@ function collectPythonMethods(classNode) {
2463
3004
  if (!body) return [];
2464
3005
  const out = [];
2465
3006
  for (const child of body.namedChildren) {
2466
- if (!child || child.type !== "function_definition") continue;
2467
- const name = child.childForFieldName("name")?.text;
3007
+ const fn = child ? pythonMethodNode(child) : null;
3008
+ if (!fn) continue;
3009
+ const name = fn.childForFieldName("name")?.text;
2468
3010
  if (!name || name === "__init__") continue;
2469
- if (isPythonStaticOrClassMethod(child)) continue;
2470
- out.push({ name, usedNames: thisRefs(child, "self") });
3011
+ out.push({ name, usedNames: thisRefs(fn, "self") });
2471
3012
  }
2472
3013
  return out;
2473
3014
  }
3015
+ function pythonMethodNode(stmt) {
3016
+ if (stmt.type === "function_definition") return stmt;
3017
+ if (stmt.type !== "decorated_definition") return null;
3018
+ if (isPythonStaticOrClassMethod(stmt)) return null;
3019
+ const def = stmt.childForFieldName("definition");
3020
+ return def?.type === "function_definition" ? def : null;
3021
+ }
2474
3022
  function methodName(methodNode) {
2475
3023
  const nameNode = methodNode.childForFieldName("name");
2476
3024
  return nameNode?.text ?? null;
@@ -2481,16 +3029,10 @@ function hasStaticModifier(methodNode) {
2481
3029
  }
2482
3030
  return false;
2483
3031
  }
2484
- function isPythonStaticOrClassMethod(funcNode) {
2485
- let prev = funcNode.previousNamedSibling;
2486
- while (prev && prev.type === "decorator") {
2487
- const text = prev.text;
2488
- if (text.includes("@staticmethod") || text.includes("@classmethod")) {
2489
- return true;
2490
- }
2491
- prev = prev.previousNamedSibling;
2492
- }
2493
- return false;
3032
+ function isPythonStaticOrClassMethod(decorated) {
3033
+ return decorated.namedChildren.some(
3034
+ (c) => c?.type === "decorator" && (c.text === "@staticmethod" || c.text === "@classmethod")
3035
+ );
2494
3036
  }
2495
3037
  function thisRefs(methodNode, selfKeyword) {
2496
3038
  const out = /* @__PURE__ */ new Set();
@@ -2635,6 +3177,7 @@ var SYMBOL_METRICS = [
2635
3177
  { name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
2636
3178
  { name: "symbol_loc", stat: "loc", unit: "lines" },
2637
3179
  { name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
3180
+ { name: "symbol_jsx_depth", stat: "jsxDepth", unit: "count", omitZero: true },
2638
3181
  { name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
2639
3182
  { name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
2640
3183
  { name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
@@ -2659,6 +3202,7 @@ function symbolMetrics(fileId2, stats, symbolNames) {
2659
3202
  for (const [name, values] of byName) {
2660
3203
  const nodeId = symbolId(fileId2, name);
2661
3204
  for (const m of SYMBOL_METRICS) {
3205
+ if (m.omitZero && values[m.stat] === 0) continue;
2662
3206
  out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
2663
3207
  }
2664
3208
  }
@@ -2673,10 +3217,6 @@ function functionShapeStats(fn, body, lines) {
2673
3217
  }
2674
3218
 
2675
3219
  // src/source-metrics.ts
2676
- var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
2677
- "function_declaration",
2678
- "method_definition"
2679
- ]);
2680
3220
  var PY_FUNCTION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
2681
3221
  var TS_NESTING_TYPES = /* @__PURE__ */ new Set([
2682
3222
  "if_statement",
@@ -2719,6 +3259,7 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
2719
3259
  "cognitive_max",
2720
3260
  "cognitive_sum",
2721
3261
  "max_nesting_depth",
3262
+ "jsx_depth_max",
2722
3263
  "class_count",
2723
3264
  "lcom4_max",
2724
3265
  ...EXCEPTION_METRIC_NAMES,
@@ -2729,10 +3270,10 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
2729
3270
  ...SYMBOL_METRIC_NAMES
2730
3271
  ]);
2731
3272
  var EMPTY_NAMES = /* @__PURE__ */ new Set();
2732
- function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
3273
+ function computeSourceMetrics(files, fileIdOf2, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
2733
3274
  const out = [];
2734
3275
  for (const file of files) {
2735
- const id = fileIdOf(file.filePath);
3276
+ const id = fileIdOf2(file.filePath);
2736
3277
  out.push(...metricsForFile(id, file, symbolNamesByFile2.get(id) ?? EMPTY_NAMES));
2737
3278
  }
2738
3279
  return out;
@@ -2780,6 +3321,8 @@ function metricsForFile(nodeId, file, symbolNames) {
2780
3321
  unit: "count"
2781
3322
  });
2782
3323
  }
3324
+ const jsxDepthMax = Math.max(jsxDepthIn(file, file.tree.rootNode), ...stats.map((s) => s.jsxDepth));
3325
+ if (jsxDepthMax > 0) out.push({ nodeId, name: "jsx_depth_max", value: jsxDepthMax, unit: "count" });
2783
3326
  out.push(...symbolMetrics(nodeId, stats, symbolNames));
2784
3327
  out.push(...computeLcomMetrics(file, nodeId));
2785
3328
  out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
@@ -2790,7 +3333,7 @@ function countLoc(content) {
2790
3333
  }
2791
3334
  function analyzeFunctions(file) {
2792
3335
  const stats = [];
2793
- const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
3336
+ const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_DECL_TYPES;
2794
3337
  const lines = file.content.split("\n");
2795
3338
  walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
2796
3339
  const fn = functionAt(node, fnTypes);
@@ -2800,6 +3343,7 @@ function analyzeFunctions(file) {
2800
3343
  cyclomatic: cyclomaticOf(fn.body, file.language),
2801
3344
  cognitive: cognitiveComplexityOf(fn.body, file.language),
2802
3345
  nestingDepth: nestingDepthOf(fn.body, file.language, 0),
3346
+ jsxDepth: jsxDepthIn(file, fn.body),
2803
3347
  loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
2804
3348
  ...functionShapeStats(fn.node, fn.body, lines)
2805
3349
  });
@@ -2811,13 +3355,17 @@ function functionAt(node, fnTypes) {
2811
3355
  const body = node.childForFieldName("body");
2812
3356
  return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
2813
3357
  }
2814
- if ((node.type === "arrow_function" || node.type === "function_expression") && node.parent?.type === "variable_declarator") {
3358
+ if (TS_BOUND_FUNCTION_TYPES.has(node.type) && node.parent?.type === "variable_declarator") {
2815
3359
  const body = node.childForFieldName("body");
2816
3360
  if (!body) return null;
2817
3361
  return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
2818
3362
  }
2819
3363
  return null;
2820
3364
  }
3365
+ function jsxDepthIn(file, root) {
3366
+ if (file.language === "python") return 0;
3367
+ return jsxDepthOf(root, (node) => functionAt(node, TS_FUNCTION_DECL_TYPES) !== null);
3368
+ }
2821
3369
  function nestingDepthOf(node, language, depth) {
2822
3370
  const nestingTypes = language === "python" ? PY_NESTING_TYPES : TS_NESTING_TYPES;
2823
3371
  let maxDepth = depth;
@@ -3010,12 +3558,12 @@ var TERMINALS2 = /* @__PURE__ */ new Set([
3010
3558
  "break_statement",
3011
3559
  "continue_statement"
3012
3560
  ]);
3013
- function computeDeadCodeMetrics(files, fileIdOf) {
3561
+ function computeDeadCodeMetrics(files, fileIdOf2) {
3014
3562
  const out = [];
3015
3563
  for (const file of files) {
3016
3564
  const counts = deadCodeOf(file);
3017
3565
  if (!counts) continue;
3018
- const id = fileIdOf(file.filePath);
3566
+ const id = fileIdOf2(file.filePath);
3019
3567
  const { unreachable, locals, params } = counts;
3020
3568
  if (unreachable > 0) {
3021
3569
  out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
@@ -3178,12 +3726,12 @@ var SEARCH_METHODS = /* @__PURE__ */ new Set([
3178
3726
  "every",
3179
3727
  "filter"
3180
3728
  ]);
3181
- function computeGrowthRiskMetrics(files, fileIdOf) {
3729
+ function computeGrowthRiskMetrics(files, fileIdOf2) {
3182
3730
  const out = [];
3183
3731
  for (const file of files) {
3184
3732
  const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
3185
3733
  const root = file.tree.rootNode;
3186
- const id = fileIdOf(file.filePath);
3734
+ const id = fileIdOf2(file.filePath);
3187
3735
  const depth = maxLoopDepth(root, loopTypes, 0);
3188
3736
  if (depth >= 2) {
3189
3737
  out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
@@ -3254,8 +3802,8 @@ function reconstructFragment(repoRoot, absPath, fileId2, basis) {
3254
3802
  nodes.push(...basis.symbolsByFile.get(fileId2) ?? []);
3255
3803
  const edges = basis.edgesBySrc.get(fileId2) ?? [];
3256
3804
  const seenExternals = /* @__PURE__ */ new Set();
3257
- for (const edge of edges) {
3258
- const dst = basis.nodesById.get(edge.dstId);
3805
+ for (const edge2 of edges) {
3806
+ const dst = basis.nodesById.get(edge2.dstId);
3259
3807
  if (dst?.kind === "external" && !seenExternals.has(dst.id)) {
3260
3808
  seenExternals.add(dst.id);
3261
3809
  nodes.push({ id: dst.id, kind: "external", name: dst.name });
@@ -3301,12 +3849,12 @@ function sourceLanguage(filePath) {
3301
3849
  const language = getLanguageFromPath2(filePath);
3302
3850
  return language === "typescript" || language === "python" ? language : null;
3303
3851
  }
3304
- async function readSourceFiles(filePaths) {
3852
+ async function readSourceFiles(filePaths, source = workingTreeSource()) {
3305
3853
  const out = [];
3306
3854
  for (const filePath of filePaths) {
3307
3855
  const language = sourceLanguage(filePath);
3308
3856
  if (language === null) continue;
3309
- const content = await fs2.readFile(filePath, "utf-8");
3857
+ const content = source.readFile(filePath);
3310
3858
  out.push({ filePath, language, content, hash: hashContent(content) });
3311
3859
  }
3312
3860
  return out;
@@ -3456,21 +4004,21 @@ function resolveRelativeId(srcFileId, specifier, fileIds) {
3456
4004
  }
3457
4005
  return null;
3458
4006
  }
3459
- function targetFileId(edge) {
3460
- return parseSymbolId(edge.dstId)?.fileId ?? edge.dstId;
4007
+ function targetFileId(edge2) {
4008
+ return parseSymbolId(edge2.dstId)?.fileId ?? edge2.dstId;
3461
4009
  }
3462
4010
  function computeDeltaAffected(basis, currentFileIds) {
3463
4011
  const affected = /* @__PURE__ */ new Set();
3464
- const removed = /* @__PURE__ */ new Set();
4012
+ const removed2 = /* @__PURE__ */ new Set();
3465
4013
  for (const id of basis.fingerprints.keys()) {
3466
- if (!currentFileIds.has(id)) removed.add(id);
4014
+ if (!currentFileIds.has(id)) removed2.add(id);
3467
4015
  }
3468
- const added = currentFileIds.size !== basis.fingerprints.size || removed.size > 0;
4016
+ const added = currentFileIds.size !== basis.fingerprints.size || removed2.size > 0;
3469
4017
  if (!added) return affected;
3470
4018
  for (const [srcId, edges] of basis.edgesBySrc) {
3471
4019
  if (!currentFileIds.has(srcId)) continue;
3472
- for (const edge of edges) {
3473
- if (isEdgeAffected(edge, srcId, removed, currentFileIds)) {
4020
+ for (const edge2 of edges) {
4021
+ if (isEdgeAffected(edge2, srcId, removed2, currentFileIds)) {
3474
4022
  affected.add(srcId);
3475
4023
  break;
3476
4024
  }
@@ -3478,11 +4026,11 @@ function computeDeltaAffected(basis, currentFileIds) {
3478
4026
  }
3479
4027
  return affected;
3480
4028
  }
3481
- function isEdgeAffected(edge, srcId, removed, currentFileIds) {
3482
- if (removed.has(targetFileId(edge))) return true;
3483
- const specifier = edge.attrs?.specifier;
4029
+ function isEdgeAffected(edge2, srcId, removed2, currentFileIds) {
4030
+ if (removed2.has(targetFileId(edge2))) return true;
4031
+ const specifier = edge2.attrs?.specifier;
3484
4032
  if (!specifier || !isRelative(specifier)) return false;
3485
- return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge);
4033
+ return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge2);
3486
4034
  }
3487
4035
 
3488
4036
  // src/metrics.ts
@@ -3562,12 +4110,12 @@ function computeCallMetrics(nodes, edges) {
3562
4110
  }
3563
4111
  function collectInbound(edges) {
3564
4112
  const inbound = /* @__PURE__ */ new Map();
3565
- for (const edge of edges) {
3566
- if (edge.kind !== "calls") continue;
3567
- let entry = inbound.get(edge.dstId);
3568
- if (!entry) inbound.set(edge.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
3569
- if (edge.srcId !== edge.dstId) entry.callers.add(edge.srcId);
3570
- entry.sites.push(...edge.attrs?.sites ?? []);
4113
+ for (const edge2 of edges) {
4114
+ if (edge2.kind !== "calls") continue;
4115
+ let entry = inbound.get(edge2.dstId);
4116
+ if (!entry) inbound.set(edge2.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
4117
+ if (edge2.srcId !== edge2.dstId) entry.callers.add(edge2.srcId);
4118
+ entry.sites.push(...edge2.attrs?.sites ?? []);
3571
4119
  }
3572
4120
  return inbound;
3573
4121
  }
@@ -3594,103 +4142,6 @@ function argumentAt(site, name, position) {
3594
4142
  return ABSENT;
3595
4143
  }
3596
4144
 
3597
- // src/analysis/test-linker.ts
3598
- var DEFAULT_MIN_CO_EDIT_COUNT = 2;
3599
- var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
3600
- var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
3601
- function pathCandidates(testId) {
3602
- const out = /* @__PURE__ */ new Set();
3603
- const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
3604
- if (deInfixed !== testId) out.add(deInfixed);
3605
- const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
3606
- if (deDir !== testId) out.add(deDir);
3607
- return [...out];
3608
- }
3609
- function splitFileIds(nodes) {
3610
- const testIds = [];
3611
- const nonTestIds = /* @__PURE__ */ new Set();
3612
- for (const n of nodes) {
3613
- if (n.kind !== "file") continue;
3614
- if (n.role === "test") testIds.push(n.id);
3615
- else nonTestIds.add(n.id);
3616
- }
3617
- return { testIds, nonTestIds };
3618
- }
3619
- function linkTestsToSources(nodes, coEditPairs, options = {}) {
3620
- const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
3621
- const { testIds, nonTestIds } = splitFileIds(nodes);
3622
- const links = [];
3623
- for (const testId of testIds) {
3624
- const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
3625
- if (matched.length > 0) {
3626
- for (const sourceId of matched) {
3627
- links.push({ testId, sourceId, method: "path" });
3628
- }
3629
- continue;
3630
- }
3631
- const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
3632
- if (partner) {
3633
- links.push({ testId, sourceId: partner.partner, method: "coedit" });
3634
- }
3635
- }
3636
- return links;
3637
- }
3638
- function testCoverageCountMetrics(links) {
3639
- const testsBySource = groupTestsBySource(links);
3640
- const out = [];
3641
- for (const [sourceId, tests] of testsBySource) {
3642
- out.push({
3643
- nodeId: sourceId,
3644
- name: "linked_test_count",
3645
- value: tests.size,
3646
- unit: "count"
3647
- });
3648
- }
3649
- return out;
3650
- }
3651
- function groupTestsBySource(links) {
3652
- const out = /* @__PURE__ */ new Map();
3653
- for (const l of links) {
3654
- let set = out.get(l.sourceId);
3655
- if (!set) {
3656
- set = /* @__PURE__ */ new Set();
3657
- out.set(l.sourceId, set);
3658
- }
3659
- set.add(l.testId);
3660
- }
3661
- return out;
3662
- }
3663
-
3664
- // src/history-recency.ts
3665
- var DAY_SECONDS = 86400;
3666
- function windowSuffix(window) {
3667
- return window === "lifetime" ? "lifetime" : `${window}d`;
3668
- }
3669
- function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
3670
- const out = [];
3671
- const ageEmitted = /* @__PURE__ */ new Set();
3672
- for (const [windowDays, ids] of churnedIdsByWindow) {
3673
- const suffix = windowSuffix(windowDays);
3674
- for (const id of ids) {
3675
- const seen = firstSeen.get(id);
3676
- let recency = 1;
3677
- if (seen !== void 0) {
3678
- const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
3679
- if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
3680
- if (!ageEmitted.has(id)) {
3681
- out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
3682
- ageEmitted.add(id);
3683
- }
3684
- }
3685
- out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
3686
- }
3687
- }
3688
- return out;
3689
- }
3690
- function round3(n) {
3691
- return Math.round(n * 1e3) / 1e3;
3692
- }
3693
-
3694
4145
  // src/history-metrics.ts
3695
4146
  var DEFAULT_CHURN_WINDOWS = [30, 90, 180];
3696
4147
  function resolveChurnWindows(requested, primaryWindow, includeLifetime) {
@@ -3702,7 +4153,12 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
3702
4153
  const knownPaths = collectFileIds(nodes);
3703
4154
  const primaryWindow = options.churnWindowDays ?? 30;
3704
4155
  const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
3705
- const wide = loadChurnEntries({ repoRoot: idRoot, windowDays: windows[windows.length - 1] });
4156
+ const wide = loadChurnEntries({
4157
+ repoRoot: idRoot,
4158
+ windowDays: windows[windows.length - 1],
4159
+ rev: options.rev,
4160
+ untilEpoch: options.nowEpoch
4161
+ });
3706
4162
  if (wide === null) return null;
3707
4163
  const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
3708
4164
  const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
@@ -3710,7 +4166,7 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
3710
4166
  const metrics = [
3711
4167
  ...churnMetrics(churnByWindow),
3712
4168
  ...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
3713
- ...recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch)
4169
+ ...recencyMetrics({ idRoot, rev: options.rev, knownPaths, nowEpoch }, churnByWindow)
3714
4170
  ];
3715
4171
  if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
3716
4172
  return { metrics, primaryEntries };
@@ -3777,14 +4233,14 @@ function mergeAuthorChurn(tests, linesByTest) {
3777
4233
  }
3778
4234
  return byAuthor;
3779
4235
  }
3780
- function recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch) {
4236
+ function recencyMetrics(at, churnByWindow) {
3781
4237
  const churnedByWindow = /* @__PURE__ */ new Map();
3782
4238
  for (const [window, byPath] of churnByWindow) {
3783
4239
  if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
3784
4240
  }
3785
4241
  if (churnedByWindow.size === 0) return [];
3786
- const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
3787
- return computeRecencyWindows(firstSeen, churnedByWindow, nowEpoch);
4242
+ const firstSeen = loadFileFirstSeen({ repoRoot: at.idRoot, knownPaths: at.knownPaths, rev: at.rev }) ?? /* @__PURE__ */ new Map();
4243
+ return computeRecencyWindows(firstSeen, churnedByWindow, at.nowEpoch);
3788
4244
  }
3789
4245
 
3790
4246
  // src/index-metrics.ts
@@ -3828,17 +4284,17 @@ function computeTestCoverage(nodes, entries, windowDays) {
3828
4284
  }
3829
4285
 
3830
4286
  // src/merge.ts
3831
- function edgeKey(edge) {
3832
- return JSON.stringify([edge.srcId, edge.dstId, edge.kind]);
4287
+ function edgeKey(edge2) {
4288
+ return JSON.stringify([edge2.srcId, edge2.dstId, edge2.kind]);
3833
4289
  }
3834
4290
  function mergeFragments(fragments) {
3835
4291
  const nodes = /* @__PURE__ */ new Map();
3836
4292
  const edges = /* @__PURE__ */ new Map();
3837
4293
  for (const fragment of fragments) {
3838
4294
  for (const node of fragment.nodes) if (!nodes.has(node.id)) nodes.set(node.id, node);
3839
- for (const edge of fragment.edges) {
3840
- const key = edgeKey(edge);
3841
- if (!edges.has(key)) edges.set(key, edge);
4295
+ for (const edge2 of fragment.edges) {
4296
+ const key = edgeKey(edge2);
4297
+ if (!edges.has(key)) edges.set(key, edge2);
3842
4298
  }
3843
4299
  }
3844
4300
  return { nodes, edges };
@@ -3882,18 +4338,18 @@ function qualifiedByName(file) {
3882
4338
  }
3883
4339
 
3884
4340
  // src/indexer.ts
3885
- var INDEX_VERSION = "0.18.0";
4341
+ var INDEX_VERSION = "0.22.0";
3886
4342
  var LANGUAGES = ["typescript", "python"];
3887
4343
  function canonicalizePath(p) {
3888
4344
  try {
3889
- return realpathSync(p);
4345
+ return realpathSync2(p);
3890
4346
  } catch {
3891
4347
  return p;
3892
4348
  }
3893
4349
  }
3894
4350
  function normalizePaths(paths) {
3895
4351
  if (paths.length === 0) throw new Error("indexPaths requires at least one path");
3896
- return [...new Set(paths.map((p) => path8.resolve(p)))];
4352
+ return [...new Set(paths.map((p) => path10.resolve(p)))];
3897
4353
  }
3898
4354
  function countByKind(items) {
3899
4355
  const out = {};
@@ -3916,8 +4372,9 @@ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
3916
4372
  const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
3917
4373
  return qualifiedSymbolAliases(files, nodeIds);
3918
4374
  }
3919
- function resolveRoots(paths) {
4375
+ function resolveRoots(paths, source) {
3920
4376
  const raw = normalizePaths(paths);
4377
+ if (source.revision) return rootsAtRevision(raw, source.revision);
3921
4378
  const gitToplevel = detectGitToplevel(raw[0]);
3922
4379
  const rootDirs = gitToplevel !== null ? raw.map(canonicalizePath) : raw;
3923
4380
  return { rootDirs, idRoot: gitToplevel ?? rootDirs[0] };
@@ -3929,16 +4386,19 @@ async function parseAll(toParse) {
3929
4386
  }
3930
4387
  return parsed;
3931
4388
  }
3932
- function historyOptions(options) {
4389
+ function historyOptions(options, source) {
3933
4390
  return {
3934
4391
  churnWindowDays: options.churnWindowDays,
3935
4392
  churnWindows: options.churnWindows,
3936
- includeLifetime: options.lifetime === true
4393
+ includeLifetime: options.lifetime === true,
4394
+ rev: source.revision?.commit,
4395
+ nowEpoch: source.revision?.commitEpoch
3937
4396
  };
3938
4397
  }
3939
4398
  async function indexPaths(store, options) {
3940
- const { rootDirs, idRoot } = resolveRoots(options.paths);
3941
- const readFiles = await readSourceFiles(await walkSourceFiles(rootDirs, LANGUAGES));
4399
+ const source = options.source ?? workingTreeSource();
4400
+ const { rootDirs, idRoot } = resolveRoots(options.paths, source);
4401
+ const readFiles = await readSourceFiles(await source.listFiles(rootDirs, LANGUAGES), source);
3942
4402
  const reuse = options.incremental !== false ? loadReuseBasis(store, INDEX_VERSION) : null;
3943
4403
  const currentFileIds = new Set(readFiles.map((rf) => fileId(idRoot, rf.filePath)));
3944
4404
  const affected = reuse ? computeDeltaAffected(reuse, currentFileIds) : /* @__PURE__ */ new Set();
@@ -3952,10 +4412,11 @@ async function indexPaths(store, options) {
3952
4412
  parsedByPath,
3953
4413
  reuse,
3954
4414
  cosmeticFileIds: classified.cosmeticFileIds,
3955
- extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig })
4415
+ extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig, source })
3956
4416
  })
3957
4417
  );
3958
- const annotated = annotateRoles([...accumulator.nodes.values()], computeRoleHints(readFiles, idRoot, fileId));
4418
+ const roleHints = computeRoleHints(readFiles, idRoot, fileId, source);
4419
+ const annotated = annotateRoles([...accumulator.nodes.values()], roleHints);
3959
4420
  accumulator.nodes = new Map(annotated.map((n) => [n.id, n]));
3960
4421
  pruneDanglingReferences(accumulator.nodes, accumulator.edges);
3961
4422
  const metrics = options.computeMetrics === false ? [] : buildIndexerMetrics({
@@ -3964,19 +4425,23 @@ async function indexPaths(store, options) {
3964
4425
  parsedFiles: [...parsedByPath.values()],
3965
4426
  reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
3966
4427
  idRoot,
3967
- history: options.computeChurn === false ? void 0 : historyOptions(options)
4428
+ history: options.computeChurn === false ? void 0 : historyOptions(options, source)
3968
4429
  });
3969
- const rootDir = rootDirs[0];
4430
+ const rootDir = source.revision ? idRoot : rootDirs[0];
3970
4431
  const ref = options.ref ?? "wd";
3971
- const bridge = computeAliasBridge(store, { ...options, rootDir, idRoot, ref, nodes: accumulator.nodes });
4432
+ const commitHash = options.commitHash ?? source.revision?.commit ?? detectGitHead(rootDir) ?? void 0;
4433
+ const bridge = computeAliasBridge(store, { ...options, rootDir, idRoot, ref, commitHash, nodes: accumulator.nodes });
3972
4434
  const aliases = [
3973
4435
  ...bridge.aliases,
3974
4436
  ...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
3975
4437
  ];
3976
- const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
3977
4438
  const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
3978
- const snapshotId = persist(store, snapshot, accumulator, aliases, metrics);
3979
- store.insertFingerprints(snapshotId, buildFingerprints(readFiles, idRoot, classified.structuralByFileId));
4439
+ const fingerprints = buildFingerprints(readFiles, idRoot, classified.structuralByFileId);
4440
+ const snapshotId = store.atomically(() => {
4441
+ const id = persist(store, snapshot, accumulator, aliases, metrics);
4442
+ store.insertFingerprints(id, fingerprints);
4443
+ return id;
4444
+ });
3980
4445
  return {
3981
4446
  snapshotId,
3982
4447
  files: readFiles.length,
@@ -3993,14 +4458,14 @@ async function indexPaths(store, options) {
3993
4458
  }
3994
4459
 
3995
4460
  // src/index.ts
3996
- import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile2, shouldIncludeFile as shouldIncludeFile2 } from "@titan-design/code-parser";
4461
+ import { getLanguageFromPath as getLanguageFromPath3, getSupportedLanguages, parseFile as parseFile3, shouldIncludeFile as shouldIncludeFile3 } from "@titan-design/code-parser";
3997
4462
 
3998
4463
  // src/extractors/deep-ast.ts
3999
4464
  import { readFileSync as readFileSync2 } from "fs";
4000
- import * as path9 from "path";
4465
+ import * as path11 from "path";
4001
4466
  import {
4002
4467
  Node as Node6,
4003
- Project as Project2,
4468
+ Project as Project4,
4004
4469
  ScriptTarget as ScriptTarget2
4005
4470
  } from "ts-morph";
4006
4471
  function computeDeepAst(input) {
@@ -4012,11 +4477,11 @@ function computeDeepAst(input) {
4012
4477
  function loadSourceFile(absPath) {
4013
4478
  try {
4014
4479
  const text = readFileSync2(absPath, "utf8");
4015
- const project = new Project2({
4480
+ const project = new Project4({
4016
4481
  useInMemoryFileSystem: true,
4017
4482
  compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
4018
4483
  });
4019
- return project.createSourceFile(`f${path9.extname(absPath) || ".ts"}`, text);
4484
+ return project.createSourceFile(`f${path11.extname(absPath) || ".ts"}`, text);
4020
4485
  } catch {
4021
4486
  return null;
4022
4487
  }
@@ -4194,11 +4659,11 @@ function innermostContaining(symbols, line) {
4194
4659
  }
4195
4660
  return best;
4196
4661
  }
4197
- function attributeCoverage(coverage, fileIdOf, symbolsByFile) {
4662
+ function attributeCoverage(coverage, fileIdOf2, symbolsByFile) {
4198
4663
  const out = [];
4199
4664
  for (const [absPath, fc] of Object.entries(coverage)) {
4200
4665
  if (!isFileCoverage(fc)) continue;
4201
- const fileId2 = fileIdOf(absPath);
4666
+ const fileId2 = fileIdOf2(absPath);
4202
4667
  if (!fileId2) continue;
4203
4668
  const fnIds = Object.keys(fc.fnMap);
4204
4669
  if (fnIds.length === 0) continue;
@@ -4229,142 +4694,196 @@ function pctMetric(nodeId, covered, total) {
4229
4694
  };
4230
4695
  }
4231
4696
 
4232
- // src/analysis/pagerank.ts
4233
- var DEFAULT_DAMPING = 0.85;
4234
- var DEFAULT_TOLERANCE = 1e-6;
4235
- var DEFAULT_MAX_ITERS = 100;
4236
- var DEFAULT_EDGE_WEIGHTS = {
4237
- imports: 1,
4238
- "re-exports": 0.5,
4239
- calls: 1.5,
4240
- extends: 1,
4241
- implements: 1,
4242
- references: 1,
4243
- "depends-on": 1
4697
+ // src/analysis/context/minify.ts
4698
+ import { parseFile as parseFile2 } from "@titan-design/code-parser";
4699
+ var IMPORT_TYPES = {
4700
+ typescript: /* @__PURE__ */ new Set(["import_statement"]),
4701
+ tsx: /* @__PURE__ */ new Set(["import_statement"]),
4702
+ python: /* @__PURE__ */ new Set(["import_statement", "import_from_statement", "future_import_statement"])
4244
4703
  };
4245
- function getEdgeWeight(kind, overrides) {
4246
- return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
4247
- }
4248
- function computePageRank(nodes, edges, options = {}) {
4249
- const n = nodes.length;
4250
- if (n === 0) return { rows: [], iterations: 0, converged: true };
4251
- const damping = options.damping ?? DEFAULT_DAMPING;
4252
- const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
4253
- const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
4254
- const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
4255
- const idx = buildIdIndex(nodes);
4256
- const adj = buildAdjacency(idx, edges, weights, n);
4257
- const pers = buildPersonalization(idx, n, options.personalization);
4258
- const { rank, iterations, converged } = powerIterate(
4259
- pers,
4260
- adj,
4261
- damping,
4262
- tolerance,
4263
- maxIters
4264
- );
4265
- return { rows: rankToRows(nodes, rank), iterations, converged };
4704
+ var DELIMITERS = /* @__PURE__ */ new Set(["(", ")", "[", "]", "{", "}", ",", ";"]);
4705
+ var STRING_TYPES = {
4706
+ typescript: ["string", "template_string"],
4707
+ tsx: ["string", "template_string"],
4708
+ python: ["string"]
4709
+ };
4710
+ function importMarker(count, language) {
4711
+ const prefix = language === "python" ? "#" : "//";
4712
+ return `${prefix} \u2026 ${count} import${count === 1 ? "" : "s"}`;
4266
4713
  }
4267
- function buildIdIndex(nodes) {
4268
- const idx = /* @__PURE__ */ new Map();
4269
- for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
4270
- return idx;
4714
+ function originalLine(minified, outputLine) {
4715
+ return minified.lineMap[outputLine - 1];
4271
4716
  }
4272
- function buildAdjacency(idx, edges, weights, n) {
4273
- const outNeighbors = Array.from(
4274
- { length: n },
4275
- () => []
4276
- );
4277
- const outWeightSum = new Array(n).fill(0);
4278
- for (const e of edges) {
4279
- const si = idx.get(e.srcId);
4280
- const di = idx.get(e.dstId);
4281
- if (si === void 0 || di === void 0) continue;
4282
- const w = weights[e.kind] ?? 1;
4283
- if (w <= 0) continue;
4284
- outNeighbors[si].push({ to: di, w });
4285
- outWeightSum[si] += w;
4286
- }
4287
- return { outNeighbors, outWeightSum };
4288
- }
4289
- function powerIterate(pers, adj, damping, tolerance, maxIters) {
4290
- const n = pers.length;
4291
- let rank = pers.slice();
4292
- let next = new Array(n).fill(0);
4293
- let iterations = 0;
4294
- let converged = false;
4295
- for (let iter = 0; iter < maxIters; iter++) {
4296
- iterations = iter + 1;
4297
- const dangling = sumDangling(rank, adj.outWeightSum);
4298
- const teleport = 1 - damping + damping * dangling;
4299
- seedTeleport(next, pers, teleport);
4300
- distributeRank(next, rank, adj, damping);
4301
- const diff = l1Diff(rank, next);
4302
- [rank, next] = [next, rank];
4303
- next.fill(0);
4304
- if (diff < tolerance) {
4305
- converged = true;
4306
- break;
4307
- }
4308
- }
4309
- return { rank, iterations, converged };
4717
+ function isMinifyLanguage(language) {
4718
+ return language === "typescript" || language === "tsx" || language === "python";
4310
4719
  }
4311
- function sumDangling(rank, outWeightSum) {
4312
- let total = 0;
4313
- for (let i = 0; i < rank.length; i++) {
4314
- if (outWeightSum[i] === 0) total += rank[i];
4720
+ async function minifySource(text, language) {
4721
+ if (!isMinifyLanguage(language)) return unchanged(text);
4722
+ const { tree } = await parseFile2(text, "source", language);
4723
+ try {
4724
+ return minifyTree(tree.rootNode, text, language);
4725
+ } finally {
4726
+ tree.delete();
4315
4727
  }
4316
- return total;
4317
- }
4318
- function seedTeleport(next, pers, teleport) {
4319
- for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
4320
4728
  }
4321
- function distributeRank(next, rank, adj, damping) {
4322
- for (let i = 0; i < rank.length; i++) {
4323
- const s = adj.outWeightSum[i];
4324
- if (s === 0 || rank[i] === 0) continue;
4325
- const factor = damping * rank[i] / s;
4326
- const list = adj.outNeighbors[i];
4327
- for (let k = 0; k < list.length; k++) {
4328
- next[list[k].to] += factor * list[k].w;
4729
+ function minifyTree(root, text, language) {
4730
+ const rows = text.split("\n");
4731
+ const cuts = cutsByRow(removedSpans(root, language));
4732
+ const verbatim = verbatimRows(root, language);
4733
+ const block = leadingImportBlock(root, language);
4734
+ const out = [];
4735
+ for (let row = 0; row < rows.length; row++) {
4736
+ if (block && row >= block.startRow && row <= block.endRow) {
4737
+ if (row === block.startRow) out.push({ text: importMarker(block.count, language), line: row + 1, verbatim: false });
4738
+ continue;
4329
4739
  }
4740
+ const kept = keepRow(rows[row] ?? "", cuts.get(row), verbatim.has(row));
4741
+ if (kept !== null) out.push({ text: kept, line: row + 1, verbatim: verbatim.has(row) });
4742
+ }
4743
+ return finish(collapseBlanks(out), text.endsWith("\n"));
4744
+ }
4745
+ function unchanged(text) {
4746
+ const rows = text.split("\n");
4747
+ const count = text === "" ? 0 : text.endsWith("\n") ? rows.length - 1 : rows.length;
4748
+ return { text, lineMap: Array.from({ length: count }, (_, i) => i + 1) };
4749
+ }
4750
+ function removedSpans(root, language) {
4751
+ const comments = descendantsOfType(root, "comment");
4752
+ if (language !== "python") {
4753
+ return [
4754
+ ...[...comments, ...descendantsOfType(root, "hash_bang_line")].map(spanOf),
4755
+ ...jsxCommentExpressions(root).map((n) => ({ ...spanOf(n), jsx: true }))
4756
+ ];
4330
4757
  }
4758
+ return [...comments.map(spanOf), ...pythonDocstrings(root).map(docstringSpan)];
4759
+ }
4760
+ function spanOf(node) {
4761
+ return { start: node.startPosition, end: node.endPosition };
4331
4762
  }
4332
- function l1Diff(a, b) {
4333
- let diff = 0;
4334
- for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
4335
- return diff;
4763
+ function docstringSpan(docstring) {
4764
+ const semicolon = docstring.nextSibling;
4765
+ const sameRow = semicolon?.type === ";" && semicolon.startPosition.row === docstring.endPosition.row;
4766
+ return { start: docstring.startPosition, end: sameRow ? semicolon.endPosition : docstring.endPosition };
4336
4767
  }
4337
- function rankToRows(nodes, rank) {
4338
- return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
4768
+ function jsxCommentExpressions(root) {
4769
+ return descendantsOfType(root, "jsx_expression").filter(
4770
+ (n) => n.namedChildCount > 0 && n.namedChildren.every((c) => c?.type === "comment")
4771
+ );
4339
4772
  }
4340
- function compareRows(a, b) {
4341
- if (b.score !== a.score) return b.score - a.score;
4342
- return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
4773
+ function pythonDocstrings(root) {
4774
+ const bodies = [...descendantsOfType(root, "class_definition"), ...descendantsOfType(root, "function_definition")].map((def) => def.childForFieldName("body")).filter((body) => body !== null);
4775
+ const docstrings = bodies.map((body) => droppableDocstring(body));
4776
+ const module = moduleDocstring(root);
4777
+ return [module && !sharesEndRow(module) ? module : null, ...docstrings].filter((d) => d !== null);
4343
4778
  }
4344
- function buildPersonalization(idx, n, pers) {
4345
- if (!pers || pers.size === 0) return uniformVector(n);
4346
- const out = new Array(n).fill(0);
4347
- const total = applySeedWeights(out, idx, pers);
4348
- if (total === 0) return uniformVector(n);
4349
- for (let i = 0; i < n; i++) out[i] /= total;
4350
- return out;
4779
+ function droppableDocstring(body) {
4780
+ const docstring = pythonDocstring(body);
4781
+ if (!docstring || bodyStatements(body, docstring).length === 0) return null;
4782
+ return sharesEndRow(docstring) ? null : docstring;
4783
+ }
4784
+ function sharesEndRow(docstring) {
4785
+ let next = docstring.nextNamedSibling;
4786
+ while (next?.type === "comment") next = next.nextNamedSibling;
4787
+ return next?.startPosition.row === docstring.endPosition.row;
4788
+ }
4789
+ function moduleDocstring(root) {
4790
+ const first = root.namedChildren.find((c) => c !== null && c.type !== "comment");
4791
+ if (first?.type !== "expression_statement" || first.namedChildCount !== 1) return null;
4792
+ return first.namedChildren[0]?.type === "string" ? first : null;
4793
+ }
4794
+ function cutsByRow(spans) {
4795
+ const cuts = /* @__PURE__ */ new Map();
4796
+ for (const { start, end, jsx } of spans) {
4797
+ for (let row = start.row; row <= end.row; row++) {
4798
+ const from = row === start.row ? start.column : 0;
4799
+ const to = row === end.row ? end.column : Number.MAX_SAFE_INTEGER;
4800
+ const list = cuts.get(row) ?? [];
4801
+ list.push({ from, to, pad: row !== start.row, jsx: jsx === true });
4802
+ cuts.set(row, list);
4803
+ }
4804
+ }
4805
+ return cuts;
4351
4806
  }
4352
- function uniformVector(n) {
4353
- return new Array(n).fill(1 / n);
4807
+ function verbatimRows(root, language) {
4808
+ const rows = /* @__PURE__ */ new Set();
4809
+ for (const type of STRING_TYPES[language]) {
4810
+ for (const s of descendantsOfType(root, type)) {
4811
+ for (let row = s.startPosition.row; row < s.endPosition.row; row++) rows.add(row);
4812
+ }
4813
+ }
4814
+ return rows;
4354
4815
  }
4355
- function applySeedWeights(out, idx, pers) {
4356
- let total = 0;
4357
- for (const [id, w] of pers) {
4358
- if (!isValidWeight(w)) continue;
4359
- const i = idx.get(id);
4360
- if (i === void 0) continue;
4361
- out[i] = w;
4362
- total += w;
4816
+ function keepRow(row, cuts, verbatim) {
4817
+ if (!cuts) return verbatim ? row : row.trimEnd();
4818
+ let kept = "";
4819
+ let cutSinceKept = false;
4820
+ let onlyJsxCut = true;
4821
+ for (let col = 0; col < row.length; col++) {
4822
+ const hits = cuts.filter((c) => col >= c.from && col < c.to);
4823
+ const char = row[col] ?? "";
4824
+ if (hits.length === 0) {
4825
+ const before = kept.at(-1) ?? " ";
4826
+ if (cutSinceKept && wouldFuse(before, char) && !(onlyJsxCut && isTagEdge(before, char))) kept += " ";
4827
+ kept += char;
4828
+ cutSinceKept = false;
4829
+ onlyJsxCut = true;
4830
+ } else if (hits.every((c) => c.pad)) kept += " ";
4831
+ else {
4832
+ cutSinceKept = true;
4833
+ onlyJsxCut &&= hits.some((c) => c.jsx);
4834
+ }
4835
+ }
4836
+ const trimmed = kept.trimEnd();
4837
+ if (trimmed === "") return null;
4838
+ return verbatim ? kept : trimmed;
4839
+ }
4840
+ function wouldFuse(before, after) {
4841
+ return /\S/.test(before) && /\S/.test(after) && !DELIMITERS.has(before) && !DELIMITERS.has(after);
4842
+ }
4843
+ function isTagEdge(before, after) {
4844
+ return before === ">" || after === "<";
4845
+ }
4846
+ function leadingImportBlock(root, language) {
4847
+ const children = root.namedChildren.filter((c) => c !== null);
4848
+ const docstring = language === "python" ? moduleDocstring(root) : null;
4849
+ let i = 0;
4850
+ while (i < children.length && isPreamble(children[i], docstring)) i++;
4851
+ const imports = [];
4852
+ for (; i < children.length; i++) {
4853
+ const child = children[i];
4854
+ if (child.type === "comment") continue;
4855
+ if (!IMPORT_TYPES[language].has(child.type)) break;
4856
+ imports.push(child);
4857
+ }
4858
+ while (docstring && imports[0]?.startPosition.row === docstring.endPosition.row) imports.shift();
4859
+ const next = children[i];
4860
+ while (next && imports.length > 0 && imports[imports.length - 1]?.endPosition.row === next.startPosition.row) imports.pop();
4861
+ const first = imports[0];
4862
+ const last = imports[imports.length - 1];
4863
+ if (!first || !last) return null;
4864
+ return { startRow: first.startPosition.row, endRow: last.endPosition.row, count: imports.length };
4865
+ }
4866
+ function isPreamble(node, docstring) {
4867
+ return node.type === "comment" || node.type === "hash_bang_line" || node.id === docstring?.id;
4868
+ }
4869
+ function collapseBlanks(lines) {
4870
+ const out = [];
4871
+ for (const line of lines) {
4872
+ const blank = line.text === "" && !line.verbatim;
4873
+ const prev = out[out.length - 1];
4874
+ if (blank && (!prev || prev.text === "" && !prev.verbatim)) continue;
4875
+ out.push(line);
4363
4876
  }
4364
- return total;
4877
+ while (out.length > 0 && out[out.length - 1]?.text === "" && !out[out.length - 1]?.verbatim) out.pop();
4878
+ return out;
4365
4879
  }
4366
- function isValidWeight(w) {
4367
- return Number.isFinite(w) && w > 0;
4880
+ function finish(lines, trailingNewline) {
4881
+ const body = lines.map((l) => l.text).join("\n");
4882
+ return {
4883
+ text: trailingNewline && lines.length > 0 ? `${body}
4884
+ ` : body,
4885
+ lineMap: lines.map((l) => l.line)
4886
+ };
4368
4887
  }
4369
4888
 
4370
4889
  // src/analysis/relevance.ts
@@ -4390,290 +4909,428 @@ function symmetrize(edges) {
4390
4909
  return out;
4391
4910
  }
4392
4911
 
4393
- // src/analysis/symbol-coupling.ts
4394
- var DEFAULT_MIN_CO_IMPORTS = 2;
4395
- var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
4396
- function computeSymbolConsumers(edges) {
4397
- const bySymbol = /* @__PURE__ */ new Map();
4398
- for (const e of edges) {
4399
- let set = bySymbol.get(e.dstId);
4400
- if (!set) {
4401
- set = /* @__PURE__ */ new Set();
4402
- bySymbol.set(e.dstId, set);
4403
- }
4404
- set.add(e.srcId);
4405
- }
4406
- const out = [];
4407
- for (const [id, consumers] of bySymbol) {
4408
- const parsed = parseSymbolId(id);
4409
- if (!parsed) continue;
4410
- out.push({
4411
- symbolId: id,
4412
- fileId: parsed.fileId,
4413
- name: parsed.name,
4414
- consumers: [...consumers].sort()
4415
- });
4912
+ // src/analysis/context/signature-tree.ts
4913
+ var ELISION_MARKER = "...";
4914
+ var TRUNCATION_SUFFIX = "\u2026";
4915
+ var DEFAULT_MAX_LINE_CHARS = 120;
4916
+ var INDENT = " ";
4917
+ function renderSignatureTree(nodes, options = {}) {
4918
+ const max = options.maxLineChars ?? DEFAULT_MAX_LINE_CHARS;
4919
+ const lines = [];
4920
+ for (const [file, entries] of groupByFile(nodes)) {
4921
+ lines.push(truncate(file, max));
4922
+ for (const line of fileLines(entries)) lines.push(truncate(line, max));
4416
4923
  }
4417
- return out.sort(compareConsumers);
4924
+ return lines.join("\n");
4418
4925
  }
4419
- function compareConsumers(a, b) {
4420
- if (b.consumers.length !== a.consumers.length) {
4421
- return b.consumers.length - a.consumers.length;
4926
+ function groupByFile(nodes) {
4927
+ const byFile = /* @__PURE__ */ new Map();
4928
+ for (const node of nodes) {
4929
+ if (node.kind !== "symbol") continue;
4930
+ const parsed = parseSymbolId(node.id);
4931
+ const file = node.parentId ?? parsed?.fileId ?? node.id;
4932
+ const list = byFile.get(file) ?? [];
4933
+ list.push(toEntry(node, parsed?.name ?? node.name));
4934
+ byFile.set(file, list);
4422
4935
  }
4423
- return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
4936
+ return [...byFile].sort(([a], [b]) => compare2(a, b));
4424
4937
  }
4425
- function computeSymbolCoupling(edges, options = {}) {
4426
- const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
4427
- const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
4428
- const bySource = groupSymbolsBySource(edges);
4429
- const counts = /* @__PURE__ */ new Map();
4430
- for (const symbols of bySource.values()) {
4431
- if (symbols.length < 2 || symbols.length > largeThreshold) continue;
4432
- accumulateSymbolPairs(symbols, counts);
4433
- }
4434
- return finalizePairs(counts, minCoImports);
4938
+ function toEntry(node, qualifiedName) {
4939
+ const start = numberAttr(node, "startLine");
4940
+ const end = numberAttr(node, "endLine") ?? start;
4941
+ return { node, start, end, depth: qualifiedName.split(".").length - 1 };
4435
4942
  }
4436
- function groupSymbolsBySource(edges) {
4437
- const perSource = /* @__PURE__ */ new Map();
4438
- for (const e of edges) {
4439
- if (!parseSymbolId(e.dstId)) continue;
4440
- let set = perSource.get(e.srcId);
4441
- if (!set) {
4442
- set = /* @__PURE__ */ new Set();
4443
- perSource.set(e.srcId, set);
4444
- }
4445
- set.add(e.dstId);
4446
- }
4447
- const out = /* @__PURE__ */ new Map();
4448
- for (const [src, set] of perSource) out.set(src, [...set].sort());
4449
- return out;
4943
+ function numberAttr(node, key) {
4944
+ const value = node.attrs?.[key];
4945
+ return typeof value === "number" ? value : void 0;
4450
4946
  }
4451
- function accumulateSymbolPairs(symbols, counts) {
4452
- for (let i = 0; i < symbols.length; i++) {
4453
- for (let j = i + 1; j < symbols.length; j++) {
4454
- const key = `${symbols[i]} ${symbols[j]}`;
4455
- counts.set(key, (counts.get(key) ?? 0) + 1);
4947
+ function fileLines(entries) {
4948
+ const ordered = entries.sort(compareEntries);
4949
+ const lines = [];
4950
+ let reach;
4951
+ for (const entry of ordered) {
4952
+ if (reach !== void 0 && entry.start !== void 0 && !adjacent(reach, entry.start)) {
4953
+ lines.push(ELISION_MARKER);
4456
4954
  }
4955
+ lines.push(INDENT.repeat(entry.depth) + symbolText(entry.node));
4956
+ if (entry.end !== void 0) reach = Math.max(reach ?? entry.end, entry.end);
4457
4957
  }
4958
+ return lines;
4458
4959
  }
4459
- function finalizePairs(counts, minCoImports) {
4460
- const out = [];
4461
- for (const [key, coImports] of counts) {
4462
- if (coImports < minCoImports) continue;
4463
- const [aId, bId] = key.split(" ");
4464
- const a = parseSymbolId(aId);
4465
- const b = parseSymbolId(bId);
4466
- out.push({
4467
- aId,
4468
- aFile: a.fileId,
4469
- aName: a.name,
4470
- bId,
4471
- bFile: b.fileId,
4472
- bName: b.name,
4473
- coImports,
4474
- crossFile: a.fileId !== b.fileId
4475
- });
4960
+ function adjacent(reach, nextStart) {
4961
+ return nextStart <= reach + 1;
4962
+ }
4963
+ function symbolText(node) {
4964
+ const signature = node.attrs?.signature;
4965
+ const text = typeof signature === "string" && signature !== "" ? signature : `${node.name} (${node.kind})`;
4966
+ return text.replace(/\s*\n\s*/g, " ");
4967
+ }
4968
+ function compareEntries(a, b) {
4969
+ if (a.start !== b.start) {
4970
+ if (a.start === void 0) return 1;
4971
+ if (b.start === void 0) return -1;
4972
+ return a.start - b.start;
4476
4973
  }
4477
- return out.sort(comparePairs);
4974
+ return compare2(a.node.name, b.node.name) || compare2(a.node.id, b.node.id);
4975
+ }
4976
+ function compare2(a, b) {
4977
+ return a < b ? -1 : a > b ? 1 : 0;
4478
4978
  }
4479
- function comparePairs(a, b) {
4480
- if (b.coImports !== a.coImports) return b.coImports - a.coImports;
4481
- if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
4482
- if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
4483
- return a.bId < b.bId ? -1 : a.bId > b.bId ? 1 : 0;
4979
+ function truncate(line, max) {
4980
+ if (line.length <= max) return line;
4981
+ if (max <= TRUNCATION_SUFFIX.length) return TRUNCATION_SUFFIX.slice(0, Math.max(max, 0));
4982
+ return line.slice(0, max - TRUNCATION_SUFFIX.length) + TRUNCATION_SUFFIX;
4484
4983
  }
4485
4984
 
4486
- // src/analysis/partition-quality.ts
4487
- var FOUNDATION_INSTABILITY_MAX = 0.3;
4488
- var TOP_INSTABILITY_MIN = 0.9;
4489
- var WEAK_COHESION_MAX = 0.5;
4490
- var TIGHT_INTENSITY_MIN = 0.6;
4491
- var MODERATE_INTENSITY_MIN = 0.3;
4492
- function computePartitionQuality(input) {
4493
- const resolveBarrels = input.resolveBarrels ?? false;
4494
- const pkgByFile = invertBuckets(input.fileByPackage);
4495
- const barrelTargets = resolveBarrels ? buildBarrelTargetMap(input.nodes, input.edges) : /* @__PURE__ */ new Map();
4496
- const resolvedPairs = buildResolvedPairs(
4497
- input.edges,
4498
- pkgByFile,
4499
- barrelTargets,
4500
- resolveBarrels
4501
- );
4502
- const counts = countByPackage(resolvedPairs);
4503
- const totalEdges = resolvedPairs.length;
4504
- const filesByPkg = filesPerPackage(input.fileByPackage);
4505
- const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
4506
- const perPackage = input.packages.map(
4507
- (p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
4508
- );
4509
- const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
4510
- const modularityQ = computeModularity(perPackage, totalEdges);
4511
- const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
4512
- return { modularityQ, totalEdges, perPackage, pairCoupling, flagsCount };
4985
+ // src/analysis/context/dossier.ts
4986
+ var SCHEMA_VERSION2 = "2";
4987
+ var CONSUMER_NOTE = "cross-file import resolution only; intra-file and dynamic import() callers are excluded";
4988
+ function splitConsumers(files, roleByFile) {
4989
+ const source = [];
4990
+ const test = [];
4991
+ for (const f of files) (roleByFile.get(f) === "test" ? test : source).push(f);
4992
+ return {
4993
+ source,
4994
+ test,
4995
+ counts: { source: source.length, test: test.length, total: files.length },
4996
+ note: CONSUMER_NOTE
4997
+ };
4513
4998
  }
4514
- function invertBuckets(fileByPackage) {
4999
+ function consumerCounts(edges) {
4515
5000
  const out = /* @__PURE__ */ new Map();
4516
- for (const [pkgId, files] of fileByPackage) {
4517
- if (pkgId === "") continue;
4518
- for (const f of files) out.set(f, pkgId);
4519
- }
5001
+ for (const c of computeSymbolConsumers(edges)) out.set(c.symbolId, c.consumers);
4520
5002
  return out;
4521
5003
  }
4522
- function filesPerPackage(fileByPackage) {
4523
- const out = /* @__PURE__ */ new Map();
4524
- for (const [pkgId, files] of fileByPackage) {
4525
- out.set(pkgId, files.length);
5004
+ function fileIdOf(symbolId2) {
5005
+ return parseSymbolId(symbolId2)?.fileId ?? null;
5006
+ }
5007
+ function buildSymbolDossier(input) {
5008
+ const id = input.target.id;
5009
+ const parsed = parseSymbolId(id);
5010
+ const fileId2 = parsed?.fileId ?? "";
5011
+ const m = input.metrics.get(id);
5012
+ const consumers = consumerCounts(input.refEdges).get(id) ?? [];
5013
+ const complexity = m?.cognitiveMax ?? input.metrics.get(fileId2)?.cognitiveMax ?? 0;
5014
+ const churn = input.churnByFile.get(fileId2) ?? 0;
5015
+ const coupledWith = computeSymbolCoupling(input.refEdges).filter((p) => p.aId === id || p.bId === id).map((p) => p.aId === id ? partner(p.bId, p.bName, p.bFile, p.coImports) : partner(p.aId, p.aName, p.aFile, p.coImports)).slice(0, 15);
5016
+ return {
5017
+ exported: input.target.attrs?.exported !== false,
5018
+ signature: attrString(input.target.attrs?.signature),
5019
+ purpose: attrString(input.target.attrs?.purpose),
5020
+ complexity: { cognitive: m?.cognitiveMax, cyclomatic: m?.cyclomaticMax },
5021
+ utilization: m?.utilization ?? 0,
5022
+ consumers: splitConsumers(consumers, input.roleByFile),
5023
+ blastRadius: (m?.utilization ?? 0) * complexity * churn,
5024
+ coupledWith
5025
+ };
5026
+ }
5027
+ function partner(symbolId2, name, fileId2, coImports) {
5028
+ return { symbolId: symbolId2, name, fileId: fileId2, coImports };
5029
+ }
5030
+ function attrString(value) {
5031
+ return typeof value === "string" && value.length > 0 ? value : null;
5032
+ }
5033
+ function fileSymbols(fileId2, nodes, metrics, consumers) {
5034
+ const lines = [];
5035
+ for (const n of nodes) {
5036
+ if (n.kind !== "symbol" || n.parentId !== fileId2) continue;
5037
+ const m = metrics.get(n.id);
5038
+ const exported = n.attrs?.exported !== false;
5039
+ lines.push({
5040
+ name: n.name,
5041
+ exported,
5042
+ signature: attrString(n.attrs?.signature) ?? void 0,
5043
+ cognitive: m?.cognitiveMax,
5044
+ cyclomatic: m?.cyclomaticMax,
5045
+ utilization: m?.utilization ?? 0,
5046
+ consumers: (consumers.get(n.id) ?? []).length,
5047
+ rank: exported ? m?.utilization ?? 0 : -(m?.cognitiveMax ?? 0)
5048
+ });
4526
5049
  }
4527
- return out;
5050
+ return sortSymbols(lines);
4528
5051
  }
4529
- function buildBarrelTargetMap(nodes, edges) {
4530
- const barrelIds = new Set(
4531
- nodes.filter((n) => n.role === "barrel").map((n) => n.id)
4532
- );
4533
- if (barrelIds.size === 0) return /* @__PURE__ */ new Map();
4534
- const directReExports = /* @__PURE__ */ new Map();
4535
- for (const e of edges) {
4536
- if (!barrelIds.has(e.srcId)) continue;
4537
- if (e.kind !== "re-exports") continue;
4538
- let list = directReExports.get(e.srcId);
4539
- if (!list) {
4540
- list = [];
4541
- directReExports.set(e.srcId, list);
4542
- }
4543
- list.push(e.dstId);
4544
- }
4545
- const resolved = /* @__PURE__ */ new Map();
4546
- const resolve4 = (barrel, visiting) => {
4547
- if (resolved.has(barrel)) return resolved.get(barrel);
4548
- if (visiting.has(barrel)) return [];
4549
- visiting.add(barrel);
4550
- const out = [];
4551
- for (const target of directReExports.get(barrel) ?? []) {
4552
- if (barrelIds.has(target)) {
4553
- for (const inner of resolve4(target, visiting)) out.push(inner);
4554
- } else {
4555
- out.push(target);
4556
- }
4557
- }
4558
- visiting.delete(barrel);
4559
- resolved.set(barrel, out);
4560
- return out;
5052
+ function sortSymbols(lines) {
5053
+ const exported = lines.filter((l) => l.exported).sort((a, b) => b.utilization - a.utilization || (b.cognitive ?? 0) - (a.cognitive ?? 0));
5054
+ const internal = lines.filter((l) => !l.exported).sort((a, b) => (b.cognitive ?? 0) - (a.cognitive ?? 0) || a.name.localeCompare(b.name));
5055
+ return [...exported, ...internal].map(({ rank: _rank, ...line }) => line);
5056
+ }
5057
+ function redistributeImportance(symbols, fileCentrality) {
5058
+ const totalUtil = symbols.reduce((sum, s) => sum + s.utilization, 0);
5059
+ if (totalUtil <= 0 || fileCentrality <= 0) return symbols;
5060
+ return symbols.map((s) => ({
5061
+ ...s,
5062
+ importance: Math.round(fileCentrality * (s.utilization / totalUtil) * 1e6) / 1e6
5063
+ }));
5064
+ }
5065
+ function fileConsumers(fileId2, refEdges) {
5066
+ const set = /* @__PURE__ */ new Set();
5067
+ for (const e of refEdges) if (fileIdOf(e.dstId) === fileId2) set.add(e.srcId);
5068
+ return [...set].sort();
5069
+ }
5070
+ function fileDependsOn(fileId2, refEdges, importEdges) {
5071
+ const set = /* @__PURE__ */ new Set();
5072
+ for (const e of refEdges) if (e.srcId === fileId2) {
5073
+ const f = fileIdOf(e.dstId);
5074
+ if (f) set.add(f);
5075
+ }
5076
+ for (const e of importEdges) if (e.srcId === fileId2) set.add(e.dstId);
5077
+ set.delete(fileId2);
5078
+ return [...set].sort();
5079
+ }
5080
+ function buildFileDossier(input) {
5081
+ const fileId2 = input.target.id;
5082
+ const consumers = consumerCounts(input.refEdges);
5083
+ const symbolUtil = collectSymbolUtil(input.nodes, input.metrics).filter((s) => s.fileId === fileId2);
5084
+ const churnValue = input.churnByFile.get(fileId2);
5085
+ const centrality = input.centrality.get(fileId2) ?? 0;
5086
+ return {
5087
+ metrics: input.metrics.get(fileId2) ?? {},
5088
+ churn: churnValue === void 0 ? null : { windowDays: input.churnWindowDays, value: churnValue },
5089
+ centrality,
5090
+ ownership: input.ownership?.get(fileId2) ?? null,
5091
+ symbols: redistributeImportance(
5092
+ fileSymbols(fileId2, input.nodes, input.metrics, consumers),
5093
+ centrality
5094
+ ),
5095
+ dependsOn: fileDependsOn(fileId2, input.refEdges, input.importEdges),
5096
+ consumers: splitConsumers(fileConsumers(fileId2, input.refEdges), input.roleByFile),
5097
+ blastRadius: buildBlastRadius(symbolUtil, input.metrics, input.churnByFile, 15)
4561
5098
  };
4562
- for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
4563
- return resolved;
4564
5099
  }
4565
- function buildResolvedPairs(edges, pkgByFile, barrelTargets, resolveBarrels) {
4566
- const out = [];
4567
- for (const e of edges) {
4568
- if (resolveBarrels && e.kind === "re-exports") continue;
4569
- const fromPkg = pkgByFile.get(e.srcId);
4570
- if (!fromPkg) continue;
4571
- const targets = barrelTargets.get(e.dstId) ?? [e.dstId];
4572
- for (const dst of targets) {
4573
- const toPkg = pkgByFile.get(dst);
4574
- if (toPkg) out.push({ from: fromPkg, to: toPkg });
4575
- }
5100
+ var SIGNATURE_NOTE = "Type signatures and docstrings are indexed for resolvable TypeScript declarations (C-79); a null signature means it was unannotated or unresolvable \u2014 open target.path at target.span.";
5101
+ function buildContextDossier(input) {
5102
+ const { target, kind } = input;
5103
+ const span = readSpan(target);
5104
+ const path14 = kind === "symbol" ? parseSymbolId(target.id)?.fileId ?? target.id : target.id;
5105
+ const notes = [SIGNATURE_NOTE];
5106
+ if (kind === "file" && input.ownership === null) {
5107
+ notes.push("Ownership omitted (git-derived; run without --no-ownership on a repo checkout).");
5108
+ }
5109
+ return {
5110
+ schemaVersion: SCHEMA_VERSION2,
5111
+ target: { id: target.id, kind, name: target.name, path: path14, span },
5112
+ provenance: input.provenance,
5113
+ symbol: kind === "symbol" ? buildSymbolDossier(input) : void 0,
5114
+ file: kind === "file" ? buildFileDossier(input) : void 0,
5115
+ notes
5116
+ };
5117
+ }
5118
+ function readSpan(node) {
5119
+ const start = node.attrs?.startLine;
5120
+ const end = node.attrs?.endLine;
5121
+ if (typeof start === "number" && typeof end === "number") return { startLine: start, endLine: end };
5122
+ return void 0;
5123
+ }
5124
+
5125
+ // src/analysis/context/format.ts
5126
+ function renderContextMarkdown(d) {
5127
+ const span = d.target.span ? `:${d.target.span.startLine}-${d.target.span.endLine}` : "";
5128
+ const lines = [
5129
+ `# ${d.target.kind === "symbol" ? d.target.name : d.target.path}`,
5130
+ "",
5131
+ `- **kind**: ${d.target.kind}`,
5132
+ `- **path**: \`${d.target.path}${span}\``,
5133
+ `- **snapshot**: ${d.provenance.ref} (${d.provenance.commitHash ?? "\u2014"}) @ ${d.provenance.takenAt}`,
5134
+ ""
5135
+ ];
5136
+ if (d.symbol) lines.push(...renderSymbol(d.symbol));
5137
+ if (d.file) lines.push(...renderFile(d.file));
5138
+ if (d.notes.length) lines.push("## Notes", ...d.notes.map((n) => `- ${n}`), "");
5139
+ return lines.join("\n");
5140
+ }
5141
+ function renderSymbol(s) {
5142
+ const out = [
5143
+ "## Symbol",
5144
+ `- **exported**: ${s.exported}`,
5145
+ `- **signature**: ${s.signature ? `\`${s.signature}\`` : "\u2014 (not indexed)"}`,
5146
+ `- **purpose**: ${s.purpose ?? "\u2014"}`,
5147
+ `- **complexity**: cognitive ${s.complexity.cognitive ?? "\u2014"}, cyclomatic ${s.complexity.cyclomatic ?? "\u2014"}`,
5148
+ `- **utilization**: ${s.utilization}`,
5149
+ `- **blast radius**: ${round(s.blastRadius)}`,
5150
+ ...renderConsumers(s.consumers)
5151
+ ];
5152
+ if (s.coupledWith.length) {
5153
+ out.push("### Co-imported with");
5154
+ for (const c of s.coupledWith) out.push(`- ${c.name} (\`${c.fileId}\`) \xD7${c.coImports}`);
5155
+ out.push("");
4576
5156
  }
4577
5157
  return out;
4578
5158
  }
4579
- function countByPackage(pairs) {
4580
- const out = /* @__PURE__ */ new Map();
4581
- const get = (id) => {
4582
- let c = out.get(id);
4583
- if (!c) {
4584
- c = { internal: 0, outgoing: 0, incoming: 0 };
4585
- out.set(id, c);
4586
- }
4587
- return c;
4588
- };
4589
- for (const p of pairs) {
4590
- if (p.from === p.to) {
4591
- get(p.from).internal += 1;
4592
- } else {
4593
- get(p.from).outgoing += 1;
4594
- get(p.to).incoming += 1;
5159
+ function renderFile(f) {
5160
+ const m = f.metrics;
5161
+ const own = f.ownership ? `${f.ownership.primaryOwner} (bus factor ${f.ownership.busFactor}, ${f.ownership.authorCount} authors)` : "\u2014";
5162
+ const out = [
5163
+ "## File",
5164
+ `- **loc**: ${m.loc ?? "\u2014"} \xB7 **cognitive** ${m.cognitiveMax ?? "\u2014"} \xB7 **cyclomatic** ${m.cyclomaticMax ?? "\u2014"}`,
5165
+ `- **fan-in/out**: ${m.fanIn ?? "\u2014"} / ${m.fanOut ?? "\u2014"} \xB7 **centrality**: ${round(f.centrality)}`,
5166
+ `- **churn**: ${f.churn ? `${f.churn.value} (${f.churn.windowDays}d)` : "\u2014"} \xB7 **role**: ${m.role ?? "\u2014"}`,
5167
+ `- **ownership**: ${own}`,
5168
+ "",
5169
+ ...section(`Depends on (${f.dependsOn.length})`, f.dependsOn),
5170
+ ...renderConsumers(f.consumers)
5171
+ ];
5172
+ if (f.symbols.length) {
5173
+ out.push(`### Symbols (${f.symbols.length})`);
5174
+ for (const s of f.symbols) {
5175
+ const tag = s.exported ? "export" : "internal";
5176
+ const sig = s.signature ? ` \`${s.signature}\`` : "";
5177
+ const imp = s.importance !== void 0 ? `, importance ${s.importance}` : "";
5178
+ out.push(`- \`${s.name}\` (${tag})${sig} \u2014 util ${s.utilization}, cog ${s.cognitive ?? "\u2014"}, consumers ${s.consumers}${imp}`);
4595
5179
  }
5180
+ out.push("");
5181
+ }
5182
+ if (f.blastRadius.length) {
5183
+ out.push("### Blast radius (look here first)");
5184
+ for (const b of f.blastRadius) out.push(`- \`${b.name}\` \u2014 score ${round(b.score)} (util ${b.utilization} \xD7 cog ${b.complexity} \xD7 churn ${b.churn})`);
5185
+ out.push("");
4596
5186
  }
4597
5187
  return out;
4598
5188
  }
4599
- function buildPackageStats(pkgId, counts, filesByPkg, abstractByPkg) {
4600
- const c = counts.get(pkgId) ?? { internal: 0, outgoing: 0, incoming: 0 };
4601
- const cohesion = ratioOrZero(c.internal, c.internal + c.outgoing);
4602
- const instability = ratioOrZero(c.outgoing, c.outgoing + c.incoming);
4603
- const fileCount = filesByPkg.get(pkgId) ?? 0;
4604
- const abstractness = ratioOrZero(abstractByPkg.get(pkgId) ?? 0, fileCount);
4605
- const layer = classifyLayer(instability);
4606
- const flags = [];
4607
- if (layer !== "top" && cohesion < WEAK_COHESION_MAX && c.internal + c.outgoing > 0) {
4608
- flags.push("weak-boundary");
4609
- }
5189
+ function section(title, items) {
5190
+ if (!items.length) return [];
5191
+ return [`### ${title}`, ...items.map((i) => `- \`${i}\``), ""];
5192
+ }
5193
+ function renderConsumers(c) {
5194
+ const header = `### Consumers \u2014 ${c.counts.source} source, ${c.counts.test} test (${c.counts.total} total)`;
5195
+ const rows = c.source.map((i) => `- \`${i}\``);
5196
+ return [header, ...rows.length ? rows : ["- (no non-test consumers)"], `> ${c.note}`, ""];
5197
+ }
5198
+ function round(n) {
5199
+ return Math.round(n * 1e3) / 1e3;
5200
+ }
5201
+
5202
+ // src/analysis/context/bundle.ts
5203
+ import { readFileSync as readFileSync3 } from "fs";
5204
+ import * as path12 from "path";
5205
+ var BUNDLE_SCHEMA_VERSION = "2";
5206
+ var EDGE_NOTE = "references resolve cross-file import usage (src = importing file \u2192 dst = symbol); a symbol's dependencies are its declaring file's outbound edges (the graph resolves the source side at file granularity).";
5207
+ var COVERAGE_NOTE = "coverage_pct overlay not ingested; run `graph coverage <istanbul-report>` to populate.";
5208
+ var SOURCE_READ_NOTE = "source file could not be read from the working tree at this path.";
5209
+ function weightOf(e) {
5210
+ const w = e.attrs?.weight;
5211
+ return typeof w === "number" ? w : 1;
5212
+ }
5213
+ function relevanceCtx(input) {
5214
+ const { relevanceByFile, targetFileId: targetFileId2 } = input;
5215
+ if (!relevanceByFile || relevanceByFile.size === 0 || targetFileId2 === void 0) return null;
5216
+ return { relevanceByFile, targetFileId: targetFileId2 };
5217
+ }
5218
+ function fileOf(id) {
5219
+ return parseSymbolId(id)?.fileId ?? id;
5220
+ }
5221
+ function rankEdges(edges, ctx) {
5222
+ if (!ctx) return sortByWeight(edges);
5223
+ const annotated = edges.map((e) => {
5224
+ const neighbor = fileOf(e.from) === ctx.targetFileId ? fileOf(e.to) : fileOf(e.from);
5225
+ return { ...e, relevance: round4(ctx.relevanceByFile.get(neighbor) ?? 0) };
5226
+ });
5227
+ return annotated.sort(
5228
+ (a, b) => (b.relevance ?? 0) - (a.relevance ?? 0) || b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)
5229
+ );
5230
+ }
5231
+ function sortByWeight(edges) {
5232
+ return edges.sort(
5233
+ (a, b) => b.weight - a.weight || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)
5234
+ );
5235
+ }
5236
+ function round4(n) {
5237
+ return Math.round(n * 1e6) / 1e6;
5238
+ }
5239
+ function symbolEdges(input) {
5240
+ const id = input.target.id;
5241
+ const fileId2 = parseSymbolId(id)?.fileId ?? "";
5242
+ const ctx = relevanceCtx(input);
5243
+ const callers = input.refEdges.filter((e) => e.dstId === id).map((e) => ({ from: e.srcId, to: id, kind: "references", weight: weightOf(e) }));
5244
+ const dependencies = [
5245
+ ...input.refEdges.filter((e) => e.srcId === fileId2).map((e) => edge(e, "references")),
5246
+ ...input.importEdges.filter((e) => e.srcId === fileId2).map((e) => edge(e, "imports"))
5247
+ ];
5248
+ const coupledWith = (input.dossier.symbol?.coupledWith ?? []).map((c) => ({
5249
+ from: id,
5250
+ to: c.symbolId,
5251
+ kind: "coupled-with",
5252
+ weight: c.coImports
5253
+ }));
4610
5254
  return {
4611
- pkgId,
4612
- fileCount,
4613
- internalEdges: c.internal,
4614
- outgoingEdges: c.outgoing,
4615
- incomingEdges: c.incoming,
4616
- cohesion,
4617
- instability,
4618
- abstractness,
4619
- layer,
4620
- flags
5255
+ callers: rankEdges(callers, ctx),
5256
+ dependencies: rankEdges(dependencies, ctx),
5257
+ coupledWith: rankEdges(coupledWith, ctx),
5258
+ note: EDGE_NOTE
4621
5259
  };
4622
5260
  }
4623
- function countAbstractFiles(nodes, pkgByFile) {
4624
- const out = /* @__PURE__ */ new Map();
4625
- for (const n of nodes) {
4626
- if (n.kind !== "file" || n.role !== "types") continue;
4627
- const pkg = pkgByFile.get(n.id);
4628
- if (pkg) out.set(pkg, (out.get(pkg) ?? 0) + 1);
5261
+ function fileEdges(input) {
5262
+ const id = input.target.id;
5263
+ const ctx = relevanceCtx(input);
5264
+ const inbound = (e) => parseSymbolId(e.dstId)?.fileId === id;
5265
+ const callers = [
5266
+ ...input.refEdges.filter(inbound).map((e) => edge(e, "references")),
5267
+ ...input.importEdges.filter((e) => e.dstId === id).map((e) => edge(e, "imports"))
5268
+ ];
5269
+ const dependencies = [
5270
+ ...input.refEdges.filter((e) => e.srcId === id).map((e) => edge(e, "references")),
5271
+ ...input.importEdges.filter((e) => e.srcId === id).map((e) => edge(e, "imports"))
5272
+ ];
5273
+ return {
5274
+ callers: rankEdges(callers, ctx),
5275
+ dependencies: rankEdges(dependencies, ctx),
5276
+ coupledWith: [],
5277
+ note: EDGE_NOTE
5278
+ };
5279
+ }
5280
+ function edge(e, kind) {
5281
+ return { from: e.srcId, to: e.dstId, kind, weight: weightOf(e) };
5282
+ }
5283
+ function readSource(input) {
5284
+ const filePath = input.kind === "symbol" ? parseSymbolId(input.target.id)?.fileId ?? input.target.id : input.target.id;
5285
+ const span = input.dossier.target.span ?? null;
5286
+ const abs = input.repoRoot ? path12.join(input.repoRoot, filePath) : filePath;
5287
+ try {
5288
+ const content = readFileSync3(abs, "utf8");
5289
+ const text = span ? sliceSpan(content, span) : content;
5290
+ return { path: filePath, span, text };
5291
+ } catch {
5292
+ return { path: filePath, span, text: null, note: SOURCE_READ_NOTE };
4629
5293
  }
4630
- return out;
4631
5294
  }
4632
- function ratioOrZero(num, denom) {
4633
- return denom === 0 ? 0 : num / denom;
5295
+ function sliceSpan(content, span) {
5296
+ return content.split("\n").slice(span.startLine - 1, span.endLine).join("\n");
4634
5297
  }
4635
- function classifyLayer(instability) {
4636
- if (instability <= FOUNDATION_INSTABILITY_MAX) return "foundation";
4637
- if (instability >= TOP_INSTABILITY_MIN) return "top";
4638
- return "middle";
5298
+ function buildContextBundle(input) {
5299
+ return {
5300
+ schemaVersion: BUNDLE_SCHEMA_VERSION,
5301
+ dossier: input.dossier,
5302
+ source: readSource(input),
5303
+ edges: input.kind === "symbol" ? symbolEdges(input) : fileEdges(input),
5304
+ coverage: {
5305
+ pct: input.coveragePct,
5306
+ note: input.coveragePct === null ? COVERAGE_NOTE : void 0
5307
+ }
5308
+ };
4639
5309
  }
4640
- function buildPairCoupling(pairs, filesByPkg) {
4641
- const counts = /* @__PURE__ */ new Map();
4642
- for (const p of pairs) {
4643
- if (p.from === p.to) continue;
4644
- const key = JSON.stringify([p.from, p.to]);
4645
- const entry = counts.get(key);
4646
- if (entry) entry.edges += 1;
4647
- else counts.set(key, { from: p.from, to: p.to, edges: 1 });
4648
- }
4649
- const out = [];
4650
- for (const { from, to, edges } of counts.values()) {
4651
- const filesFrom = filesByPkg.get(from) ?? 0;
4652
- const intensity = filesFrom === 0 ? 0 : edges / filesFrom;
4653
- const flag = classifyPairIntensity(intensity);
4654
- out.push({ from, to, edges, intensity, flag });
4655
- }
4656
- return out.sort(comparePairs2);
4657
- }
4658
- function classifyPairIntensity(intensity) {
4659
- if (intensity >= TIGHT_INTENSITY_MIN) return "tight";
4660
- if (intensity >= MODERATE_INTENSITY_MIN) return "moderate";
4661
- return "none";
4662
- }
4663
- function comparePairs2(a, b) {
4664
- if (a.from !== b.from) return a.from < b.from ? -1 : 1;
4665
- return a.to < b.to ? -1 : 1;
4666
- }
4667
- function computeModularity(perPackage, totalEdges) {
4668
- if (totalEdges === 0) return 0;
4669
- let Q = 0;
4670
- for (const p of perPackage) {
4671
- const e_cc = p.internalEdges / totalEdges;
4672
- const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
4673
- const a_c = touching / (2 * totalEdges);
4674
- Q += e_cc - a_c * a_c;
4675
- }
4676
- return Q;
5310
+ function renderBundleText(bundle) {
5311
+ const cov = bundle.coverage.pct === null ? "\u2014" : `${bundle.coverage.pct}%`;
5312
+ return [
5313
+ renderContextMarkdown(bundle.dossier),
5314
+ "## Coverage",
5315
+ `- **coverage**: ${cov}`,
5316
+ "",
5317
+ ...renderEdgeSection("Callers (inbound)", bundle.edges.callers),
5318
+ ...renderEdgeSection("Dependencies (outbound)", bundle.edges.dependencies),
5319
+ ...renderEdgeSection("Coupled with", bundle.edges.coupledWith),
5320
+ "## Source",
5321
+ "```typescript",
5322
+ bundle.source.text ?? `// ${bundle.source.note ?? "unavailable"}`,
5323
+ "```",
5324
+ ""
5325
+ ].join("\n");
5326
+ }
5327
+ function renderEdgeSection(title, edges) {
5328
+ if (!edges.length) return [];
5329
+ return [`### ${title}`, ...edges.map(renderEdgeLine), ""];
5330
+ }
5331
+ function renderEdgeLine(e) {
5332
+ const rel = e.relevance !== void 0 ? `, rel ${e.relevance}` : "";
5333
+ return `- \`${e.from}\` \u2192 \`${e.to}\` (${e.kind} \xD7${e.weight}${rel})`;
4677
5334
  }
4678
5335
 
4679
5336
  // src/analysis/snapshot.ts
@@ -4693,6 +5350,57 @@ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
4693
5350
  return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
4694
5351
  }
4695
5352
 
5353
+ // src/analysis/dashboard-symbol-coupling.ts
5354
+ var MIN_SHARED_CONSUMERS = 2;
5355
+ var MAX_PAIRS = 40;
5356
+ var MAX_GROUPS = 15;
5357
+ var MAX_SYMBOLS_PER_GROUP = 10;
5358
+ var CONSUMER_SAMPLE = 12;
5359
+ function buildSymbolCouplingPayload(edges) {
5360
+ return {
5361
+ symbolCoupling: buildCouplingRows(edges),
5362
+ symbolConsumers: buildConsumerGroups(edges)
5363
+ };
5364
+ }
5365
+ function buildCouplingRows(edges) {
5366
+ return computeSymbolCoupling(edges).slice(0, MAX_PAIRS).map((p) => ({
5367
+ aName: p.aName,
5368
+ aFile: p.aFile,
5369
+ bName: p.bName,
5370
+ bFile: p.bFile,
5371
+ coImports: p.coImports,
5372
+ crossFile: p.crossFile
5373
+ }));
5374
+ }
5375
+ function buildConsumerGroups(edges) {
5376
+ const byFile = /* @__PURE__ */ new Map();
5377
+ for (const s of computeSymbolConsumers(edges)) {
5378
+ if (s.consumers.length < MIN_SHARED_CONSUMERS) continue;
5379
+ const bucket = byFile.get(s.fileId) ?? [];
5380
+ bucket.push({
5381
+ name: s.name,
5382
+ consumers: s.consumers.slice(0, CONSUMER_SAMPLE),
5383
+ consumerCount: s.consumers.length
5384
+ });
5385
+ byFile.set(s.fileId, bucket);
5386
+ }
5387
+ return foldGroups(byFile);
5388
+ }
5389
+ function foldGroups(byFile) {
5390
+ const groups = [];
5391
+ for (const [fileId2, symbols] of byFile) {
5392
+ const kept = symbols.slice(0, MAX_SYMBOLS_PER_GROUP);
5393
+ groups.push({
5394
+ fileId: fileId2,
5395
+ symbols: kept,
5396
+ totalConsumers: kept.reduce((n, s) => n + s.consumerCount, 0)
5397
+ });
5398
+ }
5399
+ return groups.sort(
5400
+ (a, b) => b.symbols.length - a.symbols.length || b.totalConsumers - a.totalConsumers || (a.fileId < b.fileId ? -1 : 1)
5401
+ ).slice(0, MAX_GROUPS);
5402
+ }
5403
+
4696
5404
  // src/read.ts
4697
5405
  function listNodes(store, snapshotId, opts) {
4698
5406
  return store.listNodes(snapshotId, opts);
@@ -4788,7 +5496,7 @@ var SOURCE = [
4788
5496
  direction: "higher-worse",
4789
5497
  absent: "exclude",
4790
5498
  source: "source-metrics",
4791
- description: "Highest cyclomatic complexity of any function in the file."
5499
+ description: "Highest cyclomatic complexity of any function in the file; points attention at branchy code."
4792
5500
  },
4793
5501
  {
4794
5502
  name: "cyclomatic_sum",
@@ -4798,7 +5506,7 @@ var SOURCE = [
4798
5506
  direction: "higher-worse",
4799
5507
  absent: "zero",
4800
5508
  source: "source-metrics",
4801
- description: "Cyclomatic complexity summed over the file's functions."
5509
+ description: "Cyclomatic complexity summed over the file's functions; points attention at branchy code."
4802
5510
  },
4803
5511
  {
4804
5512
  name: "cognitive_max",
@@ -4808,7 +5516,7 @@ var SOURCE = [
4808
5516
  direction: "higher-worse",
4809
5517
  absent: "exclude",
4810
5518
  source: "source-metrics",
4811
- description: "Highest cognitive complexity of any function in the file."
5519
+ description: "Highest cognitive complexity of any function in the file, an estimate of comprehension friction."
4812
5520
  },
4813
5521
  {
4814
5522
  name: "cognitive_sum",
@@ -4818,7 +5526,7 @@ var SOURCE = [
4818
5526
  direction: "higher-worse",
4819
5527
  absent: "zero",
4820
5528
  source: "source-metrics",
4821
- description: "Cognitive complexity summed over the file's functions."
5529
+ description: "Cognitive complexity summed over the file's functions, an estimate of comprehension friction."
4822
5530
  },
4823
5531
  {
4824
5532
  name: "max_nesting_depth",
@@ -4830,6 +5538,16 @@ var SOURCE = [
4830
5538
  source: "source-metrics",
4831
5539
  description: "Deepest block nesting inside any function in the file."
4832
5540
  },
5541
+ {
5542
+ name: "jsx_depth_max",
5543
+ unit: "count",
5544
+ appliesTo: FILE,
5545
+ rollup: "max",
5546
+ direction: "higher-worse",
5547
+ absent: "exclude",
5548
+ source: "source-metrics",
5549
+ description: "Deepest JSX element tree in the file: the max of symbol_jsx_depth over its functions and module-scope JSX. Written only when > 0."
5550
+ },
4833
5551
  {
4834
5552
  name: "symbol_cognitive",
4835
5553
  unit: "count",
@@ -4838,7 +5556,7 @@ var SOURCE = [
4838
5556
  direction: "higher-worse",
4839
5557
  absent: "exclude",
4840
5558
  source: "source-metrics",
4841
- description: "Cognitive complexity of the function a symbol names; the max when several functions share the name."
5559
+ description: "Cognitive complexity (comprehension friction) of the function a symbol names; the max when several functions share the name."
4842
5560
  },
4843
5561
  {
4844
5562
  name: "symbol_cyclomatic",
@@ -4848,7 +5566,7 @@ var SOURCE = [
4848
5566
  direction: "higher-worse",
4849
5567
  absent: "exclude",
4850
5568
  source: "source-metrics",
4851
- description: "Cyclomatic complexity of the function a symbol names; the max when several functions share the name."
5569
+ description: "Cyclomatic complexity (an attention pointer) of the function a symbol names; the max when several functions share the name."
4852
5570
  },
4853
5571
  {
4854
5572
  name: "symbol_loc",
@@ -4870,6 +5588,16 @@ var SOURCE = [
4870
5588
  source: "source-metrics",
4871
5589
  description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
4872
5590
  },
5591
+ {
5592
+ name: "symbol_jsx_depth",
5593
+ unit: "count",
5594
+ appliesTo: SYMBOL,
5595
+ rollup: "max",
5596
+ direction: "higher-worse",
5597
+ absent: "exclude",
5598
+ source: "source-metrics",
5599
+ description: "Most JSX elements on one ancestor chain in the function, fragments not counted. Walks into {\u2026} and inline callbacks, stops at a nested named function; attribute JSX sits one below its owner. Written only when > 0."
5600
+ },
4873
5601
  {
4874
5602
  name: "symbol_comment_lines",
4875
5603
  unit: "lines",
@@ -5116,7 +5844,7 @@ var HISTORY = [
5116
5844
  absent: "exclude",
5117
5845
  source: "history",
5118
5846
  windowed: true,
5119
- description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime. A hotspot discount."
5847
+ description: "min(1, file age / window) for a file that churned in the {w} window; 1 for lifetime. Discounts the hotspot score, churn \xD7 complexity after Adam Tornhill and CodeScene."
5120
5848
  },
5121
5849
  {
5122
5850
  name: "file_age_days",
@@ -5259,39 +5987,6 @@ function indexMetrics(metrics) {
5259
5987
  return metricsByNode;
5260
5988
  }
5261
5989
 
5262
- // src/check/patterns.ts
5263
- function patternToRegex(pattern) {
5264
- if (!pattern.includes("*")) {
5265
- return new RegExp(escapeRegex(pattern));
5266
- }
5267
- let out = "";
5268
- for (let i = 0; i < pattern.length; i++) {
5269
- const c = pattern[i];
5270
- if (c === "*" && pattern[i + 1] === "*") {
5271
- out += ".*";
5272
- i++;
5273
- } else if (c === "*") {
5274
- out += "[^/]*";
5275
- } else {
5276
- out += escapeRegex(c);
5277
- }
5278
- }
5279
- return new RegExp(`^${out}$`);
5280
- }
5281
- function compilePatterns(patterns) {
5282
- if (!patterns) return [];
5283
- return patterns.map(patternToRegex);
5284
- }
5285
- function matchesAny(value, patterns) {
5286
- for (const rx of patterns) {
5287
- if (rx.test(value)) return true;
5288
- }
5289
- return false;
5290
- }
5291
- function escapeRegex(s) {
5292
- return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
5293
- }
5294
-
5295
5990
  // src/check/rule-helpers.ts
5296
5991
  function severityOf(rule) {
5297
5992
  return rule.severity ?? "error";
@@ -5300,8 +5995,8 @@ function formatNumber(n) {
5300
5995
  if (Number.isInteger(n)) return String(n);
5301
5996
  return n.toFixed(3).replace(/\.?0+$/, "");
5302
5997
  }
5303
- function isImportEdge(edge) {
5304
- return edge.kind === "imports" || edge.kind === "re-exports";
5998
+ function isImportEdge(edge2) {
5999
+ return edge2.kind === "imports" || edge2.kind === "re-exports";
5305
6000
  }
5306
6001
  function longestPrefixMatcher(prefixes) {
5307
6002
  const byLength = [...prefixes].sort((a, b) => b.length - a.length);
@@ -5324,11 +6019,17 @@ function layerIndex(layers) {
5324
6019
  function runLayeredDepsRule(rule, ctx) {
5325
6020
  const packageLayer = layerIndex(rule.layers);
5326
6021
  const packageOf = longestPrefixMatcher([...packageLayer.keys()]);
6022
+ const excludedRoles = new Set(rule.excludeRoles ?? []);
6023
+ const hasExcludedRole = (id) => {
6024
+ const role = ctx.nodesById.get(id)?.role;
6025
+ return role !== void 0 && excludedRoles.has(role);
6026
+ };
5327
6027
  const out = [];
5328
- for (const edge of ctx.edges) {
5329
- if (!isImportEdge(edge)) continue;
5330
- const srcPkg = packageOf(edge.srcId);
5331
- const dstPkg = packageOf(edge.dstId);
6028
+ for (const edge2 of ctx.edges) {
6029
+ if (!isImportEdge(edge2)) continue;
6030
+ if (hasExcludedRole(edge2.srcId) || hasExcludedRole(edge2.dstId)) continue;
6031
+ const srcPkg = packageOf(edge2.srcId);
6032
+ const dstPkg = packageOf(edge2.dstId);
5332
6033
  if (srcPkg === null || dstPkg === null) continue;
5333
6034
  const srcLayer = packageLayer.get(srcPkg);
5334
6035
  const dstLayer = packageLayer.get(dstPkg);
@@ -5336,9 +6037,9 @@ function runLayeredDepsRule(rule, ctx) {
5336
6037
  out.push({
5337
6038
  ruleId: rule.id,
5338
6039
  severity: severityOf(rule),
5339
- nodeId: edge.srcId,
5340
- destinationId: edge.dstId,
5341
- message: `${srcPkg} (layer ${srcLayer}) imports ${dstPkg} (layer ${dstLayer})`
6040
+ nodeId: edge2.srcId,
6041
+ destinationId: edge2.dstId,
6042
+ message: `${edge2.srcId} (${srcPkg}, layer ${srcLayer}) imports ${edge2.dstId} (${dstPkg}, layer ${dstLayer})`
5342
6043
  });
5343
6044
  }
5344
6045
  return out;
@@ -5346,31 +6047,32 @@ function runLayeredDepsRule(rule, ctx) {
5346
6047
  function runForbidImportRule(rule, ctx) {
5347
6048
  const fromRx = compilePatterns([rule.from]);
5348
6049
  const toRx = compilePatterns([rule.to]);
6050
+ const exceptRx = compilePatterns(rule.except);
5349
6051
  const out = [];
5350
- for (const edge of ctx.edges) {
5351
- if (!isImportEdge(edge)) continue;
5352
- if (!matchesAny(edge.srcId, fromRx)) continue;
5353
- if (!matchesAny(edge.dstId, toRx)) continue;
6052
+ for (const edge2 of ctx.edges) {
6053
+ if (!isImportEdge(edge2)) continue;
6054
+ if (!matchesAny(edge2.srcId, fromRx)) continue;
6055
+ if (!matchesAny(edge2.dstId, toRx) || matchesAny(edge2.dstId, exceptRx)) continue;
5354
6056
  out.push({
5355
6057
  ruleId: rule.id,
5356
6058
  severity: severityOf(rule),
5357
- nodeId: edge.srcId,
5358
- destinationId: edge.dstId,
5359
- message: `${edge.srcId} imports ${edge.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
6059
+ nodeId: edge2.srcId,
6060
+ destinationId: edge2.dstId,
6061
+ message: `${edge2.srcId} imports ${edge2.dstId} (forbidden: ${rule.from} \u2192 ${rule.to})`
5360
6062
  });
5361
6063
  }
5362
6064
  return out;
5363
6065
  }
5364
6066
  function importersByDestination(edges) {
5365
6067
  const importersByDst = /* @__PURE__ */ new Map();
5366
- for (const edge of edges) {
5367
- if (!isImportEdge(edge)) continue;
5368
- let list = importersByDst.get(edge.dstId);
6068
+ for (const edge2 of edges) {
6069
+ if (!isImportEdge(edge2)) continue;
6070
+ let list = importersByDst.get(edge2.dstId);
5369
6071
  if (!list) {
5370
6072
  list = [];
5371
- importersByDst.set(edge.dstId, list);
6073
+ importersByDst.set(edge2.dstId, list);
5372
6074
  }
5373
- list.push(edge.srcId);
6075
+ list.push(edge2.srcId);
5374
6076
  }
5375
6077
  return importersByDst;
5376
6078
  }
@@ -5605,13 +6307,6 @@ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
5605
6307
  }
5606
6308
  return keys;
5607
6309
  }
5608
- function violationKey(v) {
5609
- return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
5610
- }
5611
- function rebasedViolationKey(v, resolve4) {
5612
- const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
5613
- return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
5614
- }
5615
6310
  function countByOriginAndSeverity(violations) {
5616
6311
  const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
5617
6312
  for (const v of violations) {
@@ -5829,7 +6524,7 @@ function validateRule(raw, index, warn) {
5829
6524
  case "forbid-import":
5830
6525
  return assertForbidImport(r);
5831
6526
  case "layered-deps":
5832
- return assertLayeredDeps(r);
6527
+ return assertLayeredDeps(r, warn);
5833
6528
  case "no-internal-only-barrels":
5834
6529
  return assertNoInternalOnlyBarrels(r);
5835
6530
  default:
@@ -5851,11 +6546,11 @@ function assertNoInternalOnlyBarrels(r) {
5851
6546
  type: "no-internal-only-barrels",
5852
6547
  id: r.id,
5853
6548
  packageRoots: r.packageRoots,
5854
- severity: r.severity,
5855
- exclude: parseStringArray(r.exclude)
6549
+ severity: parseSeverity(r),
6550
+ exclude: parseExclude(r)
5856
6551
  };
5857
6552
  }
5858
- function assertLayeredDeps(r) {
6553
+ function assertLayeredDeps(r, warn) {
5859
6554
  if (!Array.isArray(r.layers) || r.layers.length < 2) {
5860
6555
  throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
5861
6556
  }
@@ -5880,7 +6575,8 @@ function assertLayeredDeps(r) {
5880
6575
  type: "layered-deps",
5881
6576
  id: r.id,
5882
6577
  layers: r.layers,
5883
- severity: r.severity
6578
+ severity: parseSeverity(r),
6579
+ excludeRoles: parseRoleArray(r.id, r.excludeRoles, warn)
5884
6580
  };
5885
6581
  }
5886
6582
  function assertMetricProductMax(r, warn) {
@@ -5899,9 +6595,9 @@ function assertMetricProductMax(r, warn) {
5899
6595
  id: ruleId,
5900
6596
  metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
5901
6597
  max: r.max,
5902
- kind: r.kind,
5903
- severity: r.severity,
5904
- exclude: parseStringArray(r.exclude),
6598
+ kind: parseOptionalKind(r),
6599
+ severity: parseSeverity(r),
6600
+ exclude: parseExclude(r),
5905
6601
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5906
6602
  };
5907
6603
  }
@@ -5914,9 +6610,9 @@ function assertMetricMax(r, warn) {
5914
6610
  id: ruleId,
5915
6611
  metric: healMetricName(r.metric, ruleId, warn),
5916
6612
  max: r.max,
5917
- kind: r.kind,
5918
- severity: r.severity,
5919
- exclude: parseStringArray(r.exclude),
6613
+ kind: parseOptionalKind(r),
6614
+ severity: parseSeverity(r),
6615
+ exclude: parseExclude(r),
5920
6616
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5921
6617
  };
5922
6618
  }
@@ -5929,18 +6625,16 @@ function assertMetricMin(r, warn) {
5929
6625
  id: ruleId,
5930
6626
  metric: healMetricName(r.metric, ruleId, warn),
5931
6627
  min: r.min,
5932
- kind: r.kind,
5933
- severity: r.severity,
5934
- exclude: parseStringArray(r.exclude),
6628
+ kind: parseOptionalKind(r),
6629
+ severity: parseSeverity(r),
6630
+ exclude: parseExclude(r),
5935
6631
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5936
6632
  };
5937
6633
  }
5938
6634
  var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
5939
6635
  function assertMetricOutlier(r, warn) {
5940
6636
  if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
5941
- if (typeof r.kind !== "string" || !NODE_KINDS.has(r.kind)) {
5942
- throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
5943
- }
6637
+ if (!isNodeKind(r.kind)) throw kindError(r.id);
5944
6638
  if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
5945
6639
  throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
5946
6640
  }
@@ -5963,7 +6657,7 @@ function assertMetricOutlier(r, warn) {
5963
6657
  minSample: r.minSample,
5964
6658
  floor: r.floor,
5965
6659
  rankNonZero: r.rankNonZero,
5966
- severity: r.severity
6660
+ severity: parseSeverity(r)
5967
6661
  };
5968
6662
  }
5969
6663
  function assertForbidImport(r) {
@@ -5974,15 +6668,43 @@ function assertForbidImport(r) {
5974
6668
  id: r.id,
5975
6669
  from: r.from,
5976
6670
  to: r.to,
5977
- severity: r.severity
6671
+ except: parseStringList(r, "except"),
6672
+ severity: parseSeverity(r)
5978
6673
  };
5979
6674
  }
5980
- function parseStringArray(value) {
5981
- return Array.isArray(value) ? value : void 0;
6675
+ function isNodeKind(value) {
6676
+ return typeof value === "string" && NODE_KINDS.has(value);
6677
+ }
6678
+ function kindError(ruleId) {
6679
+ return new Error(`${ruleId}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
6680
+ }
6681
+ function parseOptionalKind(r) {
6682
+ if (r.kind === void 0) return void 0;
6683
+ if (!isNodeKind(r.kind)) throw kindError(r.id);
6684
+ return r.kind;
6685
+ }
6686
+ function parseSeverity(r) {
6687
+ if (r.severity === void 0 || r.severity === "error" || r.severity === "warning") {
6688
+ return r.severity;
6689
+ }
6690
+ throw new Error(`${r.id}: severity must be "error" or "warning"`);
6691
+ }
6692
+ function parseExclude(r) {
6693
+ return parseStringList(r, "exclude");
6694
+ }
6695
+ function parseStringList(r, key) {
6696
+ const value = r[key];
6697
+ if (value === void 0) return void 0;
6698
+ if (!Array.isArray(value) || !value.every((e) => typeof e === "string")) {
6699
+ throw new Error(`${r.id}: ${key} must be an array of strings`);
6700
+ }
6701
+ return value;
5982
6702
  }
5983
6703
  var ROLE_VALUES = /* @__PURE__ */ new Set([
5984
6704
  "test",
5985
6705
  "fixture",
6706
+ "story",
6707
+ "lab",
5986
6708
  "barrel",
5987
6709
  "types",
5988
6710
  "config",
@@ -6013,7 +6735,7 @@ function healRole(entry, ruleId, warn) {
6013
6735
  }
6014
6736
 
6015
6737
  // src/check/run.ts
6016
- import { readFile as readFile2 } from "fs/promises";
6738
+ import { readFile } from "fs/promises";
6017
6739
  function resolveSnapshot(store, spec) {
6018
6740
  if (typeof spec === "number" || /^\d+$/.test(spec)) {
6019
6741
  const snap2 = store.getSnapshot(Number(spec));
@@ -6034,18 +6756,18 @@ function checkSnapshot(store, options) {
6034
6756
  });
6035
6757
  return { snapshot, baselineSnapshot, result };
6036
6758
  }
6037
- async function loadCheckRules(path11, options = {}) {
6759
+ async function loadCheckRules(path14, options = {}) {
6038
6760
  let raw;
6039
6761
  try {
6040
- raw = await readFile2(path11, "utf8");
6762
+ raw = await readFile(path14, "utf8");
6041
6763
  } catch (err) {
6042
- throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
6764
+ throw new Error(`Cannot read rules file at ${path14}: ${errorMessage(err)}`);
6043
6765
  }
6044
6766
  let parsed;
6045
6767
  try {
6046
6768
  parsed = JSON.parse(raw);
6047
6769
  } catch (err) {
6048
- throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
6770
+ throw new Error(`Invalid JSON in ${path14}: ${errorMessage(err)}`);
6049
6771
  }
6050
6772
  return validateRules(parsed, options);
6051
6773
  }
@@ -6071,13 +6793,13 @@ function indexEdges(edges) {
6071
6793
  function diffNodes(fromNodes, toNodes, aliases) {
6072
6794
  const toIndex = new Map(toNodes.map((n) => [n.id, n]));
6073
6795
  const matchedToIds = /* @__PURE__ */ new Set();
6074
- const removed = [];
6796
+ const removed2 = [];
6075
6797
  const renamed = [];
6076
6798
  for (const fromNode of fromNodes) {
6077
6799
  const newId = resolveId(fromNode.id, aliases);
6078
6800
  const toNode = toIndex.get(newId);
6079
6801
  if (!toNode) {
6080
- removed.push(fromNode);
6802
+ removed2.push(fromNode);
6081
6803
  continue;
6082
6804
  }
6083
6805
  matchedToIds.add(newId);
@@ -6086,7 +6808,7 @@ function diffNodes(fromNodes, toNodes, aliases) {
6086
6808
  }
6087
6809
  }
6088
6810
  const added = toNodes.filter((toNode) => !matchedToIds.has(toNode.id));
6089
- return { added, removed, renamed, commonNewIds: matchedToIds };
6811
+ return { added, removed: removed2, renamed, commonNewIds: matchedToIds };
6090
6812
  }
6091
6813
  function diffEdges(fromEdges, toEdges, aliases) {
6092
6814
  const remappedKey = (e) => edgeKey2(resolveId(e.srcId, aliases), resolveId(e.dstId, aliases), e.kind);
@@ -6096,8 +6818,8 @@ function diffEdges(fromEdges, toEdges, aliases) {
6096
6818
  for (const [k, e] of toIndex) {
6097
6819
  if (!fromKeys.has(k)) added.push(e);
6098
6820
  }
6099
- const removed = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
6100
- return { added, removed };
6821
+ const removed2 = fromEdges.filter((e) => !toIndex.has(remappedKey(e)));
6822
+ return { added, removed: removed2 };
6101
6823
  }
6102
6824
  function indexFromMetrics(metrics, aliases) {
6103
6825
  const fromIndex = /* @__PURE__ */ new Map();
@@ -6175,44 +6897,16 @@ function diffCheckResults(store, options) {
6175
6897
  fromSnapshotId: options.fromSnapshotId,
6176
6898
  toSnapshotId: options.toSnapshotId,
6177
6899
  rulesEvaluated: options.rules.length,
6178
- ...bucketViolations(indexByKey(from.violations, chain.resolve), indexByKey(to.violations))
6900
+ ...bucketViolations(from.violations, to.violations, chain.resolve)
6179
6901
  };
6180
6902
  }
6181
- function bucketViolations(fromByKey, toByKey) {
6182
- const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
6183
- for (const [key, toV] of toByKey) {
6184
- const fromV = fromByKey.get(key);
6185
- if (!fromV) {
6186
- buckets.newViolations.push(toV);
6187
- continue;
6188
- }
6189
- const delta = computeDelta(fromV, toV);
6190
- const entry = { from: fromV, to: toV, delta };
6191
- buckets.unchanged.push(entry);
6192
- if (delta !== null && delta > 0) buckets.worsened.push(entry);
6193
- else if (delta !== null && delta < 0) buckets.improved.push(entry);
6194
- }
6195
- for (const [key, fromV] of fromByKey) {
6196
- if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
6197
- }
6198
- return buckets;
6199
- }
6200
- function indexByKey(violations, resolve4) {
6201
- const out = /* @__PURE__ */ new Map();
6202
- for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
6203
- return out;
6204
- }
6205
- function computeDelta(from, to) {
6206
- if (from.value === void 0 || to.value === void 0) return null;
6207
- return to.value - from.value;
6208
- }
6209
6903
 
6210
6904
  // src/diff/footprint.ts
6211
- var DEFAULT_MIN_CO_IMPORTS2 = 2;
6905
+ var DEFAULT_MIN_CO_IMPORTS = 2;
6212
6906
  function computeFootprints(graph, options = {}) {
6213
6907
  const edges = referenceEdges(graph.edges, options.ignoreConsumer);
6214
6908
  const consumers = consumersBySymbol(edges);
6215
- const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS2);
6909
+ const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS);
6216
6910
  const out = /* @__PURE__ */ new Map();
6217
6911
  for (const node of graph.nodes) {
6218
6912
  if (node.kind !== "symbol") continue;
@@ -6264,6 +6958,119 @@ function partnersBySymbol(edges, minCoImports) {
6264
6958
  return new Map([...partners].map(([id, set]) => [id, [...set].sort()]));
6265
6959
  }
6266
6960
 
6961
+ // src/diff/footprint-diff.ts
6962
+ var PART_REASONS = ["signature", "consumers", "coupling"];
6963
+ function diffFootprints(store, options) {
6964
+ const { fromSnapshotId, toSnapshotId } = options;
6965
+ const resolve5 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
6966
+ const fromGraph = loadSymbolLayer(store, fromSnapshotId);
6967
+ const toGraph = loadSymbolLayer(store, toSnapshotId);
6968
+ const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve5);
6969
+ const before = computeFootprints(remapGraph(fromGraph, resolve5, new Set(mergedAway)), options);
6970
+ const after = computeFootprints(toGraph, options);
6971
+ const changes = [
6972
+ ...changedOrAdded(after, before, previousIds),
6973
+ ...removed(before, after, previousIds, resolve5),
6974
+ ...mergedAway.map((id) => ({ symbolId: id, status: "removed", reasons: [], fileId: resolve5(declaringFile(id)) }))
6975
+ ].sort((a, b) => compare3(a.symbolId, b.symbolId));
6976
+ const files = [...new Set(changes.map((c) => c.fileId))].sort(compare3);
6977
+ return { fromSnapshotId, toSnapshotId, changes, files };
6978
+ }
6979
+ function loadSymbolLayer(store, snapshotId) {
6980
+ return {
6981
+ nodes: store.listNodes(snapshotId, { includeSymbols: true }).filter((n) => n.kind === "symbol"),
6982
+ edges: store.listEdges(snapshotId, { includeReferences: true })
6983
+ };
6984
+ }
6985
+ function symbolAwareResolver(aliases) {
6986
+ return (id) => {
6987
+ const direct = aliases.resolve(id);
6988
+ if (direct !== id) return direct;
6989
+ const parsed = parseSymbolId(id);
6990
+ return parsed ? symbolId(aliases.resolve(parsed.fileId), parsed.name) : id;
6991
+ };
6992
+ }
6993
+ function remapGraph(graph, resolve5, losers) {
6994
+ return {
6995
+ nodes: graph.nodes.filter((n) => !losers.has(n.id)).map((n) => ({ ...n, id: resolve5(n.id) })),
6996
+ edges: graph.edges.filter((e) => !losers.has(e.srcId) && !losers.has(e.dstId)).map((e) => ({ ...e, srcId: resolve5(e.srcId), dstId: resolve5(e.dstId) }))
6997
+ };
6998
+ }
6999
+ function mergeSources(nodes, resolve5) {
7000
+ const groups = /* @__PURE__ */ new Map();
7001
+ for (const node of nodes) {
7002
+ const id = resolve5(node.id);
7003
+ groups.set(id, [...groups.get(id) ?? [], node.id]);
7004
+ }
7005
+ const previousIds = /* @__PURE__ */ new Map();
7006
+ const mergedAway = [];
7007
+ for (const [id, sources] of groups) {
7008
+ const keeper = sources.includes(id) ? id : [...sources].sort(compare3)[0];
7009
+ if (keeper !== id) previousIds.set(id, keeper);
7010
+ mergedAway.push(...sources.filter((source) => source !== keeper));
7011
+ }
7012
+ return { previousIds, mergedAway };
7013
+ }
7014
+ function changedOrAdded(after, before, previousIds) {
7015
+ const out = [];
7016
+ for (const [id, footprint] of after) {
7017
+ const prior = before.get(id);
7018
+ const previousId = previousIds.get(id);
7019
+ if (!prior) {
7020
+ out.push({ symbolId: id, status: "added", reasons: [], fileId: declaringFile(id) });
7021
+ continue;
7022
+ }
7023
+ const reasons = reasonsFor(prior, footprint, previousId !== void 0);
7024
+ if (reasons.length === 0) continue;
7025
+ out.push({ symbolId: id, ...previousId ? { previousId } : {}, status: "changed", reasons, fileId: declaringFile(id) });
7026
+ }
7027
+ return out;
7028
+ }
7029
+ function reasonsFor(prior, current, renamed) {
7030
+ const reasons = PART_REASONS.filter((part) => prior.parts[part] !== current.parts[part]);
7031
+ if (renamed) reasons.push("renamed");
7032
+ return reasons;
7033
+ }
7034
+ function removed(before, after, previousIds, resolve5) {
7035
+ const out = [];
7036
+ for (const id of before.keys()) {
7037
+ if (after.has(id)) continue;
7038
+ const fromId = previousIds.get(id) ?? id;
7039
+ out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve5(declaringFile(fromId)) });
7040
+ }
7041
+ return out;
7042
+ }
7043
+ function declaringFile(id) {
7044
+ return parseSymbolId(id)?.fileId ?? id;
7045
+ }
7046
+ function compare3(a, b) {
7047
+ return a < b ? -1 : a > b ? 1 : 0;
7048
+ }
7049
+
7050
+ // src/diff/gate.ts
7051
+ function unitProvenance(input) {
7052
+ return {
7053
+ unitId: input.unit.unitId,
7054
+ commit: input.snapshot.commitHash,
7055
+ symbolSetHash: symbolSetHash(input.unit, input.footprints),
7056
+ model: input.model ?? null
7057
+ };
7058
+ }
7059
+ function gateUnits(input) {
7060
+ const priorHash = new Map(input.prior.map((p) => [p.unitId, p.symbolSetHash]));
7061
+ const current = new Set(input.units.map((u) => u.unitId));
7062
+ const regenerate = [];
7063
+ const skip = [];
7064
+ for (const unit of input.units) {
7065
+ const before = priorHash.get(unit.unitId);
7066
+ if (before === void 0) regenerate.push({ unitId: unit.unitId, reason: "new" });
7067
+ else if (before !== symbolSetHash(unit, input.footprints)) regenerate.push({ unitId: unit.unitId, reason: "changed" });
7068
+ else skip.push(unit.unitId);
7069
+ }
7070
+ const orphaned = [...priorHash.keys()].filter((id) => !current.has(id));
7071
+ return { regenerate, skip, orphaned };
7072
+ }
7073
+
6267
7074
  // src/embeddings/corpus.ts
6268
7075
  import { contentHashOf } from "@titan-design/store-sqlite";
6269
7076
  function buildEmbedText(signature, purpose) {
@@ -6334,17 +7141,17 @@ function storeVectors(store, model, rows) {
6334
7141
  async function embedTextsCached(store, embedder, texts) {
6335
7142
  const hashes = texts.map((t) => hashEmbedText(t));
6336
7143
  const byHash = loadVectorsByHash(store, embedder.model, hashes);
6337
- const missing = /* @__PURE__ */ new Map();
7144
+ const missing2 = /* @__PURE__ */ new Map();
6338
7145
  texts.forEach((text, i) => {
6339
- if (!byHash.has(hashes[i])) missing.set(hashes[i], text);
7146
+ if (!byHash.has(hashes[i])) missing2.set(hashes[i], text);
6340
7147
  });
6341
- const pending = [...missing.entries()];
7148
+ const pending = [...missing2.entries()];
6342
7149
  for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
6343
7150
  const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
6344
7151
  storeVectors(store, embedder.model, rows);
6345
7152
  for (const r of rows) byHash.set(r.textHash, r.vector);
6346
7153
  }
6347
- return { byHash, newHashes: new Set(missing.keys()) };
7154
+ return { byHash, newHashes: new Set(missing2.keys()) };
6348
7155
  }
6349
7156
  async function embedBatch(embedder, batch) {
6350
7157
  const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
@@ -6514,7 +7321,7 @@ function pushMulti(map, key, value) {
6514
7321
  }
6515
7322
 
6516
7323
  // src/conventions/areas.ts
6517
- import * as path10 from "path";
7324
+ import * as path13 from "path";
6518
7325
  import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
6519
7326
  var DEFAULT_MIN_SIZE = 3;
6520
7327
  var PROMPT_FILE_CAP = 30;
@@ -6598,7 +7405,7 @@ function rankByDegree(members, edges) {
6598
7405
  function dominantDirectory(files) {
6599
7406
  const counts = /* @__PURE__ */ new Map();
6600
7407
  for (const f of files) {
6601
- const dir = path10.posix.dirname(f);
7408
+ const dir = path13.posix.dirname(f);
6602
7409
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
6603
7410
  }
6604
7411
  const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
@@ -6718,14 +7525,21 @@ function toMatch(area, score) {
6718
7525
  export {
6719
7526
  ALIAS_BASE_ATTR,
6720
7527
  ALL_ROLES,
7528
+ BUNDLE_SCHEMA_VERSION,
6721
7529
  COMMUNITY_SUMMARY_NAMESPACE,
7530
+ SCHEMA_VERSION2 as CONTEXT_SCHEMA_VERSION,
6722
7531
  COVERAGE_METRIC_NAME,
6723
7532
  CodeGraphStore,
6724
7533
  DEAD_CODE_METRIC_NAMES,
6725
7534
  DEFAULT_CHURN_WINDOWS,
7535
+ DEFAULT_HEALTH_WEIGHTS,
7536
+ DEFAULT_MAX_LINE_CHARS,
7537
+ DEFAULT_MAX_PACKAGE_SIZE,
6726
7538
  DEFAULT_OUTLIER_MIN_SAMPLE,
6727
7539
  DOMAIN_DDL,
7540
+ ELISION_MARKER,
6728
7541
  EXCEPTION_METRIC_NAMES,
7542
+ EXTERNAL_BUCKET,
6729
7543
  FINDING_DDL,
6730
7544
  GROWTH_RISK_METRIC_NAMES,
6731
7545
  INDEX_VERSION,
@@ -6740,35 +7554,55 @@ export {
6740
7554
  SYMBOL_EMBEDDING_NAMESPACE,
6741
7555
  SYMBOL_ID_SEP,
6742
7556
  SYMBOL_METRIC_NAMES,
7557
+ TRUNCATION_SUFFIX,
6743
7558
  TsMorphGraphExtractor,
7559
+ UNIMPORTED_ROLES,
7560
+ aggregateEdges,
6744
7561
  aggregateMetrics,
6745
7562
  aliasChain,
6746
7563
  annotateRoles,
6747
7564
  attributeCoverage,
7565
+ bucketFilesByPackage,
7566
+ bucketViolations,
6748
7567
  buildAliases,
7568
+ buildBlastRadius,
7569
+ buildCentralFiles,
7570
+ buildContextBundle,
7571
+ buildContextDossier,
6749
7572
  buildConventionAreas,
6750
7573
  buildEmbedText,
6751
7574
  buildFileModuleNodes,
7575
+ buildHotExports,
6752
7576
  buildIndexerMetrics,
6753
7577
  buildLineage,
7578
+ buildNodeMetrics,
7579
+ buildReportContext,
7580
+ buildSymbolCouplingPayload,
7581
+ busFactorOf,
6754
7582
  canonicalEdgeKind,
6755
7583
  canonicalMetricName,
6756
7584
  canonicalRole,
6757
7585
  carryForwardVerdicts,
6758
7586
  checkSnapshot,
7587
+ classifyCoupling,
6759
7588
  classifyRole,
6760
7589
  collectDeclaredNames,
6761
7590
  collectDeclaredSpans,
7591
+ collectNodeMetrics,
7592
+ collectSymbolUtil,
6762
7593
  compilePatterns,
7594
+ computeArch,
6763
7595
  computeDeadCodeMetrics,
6764
7596
  computeDeepAst,
6765
7597
  computeFootprints,
6766
7598
  computeGrowthRiskMetrics,
7599
+ computeHealth,
6767
7600
  computeMetrics,
6768
7601
  computePageRank,
6769
7602
  computePartitionQuality,
6770
7603
  computeRecencyWindows,
6771
7604
  computeRelevance,
7605
+ computeReportDrift,
6772
7606
  computeRoleHints,
6773
7607
  computeSourceMetrics,
6774
7608
  computeSymbolConsumers,
@@ -6783,6 +7617,7 @@ export {
6783
7617
  detectGitToplevel,
6784
7618
  detectRenames,
6785
7619
  diffCheckResults,
7620
+ diffFootprints,
6786
7621
  diffSnapshots,
6787
7622
  edgeWeight,
6788
7623
  embedSnapshot,
@@ -6790,22 +7625,29 @@ export {
6790
7625
  externalId,
6791
7626
  externalToFinding,
6792
7627
  fileId,
7628
+ filteredFileIds,
6793
7629
  findConventions,
6794
7630
  findSimilarCapability,
6795
7631
  findingKey,
7632
+ gateUnits,
6796
7633
  getConventionMap,
6797
7634
  getEdgeWeight,
6798
7635
  getLanguageFromPath3 as getLanguageFromPath,
6799
7636
  getSupportedLanguages,
7637
+ gitTreeSource,
6800
7638
  groupTestsBySource,
6801
7639
  hashContent,
6802
7640
  hashEmbedText,
6803
7641
  hashText,
7642
+ hotspotScoreOf,
7643
+ importMarker,
6804
7644
  indexPaths,
6805
7645
  invertBuckets,
6806
7646
  isGeneratedByHeuristic,
6807
7647
  isGeneratedFile,
6808
7648
  isInsideGitRepo,
7649
+ isMinifyLanguage,
7650
+ keepNode,
6809
7651
  keyFindings,
6810
7652
  lineagePath,
6811
7653
  linkTestsToSources,
@@ -6821,21 +7663,32 @@ export {
6821
7663
  loadGeneratedPatterns,
6822
7664
  loadHistoryMetrics,
6823
7665
  loadLineage,
7666
+ loadRoleGlobs,
7667
+ lookupMetric,
6824
7668
  matchesAny,
7669
+ minifySource,
6825
7670
  moduleId,
6826
7671
  normalizeFlaggedText,
6827
7672
  openCodeGraph,
7673
+ originalLine,
6828
7674
  packageId,
7675
+ packagesReferencedByEdges,
7676
+ pairKey,
6829
7677
  parentModuleId,
6830
- parseFile2 as parseFile,
7678
+ parseFile3 as parseFile,
6831
7679
  parseSymbolId,
6832
7680
  patternToRegex,
6833
7681
  percentileOf,
6834
7682
  planPrune,
6835
7683
  priorSnapshotForRef,
6836
7684
  pruneDanglingReferences,
7685
+ publicApiFiles,
6837
7686
  readSourceFiles,
6838
7687
  rebasedViolationKey,
7688
+ referencedNodes,
7689
+ renderBundleText,
7690
+ renderContextMarkdown,
7691
+ renderSignatureTree,
6839
7692
  resolveAlias,
6840
7693
  resolveBarrelEdges,
6841
7694
  resolveChurnWindows,
@@ -6845,7 +7698,7 @@ export {
6845
7698
  runPrune,
6846
7699
  saveFindings,
6847
7700
  saveVerdicts,
6848
- shouldIncludeFile2 as shouldIncludeFile,
7701
+ shouldIncludeFile3 as shouldIncludeFile,
6849
7702
  snapshotPageRank,
6850
7703
  snapshotReferenceEdges,
6851
7704
  snapshotRelevance,
@@ -6858,10 +7711,21 @@ export {
6858
7711
  symbolSetHash,
6859
7712
  testCoverageCountMetrics,
6860
7713
  toFindings,
7714
+ toSortedEdges,
7715
+ topBusFactorRisks,
7716
+ topCentralFiles,
7717
+ topDeadModules,
7718
+ topGrowthRisks,
7719
+ topHotspots,
7720
+ topTestCoverageRisks,
7721
+ topUntestedRisks,
7722
+ topUnusedExports,
6861
7723
  tryEmbedSnapshot,
7724
+ unitProvenance,
6862
7725
  validateRules,
6863
7726
  violationKey,
6864
7727
  walkSourceFiles,
6865
- windowSuffix
7728
+ windowSuffix,
7729
+ workingTreeSource
6866
7730
  };
6867
7731
  //# sourceMappingURL=index.js.map