@pratik7368patil/anchor-core 0.1.23 → 0.1.25

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
@@ -1535,16 +1535,27 @@ function calculateCoverage(input) {
1535
1535
  }
1536
1536
 
1537
1537
  // src/db/database.ts
1538
+ var CODE_WRITE_PROGRESS_INTERVAL = 500;
1539
+ function shouldEmitCodeWriteProgress(current, total) {
1540
+ return current === 0 || current === 1 || current === total || current % CODE_WRITE_PROGRESS_INTERVAL === 0;
1541
+ }
1538
1542
  function defaultDatabasePath(cwd) {
1539
1543
  return path4.join(cwd, ".anchor", "index.sqlite");
1540
1544
  }
1541
1545
  function openAnchorDatabase(cwd, databasePath = defaultDatabasePath(cwd)) {
1542
1546
  fs3.mkdirSync(path4.dirname(databasePath), { recursive: true });
1543
1547
  const db = new Database(databasePath);
1548
+ db.pragma("busy_timeout = 5000");
1544
1549
  db.pragma("journal_mode = WAL");
1545
1550
  db.pragma("foreign_keys = ON");
1546
1551
  return db;
1547
1552
  }
1553
+ function openAnchorDatabaseReadOnly(databasePath) {
1554
+ const db = new Database(databasePath, { readonly: true, fileMustExist: true });
1555
+ db.pragma("busy_timeout = 5000");
1556
+ db.pragma("foreign_keys = ON");
1557
+ return db;
1558
+ }
1548
1559
  function initializeSchema(db) {
1549
1560
  db.exec(SCHEMA_SQL);
1550
1561
  ensureColumn(db, "sync_state", "history_coverage", "TEXT");
@@ -1895,13 +1906,22 @@ function upsertPullRequest(db, pr, wisdomUnits, regressionEvents = []) {
1895
1906
  regressions: regressionEvents.length
1896
1907
  };
1897
1908
  }
