@pratik7368patil/anchor-core 0.1.11 → 0.1.12
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 +85 -4
- package/dist/index.js +803 -62
- 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 = [];
|
|
@@ -3998,7 +4732,7 @@ async function fetchMergedPullRequests(options) {
|
|
|
3998
4732
|
|
|
3999
4733
|
// src/doctor.ts
|
|
4000
4734
|
import fs5 from "fs";
|
|
4001
|
-
import
|
|
4735
|
+
import path15 from "path";
|
|
4002
4736
|
function check(name, ok, message, fix) {
|
|
4003
4737
|
return { name, ok, message, fix: ok ? void 0 : fix };
|
|
4004
4738
|
}
|
|
@@ -4059,7 +4793,7 @@ async function runDoctor(options) {
|
|
|
4059
4793
|
)
|
|
4060
4794
|
);
|
|
4061
4795
|
}
|
|
4062
|
-
const cursorConfigPath =
|
|
4796
|
+
const cursorConfigPath = path15.join(gitRoot ?? cwd, ".cursor", "mcp.json");
|
|
4063
4797
|
let cursorConfig;
|
|
4064
4798
|
let cursorConfigValid = false;
|
|
4065
4799
|
if (fs5.existsSync(cursorConfigPath)) {
|
|
@@ -4134,7 +4868,7 @@ async function runDoctor(options) {
|
|
|
4134
4868
|
"Run pnpm build, then try anchor serve from the repository."
|
|
4135
4869
|
)
|
|
4136
4870
|
);
|
|
4137
|
-
const rulePath =
|
|
4871
|
+
const rulePath = path15.join(gitRoot ?? cwd, ".cursor", "rules", "anchor.mdc");
|
|
4138
4872
|
checks.push(
|
|
4139
4873
|
check(
|
|
4140
4874
|
"Cursor rule file exists",
|
|
@@ -4190,18 +4924,22 @@ export {
|
|
|
4190
4924
|
TEAM_RULES_FILE,
|
|
4191
4925
|
addTeamRule,
|
|
4192
4926
|
anchorMcpEntry,
|
|
4927
|
+
architectureFilesFromDiff,
|
|
4193
4928
|
buildAnchorContextResult,
|
|
4929
|
+
buildArchitectureIndex,
|
|
4194
4930
|
buildFtsQuery,
|
|
4195
4931
|
buildQueryTerms,
|
|
4196
4932
|
calculateCoverage,
|
|
4197
4933
|
canonicalizeText,
|
|
4198
4934
|
categorizeWisdom,
|
|
4935
|
+
checkArchitecture,
|
|
4199
4936
|
checkSchema,
|
|
4200
4937
|
checkTeamRuleEvidence,
|
|
4201
4938
|
chunkCodeFile,
|
|
4202
4939
|
chunkHistoricalText,
|
|
4203
4940
|
claimKeyFor,
|
|
4204
4941
|
clampMaxResults,
|
|
4942
|
+
classifyArchitectureArea,
|
|
4205
4943
|
clipSentence,
|
|
4206
4944
|
confidenceAtLeast,
|
|
4207
4945
|
confidenceLevelFor,
|
|
@@ -4223,6 +4961,7 @@ export {
|
|
|
4223
4961
|
evaluateIndexHealth,
|
|
4224
4962
|
evidenceForWisdom,
|
|
4225
4963
|
explainFile,
|
|
4964
|
+
extractCodeImports,
|
|
4226
4965
|
extractCodeSymbols,
|
|
4227
4966
|
extractRegressionEvents,
|
|
4228
4967
|
extractSymbols,
|
|
@@ -4234,6 +4973,7 @@ export {
|
|
|
4234
4973
|
formatIndexStatus,
|
|
4235
4974
|
formatSearchHistory,
|
|
4236
4975
|
getAnchorIndexHealth,
|
|
4976
|
+
getArchitectureContext,
|
|
4237
4977
|
getIndexStatus,
|
|
4238
4978
|
getLastSyncTime,
|
|
4239
4979
|
getSemanticStatus,
|
|
@@ -4254,6 +4994,7 @@ export {
|
|
|
4254
4994
|
normalizePullRequest,
|
|
4255
4995
|
openAnchorDatabase,
|
|
4256
4996
|
parseGitHubRemote,
|
|
4997
|
+
rankArchitecturePatterns,
|
|
4257
4998
|
rankCodeChunks,
|
|
4258
4999
|
rankRegressionEvents,
|
|
4259
5000
|
rankRelevantTests,
|