@titan-design/code-graph 0.11.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;
@@ -1230,12 +1392,15 @@ var TsMorphGraphExtractor = class {
1230
1392
  tsConfigPath;
1231
1393
  project;
1232
1394
  ownsProject;
1395
+ source;
1233
1396
  retired = [];
1397
+ fileExists = (abs) => this.source.fileExists(abs);
1234
1398
  constructor(options) {
1235
1399
  this.repoRoot = options.repoRoot;
1236
1400
  this.tsConfigPath = options.tsConfigPath;
1237
1401
  this.project = options.project;
1238
1402
  this.ownsProject = options.project === void 0;
1403
+ this.source = options.source ?? workingTreeSource();
1239
1404
  }
1240
1405
  extract(file) {
1241
1406
  if (!isTypeScriptFile(file)) return [];
@@ -1259,13 +1424,16 @@ var TsMorphGraphExtractor = class {
1259
1424
  const calls = collectTsCallEdges(sourceFile, {
1260
1425
  repoRoot: this.repoRoot,
1261
1426
  srcFileId: fileId(this.repoRoot, sourceFile.getFilePath()),
1262
- localSymbols: new Set(symbolNodes2.map((n) => n.name))
1427
+ localSymbols: new Set(symbolNodes2.map((n) => n.name)),
1428
+ fileExists: this.fileExists
1263
1429
  });
1264
1430
  return [{ nodes: [...nodes, ...symbolNodes2, ...externalNodes], edges: [...edges, ...calls] }];
1265
1431
  }
1266
1432
  ensureProject() {
1267
1433
  if (this.project) return this.project;
1268
- 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,
1269
1437
  compilerOptions: {
1270
1438
  allowJs: true,
1271
1439
  target: ScriptTarget.ESNext,
@@ -1366,7 +1534,7 @@ var TsMorphGraphExtractor = class {
1366
1534
  if (targetSf) {
1367
1535
  const decls = targetSf.getExportedDeclarations().get(importedName);
1368
1536
  const originSf = decls && decls.length > 0 ? decls[0].getSourceFile() : targetSf;
1369
- const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath()));
1537
+ const originId = this.inRepoFileId(remapDistToSrc(originSf.getFilePath(), this.fileExists));
1370
1538
  return originId ? symbolId(originId, importedName) : null;
1371
1539
  }
1372
1540
  return resolveReExportOrigin(
@@ -1405,7 +1573,7 @@ var TsMorphGraphExtractor = class {
1405
1573
  }
1406
1574
  resolveInternal(target) {
1407
1575
  if (!target) return null;
1408
- return this.inRepoFileId(remapDistToSrc(target.getFilePath()));
1576
+ return this.inRepoFileId(remapDistToSrc(target.getFilePath(), this.fileExists));
1409
1577
  }
1410
1578
  /**
1411
1579
  * Resolve a relative specifier ts-morph failed to link by walking the
@@ -1433,14 +1601,13 @@ function isTypeScriptFile(file) {
1433
1601
  }
1434
1602
 
1435
1603
  // src/extractors/python-extractor.ts
1436
- import * as path4 from "path";
1437
- import { existsSync as existsSync2 } from "fs";
1604
+ import * as path5 from "path";
1438
1605
 
1439
1606
  // src/extractors/python-calls.ts
1440
- function collectPythonCallEdges(file, fileId2, resolve4) {
1607
+ function collectPythonCallEdges(file, fileId2, resolve5) {
1441
1608
  const declarations = /* @__PURE__ */ new Map();
1442
1609
  forEachDeclaration(file, (_name, qualifiedName, node) => declarations.set(qualifiedName, node));
1443
- const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve4) };
1610
+ const ctx = { fileId: fileId2, declarations, imports: fromImports(file.tree.rootNode, resolve5) };
1444
1611
  const agg = /* @__PURE__ */ new Map();
1445
1612
  walkScopes(file.tree.rootNode, true, (node, scope) => {
1446
1613
  if (node.type !== "call") return;
@@ -1504,11 +1671,11 @@ function parameterNames(def) {
1504
1671
  }
1505
1672
  return out;
1506
1673
  }
1507
- function fromImports(root, resolve4) {
1674
+ function fromImports(root, resolve5) {
1508
1675
  const out = /* @__PURE__ */ new Map();
1509
1676
  for (const stmt of root.descendantsOfType("import_from_statement")) {
1510
1677
  const moduleName = stmt?.childForFieldName("module_name")?.text;
1511
- const moduleFile = moduleName ? resolve4(moduleName) : null;
1678
+ const moduleFile = moduleName ? resolve5(moduleName) : null;
1512
1679
  if (!stmt || !moduleFile) continue;
1513
1680
  for (const imported of stmt.childrenForFieldName("name")) {
1514
1681
  const alias = imported?.type === "aliased_import" ? imported.childForFieldName("alias")?.text : void 0;
@@ -1550,13 +1717,15 @@ function addKeyword(site, arg) {
1550
1717
  // src/extractors/python-extractor.ts
1551
1718
  function* candidates(base) {
1552
1719
  yield `${base}.py`;
1553
- yield path4.join(base, "__init__.py");
1720
+ yield path5.join(base, "__init__.py");
1554
1721
  }
1555
1722
  var PythonGraphExtractor = class {
1556
- constructor(repoRoot) {
1723
+ constructor(repoRoot, source = workingTreeSource()) {
1557
1724
  this.repoRoot = repoRoot;
1725
+ this.source = source;
1558
1726
  }
1559
1727
  repoRoot;
1728
+ source;
1560
1729
  name = "python-graph";
1561
1730
  extract(file) {
1562
1731
  if (file.language !== "python") return [];
@@ -1588,12 +1757,12 @@ var PythonGraphExtractor = class {
1588
1757
  resolveDotted(fromAbs, specifier) {
1589
1758
  const leadingDots = /^\.*/.exec(specifier)?.[0].length ?? 0;
1590
1759
  const parts = specifier.slice(leadingDots).split(".").filter(Boolean);
1591
- const relativeBase = path4.resolve(path4.dirname(fromAbs), "../".repeat(Math.max(leadingDots - 1, 0)));
1592
- 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];
1593
1762
  for (const root of roots) {
1594
- for (const candidate of candidates(path4.join(root, ...parts))) {
1763
+ for (const candidate of candidates(path5.join(root, ...parts))) {
1595
1764
  const id = inRepoFileId(this.repoRoot, candidate);
1596
- if (id !== null && existsSync2(candidate)) return id;
1765
+ if (id !== null && this.source.fileExists(candidate)) return id;
1597
1766
  }
1598
1767
  }
1599
1768
  return null;
@@ -1637,9 +1806,10 @@ var LanguageExtractor = class {
1637
1806
  name = "language-graph";
1638
1807
  delegates;
1639
1808
  constructor(options) {
1809
+ const { repoRoot, tsConfigPath, source } = options;
1640
1810
  this.delegates = [
1641
- new TsMorphGraphExtractor({ repoRoot: options.repoRoot, tsConfigPath: options.tsConfigPath }),
1642
- new PythonGraphExtractor(options.repoRoot)
1811
+ new TsMorphGraphExtractor({ repoRoot, tsConfigPath, source }),
1812
+ new PythonGraphExtractor(repoRoot, source)
1643
1813
  ];
1644
1814
  }
1645
1815
  extract(file) {
@@ -1648,7 +1818,7 @@ var LanguageExtractor = class {
1648
1818
  };
1649
1819
 
1650
1820
  // src/git-renames.ts
1651
- import * as path5 from "path";
1821
+ import * as path6 from "path";
1652
1822
  function detectGitHead(repoRoot) {
1653
1823
  return runGit(repoRoot, ["rev-parse", "HEAD"]);
1654
1824
  }
@@ -1692,12 +1862,12 @@ function parseRenameOutput(text) {
1692
1862
  return out;
1693
1863
  }
1694
1864
  function classifyRename(oldPath, newPath) {
1695
- return path5.posix.dirname(oldPath) === path5.posix.dirname(newPath) ? "rename" : "move";
1865
+ return path6.posix.dirname(oldPath) === path6.posix.dirname(newPath) ? "rename" : "move";
1696
1866
  }
1697
1867
  function renameToAliases(repoRoot, pair) {
1698
1868
  const reason = classifyRename(pair.oldPath, pair.newPath);
1699
- const oldAbs = path5.join(repoRoot, pair.oldPath);
1700
- const newAbs = path5.join(repoRoot, pair.newPath);
1869
+ const oldAbs = path6.join(repoRoot, pair.oldPath);
1870
+ const newAbs = path6.join(repoRoot, pair.newPath);
1701
1871
  return [
1702
1872
  { oldId: fileId(repoRoot, oldAbs), newId: fileId(repoRoot, newAbs), reason },
1703
1873
  { oldId: moduleId(repoRoot, oldAbs), newId: moduleId(repoRoot, newAbs), reason }
@@ -1790,8 +1960,8 @@ function walk(requestedId, maps) {
1790
1960
  }
1791
1961
  function chainSteps(input) {
1792
1962
  if (input.from === null) return { steps: rootPath(input.lineage, input.to, input.maxHops), connected: true };
1793
- const path11 = lineagePath(input.lineage, input.from, input.to, input.maxHops);
1794
- 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 };
1795
1965
  return { steps: [{ snapshotId: input.to, direction: "forward" }], connected: false };
1796
1966
  }
1797
1967
  function createAliasChain(input) {
@@ -1876,9 +2046,11 @@ function computeAliasBridge(store, input) {
1876
2046
  return { baseSnapshotId: base.id, aliases: [...fileAliases, ...symbols] };
1877
2047
  }
1878
2048
 
2049
+ // src/roles.ts
2050
+ import * as path8 from "path";
2051
+
1879
2052
  // src/generated.ts
1880
- import { readFileSync } from "fs";
1881
- import * as path6 from "path";
2053
+ import * as path7 from "path";
1882
2054
  var HEURISTIC_FILE_RE = /(?:^|\/)[^/]*\.(?:gen|generated)\.[^/]*$/;
1883
2055
  var HEURISTIC_DIR_RE = /(?:^|\/)generated\//;
1884
2056
  function isGeneratedByHeuristic(id) {
@@ -1888,59 +2060,112 @@ function isGeneratedFile(id, patterns) {
1888
2060
  for (const rx of patterns) if (rx.test(id)) return true;
1889
2061
  return isGeneratedByHeuristic(id);
1890
2062
  }
1891
- function loadGeneratedPatterns(rootDir) {
2063
+ function loadGeneratedPatterns(rootDir, source = workingTreeSource()) {
1892
2064
  try {
1893
- const content = readFileSync(path6.join(rootDir, ".gitattributes"), "utf-8");
2065
+ const content = source.readFile(path7.join(rootDir, ".gitattributes"));
1894
2066
  return parseGeneratedPatterns(content);
1895
2067
  } catch {
1896
2068
  return [];
1897
2069
  }
1898
2070
  }
1899
2071
  function parseGeneratedPatterns(content) {
2072
+ const rules = parseGeneratedRules(content);
1900
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 = [];
1901
2083
  for (const raw of content.split("\n")) {
1902
2084
  const line = raw.trim();
1903
2085
  if (!line || line.startsWith("#")) continue;
1904
2086
  const parsed = /^("(?:[^"\\]|\\.)*"|\S+)\s*(.*)$/.exec(line);
1905
2087
  if (!parsed) continue;
1906
- const pattern = unquote(parsed[1]);
1907
- if (generatedAttr(parsed[2].split(/\s+/).filter(Boolean)) === true) {
1908
- out.push(gitattributesPatternToRegex(pattern));
1909
- }
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 });
1910
2091
  }
1911
- 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})`);
1912
2098
  }
1913
2099
  function generatedAttr(attrs) {
1914
2100
  let value;
1915
2101
  for (const a of attrs) {
1916
2102
  if (a === "linguist-generated" || a === "linguist-generated=true") value = true;
1917
- else if (a === "-linguist-generated" || a === "linguist-generated=false") value = false;
2103
+ else if (OPT_OUT_TOKENS.has(a)) value = false;
1918
2104
  }
1919
2105
  return value;
1920
2106
  }
2107
+ var OPT_OUT_TOKENS = /* @__PURE__ */ new Set([
2108
+ "-linguist-generated",
2109
+ "linguist-generated=false",
2110
+ "!linguist-generated"
2111
+ ]);
1921
2112
  function unquote(token) {
1922
2113
  return token.startsWith('"') && token.endsWith('"') ? token.slice(1, -1).replace(/\\(.)/g, "$1") : token;
1923
2114
  }
2115
+ var NEVER_MATCHES = /(?!)/;
1924
2116
  function gitattributesPatternToRegex(pattern) {
1925
2117
  let p = pattern;
1926
- const isDir = p.endsWith("/");
1927
- if (isDir) p = p.slice(0, -1);
1928
- let body = "";
1929
- for (let i = 0; i < p.length; i++) {
1930
- const c = p[i];
1931
- if (c === "*" && p[i + 1] === "*") {
1932
- body += ".*";
1933
- i++;
1934
- } else if (c === "*") body += "[^/]*";
1935
- else if (c === "?") body += "[^/]";
1936
- else body += c.replace(/[.+(){}|^$\\[\]]/, "\\$&");
1937
- }
1938
- const prefix = p.includes("/") ? "^" : "(?:^|/)";
1939
- 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;
1940
2164
  }
1941
2165
 
1942
2166
  // src/roles.ts
1943
2167
  var TEST_RE = /(?:^|\/)(?:__tests__\/|tests?\/)|\.(?:test|spec)(?:\.[a-z]+)?$/;
2168
+ var STORY_RE = /\.stories(?:\.[cm]?[jt]sx?)?$|\.mdx$/;
1944
2169
  var FIXTURE_RE = /(?:^|\/)fixtures(?:\/|$)/;
1945
2170
  var SCRIPT_RE = /(?:^|\/)(?:scripts|archive)(?:\/|$)/;
1946
2171
  var BARREL_RE = /(?:^|\/)index(?:\.[a-z]+)?$/;
@@ -1949,6 +2174,8 @@ var CONFIG_RE = /\.config(?:\.[a-z]+)?$/;
1949
2174
  var ALL_ROLES = [
1950
2175
  "test",
1951
2176
  "fixture",
2177
+ "story",
2178
+ "lab",
1952
2179
  "barrel",
1953
2180
  "types",
1954
2181
  "config",
@@ -1959,7 +2186,9 @@ var ALL_ROLES = [
1959
2186
  ];
1960
2187
  function classifyRole(id, hints) {
1961
2188
  if (hints?.isGenerated) return "generated";
2189
+ if (hints?.configuredRole) return hints.configuredRole;
1962
2190
  if (TEST_RE.test(id)) return "test";
2191
+ if (STORY_RE.test(id)) return "story";
1963
2192
  if (FIXTURE_RE.test(id)) return "fixture";
1964
2193
  if (SCRIPT_RE.test(id)) return "script";
1965
2194
  if (hints?.hasShebang) return "entry";
@@ -1968,8 +2197,14 @@ function classifyRole(id, hints) {
1968
2197
  if (CONFIG_RE.test(id)) return "config";
1969
2198
  return "source";
1970
2199
  }
1971
- function computeRoleHints(readFiles, idRoot, toId) {
1972
- 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);
1973
2208
  const shebangIds = /* @__PURE__ */ new Set();
1974
2209
  const generatedIds = /* @__PURE__ */ new Set();
1975
2210
  for (const rf of readFiles) {
@@ -1977,47 +2212,320 @@ function computeRoleHints(readFiles, idRoot, toId) {
1977
2212
  if (rf.content.startsWith("#!")) shebangIds.add(id);
1978
2213
  if (isGeneratedFile(id, patterns)) generatedIds.add(id);
1979
2214
  }
1980
- return { shebangIds, generatedIds };
2215
+ return { shebangIds, generatedIds, roleGlobs: loadRoleGlobs(idRoot, source) };
1981
2216
  }
1982
2217
  function annotateRoles(nodes, options) {
2218
+ const configuredRole = roleGlobMatcher(options?.roleGlobs, ALL_ROLES);
1983
2219
  return nodes.map(
1984
2220
  (n) => n.kind === "file" || n.kind === "module" ? {
1985
2221
  ...n,
1986
2222
  role: n.role ?? classifyRole(n.id, {
1987
2223
  hasShebang: options?.shebangIds?.has(n.id) ?? false,
1988
- isGenerated: options?.generatedIds?.has(n.id) ?? false
2224
+ isGenerated: options?.generatedIds?.has(n.id) ?? false,
2225
+ configuredRole: configuredRole(n.id)
1989
2226
  })
1990
2227
  } : n
1991
2228
  );
1992
2229
  }
1993
2230
 
1994
- // src/file-walk.ts
1995
- import * as fs from "fs/promises";
1996
- import * as path7 from "path";
1997
- import { isExcludedDir, shouldIncludeFile } from "@titan-design/code-parser";
1998
- 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) {
1999
2436
  const seen = /* @__PURE__ */ new Set();
2000
- const out = [];
2001
2437
  for (const rootDir of rootDirs) {
2002
- await collectFiles(rootDir, rootDir, languages, seen, out);
2438
+ for (const abs of ingestedUnder(tree, rootDir, languages)) seen.add(abs);
2003
2439
  }
2004
- return out;
2440
+ return [...seen].sort();
2005
2441
  }
2006
- async function collectFiles(dir, rootDir, languages, seen, out) {
2007
- const entries = await fs.readdir(dir, { withFileTypes: true });
2008
- for (const entry of entries) {
2009
- const full = path7.join(dir, entry.name);
2010
- if (entry.isDirectory()) {
2011
- if (isExcludedDir(entry.name)) continue;
2012
- await collectFiles(full, rootDir, languages, seen, out);
2013
- } else if (entry.isFile() && !seen.has(full)) {
2014
- const relative4 = path7.relative(rootDir, full);
2015
- if (shouldIncludeFile(relative4, [...languages])) {
2016
- seen.add(full);
2017
- out.push(full);
2018
- }
2019
- }
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));
2020
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 };
2021
2529
  }
2022
2530
 
2023
2531
  // src/barrel-resolve.ts
@@ -2026,9 +2534,9 @@ function edgeWeight(e) {
2026
2534
  return typeof w === "number" && w > 0 ? w : 1;
2027
2535
  }
2028
2536
  function pruneDanglingReferences(nodes, edges) {
2029
- for (const [key, edge] of edges) {
2030
- const symbolLayer = edge.kind === "references" || edge.kind === "calls";
2031
- 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);
2032
2540
  }
2033
2541
  }
2034
2542
  function resolveBarrelEdges(nodes, edges) {
@@ -2043,7 +2551,7 @@ function resolveBarrelEdges(nodes, edges) {
2043
2551
  forwards.set(e.srcId, arr);
2044
2552
  }
2045
2553
  const cache = /* @__PURE__ */ new Map();
2046
- function resolve4(id, seen) {
2554
+ function resolve5(id, seen) {
2047
2555
  if (!barrels.has(id)) return /* @__PURE__ */ new Map([[id, 1]]);
2048
2556
  const cached = cache.get(id);
2049
2557
  if (cached) return cached;
@@ -2054,7 +2562,7 @@ function resolveBarrelEdges(nodes, edges) {
2054
2562
  const nextSeen = new Set(seen).add(id);
2055
2563
  for (const t of targets) {
2056
2564
  const share = t.weight / total;
2057
- for (const [real, frac] of resolve4(t.dst, nextSeen)) {
2565
+ for (const [real, frac] of resolve5(t.dst, nextSeen)) {
2058
2566
  out.set(real, (out.get(real) ?? 0) + share * frac);
2059
2567
  }
2060
2568
  }
@@ -2069,7 +2577,7 @@ function resolveBarrelEdges(nodes, edges) {
2069
2577
  continue;
2070
2578
  }
2071
2579
  const w = edgeWeight(e);
2072
- for (const [real, frac] of resolve4(e.dstId, /* @__PURE__ */ new Set())) {
2580
+ for (const [real, frac] of resolve5(e.dstId, /* @__PURE__ */ new Set())) {
2073
2581
  if (real === e.srcId) continue;
2074
2582
  resolved.push({
2075
2583
  ...e,
@@ -2083,7 +2591,6 @@ function resolveBarrelEdges(nodes, edges) {
2083
2591
 
2084
2592
  // src/incremental.ts
2085
2593
  import { createHash } from "crypto";
2086
- import * as fs2 from "fs/promises";
2087
2594
  import { getLanguageFromPath as getLanguageFromPath2 } from "@titan-design/code-parser";
2088
2595
 
2089
2596
  // src/analysis/comment-lines.ts
@@ -2314,6 +2821,25 @@ function isLoggingCall(node) {
2314
2821
  return false;
2315
2822
  }
2316
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
+
2317
2843
  // src/cognitive-complexity.ts
2318
2844
  function cognitiveComplexityOf(body, language) {
2319
2845
  const handler = language === "python" ? PY_HANDLER : TS_HANDLER;
@@ -2347,9 +2873,8 @@ var TS_NESTING = /* @__PURE__ */ new Set([
2347
2873
  "do_statement",
2348
2874
  "switch_statement",
2349
2875
  "catch_clause",
2350
- "function_declaration",
2351
- "method_definition",
2352
- "arrow_function"
2876
+ ...TS_FUNCTION_DECL_TYPES,
2877
+ ...TS_BOUND_FUNCTION_TYPES
2353
2878
  ]);
2354
2879
  var PY_NESTING = /* @__PURE__ */ new Set([
2355
2880
  "if_statement",
@@ -2441,7 +2966,7 @@ function computeLcomMetrics(file, fileId2) {
2441
2966
  return out;
2442
2967
  }
2443
2968
  function collectClasses(file) {
2444
- 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);
2445
2970
  const out = [];
2446
2971
  const visit = (node) => {
2447
2972
  if (isClass(node)) out.push(node);
@@ -2479,14 +3004,21 @@ function collectPythonMethods(classNode) {
2479
3004
  if (!body) return [];
2480
3005
  const out = [];
2481
3006
  for (const child of body.namedChildren) {
2482
- if (!child || child.type !== "function_definition") continue;
2483
- const name = child.childForFieldName("name")?.text;
3007
+ const fn = child ? pythonMethodNode(child) : null;
3008
+ if (!fn) continue;
3009
+ const name = fn.childForFieldName("name")?.text;
2484
3010
  if (!name || name === "__init__") continue;
2485
- if (isPythonStaticOrClassMethod(child)) continue;
2486
- out.push({ name, usedNames: thisRefs(child, "self") });
3011
+ out.push({ name, usedNames: thisRefs(fn, "self") });
2487
3012
  }
2488
3013
  return out;
2489
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
+ }
2490
3022
  function methodName(methodNode) {
2491
3023
  const nameNode = methodNode.childForFieldName("name");
2492
3024
  return nameNode?.text ?? null;
@@ -2497,16 +3029,10 @@ function hasStaticModifier(methodNode) {
2497
3029
  }
2498
3030
  return false;
2499
3031
  }
2500
- function isPythonStaticOrClassMethod(funcNode) {
2501
- let prev = funcNode.previousNamedSibling;
2502
- while (prev && prev.type === "decorator") {
2503
- const text = prev.text;
2504
- if (text.includes("@staticmethod") || text.includes("@classmethod")) {
2505
- return true;
2506
- }
2507
- prev = prev.previousNamedSibling;
2508
- }
2509
- return false;
3032
+ function isPythonStaticOrClassMethod(decorated) {
3033
+ return decorated.namedChildren.some(
3034
+ (c) => c?.type === "decorator" && (c.text === "@staticmethod" || c.text === "@classmethod")
3035
+ );
2510
3036
  }
2511
3037
  function thisRefs(methodNode, selfKeyword) {
2512
3038
  const out = /* @__PURE__ */ new Set();
@@ -2651,6 +3177,7 @@ var SYMBOL_METRICS = [
2651
3177
  { name: "symbol_cyclomatic", stat: "cyclomatic", unit: "count" },
2652
3178
  { name: "symbol_loc", stat: "loc", unit: "lines" },
2653
3179
  { name: "symbol_max_nesting", stat: "nestingDepth", unit: "count" },
3180
+ { name: "symbol_jsx_depth", stat: "jsxDepth", unit: "count", omitZero: true },
2654
3181
  { name: "symbol_comment_lines", stat: "commentLines", unit: "lines" },
2655
3182
  { name: "symbol_docstring_lines", stat: "docstringLines", unit: "lines" },
2656
3183
  { name: "symbol_body_lines", stat: "bodyLines", unit: "lines" },
@@ -2675,6 +3202,7 @@ function symbolMetrics(fileId2, stats, symbolNames) {
2675
3202
  for (const [name, values] of byName) {
2676
3203
  const nodeId = symbolId(fileId2, name);
2677
3204
  for (const m of SYMBOL_METRICS) {
3205
+ if (m.omitZero && values[m.stat] === 0) continue;
2678
3206
  out.push({ nodeId, name: m.name, value: values[m.stat], unit: m.unit });
2679
3207
  }
2680
3208
  }
@@ -2689,10 +3217,6 @@ function functionShapeStats(fn, body, lines) {
2689
3217
  }
2690
3218
 
2691
3219
  // src/source-metrics.ts
2692
- var TS_FUNCTION_TYPES = /* @__PURE__ */ new Set([
2693
- "function_declaration",
2694
- "method_definition"
2695
- ]);
2696
3220
  var PY_FUNCTION_TYPES = /* @__PURE__ */ new Set(["function_definition"]);
2697
3221
  var TS_NESTING_TYPES = /* @__PURE__ */ new Set([
2698
3222
  "if_statement",
@@ -2735,6 +3259,7 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
2735
3259
  "cognitive_max",
2736
3260
  "cognitive_sum",
2737
3261
  "max_nesting_depth",
3262
+ "jsx_depth_max",
2738
3263
  "class_count",
2739
3264
  "lcom4_max",
2740
3265
  ...EXCEPTION_METRIC_NAMES,
@@ -2745,10 +3270,10 @@ var SOURCE_METRIC_NAMES = /* @__PURE__ */ new Set([
2745
3270
  ...SYMBOL_METRIC_NAMES
2746
3271
  ]);
2747
3272
  var EMPTY_NAMES = /* @__PURE__ */ new Set();
2748
- function computeSourceMetrics(files, fileIdOf, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
3273
+ function computeSourceMetrics(files, fileIdOf2, symbolNamesByFile2 = /* @__PURE__ */ new Map()) {
2749
3274
  const out = [];
2750
3275
  for (const file of files) {
2751
- const id = fileIdOf(file.filePath);
3276
+ const id = fileIdOf2(file.filePath);
2752
3277
  out.push(...metricsForFile(id, file, symbolNamesByFile2.get(id) ?? EMPTY_NAMES));
2753
3278
  }
2754
3279
  return out;
@@ -2796,6 +3321,8 @@ function metricsForFile(nodeId, file, symbolNames) {
2796
3321
  unit: "count"
2797
3322
  });
2798
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" });
2799
3326
  out.push(...symbolMetrics(nodeId, stats, symbolNames));
2800
3327
  out.push(...computeLcomMetrics(file, nodeId));
2801
3328
  out.push(...exceptionMetrics(nodeId, file.tree.rootNode, loc));
@@ -2806,7 +3333,7 @@ function countLoc(content) {
2806
3333
  }
2807
3334
  function analyzeFunctions(file) {
2808
3335
  const stats = [];
2809
- const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_TYPES;
3336
+ const fnTypes = file.language === "python" ? PY_FUNCTION_TYPES : TS_FUNCTION_DECL_TYPES;
2810
3337
  const lines = file.content.split("\n");
2811
3338
  walkScopes(file.tree.rootNode, file.language === "python", (node, scope) => {
2812
3339
  const fn = functionAt(node, fnTypes);
@@ -2816,6 +3343,7 @@ function analyzeFunctions(file) {
2816
3343
  cyclomatic: cyclomaticOf(fn.body, file.language),
2817
3344
  cognitive: cognitiveComplexityOf(fn.body, file.language),
2818
3345
  nestingDepth: nestingDepthOf(fn.body, file.language, 0),
3346
+ jsxDepth: jsxDepthIn(file, fn.body),
2819
3347
  loc: fn.node.endPosition.row - fn.node.startPosition.row + 1,
2820
3348
  ...functionShapeStats(fn.node, fn.body, lines)
2821
3349
  });
@@ -2827,13 +3355,17 @@ function functionAt(node, fnTypes) {
2827
3355
  const body = node.childForFieldName("body");
2828
3356
  return body ? { name: node.childForFieldName("name")?.text ?? null, body, node } : null;
2829
3357
  }
2830
- 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") {
2831
3359
  const body = node.childForFieldName("body");
2832
3360
  if (!body) return null;
2833
3361
  return { name: node.parent.childForFieldName("name")?.text ?? null, body, node };
2834
3362
  }
2835
3363
  return null;
2836
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
+ }
2837
3369
  function nestingDepthOf(node, language, depth) {
2838
3370
  const nestingTypes = language === "python" ? PY_NESTING_TYPES : TS_NESTING_TYPES;
2839
3371
  let maxDepth = depth;
@@ -3026,12 +3558,12 @@ var TERMINALS2 = /* @__PURE__ */ new Set([
3026
3558
  "break_statement",
3027
3559
  "continue_statement"
3028
3560
  ]);
3029
- function computeDeadCodeMetrics(files, fileIdOf) {
3561
+ function computeDeadCodeMetrics(files, fileIdOf2) {
3030
3562
  const out = [];
3031
3563
  for (const file of files) {
3032
3564
  const counts = deadCodeOf(file);
3033
3565
  if (!counts) continue;
3034
- const id = fileIdOf(file.filePath);
3566
+ const id = fileIdOf2(file.filePath);
3035
3567
  const { unreachable, locals, params } = counts;
3036
3568
  if (unreachable > 0) {
3037
3569
  out.push({ nodeId: id, name: "unreachable_statements", value: unreachable, unit: "count" });
@@ -3194,12 +3726,12 @@ var SEARCH_METHODS = /* @__PURE__ */ new Set([
3194
3726
  "every",
3195
3727
  "filter"
3196
3728
  ]);
3197
- function computeGrowthRiskMetrics(files, fileIdOf) {
3729
+ function computeGrowthRiskMetrics(files, fileIdOf2) {
3198
3730
  const out = [];
3199
3731
  for (const file of files) {
3200
3732
  const loopTypes = file.language === "python" ? PY_LOOP_TYPES : TS_LOOP_TYPES;
3201
3733
  const root = file.tree.rootNode;
3202
- const id = fileIdOf(file.filePath);
3734
+ const id = fileIdOf2(file.filePath);
3203
3735
  const depth = maxLoopDepth(root, loopTypes, 0);
3204
3736
  if (depth >= 2) {
3205
3737
  out.push({ nodeId: id, name: "loop_depth", value: depth, unit: "count" });
@@ -3270,8 +3802,8 @@ function reconstructFragment(repoRoot, absPath, fileId2, basis) {
3270
3802
  nodes.push(...basis.symbolsByFile.get(fileId2) ?? []);
3271
3803
  const edges = basis.edgesBySrc.get(fileId2) ?? [];
3272
3804
  const seenExternals = /* @__PURE__ */ new Set();
3273
- for (const edge of edges) {
3274
- const dst = basis.nodesById.get(edge.dstId);
3805
+ for (const edge2 of edges) {
3806
+ const dst = basis.nodesById.get(edge2.dstId);
3275
3807
  if (dst?.kind === "external" && !seenExternals.has(dst.id)) {
3276
3808
  seenExternals.add(dst.id);
3277
3809
  nodes.push({ id: dst.id, kind: "external", name: dst.name });
@@ -3317,12 +3849,12 @@ function sourceLanguage(filePath) {
3317
3849
  const language = getLanguageFromPath2(filePath);
3318
3850
  return language === "typescript" || language === "python" ? language : null;
3319
3851
  }
3320
- async function readSourceFiles(filePaths) {
3852
+ async function readSourceFiles(filePaths, source = workingTreeSource()) {
3321
3853
  const out = [];
3322
3854
  for (const filePath of filePaths) {
3323
3855
  const language = sourceLanguage(filePath);
3324
3856
  if (language === null) continue;
3325
- const content = await fs2.readFile(filePath, "utf-8");
3857
+ const content = source.readFile(filePath);
3326
3858
  out.push({ filePath, language, content, hash: hashContent(content) });
3327
3859
  }
3328
3860
  return out;
@@ -3472,8 +4004,8 @@ function resolveRelativeId(srcFileId, specifier, fileIds) {
3472
4004
  }
3473
4005
  return null;
3474
4006
  }
3475
- function targetFileId(edge) {
3476
- return parseSymbolId(edge.dstId)?.fileId ?? edge.dstId;
4007
+ function targetFileId(edge2) {
4008
+ return parseSymbolId(edge2.dstId)?.fileId ?? edge2.dstId;
3477
4009
  }
3478
4010
  function computeDeltaAffected(basis, currentFileIds) {
3479
4011
  const affected = /* @__PURE__ */ new Set();
@@ -3485,8 +4017,8 @@ function computeDeltaAffected(basis, currentFileIds) {
3485
4017
  if (!added) return affected;
3486
4018
  for (const [srcId, edges] of basis.edgesBySrc) {
3487
4019
  if (!currentFileIds.has(srcId)) continue;
3488
- for (const edge of edges) {
3489
- if (isEdgeAffected(edge, srcId, removed2, currentFileIds)) {
4020
+ for (const edge2 of edges) {
4021
+ if (isEdgeAffected(edge2, srcId, removed2, currentFileIds)) {
3490
4022
  affected.add(srcId);
3491
4023
  break;
3492
4024
  }
@@ -3494,11 +4026,11 @@ function computeDeltaAffected(basis, currentFileIds) {
3494
4026
  }
3495
4027
  return affected;
3496
4028
  }
3497
- function isEdgeAffected(edge, srcId, removed2, currentFileIds) {
3498
- if (removed2.has(targetFileId(edge))) return true;
3499
- 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;
3500
4032
  if (!specifier || !isRelative(specifier)) return false;
3501
- return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge);
4033
+ return resolveRelativeId(srcId, specifier, currentFileIds) !== targetFileId(edge2);
3502
4034
  }
3503
4035
 
3504
4036
  // src/metrics.ts
@@ -3569,142 +4101,45 @@ function computeCallMetrics(nodes, edges) {
3569
4101
  out.push({ nodeId: node.id, name: "symbol_caller_count", value: callers.size, unit: "count" });
3570
4102
  const single = isPrivateHelper(node) && callers.size === 1 && isSymbolCaller(callers);
3571
4103
  out.push({ nodeId: node.id, name: "symbol_single_caller_helper", value: single ? 1 : 0, unit: "count" });
3572
- const shape = readParamShape(node.attrs);
3573
- if (shape && sites.length >= 2) {
3574
- out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
3575
- }
3576
- }
3577
- return out;
3578
- }
3579
- function collectInbound(edges) {
3580
- const inbound = /* @__PURE__ */ new Map();
3581
- for (const edge of edges) {
3582
- if (edge.kind !== "calls") continue;
3583
- let entry = inbound.get(edge.dstId);
3584
- if (!entry) inbound.set(edge.dstId, entry = { callers: /* @__PURE__ */ new Set(), sites: [] });
3585
- if (edge.srcId !== edge.dstId) entry.callers.add(edge.srcId);
3586
- entry.sites.push(...edge.attrs?.sites ?? []);
3587
- }
3588
- return inbound;
3589
- }
3590
- function isPrivateHelper(node) {
3591
- const own = node.name.slice(node.name.lastIndexOf(".") + 1);
3592
- return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
3593
- }
3594
- function isSymbolCaller(callers) {
3595
- return [...callers].every((id) => parseSymbolId(id) !== null);
3596
- }
3597
- function constantParams(shape, sites) {
3598
- let count = 0;
3599
- shape.params.forEach((name, index) => {
3600
- const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
3601
- if (values.every((v) => v !== null && v === values[0])) count++;
3602
- });
3603
- return count;
3604
- }
3605
- function argumentAt(site, name, position) {
3606
- if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
3607
- if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
3608
- if (site.kwSplat) return null;
3609
- if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
3610
- return ABSENT;
3611
- }
3612
-
3613
- // src/analysis/test-linker.ts
3614
- var DEFAULT_MIN_CO_EDIT_COUNT = 2;
3615
- var TEST_INFIX_RE = /\.(?:test|spec)(\.[^./]+)$/;
3616
- var TESTS_DIR_RE = /(^|\/)(?:__tests__|tests?)\//;
3617
- function pathCandidates(testId) {
3618
- const out = /* @__PURE__ */ new Set();
3619
- const deInfixed = testId.replace(TEST_INFIX_RE, "$1");
3620
- if (deInfixed !== testId) out.add(deInfixed);
3621
- const deDir = deInfixed.replace(TESTS_DIR_RE, "$1");
3622
- if (deDir !== testId) out.add(deDir);
3623
- return [...out];
3624
- }
3625
- function splitFileIds(nodes) {
3626
- const testIds = [];
3627
- const nonTestIds = /* @__PURE__ */ new Set();
3628
- for (const n of nodes) {
3629
- if (n.kind !== "file") continue;
3630
- if (n.role === "test") testIds.push(n.id);
3631
- else nonTestIds.add(n.id);
3632
- }
3633
- return { testIds, nonTestIds };
3634
- }
3635
- function linkTestsToSources(nodes, coEditPairs, options = {}) {
3636
- const minCount = options.minCoEditCount ?? DEFAULT_MIN_CO_EDIT_COUNT;
3637
- const { testIds, nonTestIds } = splitFileIds(nodes);
3638
- const links = [];
3639
- for (const testId of testIds) {
3640
- const matched = pathCandidates(testId).filter((c) => nonTestIds.has(c));
3641
- if (matched.length > 0) {
3642
- for (const sourceId of matched) {
3643
- links.push({ testId, sourceId, method: "path" });
3644
- }
3645
- continue;
3646
- }
3647
- const partner = couplingFor(coEditPairs, testId).find((p) => p.count >= minCount && nonTestIds.has(p.partner));
3648
- if (partner) {
3649
- links.push({ testId, sourceId: partner.partner, method: "coedit" });
3650
- }
3651
- }
3652
- return links;
3653
- }
3654
- function testCoverageCountMetrics(links) {
3655
- const testsBySource = groupTestsBySource(links);
3656
- const out = [];
3657
- for (const [sourceId, tests] of testsBySource) {
3658
- out.push({
3659
- nodeId: sourceId,
3660
- name: "linked_test_count",
3661
- value: tests.size,
3662
- unit: "count"
3663
- });
3664
- }
3665
- return out;
3666
- }
3667
- function groupTestsBySource(links) {
3668
- const out = /* @__PURE__ */ new Map();
3669
- for (const l of links) {
3670
- let set = out.get(l.sourceId);
3671
- if (!set) {
3672
- set = /* @__PURE__ */ new Set();
3673
- out.set(l.sourceId, set);
3674
- }
3675
- set.add(l.testId);
3676
- }
3677
- return out;
3678
- }
3679
-
3680
- // src/history-recency.ts
3681
- var DAY_SECONDS = 86400;
3682
- function windowSuffix(window) {
3683
- return window === "lifetime" ? "lifetime" : `${window}d`;
3684
- }
3685
- function computeRecencyWindows(firstSeen, churnedIdsByWindow, nowEpoch) {
3686
- const out = [];
3687
- const ageEmitted = /* @__PURE__ */ new Set();
3688
- for (const [windowDays, ids] of churnedIdsByWindow) {
3689
- const suffix = windowSuffix(windowDays);
3690
- for (const id of ids) {
3691
- const seen = firstSeen.get(id);
3692
- let recency = 1;
3693
- if (seen !== void 0) {
3694
- const ageDays = Math.max(0, (nowEpoch - seen) / DAY_SECONDS);
3695
- if (windowDays !== "lifetime") recency = Math.min(1, ageDays / windowDays);
3696
- if (!ageEmitted.has(id)) {
3697
- out.push({ nodeId: id, name: "file_age_days", value: Math.round(ageDays), unit: "days" });
3698
- ageEmitted.add(id);
3699
- }
3700
- }
3701
- out.push({ nodeId: id, name: `recency_${suffix}`, value: round3(recency), unit: "ratio" });
4104
+ const shape = readParamShape(node.attrs);
4105
+ if (shape && sites.length >= 2) {
4106
+ out.push({ nodeId: node.id, name: "symbol_constant_params", value: constantParams(shape, sites), unit: "count" });
3702
4107
  }
3703
4108
  }
3704
4109
  return out;
3705
4110
  }
3706
- function round3(n) {
3707
- return Math.round(n * 1e3) / 1e3;
4111
+ function collectInbound(edges) {
4112
+ const inbound = /* @__PURE__ */ new Map();
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 ?? []);
4119
+ }
4120
+ return inbound;
4121
+ }
4122
+ function isPrivateHelper(node) {
4123
+ const own = node.name.slice(node.name.lastIndexOf(".") + 1);
4124
+ return node.attrs?.exported === false && !(own.startsWith("__") && own.endsWith("__"));
4125
+ }
4126
+ function isSymbolCaller(callers) {
4127
+ return [...callers].every((id) => parseSymbolId(id) !== null);
4128
+ }
4129
+ function constantParams(shape, sites) {
4130
+ let count = 0;
4131
+ shape.params.forEach((name, index) => {
4132
+ const values = sites.map((site) => argumentAt(site, name, index < shape.positional ? index : -1));
4133
+ if (values.every((v) => v !== null && v === values[0])) count++;
4134
+ });
4135
+ return count;
4136
+ }
4137
+ function argumentAt(site, name, position) {
4138
+ if (position >= 0 && position < site.args.length) return site.args[position] ?? null;
4139
+ if (site.kwargs && Object.hasOwn(site.kwargs, name)) return site.kwargs[name] ?? null;
4140
+ if (site.kwSplat) return null;
4141
+ if (position >= 0 && site.spreadFrom !== void 0 && position >= site.spreadFrom) return null;
4142
+ return ABSENT;
3708
4143
  }
3709
4144
 
3710
4145
  // src/history-metrics.ts
@@ -3718,7 +4153,12 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
3718
4153
  const knownPaths = collectFileIds(nodes);
3719
4154
  const primaryWindow = options.churnWindowDays ?? 30;
3720
4155
  const windows = resolveChurnWindows(options.churnWindows, primaryWindow, options.includeLifetime === true);
3721
- 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
+ });
3722
4162
  if (wide === null) return null;
3723
4163
  const nowEpoch = options.nowEpoch ?? Math.floor(Date.now() / 1e3);
3724
4164
  const churnByWindow = aggregateChurnWindows(wide, windows, nowEpoch, knownPaths);
@@ -3726,7 +4166,7 @@ function loadHistoryMetrics(nodes, idRoot, options = {}) {
3726
4166
  const metrics = [
3727
4167
  ...churnMetrics(churnByWindow),
3728
4168
  ...ownershipMetrics(primaryEntries, primaryWindow, knownPaths),
3729
- ...recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch)
4169
+ ...recencyMetrics({ idRoot, rev: options.rev, knownPaths, nowEpoch }, churnByWindow)
3730
4170
  ];
3731
4171
  if (options.includeLifetime === true) metrics.push(...ownershipMetrics(wide, "lifetime", knownPaths));
3732
4172
  return { metrics, primaryEntries };
@@ -3793,14 +4233,14 @@ function mergeAuthorChurn(tests, linesByTest) {
3793
4233
  }
3794
4234
  return byAuthor;
3795
4235
  }
3796
- function recencyMetrics(idRoot, churnByWindow, knownPaths, nowEpoch) {
4236
+ function recencyMetrics(at, churnByWindow) {
3797
4237
  const churnedByWindow = /* @__PURE__ */ new Map();
3798
4238
  for (const [window, byPath] of churnByWindow) {
3799
4239
  if (byPath.size > 0) churnedByWindow.set(window, new Set(byPath.keys()));
3800
4240
  }
3801
4241
  if (churnedByWindow.size === 0) return [];
3802
- const firstSeen = loadFileFirstSeen({ repoRoot: idRoot, knownPaths }) ?? /* @__PURE__ */ new Map();
3803
- 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);
3804
4244
  }
3805
4245
 
3806
4246
  // src/index-metrics.ts
@@ -3844,17 +4284,17 @@ function computeTestCoverage(nodes, entries, windowDays) {
3844
4284
  }
3845
4285
 
3846
4286
  // src/merge.ts
3847
- function edgeKey(edge) {
3848
- return JSON.stringify([edge.srcId, edge.dstId, edge.kind]);
4287
+ function edgeKey(edge2) {
4288
+ return JSON.stringify([edge2.srcId, edge2.dstId, edge2.kind]);
3849
4289
  }
3850
4290
  function mergeFragments(fragments) {
3851
4291
  const nodes = /* @__PURE__ */ new Map();
3852
4292
  const edges = /* @__PURE__ */ new Map();
3853
4293
  for (const fragment of fragments) {
3854
4294
  for (const node of fragment.nodes) if (!nodes.has(node.id)) nodes.set(node.id, node);
3855
- for (const edge of fragment.edges) {
3856
- const key = edgeKey(edge);
3857
- 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);
3858
4298
  }
3859
4299
  }
3860
4300
  return { nodes, edges };
@@ -3898,18 +4338,18 @@ function qualifiedByName(file) {
3898
4338
  }
3899
4339
 
3900
4340
  // src/indexer.ts
3901
- var INDEX_VERSION = "0.18.0";
4341
+ var INDEX_VERSION = "0.22.0";
3902
4342
  var LANGUAGES = ["typescript", "python"];
3903
4343
  function canonicalizePath(p) {
3904
4344
  try {
3905
- return realpathSync(p);
4345
+ return realpathSync2(p);
3906
4346
  } catch {
3907
4347
  return p;
3908
4348
  }
3909
4349
  }
3910
4350
  function normalizePaths(paths) {
3911
4351
  if (paths.length === 0) throw new Error("indexPaths requires at least one path");
3912
- return [...new Set(paths.map((p) => path8.resolve(p)))];
4352
+ return [...new Set(paths.map((p) => path10.resolve(p)))];
3913
4353
  }
3914
4354
  function countByKind(items) {
3915
4355
  const out = {};
@@ -3932,8 +4372,9 @@ function resolveSymbolAliases(store, idRoot, parsedByPath, nodeIds) {
3932
4372
  const files = [...parsedByPath.values()].map((file) => ({ fileId: fileId(idRoot, file.filePath), file }));
3933
4373
  return qualifiedSymbolAliases(files, nodeIds);
3934
4374
  }
3935
- function resolveRoots(paths) {
4375
+ function resolveRoots(paths, source) {
3936
4376
  const raw = normalizePaths(paths);
4377
+ if (source.revision) return rootsAtRevision(raw, source.revision);
3937
4378
  const gitToplevel = detectGitToplevel(raw[0]);
3938
4379
  const rootDirs = gitToplevel !== null ? raw.map(canonicalizePath) : raw;
3939
4380
  return { rootDirs, idRoot: gitToplevel ?? rootDirs[0] };
@@ -3945,16 +4386,19 @@ async function parseAll(toParse) {
3945
4386
  }
3946
4387
  return parsed;
3947
4388
  }
3948
- function historyOptions(options) {
4389
+ function historyOptions(options, source) {
3949
4390
  return {
3950
4391
  churnWindowDays: options.churnWindowDays,
3951
4392
  churnWindows: options.churnWindows,
3952
- includeLifetime: options.lifetime === true
4393
+ includeLifetime: options.lifetime === true,
4394
+ rev: source.revision?.commit,
4395
+ nowEpoch: source.revision?.commitEpoch
3953
4396
  };
3954
4397
  }
3955
4398
  async function indexPaths(store, options) {
3956
- const { rootDirs, idRoot } = resolveRoots(options.paths);
3957
- 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);
3958
4402
  const reuse = options.incremental !== false ? loadReuseBasis(store, INDEX_VERSION) : null;
3959
4403
  const currentFileIds = new Set(readFiles.map((rf) => fileId(idRoot, rf.filePath)));
3960
4404
  const affected = reuse ? computeDeltaAffected(reuse, currentFileIds) : /* @__PURE__ */ new Set();
@@ -3968,10 +4412,11 @@ async function indexPaths(store, options) {
3968
4412
  parsedByPath,
3969
4413
  reuse,
3970
4414
  cosmeticFileIds: classified.cosmeticFileIds,
3971
- extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig })
4415
+ extractor: new LanguageExtractor({ repoRoot: idRoot, tsConfigPath: options.tsConfig, source })
3972
4416
  })
3973
4417
  );
3974
- 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);
3975
4420
  accumulator.nodes = new Map(annotated.map((n) => [n.id, n]));
3976
4421
  pruneDanglingReferences(accumulator.nodes, accumulator.edges);
3977
4422
  const metrics = options.computeMetrics === false ? [] : buildIndexerMetrics({
@@ -3980,19 +4425,23 @@ async function indexPaths(store, options) {
3980
4425
  parsedFiles: [...parsedByPath.values()],
3981
4426
  reusedSourceMetrics: reuse ? reusedFileIds.flatMap((id) => reuse.sourceMetricsByFile.get(id) ?? []) : [],
3982
4427
  idRoot,
3983
- history: options.computeChurn === false ? void 0 : historyOptions(options)
4428
+ history: options.computeChurn === false ? void 0 : historyOptions(options, source)
3984
4429
  });
3985
- const rootDir = rootDirs[0];
4430
+ const rootDir = source.revision ? idRoot : rootDirs[0];
3986
4431
  const ref = options.ref ?? "wd";
3987
- 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 });
3988
4434
  const aliases = [
3989
4435
  ...bridge.aliases,
3990
4436
  ...resolveSymbolAliases(store, idRoot, parsedByPath, new Set(accumulator.nodes.keys()))
3991
4437
  ];
3992
- const commitHash = options.commitHash ?? detectGitHead(rootDir) ?? void 0;
3993
4438
  const snapshot = { ref, commitHash, aliasBase: bridge.baseSnapshotId };
3994
- const snapshotId = persist(store, snapshot, accumulator, aliases, metrics);
3995
- 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
+ });
3996
4445
  return {
3997
4446
  snapshotId,
3998
4447
  files: readFiles.length,
@@ -4009,14 +4458,14 @@ async function indexPaths(store, options) {
4009
4458
  }
4010
4459
 
4011
4460
  // src/index.ts
4012
- 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";
4013
4462
 
4014
4463
  // src/extractors/deep-ast.ts
4015
4464
  import { readFileSync as readFileSync2 } from "fs";
4016
- import * as path9 from "path";
4465
+ import * as path11 from "path";
4017
4466
  import {
4018
4467
  Node as Node6,
4019
- Project as Project2,
4468
+ Project as Project4,
4020
4469
  ScriptTarget as ScriptTarget2
4021
4470
  } from "ts-morph";
4022
4471
  function computeDeepAst(input) {
@@ -4028,11 +4477,11 @@ function computeDeepAst(input) {
4028
4477
  function loadSourceFile(absPath) {
4029
4478
  try {
4030
4479
  const text = readFileSync2(absPath, "utf8");
4031
- const project = new Project2({
4480
+ const project = new Project4({
4032
4481
  useInMemoryFileSystem: true,
4033
4482
  compilerOptions: { target: ScriptTarget2.Latest, allowJs: true }
4034
4483
  });
4035
- return project.createSourceFile(`f${path9.extname(absPath) || ".ts"}`, text);
4484
+ return project.createSourceFile(`f${path11.extname(absPath) || ".ts"}`, text);
4036
4485
  } catch {
4037
4486
  return null;
4038
4487
  }
@@ -4210,11 +4659,11 @@ function innermostContaining(symbols, line) {
4210
4659
  }
4211
4660
  return best;
4212
4661
  }
4213
- function attributeCoverage(coverage, fileIdOf, symbolsByFile) {
4662
+ function attributeCoverage(coverage, fileIdOf2, symbolsByFile) {
4214
4663
  const out = [];
4215
4664
  for (const [absPath, fc] of Object.entries(coverage)) {
4216
4665
  if (!isFileCoverage(fc)) continue;
4217
- const fileId2 = fileIdOf(absPath);
4666
+ const fileId2 = fileIdOf2(absPath);
4218
4667
  if (!fileId2) continue;
4219
4668
  const fnIds = Object.keys(fc.fnMap);
4220
4669
  if (fnIds.length === 0) continue;
@@ -4245,142 +4694,196 @@ function pctMetric(nodeId, covered, total) {
4245
4694
  };
4246
4695
  }
4247
4696
 
4248
- // src/analysis/pagerank.ts
4249
- var DEFAULT_DAMPING = 0.85;
4250
- var DEFAULT_TOLERANCE = 1e-6;
4251
- var DEFAULT_MAX_ITERS = 100;
4252
- var DEFAULT_EDGE_WEIGHTS = {
4253
- imports: 1,
4254
- "re-exports": 0.5,
4255
- calls: 1.5,
4256
- extends: 1,
4257
- implements: 1,
4258
- references: 1,
4259
- "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"])
4260
4703
  };
4261
- function getEdgeWeight(kind, overrides) {
4262
- return overrides?.[kind] ?? DEFAULT_EDGE_WEIGHTS[kind] ?? 1;
4263
- }
4264
- function computePageRank(nodes, edges, options = {}) {
4265
- const n = nodes.length;
4266
- if (n === 0) return { rows: [], iterations: 0, converged: true };
4267
- const damping = options.damping ?? DEFAULT_DAMPING;
4268
- const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
4269
- const maxIters = options.maxIterations ?? DEFAULT_MAX_ITERS;
4270
- const weights = { ...DEFAULT_EDGE_WEIGHTS, ...options.edgeWeights ?? {} };
4271
- const idx = buildIdIndex(nodes);
4272
- const adj = buildAdjacency(idx, edges, weights, n);
4273
- const pers = buildPersonalization(idx, n, options.personalization);
4274
- const { rank, iterations, converged } = powerIterate(
4275
- pers,
4276
- adj,
4277
- damping,
4278
- tolerance,
4279
- maxIters
4280
- );
4281
- 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"}`;
4282
4713
  }
4283
- function buildIdIndex(nodes) {
4284
- const idx = /* @__PURE__ */ new Map();
4285
- for (let i = 0; i < nodes.length; i++) idx.set(nodes[i].id, i);
4286
- return idx;
4714
+ function originalLine(minified, outputLine) {
4715
+ return minified.lineMap[outputLine - 1];
4287
4716
  }
4288
- function buildAdjacency(idx, edges, weights, n) {
4289
- const outNeighbors = Array.from(
4290
- { length: n },
4291
- () => []
4292
- );
4293
- const outWeightSum = new Array(n).fill(0);
4294
- for (const e of edges) {
4295
- const si = idx.get(e.srcId);
4296
- const di = idx.get(e.dstId);
4297
- if (si === void 0 || di === void 0) continue;
4298
- const w = weights[e.kind] ?? 1;
4299
- if (w <= 0) continue;
4300
- outNeighbors[si].push({ to: di, w });
4301
- outWeightSum[si] += w;
4302
- }
4303
- return { outNeighbors, outWeightSum };
4304
- }
4305
- function powerIterate(pers, adj, damping, tolerance, maxIters) {
4306
- const n = pers.length;
4307
- let rank = pers.slice();
4308
- let next = new Array(n).fill(0);
4309
- let iterations = 0;
4310
- let converged = false;
4311
- for (let iter = 0; iter < maxIters; iter++) {
4312
- iterations = iter + 1;
4313
- const dangling = sumDangling(rank, adj.outWeightSum);
4314
- const teleport = 1 - damping + damping * dangling;
4315
- seedTeleport(next, pers, teleport);
4316
- distributeRank(next, rank, adj, damping);
4317
- const diff = l1Diff(rank, next);
4318
- [rank, next] = [next, rank];
4319
- next.fill(0);
4320
- if (diff < tolerance) {
4321
- converged = true;
4322
- break;
4323
- }
4324
- }
4325
- return { rank, iterations, converged };
4717
+ function isMinifyLanguage(language) {
4718
+ return language === "typescript" || language === "tsx" || language === "python";
4326
4719
  }
4327
- function sumDangling(rank, outWeightSum) {
4328
- let total = 0;
4329
- for (let i = 0; i < rank.length; i++) {
4330
- 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();
4331
4727
  }
4332
- return total;
4333
4728
  }
4334
- function seedTeleport(next, pers, teleport) {
4335
- for (let i = 0; i < next.length; i++) next[i] = teleport * pers[i];
4336
- }
4337
- function distributeRank(next, rank, adj, damping) {
4338
- for (let i = 0; i < rank.length; i++) {
4339
- const s = adj.outWeightSum[i];
4340
- if (s === 0 || rank[i] === 0) continue;
4341
- const factor = damping * rank[i] / s;
4342
- const list = adj.outNeighbors[i];
4343
- for (let k = 0; k < list.length; k++) {
4344
- 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;
4345
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
+ ];
4346
4757
  }
4758
+ return [...comments.map(spanOf), ...pythonDocstrings(root).map(docstringSpan)];
4347
4759
  }
4348
- function l1Diff(a, b) {
4349
- let diff = 0;
4350
- for (let i = 0; i < a.length; i++) diff += Math.abs(b[i] - a[i]);
4351
- return diff;
4760
+ function spanOf(node) {
4761
+ return { start: node.startPosition, end: node.endPosition };
4352
4762
  }
4353
- function rankToRows(nodes, rank) {
4354
- return nodes.map((node, i) => ({ nodeId: node.id, score: rank[i] })).sort(compareRows);
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 };
4355
4767
  }
4356
- function compareRows(a, b) {
4357
- if (b.score !== a.score) return b.score - a.score;
4358
- return a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0;
4768
+ function jsxCommentExpressions(root) {
4769
+ return descendantsOfType(root, "jsx_expression").filter(
4770
+ (n) => n.namedChildCount > 0 && n.namedChildren.every((c) => c?.type === "comment")
4771
+ );
4359
4772
  }
4360
- function buildPersonalization(idx, n, pers) {
4361
- if (!pers || pers.size === 0) return uniformVector(n);
4362
- const out = new Array(n).fill(0);
4363
- const total = applySeedWeights(out, idx, pers);
4364
- if (total === 0) return uniformVector(n);
4365
- for (let i = 0; i < n; i++) out[i] /= total;
4366
- return out;
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);
4367
4778
  }
4368
- function uniformVector(n) {
4369
- return new Array(n).fill(1 / n);
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;
4806
+ }
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;
4370
4815
  }
4371
- function applySeedWeights(out, idx, pers) {
4372
- let total = 0;
4373
- for (const [id, w] of pers) {
4374
- if (!isValidWeight(w)) continue;
4375
- const i = idx.get(id);
4376
- if (i === void 0) continue;
4377
- out[i] = w;
4378
- 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);
4379
4876
  }
4380
- return total;
4877
+ while (out.length > 0 && out[out.length - 1]?.text === "" && !out[out.length - 1]?.verbatim) out.pop();
4878
+ return out;
4381
4879
  }
4382
- function isValidWeight(w) {
4383
- 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
+ };
4384
4887
  }
4385
4888
 
4386
4889
  // src/analysis/relevance.ts
@@ -4406,290 +4909,428 @@ function symmetrize(edges) {
4406
4909
  return out;
4407
4910
  }
4408
4911
 
4409
- // src/analysis/symbol-coupling.ts
4410
- var DEFAULT_MIN_CO_IMPORTS = 2;
4411
- var DEFAULT_LARGE_IMPORTER_THRESHOLD = 40;
4412
- function computeSymbolConsumers(edges) {
4413
- const bySymbol = /* @__PURE__ */ new Map();
4414
- for (const e of edges) {
4415
- let set = bySymbol.get(e.dstId);
4416
- if (!set) {
4417
- set = /* @__PURE__ */ new Set();
4418
- bySymbol.set(e.dstId, set);
4419
- }
4420
- set.add(e.srcId);
4421
- }
4422
- const out = [];
4423
- for (const [id, consumers] of bySymbol) {
4424
- const parsed = parseSymbolId(id);
4425
- if (!parsed) continue;
4426
- out.push({
4427
- symbolId: id,
4428
- fileId: parsed.fileId,
4429
- name: parsed.name,
4430
- consumers: [...consumers].sort()
4431
- });
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));
4432
4923
  }
4433
- return out.sort(compareConsumers);
4924
+ return lines.join("\n");
4434
4925
  }
4435
- function compareConsumers(a, b) {
4436
- if (b.consumers.length !== a.consumers.length) {
4437
- 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);
4438
4935
  }
4439
- return a.symbolId < b.symbolId ? -1 : a.symbolId > b.symbolId ? 1 : 0;
4936
+ return [...byFile].sort(([a], [b]) => compare2(a, b));
4440
4937
  }
4441
- function computeSymbolCoupling(edges, options = {}) {
4442
- const minCoImports = options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS;
4443
- const largeThreshold = options.largeImporterThreshold ?? DEFAULT_LARGE_IMPORTER_THRESHOLD;
4444
- const bySource = groupSymbolsBySource(edges);
4445
- const counts = /* @__PURE__ */ new Map();
4446
- for (const symbols of bySource.values()) {
4447
- if (symbols.length < 2 || symbols.length > largeThreshold) continue;
4448
- accumulateSymbolPairs(symbols, counts);
4449
- }
4450
- 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 };
4451
4942
  }
4452
- function groupSymbolsBySource(edges) {
4453
- const perSource = /* @__PURE__ */ new Map();
4454
- for (const e of edges) {
4455
- if (!parseSymbolId(e.dstId)) continue;
4456
- let set = perSource.get(e.srcId);
4457
- if (!set) {
4458
- set = /* @__PURE__ */ new Set();
4459
- perSource.set(e.srcId, set);
4460
- }
4461
- set.add(e.dstId);
4462
- }
4463
- const out = /* @__PURE__ */ new Map();
4464
- for (const [src, set] of perSource) out.set(src, [...set].sort());
4465
- return out;
4943
+ function numberAttr(node, key) {
4944
+ const value = node.attrs?.[key];
4945
+ return typeof value === "number" ? value : void 0;
4466
4946
  }
4467
- function accumulateSymbolPairs(symbols, counts) {
4468
- for (let i = 0; i < symbols.length; i++) {
4469
- for (let j = i + 1; j < symbols.length; j++) {
4470
- const key = `${symbols[i]} ${symbols[j]}`;
4471
- 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);
4472
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);
4473
4957
  }
4958
+ return lines;
4474
4959
  }
4475
- function finalizePairs(counts, minCoImports) {
4476
- const out = [];
4477
- for (const [key, coImports] of counts) {
4478
- if (coImports < minCoImports) continue;
4479
- const [aId, bId] = key.split(" ");
4480
- const a = parseSymbolId(aId);
4481
- const b = parseSymbolId(bId);
4482
- out.push({
4483
- aId,
4484
- aFile: a.fileId,
4485
- aName: a.name,
4486
- bId,
4487
- bFile: b.fileId,
4488
- bName: b.name,
4489
- coImports,
4490
- crossFile: a.fileId !== b.fileId
4491
- });
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;
4492
4973
  }
4493
- 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;
4494
4978
  }
4495
- function comparePairs(a, b) {
4496
- if (b.coImports !== a.coImports) return b.coImports - a.coImports;
4497
- if (a.crossFile !== b.crossFile) return a.crossFile ? -1 : 1;
4498
- if (a.aId !== b.aId) return a.aId < b.aId ? -1 : 1;
4499
- 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;
4500
4983
  }
4501
4984
 
4502
- // src/analysis/partition-quality.ts
4503
- var FOUNDATION_INSTABILITY_MAX = 0.3;
4504
- var TOP_INSTABILITY_MIN = 0.9;
4505
- var WEAK_COHESION_MAX = 0.5;
4506
- var TIGHT_INTENSITY_MIN = 0.6;
4507
- var MODERATE_INTENSITY_MIN = 0.3;
4508
- function computePartitionQuality(input) {
4509
- const resolveBarrels = input.resolveBarrels ?? false;
4510
- const pkgByFile = invertBuckets(input.fileByPackage);
4511
- const barrelTargets = resolveBarrels ? buildBarrelTargetMap(input.nodes, input.edges) : /* @__PURE__ */ new Map();
4512
- const resolvedPairs = buildResolvedPairs(
4513
- input.edges,
4514
- pkgByFile,
4515
- barrelTargets,
4516
- resolveBarrels
4517
- );
4518
- const counts = countByPackage(resolvedPairs);
4519
- const totalEdges = resolvedPairs.length;
4520
- const filesByPkg = filesPerPackage(input.fileByPackage);
4521
- const abstractByPkg = countAbstractFiles(input.nodes, pkgByFile);
4522
- const perPackage = input.packages.map(
4523
- (p) => buildPackageStats(p.id, counts, filesByPkg, abstractByPkg)
4524
- );
4525
- const pairCoupling = buildPairCoupling(resolvedPairs, filesByPkg);
4526
- const modularityQ = computeModularity(perPackage, totalEdges);
4527
- const flagsCount = perPackage.reduce((acc, p) => acc + p.flags.length, 0) + pairCoupling.filter((c) => c.flag === "tight").length;
4528
- 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
+ };
4529
4998
  }
4530
- function invertBuckets(fileByPackage) {
4999
+ function consumerCounts(edges) {
4531
5000
  const out = /* @__PURE__ */ new Map();
4532
- for (const [pkgId, files] of fileByPackage) {
4533
- if (pkgId === "") continue;
4534
- for (const f of files) out.set(f, pkgId);
4535
- }
5001
+ for (const c of computeSymbolConsumers(edges)) out.set(c.symbolId, c.consumers);
4536
5002
  return out;
4537
5003
  }
4538
- function filesPerPackage(fileByPackage) {
4539
- const out = /* @__PURE__ */ new Map();
4540
- for (const [pkgId, files] of fileByPackage) {
4541
- 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
+ });
4542
5049
  }
4543
- return out;
5050
+ return sortSymbols(lines);
4544
5051
  }
4545
- function buildBarrelTargetMap(nodes, edges) {
4546
- const barrelIds = new Set(
4547
- nodes.filter((n) => n.role === "barrel").map((n) => n.id)
4548
- );
4549
- if (barrelIds.size === 0) return /* @__PURE__ */ new Map();
4550
- const directReExports = /* @__PURE__ */ new Map();
4551
- for (const e of edges) {
4552
- if (!barrelIds.has(e.srcId)) continue;
4553
- if (e.kind !== "re-exports") continue;
4554
- let list = directReExports.get(e.srcId);
4555
- if (!list) {
4556
- list = [];
4557
- directReExports.set(e.srcId, list);
4558
- }
4559
- list.push(e.dstId);
4560
- }
4561
- const resolved = /* @__PURE__ */ new Map();
4562
- const resolve4 = (barrel, visiting) => {
4563
- if (resolved.has(barrel)) return resolved.get(barrel);
4564
- if (visiting.has(barrel)) return [];
4565
- visiting.add(barrel);
4566
- const out = [];
4567
- for (const target of directReExports.get(barrel) ?? []) {
4568
- if (barrelIds.has(target)) {
4569
- for (const inner of resolve4(target, visiting)) out.push(inner);
4570
- } else {
4571
- out.push(target);
4572
- }
4573
- }
4574
- visiting.delete(barrel);
4575
- resolved.set(barrel, out);
4576
- 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)
4577
5098
  };
4578
- for (const id of barrelIds) resolve4(id, /* @__PURE__ */ new Set());
4579
- return resolved;
4580
5099
  }
4581
- function buildResolvedPairs(edges, pkgByFile, barrelTargets, resolveBarrels) {
4582
- const out = [];
4583
- for (const e of edges) {
4584
- if (resolveBarrels && e.kind === "re-exports") continue;
4585
- const fromPkg = pkgByFile.get(e.srcId);
4586
- if (!fromPkg) continue;
4587
- const targets = barrelTargets.get(e.dstId) ?? [e.dstId];
4588
- for (const dst of targets) {
4589
- const toPkg = pkgByFile.get(dst);
4590
- if (toPkg) out.push({ from: fromPkg, to: toPkg });
4591
- }
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("");
4592
5156
  }
4593
5157
  return out;
4594
5158
  }
4595
- function countByPackage(pairs) {
4596
- const out = /* @__PURE__ */ new Map();
4597
- const get = (id) => {
4598
- let c = out.get(id);
4599
- if (!c) {
4600
- c = { internal: 0, outgoing: 0, incoming: 0 };
4601
- out.set(id, c);
4602
- }
4603
- return c;
4604
- };
4605
- for (const p of pairs) {
4606
- if (p.from === p.to) {
4607
- get(p.from).internal += 1;
4608
- } else {
4609
- get(p.from).outgoing += 1;
4610
- 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}`);
4611
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("");
4612
5186
  }
4613
5187
  return out;
4614
5188
  }
4615
- function buildPackageStats(pkgId, counts, filesByPkg, abstractByPkg) {
4616
- const c = counts.get(pkgId) ?? { internal: 0, outgoing: 0, incoming: 0 };
4617
- const cohesion = ratioOrZero(c.internal, c.internal + c.outgoing);
4618
- const instability = ratioOrZero(c.outgoing, c.outgoing + c.incoming);
4619
- const fileCount = filesByPkg.get(pkgId) ?? 0;
4620
- const abstractness = ratioOrZero(abstractByPkg.get(pkgId) ?? 0, fileCount);
4621
- const layer = classifyLayer(instability);
4622
- const flags = [];
4623
- if (layer !== "top" && cohesion < WEAK_COHESION_MAX && c.internal + c.outgoing > 0) {
4624
- flags.push("weak-boundary");
4625
- }
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
+ }));
4626
5254
  return {
4627
- pkgId,
4628
- fileCount,
4629
- internalEdges: c.internal,
4630
- outgoingEdges: c.outgoing,
4631
- incomingEdges: c.incoming,
4632
- cohesion,
4633
- instability,
4634
- abstractness,
4635
- layer,
4636
- flags
5255
+ callers: rankEdges(callers, ctx),
5256
+ dependencies: rankEdges(dependencies, ctx),
5257
+ coupledWith: rankEdges(coupledWith, ctx),
5258
+ note: EDGE_NOTE
4637
5259
  };
4638
5260
  }
4639
- function countAbstractFiles(nodes, pkgByFile) {
4640
- const out = /* @__PURE__ */ new Map();
4641
- for (const n of nodes) {
4642
- if (n.kind !== "file" || n.role !== "types") continue;
4643
- const pkg = pkgByFile.get(n.id);
4644
- 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 };
4645
5293
  }
4646
- return out;
4647
5294
  }
4648
- function ratioOrZero(num, denom) {
4649
- return denom === 0 ? 0 : num / denom;
5295
+ function sliceSpan(content, span) {
5296
+ return content.split("\n").slice(span.startLine - 1, span.endLine).join("\n");
4650
5297
  }
4651
- function classifyLayer(instability) {
4652
- if (instability <= FOUNDATION_INSTABILITY_MAX) return "foundation";
4653
- if (instability >= TOP_INSTABILITY_MIN) return "top";
4654
- 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
+ };
4655
5309
  }
4656
- function buildPairCoupling(pairs, filesByPkg) {
4657
- const counts = /* @__PURE__ */ new Map();
4658
- for (const p of pairs) {
4659
- if (p.from === p.to) continue;
4660
- const key = JSON.stringify([p.from, p.to]);
4661
- const entry = counts.get(key);
4662
- if (entry) entry.edges += 1;
4663
- else counts.set(key, { from: p.from, to: p.to, edges: 1 });
4664
- }
4665
- const out = [];
4666
- for (const { from, to, edges } of counts.values()) {
4667
- const filesFrom = filesByPkg.get(from) ?? 0;
4668
- const intensity = filesFrom === 0 ? 0 : edges / filesFrom;
4669
- const flag = classifyPairIntensity(intensity);
4670
- out.push({ from, to, edges, intensity, flag });
4671
- }
4672
- return out.sort(comparePairs2);
4673
- }
4674
- function classifyPairIntensity(intensity) {
4675
- if (intensity >= TIGHT_INTENSITY_MIN) return "tight";
4676
- if (intensity >= MODERATE_INTENSITY_MIN) return "moderate";
4677
- return "none";
4678
- }
4679
- function comparePairs2(a, b) {
4680
- if (a.from !== b.from) return a.from < b.from ? -1 : 1;
4681
- return a.to < b.to ? -1 : 1;
4682
- }
4683
- function computeModularity(perPackage, totalEdges) {
4684
- if (totalEdges === 0) return 0;
4685
- let Q = 0;
4686
- for (const p of perPackage) {
4687
- const e_cc = p.internalEdges / totalEdges;
4688
- const touching = p.internalEdges * 2 + p.outgoingEdges + p.incomingEdges;
4689
- const a_c = touching / (2 * totalEdges);
4690
- Q += e_cc - a_c * a_c;
4691
- }
4692
- 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})`;
4693
5334
  }
4694
5335
 
4695
5336
  // src/analysis/snapshot.ts
@@ -4709,6 +5350,57 @@ function snapshotSymbolCoupling(store, snapshotId, options = {}) {
4709
5350
  return computeSymbolCoupling(snapshotReferenceEdges(store, snapshotId), options);
4710
5351
  }
4711
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
+
4712
5404
  // src/read.ts
4713
5405
  function listNodes(store, snapshotId, opts) {
4714
5406
  return store.listNodes(snapshotId, opts);
@@ -4804,7 +5496,7 @@ var SOURCE = [
4804
5496
  direction: "higher-worse",
4805
5497
  absent: "exclude",
4806
5498
  source: "source-metrics",
4807
- 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."
4808
5500
  },
4809
5501
  {
4810
5502
  name: "cyclomatic_sum",
@@ -4814,7 +5506,7 @@ var SOURCE = [
4814
5506
  direction: "higher-worse",
4815
5507
  absent: "zero",
4816
5508
  source: "source-metrics",
4817
- description: "Cyclomatic complexity summed over the file's functions."
5509
+ description: "Cyclomatic complexity summed over the file's functions; points attention at branchy code."
4818
5510
  },
4819
5511
  {
4820
5512
  name: "cognitive_max",
@@ -4824,7 +5516,7 @@ var SOURCE = [
4824
5516
  direction: "higher-worse",
4825
5517
  absent: "exclude",
4826
5518
  source: "source-metrics",
4827
- 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."
4828
5520
  },
4829
5521
  {
4830
5522
  name: "cognitive_sum",
@@ -4834,7 +5526,7 @@ var SOURCE = [
4834
5526
  direction: "higher-worse",
4835
5527
  absent: "zero",
4836
5528
  source: "source-metrics",
4837
- description: "Cognitive complexity summed over the file's functions."
5529
+ description: "Cognitive complexity summed over the file's functions, an estimate of comprehension friction."
4838
5530
  },
4839
5531
  {
4840
5532
  name: "max_nesting_depth",
@@ -4846,6 +5538,16 @@ var SOURCE = [
4846
5538
  source: "source-metrics",
4847
5539
  description: "Deepest block nesting inside any function in the file."
4848
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
+ },
4849
5551
  {
4850
5552
  name: "symbol_cognitive",
4851
5553
  unit: "count",
@@ -4854,7 +5556,7 @@ var SOURCE = [
4854
5556
  direction: "higher-worse",
4855
5557
  absent: "exclude",
4856
5558
  source: "source-metrics",
4857
- 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."
4858
5560
  },
4859
5561
  {
4860
5562
  name: "symbol_cyclomatic",
@@ -4864,7 +5566,7 @@ var SOURCE = [
4864
5566
  direction: "higher-worse",
4865
5567
  absent: "exclude",
4866
5568
  source: "source-metrics",
4867
- 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."
4868
5570
  },
4869
5571
  {
4870
5572
  name: "symbol_loc",
@@ -4886,6 +5588,16 @@ var SOURCE = [
4886
5588
  source: "source-metrics",
4887
5589
  description: "Deepest block nesting inside the function a symbol names; the max when several share the name."
4888
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
+ },
4889
5601
  {
4890
5602
  name: "symbol_comment_lines",
4891
5603
  unit: "lines",
@@ -5132,7 +5844,7 @@ var HISTORY = [
5132
5844
  absent: "exclude",
5133
5845
  source: "history",
5134
5846
  windowed: true,
5135
- 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."
5136
5848
  },
5137
5849
  {
5138
5850
  name: "file_age_days",
@@ -5275,39 +5987,6 @@ function indexMetrics(metrics) {
5275
5987
  return metricsByNode;
5276
5988
  }
5277
5989
 
5278
- // src/check/patterns.ts
5279
- function patternToRegex(pattern) {
5280
- if (!pattern.includes("*")) {
5281
- return new RegExp(escapeRegex(pattern));
5282
- }
5283
- let out = "";
5284
- for (let i = 0; i < pattern.length; i++) {
5285
- const c = pattern[i];
5286
- if (c === "*" && pattern[i + 1] === "*") {
5287
- out += ".*";
5288
- i++;
5289
- } else if (c === "*") {
5290
- out += "[^/]*";
5291
- } else {
5292
- out += escapeRegex(c);
5293
- }
5294
- }
5295
- return new RegExp(`^${out}$`);
5296
- }
5297
- function compilePatterns(patterns) {
5298
- if (!patterns) return [];
5299
- return patterns.map(patternToRegex);
5300
- }
5301
- function matchesAny(value, patterns) {
5302
- for (const rx of patterns) {
5303
- if (rx.test(value)) return true;
5304
- }
5305
- return false;
5306
- }
5307
- function escapeRegex(s) {
5308
- return s.replace(/[.+?(){}|^$\\[\]]/g, "\\$&");
5309
- }
5310
-
5311
5990
  // src/check/rule-helpers.ts
5312
5991
  function severityOf(rule) {
5313
5992
  return rule.severity ?? "error";
@@ -5316,8 +5995,8 @@ function formatNumber(n) {
5316
5995
  if (Number.isInteger(n)) return String(n);
5317
5996
  return n.toFixed(3).replace(/\.?0+$/, "");
5318
5997
  }
5319
- function isImportEdge(edge) {
5320
- return edge.kind === "imports" || edge.kind === "re-exports";
5998
+ function isImportEdge(edge2) {
5999
+ return edge2.kind === "imports" || edge2.kind === "re-exports";
5321
6000
  }
5322
6001
  function longestPrefixMatcher(prefixes) {
5323
6002
  const byLength = [...prefixes].sort((a, b) => b.length - a.length);
@@ -5340,11 +6019,17 @@ function layerIndex(layers) {
5340
6019
  function runLayeredDepsRule(rule, ctx) {
5341
6020
  const packageLayer = layerIndex(rule.layers);
5342
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
+ };
5343
6027
  const out = [];
5344
- for (const edge of ctx.edges) {
5345
- if (!isImportEdge(edge)) continue;
5346
- const srcPkg = packageOf(edge.srcId);
5347
- 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);
5348
6033
  if (srcPkg === null || dstPkg === null) continue;
5349
6034
  const srcLayer = packageLayer.get(srcPkg);
5350
6035
  const dstLayer = packageLayer.get(dstPkg);
@@ -5352,9 +6037,9 @@ function runLayeredDepsRule(rule, ctx) {
5352
6037
  out.push({
5353
6038
  ruleId: rule.id,
5354
6039
  severity: severityOf(rule),
5355
- nodeId: edge.srcId,
5356
- destinationId: edge.dstId,
5357
- 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})`
5358
6043
  });
5359
6044
  }
5360
6045
  return out;
@@ -5362,31 +6047,32 @@ function runLayeredDepsRule(rule, ctx) {
5362
6047
  function runForbidImportRule(rule, ctx) {
5363
6048
  const fromRx = compilePatterns([rule.from]);
5364
6049
  const toRx = compilePatterns([rule.to]);
6050
+ const exceptRx = compilePatterns(rule.except);
5365
6051
  const out = [];
5366
- for (const edge of ctx.edges) {
5367
- if (!isImportEdge(edge)) continue;
5368
- if (!matchesAny(edge.srcId, fromRx)) continue;
5369
- 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;
5370
6056
  out.push({
5371
6057
  ruleId: rule.id,
5372
6058
  severity: severityOf(rule),
5373
- nodeId: edge.srcId,
5374
- destinationId: edge.dstId,
5375
- 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})`
5376
6062
  });
5377
6063
  }
5378
6064
  return out;
5379
6065
  }
5380
6066
  function importersByDestination(edges) {
5381
6067
  const importersByDst = /* @__PURE__ */ new Map();
5382
- for (const edge of edges) {
5383
- if (!isImportEdge(edge)) continue;
5384
- let list = importersByDst.get(edge.dstId);
6068
+ for (const edge2 of edges) {
6069
+ if (!isImportEdge(edge2)) continue;
6070
+ let list = importersByDst.get(edge2.dstId);
5385
6071
  if (!list) {
5386
6072
  list = [];
5387
- importersByDst.set(edge.dstId, list);
6073
+ importersByDst.set(edge2.dstId, list);
5388
6074
  }
5389
- list.push(edge.srcId);
6075
+ list.push(edge2.srcId);
5390
6076
  }
5391
6077
  return importersByDst;
5392
6078
  }
@@ -5621,13 +6307,6 @@ function collectBaselineKeys(store, baselineSnapshotId, snapshotId, rules) {
5621
6307
  }
5622
6308
  return keys;
5623
6309
  }
5624
- function violationKey(v) {
5625
- return v.destinationId ? `${v.ruleId}|${v.nodeId}|${v.destinationId}` : `${v.ruleId}|${v.nodeId}`;
5626
- }
5627
- function rebasedViolationKey(v, resolve4) {
5628
- const destinationId = v.destinationId ? resolve4(v.destinationId) : v.destinationId;
5629
- return violationKey({ ...v, nodeId: resolve4(v.nodeId), destinationId });
5630
- }
5631
6310
  function countByOriginAndSeverity(violations) {
5632
6311
  const counts = { newErrors: 0, newWarnings: 0, carryoverErrors: 0, carryoverWarnings: 0 };
5633
6312
  for (const v of violations) {
@@ -5845,7 +6524,7 @@ function validateRule(raw, index, warn) {
5845
6524
  case "forbid-import":
5846
6525
  return assertForbidImport(r);
5847
6526
  case "layered-deps":
5848
- return assertLayeredDeps(r);
6527
+ return assertLayeredDeps(r, warn);
5849
6528
  case "no-internal-only-barrels":
5850
6529
  return assertNoInternalOnlyBarrels(r);
5851
6530
  default:
@@ -5867,11 +6546,11 @@ function assertNoInternalOnlyBarrels(r) {
5867
6546
  type: "no-internal-only-barrels",
5868
6547
  id: r.id,
5869
6548
  packageRoots: r.packageRoots,
5870
- severity: r.severity,
5871
- exclude: parseStringArray(r.exclude)
6549
+ severity: parseSeverity(r),
6550
+ exclude: parseExclude(r)
5872
6551
  };
5873
6552
  }
5874
- function assertLayeredDeps(r) {
6553
+ function assertLayeredDeps(r, warn) {
5875
6554
  if (!Array.isArray(r.layers) || r.layers.length < 2) {
5876
6555
  throw new Error(`${r.id}: layers must be an array of 2+ string arrays`);
5877
6556
  }
@@ -5896,7 +6575,8 @@ function assertLayeredDeps(r) {
5896
6575
  type: "layered-deps",
5897
6576
  id: r.id,
5898
6577
  layers: r.layers,
5899
- severity: r.severity
6578
+ severity: parseSeverity(r),
6579
+ excludeRoles: parseRoleArray(r.id, r.excludeRoles, warn)
5900
6580
  };
5901
6581
  }
5902
6582
  function assertMetricProductMax(r, warn) {
@@ -5915,9 +6595,9 @@ function assertMetricProductMax(r, warn) {
5915
6595
  id: ruleId,
5916
6596
  metrics: r.metrics.map((m) => healMetricName(m, ruleId, warn)),
5917
6597
  max: r.max,
5918
- kind: r.kind,
5919
- severity: r.severity,
5920
- exclude: parseStringArray(r.exclude),
6598
+ kind: parseOptionalKind(r),
6599
+ severity: parseSeverity(r),
6600
+ exclude: parseExclude(r),
5921
6601
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5922
6602
  };
5923
6603
  }
@@ -5930,9 +6610,9 @@ function assertMetricMax(r, warn) {
5930
6610
  id: ruleId,
5931
6611
  metric: healMetricName(r.metric, ruleId, warn),
5932
6612
  max: r.max,
5933
- kind: r.kind,
5934
- severity: r.severity,
5935
- exclude: parseStringArray(r.exclude),
6613
+ kind: parseOptionalKind(r),
6614
+ severity: parseSeverity(r),
6615
+ exclude: parseExclude(r),
5936
6616
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5937
6617
  };
5938
6618
  }
@@ -5945,18 +6625,16 @@ function assertMetricMin(r, warn) {
5945
6625
  id: ruleId,
5946
6626
  metric: healMetricName(r.metric, ruleId, warn),
5947
6627
  min: r.min,
5948
- kind: r.kind,
5949
- severity: r.severity,
5950
- exclude: parseStringArray(r.exclude),
6628
+ kind: parseOptionalKind(r),
6629
+ severity: parseSeverity(r),
6630
+ exclude: parseExclude(r),
5951
6631
  excludeRoles: parseRoleArray(ruleId, r.excludeRoles, warn)
5952
6632
  };
5953
6633
  }
5954
6634
  var NODE_KINDS = /* @__PURE__ */ new Set(["package", "module", "file", "symbol", "external"]);
5955
6635
  function assertMetricOutlier(r, warn) {
5956
6636
  if (typeof r.metric !== "string") throw new Error(`${r.id}: metric must be a string`);
5957
- if (typeof r.kind !== "string" || !NODE_KINDS.has(r.kind)) {
5958
- throw new Error(`${r.id}: kind must be one of ${[...NODE_KINDS].join(", ")}`);
5959
- }
6637
+ if (!isNodeKind(r.kind)) throw kindError(r.id);
5960
6638
  if (typeof r.percentile !== "number" || r.percentile < 50 || r.percentile > 100) {
5961
6639
  throw new Error(`${r.id}: percentile must be a number from 50 to 100`);
5962
6640
  }
@@ -5979,7 +6657,7 @@ function assertMetricOutlier(r, warn) {
5979
6657
  minSample: r.minSample,
5980
6658
  floor: r.floor,
5981
6659
  rankNonZero: r.rankNonZero,
5982
- severity: r.severity
6660
+ severity: parseSeverity(r)
5983
6661
  };
5984
6662
  }
5985
6663
  function assertForbidImport(r) {
@@ -5990,15 +6668,43 @@ function assertForbidImport(r) {
5990
6668
  id: r.id,
5991
6669
  from: r.from,
5992
6670
  to: r.to,
5993
- severity: r.severity
6671
+ except: parseStringList(r, "except"),
6672
+ severity: parseSeverity(r)
5994
6673
  };
5995
6674
  }
5996
- function parseStringArray(value) {
5997
- 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;
5998
6702
  }
5999
6703
  var ROLE_VALUES = /* @__PURE__ */ new Set([
6000
6704
  "test",
6001
6705
  "fixture",
6706
+ "story",
6707
+ "lab",
6002
6708
  "barrel",
6003
6709
  "types",
6004
6710
  "config",
@@ -6029,7 +6735,7 @@ function healRole(entry, ruleId, warn) {
6029
6735
  }
6030
6736
 
6031
6737
  // src/check/run.ts
6032
- import { readFile as readFile2 } from "fs/promises";
6738
+ import { readFile } from "fs/promises";
6033
6739
  function resolveSnapshot(store, spec) {
6034
6740
  if (typeof spec === "number" || /^\d+$/.test(spec)) {
6035
6741
  const snap2 = store.getSnapshot(Number(spec));
@@ -6050,18 +6756,18 @@ function checkSnapshot(store, options) {
6050
6756
  });
6051
6757
  return { snapshot, baselineSnapshot, result };
6052
6758
  }
6053
- async function loadCheckRules(path11, options = {}) {
6759
+ async function loadCheckRules(path14, options = {}) {
6054
6760
  let raw;
6055
6761
  try {
6056
- raw = await readFile2(path11, "utf8");
6762
+ raw = await readFile(path14, "utf8");
6057
6763
  } catch (err) {
6058
- throw new Error(`Cannot read rules file at ${path11}: ${errorMessage(err)}`);
6764
+ throw new Error(`Cannot read rules file at ${path14}: ${errorMessage(err)}`);
6059
6765
  }
6060
6766
  let parsed;
6061
6767
  try {
6062
6768
  parsed = JSON.parse(raw);
6063
6769
  } catch (err) {
6064
- throw new Error(`Invalid JSON in ${path11}: ${errorMessage(err)}`);
6770
+ throw new Error(`Invalid JSON in ${path14}: ${errorMessage(err)}`);
6065
6771
  }
6066
6772
  return validateRules(parsed, options);
6067
6773
  }
@@ -6191,44 +6897,16 @@ function diffCheckResults(store, options) {
6191
6897
  fromSnapshotId: options.fromSnapshotId,
6192
6898
  toSnapshotId: options.toSnapshotId,
6193
6899
  rulesEvaluated: options.rules.length,
6194
- ...bucketViolations(indexByKey(from.violations, chain.resolve), indexByKey(to.violations))
6900
+ ...bucketViolations(from.violations, to.violations, chain.resolve)
6195
6901
  };
6196
6902
  }
6197
- function bucketViolations(fromByKey, toByKey) {
6198
- const buckets = { newViolations: [], resolvedViolations: [], unchanged: [], worsened: [], improved: [] };
6199
- for (const [key, toV] of toByKey) {
6200
- const fromV = fromByKey.get(key);
6201
- if (!fromV) {
6202
- buckets.newViolations.push(toV);
6203
- continue;
6204
- }
6205
- const delta = computeDelta(fromV, toV);
6206
- const entry = { from: fromV, to: toV, delta };
6207
- buckets.unchanged.push(entry);
6208
- if (delta !== null && delta > 0) buckets.worsened.push(entry);
6209
- else if (delta !== null && delta < 0) buckets.improved.push(entry);
6210
- }
6211
- for (const [key, fromV] of fromByKey) {
6212
- if (!toByKey.has(key)) buckets.resolvedViolations.push(fromV);
6213
- }
6214
- return buckets;
6215
- }
6216
- function indexByKey(violations, resolve4) {
6217
- const out = /* @__PURE__ */ new Map();
6218
- for (const v of violations) out.set(resolve4 ? rebasedViolationKey(v, resolve4) : violationKey(v), v);
6219
- return out;
6220
- }
6221
- function computeDelta(from, to) {
6222
- if (from.value === void 0 || to.value === void 0) return null;
6223
- return to.value - from.value;
6224
- }
6225
6903
 
6226
6904
  // src/diff/footprint.ts
6227
- var DEFAULT_MIN_CO_IMPORTS2 = 2;
6905
+ var DEFAULT_MIN_CO_IMPORTS = 2;
6228
6906
  function computeFootprints(graph, options = {}) {
6229
6907
  const edges = referenceEdges(graph.edges, options.ignoreConsumer);
6230
6908
  const consumers = consumersBySymbol(edges);
6231
- const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS2);
6909
+ const partners = partnersBySymbol(edges, options.minCoImports ?? DEFAULT_MIN_CO_IMPORTS);
6232
6910
  const out = /* @__PURE__ */ new Map();
6233
6911
  for (const node of graph.nodes) {
6234
6912
  if (node.kind !== "symbol") continue;
@@ -6284,18 +6962,18 @@ function partnersBySymbol(edges, minCoImports) {
6284
6962
  var PART_REASONS = ["signature", "consumers", "coupling"];
6285
6963
  function diffFootprints(store, options) {
6286
6964
  const { fromSnapshotId, toSnapshotId } = options;
6287
- const resolve4 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
6965
+ const resolve5 = symbolAwareResolver(aliasChain(store, fromSnapshotId, toSnapshotId));
6288
6966
  const fromGraph = loadSymbolLayer(store, fromSnapshotId);
6289
6967
  const toGraph = loadSymbolLayer(store, toSnapshotId);
6290
- const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve4);
6291
- const before = computeFootprints(remapGraph(fromGraph, resolve4, new Set(mergedAway)), options);
6968
+ const { previousIds, mergedAway } = mergeSources(fromGraph.nodes, resolve5);
6969
+ const before = computeFootprints(remapGraph(fromGraph, resolve5, new Set(mergedAway)), options);
6292
6970
  const after = computeFootprints(toGraph, options);
6293
6971
  const changes = [
6294
6972
  ...changedOrAdded(after, before, previousIds),
6295
- ...removed(before, after, previousIds, resolve4),
6296
- ...mergedAway.map((id) => ({ symbolId: id, status: "removed", reasons: [], fileId: resolve4(declaringFile(id)) }))
6297
- ].sort((a, b) => compare2(a.symbolId, b.symbolId));
6298
- const files = [...new Set(changes.map((c) => c.fileId))].sort(compare2);
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);
6299
6977
  return { fromSnapshotId, toSnapshotId, changes, files };
6300
6978
  }
6301
6979
  function loadSymbolLayer(store, snapshotId) {
@@ -6312,22 +6990,22 @@ function symbolAwareResolver(aliases) {
6312
6990
  return parsed ? symbolId(aliases.resolve(parsed.fileId), parsed.name) : id;
6313
6991
  };
6314
6992
  }
6315
- function remapGraph(graph, resolve4, losers) {
6993
+ function remapGraph(graph, resolve5, losers) {
6316
6994
  return {
6317
- nodes: graph.nodes.filter((n) => !losers.has(n.id)).map((n) => ({ ...n, id: resolve4(n.id) })),
6318
- edges: graph.edges.filter((e) => !losers.has(e.srcId) && !losers.has(e.dstId)).map((e) => ({ ...e, srcId: resolve4(e.srcId), dstId: resolve4(e.dstId) }))
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) }))
6319
6997
  };
6320
6998
  }
6321
- function mergeSources(nodes, resolve4) {
6999
+ function mergeSources(nodes, resolve5) {
6322
7000
  const groups = /* @__PURE__ */ new Map();
6323
7001
  for (const node of nodes) {
6324
- const id = resolve4(node.id);
7002
+ const id = resolve5(node.id);
6325
7003
  groups.set(id, [...groups.get(id) ?? [], node.id]);
6326
7004
  }
6327
7005
  const previousIds = /* @__PURE__ */ new Map();
6328
7006
  const mergedAway = [];
6329
7007
  for (const [id, sources] of groups) {
6330
- const keeper = sources.includes(id) ? id : [...sources].sort(compare2)[0];
7008
+ const keeper = sources.includes(id) ? id : [...sources].sort(compare3)[0];
6331
7009
  if (keeper !== id) previousIds.set(id, keeper);
6332
7010
  mergedAway.push(...sources.filter((source) => source !== keeper));
6333
7011
  }
@@ -6353,19 +7031,19 @@ function reasonsFor(prior, current, renamed) {
6353
7031
  if (renamed) reasons.push("renamed");
6354
7032
  return reasons;
6355
7033
  }
6356
- function removed(before, after, previousIds, resolve4) {
7034
+ function removed(before, after, previousIds, resolve5) {
6357
7035
  const out = [];
6358
7036
  for (const id of before.keys()) {
6359
7037
  if (after.has(id)) continue;
6360
7038
  const fromId = previousIds.get(id) ?? id;
6361
- out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve4(declaringFile(fromId)) });
7039
+ out.push({ symbolId: fromId, status: "removed", reasons: [], fileId: resolve5(declaringFile(fromId)) });
6362
7040
  }
6363
7041
  return out;
6364
7042
  }
6365
7043
  function declaringFile(id) {
6366
7044
  return parseSymbolId(id)?.fileId ?? id;
6367
7045
  }
6368
- function compare2(a, b) {
7046
+ function compare3(a, b) {
6369
7047
  return a < b ? -1 : a > b ? 1 : 0;
6370
7048
  }
6371
7049
 
@@ -6463,17 +7141,17 @@ function storeVectors(store, model, rows) {
6463
7141
  async function embedTextsCached(store, embedder, texts) {
6464
7142
  const hashes = texts.map((t) => hashEmbedText(t));
6465
7143
  const byHash = loadVectorsByHash(store, embedder.model, hashes);
6466
- const missing = /* @__PURE__ */ new Map();
7144
+ const missing2 = /* @__PURE__ */ new Map();
6467
7145
  texts.forEach((text, i) => {
6468
- if (!byHash.has(hashes[i])) missing.set(hashes[i], text);
7146
+ if (!byHash.has(hashes[i])) missing2.set(hashes[i], text);
6469
7147
  });
6470
- const pending = [...missing.entries()];
7148
+ const pending = [...missing2.entries()];
6471
7149
  for (let i = 0; i < pending.length; i += EMBED_BATCH_SIZE) {
6472
7150
  const rows = await embedBatch(embedder, pending.slice(i, i + EMBED_BATCH_SIZE));
6473
7151
  storeVectors(store, embedder.model, rows);
6474
7152
  for (const r of rows) byHash.set(r.textHash, r.vector);
6475
7153
  }
6476
- return { byHash, newHashes: new Set(missing.keys()) };
7154
+ return { byHash, newHashes: new Set(missing2.keys()) };
6477
7155
  }
6478
7156
  async function embedBatch(embedder, batch) {
6479
7157
  const vectors = await embedder.embed(batch.map(([, text]) => text), { role: "document" });
@@ -6643,7 +7321,7 @@ function pushMulti(map, key, value) {
6643
7321
  }
6644
7322
 
6645
7323
  // src/conventions/areas.ts
6646
- import * as path10 from "path";
7324
+ import * as path13 from "path";
6647
7325
  import { contentHashOf as contentHashOf2 } from "@titan-design/store-sqlite";
6648
7326
  var DEFAULT_MIN_SIZE = 3;
6649
7327
  var PROMPT_FILE_CAP = 30;
@@ -6727,7 +7405,7 @@ function rankByDegree(members, edges) {
6727
7405
  function dominantDirectory(files) {
6728
7406
  const counts = /* @__PURE__ */ new Map();
6729
7407
  for (const f of files) {
6730
- const dir = path10.posix.dirname(f);
7408
+ const dir = path13.posix.dirname(f);
6731
7409
  counts.set(dir, (counts.get(dir) ?? 0) + 1);
6732
7410
  }
6733
7411
  const dirs = [...counts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
@@ -6847,14 +7525,21 @@ function toMatch(area, score) {
6847
7525
  export {
6848
7526
  ALIAS_BASE_ATTR,
6849
7527
  ALL_ROLES,
7528
+ BUNDLE_SCHEMA_VERSION,
6850
7529
  COMMUNITY_SUMMARY_NAMESPACE,
7530
+ SCHEMA_VERSION2 as CONTEXT_SCHEMA_VERSION,
6851
7531
  COVERAGE_METRIC_NAME,
6852
7532
  CodeGraphStore,
6853
7533
  DEAD_CODE_METRIC_NAMES,
6854
7534
  DEFAULT_CHURN_WINDOWS,
7535
+ DEFAULT_HEALTH_WEIGHTS,
7536
+ DEFAULT_MAX_LINE_CHARS,
7537
+ DEFAULT_MAX_PACKAGE_SIZE,
6855
7538
  DEFAULT_OUTLIER_MIN_SAMPLE,
6856
7539
  DOMAIN_DDL,
7540
+ ELISION_MARKER,
6857
7541
  EXCEPTION_METRIC_NAMES,
7542
+ EXTERNAL_BUCKET,
6858
7543
  FINDING_DDL,
6859
7544
  GROWTH_RISK_METRIC_NAMES,
6860
7545
  INDEX_VERSION,
@@ -6869,35 +7554,55 @@ export {
6869
7554
  SYMBOL_EMBEDDING_NAMESPACE,
6870
7555
  SYMBOL_ID_SEP,
6871
7556
  SYMBOL_METRIC_NAMES,
7557
+ TRUNCATION_SUFFIX,
6872
7558
  TsMorphGraphExtractor,
7559
+ UNIMPORTED_ROLES,
7560
+ aggregateEdges,
6873
7561
  aggregateMetrics,
6874
7562
  aliasChain,
6875
7563
  annotateRoles,
6876
7564
  attributeCoverage,
7565
+ bucketFilesByPackage,
7566
+ bucketViolations,
6877
7567
  buildAliases,
7568
+ buildBlastRadius,
7569
+ buildCentralFiles,
7570
+ buildContextBundle,
7571
+ buildContextDossier,
6878
7572
  buildConventionAreas,
6879
7573
  buildEmbedText,
6880
7574
  buildFileModuleNodes,
7575
+ buildHotExports,
6881
7576
  buildIndexerMetrics,
6882
7577
  buildLineage,
7578
+ buildNodeMetrics,
7579
+ buildReportContext,
7580
+ buildSymbolCouplingPayload,
7581
+ busFactorOf,
6883
7582
  canonicalEdgeKind,
6884
7583
  canonicalMetricName,
6885
7584
  canonicalRole,
6886
7585
  carryForwardVerdicts,
6887
7586
  checkSnapshot,
7587
+ classifyCoupling,
6888
7588
  classifyRole,
6889
7589
  collectDeclaredNames,
6890
7590
  collectDeclaredSpans,
7591
+ collectNodeMetrics,
7592
+ collectSymbolUtil,
6891
7593
  compilePatterns,
7594
+ computeArch,
6892
7595
  computeDeadCodeMetrics,
6893
7596
  computeDeepAst,
6894
7597
  computeFootprints,
6895
7598
  computeGrowthRiskMetrics,
7599
+ computeHealth,
6896
7600
  computeMetrics,
6897
7601
  computePageRank,
6898
7602
  computePartitionQuality,
6899
7603
  computeRecencyWindows,
6900
7604
  computeRelevance,
7605
+ computeReportDrift,
6901
7606
  computeRoleHints,
6902
7607
  computeSourceMetrics,
6903
7608
  computeSymbolConsumers,
@@ -6920,6 +7625,7 @@ export {
6920
7625
  externalId,
6921
7626
  externalToFinding,
6922
7627
  fileId,
7628
+ filteredFileIds,
6923
7629
  findConventions,
6924
7630
  findSimilarCapability,
6925
7631
  findingKey,
@@ -6928,15 +7634,20 @@ export {
6928
7634
  getEdgeWeight,
6929
7635
  getLanguageFromPath3 as getLanguageFromPath,
6930
7636
  getSupportedLanguages,
7637
+ gitTreeSource,
6931
7638
  groupTestsBySource,
6932
7639
  hashContent,
6933
7640
  hashEmbedText,
6934
7641
  hashText,
7642
+ hotspotScoreOf,
7643
+ importMarker,
6935
7644
  indexPaths,
6936
7645
  invertBuckets,
6937
7646
  isGeneratedByHeuristic,
6938
7647
  isGeneratedFile,
6939
7648
  isInsideGitRepo,
7649
+ isMinifyLanguage,
7650
+ keepNode,
6940
7651
  keyFindings,
6941
7652
  lineagePath,
6942
7653
  linkTestsToSources,
@@ -6952,21 +7663,32 @@ export {
6952
7663
  loadGeneratedPatterns,
6953
7664
  loadHistoryMetrics,
6954
7665
  loadLineage,
7666
+ loadRoleGlobs,
7667
+ lookupMetric,
6955
7668
  matchesAny,
7669
+ minifySource,
6956
7670
  moduleId,
6957
7671
  normalizeFlaggedText,
6958
7672
  openCodeGraph,
7673
+ originalLine,
6959
7674
  packageId,
7675
+ packagesReferencedByEdges,
7676
+ pairKey,
6960
7677
  parentModuleId,
6961
- parseFile2 as parseFile,
7678
+ parseFile3 as parseFile,
6962
7679
  parseSymbolId,
6963
7680
  patternToRegex,
6964
7681
  percentileOf,
6965
7682
  planPrune,
6966
7683
  priorSnapshotForRef,
6967
7684
  pruneDanglingReferences,
7685
+ publicApiFiles,
6968
7686
  readSourceFiles,
6969
7687
  rebasedViolationKey,
7688
+ referencedNodes,
7689
+ renderBundleText,
7690
+ renderContextMarkdown,
7691
+ renderSignatureTree,
6970
7692
  resolveAlias,
6971
7693
  resolveBarrelEdges,
6972
7694
  resolveChurnWindows,
@@ -6976,7 +7698,7 @@ export {
6976
7698
  runPrune,
6977
7699
  saveFindings,
6978
7700
  saveVerdicts,
6979
- shouldIncludeFile2 as shouldIncludeFile,
7701
+ shouldIncludeFile3 as shouldIncludeFile,
6980
7702
  snapshotPageRank,
6981
7703
  snapshotReferenceEdges,
6982
7704
  snapshotRelevance,
@@ -6989,11 +7711,21 @@ export {
6989
7711
  symbolSetHash,
6990
7712
  testCoverageCountMetrics,
6991
7713
  toFindings,
7714
+ toSortedEdges,
7715
+ topBusFactorRisks,
7716
+ topCentralFiles,
7717
+ topDeadModules,
7718
+ topGrowthRisks,
7719
+ topHotspots,
7720
+ topTestCoverageRisks,
7721
+ topUntestedRisks,
7722
+ topUnusedExports,
6992
7723
  tryEmbedSnapshot,
6993
7724
  unitProvenance,
6994
7725
  validateRules,
6995
7726
  violationKey,
6996
7727
  walkSourceFiles,
6997
- windowSuffix
7728
+ windowSuffix,
7729
+ workingTreeSource
6998
7730
  };
6999
7731
  //# sourceMappingURL=index.js.map