@pratik7368patil/anchor-core 0.1.25 → 0.1.26

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.d.ts CHANGED
@@ -574,6 +574,9 @@ type OrgGraphProgress = {
574
574
  edges: number;
575
575
  apiContracts: number;
576
576
  apiConsumers: number;
577
+ current?: number;
578
+ total?: number;
579
+ kind?: "edges" | "contracts" | "consumers";
577
580
  } | {
578
581
  stage: "completed_org_graph";
579
582
  org: string;
@@ -762,6 +765,15 @@ type CodeIndexProgress = {
762
765
  stage: "writing_code_index";
763
766
  repo: string;
764
767
  phase: string;
768
+ } | {
769
+ stage: "inferring_test_awareness";
770
+ repo: string;
771
+ phase: "classifying_files" | "indexing_sources" | "linking_tests" | "completed";
772
+ current: number;
773
+ total: number;
774
+ testFiles: number;
775
+ testLinks: number;
776
+ filePath?: string;
765
777
  } | {
766
778
  stage: "deleting_existing_code_index";
767
779
  repo: string;
@@ -1131,8 +1143,11 @@ declare function indexCodebase(db: AnchorDatabase, options: {
1131
1143
  }): CodeIndexSummary;
1132
1144
  declare function emptyCodeIndexSummary(cwd: string): CodeIndexSummary;
1133
1145
 
1146
+ type TestAwarenessOptions = {
1147
+ onProgress?: (progress: CodeIndexProgress) => void;
1148
+ };
1134
1149
  declare function isTestFilePath(filePath: string): boolean;
1135
- declare function inferTestAwareness(repo: string, codeFiles: CodeFileRecord[], codeChunks: CodeChunk[]): {
1150
+ declare function inferTestAwareness(repo: string, codeFiles: CodeFileRecord[], codeChunks: CodeChunk[], options?: TestAwarenessOptions): {
1136
1151
  testFiles: TestFileRecord[];
1137
1152
  testLinks: TestLink[];
1138
1153
  };
package/dist/index.js CHANGED
@@ -1299,6 +1299,7 @@ function countValidTeamRules(cwd) {
1299
1299
 
1300
1300
  // src/indexer/test-awareness.ts
1301
1301
  import path3 from "path";
1302
+ var TEST_AWARENESS_PROGRESS_INTERVAL = 500;
1302
1303
  function normalizePath(filePath) {
1303
1304
  return filePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
1304
1305
  }
@@ -1335,25 +1336,127 @@ function strengthFor(reason) {
1335
1336
  if (reason === "same directory") return 0.7;
1336
1337
  return 0.5;
1337
1338
  }
1338
- function pathMentionedInTest(testPath, sourcePath, chunksByFile) {
1339
- const text = (chunksByFile.get(testPath) ?? []).map((chunk) => chunk.sanitizedText).join("\n");
1340
- if (!text) return false;
1341
- const sourceNoExt = sourcePath.replace(/\.[^.]+$/i, "");
1342
- const sourceBase = basenameWithoutExtensions(sourcePath);
1343
- return text.includes(sourcePath) || text.includes(sourceNoExt) || new RegExp(`from\\s+["'][^"']*${escapeRegExp(sourceBase)}["']`, "i").test(text) || new RegExp(`require\\(["'][^"']*${escapeRegExp(sourceBase)}["']\\)`, "i").test(text);
1339
+ function shouldEmitProgress(current, total) {
1340
+ return current === 0 || current === 1 || current === total || current % TEST_AWARENESS_PROGRESS_INTERVAL === 0;
1344
1341
  }
1345
- function escapeRegExp(value) {
1346
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1342
+ function addToMap(map, key, value) {
1343
+ const values = map.get(key) ?? [];
1344
+ values.push(value);
1345
+ map.set(key, values);
1346
+ }
1347
+ function withoutExtension(filePath) {
1348
+ return normalizePath(filePath).replace(/\.[^.]+$/i, "");
1349
+ }
1350
+ function testText(testPath, chunksByFile) {
1351
+ return (chunksByFile.get(testPath) ?? []).map((chunk) => chunk.sanitizedText).join("\n");
1352
+ }
1353
+ function importSpecifiers(text) {
1354
+ const specifiers = [];
1355
+ const patterns = [
1356
+ /\bfrom\s+["']([^"']+)["']/g,
1357
+ /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
1358
+ /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g
1359
+ ];
1360
+ for (const pattern of patterns) {
1361
+ for (const match of text.matchAll(pattern)) {
1362
+ if (match[1]) specifiers.push(match[1]);
1363
+ }
1364
+ }
1365
+ return uniqueStrings(specifiers);
1347
1366
  }
1348
- function inferTestAwareness(repo, codeFiles, codeChunks) {
1349
- const testFiles = codeFiles.filter((file) => isTestFilePath(file.path));
1350
- const sourceFiles = codeFiles.filter((file) => !isTestFilePath(file.path));
1367
+ function pathLikeMentions(text) {
1368
+ const mentions = /* @__PURE__ */ new Set();
1369
+ const pattern = /[A-Za-z0-9_@./-]+(?:\.[A-Za-z0-9_@./-]+)?/g;
1370
+ for (const match of text.matchAll(pattern)) {
1371
+ const value = match[0];
1372
+ if (value.includes("/") || /\.[A-Za-z0-9]+$/.test(value)) mentions.add(value);
1373
+ }
1374
+ return [...mentions];
1375
+ }
1376
+ function sourceCandidatesForSpecifier(testPath, specifier, sourcesByBase, sourcesByPath, sourcesByNoExt) {
1377
+ const normalizedSpecifier = normalizePath(specifier);
1378
+ const candidates = [];
1379
+ const add = (items) => {
1380
+ if (items) candidates.push(...items);
1381
+ };
1382
+ add(sourcesByPath.get(normalizedSpecifier));
1383
+ add(sourcesByNoExt.get(normalizedSpecifier));
1384
+ if (normalizedSpecifier.startsWith(".")) {
1385
+ const resolved = normalizePath(path3.posix.join(path3.posix.dirname(testPath), normalizedSpecifier));
1386
+ add(sourcesByPath.get(resolved));
1387
+ add(sourcesByNoExt.get(resolved));
1388
+ }
1389
+ const base = basenameWithoutExtensions(normalizedSpecifier).toLowerCase();
1390
+ if (base) add(sourcesByBase.get(base));
1391
+ return uniqueStrings(candidates.map((source) => source.path)).map((sourcePath) => sourcesByPath.get(sourcePath)?.[0]).filter((source) => source !== void 0);
1392
+ }
1393
+ function inferTestAwareness(repo, codeFiles, codeChunks, options = {}) {
1394
+ const testFiles = [];
1395
+ const sourceFiles = [];
1396
+ options.onProgress?.({
1397
+ stage: "inferring_test_awareness",
1398
+ repo,
1399
+ phase: "classifying_files",
1400
+ current: 0,
1401
+ total: codeFiles.length,
1402
+ testFiles: 0,
1403
+ testLinks: 0
1404
+ });
1405
+ for (const [index, file] of codeFiles.entries()) {
1406
+ if (isTestFilePath(file.path)) testFiles.push(file);
1407
+ else sourceFiles.push(file);
1408
+ const current = index + 1;
1409
+ if (shouldEmitProgress(current, codeFiles.length)) {
1410
+ options.onProgress?.({
1411
+ stage: "inferring_test_awareness",
1412
+ repo,
1413
+ phase: "classifying_files",
1414
+ current,
1415
+ total: codeFiles.length,
1416
+ filePath: file.path,
1417
+ testFiles: testFiles.length,
1418
+ testLinks: 0
1419
+ });
1420
+ }
1421
+ }
1351
1422
  const chunksByFile = /* @__PURE__ */ new Map();
1352
1423
  for (const chunk of codeChunks) {
1353
1424
  const chunks = chunksByFile.get(chunk.filePath) ?? [];
1354
1425
  chunks.push(chunk);
1355
1426
  chunksByFile.set(chunk.filePath, chunks);
1356
1427
  }
1428
+ const sourcesByBase = /* @__PURE__ */ new Map();
1429
+ const sourcesByDir = /* @__PURE__ */ new Map();
1430
+ const sourcesByPath = /* @__PURE__ */ new Map();
1431
+ const sourcesByNoExt = /* @__PURE__ */ new Map();
1432
+ options.onProgress?.({
1433
+ stage: "inferring_test_awareness",
1434
+ repo,
1435
+ phase: "indexing_sources",
1436
+ current: 0,
1437
+ total: sourceFiles.length,
1438
+ testFiles: testFiles.length,
1439
+ testLinks: 0
1440
+ });
1441
+ for (const [index, source] of sourceFiles.entries()) {
1442
+ addToMap(sourcesByBase, basenameWithoutExtensions(source.path).toLowerCase(), source);
1443
+ addToMap(sourcesByDir, sourceLikeDir(source.path).join("/"), source);
1444
+ addToMap(sourcesByPath, normalizePath(source.path), source);
1445
+ addToMap(sourcesByNoExt, withoutExtension(source.path), source);
1446
+ const current = index + 1;
1447
+ if (shouldEmitProgress(current, sourceFiles.length)) {
1448
+ options.onProgress?.({
1449
+ stage: "inferring_test_awareness",
1450
+ repo,
1451
+ phase: "indexing_sources",
1452
+ current,
1453
+ total: sourceFiles.length,
1454
+ filePath: source.path,
1455
+ testFiles: testFiles.length,
1456
+ testLinks: 0
1457
+ });
1458
+ }
1459
+ }
1357
1460
  const linkMap = /* @__PURE__ */ new Map();
1358
1461
  const addLink = (sourcePath, testPath, reason) => {
1359
1462
  const key = `${sourcePath}\0${testPath}\0${reason}`;
@@ -1365,22 +1468,78 @@ function inferTestAwareness(repo, codeFiles, codeChunks) {
1365
1468
  strength: strengthFor(reason)
1366
1469
  });
1367
1470
  };
1368
- for (const test of testFiles) {
1471
+ options.onProgress?.({
1472
+ stage: "inferring_test_awareness",
1473
+ repo,
1474
+ phase: "linking_tests",
1475
+ current: 0,
1476
+ total: testFiles.length,
1477
+ testFiles: testFiles.length,
1478
+ testLinks: 0
1479
+ });
1480
+ for (const [index, test] of testFiles.entries()) {
1369
1481
  const testBase = basenameWithoutExtensions(test.path).toLowerCase();
1370
1482
  const testDir = sourceLikeDir(test.path).join("/");
1371
- for (const source of sourceFiles) {
1372
- const sourceBase = basenameWithoutExtensions(source.path).toLowerCase();
1373
- const sourceDir = sourceLikeDir(source.path).join("/");
1374
- if (testBase === sourceBase) addLink(source.path, test.path, "same basename");
1375
- else if (testDir && sourceDir && testDir === sourceDir) {
1483
+ for (const source of sourcesByBase.get(testBase) ?? []) {
1484
+ addLink(source.path, test.path, "same basename");
1485
+ }
1486
+ if (testDir) {
1487
+ for (const source of sourcesByDir.get(testDir) ?? []) {
1488
+ if (basenameWithoutExtensions(source.path).toLowerCase() === testBase) continue;
1376
1489
  addLink(source.path, test.path, "same directory");
1377
1490
  }
1378
- if (pathMentionedInTest(test.path, source.path, chunksByFile)) {
1379
- addLink(source.path, test.path, "imported source path");
1491
+ }
1492
+ const text = testText(test.path, chunksByFile);
1493
+ const importedSources = /* @__PURE__ */ new Map();
1494
+ for (const specifier of importSpecifiers(text)) {
1495
+ for (const source of sourceCandidatesForSpecifier(
1496
+ test.path,
1497
+ specifier,
1498
+ sourcesByBase,
1499
+ sourcesByPath,
1500
+ sourcesByNoExt
1501
+ )) {
1502
+ importedSources.set(source.path, source);
1503
+ }
1504
+ }
1505
+ for (const mention of pathLikeMentions(text)) {
1506
+ for (const source of sourceCandidatesForSpecifier(
1507
+ test.path,
1508
+ mention,
1509
+ sourcesByBase,
1510
+ sourcesByPath,
1511
+ sourcesByNoExt
1512
+ )) {
1513
+ importedSources.set(source.path, source);
1380
1514
  }
1381
1515
  }
1516
+ for (const source of importedSources.values()) {
1517
+ addLink(source.path, test.path, "imported source path");
1518
+ }
1519
+ const current = index + 1;
1520
+ if (shouldEmitProgress(current, testFiles.length)) {
1521
+ options.onProgress?.({
1522
+ stage: "inferring_test_awareness",
1523
+ repo,
1524
+ phase: "linking_tests",
1525
+ current,
1526
+ total: testFiles.length,
1527
+ filePath: test.path,
1528
+ testFiles: testFiles.length,
1529
+ testLinks: linkMap.size
1530
+ });
1531
+ }
1382
1532
  }
1383
1533
  const dedupedTests = testFiles.map(testRecord);
1534
+ options.onProgress?.({
1535
+ stage: "inferring_test_awareness",
1536
+ repo,
1537
+ phase: "completed",
1538
+ current: testFiles.length,
1539
+ total: testFiles.length,
1540
+ testFiles: dedupedTests.length,
1541
+ testLinks: linkMap.size
1542
+ });
1384
1543
  return {
1385
1544
  testFiles: dedupedTests,
1386
1545
  testLinks: uniqueStrings([...linkMap.keys()]).map((key) => linkMap.get(key))
@@ -1911,7 +2070,9 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1911
2070
  const repoId = ensureRepository(db, repo);
1912
2071
  const now = (/* @__PURE__ */ new Date()).toISOString();
1913
2072
  options.onProgress?.({ stage: "writing_code_index", repo, phase: "Inferring test awareness" });
1914
- const testAwareness = inferTestAwareness(repo, codeFiles, codeChunks);
2073
+ const testAwareness = inferTestAwareness(repo, codeFiles, codeChunks, {
2074
+ onProgress: options.onProgress
2075
+ });
1915
2076
  options.onProgress?.({ stage: "writing_code_index", repo, phase: "Writing code index" });
1916
2077
  const transaction = db.transaction(() => {
1917
2078
  const existingChunks = db.prepare("SELECT id FROM code_chunks WHERE repo_id = ?").all(repoId);
@@ -2636,7 +2797,7 @@ import crypto2 from "crypto";
2636
2797
  import path6 from "path";
2637
2798
  var KNOWN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
2638
2799
  var ARCHITECTURE_PROGRESS_INTERVAL = 250;
2639
- function shouldEmitProgress(current, total) {
2800
+ function shouldEmitProgress2(current, total) {
2640
2801
  return current === 0 || current === 1 || current === total || current % ARCHITECTURE_PROGRESS_INTERVAL === 0;
2641
2802
  }
2642
2803
  function classifyArchitectureArea(filePath, language, content = "") {
@@ -2747,15 +2908,47 @@ function extractCodeImports(sourcePath, content, codePaths, repo = "") {
2747
2908
  return true;
2748
2909
  });
2749
2910
  }
2750
- function relatedTestsFor(filePath, allPaths) {
2911
+ function addToStringMap(map, key, value) {
2912
+ const values = map.get(key) ?? [];
2913
+ values.push(value);
2914
+ map.set(key, values);
2915
+ }
2916
+ function testBaseFor(filePath) {
2917
+ return path6.posix.parse(filePath).name.replace(/\.(test|spec)$/i, "");
2918
+ }
2919
+ function buildRelatedTestIndex(allPaths) {
2920
+ const testPaths = allPaths.filter((candidate) => isTestFilePath(candidate));
2921
+ const byBase = /* @__PURE__ */ new Map();
2922
+ const byDirectory = /* @__PURE__ */ new Map();
2923
+ for (const testPath of testPaths) {
2924
+ addToStringMap(byBase, testBaseFor(testPath), testPath);
2925
+ const segments = path6.posix.dirname(testPath).split("/").filter(Boolean);
2926
+ for (let index = 1; index <= segments.length; index += 1) {
2927
+ addToStringMap(byDirectory, segments.slice(0, index).join("/"), testPath);
2928
+ }
2929
+ }
2930
+ return { testPaths, byBase, byDirectory };
2931
+ }
2932
+ function relatedTestsFor(filePath, index) {
2751
2933
  if (isTestFilePath(filePath)) return [];
2752
2934
  const parsed = path6.posix.parse(filePath);
2753
2935
  const basename = parsed.name.replace(/\.(test|spec)$/i, "");
2754
- return allPaths.filter((candidate) => isTestFilePath(candidate)).filter((candidate) => {
2755
- const candidateParsed = path6.posix.parse(candidate);
2756
- const candidateBase = candidateParsed.name.replace(/\.(test|spec)$/i, "");
2757
- return candidateBase === basename || candidate.startsWith(`${parsed.dir}/`) || candidate.includes(`/${basename}.`);
2758
- }).slice(0, 8);
2936
+ const related = [];
2937
+ const seen = /* @__PURE__ */ new Set();
2938
+ const add = (testPath) => {
2939
+ if (seen.has(testPath)) return;
2940
+ seen.add(testPath);
2941
+ related.push(testPath);
2942
+ };
2943
+ for (const testPath of index.byBase.get(basename) ?? []) add(testPath);
2944
+ if (parsed.dir) {
2945
+ for (const testPath of index.byDirectory.get(parsed.dir) ?? []) add(testPath);
2946
+ }
2947
+ for (const testPath of index.testPaths) {
2948
+ if (testPath.includes(`/${basename}.`)) add(testPath);
2949
+ if (related.length >= 8) break;
2950
+ }
2951
+ return related.slice(0, 8);
2759
2952
  }
2760
2953
  function directoryLabel(filePath) {
2761
2954
  const directory = path6.posix.dirname(filePath.replace(/\\/g, "/"));
@@ -2789,10 +2982,15 @@ function createPattern(input) {
2789
2982
  function buildArchitectureIndex(repo, files, chunks, options = {}) {
2790
2983
  const allPaths = files.map((file) => file.path);
2791
2984
  const codePaths = new Set(allPaths);
2792
- const symbolsByPath = /* @__PURE__ */ new Map();
2985
+ const relatedTestIndex = buildRelatedTestIndex(allPaths);
2986
+ const symbolSetsByPath = /* @__PURE__ */ new Map();
2793
2987
  for (const chunk of chunks) {
2794
- const existing = symbolsByPath.get(chunk.filePath) ?? [];
2795
- symbolsByPath.set(chunk.filePath, uniqueStrings([...existing, ...chunk.symbols]).slice(0, 40));
2988
+ const existing = symbolSetsByPath.get(chunk.filePath) ?? /* @__PURE__ */ new Set();
2989
+ for (const symbol of chunk.symbols) {
2990
+ if (existing.size >= 40) break;
2991
+ existing.add(symbol);
2992
+ }
2993
+ symbolSetsByPath.set(chunk.filePath, existing);
2796
2994
  }
2797
2995
  const imports = [];
2798
2996
  options.onProgress?.({
@@ -2805,7 +3003,7 @@ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2805
3003
  for (const [index, file] of files.entries()) {
2806
3004
  imports.push(...extractCodeImports(file.path, file.content, codePaths, repo));
2807
3005
  const current = index + 1;
2808
- if (shouldEmitProgress(current, files.length)) {
3006
+ if (shouldEmitProgress2(current, files.length)) {
2809
3007
  options.onProgress?.({
2810
3008
  stage: "building_architecture_imports",
2811
3009
  repo,
@@ -2833,7 +3031,7 @@ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2833
3031
  for (const [index, file] of files.entries()) {
2834
3032
  const area = classifyArchitectureArea(file.path, file.language, file.content);
2835
3033
  const fileImports = importsByPath.get(file.path) ?? [];
2836
- const symbols = symbolsByPath.get(file.path) ?? [];
3034
+ const symbols = [...symbolSetsByPath.get(file.path) ?? []];
2837
3035
  components.push({
2838
3036
  repo,
2839
3037
  path: file.path,
@@ -2844,12 +3042,12 @@ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2844
3042
  imports: uniqueStrings(
2845
3043
  fileImports.map((item) => item.importedPath ?? item.specifier).filter(Boolean)
2846
3044
  ).slice(0, 20),
2847
- relatedTests: relatedTestsFor(file.path, allPaths),
3045
+ relatedTests: relatedTestsFor(file.path, relatedTestIndex),
2848
3046
  confidence: area === "unknown" ? 0.45 : 0.82,
2849
3047
  updatedAt: file.updatedAt
2850
3048
  });
2851
3049
  const current = index + 1;
2852
- if (shouldEmitProgress(current, files.length)) {
3050
+ if (shouldEmitProgress2(current, files.length)) {
2853
3051
  options.onProgress?.({
2854
3052
  stage: "building_architecture_components",
2855
3053
  repo,
@@ -2906,7 +3104,7 @@ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2906
3104
  })
2907
3105
  );
2908
3106
  patternProgress += 1;
2909
- if (shouldEmitProgress(patternProgress, patternTotal)) {
3107
+ if (shouldEmitProgress2(patternProgress, patternTotal)) {
2910
3108
  options.onProgress?.({
2911
3109
  stage: "building_architecture_patterns",
2912
3110
  repo,
@@ -2931,7 +3129,7 @@ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2931
3129
  })
2932
3130
  );
2933
3131
  patternProgress += 1;
2934
- if (shouldEmitProgress(patternProgress, patternTotal)) {
3132
+ if (shouldEmitProgress2(patternProgress, patternTotal)) {
2935
3133
  options.onProgress?.({
2936
3134
  stage: "building_architecture_patterns",
2937
3135
  repo,
@@ -3887,7 +4085,7 @@ function symbolMatch2(unit, querySymbols) {
3887
4085
  const lower = symbol.toLowerCase();
3888
4086
  if (unitSymbols.includes(lower)) best = Math.max(best, 1);
3889
4087
  else if (text.includes(`\`${lower}\``)) best = Math.max(best, 1);
3890
- else if (new RegExp(`\\b${escapeRegExp2(lower)}\\b`, "i").test(text))
4088
+ else if (new RegExp(`\\b${escapeRegExp(lower)}\\b`, "i").test(text))
3891
4089
  best = Math.max(best, 0.66);
3892
4090
  else if (unitSymbols.some((candidate) => candidate.includes(lower) || lower.includes(candidate))) {
3893
4091
  best = Math.max(best, 0.35);
@@ -3968,7 +4166,7 @@ function scoreUnit(unit, input, duplicateCount, repeatedEvidenceCount, freshness
3968
4166
  rankSignals: parts
3969
4167
  };
3970
4168
  }
3971
- function escapeRegExp2(value) {
4169
+ function escapeRegExp(value) {
3972
4170
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3973
4171
  }
3974
4172
  function loadCandidates(db, input) {
@@ -4131,7 +4329,7 @@ function symbolMatch3(chunk, querySymbols) {
4131
4329
  for (const symbol of querySymbols) {
4132
4330
  const lower = symbol.toLowerCase();
4133
4331
  if (chunkSymbols.includes(lower)) best = Math.max(best, 1);
4134
- else if (new RegExp(`\\b${escapeRegExp3(lower)}\\b`, "i").test(text)) best = Math.max(best, 0.7);
4332
+ else if (new RegExp(`\\b${escapeRegExp2(lower)}\\b`, "i").test(text)) best = Math.max(best, 0.7);
4135
4333
  else if (chunkSymbols.some((candidate) => candidate.includes(lower) || lower.includes(candidate))) {
4136
4334
  best = Math.max(best, 0.42);
4137
4335
  }
@@ -4167,7 +4365,7 @@ function matchReasons3(parts) {
4167
4365
  if (parts.recency >= 0.75) reasons.push("recent code file");
4168
4366
  return reasons.slice(0, 5);
4169
4367
  }
4170
- function escapeRegExp3(value) {
4368
+ function escapeRegExp2(value) {
4171
4369
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4172
4370
  }
4173
4371
  function escapeLike(value) {
@@ -4402,7 +4600,7 @@ function rowToRanked(row, input) {
4402
4600
  const text = row.sanitized_text ?? "";
4403
4601
  const matchedSymbols = (input.symbols ?? []).filter((symbol) => {
4404
4602
  const lower = symbol.toLowerCase();
4405
- return symbols.some((candidate) => candidate.toLowerCase() === lower) || new RegExp(`\\b${escapeRegExp4(symbol)}\\b`, "i").test(text);
4603
+ return symbols.some((candidate) => candidate.toLowerCase() === lower) || new RegExp(`\\b${escapeRegExp3(symbol)}\\b`, "i").test(text);
4406
4604
  });
4407
4605
  const exactFile = (input.files ?? []).some((file) => row.source_path === file);
4408
4606
  const basenameMatch = (input.files ?? []).some((file) => baseStem(file) === baseStem(row.path));
@@ -4422,7 +4620,7 @@ function rowToRanked(row, input) {
4422
4620
  matchedSymbols
4423
4621
  };
4424
4622
  }
4425
- function escapeRegExp4(value) {
4623
+ function escapeRegExp3(value) {
4426
4624
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4427
4625
  }
4428
4626
  function rankRelevantTests(db, input) {
@@ -8484,6 +8682,13 @@ function dependenciesFor(manifest) {
8484
8682
  ...Object.keys(manifest.peerDependencies ?? {})
8485
8683
  ]);
8486
8684
  }
8685
+ function packageRootForSpecifier(specifier) {
8686
+ const normalized = specifier.trim();
8687
+ if (!normalized) return "";
8688
+ const parts = normalized.split("/");
8689
+ if (normalized.startsWith("@") && parts.length >= 2) return `${parts[0]}/${parts[1]}`;
8690
+ return parts[0] ?? "";
8691
+ }
8487
8692
  function parseJsonArray9(value) {
8488
8693
  try {
8489
8694
  const parsed = JSON.parse(value);
@@ -8518,7 +8723,7 @@ function isApiConsumerText(text) {
8518
8723
  function evidenceJson(evidence) {
8519
8724
  return JSON.stringify(evidence);
8520
8725
  }
8521
- function shouldEmitProgress2(current, total, interval = 100) {
8726
+ function shouldEmitProgress3(current, total, interval = 100) {
8522
8727
  return current === 1 || current === total || current % interval === 0;
8523
8728
  }
8524
8729
  function resolveOptions(baseDirOrOptions) {
@@ -8598,33 +8803,30 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8598
8803
  FROM code_imports ci
8599
8804
  JOIN repositories r ON r.id = ci.repo_id`
8600
8805
  ).all();
8601
- const packageMatchers = [...packageNames.entries()].flatMap(([repo, names]) => names.map((name) => ({ repo, name }))).sort((a, b) => b.name.length - a.name.length);
8602
8806
  imports.forEach((item, index) => {
8603
8807
  const sourceRepo = repoByName.get(item.repo);
8604
8808
  if (!sourceRepo) return;
8605
- for (const candidate of packageMatchers) {
8606
- if (candidate.repo === item.repo) continue;
8607
- const matched = item.specifier === candidate.name || item.specifier.startsWith(`${candidate.name}/`);
8608
- if (!matched) continue;
8809
+ const rootSpecifier = packageRootForSpecifier(item.specifier);
8810
+ const targetRepo = packageToRepo.get(rootSpecifier) ?? packageToRepo.get(item.specifier);
8811
+ if (targetRepo && targetRepo !== item.repo) {
8609
8812
  addEdge({
8610
8813
  org: config.org,
8611
8814
  sourceRepo: item.repo,
8612
8815
  sourcePath: item.source_path,
8613
- targetRepo: candidate.repo,
8816
+ targetRepo,
8614
8817
  targetPath: item.imported_path ?? void 0,
8615
8818
  relationship: "imports",
8616
8819
  evidence: [
8617
8820
  fileEvidence(
8618
8821
  item.repo,
8619
8822
  item.source_path,
8620
- `imports ${sanitizeHistoricalText(candidate.name)}`
8823
+ `imports ${sanitizeHistoricalText(rootSpecifier || item.specifier)}`
8621
8824
  )
8622
8825
  ],
8623
8826
  confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8624
8827
  });
8625
- break;
8626
8828
  }
8627
- if (shouldEmitProgress2(index + 1, imports.length)) {
8829
+ if (shouldEmitProgress3(index + 1, imports.length)) {
8628
8830
  options.onProgress?.({
8629
8831
  stage: "building_import_edges",
8630
8832
  org: config.org,
@@ -8665,7 +8867,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8665
8867
  bucket.push(apiContract);
8666
8868
  contractsByToken.set(sanitizedContract, bucket);
8667
8869
  }
8668
- if (shouldEmitProgress2(index + 1, providerChunks.length)) {
8870
+ if (shouldEmitProgress3(index + 1, providerChunks.length)) {
8669
8871
  options.onProgress?.({
8670
8872
  stage: "extracting_api_contracts",
8671
8873
  org: config.org,
@@ -8725,7 +8927,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8725
8927
  });
8726
8928
  }
8727
8929
  }
8728
- if (shouldEmitProgress2(index + 1, consumerChunks.length)) {
8930
+ if (shouldEmitProgress3(index + 1, consumerChunks.length)) {
8729
8931
  options.onProgress?.({
8730
8932
  stage: "matching_api_consumers",
8731
8933
  org: config.org,
@@ -8757,7 +8959,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8757
8959
  confidence = excluded.confidence,
8758
8960
  created_at = excluded.created_at`
8759
8961
  );
8760
- for (const edge of edges) {
8962
+ for (const [index, edge] of edges.entries()) {
8761
8963
  insertEdge.run(
8762
8964
  `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8763
8965
  edge.org,
@@ -8770,6 +8972,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8770
8972
  edge.confidence,
8771
8973
  now
8772
8974
  );
8975
+ const current = index + 1;
8976
+ if (shouldEmitProgress3(current, edges.length, 500)) {
8977
+ options.onProgress?.({
8978
+ stage: "writing_org_graph",
8979
+ org: config.org,
8980
+ edges: current,
8981
+ apiContracts: apiContracts.length,
8982
+ apiConsumers: apiConsumers.length,
8983
+ current,
8984
+ total: edges.length,
8985
+ kind: "edges"
8986
+ });
8987
+ }
8773
8988
  }
8774
8989
  const insertContract = db.prepare(
8775
8990
  `INSERT INTO org_api_contracts
@@ -8781,7 +8996,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8781
8996
  confidence = excluded.confidence,
8782
8997
  created_at = excluded.created_at`
8783
8998
  );
8784
- for (const contract of apiContracts) {
8999
+ for (const [index, contract] of apiContracts.entries()) {
8785
9000
  insertContract.run(
8786
9001
  `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8787
9002
  config.org,
@@ -8792,6 +9007,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8792
9007
  contract.confidence,
8793
9008
  now
8794
9009
  );
9010
+ const current = index + 1;
9011
+ if (shouldEmitProgress3(current, apiContracts.length, 500)) {
9012
+ options.onProgress?.({
9013
+ stage: "writing_org_graph",
9014
+ org: config.org,
9015
+ edges: edges.length,
9016
+ apiContracts: current,
9017
+ apiConsumers: apiConsumers.length,
9018
+ current,
9019
+ total: apiContracts.length,
9020
+ kind: "contracts"
9021
+ });
9022
+ }
8795
9023
  }
8796
9024
  const insertConsumer = db.prepare(
8797
9025
  `INSERT INTO org_api_consumers
@@ -8803,7 +9031,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8803
9031
  confidence = excluded.confidence,
8804
9032
  created_at = excluded.created_at`
8805
9033
  );
8806
- for (const consumer of apiConsumers) {
9034
+ for (const [index, consumer] of apiConsumers.entries()) {
8807
9035
  insertConsumer.run(
8808
9036
  `oap_${stableId([
8809
9037
  consumer.org,
@@ -8823,6 +9051,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8823
9051
  consumer.confidence,
8824
9052
  now
8825
9053
  );
9054
+ const current = index + 1;
9055
+ if (shouldEmitProgress3(current, apiConsumers.length, 500)) {
9056
+ options.onProgress?.({
9057
+ stage: "writing_org_graph",
9058
+ org: config.org,
9059
+ edges: edges.length,
9060
+ apiContracts: apiContracts.length,
9061
+ apiConsumers: current,
9062
+ current,
9063
+ total: apiConsumers.length,
9064
+ kind: "consumers"
9065
+ });
9066
+ }
8826
9067
  }
8827
9068
  });
8828
9069
  transaction();