@pratik7368patil/anchor-core 0.1.11 → 0.1.13

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
@@ -376,6 +376,61 @@ CREATE TABLE IF NOT EXISTS code_index_state (
376
376
  skipped_files INTEGER NOT NULL
377
377
  );
378
378
 
379
+ CREATE TABLE IF NOT EXISTS code_imports (
380
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
381
+ repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
382
+ source_path TEXT NOT NULL,
383
+ specifier TEXT NOT NULL,
384
+ imported_path TEXT,
385
+ imported_symbols_json TEXT NOT NULL,
386
+ kind TEXT NOT NULL
387
+ );
388
+
389
+ CREATE TABLE IF NOT EXISTS architecture_components (
390
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
391
+ repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
392
+ path TEXT NOT NULL,
393
+ area TEXT NOT NULL,
394
+ kind TEXT NOT NULL,
395
+ language TEXT,
396
+ symbols_json TEXT NOT NULL,
397
+ imports_json TEXT NOT NULL,
398
+ related_tests_json TEXT NOT NULL,
399
+ confidence REAL NOT NULL,
400
+ updated_at TEXT NOT NULL,
401
+ UNIQUE(repo_id, path)
402
+ );
403
+
404
+ CREATE TABLE IF NOT EXISTS architecture_patterns (
405
+ id TEXT PRIMARY KEY,
406
+ repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
407
+ repo TEXT NOT NULL,
408
+ area TEXT NOT NULL,
409
+ name TEXT NOT NULL,
410
+ summary_sanitized TEXT NOT NULL,
411
+ source_files_json TEXT NOT NULL,
412
+ symbols_json TEXT NOT NULL,
413
+ evidence_json TEXT NOT NULL,
414
+ confidence REAL NOT NULL,
415
+ created_at TEXT NOT NULL
416
+ );
417
+
418
+ CREATE VIRTUAL TABLE IF NOT EXISTS architecture_patterns_fts USING fts5(
419
+ patternId UNINDEXED,
420
+ summary,
421
+ area,
422
+ sourceFiles,
423
+ symbols
424
+ );
425
+
426
+ CREATE TABLE IF NOT EXISTS architecture_index_state (
427
+ repo TEXT PRIMARY KEY,
428
+ last_indexed_at TEXT NOT NULL,
429
+ components INTEGER NOT NULL,
430
+ patterns INTEGER NOT NULL,
431
+ imports INTEGER NOT NULL
432
+ );
433
+
379
434
  CREATE TABLE IF NOT EXISTS test_files (
380
435
  id INTEGER PRIMARY KEY AUTOINCREMENT,
381
436
  repo_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
@@ -450,6 +505,11 @@ CREATE INDEX IF NOT EXISTS idx_wisdom_units_category ON wisdom_units(category);
450
505
  CREATE INDEX IF NOT EXISTS idx_wisdom_units_pr ON wisdom_units(pr_id);
451
506
  CREATE INDEX IF NOT EXISTS idx_code_files_path ON code_files(path);
452
507
  CREATE INDEX IF NOT EXISTS idx_code_chunks_file_path ON code_chunks(file_path);
508
+ CREATE INDEX IF NOT EXISTS idx_code_imports_source ON code_imports(source_path);
509
+ CREATE INDEX IF NOT EXISTS idx_code_imports_imported ON code_imports(imported_path);
510
+ CREATE INDEX IF NOT EXISTS idx_architecture_components_path ON architecture_components(path);
511
+ CREATE INDEX IF NOT EXISTS idx_architecture_components_area ON architecture_components(area);
512
+ CREATE INDEX IF NOT EXISTS idx_architecture_patterns_area ON architecture_patterns(area);
453
513
  CREATE INDEX IF NOT EXISTS idx_test_files_path ON test_files(path);
454
514
  CREATE INDEX IF NOT EXISTS idx_test_links_source ON test_links(source_path);
455
515
  CREATE INDEX IF NOT EXISTS idx_test_links_test ON test_links(test_path);
@@ -1217,6 +1277,12 @@ function calculateCoverage(input) {
1217
1277
  } else {
1218
1278
  reasons.push("No regression memory indexed yet.");
1219
1279
  }
1280
+ if (input.architecturePatternCount > 0) {
1281
+ score += 10;
1282
+ reasons.push(`${input.architecturePatternCount} architecture patterns indexed.`);
1283
+ } else {
1284
+ reasons.push("No architecture patterns indexed yet.");
1285
+ }
1220
1286
  if (input.teamRuleCount > 0) {
1221
1287
  score += 5;
1222
1288
  reasons.push(`${input.teamRuleCount} team-approved rules available.`);
@@ -1267,7 +1333,9 @@ function checkSchema(db) {
1267
1333
  const code = db.prepare("SELECT name FROM sqlite_master WHERE name = ?").all("code_chunks");
1268
1334
  const tests = db.prepare("SELECT name FROM sqlite_master WHERE name = ?").all("test_files");
1269
1335
  const regressions = db.prepare("SELECT name FROM sqlite_master WHERE name = ?").all("regression_events");
1270
- return tables.length > 0 && wisdom.length > 0 && codeTables.length > 0 && code.length > 0 && tests.length > 0 && regressions.length > 0;
1336
+ const architecture = db.prepare("SELECT name FROM sqlite_master WHERE name = ?").all("architecture_patterns");
1337
+ const architectureFts = db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'virtual') AND name = ?").all("architecture_patterns_fts");
1338
+ return tables.length > 0 && wisdom.length > 0 && codeTables.length > 0 && code.length > 0 && tests.length > 0 && regressions.length > 0 && architecture.length > 0 && architectureFts.length > 0;
1271
1339
  } catch {
1272
1340
  return false;
1273
1341
  }
@@ -1494,7 +1562,7 @@ function upsertPullRequest(db, pr, wisdomUnits, regressionEvents = []) {
1494
1562
  regressions: regressionEvents.length
1495
1563
  };
1496
1564
  }
