@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.d.ts +133 -5
- package/dist/index.js +994 -83
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/db/schema.sql +60 -0
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
|
-
|
|
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
|
|
2345
|
+
import crypto3 from "crypto";
|
|
1909
2346
|
import fs4 from "fs";
|
|
1910
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2000
|
-
const relativeToRoot =
|
|
2001
|
-
if (relativeToRoot.startsWith("..") ||
|
|
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:
|
|
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
|
|
2538
|
+
import crypto5 from "crypto";
|
|
2090
2539
|
|
|
2091
2540
|
// src/indexer/wisdom-extractor.ts
|
|
2092
|
-
import
|
|
2093
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2919
|
+
path9.basename(file),
|
|
2471
2920
|
...testFilenameHints(file),
|
|
2472
|
-
...
|
|
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
|
|
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 =
|
|
2544
|
-
const queryDir =
|
|
2992
|
+
const queryBase = path10.basename(queryFile).toLowerCase();
|
|
2993
|
+
const queryDir = path10.dirname(queryFile).toLowerCase();
|
|
2545
2994
|
for (const unitPath of unitPaths) {
|
|
2546
|
-
const unitBase =
|
|
2547
|
-
const unitDir =
|
|
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
|
|
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 =
|
|
2769
|
-
const unitDir =
|
|
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 =
|
|
2774
|
-
const queryDir =
|
|
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 =
|
|
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/
|
|
2906
|
-
import
|
|
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
|
|
3507
|
+
return path13.posix.basename(filePath).replace(/\.(test|spec)\.[^.]+$/i, "").replace(/\.[^.]+$/i, "").toLowerCase();
|
|
2918
3508
|
}
|
|
2919
3509
|
function rowToRanked(row, input) {
|
|
2920
|
-
const symbols =
|
|
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
|
|
2990
|
-
function
|
|
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:
|
|
3006
|
-
symbols:
|
|
3007
|
-
testPaths:
|
|
3008
|
-
authors:
|
|
3009
|
-
labels:
|
|
3010
|
-
signals:
|
|
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
|
|
3606
|
+
function filePathMatch4(eventPaths, queryFiles) {
|
|
3017
3607
|
let best = 0;
|
|
3018
3608
|
for (const queryFile of queryFiles) {
|
|
3019
|
-
const queryBase =
|
|
3020
|
-
const queryDir =
|
|
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 =
|
|
3023
|
-
const eventDir =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
3078
|
-
symbolMatch:
|
|
3079
|
-
textMatch:
|
|
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:
|
|
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
|
|
3828
|
-
octokit.pulls.get({ owner, repo, pull_number: pullNumber }),
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|