knodin 0.8.4 → 0.8.6
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/bin/cli.js +272 -24
- package/dist/src/cli-model.js +26 -0
- package/dist/src/engine/candidate-database.js +167 -0
- package/dist/src/engine/embeddings.js +8 -5
- package/dist/src/engine/index.js +1244 -305
- package/dist/src/engine/prune.js +8 -0
- package/dist/src/file-metrics.js +153 -0
- package/dist/src/repair-lease.js +8 -5
- package/dist/src/response-budget.js +2 -1
- package/dist/src/shared-index/artifact.js +65 -0
- package/dist/src/shared-index/cache.js +267 -0
- package/dist/src/shared-index/compatibility.js +70 -0
- package/dist/src/shared-index/config.js +224 -0
- package/dist/src/shared-index/contract.js +117 -0
- package/dist/src/shared-index/manifest.js +71 -0
- package/dist/src/shared-index/opportunistic.js +37 -0
- package/dist/src/shared-index/overlay.js +98 -0
- package/dist/src/shared-index/provenance.js +149 -0
- package/dist/src/shared-index/publisher.js +198 -0
- package/dist/src/shared-index/restore.js +209 -0
- package/dist/src/shared-index/s3-client.js +124 -0
- package/dist/src/shared-index/schemas.js +86 -0
- package/dist/src/shared-index/selection.js +111 -0
- package/dist/src/tools/knodin-tools.js +70 -3
- package/docs/CLI.md +39 -0
- package/docs/MCP.md +12 -0
- package/docs/SHARED-INDEX-CONTRACT.md +132 -0
- package/docs/releases/0.8.5.md +25 -0
- package/docs/releases/0.8.6.md +10 -0
- package/package.json +12 -3
- package/schemas/shared-index-branch-pointer-v1.schema.json +66 -0
- package/schemas/shared-index-config-v1.schema.json +97 -0
- package/schemas/shared-index-manifest-v1.schema.json +104 -0
- package/schemas/shared-index-provenance-v1.schema.json +100 -0
package/dist/src/engine/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import chokidar from "chokidar";
|
|
20
20
|
import Parser from "web-tree-sitter";
|
|
21
21
|
import { compareBytes } from "../compare.js";
|
|
22
|
+
import { collectFileMetrics } from "../file-metrics.js";
|
|
22
23
|
import { readIndexActivity } from "../index-activity.js";
|
|
23
24
|
import { runReadOnlyLspQuery } from "../lsp-readonly.js";
|
|
24
25
|
import { acquireRepairLease } from "../repair-lease.js";
|
|
@@ -26,6 +27,7 @@ import { analyzeResourceReachability, resourceFingerprint, } from "../resource-r
|
|
|
26
27
|
import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snapshot.js";
|
|
27
28
|
import { KNODIN_VERSION } from "../version.js";
|
|
28
29
|
import * as ann from "./ann-hnsw.js";
|
|
30
|
+
import { allocateCandidate, assertCandidate, discardCandidateFiles, promoteCandidateFile, recoverInterruptedPromotion, } from "./candidate-database.js";
|
|
29
31
|
import { computeSimilarity, generateEmbedding, generateEmbeddings, } from "./embeddings.js";
|
|
30
32
|
import { walkRepoFiles } from "./file-walker.js";
|
|
31
33
|
import { clearGitHistorySignalCache, collectGitHistorySignals, } from "./git-history.js";
|
|
@@ -59,6 +61,7 @@ export const REPO_WIDE_QUERY_PATTERNS = [
|
|
|
59
61
|
"dead_code",
|
|
60
62
|
"large_functions",
|
|
61
63
|
"large_files",
|
|
64
|
+
"file_metrics",
|
|
62
65
|
"import_cycles",
|
|
63
66
|
"flows",
|
|
64
67
|
"stats",
|
|
@@ -544,6 +547,20 @@ function persistSymbolIdentities(db, repoPath, filePaths) {
|
|
|
544
547
|
throw error;
|
|
545
548
|
}
|
|
546
549
|
}
|
|
550
|
+
/** Repair-only cancellable identity pass, bounded to one source file per transaction. */
|
|
551
|
+
async function persistSymbolIdentitiesCancellable(db, repoPath, filePaths, signal) {
|
|
552
|
+
const scope = filePaths === undefined
|
|
553
|
+
? db
|
|
554
|
+
.query("SELECT DISTINCT filePath FROM symbols ORDER BY filePath")
|
|
555
|
+
.all()
|
|
556
|
+
.map((row) => row.filePath)
|
|
557
|
+
: (scopedFilePaths(filePaths) ?? []);
|
|
558
|
+
for (const file of scope) {
|
|
559
|
+
throwIfAborted(signal);
|
|
560
|
+
persistSymbolIdentities(db, repoPath, [file]);
|
|
561
|
+
await yieldToIndexEventLoop();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
547
564
|
/** Bind name/file reference rows to exact definitions after identities exist. */
|
|
548
565
|
function persistReferenceEndpoints(db, filePaths) {
|
|
549
566
|
const refs = referencesForFiles(db, filePaths);
|
|
@@ -661,7 +678,31 @@ function symbolIdentityMatches(candidate, selector) {
|
|
|
661
678
|
const dbInstances = new Map();
|
|
662
679
|
const initPromises = new Map();
|
|
663
680
|
const watchers = new Map();
|
|
681
|
+
const watcherStates = new Map();
|
|
664
682
|
const WATCH_DEBOUNCE_MS = 50;
|
|
683
|
+
const MAX_RECURSIVE_WATCH_FILES = 20_000;
|
|
684
|
+
function explicitDatabasePath(repoPath, databasePath) {
|
|
685
|
+
if (!databasePath)
|
|
686
|
+
return undefined;
|
|
687
|
+
const stateDir = path.resolve(resolveStateDir(repoPath));
|
|
688
|
+
const resolved = path.resolve(databasePath);
|
|
689
|
+
if (resolved === path.resolve(resolveDbPath(repoPath)))
|
|
690
|
+
throw new Error("knodin candidate database must be distinct from the active database");
|
|
691
|
+
const relative = path.relative(stateDir, resolved);
|
|
692
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
693
|
+
throw new Error("knodin candidate database must be a distinct path inside repository state");
|
|
694
|
+
}
|
|
695
|
+
let cursor = stateDir;
|
|
696
|
+
for (const segment of relative.split(path.sep)) {
|
|
697
|
+
cursor = path.join(cursor, segment);
|
|
698
|
+
if (fs.existsSync(cursor) && fs.lstatSync(cursor).isSymbolicLink())
|
|
699
|
+
throw new Error("knodin candidate database path must not contain symbolic links");
|
|
700
|
+
}
|
|
701
|
+
return resolved;
|
|
702
|
+
}
|
|
703
|
+
function databaseCacheKey(repoPath, databasePath) {
|
|
704
|
+
return explicitDatabasePath(repoPath, databasePath) ?? path.resolve(repoPath);
|
|
705
|
+
}
|
|
665
706
|
const watchQueues = new Map();
|
|
666
707
|
const repositoryEngineOwners = new Map();
|
|
667
708
|
/**
|
|
@@ -717,11 +758,32 @@ export async function __evictRemovedRepoState() {
|
|
|
717
758
|
}
|
|
718
759
|
dbInstances.delete(key);
|
|
719
760
|
initPromises.delete(key);
|
|
761
|
+
watcherStates.delete(key);
|
|
720
762
|
}
|
|
721
763
|
}
|
|
764
|
+
function freshnessMechanismFor(repoPath, policy) {
|
|
765
|
+
if (policy.watcher === "disabled") {
|
|
766
|
+
return {
|
|
767
|
+
strategy: "git-probes-and-lifecycle-hooks",
|
|
768
|
+
watcher: "disabled",
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const state = watcherStates.get(path.resolve(repoPath));
|
|
772
|
+
if (state?.watcher === "active") {
|
|
773
|
+
return {
|
|
774
|
+
strategy: "watcher-git-probes-and-lifecycle-hooks",
|
|
775
|
+
watcher: "active",
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
strategy: "git-probes-and-lifecycle-hooks",
|
|
780
|
+
watcher: state?.watcher ?? "disabled",
|
|
781
|
+
...(state?.diagnostic ? { diagnostic: state.diagnostic } : {}),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
722
784
|
// Bumped whenever any repo is (re)indexed, so cached community-detection results
|
|
723
785
|
// below can be invalidated cheaply instead of recomputed on every call.
|
|
724
|
-
const KNODIN_SCHEMA_VERSION = 22;
|
|
786
|
+
export const KNODIN_SCHEMA_VERSION = 22;
|
|
725
787
|
const LAST_REBUILD_SCHEMA_VERSION = 22;
|
|
726
788
|
let indexGeneration = 0;
|
|
727
789
|
const resourceReachabilityCache = new Map();
|
|
@@ -843,18 +905,127 @@ function getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath, topN = 15, relati
|
|
|
843
905
|
const statusCache = new Map();
|
|
844
906
|
const repairOperations = new Map();
|
|
845
907
|
const STATUS_CACHE_LIMIT = 16;
|
|
846
|
-
|
|
847
|
-
|
|
908
|
+
export const ADAPTIVE_STATUS_FILE_THRESHOLD = 50_000;
|
|
909
|
+
const PERSISTED_AUDIT_VERSION = 1;
|
|
910
|
+
export function adaptiveStatusStrategy(indexedFiles) {
|
|
911
|
+
const testThreshold = process.env.KNODIN_TEST_ADAPTIVE_STATUS_THRESHOLD !== undefined
|
|
912
|
+
? Number(process.env.KNODIN_TEST_ADAPTIVE_STATUS_THRESHOLD)
|
|
913
|
+
: ADAPTIVE_STATUS_FILE_THRESHOLD;
|
|
914
|
+
return indexedFiles <= testThreshold ? "deep" : "persisted";
|
|
915
|
+
}
|
|
916
|
+
function throwIfAborted(signal) {
|
|
917
|
+
if (!signal?.aborted)
|
|
918
|
+
return;
|
|
919
|
+
throw signal.reason instanceof Error
|
|
920
|
+
? signal.reason
|
|
921
|
+
: new DOMException("The operation was aborted", "AbortError");
|
|
922
|
+
}
|
|
923
|
+
function wasAborted(error, signal) {
|
|
924
|
+
return (signal.aborted &&
|
|
925
|
+
(error === signal.reason || (error instanceof Error && error.name === "AbortError")));
|
|
926
|
+
}
|
|
927
|
+
function canonicalRepository(repoPath) {
|
|
928
|
+
try {
|
|
929
|
+
return fs.realpathSync.native(repoPath);
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
return path.resolve(repoPath);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function persistedStatusAuditPath(databasePath) {
|
|
936
|
+
return path.join(path.dirname(databasePath), "status-audit-v1.json");
|
|
937
|
+
}
|
|
938
|
+
function readPersistedStatusAudit(repoPath, databasePath, databaseFingerprint, schemaVersion) {
|
|
939
|
+
try {
|
|
940
|
+
const parsed = JSON.parse(fs.readFileSync(persistedStatusAuditPath(databasePath), "utf8"));
|
|
941
|
+
if (parsed.format !== PERSISTED_AUDIT_VERSION ||
|
|
942
|
+
parsed.repository !== canonicalRepository(repoPath) ||
|
|
943
|
+
parsed.schemaVersion !== schemaVersion ||
|
|
944
|
+
parsed.knodinVersion !== KNODIN_VERSION ||
|
|
945
|
+
parsed.databaseFingerprint !== databaseFingerprint ||
|
|
946
|
+
parsed.result?.status !== "healthy" ||
|
|
947
|
+
parsed.result?.verification?.mode !== "deep-audit" ||
|
|
948
|
+
parsed.auditVerifiedAt !== parsed.result.verification.verifiedAt)
|
|
949
|
+
return null;
|
|
950
|
+
return parsed;
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function writePersistedStatusAudit(repoPath, databasePath, databaseFingerprint, result) {
|
|
957
|
+
if (result.status !== "healthy" || result.verification.mode !== "deep-audit")
|
|
958
|
+
return;
|
|
959
|
+
const target = persistedStatusAuditPath(databasePath);
|
|
960
|
+
const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
961
|
+
const snapshot = {
|
|
962
|
+
format: PERSISTED_AUDIT_VERSION,
|
|
963
|
+
repository: canonicalRepository(repoPath),
|
|
964
|
+
schemaVersion: result.schemaVersion,
|
|
965
|
+
knodinVersion: KNODIN_VERSION,
|
|
966
|
+
databaseFingerprint,
|
|
967
|
+
auditVerifiedAt: result.verification.verifiedAt,
|
|
968
|
+
result: structuredClone(result),
|
|
969
|
+
};
|
|
970
|
+
try {
|
|
971
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
972
|
+
fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600, flag: "wx" });
|
|
973
|
+
fs.renameSync(temporary, target);
|
|
974
|
+
}
|
|
975
|
+
finally {
|
|
976
|
+
fs.rmSync(temporary, { force: true });
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function databaseFileFingerprint(databasePath, db) {
|
|
980
|
+
const files = [databasePath, `${databasePath}-wal`]
|
|
848
981
|
.map((file) => {
|
|
849
982
|
try {
|
|
850
983
|
const stat = fs.statSync(file);
|
|
851
|
-
|
|
984
|
+
if (file.endsWith("-wal") && stat.size === 0)
|
|
985
|
+
return "missing";
|
|
986
|
+
const descriptor = fs.openSync(file, "r");
|
|
987
|
+
try {
|
|
988
|
+
const header = Buffer.alloc(Math.min(100, stat.size));
|
|
989
|
+
fs.readSync(descriptor, header, 0, header.length, 0);
|
|
990
|
+
// SQLite rewrites volatile header counters during a clean close even
|
|
991
|
+
// when graph contents are unchanged. Logical DB evidence below binds
|
|
992
|
+
// mutations without making process shutdown invalidate the snapshot.
|
|
993
|
+
if (!file.endsWith("-wal") && header.length >= 40)
|
|
994
|
+
header.fill(0, 24, 40);
|
|
995
|
+
return `${stat.size}:${crypto.createHash("sha256").update(header).digest("hex")}`;
|
|
996
|
+
}
|
|
997
|
+
finally {
|
|
998
|
+
fs.closeSync(descriptor);
|
|
999
|
+
}
|
|
852
1000
|
}
|
|
853
1001
|
catch {
|
|
854
1002
|
return "missing";
|
|
855
1003
|
}
|
|
856
1004
|
})
|
|
857
1005
|
.join("|");
|
|
1006
|
+
if (!db)
|
|
1007
|
+
return files;
|
|
1008
|
+
try {
|
|
1009
|
+
const counts = ["symbols", "references", "dependencies", "symbol_embeddings", "index_state"]
|
|
1010
|
+
.map((table) => db.query(`SELECT COUNT(*) count FROM "${table}"`).get()?.count ??
|
|
1011
|
+
0)
|
|
1012
|
+
.join(":");
|
|
1013
|
+
const state = db
|
|
1014
|
+
.query("SELECT COUNT(*) count, COALESCE(SUM(mtimeMs),0) mtimes, COALESCE(SUM(size),0) sizes FROM index_state")
|
|
1015
|
+
.get();
|
|
1016
|
+
const meta = db
|
|
1017
|
+
.query("SELECT key, value FROM meta ORDER BY key")
|
|
1018
|
+
.all()
|
|
1019
|
+
.map((row) => `${row.key}=${row.value}`)
|
|
1020
|
+
.join("\0");
|
|
1021
|
+
return `${files}|${crypto
|
|
1022
|
+
.createHash("sha256")
|
|
1023
|
+
.update(`${counts}|${state?.count}:${state?.mtimes}:${state?.sizes}|${meta}`)
|
|
1024
|
+
.digest("hex")}`;
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
return files;
|
|
1028
|
+
}
|
|
858
1029
|
}
|
|
859
1030
|
const traversalSnapshotCache = new Map();
|
|
860
1031
|
const TRAVERSAL_SNAPSHOT_CACHE_LIMIT = 8;
|
|
@@ -6516,9 +6687,10 @@ async function embedPreparedBatch(items, onModelProgress) {
|
|
|
6516
6687
|
}
|
|
6517
6688
|
}
|
|
6518
6689
|
/** Incrementally generates and saves embeddings for symbols that lack them. */
|
|
6519
|
-
async function indexEmbeddings(db, repoPath, progress) {
|
|
6690
|
+
async function indexEmbeddings(db, repoPath, progress, signal) {
|
|
6520
6691
|
return measurePerfPhase("embedding_inference_write", async () => {
|
|
6521
6692
|
try {
|
|
6693
|
+
throwIfAborted(signal);
|
|
6522
6694
|
const missingStmt = db.query(`
|
|
6523
6695
|
SELECT id, name, kind, filePath, startLine, endLine, summary
|
|
6524
6696
|
FROM symbols
|
|
@@ -6560,6 +6732,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6560
6732
|
const sourceForEmbedding = createEmbeddingSourceReader(repoPath);
|
|
6561
6733
|
let lastYieldAt = Date.now();
|
|
6562
6734
|
for (let offset = 0; offset < missing.length; offset += batchSize) {
|
|
6735
|
+
throwIfAborted(signal);
|
|
6563
6736
|
const prepared = missing.slice(offset, offset + batchSize).map((sym) => {
|
|
6564
6737
|
const source = sourceForEmbedding(sym.filePath, sym.startLine, sym.endLine);
|
|
6565
6738
|
return {
|
|
@@ -6568,6 +6741,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6568
6741
|
};
|
|
6569
6742
|
});
|
|
6570
6743
|
const generated = await embedPreparedBatch(prepared, onModelProgress);
|
|
6744
|
+
throwIfAborted(signal);
|
|
6571
6745
|
db.run("BEGIN TRANSACTION;");
|
|
6572
6746
|
try {
|
|
6573
6747
|
for (const { symbol, embedding: embeddingVec } of generated) {
|
|
@@ -6590,6 +6764,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6590
6764
|
insertEmbedding.finalize();
|
|
6591
6765
|
}
|
|
6592
6766
|
catch (error) {
|
|
6767
|
+
throwIfAborted(signal);
|
|
6593
6768
|
console.error("Error during indexEmbeddings:", error);
|
|
6594
6769
|
}
|
|
6595
6770
|
});
|
|
@@ -6766,6 +6941,63 @@ function gitTrackedCandidates(repoPath) {
|
|
|
6766
6941
|
return null;
|
|
6767
6942
|
}
|
|
6768
6943
|
}
|
|
6944
|
+
async function execGitAbortable(repoPath, args, signal) {
|
|
6945
|
+
throwIfAborted(signal);
|
|
6946
|
+
return await new Promise((resolve, reject) => {
|
|
6947
|
+
child_process.execFile("git", args, {
|
|
6948
|
+
cwd: repoPath,
|
|
6949
|
+
encoding: "utf8",
|
|
6950
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
6951
|
+
signal,
|
|
6952
|
+
}, (error, stdout) => error
|
|
6953
|
+
? reject(error instanceof Error
|
|
6954
|
+
? error
|
|
6955
|
+
: new Error(typeof error === "string" ? error : "Git subprocess failed"))
|
|
6956
|
+
: resolve(stdout));
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
async function collectRepoFilesWithCoverageAbortable(repoPath, signal) {
|
|
6960
|
+
let candidates = null;
|
|
6961
|
+
try {
|
|
6962
|
+
const gitRoot = (await execGitAbortable(repoPath, ["rev-parse", "--show-toplevel"], signal)).trim();
|
|
6963
|
+
if (gitRoot && isSameDir(gitRoot, repoPath)) {
|
|
6964
|
+
candidates = splitNulPaths(await execGitAbortable(repoPath, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], signal));
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
catch {
|
|
6968
|
+
throwIfAborted(signal);
|
|
6969
|
+
}
|
|
6970
|
+
const files = [];
|
|
6971
|
+
const skippedByExtension = new Map();
|
|
6972
|
+
const source = candidates ?? walkRepoFiles(repoPath);
|
|
6973
|
+
for (let index = 0; index < source.length; index++) {
|
|
6974
|
+
if (index % 256 === 0) {
|
|
6975
|
+
throwIfAborted(signal);
|
|
6976
|
+
await yieldToIndexEventLoop();
|
|
6977
|
+
}
|
|
6978
|
+
const file = source[index];
|
|
6979
|
+
if (candidates) {
|
|
6980
|
+
try {
|
|
6981
|
+
if (!fs.statSync(path.join(repoPath, file)).isFile())
|
|
6982
|
+
continue;
|
|
6983
|
+
}
|
|
6984
|
+
catch {
|
|
6985
|
+
continue;
|
|
6986
|
+
}
|
|
6987
|
+
}
|
|
6988
|
+
if (!isIndexablePath(file))
|
|
6989
|
+
continue;
|
|
6990
|
+
if (!isIndexableSourcePath(file)) {
|
|
6991
|
+
tallyOne(skippedByExtension, extensionBucket(file));
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
6994
|
+
if (!file.endsWith(".json") || isIndexableJsonCandidate(file, repoPath))
|
|
6995
|
+
files.push(file);
|
|
6996
|
+
else
|
|
6997
|
+
tallyOne(skippedByExtension, ".json");
|
|
6998
|
+
}
|
|
6999
|
+
return { files: files.sort(compareBytes), skippedByExtension };
|
|
7000
|
+
}
|
|
6769
7001
|
/**
|
|
6770
7002
|
* Enumerate the repo-relative files knodin indexes: source files plus dbt
|
|
6771
7003
|
* `manifest.json` files and Dockerfiles. Shared by the full index and the
|
|
@@ -6871,8 +7103,8 @@ function freshnessFingerprint(value) {
|
|
|
6871
7103
|
return null;
|
|
6872
7104
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
6873
7105
|
}
|
|
6874
|
-
function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride) {
|
|
6875
|
-
const git = gitWorkTreeProbe(repoPath);
|
|
7106
|
+
function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride, gitProbe) {
|
|
7107
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath) : gitProbe;
|
|
6876
7108
|
const currentHead = git && isSameDir(git.root, repoPath) ? git.head || null : gitHead(repoPath);
|
|
6877
7109
|
const indexedHead = db ? getMeta(db, "lastIndexedHead") || null : null;
|
|
6878
7110
|
const relation = commitRelation(repoPath, indexedHead, currentHead);
|
|
@@ -7288,7 +7520,7 @@ function gitWorkTreeProbe(repoPath) {
|
|
|
7288
7520
|
// Porcelain: `XY <path>` (or `XY <orig> -> <new>` for renames).
|
|
7289
7521
|
for (const part of line.slice(3).split(" -> ")) {
|
|
7290
7522
|
const p = part.trim().replace(/^"|"$/g, "");
|
|
7291
|
-
if (p)
|
|
7523
|
+
if (p && isIndexablePath(p))
|
|
7292
7524
|
dirty.push(p);
|
|
7293
7525
|
}
|
|
7294
7526
|
}
|
|
@@ -7322,7 +7554,7 @@ function fileDriftedFromIndexState(db, repoPath, relPath) {
|
|
|
7322
7554
|
return st !== null;
|
|
7323
7555
|
if (!st)
|
|
7324
7556
|
return true;
|
|
7325
|
-
return
|
|
7557
|
+
return row.mtimeMs !== st.mtimeMs || row.size !== st.size;
|
|
7326
7558
|
}
|
|
7327
7559
|
/**
|
|
7328
7560
|
* Record the working-tree state the index now reflects. Called at the end of a
|
|
@@ -7355,8 +7587,8 @@ function recordFreshnessBaseline(repoPath, db) {
|
|
|
7355
7587
|
* pretending. A brand-new file in a non-git repo is consequently picked up by
|
|
7356
7588
|
* the watcher or a cold start, not by this probe.
|
|
7357
7589
|
*/
|
|
7358
|
-
function probeDrift(repoPath, db) {
|
|
7359
|
-
const git = gitWorkTreeProbe(repoPath);
|
|
7590
|
+
function probeDrift(repoPath, db, gitProbe) {
|
|
7591
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath) : gitProbe;
|
|
7360
7592
|
if (git && isSameDir(git.root, repoPath)) {
|
|
7361
7593
|
if ((getMeta(db, "lastIndexedHead") ?? "") !== git.head) {
|
|
7362
7594
|
return { drifted: true, verifiable: true };
|
|
@@ -7462,7 +7694,7 @@ function stalenessFor(repoPath) {
|
|
|
7462
7694
|
return freshnessProbes.get(path.resolve(repoPath))?.staleness ?? "unknown";
|
|
7463
7695
|
}
|
|
7464
7696
|
/** Recursively indexes all matching files within the repository. */
|
|
7465
|
-
async function indexRepo(repoPath, db, progress, skipEmbeddings = false) {
|
|
7697
|
+
async function indexRepo(repoPath, db, progress, skipEmbeddings = false, publishProcessState = true) {
|
|
7466
7698
|
progress?.("collecting-files", 0, "Discovering indexable files");
|
|
7467
7699
|
const collected = measurePerfPhaseSync("file_collection", () => collectRepoFilesWithCoverage(repoPath));
|
|
7468
7700
|
const files = collected.files;
|
|
@@ -7508,17 +7740,26 @@ async function indexRepo(repoPath, db, progress, skipEmbeddings = false) {
|
|
|
7508
7740
|
setMeta(db, "mcpBackfillVersion", "17");
|
|
7509
7741
|
setMeta(db, COVERAGE_UNPARSED_META_KEY, JSON.stringify(Object.fromEntries(unparsedByExtension)));
|
|
7510
7742
|
recordFreshnessBaseline(repoPath, db);
|
|
7511
|
-
|
|
7743
|
+
if (publishProcessState)
|
|
7744
|
+
indexGeneration++;
|
|
7512
7745
|
}
|
|
7513
7746
|
// makeWatchIgnorePredicate: single source of truth is src/engine/prune.ts
|
|
7514
7747
|
// (R14) — imported above, no local copy here. chokidar v4/v5 accept only a
|
|
7515
7748
|
// function/regex/literal path (no globs), which is what that shared
|
|
7516
7749
|
// predicate provides.
|
|
7517
7750
|
/** Starts a chokidar file watcher to incrementally update SQLite indexes in real-time. */
|
|
7518
|
-
function startFileWatcher(repoPath, db) {
|
|
7751
|
+
function startFileWatcher(repoPath, db, watcherFileLimit = MAX_RECURSIVE_WATCH_FILES) {
|
|
7519
7752
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
7520
7753
|
if (watchers.has(resolvedRepoPath))
|
|
7521
7754
|
return;
|
|
7755
|
+
const indexedFileCount = db.query("SELECT COUNT(*) count FROM index_state").get()?.count ?? 0;
|
|
7756
|
+
if (indexedFileCount > watcherFileLimit) {
|
|
7757
|
+
watcherStates.set(resolvedRepoPath, {
|
|
7758
|
+
watcher: "scale-limited",
|
|
7759
|
+
diagnostic: "repository-too-large",
|
|
7760
|
+
});
|
|
7761
|
+
return;
|
|
7762
|
+
}
|
|
7522
7763
|
const queue = {
|
|
7523
7764
|
pending: new Set(),
|
|
7524
7765
|
timer: null,
|
|
@@ -7633,21 +7874,85 @@ function startFileWatcher(repoPath, db) {
|
|
|
7633
7874
|
watcher.on("unlink", enqueue);
|
|
7634
7875
|
watcher.on("ready", () => {
|
|
7635
7876
|
queue.ready = true;
|
|
7877
|
+
watcherStates.set(resolvedRepoPath, { watcher: "active" });
|
|
7878
|
+
});
|
|
7879
|
+
watcher.on("error", (error) => {
|
|
7880
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
7881
|
+
? String(error.code)
|
|
7882
|
+
: "";
|
|
7883
|
+
if (degradeWatcherForResourceLimit(resolvedRepoPath, code))
|
|
7884
|
+
return;
|
|
7885
|
+
console.error(`File watcher error for ${resolvedRepoPath}:`, error);
|
|
7636
7886
|
});
|
|
7637
|
-
watcher.on("error", (error) => console.error(`File watcher error for ${resolvedRepoPath}:`, error));
|
|
7638
7887
|
watchers.set(resolvedRepoPath, watcher);
|
|
7888
|
+
watcherStates.set(resolvedRepoPath, { watcher: "active" });
|
|
7889
|
+
}
|
|
7890
|
+
/** Convert native watch exhaustion into a bounded, query-safe freshness fallback. */
|
|
7891
|
+
export function degradeWatcherForResourceLimit(repoPath, code) {
|
|
7892
|
+
if (code !== "EMFILE" && code !== "ENOSPC")
|
|
7893
|
+
return false;
|
|
7894
|
+
const resolved = path.resolve(repoPath);
|
|
7895
|
+
const queue = watchQueues.get(resolved);
|
|
7896
|
+
if (queue) {
|
|
7897
|
+
queue.closed = true;
|
|
7898
|
+
if (queue.timer)
|
|
7899
|
+
clearTimeout(queue.timer);
|
|
7900
|
+
queue.timer = null;
|
|
7901
|
+
}
|
|
7902
|
+
watcherStates.set(resolved, {
|
|
7903
|
+
watcher: "resource-limited",
|
|
7904
|
+
diagnostic: "file-descriptor-limit",
|
|
7905
|
+
});
|
|
7906
|
+
const watcher = watchers.get(resolved);
|
|
7907
|
+
if (watcher) {
|
|
7908
|
+
void watcher.close().finally(() => {
|
|
7909
|
+
watchers.delete(resolved);
|
|
7910
|
+
watchQueues.delete(resolved);
|
|
7911
|
+
});
|
|
7912
|
+
}
|
|
7913
|
+
return true;
|
|
7914
|
+
}
|
|
7915
|
+
async function suspendFileWatcher(repoPath) {
|
|
7916
|
+
const resolved = path.resolve(repoPath);
|
|
7917
|
+
const watcher = watchers.get(resolved);
|
|
7918
|
+
if (!watcher)
|
|
7919
|
+
return false;
|
|
7920
|
+
await watcher.close();
|
|
7921
|
+
watchers.delete(resolved);
|
|
7922
|
+
const queue = watchQueues.get(resolved);
|
|
7923
|
+
if (queue) {
|
|
7924
|
+
queue.closed = true;
|
|
7925
|
+
if (queue.timer)
|
|
7926
|
+
clearTimeout(queue.timer);
|
|
7927
|
+
queue.timer = null;
|
|
7928
|
+
await queue.drain();
|
|
7929
|
+
watchQueues.delete(resolved);
|
|
7930
|
+
}
|
|
7931
|
+
watcherStates.set(resolved, { watcher: "disabled" });
|
|
7932
|
+
return true;
|
|
7639
7933
|
}
|
|
7640
7934
|
/** Open and initialize SQLite connection and full repo index. */
|
|
7641
7935
|
async function getOrInitDb(repoPath, options = {}) {
|
|
7642
7936
|
const normalizedPath = path.resolve(repoPath);
|
|
7643
|
-
|
|
7937
|
+
const selectedDatabasePath = explicitDatabasePath(normalizedPath, options.databasePath);
|
|
7938
|
+
if (!selectedDatabasePath)
|
|
7939
|
+
recoverInterruptedPromotion(normalizedPath);
|
|
7940
|
+
const cacheKey = databaseCacheKey(normalizedPath, selectedDatabasePath);
|
|
7941
|
+
let promise = initPromises.get(cacheKey);
|
|
7644
7942
|
if (!promise) {
|
|
7645
7943
|
promise = (async () => {
|
|
7646
7944
|
const isTest = process.env.VITEST || process.env.NODE_ENV === "test";
|
|
7647
7945
|
let dbPath;
|
|
7648
|
-
if (isTest) {
|
|
7946
|
+
if (isTest && !selectedDatabasePath) {
|
|
7649
7947
|
dbPath = ":memory:";
|
|
7650
7948
|
}
|
|
7949
|
+
else if (selectedDatabasePath) {
|
|
7950
|
+
await fs.promises.mkdir(path.dirname(selectedDatabasePath), {
|
|
7951
|
+
recursive: true,
|
|
7952
|
+
mode: 0o700,
|
|
7953
|
+
});
|
|
7954
|
+
dbPath = selectedDatabasePath;
|
|
7955
|
+
}
|
|
7651
7956
|
else {
|
|
7652
7957
|
const stateDir = resolveStateDir(normalizedPath);
|
|
7653
7958
|
await fs.promises.mkdir(stateDir, { recursive: true });
|
|
@@ -8032,15 +8337,27 @@ async function getOrInitDb(repoPath, options = {}) {
|
|
|
8032
8337
|
}
|
|
8033
8338
|
noteFreshness(normalizedPath, reindexed > 0 ? "reconciled" : "fresh");
|
|
8034
8339
|
}
|
|
8035
|
-
if (!isTest) {
|
|
8036
|
-
startFileWatcher(normalizedPath, db);
|
|
8340
|
+
if (!selectedDatabasePath && !isTest && options.watcher !== "disabled") {
|
|
8341
|
+
startFileWatcher(normalizedPath, db, options.watcherFileLimit);
|
|
8342
|
+
}
|
|
8343
|
+
else if (!watchers.has(normalizedPath)) {
|
|
8344
|
+
watcherStates.set(normalizedPath, { watcher: "disabled" });
|
|
8037
8345
|
}
|
|
8038
|
-
dbInstances.set(
|
|
8346
|
+
dbInstances.set(cacheKey, db);
|
|
8039
8347
|
return db;
|
|
8040
8348
|
})();
|
|
8041
|
-
initPromises.set(
|
|
8349
|
+
initPromises.set(cacheKey, promise);
|
|
8042
8350
|
}
|
|
8043
8351
|
const db = await promise;
|
|
8352
|
+
if (!selectedDatabasePath &&
|
|
8353
|
+
!process.env.VITEST &&
|
|
8354
|
+
process.env.NODE_ENV !== "test" &&
|
|
8355
|
+
options.watcher !== "disabled" &&
|
|
8356
|
+
!watchers.has(normalizedPath) &&
|
|
8357
|
+
watcherStates.get(normalizedPath)?.watcher !== "scale-limited" &&
|
|
8358
|
+
watcherStates.get(normalizedPath)?.watcher !== "resource-limited") {
|
|
8359
|
+
startFileWatcher(normalizedPath, db, options.watcherFileLimit);
|
|
8360
|
+
}
|
|
8044
8361
|
// Cold start reconciles once (above); this guard covers everything after it,
|
|
8045
8362
|
// so a session whose watcher dies still answers from a verified index.
|
|
8046
8363
|
if (!options.skipFreshnessGuard)
|
|
@@ -8892,7 +9209,7 @@ function reviewChangedFiles(base, repoPath, options, modified) {
|
|
|
8892
9209
|
args.push(safeReviewRevision(base || "HEAD"));
|
|
8893
9210
|
args.push("--");
|
|
8894
9211
|
for (const file of runGit(repoPath, args).split("\0")) {
|
|
8895
|
-
if (file)
|
|
9212
|
+
if (file && isIndexablePath(file))
|
|
8896
9213
|
files.add(file);
|
|
8897
9214
|
}
|
|
8898
9215
|
}
|
|
@@ -8909,7 +9226,7 @@ function reviewChangedFiles(base, repoPath, options, modified) {
|
|
|
8909
9226
|
"--exclude-standard",
|
|
8910
9227
|
"-z",
|
|
8911
9228
|
]).split("\0")) {
|
|
8912
|
-
if (file)
|
|
9229
|
+
if (file && isIndexablePath(file))
|
|
8913
9230
|
files.add(file);
|
|
8914
9231
|
}
|
|
8915
9232
|
}
|
|
@@ -10222,8 +10539,32 @@ function findImportSpecifierLines(absPath, symbol) {
|
|
|
10222
10539
|
* threaded through each operation's many return sites, so the tag cannot be
|
|
10223
10540
|
* forgotten on an early return (a not-found explain, a refused rename).
|
|
10224
10541
|
*/
|
|
10225
|
-
function withStaleness(engine, claimRepository) {
|
|
10542
|
+
function withStaleness(engine, claimRepository, openDb, openPolicy) {
|
|
10226
10543
|
return {
|
|
10544
|
+
createCandidate(repoPath, options) {
|
|
10545
|
+
claimRepository(repoPath);
|
|
10546
|
+
return engine.createCandidate(repoPath, options);
|
|
10547
|
+
},
|
|
10548
|
+
openCandidate(candidate) {
|
|
10549
|
+
claimRepository(candidate.repo);
|
|
10550
|
+
return engine.openCandidate(candidate);
|
|
10551
|
+
},
|
|
10552
|
+
reconcileCandidate(candidate, repoPath, changes, options) {
|
|
10553
|
+
claimRepository(repoPath);
|
|
10554
|
+
return engine.reconcileCandidate(candidate, repoPath, changes, options);
|
|
10555
|
+
},
|
|
10556
|
+
auditCandidate(candidate, options) {
|
|
10557
|
+
claimRepository(candidate.repo);
|
|
10558
|
+
return engine.auditCandidate(candidate, options);
|
|
10559
|
+
},
|
|
10560
|
+
promoteCandidate(candidate) {
|
|
10561
|
+
claimRepository(candidate.repo);
|
|
10562
|
+
return engine.promoteCandidate(candidate);
|
|
10563
|
+
},
|
|
10564
|
+
discardCandidate(candidate) {
|
|
10565
|
+
claimRepository(candidate.repo);
|
|
10566
|
+
return engine.discardCandidate(candidate);
|
|
10567
|
+
},
|
|
10227
10568
|
async explain(symbol, repoPath, detailLevel, selector) {
|
|
10228
10569
|
claimRepository(repoPath);
|
|
10229
10570
|
const result = await engine.explain(symbol, repoPath, detailLevel, selector);
|
|
@@ -10260,7 +10601,7 @@ function withStaleness(engine, claimRepository) {
|
|
|
10260
10601
|
const repos = await getFederatedRepos(repoPath);
|
|
10261
10602
|
const allRepos = await Promise.all(repos.map(async (repo) => ({
|
|
10262
10603
|
path: repo,
|
|
10263
|
-
db: await
|
|
10604
|
+
db: await openDb(repo),
|
|
10264
10605
|
})));
|
|
10265
10606
|
const analytics = await getGraphAnalyticsSnapshot(allRepos, path.resolve(repoPath), n, options.relationKinds);
|
|
10266
10607
|
result.communities = analytics.communities;
|
|
@@ -10292,9 +10633,11 @@ function withStaleness(engine, claimRepository) {
|
|
|
10292
10633
|
result.staleness = stalenessFor(repoPath);
|
|
10293
10634
|
return result;
|
|
10294
10635
|
},
|
|
10295
|
-
status(repoPath, options) {
|
|
10636
|
+
async status(repoPath, options) {
|
|
10296
10637
|
claimRepository(repoPath);
|
|
10297
|
-
|
|
10638
|
+
const result = await engine.status(repoPath, options);
|
|
10639
|
+
result.freshnessMechanism = freshnessMechanismFor(repoPath, openPolicy);
|
|
10640
|
+
return result;
|
|
10298
10641
|
},
|
|
10299
10642
|
repair(repoPath, options) {
|
|
10300
10643
|
claimRepository(repoPath);
|
|
@@ -10330,15 +10673,27 @@ function withStaleness(engine, claimRepository) {
|
|
|
10330
10673
|
},
|
|
10331
10674
|
};
|
|
10332
10675
|
}
|
|
10676
|
+
const DEFAULT_ENGINE_OPEN_POLICY = Object.freeze({ watcher: "enabled" });
|
|
10333
10677
|
/** Default SQLite + Tree-Sitter engine. */
|
|
10334
|
-
export function createEngine() {
|
|
10678
|
+
export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
10335
10679
|
const owner = Symbol("knodin-engine");
|
|
10336
10680
|
const ownedRepositories = new Set();
|
|
10681
|
+
const explicitDatabaseKeys = new Set();
|
|
10682
|
+
const candidateAudits = new Map();
|
|
10683
|
+
const candidates = new Map();
|
|
10337
10684
|
let closed = false;
|
|
10685
|
+
const openDb = (repoPath, options = {}) => getOrInitDb(repoPath, {
|
|
10686
|
+
...options,
|
|
10687
|
+
watcher: options.watcher ?? openPolicy.watcher,
|
|
10688
|
+
watcherFileLimit: options.watcherFileLimit ?? openPolicy.watcherFileLimit,
|
|
10689
|
+
databasePath: options.databasePath ?? openPolicy.databasePath,
|
|
10690
|
+
});
|
|
10338
10691
|
const claimRepository = (repoPath) => {
|
|
10339
10692
|
if (closed)
|
|
10340
10693
|
throw new Error("knodin engine is closed");
|
|
10341
10694
|
const resolved = path.resolve(repoPath);
|
|
10695
|
+
if (openPolicy.databasePath)
|
|
10696
|
+
explicitDatabaseKeys.add(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
10342
10697
|
if (ownedRepositories.has(resolved))
|
|
10343
10698
|
return;
|
|
10344
10699
|
ownedRepositories.add(resolved);
|
|
@@ -10346,14 +10701,395 @@ export function createEngine() {
|
|
|
10346
10701
|
owners.add(owner);
|
|
10347
10702
|
repositoryEngineOwners.set(resolved, owners);
|
|
10348
10703
|
};
|
|
10704
|
+
const closeDatabaseKey = (key) => {
|
|
10705
|
+
const db = dbInstances.get(key);
|
|
10706
|
+
if (db) {
|
|
10707
|
+
try {
|
|
10708
|
+
db.run("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
10709
|
+
}
|
|
10710
|
+
finally {
|
|
10711
|
+
db.close();
|
|
10712
|
+
}
|
|
10713
|
+
}
|
|
10714
|
+
dbInstances.delete(key);
|
|
10715
|
+
initPromises.delete(key);
|
|
10716
|
+
};
|
|
10717
|
+
const closeCandidateDatabase = (candidate) => {
|
|
10718
|
+
const key = databaseCacheKey(candidate.repo, candidate.databasePath);
|
|
10719
|
+
closeDatabaseKey(key);
|
|
10720
|
+
for (const suffix of ["-wal", "-shm"])
|
|
10721
|
+
fs.rmSync(`${candidate.databasePath}${suffix}`, { force: true });
|
|
10722
|
+
};
|
|
10723
|
+
const candidateDigest = (candidate) => crypto.createHash("sha256").update(fs.readFileSync(candidate.databasePath)).digest("hex");
|
|
10724
|
+
async function repairThroughCandidate(resolved, repairKey, options) {
|
|
10725
|
+
let outerLease;
|
|
10726
|
+
let candidate;
|
|
10727
|
+
let candidateEngine;
|
|
10728
|
+
let before;
|
|
10729
|
+
const activeOperation = {
|
|
10730
|
+
promise: undefined,
|
|
10731
|
+
subscribers: new Set(options?.onProgress ? [options.onProgress] : []),
|
|
10732
|
+
controller: new AbortController(),
|
|
10733
|
+
};
|
|
10734
|
+
const abortShared = () => activeOperation.controller.abort();
|
|
10735
|
+
if (options?.signal?.aborted)
|
|
10736
|
+
abortShared();
|
|
10737
|
+
else
|
|
10738
|
+
options?.signal?.addEventListener("abort", abortShared, { once: true });
|
|
10739
|
+
let pendingCompleted;
|
|
10740
|
+
const deliver = (event) => {
|
|
10741
|
+
for (const subscriber of [...activeOperation.subscribers]) {
|
|
10742
|
+
try {
|
|
10743
|
+
subscriber(event);
|
|
10744
|
+
}
|
|
10745
|
+
catch {
|
|
10746
|
+
// Progress subscribers cannot change repair correctness.
|
|
10747
|
+
}
|
|
10748
|
+
}
|
|
10749
|
+
};
|
|
10750
|
+
const forward = (event) => {
|
|
10751
|
+
if (event.phase === "completed")
|
|
10752
|
+
pendingCompleted = event;
|
|
10753
|
+
else
|
|
10754
|
+
deliver(event);
|
|
10755
|
+
};
|
|
10756
|
+
const operation = Promise.resolve()
|
|
10757
|
+
.then(async () => {
|
|
10758
|
+
outerLease = acquireRepairLease(resolved);
|
|
10759
|
+
before = await engine.status(resolved, {
|
|
10760
|
+
audit: "deep",
|
|
10761
|
+
signal: activeOperation.controller.signal,
|
|
10762
|
+
});
|
|
10763
|
+
await suspendFileWatcher(resolved);
|
|
10764
|
+
const activePath = resolveDbPath(resolved);
|
|
10765
|
+
if (fs.existsSync(activePath)) {
|
|
10766
|
+
candidate = await engine.createCandidate(resolved, { empty: true });
|
|
10767
|
+
const existing = dbInstances.get(resolved);
|
|
10768
|
+
const active = existing ?? new Database(activePath, { readonly: true });
|
|
10769
|
+
try {
|
|
10770
|
+
// VACUUM INTO reads a transactionally consistent image, including WAL
|
|
10771
|
+
// pages, without checkpointing or rewriting the active graph.
|
|
10772
|
+
const destination = candidate.databasePath.replaceAll("'", "''");
|
|
10773
|
+
active.run(`VACUUM INTO '${destination}'`);
|
|
10774
|
+
}
|
|
10775
|
+
finally {
|
|
10776
|
+
if (!existing)
|
|
10777
|
+
active.close();
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
else {
|
|
10781
|
+
candidate = await engine.createCandidate(resolved);
|
|
10782
|
+
}
|
|
10783
|
+
candidateEngine = createEngine({
|
|
10784
|
+
watcher: "disabled",
|
|
10785
|
+
databasePath: candidate.databasePath,
|
|
10786
|
+
repairLease: "disabled",
|
|
10787
|
+
});
|
|
10788
|
+
const staged = await candidateEngine.repair(resolved, {
|
|
10789
|
+
...options,
|
|
10790
|
+
signal: activeOperation.controller.signal,
|
|
10791
|
+
onProgress: forward,
|
|
10792
|
+
});
|
|
10793
|
+
await candidateEngine.close();
|
|
10794
|
+
candidateEngine = undefined;
|
|
10795
|
+
if (staged.cancelled || !staged.verified) {
|
|
10796
|
+
await engine.discardCandidate(candidate);
|
|
10797
|
+
candidate = undefined;
|
|
10798
|
+
const after = staged.cancelled
|
|
10799
|
+
? before
|
|
10800
|
+
: await engine.status(resolved, { audit: "deep" });
|
|
10801
|
+
if (pendingCompleted)
|
|
10802
|
+
deliver(pendingCompleted);
|
|
10803
|
+
return {
|
|
10804
|
+
...staged,
|
|
10805
|
+
before,
|
|
10806
|
+
after,
|
|
10807
|
+
repaired: [],
|
|
10808
|
+
remaining: after.missing.files.length + after.missing.records.length,
|
|
10809
|
+
outstandingIssues: {
|
|
10810
|
+
files: after.missing.files.length,
|
|
10811
|
+
records: after.missing.records.length,
|
|
10812
|
+
total: after.missing.files.length + after.missing.records.length,
|
|
10813
|
+
},
|
|
10814
|
+
};
|
|
10815
|
+
}
|
|
10816
|
+
const audit = await engine.auditCandidate(candidate, { mode: "deep" });
|
|
10817
|
+
if (!audit.healthy)
|
|
10818
|
+
throw new Error(`repair candidate audit failed: ${audit.issues.join(", ")}`);
|
|
10819
|
+
await engine.promoteCandidate(candidate);
|
|
10820
|
+
candidate = undefined;
|
|
10821
|
+
const after = await engine.status(resolved, { audit: "deep" });
|
|
10822
|
+
if (pendingCompleted)
|
|
10823
|
+
deliver(pendingCompleted);
|
|
10824
|
+
return {
|
|
10825
|
+
...staged,
|
|
10826
|
+
before,
|
|
10827
|
+
after,
|
|
10828
|
+
verified: after.status === "healthy",
|
|
10829
|
+
outstandingIssues: {
|
|
10830
|
+
files: after.missing.files.length,
|
|
10831
|
+
records: after.missing.records.length,
|
|
10832
|
+
total: after.missing.files.length + after.missing.records.length,
|
|
10833
|
+
},
|
|
10834
|
+
};
|
|
10835
|
+
})
|
|
10836
|
+
.catch((error) => {
|
|
10837
|
+
if (pendingCompleted)
|
|
10838
|
+
deliver({
|
|
10839
|
+
...pendingCompleted,
|
|
10840
|
+
phase: "failed",
|
|
10841
|
+
message: "Repair candidate failed before promotion",
|
|
10842
|
+
});
|
|
10843
|
+
throw error;
|
|
10844
|
+
});
|
|
10845
|
+
activeOperation.promise = operation;
|
|
10846
|
+
repairOperations.set(repairKey, activeOperation);
|
|
10847
|
+
try {
|
|
10848
|
+
return await operation;
|
|
10849
|
+
}
|
|
10850
|
+
finally {
|
|
10851
|
+
if (candidateEngine)
|
|
10852
|
+
await candidateEngine.close();
|
|
10853
|
+
if (candidate && candidates.has(candidate.id))
|
|
10854
|
+
await engine.discardCandidate(candidate);
|
|
10855
|
+
outerLease?.release();
|
|
10856
|
+
options?.signal?.removeEventListener("abort", abortShared);
|
|
10857
|
+
if (options?.onProgress)
|
|
10858
|
+
activeOperation.subscribers.delete(options.onProgress);
|
|
10859
|
+
if (repairOperations.get(repairKey) === activeOperation)
|
|
10860
|
+
repairOperations.delete(repairKey);
|
|
10861
|
+
if (openPolicy.watcher === "enabled" &&
|
|
10862
|
+
before?.schemaVersion === KNODIN_SCHEMA_VERSION &&
|
|
10863
|
+
fs.existsSync(resolveDbPath(resolved))) {
|
|
10864
|
+
await openDb(resolved, {
|
|
10865
|
+
skipContentReconciliation: true,
|
|
10866
|
+
skipFreshnessGuard: true,
|
|
10867
|
+
});
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
}
|
|
10349
10871
|
// Bound to a name (rather than returned as a bare object literal) so the
|
|
10350
10872
|
// `wiki` method below can call `engine.map(..., "standard")` and reuse its output
|
|
10351
10873
|
// instead of duplicating community-detection logic. Safe: `wiki`'s body
|
|
10352
10874
|
// only runs after this whole object has been constructed and assigned.
|
|
10353
10875
|
const engine = {
|
|
10876
|
+
async createCandidate(repoPath, options) {
|
|
10877
|
+
const resolved = path.resolve(repoPath);
|
|
10878
|
+
recoverInterruptedPromotion(resolved);
|
|
10879
|
+
const candidate = allocateCandidate(resolved);
|
|
10880
|
+
try {
|
|
10881
|
+
if (options?.empty) {
|
|
10882
|
+
// Restore writes verified snapshot bytes here while the engine retains
|
|
10883
|
+
// ownership, keeping deep audit and promotion mandatory.
|
|
10884
|
+
}
|
|
10885
|
+
else if (options?.sourceDatabasePath) {
|
|
10886
|
+
const source = path.resolve(options.sourceDatabasePath);
|
|
10887
|
+
if (!fs.existsSync(source) ||
|
|
10888
|
+
!fs.statSync(source).isFile() ||
|
|
10889
|
+
fs.lstatSync(source).isSymbolicLink())
|
|
10890
|
+
throw new Error("candidate source database must be a regular non-symbolic-link file");
|
|
10891
|
+
const header = fs.readFileSync(source).subarray(0, 16).toString("binary");
|
|
10892
|
+
if (header !== "SQLite format 3\u0000")
|
|
10893
|
+
throw new Error("candidate source is not a SQLite database");
|
|
10894
|
+
fs.copyFileSync(source, candidate.databasePath, fs.constants.COPYFILE_EXCL);
|
|
10895
|
+
fs.chmodSync(candidate.databasePath, 0o600);
|
|
10896
|
+
}
|
|
10897
|
+
else {
|
|
10898
|
+
await getOrInitDb(resolved, {
|
|
10899
|
+
databasePath: candidate.databasePath,
|
|
10900
|
+
watcher: "disabled",
|
|
10901
|
+
skipContentReconciliation: true,
|
|
10902
|
+
skipFreshnessGuard: true,
|
|
10903
|
+
});
|
|
10904
|
+
closeCandidateDatabase(candidate);
|
|
10905
|
+
}
|
|
10906
|
+
candidates.set(candidate.id, candidate);
|
|
10907
|
+
return candidate;
|
|
10908
|
+
}
|
|
10909
|
+
catch (error) {
|
|
10910
|
+
closeCandidateDatabase(candidate);
|
|
10911
|
+
discardCandidateFiles(resolved, candidate);
|
|
10912
|
+
throw error;
|
|
10913
|
+
}
|
|
10914
|
+
},
|
|
10915
|
+
async openCandidate(candidate) {
|
|
10916
|
+
assertCandidate(candidate.repo, candidate);
|
|
10917
|
+
if (!candidates.has(candidate.id))
|
|
10918
|
+
throw new Error("candidate is not owned by this engine");
|
|
10919
|
+
if (!fs.existsSync(candidate.databasePath))
|
|
10920
|
+
throw new Error("candidate database is missing");
|
|
10921
|
+
const header = fs.readFileSync(candidate.databasePath).subarray(0, 16).toString("binary");
|
|
10922
|
+
if (header !== "SQLite format 3\u0000")
|
|
10923
|
+
throw new Error("candidate is not a SQLite database");
|
|
10924
|
+
const probe = new Database(candidate.databasePath, { readonly: true });
|
|
10925
|
+
try {
|
|
10926
|
+
const integrity = probe.query("PRAGMA quick_check").all();
|
|
10927
|
+
if (integrity.length !== 1 || Object.values(integrity[0] ?? {})[0] !== "ok")
|
|
10928
|
+
throw new Error("candidate SQLite integrity check failed");
|
|
10929
|
+
const schemaVersion = probe.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
10930
|
+
if (schemaVersion !== KNODIN_SCHEMA_VERSION)
|
|
10931
|
+
throw new Error(`candidate schema ${schemaVersion} is incompatible with ${KNODIN_SCHEMA_VERSION}`);
|
|
10932
|
+
}
|
|
10933
|
+
finally {
|
|
10934
|
+
probe.close();
|
|
10935
|
+
}
|
|
10936
|
+
await getOrInitDb(candidate.repo, {
|
|
10937
|
+
databasePath: candidate.databasePath,
|
|
10938
|
+
watcher: "disabled",
|
|
10939
|
+
skipContentReconciliation: true,
|
|
10940
|
+
skipFreshnessGuard: true,
|
|
10941
|
+
});
|
|
10942
|
+
return candidate;
|
|
10943
|
+
},
|
|
10944
|
+
async reconcileCandidate(candidate, repoPath, changes, options) {
|
|
10945
|
+
const resolved = path.resolve(repoPath);
|
|
10946
|
+
assertCandidate(resolved, candidate);
|
|
10947
|
+
await engine.openCandidate(candidate);
|
|
10948
|
+
candidateAudits.delete(candidate.id);
|
|
10949
|
+
const db = dbInstances.get(databaseCacheKey(resolved, candidate.databasePath));
|
|
10950
|
+
if (!db)
|
|
10951
|
+
throw new Error("candidate database did not open");
|
|
10952
|
+
if (!changes) {
|
|
10953
|
+
await indexRepo(resolved, db, options?.onProgress, options?.skipEmbeddings === true, false);
|
|
10954
|
+
return { reconciled: collectRepoFiles(resolved) };
|
|
10955
|
+
}
|
|
10956
|
+
const reconciled = [...new Set(changes)]
|
|
10957
|
+
.map((file) => path.relative(resolved, path.resolve(resolved, file)))
|
|
10958
|
+
.filter((file) => file !== "" && !file.startsWith("..") && !path.isAbsolute(file))
|
|
10959
|
+
.sort(compareBytes);
|
|
10960
|
+
for (const file of reconciled) {
|
|
10961
|
+
const absolute = path.join(resolved, file);
|
|
10962
|
+
if (fs.existsSync(absolute) && isIndexableSourcePath(file)) {
|
|
10963
|
+
await indexFile(absolute, file, resolved, db);
|
|
10964
|
+
recordIndexState(db, resolved, file);
|
|
10965
|
+
}
|
|
10966
|
+
else {
|
|
10967
|
+
db.run("BEGIN TRANSACTION;");
|
|
10968
|
+
try {
|
|
10969
|
+
deleteSymbolsForFile(db, file);
|
|
10970
|
+
db.run('DELETE FROM "references" WHERE callerFile = ?', [file]);
|
|
10971
|
+
db.run("DELETE FROM dependencies WHERE fromFile = ?", [file]);
|
|
10972
|
+
db.run("DELETE FROM mcp_tools WHERE filePath = ?", [file]);
|
|
10973
|
+
db.run("COMMIT;");
|
|
10974
|
+
removeIndexState(db, file);
|
|
10975
|
+
}
|
|
10976
|
+
catch (error) {
|
|
10977
|
+
db.run("ROLLBACK;");
|
|
10978
|
+
throw error;
|
|
10979
|
+
}
|
|
10980
|
+
}
|
|
10981
|
+
}
|
|
10982
|
+
await refreshFlowsForApexFiles(db, resolved, reconciled);
|
|
10983
|
+
persistSymbolIdentities(db, resolved, reconciled);
|
|
10984
|
+
reconcileTypeScriptDi(db, resolved);
|
|
10985
|
+
await indexEmbeddings(db, resolved);
|
|
10986
|
+
setMeta(db, "lastIndexedHead", gitHead(resolved) ?? "");
|
|
10987
|
+
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
10988
|
+
recordFreshnessBaseline(resolved, db);
|
|
10989
|
+
return { reconciled };
|
|
10990
|
+
},
|
|
10991
|
+
async auditCandidate(candidate) {
|
|
10992
|
+
assertCandidate(candidate.repo, candidate);
|
|
10993
|
+
if (!candidates.has(candidate.id))
|
|
10994
|
+
throw new Error("candidate is not owned by this engine");
|
|
10995
|
+
closeCandidateDatabase(candidate);
|
|
10996
|
+
const issues = [];
|
|
10997
|
+
const db = new Database(candidate.databasePath, { readonly: true });
|
|
10998
|
+
try {
|
|
10999
|
+
const quick = db.query("PRAGMA quick_check").all();
|
|
11000
|
+
if (quick.length !== 1 || Object.values(quick[0] ?? {})[0] !== "ok")
|
|
11001
|
+
issues.push("sqlite-integrity");
|
|
11002
|
+
if (db.query("PRAGMA foreign_key_check").all().length > 0)
|
|
11003
|
+
issues.push("foreign-key-integrity");
|
|
11004
|
+
const schema = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
11005
|
+
if (schema !== KNODIN_SCHEMA_VERSION)
|
|
11006
|
+
issues.push("database-schema-version");
|
|
11007
|
+
const required = [
|
|
11008
|
+
"symbols",
|
|
11009
|
+
"references",
|
|
11010
|
+
"dependencies",
|
|
11011
|
+
"symbol_embeddings",
|
|
11012
|
+
"index_state",
|
|
11013
|
+
"meta",
|
|
11014
|
+
];
|
|
11015
|
+
const tables = new Set(db
|
|
11016
|
+
.query("SELECT name FROM sqlite_master WHERE type='table'")
|
|
11017
|
+
.all()
|
|
11018
|
+
.map((row) => row.name));
|
|
11019
|
+
for (const table of required)
|
|
11020
|
+
if (!tables.has(table))
|
|
11021
|
+
issues.push(`missing-table:${table}`);
|
|
11022
|
+
if (tables.has("symbols") && tables.has("symbol_embeddings")) {
|
|
11023
|
+
const symbols = db.query("SELECT COUNT(*) count FROM symbols").get()?.count ?? 0;
|
|
11024
|
+
const embeddings = db.query("SELECT COUNT(*) count FROM symbol_embeddings").get()
|
|
11025
|
+
?.count ?? 0;
|
|
11026
|
+
if (symbols !== embeddings)
|
|
11027
|
+
issues.push("semantic-index-incomplete");
|
|
11028
|
+
}
|
|
11029
|
+
if (tables.has("index_state")) {
|
|
11030
|
+
const sourceFiles = collectRepoFiles(candidate.repo);
|
|
11031
|
+
const sourceSet = new Set(sourceFiles);
|
|
11032
|
+
const indexed = db
|
|
11033
|
+
.query("SELECT filePath, mtimeMs, size FROM index_state")
|
|
11034
|
+
.all();
|
|
11035
|
+
const indexedSet = new Set(indexed.map((row) => row.filePath));
|
|
11036
|
+
if (sourceFiles.some((file) => !indexedSet.has(file)))
|
|
11037
|
+
issues.push("source-coverage");
|
|
11038
|
+
if (indexed.some((row) => !sourceSet.has(row.filePath)))
|
|
11039
|
+
issues.push("ineligible-index-state");
|
|
11040
|
+
if (indexed.some((row) => fileDriftedFromIndexState(db, candidate.repo, row.filePath)))
|
|
11041
|
+
issues.push("source-snapshot-drift");
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
finally {
|
|
11045
|
+
db.close();
|
|
11046
|
+
}
|
|
11047
|
+
const databaseSha256 = candidateDigest(candidate);
|
|
11048
|
+
if (issues.length === 0)
|
|
11049
|
+
candidateAudits.set(candidate.id, databaseSha256);
|
|
11050
|
+
else
|
|
11051
|
+
candidateAudits.delete(candidate.id);
|
|
11052
|
+
return { healthy: issues.length === 0, issues: [...new Set(issues)], databaseSha256 };
|
|
11053
|
+
},
|
|
11054
|
+
async promoteCandidate(candidate) {
|
|
11055
|
+
assertCandidate(candidate.repo, candidate);
|
|
11056
|
+
const auditedDigest = candidateAudits.get(candidate.id);
|
|
11057
|
+
if (!auditedDigest)
|
|
11058
|
+
throw new Error("candidate must pass deep audit before promotion");
|
|
11059
|
+
const confirmation = await engine.auditCandidate(candidate, { mode: "deep" });
|
|
11060
|
+
if (!confirmation.healthy || confirmation.databaseSha256 !== auditedDigest)
|
|
11061
|
+
throw new Error("candidate or repository changed after deep audit");
|
|
11062
|
+
closeCandidateDatabase(candidate);
|
|
11063
|
+
if (candidateDigest(candidate) !== auditedDigest)
|
|
11064
|
+
throw new Error("candidate changed after deep audit");
|
|
11065
|
+
const resolved = candidate.repo;
|
|
11066
|
+
await suspendFileWatcher(resolved);
|
|
11067
|
+
const active = dbInstances.get(resolved);
|
|
11068
|
+
if (active) {
|
|
11069
|
+
active.run("PRAGMA wal_checkpoint(TRUNCATE);");
|
|
11070
|
+
active.close();
|
|
11071
|
+
}
|
|
11072
|
+
dbInstances.delete(resolved);
|
|
11073
|
+
initPromises.delete(resolved);
|
|
11074
|
+
for (const suffix of ["-wal", "-shm"])
|
|
11075
|
+
fs.rmSync(`${resolveDbPath(resolved)}${suffix}`, { force: true });
|
|
11076
|
+
const backupPath = promoteCandidateFile(resolved, candidate);
|
|
11077
|
+
candidates.delete(candidate.id);
|
|
11078
|
+
candidateAudits.delete(candidate.id);
|
|
11079
|
+
invalidateResolutionCaches(resolved);
|
|
11080
|
+
indexGeneration++;
|
|
11081
|
+
return { backupPath };
|
|
11082
|
+
},
|
|
11083
|
+
async discardCandidate(candidate) {
|
|
11084
|
+
assertCandidate(candidate.repo, candidate);
|
|
11085
|
+
closeCandidateDatabase(candidate);
|
|
11086
|
+
discardCandidateFiles(candidate.repo, candidate);
|
|
11087
|
+
candidateAudits.delete(candidate.id);
|
|
11088
|
+
candidates.delete(candidate.id);
|
|
11089
|
+
},
|
|
10354
11090
|
async explain(symbol, repoPath, detailLevel = "standard", selector = {}) {
|
|
10355
11091
|
const repos = await getFederatedRepos(repoPath);
|
|
10356
|
-
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await
|
|
11092
|
+
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
10357
11093
|
let targetRepoPath = repoPath;
|
|
10358
11094
|
let primaryDef = null;
|
|
10359
11095
|
const matches = [];
|
|
@@ -10518,7 +11254,7 @@ export function createEngine() {
|
|
|
10518
11254
|
};
|
|
10519
11255
|
},
|
|
10520
11256
|
async review(base, repoPath, detailLevel = "standard", options = {}) {
|
|
10521
|
-
const db = await
|
|
11257
|
+
const db = await openDb(repoPath);
|
|
10522
11258
|
const { modifiedFiles, changedFiles } = measurePerfPhaseSync("review_diff_discovery", () => {
|
|
10523
11259
|
const modifiedFiles = parseGitDiff(base, repoPath, options);
|
|
10524
11260
|
return {
|
|
@@ -10715,7 +11451,7 @@ export function createEngine() {
|
|
|
10715
11451
|
};
|
|
10716
11452
|
},
|
|
10717
11453
|
async dependencyGraph(repoPath) {
|
|
10718
|
-
const db = await
|
|
11454
|
+
const db = await openDb(repoPath);
|
|
10719
11455
|
const rows = db
|
|
10720
11456
|
.query("SELECT fromFile, toFile, kind, COALESCE(confidence, 1.0) AS confidence, sourceEvidence FROM dependencies")
|
|
10721
11457
|
.all();
|
|
@@ -10763,7 +11499,7 @@ export function createEngine() {
|
|
|
10763
11499
|
const repos = await getFederatedRepos(repoPath);
|
|
10764
11500
|
const allRepos = await Promise.all(repos.map(async (repo) => ({
|
|
10765
11501
|
path: repo,
|
|
10766
|
-
db: await
|
|
11502
|
+
db: await openDb(repo),
|
|
10767
11503
|
})));
|
|
10768
11504
|
const cacheKey = JSON.stringify([
|
|
10769
11505
|
resolvedRepoPath,
|
|
@@ -10846,7 +11582,7 @@ export function createEngine() {
|
|
|
10846
11582
|
return cached.result;
|
|
10847
11583
|
const computeMap = (async () => {
|
|
10848
11584
|
const repos = await getFederatedRepos(repoPath);
|
|
10849
|
-
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await
|
|
11585
|
+
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
10850
11586
|
const formatPath = makePathFormatter(resolvedRepoPath);
|
|
10851
11587
|
const analytics = await getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath);
|
|
10852
11588
|
const { communities, symbolCommunity } = analytics;
|
|
@@ -11108,7 +11844,7 @@ export function createEngine() {
|
|
|
11108
11844
|
// Minimal mode
|
|
11109
11845
|
const languageOfFile = createLanguageMemo();
|
|
11110
11846
|
const repos = await getFederatedRepos(repoPath);
|
|
11111
|
-
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await
|
|
11847
|
+
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
11112
11848
|
const minimalCommunities = await Promise.all(fullMap.communities.map(async (c) => {
|
|
11113
11849
|
const files = c.files ?? [];
|
|
11114
11850
|
const langs = new Map();
|
|
@@ -11157,11 +11893,13 @@ export function createEngine() {
|
|
|
11157
11893
|
},
|
|
11158
11894
|
async status(repoPath, options = {}) {
|
|
11159
11895
|
const resolved = path.resolve(repoPath);
|
|
11896
|
+
throwIfAborted(options.signal);
|
|
11160
11897
|
// Audit must not run the cold-start reconciler: otherwise status would
|
|
11161
11898
|
// repair and timestamp drift before reporting it. Reuse an active DB in
|
|
11162
11899
|
// long-lived processes, or open the existing local DB read-only.
|
|
11163
|
-
const
|
|
11164
|
-
const
|
|
11900
|
+
const diskDbPath = explicitDatabasePath(resolved, openPolicy.databasePath) ?? resolveDbPath(resolved);
|
|
11901
|
+
const statusCacheKey = databaseCacheKey(resolved, openPolicy.databasePath);
|
|
11902
|
+
const activeDb = dbInstances.get(statusCacheKey);
|
|
11165
11903
|
const db = activeDb ??
|
|
11166
11904
|
(fs.existsSync(diskDbPath) ? new Database(diskDbPath, { readonly: true }) : null);
|
|
11167
11905
|
const verifiedAt = new Date().toISOString();
|
|
@@ -11222,6 +11960,7 @@ export function createEngine() {
|
|
|
11222
11960
|
verification: { mode: "live-operation", verifiedAt },
|
|
11223
11961
|
repairSteps: [],
|
|
11224
11962
|
activity,
|
|
11963
|
+
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
11225
11964
|
};
|
|
11226
11965
|
}
|
|
11227
11966
|
if (!db) {
|
|
@@ -11253,15 +11992,56 @@ export function createEngine() {
|
|
|
11253
11992
|
lastIndexedHead: "",
|
|
11254
11993
|
freshness: buildFreshnessEnvelope(resolved, null, verifiedAt, "repair-needed"),
|
|
11255
11994
|
verification: { mode: "deep-audit", verifiedAt },
|
|
11995
|
+
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
11256
11996
|
repairSteps: [
|
|
11257
11997
|
"Run `knodin repair` to create the local index.",
|
|
11258
11998
|
"Run `knodin status` again to verify health.",
|
|
11259
11999
|
],
|
|
11260
12000
|
};
|
|
11261
12001
|
}
|
|
11262
|
-
const databaseFingerprint = databaseFileFingerprint(diskDbPath);
|
|
11263
|
-
|
|
11264
|
-
if (
|
|
12002
|
+
const databaseFingerprint = databaseFileFingerprint(diskDbPath, db);
|
|
12003
|
+
let effectiveAudit = options.audit ?? "cached";
|
|
12004
|
+
if (effectiveAudit === "adaptive") {
|
|
12005
|
+
let indexedFileCount = 0;
|
|
12006
|
+
try {
|
|
12007
|
+
indexedFileCount =
|
|
12008
|
+
db.query("SELECT COUNT(*) count FROM index_state").get()
|
|
12009
|
+
?.count ?? 0;
|
|
12010
|
+
}
|
|
12011
|
+
catch {
|
|
12012
|
+
// A malformed schema needs the existing deep diagnostic path.
|
|
12013
|
+
}
|
|
12014
|
+
if (adaptiveStatusStrategy(indexedFileCount) === "deep") {
|
|
12015
|
+
effectiveAudit = "deep";
|
|
12016
|
+
}
|
|
12017
|
+
else {
|
|
12018
|
+
const schemaVersion = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
12019
|
+
const persisted = readPersistedStatusAudit(resolved, diskDbPath, databaseFingerprint, schemaVersion);
|
|
12020
|
+
if (persisted) {
|
|
12021
|
+
throwIfAborted(options.signal);
|
|
12022
|
+
const gitProbe = measurePerfPhaseSync("freshness_probe", () => gitWorkTreeProbe(resolved));
|
|
12023
|
+
const drift = probeDrift(resolved, db, gitProbe);
|
|
12024
|
+
const probeVerifiedAt = new Date().toISOString();
|
|
12025
|
+
const freshness = buildFreshnessEnvelope(resolved, db, probeVerifiedAt, drift.verifiable ? (drift.drifted ? "stale-working-tree" : undefined) : "unknown", gitProbe);
|
|
12026
|
+
if (!activeDb)
|
|
12027
|
+
db.close();
|
|
12028
|
+
const cachedResult = structuredClone(persisted.result);
|
|
12029
|
+
return {
|
|
12030
|
+
...cachedResult,
|
|
12031
|
+
status: drift.verifiable && !drift.drifted ? "healthy" : "stale",
|
|
12032
|
+
freshness,
|
|
12033
|
+
verification: {
|
|
12034
|
+
mode: "persisted-audit",
|
|
12035
|
+
auditVerifiedAt: persisted.auditVerifiedAt,
|
|
12036
|
+
verifiedAt: probeVerifiedAt,
|
|
12037
|
+
},
|
|
12038
|
+
};
|
|
12039
|
+
}
|
|
12040
|
+
effectiveAudit = "deep";
|
|
12041
|
+
}
|
|
12042
|
+
}
|
|
12043
|
+
const cached = statusCache.get(statusCacheKey);
|
|
12044
|
+
if (effectiveAudit !== "deep" &&
|
|
11265
12045
|
cached?.generation === indexGeneration &&
|
|
11266
12046
|
cached.databaseFingerprint === databaseFingerprint) {
|
|
11267
12047
|
const auditVerifiedAt = Date.parse(cached.result.verification.verifiedAt);
|
|
@@ -11287,6 +12067,8 @@ export function createEngine() {
|
|
|
11287
12067
|
...cachedResult,
|
|
11288
12068
|
verification: {
|
|
11289
12069
|
mode: "cached-after-freshness-probe",
|
|
12070
|
+
auditVerifiedAt: cached.result.verification.auditVerifiedAt ??
|
|
12071
|
+
cached.result.verification.verifiedAt,
|
|
11290
12072
|
verifiedAt: new Date(lease.at).toISOString(),
|
|
11291
12073
|
},
|
|
11292
12074
|
};
|
|
@@ -11301,6 +12083,8 @@ export function createEngine() {
|
|
|
11301
12083
|
...cachedResult,
|
|
11302
12084
|
verification: {
|
|
11303
12085
|
mode: "cached-after-freshness-probe",
|
|
12086
|
+
auditVerifiedAt: cached.result.verification.auditVerifiedAt ??
|
|
12087
|
+
cached.result.verification.verifiedAt,
|
|
11304
12088
|
verifiedAt,
|
|
11305
12089
|
},
|
|
11306
12090
|
};
|
|
@@ -11311,7 +12095,11 @@ export function createEngine() {
|
|
|
11311
12095
|
// reports repair steps instead of hiding the database problem.
|
|
11312
12096
|
}
|
|
11313
12097
|
}
|
|
11314
|
-
|
|
12098
|
+
throwIfAborted(options.signal);
|
|
12099
|
+
const collected = options.signal
|
|
12100
|
+
? await measurePerfPhase("status_audit", () => collectRepoFilesWithCoverageAbortable(resolved, options.signal))
|
|
12101
|
+
: measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
|
|
12102
|
+
throwIfAborted(options.signal);
|
|
11315
12103
|
const sourceFiles = collected.files;
|
|
11316
12104
|
const coverageSkips = buildCoverageSkips(collected.skippedByExtension, db);
|
|
11317
12105
|
const schemaVersion = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
@@ -11373,6 +12161,7 @@ export function createEngine() {
|
|
|
11373
12161
|
lastIndexedHead: "",
|
|
11374
12162
|
freshness: buildFreshnessEnvelope(resolved, db, verifiedAt, "repair-needed"),
|
|
11375
12163
|
verification: { mode: "deep-audit", verifiedAt },
|
|
12164
|
+
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
11376
12165
|
repairSteps: schemaVersion > KNODIN_SCHEMA_VERSION
|
|
11377
12166
|
? [
|
|
11378
12167
|
"Upgrade knodin to a version that supports this local graph schema.",
|
|
@@ -11394,25 +12183,29 @@ export function createEngine() {
|
|
|
11394
12183
|
.all()
|
|
11395
12184
|
.map((r) => r.filePath));
|
|
11396
12185
|
const missingFiles = sourceFiles.filter((file) => !indexed.has(file));
|
|
11397
|
-
const staleFiles =
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
.
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
12186
|
+
const staleFiles = [];
|
|
12187
|
+
const ineligibleFiles = [];
|
|
12188
|
+
const damagedFiles = [];
|
|
12189
|
+
for (let index = 0; index < indexedRows.length; index++) {
|
|
12190
|
+
if (options.signal && index % 256 === 0) {
|
|
12191
|
+
throwIfAborted(options.signal);
|
|
12192
|
+
await yieldToIndexEventLoop();
|
|
12193
|
+
}
|
|
12194
|
+
const row = indexedRows[index];
|
|
12195
|
+
const exists = fs.existsSync(path.join(resolved, row.filePath));
|
|
12196
|
+
if (!sourceFileSet.has(row.filePath)) {
|
|
12197
|
+
(exists ? ineligibleFiles : staleFiles).push(row.filePath);
|
|
12198
|
+
continue;
|
|
12199
|
+
}
|
|
11407
12200
|
try {
|
|
11408
12201
|
const stat = fs.statSync(path.join(resolved, row.filePath));
|
|
11409
|
-
|
|
12202
|
+
if (stat.size !== row.size || stat.mtimeMs !== row.mtimeMs)
|
|
12203
|
+
damagedFiles.push(row.filePath);
|
|
11410
12204
|
}
|
|
11411
12205
|
catch {
|
|
11412
|
-
|
|
12206
|
+
// A concurrently removed file is represented by the next audit/probe.
|
|
11413
12207
|
}
|
|
11414
|
-
}
|
|
11415
|
-
.map((row) => row.filePath);
|
|
12208
|
+
}
|
|
11416
12209
|
const count = (sql) => db.query(sql).get()?.count ?? 0;
|
|
11417
12210
|
const orphaned = {
|
|
11418
12211
|
embeddings: count(`SELECT COUNT(*) count FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`),
|
|
@@ -11462,7 +12255,8 @@ export function createEngine() {
|
|
|
11462
12255
|
lastIndexedHead: getMeta(db, "lastIndexedHead") ?? "",
|
|
11463
12256
|
freshness,
|
|
11464
12257
|
indexGeneration,
|
|
11465
|
-
verification: { mode: "deep-audit", verifiedAt },
|
|
12258
|
+
verification: { mode: "deep-audit", auditVerifiedAt: verifiedAt, verifiedAt },
|
|
12259
|
+
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
11466
12260
|
repairSteps: needsRepair
|
|
11467
12261
|
? [
|
|
11468
12262
|
"Run `knodin repair` to rebuild only missing or damaged local state.",
|
|
@@ -11470,20 +12264,31 @@ export function createEngine() {
|
|
|
11470
12264
|
]
|
|
11471
12265
|
: [],
|
|
11472
12266
|
};
|
|
11473
|
-
if (!activeDb)
|
|
11474
|
-
db.close();
|
|
11475
12267
|
if (!needsRepair && !stale)
|
|
11476
12268
|
noteFreshness(resolved, "fresh");
|
|
11477
|
-
|
|
12269
|
+
if (activeDb) {
|
|
12270
|
+
try {
|
|
12271
|
+
db.run("PRAGMA wal_checkpoint(PASSIVE);");
|
|
12272
|
+
}
|
|
12273
|
+
catch {
|
|
12274
|
+
// A concurrent reader can defer checkpointing; the WAL fingerprint remains binding.
|
|
12275
|
+
}
|
|
12276
|
+
}
|
|
12277
|
+
const finalDatabaseFingerprint = databaseFileFingerprint(diskDbPath, db);
|
|
12278
|
+
setBoundedCache(statusCache, statusCacheKey, {
|
|
11478
12279
|
generation: indexGeneration,
|
|
11479
|
-
databaseFingerprint,
|
|
12280
|
+
databaseFingerprint: finalDatabaseFingerprint,
|
|
11480
12281
|
result: structuredClone(result),
|
|
11481
12282
|
}, STATUS_CACHE_LIMIT);
|
|
12283
|
+
writePersistedStatusAudit(resolved, diskDbPath, finalDatabaseFingerprint, result);
|
|
12284
|
+
if (!activeDb)
|
|
12285
|
+
db.close();
|
|
11482
12286
|
return result;
|
|
11483
12287
|
},
|
|
11484
12288
|
async repair(repoPath, options) {
|
|
11485
12289
|
const resolved = path.resolve(repoPath);
|
|
11486
|
-
const
|
|
12290
|
+
const repairKey = databaseCacheKey(resolved, openPolicy.databasePath);
|
|
12291
|
+
const inFlight = repairOperations.get(repairKey);
|
|
11487
12292
|
if (inFlight) {
|
|
11488
12293
|
if (options?.onProgress)
|
|
11489
12294
|
inFlight.subscribers.add(options.onProgress);
|
|
@@ -11503,7 +12308,11 @@ export function createEngine() {
|
|
|
11503
12308
|
options?.signal?.removeEventListener("abort", abortShared);
|
|
11504
12309
|
}
|
|
11505
12310
|
}
|
|
11506
|
-
|
|
12311
|
+
if (!openPolicy.databasePath && !process.env.VITEST && process.env.NODE_ENV !== "test")
|
|
12312
|
+
return repairThroughCandidate(resolved, repairKey, options);
|
|
12313
|
+
const watcherStateBefore = watcherStates.get(resolved)?.watcher;
|
|
12314
|
+
if (!openPolicy.databasePath)
|
|
12315
|
+
invalidateResolutionCaches(resolved);
|
|
11507
12316
|
const operationId = crypto.randomUUID();
|
|
11508
12317
|
const startedAt = Date.now();
|
|
11509
12318
|
let sequence = 0;
|
|
@@ -11559,17 +12368,22 @@ export function createEngine() {
|
|
|
11559
12368
|
};
|
|
11560
12369
|
const operation = Promise.resolve()
|
|
11561
12370
|
.then(async () => {
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
|
|
12371
|
+
if (!openPolicy.databasePath)
|
|
12372
|
+
await suspendFileWatcher(resolved);
|
|
12373
|
+
if (openPolicy.repairLease !== "disabled")
|
|
12374
|
+
repairLease = acquireRepairLease(resolved);
|
|
12375
|
+
const cancelBoundary = () => throwIfAborted(activeOperation.controller.signal);
|
|
11567
12376
|
const invalidateCommittedWork = () => {
|
|
11568
|
-
if (
|
|
12377
|
+
if (!openPolicy.databasePath &&
|
|
12378
|
+
committedWork &&
|
|
12379
|
+
indexGeneration === generationBeforeRepair)
|
|
11569
12380
|
indexGeneration++;
|
|
11570
12381
|
};
|
|
11571
12382
|
emitProgress("audit", 0, "Auditing repository health");
|
|
11572
|
-
before = await engine.status(resolved
|
|
12383
|
+
before = await engine.status(resolved, {
|
|
12384
|
+
audit: "deep",
|
|
12385
|
+
signal: activeOperation.controller.signal,
|
|
12386
|
+
});
|
|
11573
12387
|
emitProgress("audit", 1, "Repository health audit completed", {
|
|
11574
12388
|
phaseTotal: 1,
|
|
11575
12389
|
});
|
|
@@ -11609,9 +12423,9 @@ export function createEngine() {
|
|
|
11609
12423
|
// Repair partial current-schema databases before normal initialization,
|
|
11610
12424
|
// whose reconciliation queries require these columns. ALTER preserves all
|
|
11611
12425
|
// healthy index_state rows; the reconciler then refreshes their snapshots.
|
|
11612
|
-
const diskDbPath = resolveDbPath(resolved);
|
|
12426
|
+
const diskDbPath = explicitDatabasePath(resolved, openPolicy.databasePath) ?? resolveDbPath(resolved);
|
|
11613
12427
|
if (fs.existsSync(diskDbPath)) {
|
|
11614
|
-
const activeDb = dbInstances.get(
|
|
12428
|
+
const activeDb = dbInstances.get(repairKey);
|
|
11615
12429
|
const schemaDb = activeDb ?? new Database(diskDbPath);
|
|
11616
12430
|
const hasIndexState = schemaDb
|
|
11617
12431
|
.query("SELECT COUNT(*) count FROM sqlite_master WHERE type='table' AND name='index_state'")
|
|
@@ -11636,10 +12450,11 @@ export function createEngine() {
|
|
|
11636
12450
|
// A preceding audit already established the exact repair set. Reuse an
|
|
11637
12451
|
// active writable handle so getOrInitDb does not run the freshness guard
|
|
11638
12452
|
// again and independently reconcile the same paths.
|
|
11639
|
-
const db = dbInstances.get(
|
|
11640
|
-
(await
|
|
12453
|
+
const db = dbInstances.get(repairKey) ??
|
|
12454
|
+
(await openDb(resolved, {
|
|
11641
12455
|
skipContentReconciliation: true,
|
|
11642
12456
|
skipFreshnessGuard: true,
|
|
12457
|
+
watcher: "disabled",
|
|
11643
12458
|
}));
|
|
11644
12459
|
const schemaPrepared = before.schemaVersion !== KNODIN_SCHEMA_VERSION;
|
|
11645
12460
|
if (schemaPrepared)
|
|
@@ -11714,7 +12529,11 @@ export function createEngine() {
|
|
|
11714
12529
|
phaseTotal: 1,
|
|
11715
12530
|
});
|
|
11716
12531
|
cancelBoundary();
|
|
11717
|
-
|
|
12532
|
+
const identityScope = inheritedPostprocessing ? undefined : repairPaths;
|
|
12533
|
+
if (options?.signal)
|
|
12534
|
+
await persistSymbolIdentitiesCancellable(db, resolved, identityScope, activeOperation.controller.signal);
|
|
12535
|
+
else
|
|
12536
|
+
persistSymbolIdentities(db, resolved, identityScope);
|
|
11718
12537
|
if (repairAffectsTypeScriptDi)
|
|
11719
12538
|
reconcileTypeScriptDi(db, resolved);
|
|
11720
12539
|
committedWork = true;
|
|
@@ -11737,7 +12556,7 @@ export function createEngine() {
|
|
|
11737
12556
|
const model = details.modelFile ? ` (${details.modelFile})` : "";
|
|
11738
12557
|
emitProgress("embeddings", 0, `${message}${model}`);
|
|
11739
12558
|
}
|
|
11740
|
-
});
|
|
12559
|
+
}, activeOperation.controller.signal);
|
|
11741
12560
|
committedWork = true;
|
|
11742
12561
|
setMeta(db, "repairPostprocessingPending", "0");
|
|
11743
12562
|
overallCompleted++;
|
|
@@ -11793,7 +12612,10 @@ export function createEngine() {
|
|
|
11793
12612
|
phaseTotal: 1,
|
|
11794
12613
|
});
|
|
11795
12614
|
cancelBoundary();
|
|
11796
|
-
let after = await engine.status(resolved, {
|
|
12615
|
+
let after = await engine.status(resolved, {
|
|
12616
|
+
audit: "deep",
|
|
12617
|
+
signal: activeOperation.controller.signal,
|
|
12618
|
+
});
|
|
11797
12619
|
cancelBoundary();
|
|
11798
12620
|
overallCompleted++;
|
|
11799
12621
|
emitProgress("verification", 1, "Repair verification completed", {
|
|
@@ -11816,21 +12638,27 @@ export function createEngine() {
|
|
|
11816
12638
|
}
|
|
11817
12639
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
11818
12640
|
recordFreshnessBaseline(resolved, db);
|
|
11819
|
-
statusCache.delete(resolved);
|
|
11820
|
-
const candidate = await engine.status(resolved, {
|
|
12641
|
+
statusCache.delete(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
12642
|
+
const candidate = await engine.status(resolved, {
|
|
12643
|
+
audit: "deep",
|
|
12644
|
+
signal: activeOperation.controller.signal,
|
|
12645
|
+
});
|
|
11821
12646
|
if (candidate.status === "healthy") {
|
|
11822
12647
|
db.run("COMMIT;");
|
|
11823
12648
|
after = candidate;
|
|
11824
12649
|
}
|
|
11825
12650
|
else {
|
|
11826
12651
|
db.run("ROLLBACK;");
|
|
11827
|
-
statusCache.delete(resolved);
|
|
11828
|
-
after = await engine.status(resolved, {
|
|
12652
|
+
statusCache.delete(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
12653
|
+
after = await engine.status(resolved, {
|
|
12654
|
+
audit: "deep",
|
|
12655
|
+
signal: activeOperation.controller.signal,
|
|
12656
|
+
});
|
|
11829
12657
|
}
|
|
11830
12658
|
}
|
|
11831
12659
|
catch (error) {
|
|
11832
12660
|
db.run("ROLLBACK;");
|
|
11833
|
-
statusCache.delete(resolved);
|
|
12661
|
+
statusCache.delete(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
11834
12662
|
throw error;
|
|
11835
12663
|
}
|
|
11836
12664
|
}
|
|
@@ -11855,10 +12683,14 @@ export function createEngine() {
|
|
|
11855
12683
|
return result;
|
|
11856
12684
|
})
|
|
11857
12685
|
.catch(async (error) => {
|
|
11858
|
-
if (error
|
|
11859
|
-
if (
|
|
12686
|
+
if (wasAborted(error, activeOperation.controller.signal)) {
|
|
12687
|
+
if (!openPolicy.databasePath &&
|
|
12688
|
+
committedWork &&
|
|
12689
|
+
indexGeneration === generationBeforeRepair)
|
|
11860
12690
|
indexGeneration++;
|
|
11861
|
-
const after =
|
|
12691
|
+
const after = openPolicy.databasePath
|
|
12692
|
+
? before
|
|
12693
|
+
: await engine.status(resolved, { audit: "deep" });
|
|
11862
12694
|
const remaining = Math.max(0, repairPaths.length - completedFiles);
|
|
11863
12695
|
emitProgress("cancelled", completedFiles, `Repair cancelled with ${remaining} file(s) remaining`, { phaseTotal: repairPaths.length });
|
|
11864
12696
|
return {
|
|
@@ -11882,7 +12714,7 @@ export function createEngine() {
|
|
|
11882
12714
|
throw error;
|
|
11883
12715
|
});
|
|
11884
12716
|
activeOperation.promise = operation;
|
|
11885
|
-
repairOperations.set(
|
|
12717
|
+
repairOperations.set(repairKey, activeOperation);
|
|
11886
12718
|
try {
|
|
11887
12719
|
return await operation;
|
|
11888
12720
|
}
|
|
@@ -11891,223 +12723,306 @@ export function createEngine() {
|
|
|
11891
12723
|
options?.signal?.removeEventListener("abort", abortShared);
|
|
11892
12724
|
if (options?.onProgress)
|
|
11893
12725
|
activeOperation.subscribers.delete(options.onProgress);
|
|
11894
|
-
if (repairOperations.get(
|
|
11895
|
-
repairOperations.delete(
|
|
12726
|
+
if (repairOperations.get(repairKey) === activeOperation)
|
|
12727
|
+
repairOperations.delete(repairKey);
|
|
12728
|
+
const activeDb = dbInstances.get(resolved);
|
|
12729
|
+
if (!openPolicy.databasePath &&
|
|
12730
|
+
activeDb &&
|
|
12731
|
+
openPolicy.watcher === "enabled" &&
|
|
12732
|
+
watcherStateBefore !== "scale-limited" &&
|
|
12733
|
+
watcherStateBefore !== "resource-limited")
|
|
12734
|
+
startFileWatcher(resolved, activeDb, openPolicy.watcherFileLimit);
|
|
11896
12735
|
}
|
|
11897
12736
|
},
|
|
11898
12737
|
async index(repoPath, files, clean = false, options) {
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
12738
|
+
const resolved = path.resolve(repoPath);
|
|
12739
|
+
if (clean &&
|
|
12740
|
+
(!files || files.length === 0) &&
|
|
12741
|
+
!openPolicy.databasePath &&
|
|
12742
|
+
!process.env.VITEST &&
|
|
12743
|
+
process.env.NODE_ENV !== "test") {
|
|
12744
|
+
const progress = createIndexProgressReporter(options?.onProgress);
|
|
12745
|
+
progress("starting", 0, "Creating isolated clean-index candidate");
|
|
12746
|
+
const candidate = await engine.createCandidate(resolved);
|
|
12747
|
+
let promoted = false;
|
|
12748
|
+
try {
|
|
12749
|
+
const reconciliation = await engine.reconcileCandidate(candidate, resolved, undefined, {
|
|
12750
|
+
onProgress: progress,
|
|
12751
|
+
skipEmbeddings: options?.skipEmbeddings,
|
|
12752
|
+
});
|
|
12753
|
+
progress("verifying", 0, "Deep-auditing clean-index candidate", { phaseTotal: 1 });
|
|
12754
|
+
const audit = await engine.auditCandidate(candidate, { mode: "deep" });
|
|
12755
|
+
if (!audit.healthy)
|
|
12756
|
+
throw new Error(`clean-index candidate audit failed: ${audit.issues.join(", ")}`);
|
|
12757
|
+
await engine.promoteCandidate(candidate);
|
|
12758
|
+
promoted = true;
|
|
12759
|
+
const activeDb = await openDb(resolved, {
|
|
12760
|
+
skipContentReconciliation: true,
|
|
12761
|
+
skipFreshnessGuard: true,
|
|
12762
|
+
watcher: openPolicy.watcher,
|
|
12763
|
+
});
|
|
12764
|
+
if (!process.env.VITEST && process.env.NODE_ENV !== "test")
|
|
12765
|
+
publishStructuralSnapshot(resolved, activeDb);
|
|
12766
|
+
const health = await engine.status(resolved, {
|
|
12767
|
+
audit: "deep",
|
|
12768
|
+
ignoreActiveOperation: true,
|
|
12769
|
+
});
|
|
12770
|
+
const issueCount = health.missing.files.length + health.missing.records.length;
|
|
12771
|
+
progress("verifying", 1, health.status === "healthy"
|
|
12772
|
+
? "Clean-index candidate promoted and verified"
|
|
12773
|
+
: `Promoted graph has ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: 1 });
|
|
12774
|
+
progress("completed", reconciliation.reconciled.length, "Clean index completed", {
|
|
12775
|
+
phaseTotal: reconciliation.reconciled.length,
|
|
12776
|
+
});
|
|
12777
|
+
return {
|
|
12778
|
+
indexed: reconciliation.reconciled,
|
|
12779
|
+
unchanged: [],
|
|
12780
|
+
verification: {
|
|
12781
|
+
status: health.status,
|
|
12782
|
+
issueCount,
|
|
12783
|
+
missing: health.missing,
|
|
12784
|
+
verifiedAt: health.verification.verifiedAt,
|
|
12785
|
+
},
|
|
12786
|
+
};
|
|
12787
|
+
}
|
|
12788
|
+
catch (error) {
|
|
12789
|
+
if (!promoted && candidates.has(candidate.id))
|
|
12790
|
+
await engine.discardCandidate(candidate);
|
|
12791
|
+
if (openPolicy.watcher === "enabled" && fs.existsSync(resolveDbPath(resolved)))
|
|
12792
|
+
await openDb(resolved, {
|
|
12793
|
+
skipContentReconciliation: true,
|
|
12794
|
+
skipFreshnessGuard: true,
|
|
12795
|
+
});
|
|
12796
|
+
throw error;
|
|
12797
|
+
}
|
|
12798
|
+
}
|
|
12799
|
+
const watcherStateBefore = watcherStates.get(resolved)?.watcher;
|
|
12800
|
+
if (!openPolicy.databasePath)
|
|
12801
|
+
await suspendFileWatcher(resolved);
|
|
12802
|
+
try {
|
|
12803
|
+
invalidateResolutionCaches(repoPath);
|
|
12804
|
+
const indexed = [];
|
|
12805
|
+
const unchanged = [];
|
|
12806
|
+
let scipReport;
|
|
12807
|
+
let sarifReport;
|
|
12808
|
+
const progress = createIndexProgressReporter(options?.onProgress);
|
|
12809
|
+
progress("starting", 0, "Opening local graph database");
|
|
12810
|
+
const db = await openDb(repoPath, {
|
|
12811
|
+
onProgress: progress,
|
|
12812
|
+
watcher: "disabled",
|
|
12813
|
+
// The explicit lifecycle operation owns all content work. Otherwise
|
|
12814
|
+
// getOrInitDb's freshness guard can perform a full, invisible
|
|
12815
|
+
// reconciliation before a targeted index even starts.
|
|
12816
|
+
skipContentReconciliation: true,
|
|
12817
|
+
skipFreshnessGuard: true,
|
|
11918
12818
|
});
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
else {
|
|
11931
|
-
// File was deleted
|
|
11932
|
-
db.run("BEGIN TRANSACTION;");
|
|
11933
|
-
try {
|
|
11934
|
-
deleteSymbolsForFile(db, relativePath);
|
|
11935
|
-
db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
|
|
11936
|
-
db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
|
|
11937
|
-
db.run("DELETE FROM mcp_tools WHERE filePath = ?", [relativePath]);
|
|
11938
|
-
db.run("COMMIT;");
|
|
12819
|
+
let completionMessage = "Local graph is current";
|
|
12820
|
+
if (files && files.length > 0) {
|
|
12821
|
+
progress("indexing-files", 0, `Checking ${files.length.toLocaleString()} requested files`, {
|
|
12822
|
+
phaseTotal: files.length,
|
|
12823
|
+
});
|
|
12824
|
+
for (const [fileIndex, file] of files.entries()) {
|
|
12825
|
+
const relativePath = path.isAbsolute(file) ? path.relative(repoPath, file) : file;
|
|
12826
|
+
const absPath = path.resolve(repoPath, relativePath);
|
|
12827
|
+
const needsUpdate = clean || fileDriftedFromIndexState(db, repoPath, relativePath);
|
|
12828
|
+
if (!needsUpdate) {
|
|
12829
|
+
unchanged.push(relativePath);
|
|
11939
12830
|
}
|
|
11940
|
-
|
|
11941
|
-
db
|
|
11942
|
-
|
|
12831
|
+
else if (fs.existsSync(absPath)) {
|
|
12832
|
+
await indexFile(absPath, relativePath, repoPath, db);
|
|
12833
|
+
recordIndexState(db, repoPath, relativePath);
|
|
11943
12834
|
}
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
.query("SELECT COUNT(*) as count FROM symbols")
|
|
11975
|
-
.all();
|
|
11976
|
-
if (countResult && countResult.length > 0 && countResult[0].count > 0) {
|
|
11977
|
-
hasSymbols = true;
|
|
12835
|
+
else {
|
|
12836
|
+
// File was deleted
|
|
12837
|
+
db.run("BEGIN TRANSACTION;");
|
|
12838
|
+
try {
|
|
12839
|
+
deleteSymbolsForFile(db, relativePath);
|
|
12840
|
+
db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
|
|
12841
|
+
db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
|
|
12842
|
+
db.run("DELETE FROM mcp_tools WHERE filePath = ?", [relativePath]);
|
|
12843
|
+
db.run("COMMIT;");
|
|
12844
|
+
}
|
|
12845
|
+
catch (e) {
|
|
12846
|
+
db.run("ROLLBACK;");
|
|
12847
|
+
console.error(`Error deleting file from index: ${relativePath}`, e);
|
|
12848
|
+
}
|
|
12849
|
+
removeIndexState(db, relativePath);
|
|
12850
|
+
}
|
|
12851
|
+
if (needsUpdate)
|
|
12852
|
+
indexed.push(relativePath);
|
|
12853
|
+
progress("indexing-files", fileIndex + 1, `Checking ${files.length.toLocaleString()} requested files`, { phaseTotal: files.length });
|
|
12854
|
+
}
|
|
12855
|
+
const refreshedFlows = await refreshFlowsForApexFiles(db, repoPath, indexed);
|
|
12856
|
+
for (const flow of refreshedFlows)
|
|
12857
|
+
if (!indexed.includes(flow))
|
|
12858
|
+
indexed.push(flow);
|
|
12859
|
+
if (indexed.length > 0) {
|
|
12860
|
+
progress("finalizing", 0, "Refreshing identities and semantic embeddings");
|
|
12861
|
+
persistSymbolIdentities(db, repoPath, indexed);
|
|
12862
|
+
reconcileTypeScriptDi(db, repoPath);
|
|
12863
|
+
await indexEmbeddings(db, repoPath, progress);
|
|
12864
|
+
indexGeneration++;
|
|
11978
12865
|
}
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
12866
|
+
const alreadyCurrent = unchanged.length === 1
|
|
12867
|
+
? "1 requested file already current"
|
|
12868
|
+
: `${unchanged.length.toLocaleString()} requested files already current`;
|
|
12869
|
+
completionMessage =
|
|
12870
|
+
indexed.length === 0
|
|
12871
|
+
? alreadyCurrent
|
|
12872
|
+
: `Indexed ${indexed.length.toLocaleString()} requested file(s); ${alreadyCurrent}`;
|
|
11984
12873
|
}
|
|
11985
12874
|
else {
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
11991
|
-
|
|
11992
|
-
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
|
|
12008
|
-
|
|
12009
|
-
|
|
12010
|
-
|
|
12011
|
-
|
|
12012
|
-
languages: facts.languages,
|
|
12013
|
-
indexers: facts.indexers,
|
|
12014
|
-
elapsedMs: facts.elapsedMs,
|
|
12015
|
-
bounds: {
|
|
12016
|
-
maxBytes: bounds.maxBytes,
|
|
12017
|
-
maxFiles: bounds.maxFiles,
|
|
12018
|
-
maxFacts: bounds.maxFacts,
|
|
12019
|
-
timeoutMs: bounds.timeoutMs,
|
|
12020
|
-
},
|
|
12021
|
-
};
|
|
12022
|
-
for (const file of affectedFiles)
|
|
12023
|
-
if (!indexed.includes(file))
|
|
12875
|
+
// Full re-index or incremental sync
|
|
12876
|
+
let hasSymbols = false;
|
|
12877
|
+
try {
|
|
12878
|
+
const countResult = db
|
|
12879
|
+
.query("SELECT COUNT(*) as count FROM symbols")
|
|
12880
|
+
.all();
|
|
12881
|
+
if (countResult && countResult.length > 0 && countResult[0].count > 0) {
|
|
12882
|
+
hasSymbols = true;
|
|
12883
|
+
}
|
|
12884
|
+
}
|
|
12885
|
+
catch (_) { }
|
|
12886
|
+
if (!clean && hasSymbols) {
|
|
12887
|
+
progress("collecting-files", 0, "Checking existing local graph for changes");
|
|
12888
|
+
await reconcileIndex(repoPath, db, progress);
|
|
12889
|
+
}
|
|
12890
|
+
else {
|
|
12891
|
+
await indexRepo(repoPath, db, progress, options?.skipEmbeddings === true);
|
|
12892
|
+
}
|
|
12893
|
+
// Reuse the same candidate set as indexing/reconciliation. This keeps
|
|
12894
|
+
// LWC HTML/CSS bundle members visible in the response without admitting
|
|
12895
|
+
// arbitrary web assets to the source index.
|
|
12896
|
+
// Appended in a loop, not `push(...files)`: spreading an array as
|
|
12897
|
+
// arguments overflows the call stack once a repository is large
|
|
12898
|
+
// enough. Observed on a 40k-file checkout, which is well within the
|
|
12899
|
+
// range knodin is meant to index.
|
|
12900
|
+
for (const file of collectRepoFiles(repoPath))
|
|
12024
12901
|
indexed.push(file);
|
|
12025
|
-
|
|
12026
|
-
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12902
|
+
completionMessage = `${indexed.length.toLocaleString()} repository file(s) are current`;
|
|
12903
|
+
}
|
|
12904
|
+
if (options?.scip) {
|
|
12905
|
+
const facts = readScipIndex(repoPath, options.scip);
|
|
12906
|
+
const affectedFiles = persistScipFacts(db, repoPath, facts);
|
|
12907
|
+
progress("finalizing", 0, "Refreshing SCIP identities and semantic embeddings");
|
|
12908
|
+
await indexEmbeddings(db, repoPath, progress);
|
|
12909
|
+
indexGeneration++;
|
|
12910
|
+
const bounds = { ...SCIP_DEFAULT_LIMITS, ...options.scip };
|
|
12911
|
+
scipReport = {
|
|
12912
|
+
provenance: "scip",
|
|
12913
|
+
inputPath: facts.inputPath,
|
|
12914
|
+
bytes: facts.bytes,
|
|
12915
|
+
files: facts.files,
|
|
12916
|
+
facts: facts.facts,
|
|
12917
|
+
languages: facts.languages,
|
|
12918
|
+
indexers: facts.indexers,
|
|
12919
|
+
elapsedMs: facts.elapsedMs,
|
|
12920
|
+
bounds: {
|
|
12921
|
+
maxBytes: bounds.maxBytes,
|
|
12922
|
+
maxFiles: bounds.maxFiles,
|
|
12923
|
+
maxFacts: bounds.maxFacts,
|
|
12924
|
+
timeoutMs: bounds.timeoutMs,
|
|
12925
|
+
},
|
|
12926
|
+
};
|
|
12927
|
+
for (const file of affectedFiles)
|
|
12928
|
+
if (!indexed.includes(file))
|
|
12929
|
+
indexed.push(file);
|
|
12930
|
+
}
|
|
12931
|
+
if (options?.sarif) {
|
|
12932
|
+
const facts = readSarifLog(repoPath, options.sarif);
|
|
12933
|
+
persistSarifFindings(db, facts);
|
|
12934
|
+
// Findings are not graph facts, so no re-embedding and no generation
|
|
12935
|
+
// bump: the graph is unchanged and existing answers stay valid.
|
|
12936
|
+
const bounds = { ...SARIF_DEFAULT_LIMITS, ...options.sarif };
|
|
12937
|
+
sarifReport = {
|
|
12938
|
+
provenance: "sarif",
|
|
12939
|
+
inputPath: facts.inputPath,
|
|
12940
|
+
bytes: facts.bytes,
|
|
12941
|
+
tools: facts.tools,
|
|
12942
|
+
rules: facts.rules,
|
|
12943
|
+
files: facts.files,
|
|
12944
|
+
findings: facts.findings.length,
|
|
12945
|
+
skippedUnresolved: facts.skippedUnresolved,
|
|
12946
|
+
elapsedMs: facts.elapsedMs,
|
|
12947
|
+
bounds: {
|
|
12948
|
+
maxBytes: bounds.maxBytes,
|
|
12949
|
+
maxFindings: bounds.maxFindings,
|
|
12950
|
+
timeoutMs: bounds.timeoutMs,
|
|
12951
|
+
},
|
|
12952
|
+
};
|
|
12953
|
+
}
|
|
12954
|
+
progress("verifying", 0, "Verifying local graph health", {
|
|
12955
|
+
phaseTotal: 1,
|
|
12956
|
+
});
|
|
12957
|
+
let health = await engine.status(repoPath, {
|
|
12067
12958
|
audit: "deep",
|
|
12068
12959
|
ignoreActiveOperation: true,
|
|
12069
12960
|
});
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
// Explicit indexing counts as a successful reconciliation only after
|
|
12077
|
-
// the audit proves that no requested or unrequested drift remains.
|
|
12078
|
-
if (files && files.length > 0) {
|
|
12961
|
+
const contentVerified = health.coverage.sourceFiles === health.coverage.indexedFiles &&
|
|
12962
|
+
health.missing.files.length === 0 &&
|
|
12963
|
+
health.missing.records.length === 0 &&
|
|
12964
|
+
Object.values(health.orphaned).every((count) => count === 0);
|
|
12965
|
+
if (contentVerified &&
|
|
12966
|
+
(health.freshness.state === "stale-head" || health.freshness.state === "unknown")) {
|
|
12079
12967
|
setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
|
|
12080
12968
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
12081
12969
|
recordFreshnessBaseline(repoPath, db);
|
|
12970
|
+
statusCache.delete(databaseCacheKey(repoPath, openPolicy.databasePath));
|
|
12971
|
+
health = await engine.status(repoPath, {
|
|
12972
|
+
audit: "deep",
|
|
12973
|
+
ignoreActiveOperation: true,
|
|
12974
|
+
});
|
|
12975
|
+
}
|
|
12976
|
+
// The postcondition belongs to this index result. Do not turn the next
|
|
12977
|
+
// explicit `status` call into a cache hit against an internal audit.
|
|
12978
|
+
statusCache.delete(databaseCacheKey(repoPath, openPolicy.databasePath));
|
|
12979
|
+
const issueCount = health.missing.files.length + health.missing.records.length;
|
|
12980
|
+
if (health.status === "healthy") {
|
|
12981
|
+
// Explicit indexing counts as a successful reconciliation only after
|
|
12982
|
+
// the audit proves that no requested or unrequested drift remains.
|
|
12983
|
+
if (files && files.length > 0) {
|
|
12984
|
+
setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
|
|
12985
|
+
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
12986
|
+
recordFreshnessBaseline(repoPath, db);
|
|
12987
|
+
}
|
|
12988
|
+
noteFreshness(repoPath, "fresh");
|
|
12989
|
+
// Test engines use an in-memory graph and must not create untracked
|
|
12990
|
+
// repository artifacts that pollute Git-diff/review fixtures.
|
|
12991
|
+
if (!process.env.VITEST && process.env.NODE_ENV !== "test")
|
|
12992
|
+
publishStructuralSnapshot(repoPath, db);
|
|
12082
12993
|
}
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12994
|
+
else {
|
|
12995
|
+
freshnessProbes.delete(path.resolve(repoPath));
|
|
12996
|
+
}
|
|
12997
|
+
progress("verifying", 1, health.status === "healthy"
|
|
12998
|
+
? "Local graph health verified"
|
|
12999
|
+
: `Local graph still has ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: 1 });
|
|
13000
|
+
const completed = files?.length ?? indexed.length;
|
|
13001
|
+
progress("completed", completed, health.status === "healthy"
|
|
13002
|
+
? completionMessage
|
|
13003
|
+
: `${completionMessage}; verification found ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: completed });
|
|
13004
|
+
return {
|
|
13005
|
+
indexed,
|
|
13006
|
+
unchanged,
|
|
13007
|
+
scip: scipReport,
|
|
13008
|
+
sarif: sarifReport,
|
|
13009
|
+
verification: {
|
|
13010
|
+
status: health.status,
|
|
13011
|
+
issueCount,
|
|
13012
|
+
missing: health.missing,
|
|
13013
|
+
verifiedAt: health.verification.verifiedAt,
|
|
13014
|
+
},
|
|
13015
|
+
};
|
|
13016
|
+
}
|
|
13017
|
+
finally {
|
|
13018
|
+
const activeDb = dbInstances.get(resolved);
|
|
13019
|
+
if (!openPolicy.databasePath &&
|
|
13020
|
+
activeDb &&
|
|
13021
|
+
openPolicy.watcher === "enabled" &&
|
|
13022
|
+
watcherStateBefore !== "scale-limited" &&
|
|
13023
|
+
watcherStateBefore !== "resource-limited")
|
|
13024
|
+
startFileWatcher(resolved, activeDb, openPolicy.watcherFileLimit);
|
|
12088
13025
|
}
|
|
12089
|
-
else {
|
|
12090
|
-
freshnessProbes.delete(path.resolve(repoPath));
|
|
12091
|
-
}
|
|
12092
|
-
progress("verifying", 1, health.status === "healthy"
|
|
12093
|
-
? "Local graph health verified"
|
|
12094
|
-
: `Local graph still has ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: 1 });
|
|
12095
|
-
const completed = files?.length ?? indexed.length;
|
|
12096
|
-
progress("completed", completed, health.status === "healthy"
|
|
12097
|
-
? completionMessage
|
|
12098
|
-
: `${completionMessage}; verification found ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: completed });
|
|
12099
|
-
return {
|
|
12100
|
-
indexed,
|
|
12101
|
-
unchanged,
|
|
12102
|
-
scip: scipReport,
|
|
12103
|
-
sarif: sarifReport,
|
|
12104
|
-
verification: {
|
|
12105
|
-
status: health.status,
|
|
12106
|
-
issueCount,
|
|
12107
|
-
missing: health.missing,
|
|
12108
|
-
verifiedAt: health.verification.verifiedAt,
|
|
12109
|
-
},
|
|
12110
|
-
};
|
|
12111
13026
|
},
|
|
12112
13027
|
async search(query, repoPath, limit, options = {}) {
|
|
12113
13028
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
@@ -12117,7 +13032,7 @@ export function createEngine() {
|
|
|
12117
13032
|
if (options.testScope && !["all", "test", "production"].includes(options.testScope))
|
|
12118
13033
|
throw new Error("knodin search: invalid testScope");
|
|
12119
13034
|
const repos = options.federate === false ? [resolvedRepoPath] : await getFederatedRepos(repoPath);
|
|
12120
|
-
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await
|
|
13035
|
+
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
12121
13036
|
// Computed before any retrieval route runs, so every exit path can report
|
|
12122
13037
|
// it: a short result list caused by missing embeddings must not be
|
|
12123
13038
|
// indistinguishable from a short list caused by the query.
|
|
@@ -12569,8 +13484,21 @@ export function createEngine() {
|
|
|
12569
13484
|
pattern === "lsp_implementations") {
|
|
12570
13485
|
return await runReadOnlyLspQuery(pattern, target, path.resolve(repoPath));
|
|
12571
13486
|
}
|
|
13487
|
+
if (pattern === "file_metrics") {
|
|
13488
|
+
const fileMetrics = collectFileMetrics(path.resolve(repoPath), {
|
|
13489
|
+
base: options.base,
|
|
13490
|
+
testScope: options.testScope,
|
|
13491
|
+
});
|
|
13492
|
+
return {
|
|
13493
|
+
pattern,
|
|
13494
|
+
target: "",
|
|
13495
|
+
count: fileMetrics.files.length,
|
|
13496
|
+
results: [],
|
|
13497
|
+
fileMetrics,
|
|
13498
|
+
};
|
|
13499
|
+
}
|
|
12572
13500
|
const repos = await getFederatedRepos(repoPath);
|
|
12573
|
-
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await
|
|
13501
|
+
const allRepos = await Promise.all(repos.map(async (r) => ({ path: r, db: await openDb(r) })));
|
|
12574
13502
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
12575
13503
|
const formatPath = makePathFormatter(resolvedRepoPath);
|
|
12576
13504
|
const primary = allRepos.find((r) => r.path === repoPath || r.path === resolvedRepoPath) ?? allRepos[0];
|
|
@@ -14760,7 +15688,7 @@ export function createEngine() {
|
|
|
14760
15688
|
}
|
|
14761
15689
|
},
|
|
14762
15690
|
async rename(oldName, newName, repoPath, apply = false, verify = true, selector = {}) {
|
|
14763
|
-
const db = await
|
|
15691
|
+
const db = await openDb(repoPath);
|
|
14764
15692
|
const resolvedRepoPath = path.resolve(repoPath);
|
|
14765
15693
|
const resolution = resolveSymbolRows(db, resolvedRepoPath, oldName, selector);
|
|
14766
15694
|
if (resolution.ambiguity)
|
|
@@ -15069,26 +15997,37 @@ export function createEngine() {
|
|
|
15069
15997
|
db.close();
|
|
15070
15998
|
dbInstances.delete(repo);
|
|
15071
15999
|
initPromises.delete(repo);
|
|
16000
|
+
watcherStates.delete(repo);
|
|
16001
|
+
}
|
|
16002
|
+
for (const key of explicitDatabaseKeys) {
|
|
16003
|
+
const db = dbInstances.get(key);
|
|
16004
|
+
if (db)
|
|
16005
|
+
db.close();
|
|
16006
|
+
dbInstances.delete(key);
|
|
16007
|
+
initPromises.delete(key);
|
|
16008
|
+
statusCache.delete(key);
|
|
16009
|
+
}
|
|
16010
|
+
if (repositoryEngineOwners.size === 0) {
|
|
16011
|
+
freshnessProbes.clear();
|
|
16012
|
+
mapCache.clear();
|
|
16013
|
+
minimalMapCache.clear();
|
|
16014
|
+
graphAnalyticsCache.clear();
|
|
16015
|
+
architectureMetadataCache.clear();
|
|
16016
|
+
architectureOverviewCache.clear();
|
|
16017
|
+
traversalSnapshotCache.clear();
|
|
16018
|
+
statusCache.clear();
|
|
16019
|
+
flowsCache.clear();
|
|
16020
|
+
fileToFlowLookupCache.clear();
|
|
16021
|
+
clearGitHistorySignalCache();
|
|
16022
|
+
annIndexCache.clear();
|
|
16023
|
+
searchMetadataCache.clear();
|
|
16024
|
+
searchCommunityLookupCache.clear();
|
|
16025
|
+
resourceCorpusCache.clear();
|
|
16026
|
+
resourceReachabilityCache.clear();
|
|
16027
|
+
packageMapCache.clear();
|
|
16028
|
+
reExportCache.clear();
|
|
15072
16029
|
}
|
|
15073
|
-
freshnessProbes.clear();
|
|
15074
|
-
mapCache.clear();
|
|
15075
|
-
minimalMapCache.clear();
|
|
15076
|
-
graphAnalyticsCache.clear();
|
|
15077
|
-
architectureMetadataCache.clear();
|
|
15078
|
-
architectureOverviewCache.clear();
|
|
15079
|
-
traversalSnapshotCache.clear();
|
|
15080
|
-
statusCache.clear();
|
|
15081
|
-
flowsCache.clear();
|
|
15082
|
-
fileToFlowLookupCache.clear();
|
|
15083
|
-
clearGitHistorySignalCache();
|
|
15084
|
-
annIndexCache.clear();
|
|
15085
|
-
searchMetadataCache.clear();
|
|
15086
|
-
searchCommunityLookupCache.clear();
|
|
15087
|
-
resourceCorpusCache.clear();
|
|
15088
|
-
resourceReachabilityCache.clear();
|
|
15089
|
-
packageMapCache.clear();
|
|
15090
|
-
reExportCache.clear();
|
|
15091
16030
|
},
|
|
15092
16031
|
};
|
|
15093
|
-
return withStaleness(engine, claimRepository);
|
|
16032
|
+
return withStaleness(engine, claimRepository, openDb, openPolicy);
|
|
15094
16033
|
}
|