1497
- function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd) {
1565
+ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd, architecture = { components: [], patterns: [], imports: [] }) {
1498
1566
  initializeSchema(db);
1499
1567
  const repoId = ensureRepository(db, repo);
1500
1568
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -1507,6 +1575,7 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd) {
1507
1575
  db.prepare("DELETE FROM code_files WHERE repo_id = ?").run(repoId);
1508
1576
  db.prepare("DELETE FROM test_links WHERE repo_id = ? AND reason != 'PR co-change'").run(repoId);
1509
1577
  db.prepare("DELETE FROM test_files WHERE repo_id = ?").run(repoId);
1578
+ deleteExistingArchitectureData(db, repoId);
1510
1579
  const insertFile = db.prepare(
1511
1580
  `INSERT INTO code_files
1512
1581
  (repo_id, path, language, size_bytes, content_hash, updated_at)
@@ -1561,6 +1630,7 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd) {
1561
1630
  );
1562
1631
  }
1563
1632
  insertTestAwareness(db, repoId, testAwareness.testFiles, testAwareness.testLinks);
1633
+ insertArchitectureData(db, repoId, architecture);
1564
1634
  db.prepare(
1565
1635
  `INSERT INTO code_index_state (repo, last_indexed_at, indexed_files, code_chunks, skipped_files)
1566
1636
  VALUES (?, ?, ?, ?, ?)
@@ -1570,6 +1640,21 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd) {
1570
1640
  code_chunks = excluded.code_chunks,
1571
1641
  skipped_files = excluded.skipped_files`
1572
1642
  ).run(repo, now, codeFiles.length, codeChunks.length, skippedFiles);
1643
+ db.prepare(
1644
+ `INSERT INTO architecture_index_state (repo, last_indexed_at, components, patterns, imports)
1645
+ VALUES (?, ?, ?, ?, ?)
1646
+ ON CONFLICT(repo) DO UPDATE SET
1647
+ last_indexed_at = excluded.last_indexed_at,
1648
+ components = excluded.components,
1649
+ patterns = excluded.patterns,
1650
+ imports = excluded.imports`
1651
+ ).run(
1652
+ repo,
1653
+ now,
1654
+ architecture.components.length,
1655
+ architecture.patterns.length,
1656
+ architecture.imports.length
1657
+ );
1573
1658
  });
1574
1659
  transaction();
1575
1660
  return {
@@ -1577,10 +1662,90 @@ function replaceCodeIndex(db, repo, codeFiles, codeChunks, skippedFiles, cwd) {
1577
1662
  codeChunksCreated: codeChunks.length,
1578
1663
  testFilesIndexed: testAwareness.testFiles.length,
1579
1664
  testLinksCreated: testAwareness.testLinks.length,
1665
+ architectureComponentsIndexed: architecture.components.length,
1666
+ architecturePatternsIndexed: architecture.patterns.length,
1667
+ architectureImportsIndexed: architecture.imports.length,
1580
1668
  skippedFiles,
1581
1669
  databasePath: defaultDatabasePath(cwd)
1582
1670
  };
1583
1671
  }
1672
+ function deleteExistingArchitectureData(db, repoId) {
1673
+ const patternRows = db.prepare("SELECT id FROM architecture_patterns WHERE repo_id = ?").all(repoId);
1674
+ const deleteFts = db.prepare("DELETE FROM architecture_patterns_fts WHERE patternId = ?");
1675
+ for (const row of patternRows) deleteFts.run(row.id);
1676
+ db.prepare("DELETE FROM architecture_patterns WHERE repo_id = ?").run(repoId);
1677
+ db.prepare("DELETE FROM architecture_components WHERE repo_id = ?").run(repoId);
1678
+ db.prepare("DELETE FROM code_imports WHERE repo_id = ?").run(repoId);
1679
+ }
1680
+ function insertArchitectureData(db, repoId, architecture) {
1681
+ const insertImport = db.prepare(
1682
+ `INSERT INTO code_imports
1683
+ (repo_id, source_path, specifier, imported_path, imported_symbols_json, kind)
1684
+ VALUES (?, ?, ?, ?, ?, ?)`
1685
+ );
1686
+ for (const item of architecture.imports) {
1687
+ insertImport.run(
1688
+ repoId,
1689
+ item.sourcePath,
1690
+ item.specifier,
1691
+ item.importedPath ?? null,
1692
+ JSON.stringify(item.importedSymbols),
1693
+ item.kind
1694
+ );
1695
+ }
1696
+ const insertComponent = db.prepare(
1697
+ `INSERT INTO architecture_components
1698
+ (repo_id, path, area, kind, language, symbols_json, imports_json, related_tests_json,
1699
+ confidence, updated_at)
1700
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1701
+ );
1702
+ for (const component of architecture.components) {
1703
+ insertComponent.run(
1704
+ repoId,
1705
+ component.path,
1706
+ component.area,
1707
+ component.kind,
1708
+ component.language ?? null,
1709
+ JSON.stringify(component.symbols),
1710
+ JSON.stringify(component.imports),
1711
+ JSON.stringify(component.relatedTests),
1712
+ component.confidence,
1713
+ component.updatedAt
1714
+ );
1715
+ }
1716
+ const insertPattern = db.prepare(
1717
+ `INSERT INTO architecture_patterns
1718
+ (id, repo_id, repo, area, name, summary_sanitized, source_files_json, symbols_json,
1719
+ evidence_json, confidence, created_at)
1720
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1721
+ );
1722
+ const insertFts = db.prepare(
1723
+ `INSERT INTO architecture_patterns_fts (patternId, summary, area, sourceFiles, symbols)
1724
+ VALUES (?, ?, ?, ?, ?)`
1725
+ );
1726
+ for (const pattern of architecture.patterns) {
1727
+ insertPattern.run(
1728
+ pattern.id,
1729
+ repoId,
1730
+ pattern.repo,
1731
+ pattern.area,
1732
+ pattern.name,
1733
+ pattern.sanitizedSummary,
1734
+ JSON.stringify(pattern.sourceFiles),
1735
+ JSON.stringify(pattern.symbols),
1736
+ JSON.stringify(pattern.evidence),
1737
+ pattern.confidence,
1738
+ pattern.createdAt
1739
+ );
1740
+ insertFts.run(
1741
+ pattern.id,
1742
+ pattern.sanitizedSummary,
1743
+ pattern.area,
1744
+ pattern.sourceFiles.join(" "),
1745
+ pattern.symbols.join(" ")
1746
+ );
1747
+ }
1748
+ }
1584
1749
  function insertPrCochangeTestLinks(db, repoId, filePaths) {
1585
1750
  const testPaths = filePaths.filter(isTestFilePath);
1586
1751
  const sourcePaths = filePaths.filter((filePath) => !isTestFilePath(filePath));
@@ -1649,6 +1814,7 @@ function withCoverage(status) {
1649
1814
  codeChunkCount: status.codeChunkCount,
1650
1815
  testLinkCount: status.testLinkCount,
1651
1816
  regressionEventCount: status.regressionEventCount,
1817
+ architecturePatternCount: status.architecturePatternCount,
1652
1818
  teamRuleCount: status.teamRuleCount,
1653
1819
  historyCoverage: status.historyCoverage,
1654
1820
  staleEvidenceCount: status.staleEvidenceCount,
@@ -1670,6 +1836,9 @@ function getIndexStatus(cwd, githubTokenConfigured = Boolean(resolveGitHubToken(
1670
1836
  testFileCount: 0,
1671
1837
  testLinkCount: 0,
1672
1838
  regressionEventCount: 0,
1839
+ architectureComponentCount: 0,
1840
+ architecturePatternCount: 0,
1841
+ architectureImportCount: 0,
1673
1842
  historyCoverage: "unknown",
1674
1843
  staleEvidenceCount: 0,
1675
1844
  teamRuleCount: rules.count,
@@ -1695,6 +1864,9 @@ function getIndexStatus(cwd, githubTokenConfigured = Boolean(resolveGitHubToken(
1695
1864
  testFileCount: 0,
1696
1865
  testLinkCount: 0,
1697
1866
  regressionEventCount: 0,
1867
+ architectureComponentCount: 0,
1868
+ architecturePatternCount: 0,
1869
+ architectureImportCount: 0,
1698
1870
  historyCoverage: "unknown",
1699
1871
  staleEvidenceCount: 0,
1700
1872
  teamRuleCount: rules2.count,
@@ -1710,6 +1882,9 @@ function getIndexStatus(cwd, githubTokenConfigured = Boolean(resolveGitHubToken(
1710
1882
  "SELECT last_sync_at, history_coverage, history_limit FROM sync_state ORDER BY updated_at DESC LIMIT 1"
1711
1883
  ).get();
1712
1884
  const codeIndexRow = db.prepare("SELECT last_indexed_at FROM code_index_state ORDER BY last_indexed_at DESC LIMIT 1").get();
1885
+ const architectureIndexRow = db.prepare(
1886
+ "SELECT last_indexed_at FROM architecture_index_state ORDER BY last_indexed_at DESC LIMIT 1"
1887
+ ).get();
1713
1888
  const wisdomUnitCount = count("wisdom_units");
1714
1889
  const codeChunkCount = count("code_chunks");
1715
1890
  const lastSuccessfulRun = db.prepare(
@@ -1733,12 +1908,16 @@ function getIndexStatus(cwd, githubTokenConfigured = Boolean(resolveGitHubToken(
1733
1908
  testFileCount: count("test_files"),
1734
1909
  testLinkCount: count("test_links"),
1735
1910
  regressionEventCount: count("regression_events"),
1911
+ architectureComponentCount: count("architecture_components"),
1912
+ architecturePatternCount: count("architecture_patterns"),
1913
+ architectureImportCount: count("code_imports"),
1736
1914
  historyCoverage: syncRow?.history_coverage ?? "unknown",
1737
1915
  historyLimit: syncRow?.history_limit ?? void 0,
1738
1916
  staleEvidenceCount: countStaleEvidence(db),
1739
1917
  teamRuleCount: rules.count,
1740
1918
  lastSyncTime: syncRow?.last_sync_at ?? void 0,
1741
1919
  lastCodeIndexTime: codeIndexRow?.last_indexed_at ?? void 0,
1920
+ lastArchitectureIndexTime: architectureIndexRow?.last_indexed_at ?? void 0,
1742
1921
  lastRuleIndexTime: rules.lastRuleIndexTime,
1743
1922
  lastSuccessfulRun: lastSuccessfulRun?.finished_at ?? void 0,
1744
1923
  lastFailedRun: lastFailedRun?.finished_at ?? void 0,
@@ -1903,11 +2082,269 @@ function chunkCodeFile(file, options = {}) {
1903
2082
  return chunks;
1904
2083
  }
1905
2084
 
2085
+ // src/indexer/architecture-indexer.ts
2086
+ import crypto2 from "crypto";
2087
+ import path6 from "path";
2088
+ var KNOWN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json"];
2089
+ function classifyArchitectureArea(filePath, language, content = "") {
2090
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase();
2091
+ const basename = path6.basename(normalized);
2092
+ if (isTestFilePath(normalized)) return "test";
2093
+ if (/\b(route|routes|router|pages|app)\b/.test(normalized) || basename === "route.ts") {
2094
+ return "route";
2095
+ }
2096
+ if (/\/(api|apis)\//.test(normalized) || /\b(api|client|request|graphql|rest)\b/.test(basename)) {
2097
+ return "api";
2098
+ }
2099
+ if (/\/(services?|clients?|repositories?)\//.test(normalized)) return "service";
2100
+ if (/\/(hooks?)\//.test(normalized) || /^use[A-Z]/.test(path6.basename(filePath))) return "hook";
2101
+ if (/\/(components?|ui)\//.test(normalized) || language === "tsx" || /\bjsx?\b/.test(language ?? "")) {
2102
+ return "component";
2103
+ }
2104
+ if (/\/(stores?|state|redux|zustand)\//.test(normalized)) return "store";
2105
+ if (/\/(schemas?|validation|validators?)\//.test(normalized) || /\b(schema|zod)\b/.test(content)) {
2106
+ return "schema";
2107
+ }
2108
+ if (/\/(types?|interfaces?|models?)\//.test(normalized) || normalized.endsWith(".d.ts")) {
2109
+ return "type";
2110
+ }
2111
+ if (/\/(configs?|settings)\//.test(normalized) || /\b(config|rc)\b/.test(basename)) {
2112
+ return "config";
2113
+ }
2114
+ if (/\/(utils?|helpers?|lib)\//.test(normalized)) return "util";
2115
+ return "unknown";
2116
+ }
2117
+ function stablePatternId(repo, area, name, sourceFiles) {
2118
+ const hash = crypto2.createHash("sha256").update([repo, area, name, ...sourceFiles].join("\0")).digest("hex").slice(0, 24);
2119
+ return `ap_${hash}`;
2120
+ }
2121
+ function parseImportedSymbols(importClause) {
2122
+ const symbols = [];
2123
+ const named = importClause.match(/\{([^}]+)\}/)?.[1];
2124
+ if (named) {
2125
+ for (const item of named.split(",")) {
2126
+ const symbol = item.trim().split(/\s+as\s+/i)[0]?.trim();
2127
+ if (symbol) symbols.push(symbol);
2128
+ }
2129
+ }
2130
+ const defaultImport = importClause.replace(/\{[^}]+\}/g, "").split(",")[0]?.trim().replace(/^type\s+/, "");
2131
+ if (defaultImport && /^[A-Za-z_$][\w$]*$/.test(defaultImport)) symbols.push(defaultImport);
2132
+ return uniqueStrings(symbols).slice(0, 20);
2133
+ }
2134
+ function resolveRelativeImport(sourcePath, specifier, codePaths) {
2135
+ if (!specifier.startsWith(".")) return void 0;
2136
+ const sourceDir = path6.posix.dirname(sourcePath.replace(/\\/g, "/"));
2137
+ const base = path6.posix.normalize(path6.posix.join(sourceDir, specifier));
2138
+ const candidates = [
2139
+ base,
2140
+ ...KNOWN_EXTENSIONS.map((extension) => `${base}${extension}`),
2141
+ ...KNOWN_EXTENSIONS.map((extension) => path6.posix.join(base, `index${extension}`))
2142
+ ];
2143
+ return candidates.find((candidate) => codePaths.has(candidate));
2144
+ }
2145
+ function extractCodeImports(sourcePath, content, codePaths, repo = "") {
2146
+ const imports = [];
2147
+ const staticImports = content.matchAll(
2148
+ /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g
2149
+ );
2150
+ for (const match of staticImports) {
2151
+ const importClause = match[1] ?? "";
2152
+ const specifier = match[2] ?? "";
2153
+ const sanitizedSpecifier = sanitizeHistoricalText(specifier);
2154
+ imports.push({
2155
+ repo,
2156
+ sourcePath,
2157
+ specifier: sanitizedSpecifier,
2158
+ importedPath: resolveRelativeImport(sourcePath, specifier, codePaths),
2159
+ importedSymbols: parseImportedSymbols(importClause),
2160
+ kind: "static"
2161
+ });
2162
+ }
2163
+ const dynamicImports = content.matchAll(/import\s*\(\s*["']([^"']+)["']\s*\)/g);
2164
+ for (const match of dynamicImports) {
2165
+ const specifier = match[1] ?? "";
2166
+ const sanitizedSpecifier = sanitizeHistoricalText(specifier);
2167
+ imports.push({
2168
+ repo,
2169
+ sourcePath,
2170
+ specifier: sanitizedSpecifier,
2171
+ importedPath: resolveRelativeImport(sourcePath, specifier, codePaths),
2172
+ importedSymbols: [],
2173
+ kind: "dynamic"
2174
+ });
2175
+ }
2176
+ const requireImports = content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g);
2177
+ for (const match of requireImports) {
2178
+ const specifier = match[1] ?? "";
2179
+ const sanitizedSpecifier = sanitizeHistoricalText(specifier);
2180
+ imports.push({
2181
+ repo,
2182
+ sourcePath,
2183
+ specifier: sanitizedSpecifier,
2184
+ importedPath: resolveRelativeImport(sourcePath, specifier, codePaths),
2185
+ importedSymbols: [],
2186
+ kind: "require"
2187
+ });
2188
+ }
2189
+ const seen = /* @__PURE__ */ new Set();
2190
+ return imports.filter((item) => {
2191
+ const key = `${item.sourcePath}:${item.specifier}:${item.kind}`;
2192
+ if (seen.has(key)) return false;
2193
+ seen.add(key);
2194
+ return true;
2195
+ });
2196
+ }
2197
+ function relatedTestsFor(filePath, allPaths) {
2198
+ if (isTestFilePath(filePath)) return [];
2199
+ const parsed = path6.posix.parse(filePath);
2200
+ const basename = parsed.name.replace(/\.(test|spec)$/i, "");
2201
+ return allPaths.filter((candidate) => isTestFilePath(candidate)).filter((candidate) => {
2202
+ const candidateParsed = path6.posix.parse(candidate);
2203
+ const candidateBase = candidateParsed.name.replace(/\.(test|spec)$/i, "");
2204
+ return candidateBase === basename || candidate.startsWith(`${parsed.dir}/`) || candidate.includes(`/${basename}.`);
2205
+ }).slice(0, 8);
2206
+ }
2207
+ function directoryLabel(filePath) {
2208
+ const directory = path6.posix.dirname(filePath.replace(/\\/g, "/"));
2209
+ return directory === "." ? "repo root" : directory;
2210
+ }
2211
+ function topDirectories(files) {
2212
+ const counts = /* @__PURE__ */ new Map();
2213
+ for (const file of files) {
2214
+ const directory = directoryLabel(file);
2215
+ counts.set(directory, (counts.get(directory) ?? 0) + 1);
2216
+ }
2217
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 4).map(([directory]) => directory);
2218
+ }
2219
+ function createPattern(input) {
2220
+ const sourceFiles = uniqueStrings(input.sourceFiles).slice(0, 12);
2221
+ const sanitizedSummary = sanitizeHistoricalText(input.summary);
2222
+ return {
2223
+ id: stablePatternId(input.repo, input.area, input.name, sourceFiles),
2224
+ repo: input.repo,
2225
+ area: input.area,
2226
+ name: input.name,
2227
+ summary: sanitizedSummary,
2228
+ sanitizedSummary,
2229
+ sourceFiles,
2230
+ symbols: uniqueStrings(input.symbols).slice(0, 30),
2231
+ evidence: [],
2232
+ confidence: Number(Math.min(0.95, Math.max(0.35, input.confidence)).toFixed(2)),
2233
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2234
+ };
2235
+ }
2236
+ function buildArchitectureIndex(repo, files, chunks) {
2237
+ const allPaths = files.map((file) => file.path);
2238
+ const codePaths = new Set(allPaths);
2239
+ const symbolsByPath = /* @__PURE__ */ new Map();
2240
+ for (const chunk of chunks) {
2241
+ const existing = symbolsByPath.get(chunk.filePath) ?? [];
2242
+ symbolsByPath.set(chunk.filePath, uniqueStrings([...existing, ...chunk.symbols]).slice(0, 40));
2243
+ }
2244
+ const imports = files.flatMap(
2245
+ (file) => extractCodeImports(file.path, file.content, codePaths, repo)
2246
+ );
2247
+ const importsByPath = /* @__PURE__ */ new Map();
2248
+ for (const item of imports) {
2249
+ const existing = importsByPath.get(item.sourcePath) ?? [];
2250
+ existing.push(item);
2251
+ importsByPath.set(item.sourcePath, existing);
2252
+ }
2253
+ const components = files.map((file) => {
2254
+ const area = classifyArchitectureArea(file.path, file.language, file.content);
2255
+ const fileImports = importsByPath.get(file.path) ?? [];
2256
+ const symbols = symbolsByPath.get(file.path) ?? [];
2257
+ return {
2258
+ repo,
2259
+ path: file.path,
2260
+ area,
2261
+ kind: area,
2262
+ language: file.language,
2263
+ symbols,
2264
+ imports: uniqueStrings(
2265
+ fileImports.map((item) => item.importedPath ?? item.specifier).filter(Boolean)
2266
+ ).slice(0, 20),
2267
+ relatedTests: relatedTestsFor(file.path, allPaths),
2268
+ confidence: area === "unknown" ? 0.45 : 0.82,
2269
+ updatedAt: file.updatedAt
2270
+ };
2271
+ });
2272
+ const componentByPath = new Map(components.map((component) => [component.path, component]));
2273
+ const patterns = [];
2274
+ const componentsByArea = /* @__PURE__ */ new Map();
2275
+ for (const component of components) {
2276
+ const existing = componentsByArea.get(component.area) ?? [];
2277
+ existing.push(component);
2278
+ componentsByArea.set(component.area, existing);
2279
+ }
2280
+ for (const [area, areaComponents] of componentsByArea.entries()) {
2281
+ const filesForArea = areaComponents.map((component) => component.path);
2282
+ const directories = topDirectories(filesForArea);
2283
+ const symbols = areaComponents.flatMap((component) => component.symbols);
2284
+ patterns.push(
2285
+ createPattern({
2286
+ repo,
2287
+ area,
2288
+ name: `${area} area placement`,
2289
+ summary: `${area} code is represented by ${filesForArea.length} file(s), commonly under ${directories.join(", ")}. Use these files as current architecture evidence before adding or changing similar code.`,
2290
+ sourceFiles: filesForArea,
2291
+ symbols,
2292
+ confidence: 0.55 + Math.min(0.3, filesForArea.length * 0.04)
2293
+ })
2294
+ );
2295
+ }
2296
+ const importDirectionCounts = /* @__PURE__ */ new Map();
2297
+ for (const item of imports) {
2298
+ if (!item.importedPath) continue;
2299
+ const source = componentByPath.get(item.sourcePath);
2300
+ const target = componentByPath.get(item.importedPath);
2301
+ if (!source || !target || source.area === target.area) continue;
2302
+ const key = `${source.area}->${target.area}`;
2303
+ const existing = importDirectionCounts.get(key) ?? { count: 0, files: [], symbols: [] };
2304
+ existing.count += 1;
2305
+ existing.files.push(source.path, target.path);
2306
+ existing.symbols.push(...item.importedSymbols);
2307
+ importDirectionCounts.set(key, existing);
2308
+ }
2309
+ for (const [key, value] of importDirectionCounts.entries()) {
2310
+ const [sourceArea, targetArea] = key.split("->");
2311
+ patterns.push(
2312
+ createPattern({
2313
+ repo,
2314
+ area: sourceArea,
2315
+ name: `${sourceArea} imports ${targetArea}`,
2316
+ summary: `${sourceArea} files import ${targetArea} files in ${value.count} observed edge(s). Prefer this direction when adding similar code unless cited repo evidence says otherwise.`,
2317
+ sourceFiles: value.files,
2318
+ symbols: value.symbols,
2319
+ confidence: 0.62 + Math.min(0.25, value.count * 0.05)
2320
+ })
2321
+ );
2322
+ }
2323
+ const testedComponents = components.filter((component) => component.relatedTests.length > 0);
2324
+ if (testedComponents.length > 0) {
2325
+ patterns.push(
2326
+ createPattern({
2327
+ repo,
2328
+ area: "test",
2329
+ name: "source files have nearby tests",
2330
+ summary: `${testedComponents.length} source file(s) have nearby tests. When editing these areas, update or add sibling tests that match the existing placement.`,
2331
+ sourceFiles: testedComponents.flatMap((component) => [
2332
+ component.path,
2333
+ ...component.relatedTests
2334
+ ]),
2335
+ symbols: testedComponents.flatMap((component) => component.symbols),
2336
+ confidence: 0.72
2337
+ })
2338
+ );
2339
+ }
2340
+ return { components, patterns, imports };
2341
+ }
2342
+
1906
2343
  // src/indexer/code-file-discovery.ts
1907
2344
  import { execFileSync as execFileSync3 } from "child_process";
1908
- import crypto2 from "crypto";
2345
+ import crypto3 from "crypto";
1909
2346
  import fs4 from "fs";
1910
- import path6 from "path";
2347
+ import path7 from "path";
1911
2348
  var DEFAULT_MAX_CODE_FILE_BYTES = 512 * 1024;
1912
2349
  var HARD_EXCLUDED_SEGMENTS = /* @__PURE__ */ new Set([
1913
2350
  ".git",
@@ -1955,7 +2392,7 @@ function isHardExcludedCodePath(filePath) {
1955
2392
  const normalized = normalizeGitPath(filePath);
1956
2393
  const segments = normalized.split("/");
1957
2394
  if (segments.some((segment) => HARD_EXCLUDED_SEGMENTS.has(segment))) return true;
1958
- const basename = path6.posix.basename(normalized).toLowerCase();
2395
+ const basename = path7.posix.basename(normalized).toLowerCase();
1959
2396
  if ([".netrc", ".npmrc", ".pypirc", ".yarnrc"].includes(basename)) return true;
1960
2397
  if (basename === ".env" || basename.startsWith(".env.")) return true;
1961
2398
  if (basename === "id_rsa" || basename === "id_rsa.pub" || basename === "id_dsa" || basename === "id_ecdsa" || basename === "id_ed25519") {
@@ -1965,7 +2402,7 @@ function isHardExcludedCodePath(filePath) {
1965
2402
  return false;
1966
2403
  }
1967
2404
  function languageForPath(filePath) {
1968
- const extension = path6.extname(filePath).toLowerCase();
2405
+ const extension = path7.extname(filePath).toLowerCase();
1969
2406
  return LANGUAGE_BY_EXTENSION[extension];
1970
2407
  }
1971
2408
  function isProbablyBinary(buffer) {
@@ -1988,7 +2425,7 @@ function discoverGitFiles(cwd) {
1988
2425
  }
1989
2426
  function discoverCodeFiles(cwd, repo, options = {}) {
1990
2427
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_CODE_FILE_BYTES;
1991
- const rootPath = path6.resolve(cwd);
2428
+ const rootPath = path7.resolve(cwd);
1992
2429
  const files = [];
1993
2430
  let skippedFiles = 0;
1994
2431
  for (const filePath of discoverGitFiles(cwd)) {
@@ -1996,9 +2433,9 @@ function discoverCodeFiles(cwd, repo, options = {}) {
1996
2433
  skippedFiles += 1;
1997
2434
  continue;
1998
2435
  }
1999
- const absolutePath = path6.resolve(cwd, filePath);
2000
- const relativeToRoot = path6.relative(rootPath, absolutePath);
2001
- if (relativeToRoot.startsWith("..") || path6.isAbsolute(relativeToRoot)) {
2436
+ const absolutePath = path7.resolve(cwd, filePath);
2437
+ const relativeToRoot = path7.relative(rootPath, absolutePath);
2438
+ if (relativeToRoot.startsWith("..") || path7.isAbsolute(relativeToRoot)) {
2002
2439
  skippedFiles += 1;
2003
2440
  continue;
2004
2441
  }
@@ -2024,7 +2461,7 @@ function discoverCodeFiles(cwd, repo, options = {}) {
2024
2461
  path: filePath,
2025
2462
  language: languageForPath(filePath),
2026
2463
  sizeBytes: stat.size,
2027
- contentHash: crypto2.createHash("sha256").update(buffer).digest("hex"),
2464
+ contentHash: crypto3.createHash("sha256").update(buffer).digest("hex"),
2028
2465
  updatedAt: stat.mtime.toISOString(),
2029
2466
  absolutePath,
2030
2467
  content
@@ -2065,13 +2502,22 @@ function indexCodebase(db, options) {
2065
2502
  chunks: fileChunks.length
2066
2503
  });
2067
2504
  }
2505
+ const architecture = buildArchitectureIndex(options.repo, discovery.files, chunks);
2506
+ options.onProgress?.({
2507
+ stage: "indexed_architecture",
2508
+ repo: options.repo,
2509
+ components: architecture.components.length,
2510
+ patterns: architecture.patterns.length,
2511
+ imports: architecture.imports.length
2512
+ });
2068
2513
  return replaceCodeIndex(
2069
2514
  db,
2070
2515
  options.repo,
2071
2516
  discovery.files.map(({ content: _content, absolutePath: _absolutePath, ...file }) => file),
2072
2517
  chunks,
2073
2518
  discovery.skippedFiles,
2074
- options.cwd
2519
+ options.cwd,
2520
+ architecture
2075
2521
  );
2076
2522
  }
2077
2523
  function emptyCodeIndexSummary(cwd) {
@@ -2080,17 +2526,20 @@ function emptyCodeIndexSummary(cwd) {
2080
2526
  codeChunksCreated: 0,
2081
2527
  testFilesIndexed: 0,
2082
2528
  testLinksCreated: 0,
2529
+ architectureComponentsIndexed: 0,
2530
+ architecturePatternsIndexed: 0,
2531
+ architectureImportsIndexed: 0,
2083
2532
  skippedFiles: 0,
2084
2533
  databasePath: defaultDatabasePath(cwd)
2085
2534
  };
2086
2535
  }
2087
2536
 
2088
2537
  // src/indexer/regression-extractor.ts
2089
- import crypto4 from "crypto";
2538
+ import crypto5 from "crypto";
2090
2539
 
2091
2540
  // src/indexer/wisdom-extractor.ts
2092
- import crypto3 from "crypto";
2093
- import path7 from "path";
2541
+ import crypto4 from "crypto";
2542
+ import path8 from "path";
2094
2543
  var CATEGORY_KEYWORDS = [
2095
2544
  ["security_note", /\b(security|secret|token|bearer|oauth|credential|xss|csrf|injection|sanitize|redact)\b/i],
2096
2545
  ["architecture_decision", /\b(architecture decision|architectural|we intentionally|design decision)\b/i],
@@ -2122,7 +2571,7 @@ function extractSymbols(text, filePaths) {
2122
2571
  }
2123
2572
  }
2124
2573
  for (const filePath of filePaths) {
2125
- const basename = path7.basename(filePath).replace(/\.[^.]+$/, "");
2574
+ const basename = path8.basename(filePath).replace(/\.[^.]+$/, "");
2126
2575
  if (/^[A-Za-z_$][\w$]*$/.test(basename)) symbols.push(basename);
2127
2576
  }
2128
2577
  return uniqueStrings(symbols).slice(0, 30);
@@ -2146,7 +2595,7 @@ function confidenceFor(entry, text, category, duplicateCount) {
2146
2595
  return Math.max(0, Math.min(1, Number(confidence.toFixed(2))));
2147
2596
  }
2148
2597
  function stableWisdomId(pr, sourceType, text, filePaths, createdAt, authors) {
2149
- const hash = crypto3.createHash("sha256").update(
2598
+ const hash = crypto4.createHash("sha256").update(
2150
2599
  [pr.repo, pr.number, sourceType, canonicalizeText(text), filePaths.join("|"), createdAt, authors.join("|")].join(
2151
2600
  "\0"
2152
2601
  )
@@ -2297,7 +2746,7 @@ function sourceTexts(pr) {
2297
2746
  ].filter((text) => text.trim());
2298
2747
  }
2299
2748
  function stableRegressionId(pr, summary, signals) {
2300
- const hash = crypto4.createHash("sha256").update([pr.repo, pr.number, canonicalizeText(summary), signals.join("|")].join("\0")).digest("hex").slice(0, 24);
2749
+ const hash = crypto5.createHash("sha256").update([pr.repo, pr.number, canonicalizeText(summary), signals.join("|")].join("\0")).digest("hex").slice(0, 24);
2301
2750
  return `re_${hash}`;
2302
2751
  }
2303
2752
  function extractRegressionEvents(pr) {
@@ -2420,7 +2869,7 @@ function shouldSyncSince(db, repo, fallbackSince) {
2420
2869
  }
2421
2870
 
2422
2871
  // src/retrieval/query-builder.ts
2423
- import path8 from "path";
2872
+ import path9 from "path";
2424
2873
  var CATEGORY_HINTS = [
2425
2874
  "security",
2426
2875
  "regression",
@@ -2437,7 +2886,7 @@ function ftsToken(token) {
2437
2886
  return `${clean}*`;
2438
2887
  }
2439
2888
  function testFilenameHints(filePath) {
2440
- const parsed = path8.parse(filePath);
2889
+ const parsed = path9.parse(filePath);
2441
2890
  const base = parsed.name.replace(/\.(test|spec)$/i, "");
2442
2891
  return [`${base}.test${parsed.ext}`, `${base}.spec${parsed.ext}`];
2443
2892
  }
@@ -2467,9 +2916,9 @@ function buildQueryTerms(input) {
2467
2916
  const baseText = "task" in input ? input.task : input.query;
2468
2917
  const fileTerms = files.flatMap((file) => [
2469
2918
  file,
2470
- path8.basename(file),
2919
+ path9.basename(file),
2471
2920
  ...testFilenameHints(file),
2472
- ...path8.dirname(file).split(/[\\/]/).filter(Boolean)
2921
+ ...path9.dirname(file).split(/[\\/]/).filter(Boolean)
2473
2922
  ]);
2474
2923
  return uniqueStrings([
2475
2924
  ...tokenizeSearchText(baseText, 24),
@@ -2493,7 +2942,7 @@ function clampMaxResults(value, defaultValue) {
2493
2942
  }
2494
2943
 
2495
2944
  // src/retrieval/ranker.ts
2496
- import path9 from "path";
2945
+ import path10 from "path";
2497
2946
  function parseJsonArray3(value) {
2498
2947
  try {
2499
2948
  const parsed = JSON.parse(value);
@@ -2540,11 +2989,11 @@ function filePathMatch(unitPaths, queryFiles) {
2540
2989
  if (queryFiles.length === 0 || unitPaths.length === 0) return 0;
2541
2990
  let best = 0;
2542
2991
  for (const queryFile of queryFiles) {
2543
- const queryBase = path9.basename(queryFile).toLowerCase();
2544
- const queryDir = path9.dirname(queryFile).toLowerCase();
2992
+ const queryBase = path10.basename(queryFile).toLowerCase();
2993
+ const queryDir = path10.dirname(queryFile).toLowerCase();
2545
2994
  for (const unitPath of unitPaths) {
2546
- const unitBase = path9.basename(unitPath).toLowerCase();
2547
- const unitDir = path9.dirname(unitPath).toLowerCase();
2995
+ const unitBase = path10.basename(unitPath).toLowerCase();
2996
+ const unitDir = path10.dirname(unitPath).toLowerCase();
2548
2997
  const q = queryFile.toLowerCase();
2549
2998
  const u = unitPath.toLowerCase();
2550
2999
  if (q === u) best = Math.max(best, 1);
@@ -2738,7 +3187,7 @@ function rankWisdomUnits(db, input) {
2738
3187
  }
2739
3188
 
2740
3189
  // src/retrieval/code-ranker.ts
2741
- import path10 from "path";
3190
+ import path11 from "path";
2742
3191
  function parseJsonArray4(value) {
2743
3192
  try {
2744
3193
  const parsed = JSON.parse(value);
@@ -2765,13 +3214,13 @@ function rowToCodeChunk(row) {
2765
3214
  function filePathMatch2(filePath, queryFiles) {
2766
3215
  if (queryFiles.length === 0) return 0;
2767
3216
  let best = 0;
2768
- const unitBase = path10.basename(filePath).toLowerCase();
2769
- const unitDir = path10.dirname(filePath).toLowerCase();
3217
+ const unitBase = path11.basename(filePath).toLowerCase();
3218
+ const unitDir = path11.dirname(filePath).toLowerCase();
2770
3219
  const unit = filePath.toLowerCase();
2771
3220
  for (const queryFile of queryFiles) {
2772
3221
  const query = queryFile.toLowerCase();
2773
- const queryBase = path10.basename(queryFile).toLowerCase();
2774
- const queryDir = path10.dirname(queryFile).toLowerCase();
3222
+ const queryBase = path11.basename(queryFile).toLowerCase();
3223
+ const queryDir = path11.dirname(queryFile).toLowerCase();
2775
3224
  if (query === unit) best = Math.max(best, 1);
2776
3225
  else if (queryBase === unitBase) best = Math.max(best, 0.72);
2777
3226
  else if (queryDir === unitDir) best = Math.max(best, 0.62);
@@ -2851,7 +3300,7 @@ function loadCodeCandidates(db, input) {
2851
3300
  }
2852
3301
  }
2853
3302
  for (const file of input.files ?? []) {
2854
- const basename = path10.basename(file);
3303
+ const basename = path11.basename(file);
2855
3304
  const rows = db.prepare(
2856
3305
  `SELECT cc.*, NULL AS bm25
2857
3306
  FROM code_chunks cc
@@ -2902,9 +3351,150 @@ function rankCodeChunks(db, input) {
2902
3351
  return ranked.slice(0, limit);
2903
3352
  }
2904
3353
 
2905
- // src/retrieval/test-ranker.ts
2906
- import path11 from "path";
3354
+ // src/retrieval/architecture-ranker.ts
3355
+ import path12 from "path";
2907
3356
  function parseJsonArray5(value) {
3357
+ try {
3358
+ const parsed = JSON.parse(value);
3359
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
3360
+ } catch {
3361
+ return [];
3362
+ }
3363
+ }
3364
+ function parseEvidence(value) {
3365
+ try {
3366
+ const parsed = JSON.parse(value);
3367
+ return Array.isArray(parsed) ? parsed : [];
3368
+ } catch {
3369
+ return [];
3370
+ }
3371
+ }
3372
+ function rowToPattern(row) {
3373
+ return {
3374
+ id: row.id,
3375
+ repo: row.repo,
3376
+ area: row.area,
3377
+ name: row.name,
3378
+ summary: row.summary_sanitized,
3379
+ sanitizedSummary: row.summary_sanitized,
3380
+ sourceFiles: parseJsonArray5(row.source_files_json),
3381
+ symbols: parseJsonArray5(row.symbols_json),
3382
+ evidence: parseEvidence(row.evidence_json),
3383
+ confidence: row.confidence,
3384
+ createdAt: row.created_at,
3385
+ bm25: row.bm25 ?? void 0
3386
+ };
3387
+ }
3388
+ function filePathMatch3(pattern, files) {
3389
+ if (files.length === 0) return 0;
3390
+ let best = 0;
3391
+ for (const sourceFile of pattern.sourceFiles) {
3392
+ const sourceBase = path12.basename(sourceFile).toLowerCase();
3393
+ const sourceDir = path12.dirname(sourceFile).toLowerCase();
3394
+ for (const queryFile of files) {
3395
+ const queryBase = path12.basename(queryFile).toLowerCase();
3396
+ const queryDir = path12.dirname(queryFile).toLowerCase();
3397
+ if (sourceFile.toLowerCase() === queryFile.toLowerCase()) best = Math.max(best, 1);
3398
+ else if (sourceBase === queryBase) best = Math.max(best, 0.72);
3399
+ else if (sourceDir === queryDir) best = Math.max(best, 0.62);
3400
+ else if (sourceDir.startsWith(queryDir) || queryDir.startsWith(sourceDir)) {
3401
+ best = Math.max(best, 0.38);
3402
+ }
3403
+ }
3404
+ }
3405
+ return best;
3406
+ }
3407
+ function symbolMatch4(pattern, symbols) {
3408
+ if (symbols.length === 0) return 0;
3409
+ const indexed = pattern.symbols.map((symbol) => symbol.toLowerCase());
3410
+ let best = 0;
3411
+ for (const symbol of symbols) {
3412
+ const lower = symbol.toLowerCase();
3413
+ if (indexed.includes(lower)) best = Math.max(best, 1);
3414
+ else if (indexed.some((candidate) => candidate.includes(lower) || lower.includes(candidate))) {
3415
+ best = Math.max(best, 0.45);
3416
+ }
3417
+ }
3418
+ return best;
3419
+ }
3420
+ function textMatch4(pattern, input) {
3421
+ const terms = buildQueryTerms(input).slice(0, 32);
3422
+ const bm25Signal = pattern.bm25 === void 0 ? 0 : Math.max(0.25, Math.min(1, 1 / (1 + Math.abs(pattern.bm25))));
3423
+ if (terms.length === 0) return bm25Signal;
3424
+ const haystack = `${pattern.area} ${pattern.name} ${pattern.sanitizedSummary} ${pattern.sourceFiles.join(
3425
+ " "
3426
+ )} ${pattern.symbols.join(" ")}`.toLowerCase();
3427
+ const overlap = terms.filter((term) => haystack.includes(term.toLowerCase())).length / terms.length;
3428
+ return Math.max(overlap, bm25Signal);
3429
+ }
3430
+ function matchReasons4(parts, pattern) {
3431
+ const reasons = [`${pattern.area} architecture pattern`];
3432
+ if (parts.filePath >= 0.9) reasons.push("exact file architecture evidence");
3433
+ else if (parts.filePath >= 0.45) reasons.push("nearby file architecture evidence");
3434
+ if (parts.symbol >= 0.9) reasons.push("symbol match");
3435
+ else if (parts.symbol >= 0.4) reasons.push("related symbol match");
3436
+ if (parts.area >= 0.9) reasons.push("area match");
3437
+ if (parts.text >= 0.25) reasons.push("query terms matched pattern");
3438
+ return reasons.slice(0, 5);
3439
+ }
3440
+ function rankArchitecturePatterns(db, input) {
3441
+ const fileAreas = /* @__PURE__ */ new Set();
3442
+ for (const file of input.files ?? []) {
3443
+ const row = db.prepare("SELECT area FROM architecture_components WHERE path = ? LIMIT 1").get(file);
3444
+ if (row?.area) fileAreas.add(row.area);
3445
+ }
3446
+ const candidates = /* @__PURE__ */ new Map();
3447
+ const ftsQuery = buildFtsQuery(input);
3448
+ if (ftsQuery) {
3449
+ const rows2 = db.prepare(
3450
+ `SELECT ap.id, ap.repo, ap.area, ap.name, ap.summary_sanitized, ap.source_files_json,
3451
+ ap.symbols_json, ap.evidence_json, ap.confidence, ap.created_at,
3452
+ bm25(architecture_patterns_fts) AS bm25
3453
+ FROM architecture_patterns_fts
3454
+ JOIN architecture_patterns ap ON ap.id = architecture_patterns_fts.patternId
3455
+ WHERE architecture_patterns_fts MATCH ?
3456
+ ORDER BY bm25(architecture_patterns_fts)
3457
+ LIMIT 150`
3458
+ ).all(ftsQuery);
3459
+ for (const row of rows2) {
3460
+ const pattern = rowToPattern(row);
3461
+ candidates.set(pattern.id, pattern);
3462
+ }
3463
+ }
3464
+ const rows = db.prepare(
3465
+ `SELECT id, repo, area, name, summary_sanitized, source_files_json, symbols_json,
3466
+ evidence_json, confidence, created_at, NULL AS bm25
3467
+ FROM architecture_patterns
3468
+ ORDER BY confidence DESC, created_at DESC`
3469
+ ).all();
3470
+ for (const row of rows) {
3471
+ const pattern = rowToPattern(row);
3472
+ candidates.set(pattern.id, { ...pattern, bm25: candidates.get(pattern.id)?.bm25 });
3473
+ }
3474
+ return [...candidates.values()].filter((pattern) => !input.area || pattern.area === input.area).map((pattern) => {
3475
+ const parts = {
3476
+ filePath: filePathMatch3(pattern, input.files ?? []),
3477
+ symbol: symbolMatch4(pattern, input.symbols ?? []),
3478
+ text: textMatch4(pattern, input),
3479
+ area: input.area && pattern.area === input.area ? 1 : fileAreas.has(pattern.area) ? 1 : 0,
3480
+ confidence: pattern.confidence
3481
+ };
3482
+ const score = (0.34 * parts.filePath + 0.2 * parts.symbol + 0.18 * parts.text + 0.13 * parts.area + 0.15 * parts.confidence) * (fileAreas.size > 0 && !fileAreas.has(pattern.area) ? 0.75 : 1);
3483
+ return {
3484
+ ...pattern,
3485
+ score: Number(score.toFixed(4)),
3486
+ matchReasons: matchReasons4(parts, pattern),
3487
+ rankSignals: parts
3488
+ };
3489
+ }).filter((pattern) => {
3490
+ if (input.files?.length || input.symbols?.length || input.area) return pattern.score > 0.08;
3491
+ return true;
3492
+ }).sort((a, b) => b.score - a.score).slice(0, Math.min(input.maxResults ?? 6, 12));
3493
+ }
3494
+
3495
+ // src/retrieval/test-ranker.ts
3496
+ import path13 from "path";
3497
+ function parseJsonArray6(value) {
2908
3498
  if (!value) return [];
2909
3499
  try {
2910
3500
  const parsed = JSON.parse(value);
@@ -2914,10 +3504,10 @@ function parseJsonArray5(value) {
2914
3504
  }
2915
3505
  }
2916
3506
  function baseStem(filePath) {
2917
- return path11.posix.basename(filePath).replace(/\.(test|spec)\.[^.]+$/i, "").replace(/\.[^.]+$/i, "").toLowerCase();
3507
+ return path13.posix.basename(filePath).replace(/\.(test|spec)\.[^.]+$/i, "").replace(/\.[^.]+$/i, "").toLowerCase();
2918
3508
  }
2919
3509
  function rowToRanked(row, input) {
2920
- const symbols = parseJsonArray5(row.symbols_json);
3510
+ const symbols = parseJsonArray6(row.symbols_json);
2921
3511
  const text = row.sanitized_text ?? "";
2922
3512
  const matchedSymbols = (input.symbols ?? []).filter((symbol) => {
2923
3513
  const lower = symbol.toLowerCase();
@@ -2986,8 +3576,8 @@ function rankRelevantTests(db, input) {
2986
3576
  }
2987
3577
 
2988
3578
  // src/retrieval/regression-ranker.ts
2989
- import path12 from "path";
2990
- function parseJsonArray6(value) {
3579
+ import path14 from "path";
3580
+ function parseJsonArray7(value) {
2991
3581
  try {
2992
3582
  const parsed = JSON.parse(value);
2993
3583
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
@@ -3002,25 +3592,25 @@ function rowToEvent(row) {
3002
3592
  prNumber: row.pr_number,
3003
3593
  prUrl: row.pr_url,
3004
3594
  summary: row.summary_sanitized,
3005
- filePaths: parseJsonArray6(row.file_paths_json),
3006
- symbols: parseJsonArray6(row.symbols_json),
3007
- testPaths: parseJsonArray6(row.test_paths_json),
3008
- authors: parseJsonArray6(row.authors_json),
3009
- labels: parseJsonArray6(row.labels_json),
3010
- signals: parseJsonArray6(row.signals_json),
3595
+ filePaths: parseJsonArray7(row.file_paths_json),
3596
+ symbols: parseJsonArray7(row.symbols_json),
3597
+ testPaths: parseJsonArray7(row.test_paths_json),
3598
+ authors: parseJsonArray7(row.authors_json),
3599
+ labels: parseJsonArray7(row.labels_json),
3600
+ signals: parseJsonArray7(row.signals_json),
3011
3601
  createdAt: row.created_at,
3012
3602
  mergedAt: row.merged_at ?? void 0,
3013
3603
  confidence: row.confidence
3014
3604
  };
3015
3605
  }
3016
- function filePathMatch3(eventPaths, queryFiles) {
3606
+ function filePathMatch4(eventPaths, queryFiles) {
3017
3607
  let best = 0;
3018
3608
  for (const queryFile of queryFiles) {
3019
- const queryBase = path12.posix.basename(queryFile).toLowerCase();
3020
- const queryDir = path12.posix.dirname(queryFile).toLowerCase();
3609
+ const queryBase = path14.posix.basename(queryFile).toLowerCase();
3610
+ const queryDir = path14.posix.dirname(queryFile).toLowerCase();
3021
3611
  for (const eventPath of eventPaths) {
3022
- const eventBase = path12.posix.basename(eventPath).toLowerCase();
3023
- const eventDir = path12.posix.dirname(eventPath).toLowerCase();
3612
+ const eventBase = path14.posix.basename(eventPath).toLowerCase();
3613
+ const eventDir = path14.posix.dirname(eventPath).toLowerCase();
3024
3614
  if (queryFile.toLowerCase() === eventPath.toLowerCase()) best = Math.max(best, 1);
3025
3615
  else if (queryBase === eventBase) best = Math.max(best, 0.7);
3026
3616
  else if (queryDir === eventDir) best = Math.max(best, 0.55);
@@ -3028,7 +3618,7 @@ function filePathMatch3(eventPaths, queryFiles) {
3028
3618
  }
3029
3619
  return best;
3030
3620
  }
3031
- function symbolMatch4(event, querySymbols) {
3621
+ function symbolMatch5(event, querySymbols) {
3032
3622
  const eventSymbols = event.symbols.map((symbol) => symbol.toLowerCase());
3033
3623
  let best = 0;
3034
3624
  for (const symbol of querySymbols) {
@@ -3038,7 +3628,7 @@ function symbolMatch4(event, querySymbols) {
3038
3628
  }
3039
3629
  return best;
3040
3630
  }
3041
- function textMatch4(event, inputText) {
3631
+ function textMatch5(event, inputText) {
3042
3632
  const tokens = tokenizeSearchText(inputText, 32);
3043
3633
  if (tokens.length === 0) return 0;
3044
3634
  const haystack = `${event.summary} ${event.filePaths.join(" ")} ${event.symbols.join(" ")} ${event.signals.join(" ")}`.toLowerCase();
@@ -3052,7 +3642,7 @@ function recencyScore3(event) {
3052
3642
  if (ageDays < 730) return 0.7;
3053
3643
  return 0.35;
3054
3644
  }
3055
- function matchReasons4(parts, event) {
3645
+ function matchReasons5(parts, event) {
3056
3646
  const reasons = [];
3057
3647
  if ((parts.filePathMatch ?? 0) >= 0.9) reasons.push("exact file path match");
3058
3648
  else if ((parts.filePathMatch ?? 0) >= 0.45) reasons.push("related file path match");
@@ -3074,9 +3664,9 @@ function rankRegressionEvents(db, input) {
3074
3664
  const inputText = "task" in input ? `${input.task} ${input.diff ?? ""} ${input.currentCode ?? ""}` : input.query;
3075
3665
  const ranked = loadRegressionEvents(db).map((event) => {
3076
3666
  const parts = {
3077
- filePathMatch: filePathMatch3(event.filePaths, queryFiles),
3078
- symbolMatch: symbolMatch4(event, querySymbols),
3079
- textMatch: textMatch4(event, inputText),
3667
+ filePathMatch: filePathMatch4(event.filePaths, queryFiles),
3668
+ symbolMatch: symbolMatch5(event, querySymbols),
3669
+ textMatch: textMatch5(event, inputText),
3080
3670
  recency: recencyScore3(event),
3081
3671
  confidence: event.confidence
3082
3672
  };
@@ -3086,7 +3676,7 @@ function rankRegressionEvents(db, input) {
3086
3676
  filePaths: uniqueStrings(event.filePaths),
3087
3677
  symbols: uniqueStrings(event.symbols),
3088
3678
  score: Number(score.toFixed(4)),
3089
- matchReasons: matchReasons4(parts, event),
3679
+ matchReasons: matchReasons5(parts, event),
3090
3680
  rankSignals: parts
3091
3681
  };
3092
3682
  }).filter((event) => event.score > 0 || "regressionsOnly" in input && input.regressionsOnly).sort((a, b) => b.score - a.score || b.confidence - a.confidence);
@@ -3139,7 +3729,7 @@ function riskLines(units) {
3139
3729
  }
3140
3730
  return [...risks].slice(0, 4);
3141
3731
  }
3142
- function formatAnchorContext(units, input, codeChunks = [], teamRules = [], warnings = [], relevantTests = [], regressionEvents = [], extraMetadata = {}) {
3732
+ function formatAnchorContext(units, input, codeChunks = [], teamRules = [], warnings = [], relevantTests = [], regressionEvents = [], architecturePatterns = [], extraMetadata = {}) {
3143
3733
  const lines = ["# Anchor Context", ""];
3144
3734
  if (warnings.length > 0) {
3145
3735
  lines.push("## Warnings", "");
@@ -3189,6 +3779,20 @@ function formatAnchorContext(units, input, codeChunks = [], teamRules = [], warn
3189
3779
  lines.push("");
3190
3780
  });
3191
3781
  }
3782
+ lines.push("## Architecture Guidance", "");
3783
+ if (architecturePatterns.length === 0) {
3784
+ lines.push("No directly relevant architecture patterns found in the local index.", "");
3785
+ } else {
3786
+ architecturePatterns.forEach((pattern, index) => {
3787
+ lines.push(`${index + 1}. [${pattern.area}] ${clipSentence(pattern.sanitizedSummary, 240)}`);
3788
+ lines.push(` Evidence: ${pattern.sourceFiles.slice(0, 5).join(", ") || "indexed code"}`);
3789
+ lines.push(` Confidence: ${pattern.confidence.toFixed(2)}`);
3790
+ lines.push(
3791
+ ` Why it matters: Follow this current-code pattern unless stronger PR or team-rule evidence says otherwise.`
3792
+ );
3793
+ lines.push("");
3794
+ });
3795
+ }
3192
3796
  lines.push("## Relevant tests", "");
3193
3797
  if (relevantTests.length === 0) {
3194
3798
  lines.push("No directly related tests found in the local index.", "");
@@ -3280,6 +3884,19 @@ function formatAnchorContext(units, input, codeChunks = [], teamRules = [], warn
3280
3884
  matchReasons: chunk.matchReasons,
3281
3885
  rankSignals: chunk.rankSignals
3282
3886
  })),
3887
+ architecturePatterns: architecturePatterns.map((pattern) => ({
3888
+ id: pattern.id,
3889
+ score: pattern.score,
3890
+ area: pattern.area,
3891
+ name: pattern.name,
3892
+ sanitizedSummary: clipSentence(pattern.sanitizedSummary, 280),
3893
+ sourceFiles: pattern.sourceFiles,
3894
+ symbols: pattern.symbols,
3895
+ confidence: pattern.confidence,
3896
+ evidence: pattern.evidence,
3897
+ matchReasons: pattern.matchReasons,
3898
+ rankSignals: pattern.rankSignals
3899
+ })),
3283
3900
  relevantTests: relevantTests.map((test) => ({
3284
3901
  path: test.path,
3285
3902
  sourcePath: test.sourcePath,
@@ -3356,6 +3973,9 @@ function formatIndexStatus(status) {
3356
3973
  `- Test files: ${status.testFileCount}`,
3357
3974
  `- Test links: ${status.testLinkCount}`,
3358
3975
  `- Regression events: ${status.regressionEventCount}`,
3976
+ `- Architecture components: ${status.architectureComponentCount}`,
3977
+ `- Architecture patterns: ${status.architecturePatternCount}`,
3978
+ `- Architecture imports: ${status.architectureImportCount}`,
3359
3979
  `- Anchor coverage: ${status.coverageScore}% (${status.coverageGrade})`,
3360
3980
  `- History coverage: ${status.historyCoverage ?? "unknown"}`,
3361
3981
  `- History limit: ${status.historyLimit ?? "n/a"}`,
@@ -3363,6 +3983,7 @@ function formatIndexStatus(status) {
3363
3983
  `- Team rules: ${status.teamRuleCount}`,
3364
3984
  `- Last sync: ${status.lastSyncTime ?? "never"}`,
3365
3985
  `- Last code index: ${status.lastCodeIndexTime ?? "never"}`,
3986
+ `- Last architecture index: ${status.lastArchitectureIndexTime ?? "never"}`,
3366
3987
  `- Last rule index: ${status.lastRuleIndexTime ?? "never"}`,
3367
3988
  `- Last successful index run: ${status.lastSuccessfulRun ?? "never"}`,
3368
3989
  `- Last failed index run: ${status.lastFailedRun ?? "never"}`,
@@ -3415,6 +4036,7 @@ function buildAnchorContextResult(db, cwd, input, warnings = []) {
3415
4036
  const rules = rankTeamRules(db, cwd, input);
3416
4037
  const tests = rankRelevantTests(db, input);
3417
4038
  const regressions = rankRegressionEvents(db, input);
4039
+ const architecture = rankArchitecturePatterns(db, input);
3418
4040
  const indexStatus = getIndexStatus(cwd);
3419
4041
  const semanticStatus = getSemanticStatus();
3420
4042
  const strictWarnings = input.strict && indexStatus.historyCoverage !== "all" ? [
@@ -3428,12 +4050,14 @@ function buildAnchorContextResult(db, cwd, input, warnings = []) {
3428
4050
  [...warnings, ...strictWarnings],
3429
4051
  tests,
3430
4052
  regressions,
4053
+ architecture,
3431
4054
  {
3432
4055
  indexHealth: {
3433
4056
  historyCoverage: indexStatus.historyCoverage ?? "unknown",
3434
4057
  staleCodeIndex: Boolean(indexStatus.staleCodeIndex),
3435
4058
  lastSuccessfulRun: indexStatus.lastSuccessfulRun,
3436
- lastFailedRun: indexStatus.lastFailedRun
4059
+ lastFailedRun: indexStatus.lastFailedRun,
4060
+ architecturePatternCount: indexStatus.architecturePatternCount
3437
4061
  },
3438
4062
  semanticStatus
3439
4063
  }
@@ -3521,6 +4145,116 @@ function explainFile(db, cwd, input) {
3521
4145
  };
3522
4146
  }
3523
4147
 
4148
+ // src/retrieval/architecture.ts
4149
+ function architectureFilesFromDiff(diff) {
4150
+ const files = [];
4151
+ for (const line of diff.split("\n")) {
4152
+ const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
4153
+ if (match?.[2] && match[2] !== "/dev/null") files.push(match[2]);
4154
+ const plus = line.match(/^\+\+\+ b\/(.+)$/);
4155
+ if (plus?.[1] && plus[1] !== "/dev/null") files.push(plus[1]);
4156
+ }
4157
+ return uniqueStrings(files);
4158
+ }
4159
+ function formatPatternList(patterns) {
4160
+ if (patterns.length === 0) return ["No matching architecture patterns found."];
4161
+ return patterns.flatMap((pattern, index) => [
4162
+ `${index + 1}. [${pattern.area}] ${clipSentence(pattern.sanitizedSummary, 260)}`,
4163
+ ` Evidence: ${pattern.sourceFiles.slice(0, 6).join(", ") || "indexed code"}`,
4164
+ ` Confidence: ${pattern.confidence.toFixed(2)}`,
4165
+ ` Match: ${pattern.matchReasons.join(", ")}`,
4166
+ ""
4167
+ ]);
4168
+ }
4169
+ function architectureMetadata(mode, patterns, extra = {}) {
4170
+ return {
4171
+ mode,
4172
+ architecturePatterns: patterns.map((pattern) => ({
4173
+ id: pattern.id,
4174
+ score: pattern.score,
4175
+ area: pattern.area,
4176
+ name: pattern.name,
4177
+ sanitizedSummary: clipSentence(pattern.sanitizedSummary, 280),
4178
+ sourceFiles: pattern.sourceFiles,
4179
+ symbols: pattern.symbols,
4180
+ confidence: pattern.confidence,
4181
+ evidence: pattern.evidence,
4182
+ matchReasons: pattern.matchReasons,
4183
+ rankSignals: pattern.rankSignals
4184
+ })),
4185
+ ...extra
4186
+ };
4187
+ }
4188
+ function getArchitectureContext(db, _cwd, input = {}) {
4189
+ const task = input.query ?? (input.file ? `Explain architecture patterns for ${input.file}` : input.area ? `Explain ${input.area} architecture patterns` : "Summarize repository architecture patterns");
4190
+ const patterns = rankArchitecturePatterns(db, {
4191
+ task,
4192
+ files: input.file ? [input.file] : void 0,
4193
+ area: input.area,
4194
+ maxResults: input.maxResults ?? 8
4195
+ });
4196
+ const lines = ["# Anchor Architecture", ""];
4197
+ if (input.file) lines.push(`File: ${input.file}`);
4198
+ if (input.area) lines.push(`Area: ${input.area}`);
4199
+ if (input.query) lines.push(`Query: ${input.query}`);
4200
+ if (input.file || input.area || input.query) lines.push("");
4201
+ lines.push("## Patterns", "", ...formatPatternList(patterns));
4202
+ lines.push("## Recommended implementation path", "");
4203
+ if (patterns.length === 0) {
4204
+ lines.push("- Run `anchor index-code` to refresh current-code architecture evidence.");
4205
+ lines.push("- Search nearby files manually before changing architecture-sensitive code.");
4206
+ } else {
4207
+ lines.push("- Follow the highest-ranked current-code pattern for placement and imports.");
4208
+ lines.push("- Update related tests when the pattern evidence cites nearby tests.");
4209
+ lines.push(
4210
+ "- Use PR/team-rule evidence from `anchor_get_context` for stronger historical constraints."
4211
+ );
4212
+ }
4213
+ return {
4214
+ markdown: lines.join("\n"),
4215
+ metadata: architectureMetadata("architecture", patterns, {
4216
+ file: input.file,
4217
+ area: input.area,
4218
+ query: input.query
4219
+ })
4220
+ };
4221
+ }
4222
+ function checkArchitecture(db, _cwd, input) {
4223
+ const files = input.files?.length ? input.files : architectureFilesFromDiff(input.diff);
4224
+ const patterns = rankArchitecturePatterns(db, {
4225
+ task: "Check this diff against current architecture patterns",
4226
+ files,
4227
+ diff: input.diff,
4228
+ maxResults: input.maxResults ?? 8
4229
+ });
4230
+ const lines = [
4231
+ "# Anchor Architecture Check",
4232
+ "",
4233
+ `Changed files: ${files.join(", ") || "n/a"}`,
4234
+ "",
4235
+ "## Matching patterns",
4236
+ "",
4237
+ ...formatPatternList(patterns),
4238
+ "## Architecture risks",
4239
+ ""
4240
+ ];
4241
+ if (patterns.length === 0) {
4242
+ lines.push(
4243
+ "- No matching architecture evidence found. Run `anchor index-code` or inspect nearby files."
4244
+ );
4245
+ } else {
4246
+ lines.push("- Check that new files live in the same layer/area as matching examples.");
4247
+ lines.push("- Check imports follow the observed direction between layers.");
4248
+ lines.push("- Check related tests follow the cited test placement pattern.");
4249
+ }
4250
+ return {
4251
+ markdown: lines.join("\n"),
4252
+ metadata: architectureMetadata("architecture_check", patterns, {
4253
+ changedFiles: files
4254
+ })
4255
+ };
4256
+ }
4257
+
3524
4258
  // src/retrieval/review-diff.ts
3525
4259
  function filesFromDiff(diff) {
3526
4260
  const files = [];
@@ -3820,28 +4554,168 @@ function createGitHubClient(token) {
3820
4554
  });
3821
4555
  }
3822
4556
 
4557
+ // src/github/rate-limit.ts
4558
+ function isGitHubRateLimitError(error) {
4559
+ const candidate = error;
4560
+ if (candidate.status !== 403 && candidate.status !== 429) return false;
4561
+ const message = candidate.message?.toLowerCase() ?? "";
4562
+ const headers = candidate.response?.headers ?? {};
4563
+ return candidate.status === 429 || headers["retry-after"] !== void 0 || headers["x-ratelimit-remaining"] === "0" || message.includes("rate limit") || message.includes("secondary limit");
4564
+ }
4565
+ function getGitHubRateLimitDelayMs(error, attempt, now = Date.now()) {
4566
+ const headers = error.response?.headers ?? {};
4567
+ const retryAfter = Number(headers["retry-after"]);
4568
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
4569
+ return {
4570
+ delayMs: Math.ceil(retryAfter * 1e3),
4571
+ reason: `retry-after header requested ${Math.ceil(retryAfter)} seconds`
4572
+ };
4573
+ }
4574
+ const remaining = String(headers["x-ratelimit-remaining"] ?? "");
4575
+ const reset = Number(headers["x-ratelimit-reset"]);
4576
+ if (remaining === "0" && Number.isFinite(reset) && reset > 0) {
4577
+ const resetDelayMs = Math.max(0, reset * 1e3 - now);
4578
+ return {
4579
+ delayMs: Math.ceil(resetDelayMs + 2e3),
4580
+ reason: `primary rate limit resets at ${new Date(reset * 1e3).toISOString()}`
4581
+ };
4582
+ }
4583
+ const backoffSeconds = Math.min(900, 60 * 2 ** Math.max(0, attempt - 1));
4584
+ return {
4585
+ delayMs: backoffSeconds * 1e3,
4586
+ reason: `secondary rate limit backoff for ${backoffSeconds} seconds`
4587
+ };
4588
+ }
4589
+ async function sleep(milliseconds) {
4590
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
4591
+ }
4592
+ async function waitForGlobalBlock(controller) {
4593
+ const now = controller.now?.() ?? Date.now();
4594
+ const waitMs = Math.max(0, (controller.blockedUntilMs ?? 0) - now);
4595
+ if (waitMs > 0) {
4596
+ await (controller.sleep ?? sleep)(waitMs);
4597
+ }
4598
+ }
4599
+ async function requestWithGitHubRateLimit(request, options) {
4600
+ const maxRetries = options.maxRetries ?? 8;
4601
+ for (let attempt = 1; ; attempt += 1) {
4602
+ await waitForGlobalBlock(options.controller);
4603
+ try {
4604
+ return await request();
4605
+ } catch (error) {
4606
+ if (!isGitHubRateLimitError(error) || attempt > maxRetries) throw error;
4607
+ const now = options.controller.now?.() ?? Date.now();
4608
+ const { delayMs, reason } = getGitHubRateLimitDelayMs(error, attempt, now);
4609
+ const retryAtMs = now + delayMs;
4610
+ options.controller.blockedUntilMs = Math.max(
4611
+ options.controller.blockedUntilMs ?? 0,
4612
+ retryAtMs
4613
+ );
4614
+ options.controller.onRateLimit?.({
4615
+ waitSeconds: Math.ceil(delayMs / 1e3),
4616
+ retryAt: new Date(retryAtMs).toISOString(),
4617
+ reason,
4618
+ request: options.requestName,
4619
+ attempt
4620
+ });
4621
+ await (options.controller.sleep ?? sleep)(delayMs);
4622
+ }
4623
+ }
4624
+ }
4625
+ function hasNextPage(headers) {
4626
+ return String(headers.link ?? "").includes('rel="next"');
4627
+ }
4628
+ async function paginateWithGitHubRateLimit(requestPage, options) {
4629
+ const results = [];
4630
+ for (let page = 1; ; page += 1) {
4631
+ const response = await requestWithGitHubRateLimit(() => requestPage(page), {
4632
+ controller: options.controller,
4633
+ requestName: `${options.requestName} page ${page}`
4634
+ });
4635
+ results.push(...response.data);
4636
+ if (!hasNextPage(response.headers) && response.data.length < 100) break;
4637
+ if (!hasNextPage(response.headers) && response.data.length === 0) break;
4638
+ if (!hasNextPage(response.headers)) break;
4639
+ }
4640
+ return results;
4641
+ }
4642
+
3823
4643
  // src/github/fetch-pr-details.ts
3824
- async function fetchPullRequestDetails(octokit, repoFullName, pullNumber) {
4644
+ async function fetchPullRequestDetails(octokit, repoFullName, pullNumber, controller = {}) {
3825
4645
  const [owner, repo] = repoFullName.split("/");
3826
4646
  if (!owner || !repo) throw new Error(`Invalid repo '${repoFullName}'. Expected owner/name.`);
3827
- const [{ data: pull }, files, reviews, reviewComments, issueComments, commits] = await Promise.all([
3828
- octokit.pulls.get({ owner, repo, pull_number: pullNumber }),
3829
- octokit.paginate(octokit.pulls.listFiles, { owner, repo, pull_number: pullNumber, per_page: 100 }),
3830
- octokit.paginate(octokit.pulls.listReviews, { owner, repo, pull_number: pullNumber, per_page: 100 }),
3831
- octokit.paginate(octokit.pulls.listReviewComments, {
4647
+ const { data: pull } = await requestWithGitHubRateLimit(
4648
+ () => octokit.pulls.get({ owner, repo, pull_number: pullNumber }),
4649
+ {
4650
+ controller,
4651
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}`
4652
+ }
4653
+ );
4654
+ const files = await paginateWithGitHubRateLimit(
4655
+ (page) => octokit.pulls.listFiles({
4656
+ owner,
4657
+ repo,
4658
+ pull_number: pullNumber,
4659
+ per_page: 100,
4660
+ page
4661
+ }),
4662
+ {
4663
+ controller,
4664
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}/files`
4665
+ }
4666
+ );
4667
+ const reviews = await paginateWithGitHubRateLimit(
4668
+ (page) => octokit.pulls.listReviews({
4669
+ owner,
4670
+ repo,
4671
+ pull_number: pullNumber,
4672
+ per_page: 100,
4673
+ page
4674
+ }),
4675
+ {
4676
+ controller,
4677
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}/reviews`
4678
+ }
4679
+ );
4680
+ const reviewComments = await paginateWithGitHubRateLimit(
4681
+ (page) => octokit.pulls.listReviewComments({
3832
4682
  owner,
3833
4683
  repo,
3834
4684
  pull_number: pullNumber,
3835
- per_page: 100
4685
+ per_page: 100,
4686
+ page
3836
4687
  }),
3837
- octokit.paginate(octokit.issues.listComments, {
4688
+ {
4689
+ controller,
4690
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}/comments`
4691
+ }
4692
+ );
4693
+ const issueComments = await paginateWithGitHubRateLimit(
4694
+ (page) => octokit.issues.listComments({
3838
4695
  owner,
3839
4696
  repo,
3840
4697
  issue_number: pullNumber,
3841
- per_page: 100
4698
+ per_page: 100,
4699
+ page
3842
4700
  }),
3843
- octokit.paginate(octokit.pulls.listCommits, { owner, repo, pull_number: pullNumber, per_page: 100 })
3844
- ]);
4701
+ {
4702
+ controller,
4703
+ requestName: `GET /repos/${repoFullName}/issues/${pullNumber}/comments`
4704
+ }
4705
+ );
4706
+ const commits = await paginateWithGitHubRateLimit(
4707
+ (page) => octokit.pulls.listCommits({
4708
+ owner,
4709
+ repo,
4710
+ pull_number: pullNumber,
4711
+ per_page: 100,
4712
+ page
4713
+ }),
4714
+ {
4715
+ controller,
4716
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}/commits`
4717
+ }
4718
+ );
3845
4719
  return {
3846
4720
  repo: repoFullName,
3847
4721
  number: pull.number,
@@ -3914,7 +4788,12 @@ async function fetchPullRequestDetailsConcurrently(options) {
3914
4788
  prNumber: pullNumber,
3915
4789
  detailConcurrency: options.detailConcurrency
3916
4790
  });
3917
- results[index] = await fetchPullRequestDetails(options.octokit, options.repo, pullNumber);
4791
+ results[index] = await fetchPullRequestDetails(
4792
+ options.octokit,
4793
+ options.repo,
4794
+ pullNumber,
4795
+ options.controller
4796
+ );
3918
4797
  completed += 1;
3919
4798
  options.onProgress?.({
3920
4799
  stage: "fetched_pull_request_details",
@@ -3940,10 +4819,18 @@ async function fetchMergedPullRequests(options) {
3940
4819
  const octokit = createGitHubClient(options.token);
3941
4820
  const limit = resolvePullRequestFetchLimit(options);
3942
4821
  const detailConcurrency = resolvePullRequestDetailConcurrency(options);
4822
+ const rateLimitController = {
4823
+ onRateLimit: (progress) => options.onProgress?.({
4824
+ stage: "github_rate_limited",
4825
+ repo: options.repo,
4826
+ ...progress
4827
+ })
4828
+ };
3943
4829
  const sinceTime = options.since ? Date.parse(options.since) : void 0;
3944
4830
  const pullNumbers = [];
3945
4831
  let scannedPullRequests = 0;
3946
4832
  let reachedSinceBoundary = false;
4833
+ let page = 1;
3947
4834
  options.onProgress?.({
3948
4835
  stage: "discovering_pull_requests",
3949
4836
  repo: options.repo,
@@ -3951,14 +4838,22 @@ async function fetchMergedPullRequests(options) {
3951
4838
  limit,
3952
4839
  since: options.since
3953
4840
  });
3954
- for await (const response of octokit.paginate.iterator(octokit.pulls.list, {
3955
- owner,
3956
- repo,
3957
- state: "closed",
3958
- sort: "updated",
3959
- direction: "desc",
3960
- per_page: 100
3961
- })) {
4841
+ while (true) {
4842
+ const response = await requestWithGitHubRateLimit(
4843
+ () => octokit.pulls.list({
4844
+ owner,
4845
+ repo,
4846
+ state: "closed",
4847
+ sort: "updated",
4848
+ direction: "desc",
4849
+ per_page: 100,
4850
+ page
4851
+ }),
4852
+ {
4853
+ controller: rateLimitController,
4854
+ requestName: `GET /repos/${options.repo}/pulls page ${page}`
4855
+ }
4856
+ );
3962
4857
  scannedPullRequests += response.data.length;
3963
4858
  for (const pull of response.data) {
3964
4859
  if (sinceTime && Date.parse(pull.updated_at) < sinceTime) {
@@ -3977,7 +4872,11 @@ async function fetchMergedPullRequests(options) {
3977
4872
  scannedPullRequests,
3978
4873
  matchedMergedPullRequests: pullNumbers.length
3979
4874
  });
3980
- if (reachedSinceBoundary || limit !== void 0 && pullNumbers.length >= limit) break;
4875
+ const hasNextPage2 = String(response.headers.link ?? "").includes('rel="next"');
4876
+ if (reachedSinceBoundary || limit !== void 0 && pullNumbers.length >= limit || !hasNextPage2) {
4877
+ break;
4878
+ }
4879
+ page += 1;
3981
4880
  }
3982
4881
  options.onProgress?.({
3983
4882
  stage: "discovered_pull_requests",
@@ -3992,13 +4891,14 @@ async function fetchMergedPullRequests(options) {
3992
4891
  repo: options.repo,
3993
4892
  pullNumbers,
3994
4893
  detailConcurrency,
4894
+ controller: rateLimitController,
3995
4895
  onProgress: options.onProgress
3996
4896
  });
3997
4897
  }
3998
4898
 
3999
4899
  // src/doctor.ts
4000
4900
  import fs5 from "fs";
4001
- import path13 from "path";
4901
+ import path15 from "path";
4002
4902
  function check(name, ok, message, fix) {
4003
4903
  return { name, ok, message, fix: ok ? void 0 : fix };
4004
4904
  }
@@ -4059,7 +4959,7 @@ async function runDoctor(options) {
4059
4959
  )
4060
4960
  );
4061
4961
  }
4062
- const cursorConfigPath = path13.join(gitRoot ?? cwd, ".cursor", "mcp.json");
4962
+ const cursorConfigPath = path15.join(gitRoot ?? cwd, ".cursor", "mcp.json");
4063
4963
  let cursorConfig;
4064
4964
  let cursorConfigValid = false;
4065
4965
  if (fs5.existsSync(cursorConfigPath)) {
@@ -4134,7 +5034,7 @@ async function runDoctor(options) {
4134
5034
  "Run pnpm build, then try anchor serve from the repository."
4135
5035
  )
4136
5036
  );
4137
- const rulePath = path13.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
5037
+ const rulePath = path15.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
4138
5038
  checks.push(
4139
5039
  check(
4140
5040
  "Cursor rule file exists",
@@ -4190,18 +5090,22 @@ export {
4190
5090
  TEAM_RULES_FILE,
4191
5091
  addTeamRule,
4192
5092
  anchorMcpEntry,
5093
+ architectureFilesFromDiff,
4193
5094
  buildAnchorContextResult,
5095
+ buildArchitectureIndex,
4194
5096
  buildFtsQuery,
4195
5097
  buildQueryTerms,
4196
5098
  calculateCoverage,
4197
5099
  canonicalizeText,
4198
5100
  categorizeWisdom,
5101
+ checkArchitecture,
4199
5102
  checkSchema,
4200
5103
  checkTeamRuleEvidence,
4201
5104
  chunkCodeFile,
4202
5105
  chunkHistoricalText,
4203
5106
  claimKeyFor,
4204
5107
  clampMaxResults,
5108
+ classifyArchitectureArea,
4205
5109
  clipSentence,
4206
5110
  confidenceAtLeast,
4207
5111
  confidenceLevelFor,
@@ -4223,6 +5127,7 @@ export {
4223
5127
  evaluateIndexHealth,
4224
5128
  evidenceForWisdom,
4225
5129
  explainFile,
5130
+ extractCodeImports,
4226
5131
  extractCodeSymbols,
4227
5132
  extractRegressionEvents,
4228
5133
  extractSymbols,
@@ -4234,6 +5139,8 @@ export {
4234
5139
  formatIndexStatus,
4235
5140
  formatSearchHistory,
4236
5141
  getAnchorIndexHealth,
5142
+ getArchitectureContext,
5143
+ getGitHubRateLimitDelayMs,
4237
5144
  getIndexStatus,
4238
5145
  getLastSyncTime,
4239
5146
  getSemanticStatus,
@@ -4246,6 +5153,7 @@ export {
4246
5153
  indexPullRequests,
4247
5154
  inferTestAwareness,
4248
5155
  initializeSchema,
5156
+ isGitHubRateLimitError,
4249
5157
  isHardExcludedCodePath,
4250
5158
  isTestFilePath,
4251
5159
  loadCurrentCodeSnapshot,
@@ -4253,7 +5161,9 @@ export {
4253
5161
  mergeAnchorMcpConfig,
4254
5162
  normalizePullRequest,
4255
5163
  openAnchorDatabase,
5164
+ paginateWithGitHubRateLimit,
4256
5165
  parseGitHubRemote,
5166
+ rankArchitecturePatterns,
4257
5167
  rankCodeChunks,
4258
5168
  rankRegressionEvents,
4259
5169
  rankRelevantTests,
@@ -4263,6 +5173,7 @@ export {
4263
5173
  redactSecrets,
4264
5174
  redactedHistoricalText,
4265
5175
  replaceCodeIndex,
5176
+ requestWithGitHubRateLimit,
4266
5177
  resolveGitHubToken,
4267
5178
  resolvePullRequestDetailConcurrency,
4268
5179
  resolvePullRequestFetchLimit,