@pratik7368patil/anchor-core 0.1.24 → 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.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);
1366
+ }
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];
1347
1375
  }
1348
- function inferTestAwareness(repo, codeFiles, codeChunks) {
1349
- const testFiles = codeFiles.filter((file) => isTestFilePath(file.path));
1350
- const sourceFiles = codeFiles.filter((file) => !isTestFilePath(file.path));
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))
@@ -1535,6 +1694,10 @@ function calculateCoverage(input) {
1535
1694
  }
1536
1695
 
1537
1696
  // src/db/database.ts
1697
+ var CODE_WRITE_PROGRESS_INTERVAL = 500;
1698
+ function shouldEmitCodeWriteProgress(current, total) {
1699
+ return current === 0 || current === 1 || current === total || current % CODE_WRITE_PROGRESS_INTERVAL === 0;
1700
+ }
1538
1701
  function defaultDatabasePath(cwd) {
1539
1702
  return path4.join(cwd, ".anchor", "index.sqlite");
1540
1703
  }
@@ -1902,13 +2065,24 @@ function upsertPullRequest(db, pr, wisdomUnits, regressionEvents = []) {
1902
2065
  regressions: regressionEvents.length
1903
2066
  };
1904
2067
  }
1905
- function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, architecture = { components: [], patterns: [], imports: [] }) {
2068
+ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, architecture = { components: [], patterns: [], imports: [] }, options = {}) {
1906
2069
  initializeSchema(db);
1907
2070
  const repoId = ensureRepository(db, repo);
1908
2071
  const now = (/* @__PURE__ */ new Date()).toISOString();
1909
- const testAwareness = inferTestAwareness(repo, codeFiles, codeChunks);
2072
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Inferring test awareness" });
2073
+ const testAwareness = inferTestAwareness(repo, codeFiles, codeChunks, {
2074
+ onProgress: options.onProgress
2075
+ });
2076
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Writing code index" });
1910
2077
  const transaction = db.transaction(() => {
1911
2078
  const existingChunks = db.prepare("SELECT id FROM code_chunks WHERE repo_id = ?").all(repoId);
2079
+ const existingPatternCount = db.prepare("SELECT COUNT(*) AS count FROM architecture_patterns WHERE repo_id = ?").get(repoId).count;
2080
+ options.onProgress?.({
2081
+ stage: "deleting_existing_code_index",
2082
+ repo,
2083
+ chunks: existingChunks.length,
2084
+ patterns: existingPatternCount
2085
+ });
1912
2086
  const deleteFts = db.prepare("DELETE FROM code_chunks_fts WHERE chunkId = ?");
1913
2087
  for (const row of existingChunks) deleteFts.run(row.id);
1914
2088
  db.prepare("DELETE FROM code_chunks WHERE repo_id = ?").run(repoId);
@@ -1921,7 +2095,13 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1921
2095
  (repo_id, path, language, size_bytes, content_hash, updated_at)
1922
2096
  VALUES (?, ?, ?, ?, ?, ?)`
1923
2097
  );
1924
- for (const file of codeFiles) {
2098
+ options.onProgress?.({
2099
+ stage: "writing_code_files",
2100
+ repo,
2101
+ current: 0,
2102
+ total: codeFiles.length
2103
+ });
2104
+ for (const [index, file] of codeFiles.entries()) {
1925
2105
  insertFile.run(
1926
2106
  repoId,
1927
2107
  file.path,
@@ -1930,6 +2110,16 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1930
2110
  file.contentHash,
1931
2111
  file.updatedAt
1932
2112
  );
2113
+ const current = index + 1;
2114
+ if (shouldEmitCodeWriteProgress(current, codeFiles.length)) {
2115
+ options.onProgress?.({
2116
+ stage: "writing_code_files",
2117
+ repo,
2118
+ current,
2119
+ total: codeFiles.length,
2120
+ filePath: file.path
2121
+ });
2122
+ }
1933
2123
  }
1934
2124
  const fileRows = db.prepare("SELECT id, path FROM code_files WHERE repo_id = ?").all(repoId);
1935
2125
  const fileIds = new Map(fileRows.map((row) => [row.path, row.id]));
@@ -1944,7 +2134,15 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1944
2134
  (chunkId, sanitizedText, filePath, symbols, language)
1945
2135
  VALUES (?, ?, ?, ?, ?)`
1946
2136
  );
1947
- for (const chunk of codeChunks) {
2137
+ options.onProgress?.({
2138
+ stage: "writing_code_chunks",
2139
+ repo,
2140
+ current: 0,
2141
+ total: codeChunks.length,
2142
+ chunks: 0
2143
+ });
2144
+ let writtenChunks = 0;
2145
+ for (const [index, chunk] of codeChunks.entries()) {
1948
2146
  const fileId = fileIds.get(chunk.filePath);
1949
2147
  if (!fileId) continue;
1950
2148
  insertChunk.run(
@@ -1968,10 +2166,23 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1968
2166
  chunk.symbols.join(" "),
1969
2167
  chunk.language ?? ""
1970
2168
  );
2169
+ writtenChunks += 1;
2170
+ const current = index + 1;
2171
+ if (shouldEmitCodeWriteProgress(current, codeChunks.length)) {
2172
+ options.onProgress?.({
2173
+ stage: "writing_code_chunks",
2174
+ repo,
2175
+ current,
2176
+ total: codeChunks.length,
2177
+ filePath: chunk.filePath,
2178
+ chunks: writtenChunks
2179
+ });
2180
+ }
1971
2181
  }
1972
- insertTestAwareness(db, repoId, testAwareness.testFiles, testAwareness.testLinks);
1973
- insertArchitectureData(db, repoId, architecture);
1974
- insertArchitectureMapEdges(db, repoId, repo, architecture, testAwareness.testLinks);
2182
+ insertTestAwareness(db, repoId, repo, testAwareness.testFiles, testAwareness.testLinks, options);
2183
+ insertArchitectureData(db, repoId, repo, architecture, options);
2184
+ insertArchitectureMapEdges(db, repoId, repo, architecture, testAwareness.testLinks, options);
2185
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Updating index state" });
1975
2186
  db.prepare(
1976
2187
  `INSERT INTO code_index_state (repo, last_indexed_at, indexed_files, code_chunks, skipped_files)
1977
2188
  VALUES (?, ?, ?, ?, ?)
@@ -2019,13 +2230,20 @@ function deleteExistingArchitectureData(db, repoId) {
2019
2230
  db.prepare("DELETE FROM code_imports WHERE repo_id = ?").run(repoId);
2020
2231
  db.prepare("DELETE FROM architecture_map_edges WHERE repo_id = ?").run(repoId);
2021
2232
  }
2022
- function insertArchitectureData(db, repoId, architecture) {
2233
+ function insertArchitectureData(db, repoId, repo, architecture, options = {}) {
2023
2234
  const insertImport = db.prepare(
2024
2235
  `INSERT INTO code_imports
2025
2236
  (repo_id, source_path, specifier, imported_path, imported_symbols_json, kind)
2026
2237
  VALUES (?, ?, ?, ?, ?, ?)`
2027
2238
  );
2028
- for (const item of architecture.imports) {
2239
+ options.onProgress?.({
2240
+ stage: "writing_architecture_data",
2241
+ repo,
2242
+ current: 0,
2243
+ total: architecture.imports.length,
2244
+ kind: "imports"
2245
+ });
2246
+ for (const [index, item] of architecture.imports.entries()) {
2029
2247
  insertImport.run(
2030
2248
  repoId,
2031
2249
  item.sourcePath,
@@ -2034,6 +2252,16 @@ function insertArchitectureData(db, repoId, architecture) {
2034
2252
  JSON.stringify(item.importedSymbols),
2035
2253
  item.kind
2036
2254
  );
2255
+ const current = index + 1;
2256
+ if (shouldEmitCodeWriteProgress(current, architecture.imports.length)) {
2257
+ options.onProgress?.({
2258
+ stage: "writing_architecture_data",
2259
+ repo,
2260
+ current,
2261
+ total: architecture.imports.length,
2262
+ kind: "imports"
2263
+ });
2264
+ }
2037
2265
  }
2038
2266
  const insertComponent = db.prepare(
2039
2267
  `INSERT INTO architecture_components
@@ -2041,7 +2269,14 @@ function insertArchitectureData(db, repoId, architecture) {
2041
2269
  confidence, updated_at)
2042
2270
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2043
2271
  );
2044
- for (const component of architecture.components) {
2272
+ options.onProgress?.({
2273
+ stage: "writing_architecture_data",
2274
+ repo,
2275
+ current: 0,
2276
+ total: architecture.components.length,
2277
+ kind: "components"
2278
+ });
2279
+ for (const [index, component] of architecture.components.entries()) {
2045
2280
  insertComponent.run(
2046
2281
  repoId,
2047
2282
  component.path,
@@ -2054,6 +2289,16 @@ function insertArchitectureData(db, repoId, architecture) {
2054
2289
  component.confidence,
2055
2290
  component.updatedAt
2056
2291
  );
2292
+ const current = index + 1;
2293
+ if (shouldEmitCodeWriteProgress(current, architecture.components.length)) {
2294
+ options.onProgress?.({
2295
+ stage: "writing_architecture_data",
2296
+ repo,
2297
+ current,
2298
+ total: architecture.components.length,
2299
+ kind: "components"
2300
+ });
2301
+ }
2057
2302
  }
2058
2303
  const insertPattern = db.prepare(
2059
2304
  `INSERT INTO architecture_patterns
@@ -2065,7 +2310,14 @@ function insertArchitectureData(db, repoId, architecture) {
2065
2310
  `INSERT INTO architecture_patterns_fts (patternId, summary, area, sourceFiles, symbols)
2066
2311
  VALUES (?, ?, ?, ?, ?)`
2067
2312
  );
2068
- for (const pattern of architecture.patterns) {
2313
+ options.onProgress?.({
2314
+ stage: "writing_architecture_data",
2315
+ repo,
2316
+ current: 0,
2317
+ total: architecture.patterns.length,
2318
+ kind: "patterns"
2319
+ });
2320
+ for (const [index, pattern] of architecture.patterns.entries()) {
2069
2321
  insertPattern.run(
2070
2322
  pattern.id,
2071
2323
  repoId,
@@ -2086,9 +2338,19 @@ function insertArchitectureData(db, repoId, architecture) {
2086
2338
  pattern.sourceFiles.join(" "),
2087
2339
  pattern.symbols.join(" ")
2088
2340
  );
2341
+ const current = index + 1;
2342
+ if (shouldEmitCodeWriteProgress(current, architecture.patterns.length)) {
2343
+ options.onProgress?.({
2344
+ stage: "writing_architecture_data",
2345
+ repo,
2346
+ current,
2347
+ total: architecture.patterns.length,
2348
+ kind: "patterns"
2349
+ });
2350
+ }
2089
2351
  }
2090
2352
  }
2091
- function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks) {
2353
+ function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks, options = {}) {
2092
2354
  const now = (/* @__PURE__ */ new Date()).toISOString();
2093
2355
  const insert = db.prepare(
2094
2356
  `INSERT INTO architecture_map_edges
@@ -2103,11 +2365,40 @@ function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks) {
2103
2365
  seen.add(id);
2104
2366
  insert.run(id, repoId, repo, sourcePath, targetPath, relationship, weight, now);
2105
2367
  };
2368
+ const total = architecture.imports.length + testLinks.length;
2369
+ let current = 0;
2370
+ options.onProgress?.({
2371
+ stage: "writing_architecture_map_edges",
2372
+ repo,
2373
+ current,
2374
+ total,
2375
+ edges: 0
2376
+ });
2106
2377
  for (const item of architecture.imports) {
2107
2378
  if (item.importedPath) addEdge(item.sourcePath, item.importedPath, "imports", 0.9);
2379
+ current += 1;
2380
+ if (shouldEmitCodeWriteProgress(current, total)) {
2381
+ options.onProgress?.({
2382
+ stage: "writing_architecture_map_edges",
2383
+ repo,
2384
+ current,
2385
+ total,
2386
+ edges: seen.size
2387
+ });
2388
+ }
2108
2389
  }
2109
2390
  for (const link of testLinks) {
2110
2391
  addEdge(link.sourcePath, link.testPath, "tested_by", link.strength);
2392
+ current += 1;
2393
+ if (shouldEmitCodeWriteProgress(current, total)) {
2394
+ options.onProgress?.({
2395
+ stage: "writing_architecture_map_edges",
2396
+ repo,
2397
+ current,
2398
+ total,
2399
+ edges: seen.size
2400
+ });
2401
+ }
2111
2402
  }
2112
2403
  }
2113
2404
  function insertPrCochangeTestLinks(db, repoId, filePaths) {
@@ -2123,13 +2414,20 @@ function insertPrCochangeTestLinks(db, repoId, filePaths) {
2123
2414
  for (const testPath of testPaths) insert.run(repoId, sourcePath, testPath);
2124
2415
  }
2125
2416
  }
2126
- function insertTestAwareness(db, repoId, testFiles, testLinks) {
2417
+ function insertTestAwareness(db, repoId, repo, testFiles, testLinks, options = {}) {
2127
2418
  const insertTestFile = db.prepare(
2128
2419
  `INSERT INTO test_files
2129
2420
  (repo_id, path, language, size_bytes, content_hash, updated_at)
2130
2421
  VALUES (?, ?, ?, ?, ?, ?)`
2131
2422
  );
2132
- for (const file of testFiles) {
2423
+ options.onProgress?.({
2424
+ stage: "writing_test_awareness",
2425
+ repo,
2426
+ current: 0,
2427
+ total: testFiles.length,
2428
+ kind: "test_files"
2429
+ });
2430
+ for (const [index, file] of testFiles.entries()) {
2133
2431
  insertTestFile.run(
2134
2432
  repoId,
2135
2433
  file.path,
@@ -2138,13 +2436,40 @@ function insertTestAwareness(db, repoId, testFiles, testLinks) {
2138
2436
  file.contentHash,
2139
2437
  file.updatedAt
2140
2438
  );
2439
+ const current = index + 1;
2440
+ if (shouldEmitCodeWriteProgress(current, testFiles.length)) {
2441
+ options.onProgress?.({
2442
+ stage: "writing_test_awareness",
2443
+ repo,
2444
+ current,
2445
+ total: testFiles.length,
2446
+ kind: "test_files"
2447
+ });
2448
+ }
2141
2449
  }
2142
2450
  const insertTestLink = db.prepare(
2143
2451
  `INSERT INTO test_links (repo_id, source_path, test_path, reason, strength)
2144
2452
  VALUES (?, ?, ?, ?, ?)`
2145
2453
  );
2146
- for (const link of testLinks) {
2454
+ options.onProgress?.({
2455
+ stage: "writing_test_awareness",
2456
+ repo,
2457
+ current: 0,
2458
+ total: testLinks.length,
2459
+ kind: "test_links"
2460
+ });
2461
+ for (const [index, link] of testLinks.entries()) {
2147
2462
  insertTestLink.run(repoId, link.sourcePath, link.testPath, link.reason, link.strength);
2463
+ const current = index + 1;
2464
+ if (shouldEmitCodeWriteProgress(current, testLinks.length)) {
2465
+ options.onProgress?.({
2466
+ stage: "writing_test_awareness",
2467
+ repo,
2468
+ current,
2469
+ total: testLinks.length,
2470
+ kind: "test_links"
2471
+ });
2472
+ }
2148
2473
  }
2149
2474
  }
2150
2475
  function recordIndexRun(db, run) {
@@ -2471,6 +2796,10 @@ function chunkCodeFile(file, options = {}) {
2471
2796
  import crypto2 from "crypto";
2472
2797
  import path6 from "path";
2473
2798
  var KNOWN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
2799
+ var ARCHITECTURE_PROGRESS_INTERVAL = 250;
2800
+ function shouldEmitProgress2(current, total) {
2801
+ return current === 0 || current === 1 || current === total || current % ARCHITECTURE_PROGRESS_INTERVAL === 0;
2802
+ }
2474
2803
  function classifyArchitectureArea(filePath, language, content = "") {
2475
2804
  const normalized = filePath.replace(/\\/g, "/").toLowerCase();
2476
2805
  const basename = path6.basename(normalized);
@@ -2579,15 +2908,47 @@ function extractCodeImports(sourcePath, content, codePaths, repo = "") {
2579
2908
  return true;
2580
2909
  });
2581
2910
  }
2582
- 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) {
2583
2933
  if (isTestFilePath(filePath)) return [];
2584
2934
  const parsed = path6.posix.parse(filePath);
2585
2935
  const basename = parsed.name.replace(/\.(test|spec)$/i, "");
2586
- return allPaths.filter((candidate) => isTestFilePath(candidate)).filter((candidate) => {
2587
- const candidateParsed = path6.posix.parse(candidate);
2588
- const candidateBase = candidateParsed.name.replace(/\.(test|spec)$/i, "");
2589
- return candidateBase === basename || candidate.startsWith(`${parsed.dir}/`) || candidate.includes(`/${basename}.`);
2590
- }).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);
2591
2952
  }
2592
2953
  function directoryLabel(filePath) {
2593
2954
  const directory = path6.posix.dirname(filePath.replace(/\\/g, "/"));
@@ -2618,28 +2979,60 @@ function createPattern(input) {
2618
2979
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2619
2980
  };
2620
2981
  }
2621
- function buildArchitectureIndex(repo, files, chunks) {
2982
+ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2622
2983
  const allPaths = files.map((file) => file.path);
2623
2984
  const codePaths = new Set(allPaths);
2624
- const symbolsByPath = /* @__PURE__ */ new Map();
2985
+ const relatedTestIndex = buildRelatedTestIndex(allPaths);
2986
+ const symbolSetsByPath = /* @__PURE__ */ new Map();
2625
2987
  for (const chunk of chunks) {
2626
- const existing = symbolsByPath.get(chunk.filePath) ?? [];
2627
- 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);
2994
+ }
2995
+ const imports = [];
2996
+ options.onProgress?.({
2997
+ stage: "building_architecture_imports",
2998
+ repo,
2999
+ current: 0,
3000
+ total: files.length,
3001
+ imports: 0
3002
+ });
3003
+ for (const [index, file] of files.entries()) {
3004
+ imports.push(...extractCodeImports(file.path, file.content, codePaths, repo));
3005
+ const current = index + 1;
3006
+ if (shouldEmitProgress2(current, files.length)) {
3007
+ options.onProgress?.({
3008
+ stage: "building_architecture_imports",
3009
+ repo,
3010
+ current,
3011
+ total: files.length,
3012
+ filePath: file.path,
3013
+ imports: imports.length
3014
+ });
3015
+ }
2628
3016
  }
2629
- const imports = files.flatMap(
2630
- (file) => extractCodeImports(file.path, file.content, codePaths, repo)
2631
- );
2632
3017
  const importsByPath = /* @__PURE__ */ new Map();
2633
3018
  for (const item of imports) {
2634
3019
  const existing = importsByPath.get(item.sourcePath) ?? [];
2635
3020
  existing.push(item);
2636
3021
  importsByPath.set(item.sourcePath, existing);
2637
3022
  }
2638
- const components = files.map((file) => {
3023
+ const components = [];
3024
+ options.onProgress?.({
3025
+ stage: "building_architecture_components",
3026
+ repo,
3027
+ current: 0,
3028
+ total: files.length,
3029
+ components: 0
3030
+ });
3031
+ for (const [index, file] of files.entries()) {
2639
3032
  const area = classifyArchitectureArea(file.path, file.language, file.content);
2640
3033
  const fileImports = importsByPath.get(file.path) ?? [];
2641
- const symbols = symbolsByPath.get(file.path) ?? [];
2642
- return {
3034
+ const symbols = [...symbolSetsByPath.get(file.path) ?? []];
3035
+ components.push({
2643
3036
  repo,
2644
3037
  path: file.path,
2645
3038
  area,
@@ -2649,19 +3042,52 @@ function buildArchitectureIndex(repo, files, chunks) {
2649
3042
  imports: uniqueStrings(
2650
3043
  fileImports.map((item) => item.importedPath ?? item.specifier).filter(Boolean)
2651
3044
  ).slice(0, 20),
2652
- relatedTests: relatedTestsFor(file.path, allPaths),
3045
+ relatedTests: relatedTestsFor(file.path, relatedTestIndex),
2653
3046
  confidence: area === "unknown" ? 0.45 : 0.82,
2654
3047
  updatedAt: file.updatedAt
2655
- };
2656
- });
3048
+ });
3049
+ const current = index + 1;
3050
+ if (shouldEmitProgress2(current, files.length)) {
3051
+ options.onProgress?.({
3052
+ stage: "building_architecture_components",
3053
+ repo,
3054
+ current,
3055
+ total: files.length,
3056
+ filePath: file.path,
3057
+ components: components.length
3058
+ });
3059
+ }
3060
+ }
2657
3061
  const componentByPath = new Map(components.map((component) => [component.path, component]));
2658
- const patterns = [];
2659
3062
  const componentsByArea = /* @__PURE__ */ new Map();
2660
3063
  for (const component of components) {
2661
3064
  const existing = componentsByArea.get(component.area) ?? [];
2662
3065
  existing.push(component);
2663
3066
  componentsByArea.set(component.area, existing);
2664
3067
  }
3068
+ const importDirectionCounts = /* @__PURE__ */ new Map();
3069
+ for (const item of imports) {
3070
+ if (!item.importedPath) continue;
3071
+ const source = componentByPath.get(item.sourcePath);
3072
+ const target = componentByPath.get(item.importedPath);
3073
+ if (!source || !target || source.area === target.area) continue;
3074
+ const key = `${source.area}->${target.area}`;
3075
+ const existing = importDirectionCounts.get(key) ?? { count: 0, files: [], symbols: [] };
3076
+ existing.count += 1;
3077
+ existing.files.push(source.path, target.path);
3078
+ existing.symbols.push(...item.importedSymbols);
3079
+ importDirectionCounts.set(key, existing);
3080
+ }
3081
+ const patterns = [];
3082
+ const patternTotal = componentsByArea.size + importDirectionCounts.size;
3083
+ let patternProgress = 0;
3084
+ options.onProgress?.({
3085
+ stage: "building_architecture_patterns",
3086
+ repo,
3087
+ current: 0,
3088
+ total: patternTotal,
3089
+ patterns: 0
3090
+ });
2665
3091
  for (const [area, areaComponents] of componentsByArea.entries()) {
2666
3092
  const filesForArea = areaComponents.map((component) => component.path);
2667
3093
  const directories = topDirectories(filesForArea);
@@ -2677,19 +3103,17 @@ function buildArchitectureIndex(repo, files, chunks) {
2677
3103
  confidence: 0.55 + Math.min(0.3, filesForArea.length * 0.04)
2678
3104
  })
2679
3105
  );
2680
- }
2681
- const importDirectionCounts = /* @__PURE__ */ new Map();
2682
- for (const item of imports) {
2683
- if (!item.importedPath) continue;
2684
- const source = componentByPath.get(item.sourcePath);
2685
- const target = componentByPath.get(item.importedPath);
2686
- if (!source || !target || source.area === target.area) continue;
2687
- const key = `${source.area}->${target.area}`;
2688
- const existing = importDirectionCounts.get(key) ?? { count: 0, files: [], symbols: [] };
2689
- existing.count += 1;
2690
- existing.files.push(source.path, target.path);
2691
- existing.symbols.push(...item.importedSymbols);
2692
- importDirectionCounts.set(key, existing);
3106
+ patternProgress += 1;
3107
+ if (shouldEmitProgress2(patternProgress, patternTotal)) {
3108
+ options.onProgress?.({
3109
+ stage: "building_architecture_patterns",
3110
+ repo,
3111
+ current: patternProgress,
3112
+ total: patternTotal,
3113
+ area,
3114
+ patterns: patterns.length
3115
+ });
3116
+ }
2693
3117
  }
2694
3118
  for (const [key, value] of importDirectionCounts.entries()) {
2695
3119
  const [sourceArea, targetArea] = key.split("->");
@@ -2704,6 +3128,17 @@ function buildArchitectureIndex(repo, files, chunks) {
2704
3128
  confidence: 0.62 + Math.min(0.25, value.count * 0.05)
2705
3129
  })
2706
3130
  );
3131
+ patternProgress += 1;
3132
+ if (shouldEmitProgress2(patternProgress, patternTotal)) {
3133
+ options.onProgress?.({
3134
+ stage: "building_architecture_patterns",
3135
+ repo,
3136
+ current: patternProgress,
3137
+ total: patternTotal,
3138
+ area: sourceArea,
3139
+ patterns: patterns.length
3140
+ });
3141
+ }
2707
3142
  }
2708
3143
  const testedComponents = components.filter((component) => component.relatedTests.length > 0);
2709
3144
  if (testedComponents.length > 0) {
@@ -3033,8 +3468,24 @@ function detectTestCommands(db, cwd, files = []) {
3033
3468
  return true;
3034
3469
  });
3035
3470
  }
3036
- function refreshTestCommands(db, cwd, repo, files = []) {
3471
+ function refreshTestCommands(db, cwd, repo, files = [], options = {}) {
3472
+ options.onProgress?.({
3473
+ stage: "refreshing_test_commands",
3474
+ repo,
3475
+ phase: "detecting",
3476
+ current: 0,
3477
+ total: files.length,
3478
+ commands: 0
3479
+ });
3037
3480
  const commands = detectTestCommands(db, cwd, files);
3481
+ options.onProgress?.({
3482
+ stage: "refreshing_test_commands",
3483
+ repo,
3484
+ phase: "writing",
3485
+ current: 0,
3486
+ total: commands.length,
3487
+ commands: commands.length
3488
+ });
3038
3489
  const now = (/* @__PURE__ */ new Date()).toISOString();
3039
3490
  const transaction = db.transaction(() => {
3040
3491
  db.prepare("DELETE FROM test_commands WHERE repo = ?").run(repo);
@@ -3042,7 +3493,7 @@ function refreshTestCommands(db, cwd, repo, files = []) {
3042
3493
  `INSERT INTO test_commands (id, repo, file_path, command, reason, confidence, created_at)
3043
3494
  VALUES (?, ?, ?, ?, ?, ?, ?)`
3044
3495
  );
3045
- for (const command of commands) {
3496
+ for (const [index, command] of commands.entries()) {
3046
3497
  insert.run(
3047
3498
  commandId(repo, command),
3048
3499
  repo,
@@ -3052,6 +3503,17 @@ function refreshTestCommands(db, cwd, repo, files = []) {
3052
3503
  command.confidence,
3053
3504
  now
3054
3505
  );
3506
+ const current = index + 1;
3507
+ if (current === 1 || current === commands.length || current % 250 === 0) {
3508
+ options.onProgress?.({
3509
+ stage: "refreshing_test_commands",
3510
+ repo,
3511
+ phase: "writing",
3512
+ current,
3513
+ total: commands.length,
3514
+ commands: commands.length
3515
+ });
3516
+ }
3055
3517
  }
3056
3518
  });
3057
3519
  transaction();
@@ -3090,7 +3552,9 @@ function indexCodebase(db, options) {
3090
3552
  chunks: fileChunks.length
3091
3553
  });
3092
3554
  }
3093
- const architecture = buildArchitectureIndex(options.repo, discovery.files, chunks);
3555
+ const architecture = buildArchitectureIndex(options.repo, discovery.files, chunks, {
3556
+ onProgress: options.onProgress
3557
+ });
3094
3558
  options.onProgress?.({
3095
3559
  stage: "indexed_architecture",
3096
3560
  repo: options.repo,
@@ -3105,9 +3569,10 @@ function indexCodebase(db, options) {
3105
3569
  chunks,
3106
3570
  discovery.skippedFiles,
3107
3571
  options.cwd,
3108
- architecture
3572
+ architecture,
3573
+ { onProgress: options.onProgress }
3109
3574
  );
3110
- refreshTestCommands(db, options.cwd, options.repo);
3575
+ refreshTestCommands(db, options.cwd, options.repo, [], { onProgress: options.onProgress });
3111
3576
  options.onProgress?.({
3112
3577
  stage: "completed_code_index",
3113
3578
  repo: options.repo,
@@ -3620,7 +4085,7 @@ function symbolMatch2(unit, querySymbols) {
3620
4085
  const lower = symbol.toLowerCase();
3621
4086
  if (unitSymbols.includes(lower)) best = Math.max(best, 1);
3622
4087
  else if (text.includes(`\`${lower}\``)) best = Math.max(best, 1);
3623
- else if (new RegExp(`\\b${escapeRegExp2(lower)}\\b`, "i").test(text))
4088
+ else if (new RegExp(`\\b${escapeRegExp(lower)}\\b`, "i").test(text))
3624
4089
  best = Math.max(best, 0.66);
3625
4090
  else if (unitSymbols.some((candidate) => candidate.includes(lower) || lower.includes(candidate))) {
3626
4091
  best = Math.max(best, 0.35);
@@ -3701,7 +4166,7 @@ function scoreUnit(unit, input, duplicateCount, repeatedEvidenceCount, freshness
3701
4166
  rankSignals: parts
3702
4167
  };
3703
4168
  }
3704
- function escapeRegExp2(value) {
4169
+ function escapeRegExp(value) {
3705
4170
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3706
4171
  }
3707
4172
  function loadCandidates(db, input) {
@@ -3864,7 +4329,7 @@ function symbolMatch3(chunk, querySymbols) {
3864
4329
  for (const symbol of querySymbols) {
3865
4330
  const lower = symbol.toLowerCase();
3866
4331
  if (chunkSymbols.includes(lower)) best = Math.max(best, 1);
3867
- 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);
3868
4333
  else if (chunkSymbols.some((candidate) => candidate.includes(lower) || lower.includes(candidate))) {
3869
4334
  best = Math.max(best, 0.42);
3870
4335
  }
@@ -3900,7 +4365,7 @@ function matchReasons3(parts) {
3900
4365
  if (parts.recency >= 0.75) reasons.push("recent code file");
3901
4366
  return reasons.slice(0, 5);
3902
4367
  }
3903
- function escapeRegExp3(value) {
4368
+ function escapeRegExp2(value) {
3904
4369
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3905
4370
  }
3906
4371
  function escapeLike(value) {
@@ -4135,7 +4600,7 @@ function rowToRanked(row, input) {
4135
4600
  const text = row.sanitized_text ?? "";
4136
4601
  const matchedSymbols = (input.symbols ?? []).filter((symbol) => {
4137
4602
  const lower = symbol.toLowerCase();
4138
- 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);
4139
4604
  });
4140
4605
  const exactFile = (input.files ?? []).some((file) => row.source_path === file);
4141
4606
  const basenameMatch = (input.files ?? []).some((file) => baseStem(file) === baseStem(row.path));
@@ -4155,7 +4620,7 @@ function rowToRanked(row, input) {
4155
4620
  matchedSymbols
4156
4621
  };
4157
4622
  }
4158
- function escapeRegExp4(value) {
4623
+ function escapeRegExp3(value) {
4159
4624
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4160
4625
  }
4161
4626
  function rankRelevantTests(db, input) {
@@ -7929,10 +8394,67 @@ function parseHeartbeat(value) {
7929
8394
  repoIndex: typeof candidate.repoIndex === "number" ? candidate.repoIndex : void 0,
7930
8395
  repoTotal: typeof candidate.repoTotal === "number" ? candidate.repoTotal : void 0,
7931
8396
  phase: candidate.phase,
8397
+ timeline: parseTimeline(candidate.timeline),
7932
8398
  startedAt: candidate.startedAt,
7933
8399
  updatedAt: candidate.updatedAt
7934
8400
  };
7935
8401
  }
8402
+ function parseTimelineStatus(value) {
8403
+ if (value === "active" || value === "done" || value === "skipped" || value === "warn" || value === "fail" || value === "wait") {
8404
+ return value;
8405
+ }
8406
+ return void 0;
8407
+ }
8408
+ function parseTimelineNumber(value) {
8409
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8410
+ }
8411
+ function parseTimelineStep(value) {
8412
+ if (!value || typeof value !== "object") return void 0;
8413
+ const candidate = value;
8414
+ const status = parseTimelineStatus(candidate.status);
8415
+ if (typeof candidate.id !== "string" || typeof candidate.label !== "string" || !status || typeof candidate.startedAt !== "string" || typeof candidate.updatedAt !== "string") {
8416
+ return void 0;
8417
+ }
8418
+ return {
8419
+ id: candidate.id,
8420
+ label: candidate.label,
8421
+ status,
8422
+ startedAt: candidate.startedAt,
8423
+ updatedAt: candidate.updatedAt,
8424
+ completedAt: typeof candidate.completedAt === "string" ? candidate.completedAt : void 0,
8425
+ durationMs: parseTimelineNumber(candidate.durationMs),
8426
+ current: parseTimelineNumber(candidate.current),
8427
+ total: parseTimelineNumber(candidate.total),
8428
+ detail: typeof candidate.detail === "string" ? candidate.detail : void 0
8429
+ };
8430
+ }
8431
+ function parseTimelineRepoSummary(value) {
8432
+ if (!value || typeof value !== "object") return void 0;
8433
+ const candidate = value;
8434
+ const status = parseTimelineStatus(candidate.status);
8435
+ if (typeof candidate.repo !== "string" || !status) return void 0;
8436
+ return {
8437
+ repo: candidate.repo,
8438
+ status,
8439
+ durationMs: parseTimelineNumber(candidate.durationMs) ?? 0,
8440
+ detail: typeof candidate.detail === "string" ? candidate.detail : void 0
8441
+ };
8442
+ }
8443
+ function parseTimeline(value) {
8444
+ if (!value || typeof value !== "object") return void 0;
8445
+ const candidate = value;
8446
+ if (!Array.isArray(candidate.steps) || !Array.isArray(candidate.recentRepos)) return void 0;
8447
+ const steps = candidate.steps.map(parseTimelineStep).filter((step) => step !== void 0);
8448
+ const recentRepos = candidate.recentRepos.map(parseTimelineRepoSummary).filter((repo) => repo !== void 0);
8449
+ return {
8450
+ repo: typeof candidate.repo === "string" ? candidate.repo : void 0,
8451
+ repoIndex: parseTimelineNumber(candidate.repoIndex),
8452
+ repoTotal: parseTimelineNumber(candidate.repoTotal),
8453
+ activeStepId: typeof candidate.activeStepId === "string" ? candidate.activeStepId : void 0,
8454
+ steps,
8455
+ recentRepos
8456
+ };
8457
+ }
7936
8458
  function writeOrgHeartbeat(heartbeat, baseDir) {
7937
8459
  atomicWriteJson2(orgHeartbeatPath(heartbeat.org, baseDir), heartbeat);
7938
8460
  }
@@ -8160,6 +8682,13 @@ function dependenciesFor(manifest) {
8160
8682
  ...Object.keys(manifest.peerDependencies ?? {})
8161
8683
  ]);
8162
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
+ }
8163
8692
  function parseJsonArray9(value) {
8164
8693
  try {
8165
8694
  const parsed = JSON.parse(value);
@@ -8194,7 +8723,7 @@ function isApiConsumerText(text) {
8194
8723
  function evidenceJson(evidence) {
8195
8724
  return JSON.stringify(evidence);
8196
8725
  }
8197
- function shouldEmitProgress(current, total, interval = 100) {
8726
+ function shouldEmitProgress3(current, total, interval = 100) {
8198
8727
  return current === 1 || current === total || current % interval === 0;
8199
8728
  }
8200
8729
  function resolveOptions(baseDirOrOptions) {
@@ -8274,33 +8803,30 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8274
8803
  FROM code_imports ci
8275
8804
  JOIN repositories r ON r.id = ci.repo_id`
8276
8805
  ).all();
8277
- const packageMatchers = [...packageNames.entries()].flatMap(([repo, names]) => names.map((name) => ({ repo, name }))).sort((a, b) => b.name.length - a.name.length);
8278
8806
  imports.forEach((item, index) => {
8279
8807
  const sourceRepo = repoByName.get(item.repo);
8280
8808
  if (!sourceRepo) return;
8281
- for (const candidate of packageMatchers) {
8282
- if (candidate.repo === item.repo) continue;
8283
- const matched = item.specifier === candidate.name || item.specifier.startsWith(`${candidate.name}/`);
8284
- 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) {
8285
8812
  addEdge({
8286
8813
  org: config.org,
8287
8814
  sourceRepo: item.repo,
8288
8815
  sourcePath: item.source_path,
8289
- targetRepo: candidate.repo,
8816
+ targetRepo,
8290
8817
  targetPath: item.imported_path ?? void 0,
8291
8818
  relationship: "imports",
8292
8819
  evidence: [
8293
8820
  fileEvidence(
8294
8821
  item.repo,
8295
8822
  item.source_path,
8296
- `imports ${sanitizeHistoricalText(candidate.name)}`
8823
+ `imports ${sanitizeHistoricalText(rootSpecifier || item.specifier)}`
8297
8824
  )
8298
8825
  ],
8299
8826
  confidence: parseJsonArray9(item.imported_symbols_json).length > 0 ? 0.88 : 0.76
8300
8827
  });
8301
- break;
8302
8828
  }
8303
- if (shouldEmitProgress(index + 1, imports.length)) {
8829
+ if (shouldEmitProgress3(index + 1, imports.length)) {
8304
8830
  options.onProgress?.({
8305
8831
  stage: "building_import_edges",
8306
8832
  org: config.org,
@@ -8341,7 +8867,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8341
8867
  bucket.push(apiContract);
8342
8868
  contractsByToken.set(sanitizedContract, bucket);
8343
8869
  }
8344
- if (shouldEmitProgress(index + 1, providerChunks.length)) {
8870
+ if (shouldEmitProgress3(index + 1, providerChunks.length)) {
8345
8871
  options.onProgress?.({
8346
8872
  stage: "extracting_api_contracts",
8347
8873
  org: config.org,
@@ -8401,7 +8927,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8401
8927
  });
8402
8928
  }
8403
8929
  }
8404
- if (shouldEmitProgress(index + 1, consumerChunks.length)) {
8930
+ if (shouldEmitProgress3(index + 1, consumerChunks.length)) {
8405
8931
  options.onProgress?.({
8406
8932
  stage: "matching_api_consumers",
8407
8933
  org: config.org,
@@ -8433,7 +8959,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8433
8959
  confidence = excluded.confidence,
8434
8960
  created_at = excluded.created_at`
8435
8961
  );
8436
- for (const edge of edges) {
8962
+ for (const [index, edge] of edges.entries()) {
8437
8963
  insertEdge.run(
8438
8964
  `oge_${stableId([edge.org, edge.sourceRepo, edge.sourcePath, edge.targetRepo, edge.targetPath ?? "", edge.relationship])}`,
8439
8965
  edge.org,
@@ -8446,6 +8972,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8446
8972
  edge.confidence,
8447
8973
  now
8448
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
+ }
8449
8988
  }
8450
8989
  const insertContract = db.prepare(
8451
8990
  `INSERT INTO org_api_contracts
@@ -8457,7 +8996,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8457
8996
  confidence = excluded.confidence,
8458
8997
  created_at = excluded.created_at`
8459
8998
  );
8460
- for (const contract of apiContracts) {
8999
+ for (const [index, contract] of apiContracts.entries()) {
8461
9000
  insertContract.run(
8462
9001
  `oac_${stableId([config.org, contract.repo, contract.filePath, contract.contract])}`,
8463
9002
  config.org,
@@ -8468,6 +9007,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8468
9007
  contract.confidence,
8469
9008
  now
8470
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
+ }
8471
9023
  }
8472
9024
  const insertConsumer = db.prepare(
8473
9025
  `INSERT INTO org_api_consumers
@@ -8479,7 +9031,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8479
9031
  confidence = excluded.confidence,
8480
9032
  created_at = excluded.created_at`
8481
9033
  );
8482
- for (const consumer of apiConsumers) {
9034
+ for (const [index, consumer] of apiConsumers.entries()) {
8483
9035
  insertConsumer.run(
8484
9036
  `oap_${stableId([
8485
9037
  consumer.org,
@@ -8499,6 +9051,19 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8499
9051
  consumer.confidence,
8500
9052
  now
8501
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
+ }
8502
9067
  }
8503
9068
  });
8504
9069
  transaction();