knodin 0.8.5 → 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 +10 -4
- package/dist/src/engine/index.js +325 -54
- package/docs/releases/0.8.6.md +10 -0
- package/package.json +2 -1
package/dist/bin/cli.js
CHANGED
|
@@ -422,10 +422,16 @@ function formatStatusHuman(result) {
|
|
|
422
422
|
`${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
|
|
423
423
|
`Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n` +
|
|
424
424
|
`Freshness mechanism: ${result.freshnessMechanism.strategy}; watcher ${result.freshnessMechanism.watcher}${result.freshnessMechanism.diagnostic ? ` (${result.freshnessMechanism.diagnostic})` : ""}.\n`;
|
|
425
|
-
if (result.status === "healthy")
|
|
426
|
-
|
|
425
|
+
if (result.status === "healthy") {
|
|
426
|
+
const auditNote = result.verification.mode === "persisted-audit"
|
|
427
|
+
? ` Cached deep-audit evidence from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
|
|
428
|
+
: "";
|
|
429
|
+
return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).${auditNote}\n${freshnessLine}${lifecycleLine}${integrationLine}`;
|
|
430
|
+
}
|
|
427
431
|
if (result.status === "stale")
|
|
428
|
-
return `Graph content is intact but evidence is stale (${coverage})
|
|
432
|
+
return `Graph content is intact but evidence is stale (${coverage}).${result.verification.mode === "persisted-audit"
|
|
433
|
+
? ` Cached deep-audit evidence is from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness was probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
|
|
434
|
+
: ""}\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
|
|
429
435
|
const outstanding = result.missing.files.length + result.missing.records.length;
|
|
430
436
|
const firstIssue = result.missing.files[0] ?? result.missing.records[0];
|
|
431
437
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
@@ -1864,7 +1870,7 @@ async function main() {
|
|
|
1864
1870
|
case "status": {
|
|
1865
1871
|
const snapshot = async () => ({
|
|
1866
1872
|
...attachLifecycleHealth(repo, await engine.status(repo, {
|
|
1867
|
-
audit: rest.includes("--deep") ? "deep" : "
|
|
1873
|
+
audit: rest.includes("--deep") ? "deep" : "adaptive",
|
|
1868
1874
|
})),
|
|
1869
1875
|
integration: inspectRepositoryIntegrationStatus(repo),
|
|
1870
1876
|
update: trustedUpdateStatus({
|
package/dist/src/engine/index.js
CHANGED
|
@@ -547,6 +547,20 @@ function persistSymbolIdentities(db, repoPath, filePaths) {
|
|
|
547
547
|
throw error;
|
|
548
548
|
}
|
|
549
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
|
+
}
|
|
550
564
|
/** Bind name/file reference rows to exact definitions after identities exist. */
|
|
551
565
|
function persistReferenceEndpoints(db, filePaths) {
|
|
552
566
|
const refs = referencesForFiles(db, filePaths);
|
|
@@ -891,18 +905,127 @@ function getGraphAnalyticsSnapshot(allRepos, resolvedRepoPath, topN = 15, relati
|
|
|
891
905
|
const statusCache = new Map();
|
|
892
906
|
const repairOperations = new Map();
|
|
893
907
|
const STATUS_CACHE_LIMIT = 16;
|
|
894
|
-
|
|
895
|
-
|
|
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`]
|
|
896
981
|
.map((file) => {
|
|
897
982
|
try {
|
|
898
983
|
const stat = fs.statSync(file);
|
|
899
|
-
|
|
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
|
+
}
|
|
900
1000
|
}
|
|
901
1001
|
catch {
|
|
902
1002
|
return "missing";
|
|
903
1003
|
}
|
|
904
1004
|
})
|
|
905
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
|
+
}
|
|
906
1029
|
}
|
|
907
1030
|
const traversalSnapshotCache = new Map();
|
|
908
1031
|
const TRAVERSAL_SNAPSHOT_CACHE_LIMIT = 8;
|
|
@@ -6564,9 +6687,10 @@ async function embedPreparedBatch(items, onModelProgress) {
|
|
|
6564
6687
|
}
|
|
6565
6688
|
}
|
|
6566
6689
|
/** Incrementally generates and saves embeddings for symbols that lack them. */
|
|
6567
|
-
async function indexEmbeddings(db, repoPath, progress) {
|
|
6690
|
+
async function indexEmbeddings(db, repoPath, progress, signal) {
|
|
6568
6691
|
return measurePerfPhase("embedding_inference_write", async () => {
|
|
6569
6692
|
try {
|
|
6693
|
+
throwIfAborted(signal);
|
|
6570
6694
|
const missingStmt = db.query(`
|
|
6571
6695
|
SELECT id, name, kind, filePath, startLine, endLine, summary
|
|
6572
6696
|
FROM symbols
|
|
@@ -6608,6 +6732,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6608
6732
|
const sourceForEmbedding = createEmbeddingSourceReader(repoPath);
|
|
6609
6733
|
let lastYieldAt = Date.now();
|
|
6610
6734
|
for (let offset = 0; offset < missing.length; offset += batchSize) {
|
|
6735
|
+
throwIfAborted(signal);
|
|
6611
6736
|
const prepared = missing.slice(offset, offset + batchSize).map((sym) => {
|
|
6612
6737
|
const source = sourceForEmbedding(sym.filePath, sym.startLine, sym.endLine);
|
|
6613
6738
|
return {
|
|
@@ -6616,6 +6741,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6616
6741
|
};
|
|
6617
6742
|
});
|
|
6618
6743
|
const generated = await embedPreparedBatch(prepared, onModelProgress);
|
|
6744
|
+
throwIfAborted(signal);
|
|
6619
6745
|
db.run("BEGIN TRANSACTION;");
|
|
6620
6746
|
try {
|
|
6621
6747
|
for (const { symbol, embedding: embeddingVec } of generated) {
|
|
@@ -6638,6 +6764,7 @@ async function indexEmbeddings(db, repoPath, progress) {
|
|
|
6638
6764
|
insertEmbedding.finalize();
|
|
6639
6765
|
}
|
|
6640
6766
|
catch (error) {
|
|
6767
|
+
throwIfAborted(signal);
|
|
6641
6768
|
console.error("Error during indexEmbeddings:", error);
|
|
6642
6769
|
}
|
|
6643
6770
|
});
|
|
@@ -6814,6 +6941,63 @@ function gitTrackedCandidates(repoPath) {
|
|
|
6814
6941
|
return null;
|
|
6815
6942
|
}
|
|
6816
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
|
+
}
|
|
6817
7001
|
/**
|
|
6818
7002
|
* Enumerate the repo-relative files knodin indexes: source files plus dbt
|
|
6819
7003
|
* `manifest.json` files and Dockerfiles. Shared by the full index and the
|
|
@@ -6919,8 +7103,8 @@ function freshnessFingerprint(value) {
|
|
|
6919
7103
|
return null;
|
|
6920
7104
|
return crypto.createHash("sha256").update(value).digest("hex");
|
|
6921
7105
|
}
|
|
6922
|
-
function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride) {
|
|
6923
|
-
const git = gitWorkTreeProbe(repoPath);
|
|
7106
|
+
function buildFreshnessEnvelope(repoPath, db, verifiedAt, stateOverride, gitProbe) {
|
|
7107
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath) : gitProbe;
|
|
6924
7108
|
const currentHead = git && isSameDir(git.root, repoPath) ? git.head || null : gitHead(repoPath);
|
|
6925
7109
|
const indexedHead = db ? getMeta(db, "lastIndexedHead") || null : null;
|
|
6926
7110
|
const relation = commitRelation(repoPath, indexedHead, currentHead);
|
|
@@ -7336,7 +7520,7 @@ function gitWorkTreeProbe(repoPath) {
|
|
|
7336
7520
|
// Porcelain: `XY <path>` (or `XY <orig> -> <new>` for renames).
|
|
7337
7521
|
for (const part of line.slice(3).split(" -> ")) {
|
|
7338
7522
|
const p = part.trim().replace(/^"|"$/g, "");
|
|
7339
|
-
if (p)
|
|
7523
|
+
if (p && isIndexablePath(p))
|
|
7340
7524
|
dirty.push(p);
|
|
7341
7525
|
}
|
|
7342
7526
|
}
|
|
@@ -7403,8 +7587,8 @@ function recordFreshnessBaseline(repoPath, db) {
|
|
|
7403
7587
|
* pretending. A brand-new file in a non-git repo is consequently picked up by
|
|
7404
7588
|
* the watcher or a cold start, not by this probe.
|
|
7405
7589
|
*/
|
|
7406
|
-
function probeDrift(repoPath, db) {
|
|
7407
|
-
const git = gitWorkTreeProbe(repoPath);
|
|
7590
|
+
function probeDrift(repoPath, db, gitProbe) {
|
|
7591
|
+
const git = gitProbe === undefined ? gitWorkTreeProbe(repoPath) : gitProbe;
|
|
7408
7592
|
if (git && isSameDir(git.root, repoPath)) {
|
|
7409
7593
|
if ((getMeta(db, "lastIndexedHead") ?? "") !== git.head) {
|
|
7410
7594
|
return { drifted: true, verifiable: true };
|
|
@@ -9025,7 +9209,7 @@ function reviewChangedFiles(base, repoPath, options, modified) {
|
|
|
9025
9209
|
args.push(safeReviewRevision(base || "HEAD"));
|
|
9026
9210
|
args.push("--");
|
|
9027
9211
|
for (const file of runGit(repoPath, args).split("\0")) {
|
|
9028
|
-
if (file)
|
|
9212
|
+
if (file && isIndexablePath(file))
|
|
9029
9213
|
files.add(file);
|
|
9030
9214
|
}
|
|
9031
9215
|
}
|
|
@@ -9042,7 +9226,7 @@ function reviewChangedFiles(base, repoPath, options, modified) {
|
|
|
9042
9226
|
"--exclude-standard",
|
|
9043
9227
|
"-z",
|
|
9044
9228
|
]).split("\0")) {
|
|
9045
|
-
if (file)
|
|
9229
|
+
if (file && isIndexablePath(file))
|
|
9046
9230
|
files.add(file);
|
|
9047
9231
|
}
|
|
9048
9232
|
}
|
|
@@ -10572,23 +10756,30 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
10572
10756
|
const operation = Promise.resolve()
|
|
10573
10757
|
.then(async () => {
|
|
10574
10758
|
outerLease = acquireRepairLease(resolved);
|
|
10575
|
-
before = await engine.status(resolved, {
|
|
10759
|
+
before = await engine.status(resolved, {
|
|
10760
|
+
audit: "deep",
|
|
10761
|
+
signal: activeOperation.controller.signal,
|
|
10762
|
+
});
|
|
10576
10763
|
await suspendFileWatcher(resolved);
|
|
10577
10764
|
const activePath = resolveDbPath(resolved);
|
|
10578
10765
|
if (fs.existsSync(activePath)) {
|
|
10579
|
-
|
|
10766
|
+
candidate = await engine.createCandidate(resolved, { empty: true });
|
|
10767
|
+
const existing = dbInstances.get(resolved);
|
|
10768
|
+
const active = existing ?? new Database(activePath, { readonly: true });
|
|
10580
10769
|
try {
|
|
10581
|
-
|
|
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}'`);
|
|
10582
10774
|
}
|
|
10583
10775
|
finally {
|
|
10584
|
-
|
|
10776
|
+
if (!existing)
|
|
10777
|
+
active.close();
|
|
10585
10778
|
}
|
|
10586
|
-
dbInstances.delete(resolved);
|
|
10587
|
-
initPromises.delete(resolved);
|
|
10588
|
-
for (const suffix of ["-wal", "-shm"])
|
|
10589
|
-
fs.rmSync(`${activePath}${suffix}`, { force: true });
|
|
10590
10779
|
}
|
|
10591
|
-
|
|
10780
|
+
else {
|
|
10781
|
+
candidate = await engine.createCandidate(resolved);
|
|
10782
|
+
}
|
|
10592
10783
|
candidateEngine = createEngine({
|
|
10593
10784
|
watcher: "disabled",
|
|
10594
10785
|
databasePath: candidate.databasePath,
|
|
@@ -10604,7 +10795,9 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
10604
10795
|
if (staged.cancelled || !staged.verified) {
|
|
10605
10796
|
await engine.discardCandidate(candidate);
|
|
10606
10797
|
candidate = undefined;
|
|
10607
|
-
const after =
|
|
10798
|
+
const after = staged.cancelled
|
|
10799
|
+
? before
|
|
10800
|
+
: await engine.status(resolved, { audit: "deep" });
|
|
10608
10801
|
if (pendingCompleted)
|
|
10609
10802
|
deliver(pendingCompleted);
|
|
10610
10803
|
return {
|
|
@@ -11700,6 +11893,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11700
11893
|
},
|
|
11701
11894
|
async status(repoPath, options = {}) {
|
|
11702
11895
|
const resolved = path.resolve(repoPath);
|
|
11896
|
+
throwIfAborted(options.signal);
|
|
11703
11897
|
// Audit must not run the cold-start reconciler: otherwise status would
|
|
11704
11898
|
// repair and timestamp drift before reporting it. Reuse an active DB in
|
|
11705
11899
|
// long-lived processes, or open the existing local DB read-only.
|
|
@@ -11805,9 +11999,49 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11805
11999
|
],
|
|
11806
12000
|
};
|
|
11807
12001
|
}
|
|
11808
|
-
const databaseFingerprint = databaseFileFingerprint(diskDbPath);
|
|
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
|
+
}
|
|
11809
12043
|
const cached = statusCache.get(statusCacheKey);
|
|
11810
|
-
if (
|
|
12044
|
+
if (effectiveAudit !== "deep" &&
|
|
11811
12045
|
cached?.generation === indexGeneration &&
|
|
11812
12046
|
cached.databaseFingerprint === databaseFingerprint) {
|
|
11813
12047
|
const auditVerifiedAt = Date.parse(cached.result.verification.verifiedAt);
|
|
@@ -11833,6 +12067,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11833
12067
|
...cachedResult,
|
|
11834
12068
|
verification: {
|
|
11835
12069
|
mode: "cached-after-freshness-probe",
|
|
12070
|
+
auditVerifiedAt: cached.result.verification.auditVerifiedAt ??
|
|
12071
|
+
cached.result.verification.verifiedAt,
|
|
11836
12072
|
verifiedAt: new Date(lease.at).toISOString(),
|
|
11837
12073
|
},
|
|
11838
12074
|
};
|
|
@@ -11847,6 +12083,8 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11847
12083
|
...cachedResult,
|
|
11848
12084
|
verification: {
|
|
11849
12085
|
mode: "cached-after-freshness-probe",
|
|
12086
|
+
auditVerifiedAt: cached.result.verification.auditVerifiedAt ??
|
|
12087
|
+
cached.result.verification.verifiedAt,
|
|
11850
12088
|
verifiedAt,
|
|
11851
12089
|
},
|
|
11852
12090
|
};
|
|
@@ -11857,7 +12095,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11857
12095
|
// reports repair steps instead of hiding the database problem.
|
|
11858
12096
|
}
|
|
11859
12097
|
}
|
|
11860
|
-
|
|
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);
|
|
11861
12103
|
const sourceFiles = collected.files;
|
|
11862
12104
|
const coverageSkips = buildCoverageSkips(collected.skippedByExtension, db);
|
|
11863
12105
|
const schemaVersion = db.query("PRAGMA user_version").get()?.user_version ?? 0;
|
|
@@ -11941,25 +12183,29 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
11941
12183
|
.all()
|
|
11942
12184
|
.map((r) => r.filePath));
|
|
11943
12185
|
const missingFiles = sourceFiles.filter((file) => !indexed.has(file));
|
|
11944
|
-
const staleFiles =
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
.
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
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
|
+
}
|
|
11954
12200
|
try {
|
|
11955
12201
|
const stat = fs.statSync(path.join(resolved, row.filePath));
|
|
11956
|
-
|
|
12202
|
+
if (stat.size !== row.size || stat.mtimeMs !== row.mtimeMs)
|
|
12203
|
+
damagedFiles.push(row.filePath);
|
|
11957
12204
|
}
|
|
11958
12205
|
catch {
|
|
11959
|
-
|
|
12206
|
+
// A concurrently removed file is represented by the next audit/probe.
|
|
11960
12207
|
}
|
|
11961
|
-
}
|
|
11962
|
-
.map((row) => row.filePath);
|
|
12208
|
+
}
|
|
11963
12209
|
const count = (sql) => db.query(sql).get()?.count ?? 0;
|
|
11964
12210
|
const orphaned = {
|
|
11965
12211
|
embeddings: count(`SELECT COUNT(*) count FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`),
|
|
@@ -12009,7 +12255,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12009
12255
|
lastIndexedHead: getMeta(db, "lastIndexedHead") ?? "",
|
|
12010
12256
|
freshness,
|
|
12011
12257
|
indexGeneration,
|
|
12012
|
-
verification: { mode: "deep-audit", verifiedAt },
|
|
12258
|
+
verification: { mode: "deep-audit", auditVerifiedAt: verifiedAt, verifiedAt },
|
|
12013
12259
|
freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
|
|
12014
12260
|
repairSteps: needsRepair
|
|
12015
12261
|
? [
|
|
@@ -12018,15 +12264,25 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12018
12264
|
]
|
|
12019
12265
|
: [],
|
|
12020
12266
|
};
|
|
12021
|
-
if (!activeDb)
|
|
12022
|
-
db.close();
|
|
12023
12267
|
if (!needsRepair && !stale)
|
|
12024
12268
|
noteFreshness(resolved, "fresh");
|
|
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);
|
|
12025
12278
|
setBoundedCache(statusCache, statusCacheKey, {
|
|
12026
12279
|
generation: indexGeneration,
|
|
12027
|
-
databaseFingerprint,
|
|
12280
|
+
databaseFingerprint: finalDatabaseFingerprint,
|
|
12028
12281
|
result: structuredClone(result),
|
|
12029
12282
|
}, STATUS_CACHE_LIMIT);
|
|
12283
|
+
writePersistedStatusAudit(resolved, diskDbPath, finalDatabaseFingerprint, result);
|
|
12284
|
+
if (!activeDb)
|
|
12285
|
+
db.close();
|
|
12030
12286
|
return result;
|
|
12031
12287
|
},
|
|
12032
12288
|
async repair(repoPath, options) {
|
|
@@ -12116,10 +12372,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12116
12372
|
await suspendFileWatcher(resolved);
|
|
12117
12373
|
if (openPolicy.repairLease !== "disabled")
|
|
12118
12374
|
repairLease = acquireRepairLease(resolved);
|
|
12119
|
-
const cancelBoundary = () =>
|
|
12120
|
-
if (activeOperation.controller.signal.aborted)
|
|
12121
|
-
throw activeOperation.controller.signal;
|
|
12122
|
-
};
|
|
12375
|
+
const cancelBoundary = () => throwIfAborted(activeOperation.controller.signal);
|
|
12123
12376
|
const invalidateCommittedWork = () => {
|
|
12124
12377
|
if (!openPolicy.databasePath &&
|
|
12125
12378
|
committedWork &&
|
|
@@ -12127,7 +12380,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12127
12380
|
indexGeneration++;
|
|
12128
12381
|
};
|
|
12129
12382
|
emitProgress("audit", 0, "Auditing repository health");
|
|
12130
|
-
before = await engine.status(resolved
|
|
12383
|
+
before = await engine.status(resolved, {
|
|
12384
|
+
audit: "deep",
|
|
12385
|
+
signal: activeOperation.controller.signal,
|
|
12386
|
+
});
|
|
12131
12387
|
emitProgress("audit", 1, "Repository health audit completed", {
|
|
12132
12388
|
phaseTotal: 1,
|
|
12133
12389
|
});
|
|
@@ -12273,7 +12529,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12273
12529
|
phaseTotal: 1,
|
|
12274
12530
|
});
|
|
12275
12531
|
cancelBoundary();
|
|
12276
|
-
|
|
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);
|
|
12277
12537
|
if (repairAffectsTypeScriptDi)
|
|
12278
12538
|
reconcileTypeScriptDi(db, resolved);
|
|
12279
12539
|
committedWork = true;
|
|
@@ -12296,7 +12556,7 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12296
12556
|
const model = details.modelFile ? ` (${details.modelFile})` : "";
|
|
12297
12557
|
emitProgress("embeddings", 0, `${message}${model}`);
|
|
12298
12558
|
}
|
|
12299
|
-
});
|
|
12559
|
+
}, activeOperation.controller.signal);
|
|
12300
12560
|
committedWork = true;
|
|
12301
12561
|
setMeta(db, "repairPostprocessingPending", "0");
|
|
12302
12562
|
overallCompleted++;
|
|
@@ -12352,7 +12612,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12352
12612
|
phaseTotal: 1,
|
|
12353
12613
|
});
|
|
12354
12614
|
cancelBoundary();
|
|
12355
|
-
let after = await engine.status(resolved, {
|
|
12615
|
+
let after = await engine.status(resolved, {
|
|
12616
|
+
audit: "deep",
|
|
12617
|
+
signal: activeOperation.controller.signal,
|
|
12618
|
+
});
|
|
12356
12619
|
cancelBoundary();
|
|
12357
12620
|
overallCompleted++;
|
|
12358
12621
|
emitProgress("verification", 1, "Repair verification completed", {
|
|
@@ -12376,7 +12639,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12376
12639
|
setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
|
|
12377
12640
|
recordFreshnessBaseline(resolved, db);
|
|
12378
12641
|
statusCache.delete(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
12379
|
-
const candidate = await engine.status(resolved, {
|
|
12642
|
+
const candidate = await engine.status(resolved, {
|
|
12643
|
+
audit: "deep",
|
|
12644
|
+
signal: activeOperation.controller.signal,
|
|
12645
|
+
});
|
|
12380
12646
|
if (candidate.status === "healthy") {
|
|
12381
12647
|
db.run("COMMIT;");
|
|
12382
12648
|
after = candidate;
|
|
@@ -12384,7 +12650,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12384
12650
|
else {
|
|
12385
12651
|
db.run("ROLLBACK;");
|
|
12386
12652
|
statusCache.delete(databaseCacheKey(resolved, openPolicy.databasePath));
|
|
12387
|
-
after = await engine.status(resolved, {
|
|
12653
|
+
after = await engine.status(resolved, {
|
|
12654
|
+
audit: "deep",
|
|
12655
|
+
signal: activeOperation.controller.signal,
|
|
12656
|
+
});
|
|
12388
12657
|
}
|
|
12389
12658
|
}
|
|
12390
12659
|
catch (error) {
|
|
@@ -12414,12 +12683,14 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
|
|
|
12414
12683
|
return result;
|
|
12415
12684
|
})
|
|
12416
12685
|
.catch(async (error) => {
|
|
12417
|
-
if (error
|
|
12686
|
+
if (wasAborted(error, activeOperation.controller.signal)) {
|
|
12418
12687
|
if (!openPolicy.databasePath &&
|
|
12419
12688
|
committedWork &&
|
|
12420
12689
|
indexGeneration === generationBeforeRepair)
|
|
12421
12690
|
indexGeneration++;
|
|
12422
|
-
const after =
|
|
12691
|
+
const after = openPolicy.databasePath
|
|
12692
|
+
? before
|
|
12693
|
+
: await engine.status(resolved, { audit: "deep" });
|
|
12423
12694
|
const remaining = Math.max(0, repairPaths.length - completedFiles);
|
|
12424
12695
|
emitProgress("cancelled", completedFiles, `Repair cancelled with ${remaining} file(s) remaining`, { phaseTotal: repairPaths.length });
|
|
12425
12696
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# knodin 0.8.6
|
|
2
|
+
|
|
3
|
+
This patch release makes bare `knodin status` adaptive and improves graceful repair cancellation.
|
|
4
|
+
|
|
5
|
+
- Repositories with at most 50,000 indexed files receive the existing exact deep audit.
|
|
6
|
+
- Larger repositories may reuse healthy persisted deep-audit evidence after a current Git freshness probe. The evidence is bound to the canonical repository, graph schema, knodin version, and SQLite/WAL fingerprint; `knodin status --deep` always bypasses it.
|
|
7
|
+
- Human and JSON status output distinguish when the full audit ran from when freshness was most recently probed.
|
|
8
|
+
- Deep status accepts an abort signal, embedding repair checks cancellation between batches, and cancelled candidate repair returns without starting another deep audit.
|
|
9
|
+
|
|
10
|
+
Persisted evidence does not claim that every source file was inspected during the latest status invocation. Use `knodin status --deep` whenever an exact current filesystem/schema/orphan audit is required.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "knodin — source-evidenced local code intelligence with known bounds. Stable identity, fresh evidence, truthful budgets, and recoverable bounded views.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"docs/releases/0.8.3.md",
|
|
63
63
|
"docs/releases/0.8.4.md",
|
|
64
64
|
"docs/releases/0.8.5.md",
|
|
65
|
+
"docs/releases/0.8.6.md",
|
|
65
66
|
"docs/assets/knodin-favicon.svg",
|
|
66
67
|
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
67
68
|
"docs/TELEMETRY.md",
|