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 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
- return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
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}).\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
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" : "cached",
1873
+ audit: rest.includes("--deep") ? "deep" : "adaptive",
1868
1874
  })),
1869
1875
  integration: inspectRepositoryIntegrationStatus(repo),
1870
1876
  update: trustedUpdateStatus({
@@ -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
- function databaseFileFingerprint(databasePath) {
895
- return [databasePath, `${databasePath}-wal`]
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
- return `${stat.size}:${stat.mtimeMs}`;
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, { audit: "deep" });
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
- const active = dbInstances.get(resolved) ?? new Database(activePath);
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
- active.run("PRAGMA wal_checkpoint(TRUNCATE);");
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
- active.close();
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
- candidate = await engine.createCandidate(resolved, fs.existsSync(activePath) ? { sourceDatabasePath: activePath } : undefined);
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 = await engine.status(resolved, { audit: "deep" });
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 (options.audit !== "deep" &&
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
- const collected = measurePerfPhaseSync("status_audit", () => collectRepoFilesWithCoverage(resolved));
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 = indexedRows
11945
- .map((row) => row.filePath)
11946
- .filter((file) => !sourceFileSet.has(file) && !fs.existsSync(path.join(resolved, file)));
11947
- const ineligibleFiles = indexedRows
11948
- .map((row) => row.filePath)
11949
- .filter((file) => !sourceFileSet.has(file) && fs.existsSync(path.join(resolved, file)));
11950
- const damagedFiles = indexedRows
11951
- .filter((row) => {
11952
- if (!sourceFileSet.has(row.filePath))
11953
- return false;
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
- return stat.size !== row.size || stat.mtimeMs !== row.mtimeMs;
12202
+ if (stat.size !== row.size || stat.mtimeMs !== row.mtimeMs)
12203
+ damagedFiles.push(row.filePath);
11957
12204
  }
11958
12205
  catch {
11959
- return false;
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
- persistSymbolIdentities(db, resolved, inheritedPostprocessing ? undefined : repairPaths);
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, { audit: "deep" });
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, { audit: "deep" });
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, { audit: "deep" });
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 === activeOperation.controller.signal) {
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 = await engine.status(resolved, { audit: "deep" });
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.5",
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",