1898
- function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, architecture = { components: [], patterns: [], imports: [] }) {
1909
+ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, architecture = { components: [], patterns: [], imports: [] }, options = {}) {
1899
1910
  initializeSchema(db);
1900
1911
  const repoId = ensureRepository(db, repo);
1901
1912
  const now = (/* @__PURE__ */ new Date()).toISOString();
1913
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Inferring test awareness" });
1902
1914
  const testAwareness = inferTestAwareness(repo, codeFiles, codeChunks);
1915
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Writing code index" });
1903
1916
  const transaction = db.transaction(() => {
1904
1917
  const existingChunks = db.prepare("SELECT id FROM code_chunks WHERE repo_id = ?").all(repoId);
1918
+ const existingPatternCount = db.prepare("SELECT COUNT(*) AS count FROM architecture_patterns WHERE repo_id = ?").get(repoId).count;
1919
+ options.onProgress?.({
1920
+ stage: "deleting_existing_code_index",
1921
+ repo,
1922
+ chunks: existingChunks.length,
1923
+ patterns: existingPatternCount
1924
+ });
1905
1925
  const deleteFts = db.prepare("DELETE FROM code_chunks_fts WHERE chunkId = ?");
1906
1926
  for (const row of existingChunks) deleteFts.run(row.id);
1907
1927
  db.prepare("DELETE FROM code_chunks WHERE repo_id = ?").run(repoId);
@@ -1914,7 +1934,13 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1914
1934
  (repo_id, path, language, size_bytes, content_hash, updated_at)
1915
1935
  VALUES (?, ?, ?, ?, ?, ?)`
1916
1936
  );
1917
- for (const file of codeFiles) {
1937
+ options.onProgress?.({
1938
+ stage: "writing_code_files",
1939
+ repo,
1940
+ current: 0,
1941
+ total: codeFiles.length
1942
+ });
1943
+ for (const [index, file] of codeFiles.entries()) {
1918
1944
  insertFile.run(
1919
1945
  repoId,
1920
1946
  file.path,
@@ -1923,6 +1949,16 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1923
1949
  file.contentHash,
1924
1950
  file.updatedAt
1925
1951
  );
1952
+ const current = index + 1;
1953
+ if (shouldEmitCodeWriteProgress(current, codeFiles.length)) {
1954
+ options.onProgress?.({
1955
+ stage: "writing_code_files",
1956
+ repo,
1957
+ current,
1958
+ total: codeFiles.length,
1959
+ filePath: file.path
1960
+ });
1961
+ }
1926
1962
  }
1927
1963
  const fileRows = db.prepare("SELECT id, path FROM code_files WHERE repo_id = ?").all(repoId);
1928
1964
  const fileIds = new Map(fileRows.map((row) => [row.path, row.id]));
@@ -1937,7 +1973,15 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1937
1973
  (chunkId, sanitizedText, filePath, symbols, language)
1938
1974
  VALUES (?, ?, ?, ?, ?)`
1939
1975
  );
1940
- for (const chunk of codeChunks) {
1976
+ options.onProgress?.({
1977
+ stage: "writing_code_chunks",
1978
+ repo,
1979
+ current: 0,
1980
+ total: codeChunks.length,
1981
+ chunks: 0
1982
+ });
1983
+ let writtenChunks = 0;
1984
+ for (const [index, chunk] of codeChunks.entries()) {
1941
1985
  const fileId = fileIds.get(chunk.filePath);
1942
1986
  if (!fileId) continue;
1943
1987
  insertChunk.run(
@@ -1961,10 +2005,23 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, ar
1961
2005
  chunk.symbols.join(" "),
1962
2006
  chunk.language ?? ""
1963
2007
  );
2008
+ writtenChunks += 1;
2009
+ const current = index + 1;
2010
+ if (shouldEmitCodeWriteProgress(current, codeChunks.length)) {
2011
+ options.onProgress?.({
2012
+ stage: "writing_code_chunks",
2013
+ repo,
2014
+ current,
2015
+ total: codeChunks.length,
2016
+ filePath: chunk.filePath,
2017
+ chunks: writtenChunks
2018
+ });
2019
+ }
1964
2020
  }
1965
- insertTestAwareness(db, repoId, testAwareness.testFiles, testAwareness.testLinks);
1966
- insertArchitectureData(db, repoId, architecture);
1967
- insertArchitectureMapEdges(db, repoId, repo, architecture, testAwareness.testLinks);
2021
+ insertTestAwareness(db, repoId, repo, testAwareness.testFiles, testAwareness.testLinks, options);
2022
+ insertArchitectureData(db, repoId, repo, architecture, options);
2023
+ insertArchitectureMapEdges(db, repoId, repo, architecture, testAwareness.testLinks, options);
2024
+ options.onProgress?.({ stage: "writing_code_index", repo, phase: "Updating index state" });
1968
2025
  db.prepare(
1969
2026
  `INSERT INTO code_index_state (repo, last_indexed_at, indexed_files, code_chunks, skipped_files)
1970
2027
  VALUES (?, ?, ?, ?, ?)
@@ -2012,13 +2069,20 @@ function deleteExistingArchitectureData(db, repoId) {
2012
2069
  db.prepare("DELETE FROM code_imports WHERE repo_id = ?").run(repoId);
2013
2070
  db.prepare("DELETE FROM architecture_map_edges WHERE repo_id = ?").run(repoId);
2014
2071
  }
2015
- function insertArchitectureData(db, repoId, architecture) {
2072
+ function insertArchitectureData(db, repoId, repo, architecture, options = {}) {
2016
2073
  const insertImport = db.prepare(
2017
2074
  `INSERT INTO code_imports
2018
2075
  (repo_id, source_path, specifier, imported_path, imported_symbols_json, kind)
2019
2076
  VALUES (?, ?, ?, ?, ?, ?)`
2020
2077
  );
2021
- for (const item of architecture.imports) {
2078
+ options.onProgress?.({
2079
+ stage: "writing_architecture_data",
2080
+ repo,
2081
+ current: 0,
2082
+ total: architecture.imports.length,
2083
+ kind: "imports"
2084
+ });
2085
+ for (const [index, item] of architecture.imports.entries()) {
2022
2086
  insertImport.run(
2023
2087
  repoId,
2024
2088
  item.sourcePath,
@@ -2027,6 +2091,16 @@ function insertArchitectureData(db, repoId, architecture) {
2027
2091
  JSON.stringify(item.importedSymbols),
2028
2092
  item.kind
2029
2093
  );
2094
+ const current = index + 1;
2095
+ if (shouldEmitCodeWriteProgress(current, architecture.imports.length)) {
2096
+ options.onProgress?.({
2097
+ stage: "writing_architecture_data",
2098
+ repo,
2099
+ current,
2100
+ total: architecture.imports.length,
2101
+ kind: "imports"
2102
+ });
2103
+ }
2030
2104
  }
2031
2105
  const insertComponent = db.prepare(
2032
2106
  `INSERT INTO architecture_components
@@ -2034,7 +2108,14 @@ function insertArchitectureData(db, repoId, architecture) {
2034
2108
  confidence, updated_at)
2035
2109
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2036
2110
  );
2037
- for (const component of architecture.components) {
2111
+ options.onProgress?.({
2112
+ stage: "writing_architecture_data",
2113
+ repo,
2114
+ current: 0,
2115
+ total: architecture.components.length,
2116
+ kind: "components"
2117
+ });
2118
+ for (const [index, component] of architecture.components.entries()) {
2038
2119
  insertComponent.run(
2039
2120
  repoId,
2040
2121
  component.path,
@@ -2047,6 +2128,16 @@ function insertArchitectureData(db, repoId, architecture) {
2047
2128
  component.confidence,
2048
2129
  component.updatedAt
2049
2130
  );
2131
+ const current = index + 1;
2132
+ if (shouldEmitCodeWriteProgress(current, architecture.components.length)) {
2133
+ options.onProgress?.({
2134
+ stage: "writing_architecture_data",
2135
+ repo,
2136
+ current,
2137
+ total: architecture.components.length,
2138
+ kind: "components"
2139
+ });
2140
+ }
2050
2141
  }
2051
2142
  const insertPattern = db.prepare(
2052
2143
  `INSERT INTO architecture_patterns
@@ -2058,7 +2149,14 @@ function insertArchitectureData(db, repoId, architecture) {
2058
2149
  `INSERT INTO architecture_patterns_fts (patternId, summary, area, sourceFiles, symbols)
2059
2150
  VALUES (?, ?, ?, ?, ?)`
2060
2151
  );
2061
- for (const pattern of architecture.patterns) {
2152
+ options.onProgress?.({
2153
+ stage: "writing_architecture_data",
2154
+ repo,
2155
+ current: 0,
2156
+ total: architecture.patterns.length,
2157
+ kind: "patterns"
2158
+ });
2159
+ for (const [index, pattern] of architecture.patterns.entries()) {
2062
2160
  insertPattern.run(
2063
2161
  pattern.id,
2064
2162
  repoId,
@@ -2079,9 +2177,19 @@ function insertArchitectureData(db, repoId, architecture) {
2079
2177
  pattern.sourceFiles.join(" "),
2080
2178
  pattern.symbols.join(" ")
2081
2179
  );
2180
+ const current = index + 1;
2181
+ if (shouldEmitCodeWriteProgress(current, architecture.patterns.length)) {
2182
+ options.onProgress?.({
2183
+ stage: "writing_architecture_data",
2184
+ repo,
2185
+ current,
2186
+ total: architecture.patterns.length,
2187
+ kind: "patterns"
2188
+ });
2189
+ }
2082
2190
  }
2083
2191
  }
2084
- function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks) {
2192
+ function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks, options = {}) {
2085
2193
  const now = (/* @__PURE__ */ new Date()).toISOString();
2086
2194
  const insert = db.prepare(
2087
2195
  `INSERT INTO architecture_map_edges
@@ -2096,11 +2204,40 @@ function insertArchitectureMapEdges(db, repoId, repo, architecture, testLinks) {
2096
2204
  seen.add(id);
2097
2205
  insert.run(id, repoId, repo, sourcePath, targetPath, relationship, weight, now);
2098
2206
  };
2207
+ const total = architecture.imports.length + testLinks.length;
2208
+ let current = 0;
2209
+ options.onProgress?.({
2210
+ stage: "writing_architecture_map_edges",
2211
+ repo,
2212
+ current,
2213
+ total,
2214
+ edges: 0
2215
+ });
2099
2216
  for (const item of architecture.imports) {
2100
2217
  if (item.importedPath) addEdge(item.sourcePath, item.importedPath, "imports", 0.9);
2218
+ current += 1;
2219
+ if (shouldEmitCodeWriteProgress(current, total)) {
2220
+ options.onProgress?.({
2221
+ stage: "writing_architecture_map_edges",
2222
+ repo,
2223
+ current,
2224
+ total,
2225
+ edges: seen.size
2226
+ });
2227
+ }
2101
2228
  }
2102
2229
  for (const link of testLinks) {
2103
2230
  addEdge(link.sourcePath, link.testPath, "tested_by", link.strength);
2231
+ current += 1;
2232
+ if (shouldEmitCodeWriteProgress(current, total)) {
2233
+ options.onProgress?.({
2234
+ stage: "writing_architecture_map_edges",
2235
+ repo,
2236
+ current,
2237
+ total,
2238
+ edges: seen.size
2239
+ });
2240
+ }
2104
2241
  }
2105
2242
  }
2106
2243
  function insertPrCochangeTestLinks(db, repoId, filePaths) {
@@ -2116,13 +2253,20 @@ function insertPrCochangeTestLinks(db, repoId, filePaths) {
2116
2253
  for (const testPath of testPaths) insert.run(repoId, sourcePath, testPath);
2117
2254
  }
2118
2255
  }
2119
- function insertTestAwareness(db, repoId, testFiles, testLinks) {
2256
+ function insertTestAwareness(db, repoId, repo, testFiles, testLinks, options = {}) {
2120
2257
  const insertTestFile = db.prepare(
2121
2258
  `INSERT INTO test_files
2122
2259
  (repo_id, path, language, size_bytes, content_hash, updated_at)
2123
2260
  VALUES (?, ?, ?, ?, ?, ?)`
2124
2261
  );
2125
- for (const file of testFiles) {
2262
+ options.onProgress?.({
2263
+ stage: "writing_test_awareness",
2264
+ repo,
2265
+ current: 0,
2266
+ total: testFiles.length,
2267
+ kind: "test_files"
2268
+ });
2269
+ for (const [index, file] of testFiles.entries()) {
2126
2270
  insertTestFile.run(
2127
2271
  repoId,
2128
2272
  file.path,
@@ -2131,13 +2275,40 @@ function insertTestAwareness(db, repoId, testFiles, testLinks) {
2131
2275
  file.contentHash,
2132
2276
  file.updatedAt
2133
2277
  );
2278
+ const current = index + 1;
2279
+ if (shouldEmitCodeWriteProgress(current, testFiles.length)) {
2280
+ options.onProgress?.({
2281
+ stage: "writing_test_awareness",
2282
+ repo,
2283
+ current,
2284
+ total: testFiles.length,
2285
+ kind: "test_files"
2286
+ });
2287
+ }
2134
2288
  }
2135
2289
  const insertTestLink = db.prepare(
2136
2290
  `INSERT INTO test_links (repo_id, source_path, test_path, reason, strength)
2137
2291
  VALUES (?, ?, ?, ?, ?)`
2138
2292
  );
2139
- for (const link of testLinks) {
2293
+ options.onProgress?.({
2294
+ stage: "writing_test_awareness",
2295
+ repo,
2296
+ current: 0,
2297
+ total: testLinks.length,
2298
+ kind: "test_links"
2299
+ });
2300
+ for (const [index, link] of testLinks.entries()) {
2140
2301
  insertTestLink.run(repoId, link.sourcePath, link.testPath, link.reason, link.strength);
2302
+ const current = index + 1;
2303
+ if (shouldEmitCodeWriteProgress(current, testLinks.length)) {
2304
+ options.onProgress?.({
2305
+ stage: "writing_test_awareness",
2306
+ repo,
2307
+ current,
2308
+ total: testLinks.length,
2309
+ kind: "test_links"
2310
+ });
2311
+ }
2141
2312
  }
2142
2313
  }
2143
2314
  function recordIndexRun(db, run) {
@@ -2464,6 +2635,10 @@ function chunkCodeFile(file, options = {}) {
2464
2635
  import crypto2 from "crypto";
2465
2636
  import path6 from "path";
2466
2637
  var KNOWN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
2638
+ var ARCHITECTURE_PROGRESS_INTERVAL = 250;
2639
+ function shouldEmitProgress(current, total) {
2640
+ return current === 0 || current === 1 || current === total || current % ARCHITECTURE_PROGRESS_INTERVAL === 0;
2641
+ }
2467
2642
  function classifyArchitectureArea(filePath, language, content = "") {
2468
2643
  const normalized = filePath.replace(/\\/g, "/").toLowerCase();
2469
2644
  const basename = path6.basename(normalized);
@@ -2611,7 +2786,7 @@ function createPattern(input) {
2611
2786
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
2612
2787
  };
2613
2788
  }
2614
- function buildArchitectureIndex(repo, files, chunks) {
2789
+ function buildArchitectureIndex(repo, files, chunks, options = {}) {
2615
2790
  const allPaths = files.map((file) => file.path);
2616
2791
  const codePaths = new Set(allPaths);
2617
2792
  const symbolsByPath = /* @__PURE__ */ new Map();
@@ -2619,20 +2794,47 @@ function buildArchitectureIndex(repo, files, chunks) {
2619
2794
  const existing = symbolsByPath.get(chunk.filePath) ?? [];
2620
2795
  symbolsByPath.set(chunk.filePath, uniqueStrings([...existing, ...chunk.symbols]).slice(0, 40));
2621
2796
  }
2622
- const imports = files.flatMap(
2623
- (file) => extractCodeImports(file.path, file.content, codePaths, repo)
2624
- );
2797
+ const imports = [];
2798
+ options.onProgress?.({
2799
+ stage: "building_architecture_imports",
2800
+ repo,
2801
+ current: 0,
2802
+ total: files.length,
2803
+ imports: 0
2804
+ });
2805
+ for (const [index, file] of files.entries()) {
2806
+ imports.push(...extractCodeImports(file.path, file.content, codePaths, repo));
2807
+ const current = index + 1;
2808
+ if (shouldEmitProgress(current, files.length)) {
2809
+ options.onProgress?.({
2810
+ stage: "building_architecture_imports",
2811
+ repo,
2812
+ current,
2813
+ total: files.length,
2814
+ filePath: file.path,
2815
+ imports: imports.length
2816
+ });
2817
+ }
2818
+ }
2625
2819
  const importsByPath = /* @__PURE__ */ new Map();
2626
2820
  for (const item of imports) {
2627
2821
  const existing = importsByPath.get(item.sourcePath) ?? [];
2628
2822
  existing.push(item);
2629
2823
  importsByPath.set(item.sourcePath, existing);
2630
2824
  }
2631
- const components = files.map((file) => {
2825
+ const components = [];
2826
+ options.onProgress?.({
2827
+ stage: "building_architecture_components",
2828
+ repo,
2829
+ current: 0,
2830
+ total: files.length,
2831
+ components: 0
2832
+ });
2833
+ for (const [index, file] of files.entries()) {
2632
2834
  const area = classifyArchitectureArea(file.path, file.language, file.content);
2633
2835
  const fileImports = importsByPath.get(file.path) ?? [];
2634
2836
  const symbols = symbolsByPath.get(file.path) ?? [];
2635
- return {
2837
+ components.push({
2636
2838
  repo,
2637
2839
  path: file.path,
2638
2840
  area,
@@ -2645,16 +2847,49 @@ function buildArchitectureIndex(repo, files, chunks) {
2645
2847
  relatedTests: relatedTestsFor(file.path, allPaths),
2646
2848
  confidence: area === "unknown" ? 0.45 : 0.82,
2647
2849
  updatedAt: file.updatedAt
2648
- };
2649
- });
2850
+ });
2851
+ const current = index + 1;
2852
+ if (shouldEmitProgress(current, files.length)) {
2853
+ options.onProgress?.({
2854
+ stage: "building_architecture_components",
2855
+ repo,
2856
+ current,
2857
+ total: files.length,
2858
+ filePath: file.path,
2859
+ components: components.length
2860
+ });
2861
+ }
2862
+ }
2650
2863
  const componentByPath = new Map(components.map((component) => [component.path, component]));
2651
- const patterns = [];
2652
2864
  const componentsByArea = /* @__PURE__ */ new Map();
2653
2865
  for (const component of components) {
2654
2866
  const existing = componentsByArea.get(component.area) ?? [];
2655
2867
  existing.push(component);
2656
2868
  componentsByArea.set(component.area, existing);
2657
2869
  }
2870
+ const importDirectionCounts = /* @__PURE__ */ new Map();
2871
+ for (const item of imports) {
2872
+ if (!item.importedPath) continue;
2873
+ const source = componentByPath.get(item.sourcePath);
2874
+ const target = componentByPath.get(item.importedPath);
2875
+ if (!source || !target || source.area === target.area) continue;
2876
+ const key = `${source.area}->${target.area}`;
2877
+ const existing = importDirectionCounts.get(key) ?? { count: 0, files: [], symbols: [] };
2878
+ existing.count += 1;
2879
+ existing.files.push(source.path, target.path);
2880
+ existing.symbols.push(...item.importedSymbols);
2881
+ importDirectionCounts.set(key, existing);
2882
+ }
2883
+ const patterns = [];
2884
+ const patternTotal = componentsByArea.size + importDirectionCounts.size;
2885
+ let patternProgress = 0;
2886
+ options.onProgress?.({
2887
+ stage: "building_architecture_patterns",
2888
+ repo,
2889
+ current: 0,
2890
+ total: patternTotal,
2891
+ patterns: 0
2892
+ });
2658
2893
  for (const [area, areaComponents] of componentsByArea.entries()) {
2659
2894
  const filesForArea = areaComponents.map((component) => component.path);
2660
2895
  const directories = topDirectories(filesForArea);
@@ -2670,19 +2905,17 @@ function buildArchitectureIndex(repo, files, chunks) {
2670
2905
  confidence: 0.55 + Math.min(0.3, filesForArea.length * 0.04)
2671
2906
  })
2672
2907
  );
2673
- }
2674
- const importDirectionCounts = /* @__PURE__ */ new Map();
2675
- for (const item of imports) {
2676
- if (!item.importedPath) continue;
2677
- const source = componentByPath.get(item.sourcePath);
2678
- const target = componentByPath.get(item.importedPath);
2679
- if (!source || !target || source.area === target.area) continue;
2680
- const key = `${source.area}->${target.area}`;
2681
- const existing = importDirectionCounts.get(key) ?? { count: 0, files: [], symbols: [] };
2682
- existing.count += 1;
2683
- existing.files.push(source.path, target.path);
2684
- existing.symbols.push(...item.importedSymbols);
2685
- importDirectionCounts.set(key, existing);
2908
+ patternProgress += 1;
2909
+ if (shouldEmitProgress(patternProgress, patternTotal)) {
2910
+ options.onProgress?.({
2911
+ stage: "building_architecture_patterns",
2912
+ repo,
2913
+ current: patternProgress,
2914
+ total: patternTotal,
2915
+ area,
2916
+ patterns: patterns.length
2917
+ });
2918
+ }
2686
2919
  }
2687
2920
  for (const [key, value] of importDirectionCounts.entries()) {
2688
2921
  const [sourceArea, targetArea] = key.split("->");
@@ -2697,6 +2930,17 @@ function buildArchitectureIndex(repo, files, chunks) {
2697
2930
  confidence: 0.62 + Math.min(0.25, value.count * 0.05)
2698
2931
  })
2699
2932
  );
2933
+ patternProgress += 1;
2934
+ if (shouldEmitProgress(patternProgress, patternTotal)) {
2935
+ options.onProgress?.({
2936
+ stage: "building_architecture_patterns",
2937
+ repo,
2938
+ current: patternProgress,
2939
+ total: patternTotal,
2940
+ area: sourceArea,
2941
+ patterns: patterns.length
2942
+ });
2943
+ }
2700
2944
  }
2701
2945
  const testedComponents = components.filter((component) => component.relatedTests.length > 0);
2702
2946
  if (testedComponents.length > 0) {
@@ -3026,8 +3270,24 @@ function detectTestCommands(db, cwd, files = []) {
3026
3270
  return true;
3027
3271
  });
3028
3272
  }
3029
- function refreshTestCommands(db, cwd, repo, files = []) {
3273
+ function refreshTestCommands(db, cwd, repo, files = [], options = {}) {
3274
+ options.onProgress?.({
3275
+ stage: "refreshing_test_commands",
3276
+ repo,
3277
+ phase: "detecting",
3278
+ current: 0,
3279
+ total: files.length,
3280
+ commands: 0
3281
+ });
3030
3282
  const commands = detectTestCommands(db, cwd, files);
3283
+ options.onProgress?.({
3284
+ stage: "refreshing_test_commands",
3285
+ repo,
3286
+ phase: "writing",
3287
+ current: 0,
3288
+ total: commands.length,
3289
+ commands: commands.length
3290
+ });
3031
3291
  const now = (/* @__PURE__ */ new Date()).toISOString();
3032
3292
  const transaction = db.transaction(() => {
3033
3293
  db.prepare("DELETE FROM test_commands WHERE repo = ?").run(repo);
@@ -3035,7 +3295,7 @@ function refreshTestCommands(db, cwd, repo, files = []) {
3035
3295
  `INSERT INTO test_commands (id, repo, file_path, command, reason, confidence, created_at)
3036
3296
  VALUES (?, ?, ?, ?, ?, ?, ?)`
3037
3297
  );
3038
- for (const command of commands) {
3298
+ for (const [index, command] of commands.entries()) {
3039
3299
  insert.run(
3040
3300
  commandId(repo, command),
3041
3301
  repo,
@@ -3045,6 +3305,17 @@ function refreshTestCommands(db, cwd, repo, files = []) {
3045
3305
  command.confidence,
3046
3306
  now
3047
3307
  );
3308
+ const current = index + 1;
3309
+ if (current === 1 || current === commands.length || current % 250 === 0) {
3310
+ options.onProgress?.({
3311
+ stage: "refreshing_test_commands",
3312
+ repo,
3313
+ phase: "writing",
3314
+ current,
3315
+ total: commands.length,
3316
+ commands: commands.length
3317
+ });
3318
+ }
3048
3319
  }
3049
3320
  });
3050
3321
  transaction();
@@ -3083,7 +3354,9 @@ function indexCodebase(db, options) {
3083
3354
  chunks: fileChunks.length
3084
3355
  });
3085
3356
  }
3086
- const architecture = buildArchitectureIndex(options.repo, discovery.files, chunks);
3357
+ const architecture = buildArchitectureIndex(options.repo, discovery.files, chunks, {
3358
+ onProgress: options.onProgress
3359
+ });
3087
3360
  options.onProgress?.({
3088
3361
  stage: "indexed_architecture",
3089
3362
  repo: options.repo,
@@ -3098,9 +3371,22 @@ function indexCodebase(db, options) {
3098
3371
  chunks,
3099
3372
  discovery.skippedFiles,
3100
3373
  options.cwd,
3101
- architecture
3374
+ architecture,
3375
+ { onProgress: options.onProgress }
3102
3376
  );
3103
- refreshTestCommands(db, options.cwd, options.repo);
3377
+ refreshTestCommands(db, options.cwd, options.repo, [], { onProgress: options.onProgress });
3378
+ options.onProgress?.({
3379
+ stage: "completed_code_index",
3380
+ repo: options.repo,
3381
+ files: summary.indexedFiles,
3382
+ chunks: summary.codeChunksCreated,
3383
+ skippedFiles: summary.skippedFiles,
3384
+ testFiles: summary.testFilesIndexed,
3385
+ testLinks: summary.testLinksCreated,
3386
+ architectureComponents: summary.architectureComponentsIndexed,
3387
+ architecturePatterns: summary.architecturePatternsIndexed,
3388
+ architectureImports: summary.architectureImportsIndexed
3389
+ });
3104
3390
  return summary;
3105
3391
  }
3106
3392
  function emptyCodeIndexSummary(cwd) {
@@ -3425,7 +3711,8 @@ function indexPullRequests(db, pullRequests, options) {
3425
3711
  current: index + 1,
3426
3712
  total: pullRequests.length,
3427
3713
  prNumber: pr.number,
3428
- wisdomUnitsCreated: result.wisdom
3714
+ wisdomUnitsCreated: result.wisdom,
3715
+ regressionEventsCreated: result.regressions
3429
3716
  });
3430
3717
  }
3431
3718
  if (options.updateSyncStateAfter !== false) {
@@ -6292,6 +6579,18 @@ function parseGraphQLResponse(text, status, headers) {
6292
6579
  );
6293
6580
  }
6294
6581
  }
6582
+ function sleep2(ms) {
6583
+ return new Promise((resolve) => setTimeout(resolve, ms));
6584
+ }
6585
+ function isTransientGraphQLError(error) {
6586
+ const status = error.status;
6587
+ if (status === 502 || status === 503 || status === 504) return true;
6588
+ const message = (error.message ?? "").toLowerCase();
6589
+ return message.includes("fetch failed") || message.includes("econnreset") || message.includes("etimedout") || message.includes("socket hang up") || message.includes("network") || message.includes("non-json response") && (message.includes("text/html") || message.includes("<!doctype") || message.includes("<html") || typeof status === "number" && status >= 500);
6590
+ }
6591
+ function transientRetryDelayMs(attempt) {
6592
+ return Math.min(4e3, 500 * 2 ** Math.max(0, attempt - 1));
6593
+ }
6295
6594
  function createGitHubGraphQLRequester(options) {
6296
6595
  if (!options.token.trim()) {
6297
6596
  throw new Error(
@@ -6303,36 +6602,55 @@ function createGitHubGraphQLRequester(options) {
6303
6602
  return async function requestGitHubGraphQL(query, variables, requestOptions) {
6304
6603
  return requestWithGitHubRateLimit(
6305
6604
  async () => {
6306
- const response = await fetchImpl("https://api.github.com/graphql", {
6307
- method: "POST",
6308
- headers: {
6309
- accept: "application/vnd.github+json",
6310
- authorization: `Bearer ${options.token}`,
6311
- "content-type": "application/json",
6312
- "user-agent": "anchor-local-mcp"
6313
- },
6314
- body: JSON.stringify({ query, variables })
6315
- });
6316
- const headers = headersToRecord(response.headers);
6317
- const raw = parseGraphQLResponse(await response.text(), response.status, headers);
6318
- if (!response.ok || raw.errors?.length) {
6319
- throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6320
- status: errorStatus(response.status, raw.errors),
6321
- headers
6322
- });
6323
- }
6324
- if (!raw.data) {
6325
- throw new GitHubGraphQLError("GitHub GraphQL response did not include data.", {
6326
- status: response.status,
6327
- headers
6328
- });
6605
+ const maxAttempts = Math.max(1, requestOptions.maxTransientRetries ?? 3);
6606
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
6607
+ try {
6608
+ const response = await fetchImpl("https://api.github.com/graphql", {
6609
+ method: "POST",
6610
+ headers: {
6611
+ accept: "application/vnd.github+json",
6612
+ authorization: `Bearer ${options.token}`,
6613
+ "content-type": "application/json",
6614
+ "user-agent": "anchor-local-mcp"
6615
+ },
6616
+ body: JSON.stringify({ query, variables })
6617
+ });
6618
+ const headers = headersToRecord(response.headers);
6619
+ const raw = parseGraphQLResponse(await response.text(), response.status, headers);
6620
+ if (!response.ok || raw.errors?.length) {
6621
+ throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6622
+ status: errorStatus(response.status, raw.errors),
6623
+ headers
6624
+ });
6625
+ }
6626
+ if (!raw.data) {
6627
+ throw new GitHubGraphQLError("GitHub GraphQL response did not include data.", {
6628
+ status: response.status,
6629
+ headers
6630
+ });
6631
+ }
6632
+ updateGitHubGraphQLRateLimitState(
6633
+ requestOptions.controller,
6634
+ raw.data.rateLimit,
6635
+ requestOptions.requestName
6636
+ );
6637
+ return { data: raw.data, headers };
6638
+ } catch (error) {
6639
+ if (attempt >= maxAttempts || !isTransientGraphQLError(error)) throw error;
6640
+ const waitMs = transientRetryDelayMs(attempt);
6641
+ requestOptions.onTransientRetry?.({
6642
+ attempt,
6643
+ maxAttempts,
6644
+ waitMs,
6645
+ reason: error instanceof Error ? error.message : String(error)
6646
+ });
6647
+ await sleep2(waitMs);
6648
+ }
6329
6649
  }
6330
- updateGitHubGraphQLRateLimitState(
6331
- requestOptions.controller,
6332
- raw.data.rateLimit,
6333
- requestOptions.requestName
6334
- );
6335
- return { data: raw.data, headers };
6650
+ throw new GitHubGraphQLError("GitHub GraphQL request retry loop exited unexpectedly.", {
6651
+ status: 500,
6652
+ headers: {}
6653
+ });
6336
6654
  },
6337
6655
  {
6338
6656
  controller: requestOptions.controller,
@@ -6518,6 +6836,16 @@ var GraphQLBudget = class {
6518
6836
  };
6519
6837
  }
6520
6838
  };
6839
+ function graphqlRetryProgress(repo, onProgress) {
6840
+ return (retry) => onProgress?.({
6841
+ stage: "github_graphql_retry",
6842
+ repo,
6843
+ attempt: retry.attempt,
6844
+ maxAttempts: retry.maxAttempts,
6845
+ waitMs: retry.waitMs,
6846
+ reason: retry.reason
6847
+ });
6848
+ }
6521
6849
  var PULL_REQUEST_FIELDS = `
6522
6850
  number
6523
6851
  url
@@ -6655,7 +6983,8 @@ function connectionNodes(connection) {
6655
6983
  async function requestGraphQLWithBudget(requestGraphQL, query, variables, options) {
6656
6984
  const response = await requestGraphQL(query, variables, {
6657
6985
  controller: options.controller,
6658
- requestName: options.requestName
6986
+ requestName: options.requestName,
6987
+ onTransientRetry: options.onTransientRetry
6659
6988
  });
6660
6989
  options.budget.observe(response.data.rateLimit);
6661
6990
  return response;
@@ -6741,7 +7070,8 @@ async function appendAdditionalFiles(requestGraphQL, record, initialConnection,
6741
7070
  {
6742
7071
  controller: options.controller,
6743
7072
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/files`,
6744
- budget: options.budget
7073
+ budget: options.budget,
7074
+ onTransientRetry: options.onTransientRetry
6745
7075
  }
6746
7076
  );
6747
7077
  record.files.push(
@@ -6767,7 +7097,8 @@ async function appendAdditionalIssueComments(requestGraphQL, record, initialConn
6767
7097
  {
6768
7098
  controller: options.controller,
6769
7099
  requestName: `GraphQL /repos/${record.repo}/issues/${record.number}/comments`,
6770
- budget: options.budget
7100
+ budget: options.budget,
7101
+ onTransientRetry: options.onTransientRetry
6771
7102
  }
6772
7103
  );
6773
7104
  record.issueComments?.push(...connectionNodes(connection).map(mapIssueComment));
@@ -6791,7 +7122,8 @@ async function appendAdditionalCommits(requestGraphQL, record, initialConnection
6791
7122
  {
6792
7123
  controller: options.controller,
6793
7124
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/commits`,
6794
- budget: options.budget
7125
+ budget: options.budget,
7126
+ onTransientRetry: options.onTransientRetry
6795
7127
  }
6796
7128
  );
6797
7129
  record.commits?.push(
@@ -6816,7 +7148,8 @@ async function appendAdditionalReviewComments(requestGraphQL, record, review, op
6816
7148
  {
6817
7149
  controller: options.controller,
6818
7150
  requestName: `GraphQL /pull-request-reviews/${review.id}/comments`,
6819
- budget: options.budget
7151
+ budget: options.budget,
7152
+ onTransientRetry: options.onTransientRetry
6820
7153
  }
6821
7154
  );
6822
7155
  const connection = response.data.node?.comments;
@@ -6842,7 +7175,8 @@ async function appendAdditionalReviews(requestGraphQL, record, initialConnection
6842
7175
  {
6843
7176
  controller: options.controller,
6844
7177
  requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/reviews`,
6845
- budget: options.budget
7178
+ budget: options.budget,
7179
+ onTransientRetry: options.onTransientRetry
6846
7180
  }
6847
7181
  );
6848
7182
  const reviewNodes = connectionNodes(connection);
@@ -6856,7 +7190,8 @@ async function appendAdditionalReviews(requestGraphQL, record, initialConnection
6856
7190
  for (const review of reviewsToHydrate) {
6857
7191
  await appendAdditionalReviewComments(requestGraphQL, record, review, {
6858
7192
  controller: options.controller,
6859
- budget: options.budget
7193
+ budget: options.budget,
7194
+ onTransientRetry: options.onTransientRetry
6860
7195
  });
6861
7196
  }
6862
7197
  }
@@ -7012,6 +7347,7 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7012
7347
  checkpoint?.pageSize ?? Math.min(INITIAL_PULL_REQUEST_PAGE_SIZE, options.limit ?? INITIAL_PULL_REQUEST_PAGE_SIZE)
7013
7348
  );
7014
7349
  const budget = new GraphQLBudget(GRAPHQL_RATE_LIMIT_RESERVE);
7350
+ const onTransientRetry = graphqlRetryProgress(options.repo, options.onProgress);
7015
7351
  const checkpointScope = checkpoint?.scope ?? `${options.repo}|${options.limit === void 0 ? "all" : `limit:${options.limit}`}|since:${options.since ?? ""}`;
7016
7352
  options.onProgress?.({
7017
7353
  stage: "discovering_pull_requests",
@@ -7038,7 +7374,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7038
7374
  {
7039
7375
  controller: options.controller,
7040
7376
  requestName: "GraphQL rate limit preflight",
7041
- budget
7377
+ budget,
7378
+ onTransientRetry
7042
7379
  }
7043
7380
  );
7044
7381
  const preflightRateLimit = budget.rateLimit();
@@ -7100,7 +7437,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7100
7437
  {
7101
7438
  controller: options.controller,
7102
7439
  requestName: `GraphQL /repos/${options.repo}/pullRequests`,
7103
- budget
7440
+ budget,
7441
+ onTransientRetry
7104
7442
  }
7105
7443
  );
7106
7444
  } catch (error) {
@@ -7131,7 +7469,8 @@ async function fetchMergedPullRequestsWithGraphQL(options) {
7131
7469
  owner,
7132
7470
  name,
7133
7471
  controller: options.controller,
7134
- budget
7472
+ budget,
7473
+ onTransientRetry
7135
7474
  });
7136
7475
  records.push(record);
7137
7476
  if (options.limit !== void 0 && records.length >= options.limit) break;
@@ -7553,6 +7892,9 @@ function openOrgDatabase(org, baseDir) {
7553
7892
  initializeSchema(db);
7554
7893
  return db;
7555
7894
  }
7895
+ function openOrgDatabaseReadOnly(org, baseDir) {
7896
+ return openAnchorDatabaseReadOnly(orgDatabasePath(org, baseDir));
7897
+ }
7556
7898
  function syncOrgConfigToDatabase(db, config, baseDir) {
7557
7899
  initializeSchema(db);
7558
7900
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -7730,9 +8072,11 @@ function grade(score) {
7730
8072
  if (score < 80) return "good";
7731
8073
  return "excellent";
7732
8074
  }
7733
- function getOrgStatus(db, config, baseDir) {
7734
- initializeSchema(db);
7735
- syncOrgConfigToDatabase(db, config, baseDir);
8075
+ function getOrgStatus(db, config, baseDir, options = {}) {
8076
+ if (options.syncConfig !== false) {
8077
+ initializeSchema(db);
8078
+ syncOrgConfigToDatabase(db, config, baseDir);
8079
+ }
7736
8080
  const enabledRepos = config.repos.filter((repo) => repo.enabled);
7737
8081
  const states = new Map(
7738
8082
  db.prepare("SELECT * FROM org_repo_state WHERE org = ?").all(config.org).map((row) => [row.repo, row])
@@ -7779,6 +8123,8 @@ function getOrgStatus(db, config, baseDir) {
7779
8123
  org: config.org,
7780
8124
  root: orgRoot(config.org, baseDir),
7781
8125
  databasePath: orgDatabasePath(config.org, baseDir),
8126
+ statusReadError: options.statusReadError,
8127
+ activeRun: options.activeRun,
7782
8128
  repoCount: config.repos.length,
7783
8129
  enabledRepoCount: enabledRepos.length,
7784
8130
  clonedRepoCount,
@@ -7813,10 +8159,141 @@ function getOrgStatus(db, config, baseDir) {
7813
8159
  };
7814
8160
  }
7815
8161
 
7816
- // src/org/clone.ts
7817
- import { execFileSync as execFileSync4 } from "child_process";
8162
+ // src/org/heartbeat.ts
7818
8163
  import fs11 from "fs";
7819
8164
  import path21 from "path";
8165
+ var HEARTBEAT_STALE_AFTER_MS = 2 * 60 * 1e3;
8166
+ function orgHeartbeatPath(org, baseDir) {
8167
+ return path21.join(orgRoot(validateOrgName(org), baseDir), "sync-heartbeat.json");
8168
+ }
8169
+ function atomicWriteJson2(filePath, value) {
8170
+ fs11.mkdirSync(path21.dirname(filePath), { recursive: true });
8171
+ const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
8172
+ fs11.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
8173
+ `, { mode: 384 });
8174
+ fs11.renameSync(tmp, filePath);
8175
+ }
8176
+ function processIsRunning(pid) {
8177
+ if (!Number.isInteger(pid) || pid <= 0) return false;
8178
+ try {
8179
+ process.kill(pid, 0);
8180
+ return true;
8181
+ } catch {
8182
+ return false;
8183
+ }
8184
+ }
8185
+ function parseHeartbeat(value) {
8186
+ if (!value || typeof value !== "object") return void 0;
8187
+ const candidate = value;
8188
+ if (typeof candidate.pid !== "number" || typeof candidate.command !== "string" || typeof candidate.org !== "string" || typeof candidate.phase !== "string" || typeof candidate.startedAt !== "string" || typeof candidate.updatedAt !== "string") {
8189
+ return void 0;
8190
+ }
8191
+ return {
8192
+ pid: candidate.pid,
8193
+ command: candidate.command,
8194
+ org: candidate.org,
8195
+ repo: typeof candidate.repo === "string" ? candidate.repo : void 0,
8196
+ repoIndex: typeof candidate.repoIndex === "number" ? candidate.repoIndex : void 0,
8197
+ repoTotal: typeof candidate.repoTotal === "number" ? candidate.repoTotal : void 0,
8198
+ phase: candidate.phase,
8199
+ timeline: parseTimeline(candidate.timeline),
8200
+ startedAt: candidate.startedAt,
8201
+ updatedAt: candidate.updatedAt
8202
+ };
8203
+ }
8204
+ function parseTimelineStatus(value) {
8205
+ if (value === "active" || value === "done" || value === "skipped" || value === "warn" || value === "fail" || value === "wait") {
8206
+ return value;
8207
+ }
8208
+ return void 0;
8209
+ }
8210
+ function parseTimelineNumber(value) {
8211
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
8212
+ }
8213
+ function parseTimelineStep(value) {
8214
+ if (!value || typeof value !== "object") return void 0;
8215
+ const candidate = value;
8216
+ const status = parseTimelineStatus(candidate.status);
8217
+ if (typeof candidate.id !== "string" || typeof candidate.label !== "string" || !status || typeof candidate.startedAt !== "string" || typeof candidate.updatedAt !== "string") {
8218
+ return void 0;
8219
+ }
8220
+ return {
8221
+ id: candidate.id,
8222
+ label: candidate.label,
8223
+ status,
8224
+ startedAt: candidate.startedAt,
8225
+ updatedAt: candidate.updatedAt,
8226
+ completedAt: typeof candidate.completedAt === "string" ? candidate.completedAt : void 0,
8227
+ durationMs: parseTimelineNumber(candidate.durationMs),
8228
+ current: parseTimelineNumber(candidate.current),
8229
+ total: parseTimelineNumber(candidate.total),
8230
+ detail: typeof candidate.detail === "string" ? candidate.detail : void 0
8231
+ };
8232
+ }
8233
+ function parseTimelineRepoSummary(value) {
8234
+ if (!value || typeof value !== "object") return void 0;
8235
+ const candidate = value;
8236
+ const status = parseTimelineStatus(candidate.status);
8237
+ if (typeof candidate.repo !== "string" || !status) return void 0;
8238
+ return {
8239
+ repo: candidate.repo,
8240
+ status,
8241
+ durationMs: parseTimelineNumber(candidate.durationMs) ?? 0,
8242
+ detail: typeof candidate.detail === "string" ? candidate.detail : void 0
8243
+ };
8244
+ }
8245
+ function parseTimeline(value) {
8246
+ if (!value || typeof value !== "object") return void 0;
8247
+ const candidate = value;
8248
+ if (!Array.isArray(candidate.steps) || !Array.isArray(candidate.recentRepos)) return void 0;
8249
+ const steps = candidate.steps.map(parseTimelineStep).filter((step) => step !== void 0);
8250
+ const recentRepos = candidate.recentRepos.map(parseTimelineRepoSummary).filter((repo) => repo !== void 0);
8251
+ return {
8252
+ repo: typeof candidate.repo === "string" ? candidate.repo : void 0,
8253
+ repoIndex: parseTimelineNumber(candidate.repoIndex),
8254
+ repoTotal: parseTimelineNumber(candidate.repoTotal),
8255
+ activeStepId: typeof candidate.activeStepId === "string" ? candidate.activeStepId : void 0,
8256
+ steps,
8257
+ recentRepos
8258
+ };
8259
+ }
8260
+ function writeOrgHeartbeat(heartbeat, baseDir) {
8261
+ atomicWriteJson2(orgHeartbeatPath(heartbeat.org, baseDir), heartbeat);
8262
+ }
8263
+ function clearOrgHeartbeat(org, baseDir) {
8264
+ try {
8265
+ const filePath = orgHeartbeatPath(org, baseDir);
8266
+ if (fs11.existsSync(filePath)) fs11.unlinkSync(filePath);
8267
+ } catch {
8268
+ }
8269
+ }
8270
+ function readOrgHeartbeat(org, baseDir) {
8271
+ const filePath = orgHeartbeatPath(org, baseDir);
8272
+ if (!fs11.existsSync(filePath)) return void 0;
8273
+ try {
8274
+ const heartbeat = parseHeartbeat(JSON.parse(fs11.readFileSync(filePath, "utf8")));
8275
+ if (!heartbeat) return void 0;
8276
+ const now = Date.now();
8277
+ const startedAtMs = Date.parse(heartbeat.startedAt);
8278
+ const updatedAtMs = Date.parse(heartbeat.updatedAt);
8279
+ const pidRunning = processIsRunning(heartbeat.pid);
8280
+ const lastUpdateAgeSeconds = Number.isFinite(updatedAtMs) ? Math.max(0, Math.floor((now - updatedAtMs) / 1e3)) : 0;
8281
+ return {
8282
+ ...heartbeat,
8283
+ pidRunning,
8284
+ stale: !pidRunning || !Number.isFinite(updatedAtMs) || now - updatedAtMs > HEARTBEAT_STALE_AFTER_MS,
8285
+ elapsedSeconds: Number.isFinite(startedAtMs) ? Math.max(0, Math.floor((now - startedAtMs) / 1e3)) : 0,
8286
+ lastUpdateAgeSeconds
8287
+ };
8288
+ } catch {
8289
+ return void 0;
8290
+ }
8291
+ }
8292
+
8293
+ // src/org/clone.ts
8294
+ import { execFileSync as execFileSync4 } from "child_process";
8295
+ import fs12 from "fs";
8296
+ import path22 from "path";
7820
8297
  function defaultGitCommandRunner(command, args, options = {}) {
7821
8298
  return execFileSync4(command, args, {
7822
8299
  cwd: options.cwd,
@@ -7832,7 +8309,7 @@ function currentCommit(runner, localPath) {
7832
8309
  }
7833
8310
  }
7834
8311
  function plannedOrgCloneCommands(repo, localPath) {
7835
- if (!fs11.existsSync(path21.join(localPath, ".git"))) {
8312
+ if (!fs12.existsSync(path22.join(localPath, ".git"))) {
7836
8313
  return [
7837
8314
  {
7838
8315
  command: "git",
@@ -7861,8 +8338,8 @@ function plannedOrgCloneCommands(repo, localPath) {
7861
8338
  function cloneOrPullOrgRepo(input) {
7862
8339
  const runner = input.runner ?? defaultGitCommandRunner;
7863
8340
  const localPath = orgRepoLocalPath(input.org, input.repo, input.baseDir);
7864
- const existed = fs11.existsSync(path21.join(localPath, ".git"));
7865
- fs11.mkdirSync(path21.dirname(localPath), { recursive: true });
8341
+ const existed = fs12.existsSync(path22.join(localPath, ".git"));
8342
+ fs12.mkdirSync(path22.dirname(localPath), { recursive: true });
7866
8343
  const now = (/* @__PURE__ */ new Date()).toISOString();
7867
8344
  try {
7868
8345
  const commands = plannedOrgCloneCommands(input.repo, localPath);
@@ -7963,8 +8440,8 @@ function orgCloneStateFromResult(org, repo, result) {
7963
8440
 
7964
8441
  // src/org/graph.ts
7965
8442
  import crypto9 from "crypto";
7966
- import fs12 from "fs";
7967
- import path22 from "path";
8443
+ import fs13 from "fs";
8444
+ import path23 from "path";
7968
8445
  function stableId(parts) {
7969
8446
  return crypto9.createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
7970
8447
  }
@@ -7978,10 +8455,10 @@ function fileEvidence(repo, filePath, note) {
7978
8455
  };
7979
8456
  }
7980
8457
  function readPackageManifest(repoPath) {
7981
- const packagePath = path22.join(repoPath, "package.json");
7982
- if (!fs12.existsSync(packagePath)) return void 0;
8458
+ const packagePath = path23.join(repoPath, "package.json");
8459
+ if (!fs13.existsSync(packagePath)) return void 0;
7983
8460
  try {
7984
- return JSON.parse(fs12.readFileSync(packagePath, "utf8"));
8461
+ return JSON.parse(fs13.readFileSync(packagePath, "utf8"));
7985
8462
  } catch {
7986
8463
  return void 0;
7987
8464
  }
@@ -8041,7 +8518,7 @@ function isApiConsumerText(text) {
8041
8518
  function evidenceJson(evidence) {
8042
8519
  return JSON.stringify(evidence);
8043
8520
  }
8044
- function shouldEmitProgress(current, total, interval = 100) {
8521
+ function shouldEmitProgress2(current, total, interval = 100) {
8045
8522
  return current === 1 || current === total || current % interval === 0;
8046
8523
  }
8047
8524
  function resolveOptions(baseDirOrOptions) {
@@ -8147,7 +8624,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8147
8624
  });
8148
8625
  break;
8149
8626
  }
8150
- if (shouldEmitProgress(index + 1, imports.length)) {
8627
+ if (shouldEmitProgress2(index + 1, imports.length)) {
8151
8628
  options.onProgress?.({
8152
8629
  stage: "building_import_edges",
8153
8630
  org: config.org,
@@ -8188,7 +8665,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8188
8665
  bucket.push(apiContract);
8189
8666
  contractsByToken.set(sanitizedContract, bucket);
8190
8667
  }
8191
- if (shouldEmitProgress(index + 1, providerChunks.length)) {
8668
+ if (shouldEmitProgress2(index + 1, providerChunks.length)) {
8192
8669
  options.onProgress?.({
8193
8670
  stage: "extracting_api_contracts",
8194
8671
  org: config.org,
@@ -8248,7 +8725,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8248
8725
  });
8249
8726
  }
8250
8727
  }
8251
- if (shouldEmitProgress(index + 1, consumerChunks.length)) {
8728
+ if (shouldEmitProgress2(index + 1, consumerChunks.length)) {
8252
8729
  options.onProgress?.({
8253
8730
  stage: "matching_api_consumers",
8254
8731
  org: config.org,
@@ -8274,7 +8751,11 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8274
8751
  const insertEdge = db.prepare(
8275
8752
  `INSERT INTO org_cross_repo_edges
8276
8753
  (id, org, source_repo, source_path, target_repo, target_path, relationship, evidence_json, confidence, created_at)
8277
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8754
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8755
+ ON CONFLICT(id) DO UPDATE SET
8756
+ evidence_json = excluded.evidence_json,
8757
+ confidence = excluded.confidence,
8758
+ created_at = excluded.created_at`
8278
8759
  );
8279
8760
  for (const edge of edges) {
8280
8761
  insertEdge.run(
@@ -8293,7 +8774,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8293
8774
  const insertContract = db.prepare(
8294
8775
  `INSERT INTO org_api_contracts
8295
8776
  (id, org, repo, file_path, contract, evidence_json, confidence, created_at)
8296
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
8777
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
8778
+ ON CONFLICT(id) DO UPDATE SET
8779
+ contract = excluded.contract,
8780
+ evidence_json = excluded.evidence_json,
8781
+ confidence = excluded.confidence,
8782
+ created_at = excluded.created_at`
8297
8783
  );
8298
8784
  for (const contract of apiContracts) {
8299
8785
  insertContract.run(
@@ -8310,7 +8796,12 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8310
8796
  const insertConsumer = db.prepare(
8311
8797
  `INSERT INTO org_api_consumers
8312
8798
  (id, org, provider_repo, provider_path, consumer_repo, consumer_path, contract, evidence_json, confidence, created_at)
8313
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8799
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8800
+ ON CONFLICT(id) DO UPDATE SET
8801
+ contract = excluded.contract,
8802
+ evidence_json = excluded.evidence_json,
8803
+ confidence = excluded.confidence,
8804
+ created_at = excluded.created_at`
8314
8805
  );
8315
8806
  for (const consumer of apiConsumers) {
8316
8807
  insertConsumer.run(
@@ -8373,7 +8864,7 @@ function rebuildOrgGraph(db, config, baseDirOrOptions) {
8373
8864
  }
8374
8865
 
8375
8866
  // src/org/index.ts
8376
- import fs13 from "fs";
8867
+ import fs14 from "fs";
8377
8868
  var ORG_SYNC_RESUME_WINDOW_MS = 12 * 60 * 60 * 1e3;
8378
8869
  function readCommit(runner, cwd) {
8379
8870
  try {
@@ -8417,14 +8908,42 @@ async function indexOrgRepos(db, config, options = {}) {
8417
8908
  const auth = options.token ? { token: options.token } : resolveGitHubToken();
8418
8909
  const results = [];
8419
8910
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8911
+ const startedAtMs = Date.now();
8420
8912
  const graphState = getOrgGraphState(db, config.org);
8421
- for (const repo of repos) {
8913
+ const command = options.command ?? "org index";
8914
+ const emit = (progress) => options.onLifecycleProgress?.(progress);
8915
+ emit({
8916
+ stage: "org_sync_started",
8917
+ org: config.org,
8918
+ command,
8919
+ totalRepos: repos.length
8920
+ });
8921
+ for (const [repoIndex, repo] of repos.entries()) {
8922
+ const repoPosition = repoIndex + 1;
8422
8923
  const localPath = orgRepoLocalPath(config.org, repo, options.baseDir);
8423
8924
  const repoStartedAt = (/* @__PURE__ */ new Date()).toISOString();
8925
+ const repoStartedAtMs = Date.now();
8424
8926
  let prsIndexed = 0;
8425
8927
  let codeFilesIndexed = 0;
8426
8928
  try {
8427
- if (!fs13.existsSync(localPath)) throw new Error(missingCloneError(repo.fullName, localPath));
8929
+ emit({
8930
+ stage: "org_repo_started",
8931
+ org: config.org,
8932
+ command,
8933
+ repo: repo.fullName,
8934
+ current: repoPosition,
8935
+ total: repos.length
8936
+ });
8937
+ if (!fs14.existsSync(localPath)) throw new Error(missingCloneError(repo.fullName, localPath));
8938
+ emit({
8939
+ stage: "org_repo_phase",
8940
+ org: config.org,
8941
+ command,
8942
+ repo: repo.fullName,
8943
+ current: repoPosition,
8944
+ total: repos.length,
8945
+ phase: "Reading current commit"
8946
+ });
8428
8947
  const currentCommit2 = readCommit(runner, localPath);
8429
8948
  const state = getOrgRepoState(db, config.org, repo.fullName);
8430
8949
  let history;
@@ -8442,6 +8961,15 @@ async function indexOrgRepos(db, config, options = {}) {
8442
8961
  })) {
8443
8962
  skippedHistory = true;
8444
8963
  historySkippedReason = "PR history already synced; resuming unfinished org graph/index work.";
8964
+ emit({
8965
+ stage: "org_repo_skipped_history",
8966
+ org: config.org,
8967
+ command,
8968
+ repo: repo.fullName,
8969
+ current: repoPosition,
8970
+ total: repos.length,
8971
+ reason: historySkippedReason
8972
+ });
8445
8973
  options.onFetchProgress?.({
8446
8974
  stage: "skipped_pull_request_fetch",
8447
8975
  repo: repo.fullName,
@@ -8453,6 +8981,15 @@ async function indexOrgRepos(db, config, options = {}) {
8453
8981
  );
8454
8982
  } else {
8455
8983
  try {
8984
+ emit({
8985
+ stage: "org_repo_phase",
8986
+ org: config.org,
8987
+ command,
8988
+ repo: repo.fullName,
8989
+ current: repoPosition,
8990
+ total: repos.length,
8991
+ phase: "Fetching PR history"
8992
+ });
8456
8993
  const since = options.since ?? (options.command === "org sync" ? state?.lastPrSyncAt ?? getLastSyncTime(db, repo.fullName) : void 0);
8457
8994
  const pullRequests = await fetchPullRequests({
8458
8995
  token: auth.token,
@@ -8462,6 +8999,16 @@ async function indexOrgRepos(db, config, options = {}) {
8462
8999
  detailConcurrency: options.concurrency,
8463
9000
  onProgress: options.onFetchProgress
8464
9001
  });
9002
+ emit({
9003
+ stage: "org_repo_phase",
9004
+ org: config.org,
9005
+ command,
9006
+ repo: repo.fullName,
9007
+ current: repoPosition,
9008
+ total: repos.length,
9009
+ phase: "Indexing PR history into SQLite",
9010
+ detail: `${pullRequests.length} PR(s)`
9011
+ });
8465
9012
  history = indexPullRequests(db, pullRequests, {
8466
9013
  cwd: localPath,
8467
9014
  repo: repo.fullName,
@@ -8486,6 +9033,15 @@ async function indexOrgRepos(db, config, options = {}) {
8486
9033
  }
8487
9034
  const codeUnchanged = !options.force && currentCommit2 && state?.lastCodeIndexedCommit && currentCommit2 === state.lastCodeIndexedCommit;
8488
9035
  if (!options.prsOnly && !codeUnchanged) {
9036
+ emit({
9037
+ stage: "org_repo_phase",
9038
+ org: config.org,
9039
+ command,
9040
+ repo: repo.fullName,
9041
+ current: repoPosition,
9042
+ total: repos.length,
9043
+ phase: "Indexing code and architecture"
9044
+ });
8489
9045
  code = indexCodebase(db, {
8490
9046
  cwd: localPath,
8491
9047
  repo: repo.fullName,
@@ -8501,6 +9057,26 @@ async function indexOrgRepos(db, config, options = {}) {
8501
9057
  lastCodeIndexedCommit: currentCommit2,
8502
9058
  lastCodeIndexedAt: (/* @__PURE__ */ new Date()).toISOString()
8503
9059
  });
9060
+ } else if (!options.prsOnly && codeUnchanged) {
9061
+ emit({
9062
+ stage: "org_repo_skipped_code",
9063
+ org: config.org,
9064
+ command,
9065
+ repo: repo.fullName,
9066
+ current: repoPosition,
9067
+ total: repos.length,
9068
+ reason: "Code skipped: current commit already indexed."
9069
+ });
9070
+ } else if (options.prsOnly) {
9071
+ emit({
9072
+ stage: "org_repo_skipped_code",
9073
+ org: config.org,
9074
+ command,
9075
+ repo: repo.fullName,
9076
+ current: repoPosition,
9077
+ total: repos.length,
9078
+ reason: "Code skipped because --prs-only was passed."
9079
+ });
8504
9080
  }
8505
9081
  if (repoFailures.length > 0) {
8506
9082
  updateOrgRepoState(db, {
@@ -8512,6 +9088,14 @@ async function indexOrgRepos(db, config, options = {}) {
8512
9088
  lastError: repoFailures.join("; ")
8513
9089
  });
8514
9090
  }
9091
+ emit({
9092
+ stage: "org_repo_finalizing",
9093
+ org: config.org,
9094
+ command,
9095
+ repo: repo.fullName,
9096
+ current: repoPosition,
9097
+ total: repos.length
9098
+ });
8515
9099
  results.push({
8516
9100
  repo: repo.fullName,
8517
9101
  skippedCode: Boolean(codeUnchanged || options.prsOnly),
@@ -8525,7 +9109,7 @@ async function indexOrgRepos(db, config, options = {}) {
8525
9109
  recordOrgIndexRun(db, {
8526
9110
  org: config.org,
8527
9111
  repo: repo.fullName,
8528
- command: options.command ?? "org index",
9112
+ command,
8529
9113
  startedAt: repoStartedAt,
8530
9114
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8531
9115
  status: repoFailures.length > 0 ? "partial" : "success",
@@ -8533,6 +9117,20 @@ async function indexOrgRepos(db, config, options = {}) {
8533
9117
  codeFilesIndexed,
8534
9118
  failures: repoFailures
8535
9119
  });
9120
+ emit({
9121
+ stage: "org_repo_completed",
9122
+ org: config.org,
9123
+ command,
9124
+ repo: repo.fullName,
9125
+ current: repoPosition,
9126
+ total: repos.length,
9127
+ skippedHistory,
9128
+ skippedCode: Boolean(codeUnchanged || options.prsOnly),
9129
+ prsIndexed,
9130
+ codeFilesIndexed,
9131
+ durationMs: Date.now() - repoStartedAtMs,
9132
+ error: repoFailures.join("; ") || void 0
9133
+ });
8536
9134
  } catch (error) {
8537
9135
  const message = error instanceof Error ? error.message : String(error);
8538
9136
  updateOrgRepoState(db, {
@@ -8545,7 +9143,7 @@ async function indexOrgRepos(db, config, options = {}) {
8545
9143
  recordOrgIndexRun(db, {
8546
9144
  org: config.org,
8547
9145
  repo: repo.fullName,
8548
- command: options.command ?? "org index",
9146
+ command,
8549
9147
  startedAt: repoStartedAt,
8550
9148
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8551
9149
  status: "failed",
@@ -8556,6 +9154,20 @@ async function indexOrgRepos(db, config, options = {}) {
8556
9154
  skippedCode: false,
8557
9155
  error: message
8558
9156
  });
9157
+ emit({
9158
+ stage: "org_repo_completed",
9159
+ org: config.org,
9160
+ command,
9161
+ repo: repo.fullName,
9162
+ current: repoPosition,
9163
+ total: repos.length,
9164
+ skippedHistory: false,
9165
+ skippedCode: false,
9166
+ prsIndexed,
9167
+ codeFilesIndexed,
9168
+ durationMs: Date.now() - repoStartedAtMs,
9169
+ error: message
9170
+ });
8559
9171
  }
8560
9172
  }
8561
9173
  let graph;
@@ -8568,6 +9180,12 @@ async function indexOrgRepos(db, config, options = {}) {
8568
9180
  apiContractCount: counts.apiContracts,
8569
9181
  apiConsumerCount: counts.apiConsumers
8570
9182
  });
9183
+ emit({
9184
+ stage: "org_graph_skipped",
9185
+ org: config.org,
9186
+ command,
9187
+ reason: "Graph skipped because --no-graph was passed."
9188
+ });
8571
9189
  graph = { ...counts, skipped: true };
8572
9190
  } else {
8573
9191
  try {
@@ -8588,7 +9206,7 @@ async function indexOrgRepos(db, config, options = {}) {
8588
9206
  }
8589
9207
  recordOrgIndexRun(db, {
8590
9208
  org: config.org,
8591
- command: options.command ?? "org index",
9209
+ command,
8592
9210
  startedAt,
8593
9211
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
8594
9212
  status: results.some((result) => result.error) || graph.error ? "partial" : "success",
@@ -8596,6 +9214,15 @@ async function indexOrgRepos(db, config, options = {}) {
8596
9214
  codeFilesIndexed: results.reduce((sum, result) => sum + (result.code?.indexedFiles ?? 0), 0),
8597
9215
  failures: results.map((result) => result.error).concat(graph.error ? [graph.error] : []).filter((error) => Boolean(error))
8598
9216
  });
9217
+ emit({
9218
+ stage: "org_sync_completed",
9219
+ org: config.org,
9220
+ command,
9221
+ totalRepos: repos.length,
9222
+ succeededRepos: results.filter((result) => !result.error).length,
9223
+ failedRepos: results.filter((result) => result.error).length,
9224
+ durationMs: Date.now() - startedAtMs
9225
+ });
8599
9226
  return {
8600
9227
  org: config.org,
8601
9228
  repos: results.sort((a, b) => a.repo.localeCompare(b.repo)),
@@ -9220,8 +9847,8 @@ function isTestPath2(filePath) {
9220
9847
  }
9221
9848
 
9222
9849
  // src/doctor.ts
9223
- import fs14 from "fs";
9224
- import path23 from "path";
9850
+ import fs15 from "fs";
9851
+ import path24 from "path";
9225
9852
  function check(name, ok, message, fix) {
9226
9853
  return { name, ok, message, fix: ok ? void 0 : fix };
9227
9854
  }
@@ -9325,12 +9952,12 @@ async function runDoctor(options) {
9325
9952
  )
9326
9953
  );
9327
9954
  }
9328
- const cursorConfigPath = path23.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9955
+ const cursorConfigPath = path24.join(gitRoot ?? cwd, ".cursor", "mcp.json");
9329
9956
  let cursorConfig;
9330
9957
  let cursorConfigValid = false;
9331
- if (fs14.existsSync(cursorConfigPath)) {
9958
+ if (fs15.existsSync(cursorConfigPath)) {
9332
9959
  try {
9333
- cursorConfig = JSON.parse(fs14.readFileSync(cursorConfigPath, "utf8"));
9960
+ cursorConfig = JSON.parse(fs15.readFileSync(cursorConfigPath, "utf8"));
9334
9961
  cursorConfigValid = true;
9335
9962
  } catch {
9336
9963
  cursorConfigValid = false;
@@ -9339,7 +9966,7 @@ async function runDoctor(options) {
9339
9966
  checks.push(
9340
9967
  check(
9341
9968
  ".cursor/mcp.json valid",
9342
- fs14.existsSync(cursorConfigPath) && cursorConfigValid,
9969
+ fs15.existsSync(cursorConfigPath) && cursorConfigValid,
9343
9970
  cursorConfigValid ? ".cursor/mcp.json exists and is valid JSON." : ".cursor/mcp.json is missing or invalid.",
9344
9971
  "Run anchor init. If the file is malformed, fix the JSON and rerun anchor init."
9345
9972
  )
@@ -9356,7 +9983,7 @@ async function runDoctor(options) {
9356
9983
  )
9357
9984
  );
9358
9985
  const dbPath = defaultDatabasePath(gitRoot ?? cwd);
9359
- const dbExists = fs14.existsSync(dbPath);
9986
+ const dbExists = fs15.existsSync(dbPath);
9360
9987
  checks.push(
9361
9988
  check(
9362
9989
  ".anchor/index.sqlite exists",
@@ -9400,12 +10027,12 @@ async function runDoctor(options) {
9400
10027
  "Run pnpm build, then try anchor serve from the repository."
9401
10028
  )
9402
10029
  );
9403
- const rulePath = path23.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
10030
+ const rulePath = path24.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
9404
10031
  checks.push(
9405
10032
  check(
9406
10033
  "Cursor rule file exists",
9407
- fs14.existsSync(rulePath),
9408
- fs14.existsSync(rulePath) ? "Cursor rule file exists." : "Cursor rule file is missing.",
10034
+ fs15.existsSync(rulePath),
10035
+ fs15.existsSync(rulePath) ? "Cursor rule file exists." : "Cursor rule file is missing.",
9409
10036
  "Run anchor init to create .cursor/rules/anchor.mdc."
9410
10037
  )
9411
10038
  );
@@ -9482,6 +10109,7 @@ export {
9482
10109
  clampMaxResults,
9483
10110
  classifyArchitectureArea,
9484
10111
  clearGraphQLFetchCheckpoint,
10112
+ clearOrgHeartbeat,
9485
10113
  clipSentence,
9486
10114
  cloneOrPullOrgRepo,
9487
10115
  cloneOrgRepos,
@@ -9568,10 +10196,13 @@ export {
9568
10196
  mergeAnchorMcpConfig,
9569
10197
  normalizePullRequest,
9570
10198
  openAnchorDatabase,
10199
+ openAnchorDatabaseReadOnly,
9571
10200
  openOrgDatabase,
10201
+ openOrgDatabaseReadOnly,
9572
10202
  orgCloneStateFromResult,
9573
10203
  orgConfigPath,
9574
10204
  orgDatabasePath,
10205
+ orgHeartbeatPath,
9575
10206
  orgRepoLocalPath,
9576
10207
  orgReposRoot,
9577
10208
  orgRoot,
@@ -9585,6 +10216,7 @@ export {
9585
10216
  rankRelevantTests,
9586
10217
  rankTeamRules,
9587
10218
  rankWisdomUnits,
10219
+ readOrgHeartbeat,
9588
10220
  rebuildOrgGraph,
9589
10221
  recordFeedback,
9590
10222
  recordIndexRun,
@@ -9628,6 +10260,7 @@ export {
9628
10260
  validateOrgRepoFullName,
9629
10261
  validateOrgRepoGroup,
9630
10262
  validateTeamRulesFile,
9631
- watchCodebase
10263
+ watchCodebase,
10264
+ writeOrgHeartbeat
9632
10265
  };
9633
10266
  //# sourceMappingURL=index.js.map