knodin 0.8.7 → 0.9.0

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.
Files changed (44) hide show
  1. package/dist/bin/cli.js +307 -85
  2. package/dist/fixtures/update-verification/config/application.xml +4 -0
  3. package/dist/fixtures/update-verification/dbt/manifest.json +17 -0
  4. package/dist/fixtures/update-verification/dbt/models/accounts.sql +1 -0
  5. package/dist/fixtures/update-verification/dbt/models/contacts.sql +1 -0
  6. package/dist/fixtures/update-verification/fixture.json +147 -0
  7. package/dist/fixtures/update-verification/force-app/main/default/classes/UpdateFixture.cls +14 -0
  8. package/dist/fixtures/update-verification/force-app/main/default/flows/Update_Verification.flow-meta.xml +9 -0
  9. package/dist/fixtures/update-verification/package.json +5 -0
  10. package/dist/fixtures/update-verification/prisma/schema.prisma +10 -0
  11. package/dist/fixtures/update-verification/sql/functions.sql +16 -0
  12. package/dist/fixtures/update-verification/src/CsharpFixture.cs +6 -0
  13. package/dist/fixtures/update-verification/src/JavaFixture.java +9 -0
  14. package/dist/fixtures/update-verification/src/javascript.js +7 -0
  15. package/dist/fixtures/update-verification/src/python_fixture.py +6 -0
  16. package/dist/fixtures/update-verification/src/typescript.ts +7 -0
  17. package/dist/fixtures/update-verification/terraform/main.tf +7 -0
  18. package/dist/fixtures/update-verification/terraform/modules/verified-child/main.tf +3 -0
  19. package/dist/src/agent-integration.js +26 -3
  20. package/dist/src/backup-retention.js +811 -0
  21. package/dist/src/cli-model.js +35 -8
  22. package/dist/src/docs-sections.js +1 -0
  23. package/dist/src/doctor.js +142 -12
  24. package/dist/src/engine/embeddings.js +8 -1
  25. package/dist/src/engine/index.js +355 -292
  26. package/dist/src/init.js +222 -179
  27. package/dist/src/manager-update.js +455 -0
  28. package/dist/src/release-preflight.js +106 -95
  29. package/dist/src/repair-lease.js +77 -8
  30. package/dist/src/response-budget.js +5 -1
  31. package/dist/src/server.js +18 -5
  32. package/dist/src/shared-index/compatibility.js +4 -2
  33. package/dist/src/tools/knodin-tools.js +69 -10
  34. package/dist/src/update-ceremony.js +20 -24
  35. package/dist/src/update-coordination.js +158 -0
  36. package/dist/src/update-executor.js +336 -0
  37. package/dist/src/update-verifier.js +358 -0
  38. package/docs/BACKUP-RETENTION.md +54 -0
  39. package/docs/CLI.md +6 -0
  40. package/docs/DOCTOR-AND-UPDATES.md +44 -12
  41. package/docs/SIGNED-UPDATES.md +29 -22
  42. package/docs/releases/0.9.0.md +43 -0
  43. package/package.json +7 -1
  44. package/roadmap/competitive-roadmap.md +8 -0
@@ -25,6 +25,7 @@ import { runReadOnlyLspQuery } from "../lsp-readonly.js";
25
25
  import { acquireRepairLease } from "../repair-lease.js";
26
26
  import { analyzeResourceReachability, resourceFingerprint, } from "../resource-reachability.js";
27
27
  import { contentFingerprint, writeStructuralSnapshot, } from "../structural-snapshot.js";
28
+ import { acquireLifecycleCoordination } from "../update-coordination.js";
28
29
  import { KNODIN_VERSION } from "../version.js";
29
30
  import * as ann from "./ann-hnsw.js";
30
31
  import { allocateCandidate, assertCandidate, discardCandidateFiles, promoteCandidateFile, recoverInterruptedPromotion, } from "./candidate-database.js";
@@ -783,7 +784,7 @@ function freshnessMechanismFor(repoPath, policy) {
783
784
  }
784
785
  // Bumped whenever any repo is (re)indexed, so cached community-detection results
785
786
  // below can be invalidated cheaply instead of recomputed on every call.
786
- export const KNODIN_SCHEMA_VERSION = 22;
787
+ export const KNODIN_SCHEMA_VERSION = 23;
787
788
  const LAST_REBUILD_SCHEMA_VERSION = 22;
788
789
  let indexGeneration = 0;
789
790
  const resourceReachabilityCache = new Map();
@@ -969,7 +970,10 @@ function writePersistedStatusAudit(repoPath, databasePath, databaseFingerprint,
969
970
  };
970
971
  try {
971
972
  fs.mkdirSync(path.dirname(target), { recursive: true });
972
- fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, { mode: 0o600, flag: "wx" });
973
+ fs.writeFileSync(temporary, `${JSON.stringify(snapshot)}\n`, {
974
+ mode: 0o600,
975
+ flag: "wx",
976
+ });
973
977
  fs.renameSync(temporary, target);
974
978
  }
975
979
  finally {
@@ -3229,7 +3233,10 @@ function getPackageMap(repoPath) {
3229
3233
  }
3230
3234
  }
3231
3235
  if (cacheable)
3232
- packageMapCache.set(resolvedRepoPath, { generation: indexGeneration, value: map });
3236
+ packageMapCache.set(resolvedRepoPath, {
3237
+ generation: indexGeneration,
3238
+ value: map,
3239
+ });
3233
3240
  return map;
3234
3241
  }
3235
3242
  /** Best source entry file for a package: prefer source-y fields, else src/index, else index. */
@@ -3749,7 +3756,13 @@ function extractApexPlatformDependencies(content, repoPath) {
3749
3756
  async function indexSalesforceMetadataFile(content, relativePath, repoPath, db, metadata) {
3750
3757
  const dependencies = [];
3751
3758
  const add = (to, kind, sourceEvidence, methodReference) => {
3752
- dependencies.push({ to, kind, confidence: 1, sourceEvidence, methodReference });
3759
+ dependencies.push({
3760
+ to,
3761
+ kind,
3762
+ confidence: 1,
3763
+ sourceEvidence,
3764
+ methodReference,
3765
+ });
3753
3766
  };
3754
3767
  const addField = (object, value, kind, evidence) => {
3755
3768
  const qualified = value.includes(".") ? value : `${object}.${value}`;
@@ -5579,10 +5592,6 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
5579
5592
  const hasRelation = colDecl.text.includes("@relation");
5580
5593
  if (hasRelation) {
5581
5594
  dependencies.push({
5582
- from: modelName,
5583
- to: targetModel,
5584
- kind: "orm-relation",
5585
- }, {
5586
5595
  from: relativePath,
5587
5596
  to: targetModel,
5588
5597
  kind: "orm-relation",
@@ -5605,7 +5614,6 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
5605
5614
  const modelBlockRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
5606
5615
  let mBlock = modelBlockRegex.exec(content);
5607
5616
  while (mBlock !== null) {
5608
- const srcModel = mBlock[1];
5609
5617
  const blockContent = mBlock[2];
5610
5618
  // `(?<!\w)` pins the leading field name to a word start; without it the scan
5611
5619
  // restarts inside every identifier and re-walks it, which is quadratic.
@@ -5613,11 +5621,6 @@ async function indexPrismaFile(content, tree, relativePath, _repoPath, db) {
5613
5621
  let rel = relationRegex.exec(blockContent);
5614
5622
  while (rel !== null) {
5615
5623
  const targetModel = rel[1];
5616
- dependencies.push({
5617
- from: srcModel,
5618
- to: targetModel,
5619
- kind: "orm-relation",
5620
- });
5621
5624
  dependencies.push({
5622
5625
  from: relativePath,
5623
5626
  to: targetModel,
@@ -6252,7 +6255,10 @@ function reconcileTypeScriptDi(db, repoPath) {
6252
6255
  .replace(/^type\s+/, "")
6253
6256
  .split(/\s+as\s+/);
6254
6257
  if (parts[0])
6255
- imports.set(parts[1] ?? parts[0], { original: parts[0], module: match[2] });
6258
+ imports.set(parts[1] ?? parts[0], {
6259
+ original: parts[0],
6260
+ module: match[2],
6261
+ });
6256
6262
  }
6257
6263
  }
6258
6264
  for (const match of masked.matchAll(/\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*["']([^"']+)["']/g))
@@ -6307,11 +6313,15 @@ function reconcileTypeScriptDi(db, repoPath) {
6307
6313
  return { reason: `Token import ${token} does not resolve uniquely.` };
6308
6314
  const definitionFile = followReExports(repoPath, moduleFile, imported.original, 0);
6309
6315
  if (!exactSymbol(definitionFile, imported.original))
6310
- return { reason: `Token ${token} does not have one indexed declaration.` };
6316
+ return {
6317
+ reason: `Token ${token} does not have one indexed declaration.`,
6318
+ };
6311
6319
  return { value: `symbol:${definitionFile}#${imported.original}` };
6312
6320
  }
6313
6321
  if (!exactSymbol(file, token))
6314
- return { reason: `Token ${token} does not have one indexed declaration.` };
6322
+ return {
6323
+ reason: `Token ${token} does not have one indexed declaration.`,
6324
+ };
6315
6325
  return { value: `symbol:${file}#${token}` };
6316
6326
  };
6317
6327
  const implementation = (name) => {
@@ -6471,7 +6481,10 @@ function reconcileTypeScriptDi(db, repoPath) {
6471
6481
  ? bindingCandidates.filter((binding) => binding.framework === resolution.framework && binding.token === resolution.token)
6472
6482
  : (bindingsByToken.get(`${resolution.framework}\0${resolution.registry}\0${resolution.token}`) ?? []);
6473
6483
  if (bindings.length === 1)
6474
- candidates.push({ ...resolution, implementation: bindings[0].implementation });
6484
+ candidates.push({
6485
+ ...resolution,
6486
+ implementation: bindings[0].implementation,
6487
+ });
6475
6488
  else
6476
6489
  candidates.push({
6477
6490
  ...resolution,
@@ -6844,7 +6857,9 @@ function mirrorSnapshotFor(repoPath) {
6844
6857
  const record = lookupMirror(repoPath);
6845
6858
  if (!record)
6846
6859
  return {};
6847
- return { mirror: { url: record.url, sha: record.sha, fetchedAt: record.fetchedAt } };
6860
+ return {
6861
+ mirror: { url: record.url, sha: record.sha, fetchedAt: record.fetchedAt },
6862
+ };
6848
6863
  }
6849
6864
  /**
6850
6865
  * Compares indexed symbols against embedded ones. A repository with no symbols
@@ -8025,6 +8040,8 @@ async function getOrInitDb(repoPath, options = {}) {
8025
8040
  // newly indexable JSON files. v22 admits markdown as a link-only
8026
8041
  // participant, so previously skipped documentation must be visited once
8027
8042
  // to populate its doc_link / doc_link_broken edges.
8043
+ // v23 changes the embedding runtime from FP32 to q8. Source topology is
8044
+ // unchanged, so migrate in place and regenerate only semantic vectors.
8028
8045
  // Highest version whose upgrade needs the stored data REBUILT. Versions
8029
8046
  // above it migrate in place, so an upgrade costs a DELETE rather than a
8030
8047
  // full re-index + re-embed (~40 min of CPU on an 18k-symbol corpus).
@@ -8034,6 +8051,7 @@ async function getOrInitDb(repoPath, options = {}) {
8034
8051
  // v13 -> v14 purges embeddings orphaned while the FK cascade was inert.
8035
8052
  // Runs after the CREATE TABLEs below, since the table must exist.
8036
8053
  const needsOrphanPurge = storedVersion < KNODIN_SCHEMA_VERSION;
8054
+ const needsEmbeddingRepresentationRefresh = storedVersion >= 22 && storedVersion < 23;
8037
8055
  if (storedVersion < LAST_REBUILD_SCHEMA_VERSION) {
8038
8056
  db.run("DROP TRIGGER IF EXISTS after_symbol_insert;");
8039
8057
  db.run("DROP TRIGGER IF EXISTS after_symbol_delete;");
@@ -8252,6 +8270,8 @@ async function getOrInitDb(repoPath, options = {}) {
8252
8270
  if (needsOrphanPurge) {
8253
8271
  purgeOrphanEmbeddings(db);
8254
8272
  }
8273
+ if (needsEmbeddingRepresentationRefresh)
8274
+ db.run("DELETE FROM symbol_embeddings;");
8255
8275
  finishDbOpenMigration();
8256
8276
  // Check if we need to do a full index (e.g. if the database is brand new/empty)
8257
8277
  let needsIndex = true;
@@ -10673,7 +10693,9 @@ function withStaleness(engine, claimRepository, openDb, openPolicy) {
10673
10693
  },
10674
10694
  };
10675
10695
  }
10676
- const DEFAULT_ENGINE_OPEN_POLICY = Object.freeze({ watcher: "enabled" });
10696
+ const DEFAULT_ENGINE_OPEN_POLICY = Object.freeze({
10697
+ watcher: "enabled",
10698
+ });
10677
10699
  /** Default SQLite + Tree-Sitter engine. */
10678
10700
  export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
10679
10701
  const owner = Symbol("knodin-engine");
@@ -11049,14 +11071,20 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
11049
11071
  candidateAudits.set(candidate.id, databaseSha256);
11050
11072
  else
11051
11073
  candidateAudits.delete(candidate.id);
11052
- return { healthy: issues.length === 0, issues: [...new Set(issues)], databaseSha256 };
11074
+ return {
11075
+ healthy: issues.length === 0,
11076
+ issues: [...new Set(issues)],
11077
+ databaseSha256,
11078
+ };
11053
11079
  },
11054
11080
  async promoteCandidate(candidate) {
11055
11081
  assertCandidate(candidate.repo, candidate);
11056
11082
  const auditedDigest = candidateAudits.get(candidate.id);
11057
11083
  if (!auditedDigest)
11058
11084
  throw new Error("candidate must pass deep audit before promotion");
11059
- const confirmation = await engine.auditCandidate(candidate, { mode: "deep" });
11085
+ const confirmation = await engine.auditCandidate(candidate, {
11086
+ mode: "deep",
11087
+ });
11060
11088
  if (!confirmation.healthy || confirmation.databaseSha256 !== auditedDigest)
11061
11089
  throw new Error("candidate or repository changed after deep audit");
11062
11090
  closeCandidateDatabase(candidate);
@@ -11245,7 +11273,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
11245
11273
  callees: allCallees.slice(0, cap),
11246
11274
  calleeCount: allCallees.length,
11247
11275
  ...(omissions.length > 0
11248
- ? { omissions: omissions.slice(0, cap), omissionCount: omissions.length }
11276
+ ? {
11277
+ omissions: omissions.slice(0, cap),
11278
+ omissionCount: omissions.length,
11279
+ }
11249
11280
  : {}),
11250
11281
  blastRadius: allBlastFiles.slice(0, cap),
11251
11282
  blastRadiusCount: allBlastFiles.length,
@@ -12209,8 +12240,12 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
12209
12240
  const count = (sql) => db.query(sql).get()?.count ?? 0;
12210
12241
  const orphaned = {
12211
12242
  embeddings: count(`SELECT COUNT(*) count FROM symbol_embeddings WHERE ${ORPHANED_EMBEDDING_PREDICATE}`),
12212
- references: count('SELECT COUNT(*) count FROM "references" r WHERE NOT EXISTS (SELECT 1 FROM index_state i WHERE i.filePath=r.callerFile)'),
12213
- dependencies: count("SELECT COUNT(*) count FROM dependencies d WHERE NOT EXISTS (SELECT 1 FROM index_state i WHERE i.filePath=d.fromFile)"),
12243
+ references: count(`SELECT COUNT(*) count FROM "references" r
12244
+ WHERE NOT EXISTS (SELECT 1 FROM index_state i WHERE i.filePath=r.callerFile)
12245
+ AND NOT (r.callerFile LIKE '.knodin/dbt/%' AND EXISTS (SELECT 1 FROM symbols s WHERE s.filePath=r.callerFile))`),
12246
+ dependencies: count(`SELECT COUNT(*) count FROM dependencies d
12247
+ WHERE NOT EXISTS (SELECT 1 FROM index_state i WHERE i.filePath=d.fromFile)
12248
+ AND NOT (d.fromFile LIKE '.knodin/dbt/%' AND EXISTS (SELECT 1 FROM symbols s WHERE s.filePath=d.fromFile))`),
12214
12249
  };
12215
12250
  const records = staleFiles.map((file) => `indexed file no longer exists: ${file}`);
12216
12251
  for (const file of ineligibleFiles)
@@ -12255,7 +12290,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
12255
12290
  lastIndexedHead: getMeta(db, "lastIndexedHead") ?? "",
12256
12291
  freshness,
12257
12292
  indexGeneration,
12258
- verification: { mode: "deep-audit", auditVerifiedAt: verifiedAt, verifiedAt },
12293
+ verification: {
12294
+ mode: "deep-audit",
12295
+ auditVerifiedAt: verifiedAt,
12296
+ verifiedAt,
12297
+ },
12259
12298
  freshnessMechanism: freshnessMechanismFor(resolved, openPolicy),
12260
12299
  repairSteps: needsRepair
12261
12300
  ? [
@@ -12736,292 +12775,306 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
12736
12775
  },
12737
12776
  async index(repoPath, files, clean = false, options) {
12738
12777
  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;
12778
+ const indexCoordination = acquireLifecycleCoordination({
12779
+ command: "index",
12780
+ repository: resolved,
12781
+ verificationToken: process.env.KNODIN_UPDATE_VERIFICATION_TOKEN,
12782
+ });
12783
+ try {
12784
+ if (clean &&
12785
+ (!files || files.length === 0) &&
12786
+ !openPolicy.databasePath &&
12787
+ !process.env.VITEST &&
12788
+ process.env.NODE_ENV !== "test") {
12789
+ const progress = createIndexProgressReporter(options?.onProgress);
12790
+ progress("starting", 0, "Creating isolated clean-index candidate");
12791
+ const candidate = await engine.createCandidate(resolved);
12792
+ let promoted = false;
12793
+ try {
12794
+ const reconciliation = await engine.reconcileCandidate(candidate, resolved, undefined, {
12795
+ onProgress: progress,
12796
+ skipEmbeddings: options?.skipEmbeddings,
12797
+ });
12798
+ progress("verifying", 0, "Deep-auditing clean-index candidate", {
12799
+ phaseTotal: 1,
12800
+ });
12801
+ const audit = await engine.auditCandidate(candidate, {
12802
+ mode: "deep",
12803
+ });
12804
+ if (!audit.healthy)
12805
+ throw new Error(`clean-index candidate audit failed: ${audit.issues.join(", ")}`);
12806
+ await engine.promoteCandidate(candidate);
12807
+ promoted = true;
12808
+ const activeDb = await openDb(resolved, {
12809
+ skipContentReconciliation: true,
12810
+ skipFreshnessGuard: true,
12811
+ watcher: openPolicy.watcher,
12812
+ });
12813
+ if (!process.env.VITEST && process.env.NODE_ENV !== "test")
12814
+ publishStructuralSnapshot(resolved, activeDb);
12815
+ const health = await engine.status(resolved, {
12816
+ audit: "deep",
12817
+ ignoreActiveOperation: true,
12818
+ });
12819
+ const issueCount = health.missing.files.length + health.missing.records.length;
12820
+ progress("verifying", 1, health.status === "healthy"
12821
+ ? "Clean-index candidate promoted and verified"
12822
+ : `Promoted graph has ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: 1 });
12823
+ progress("completed", reconciliation.reconciled.length, "Clean index completed", {
12824
+ phaseTotal: reconciliation.reconciled.length,
12825
+ });
12826
+ return {
12827
+ indexed: reconciliation.reconciled,
12828
+ unchanged: [],
12829
+ verification: {
12830
+ status: health.status,
12831
+ issueCount,
12832
+ missing: health.missing,
12833
+ verifiedAt: health.verification.verifiedAt,
12834
+ },
12835
+ };
12836
+ }
12837
+ catch (error) {
12838
+ if (!promoted && candidates.has(candidate.id))
12839
+ await engine.discardCandidate(candidate);
12840
+ if (openPolicy.watcher === "enabled" && fs.existsSync(resolveDbPath(resolved)))
12841
+ await openDb(resolved, {
12842
+ skipContentReconciliation: true,
12843
+ skipFreshnessGuard: true,
12844
+ });
12845
+ throw error;
12846
+ }
12847
+ }
12848
+ const watcherStateBefore = watcherStates.get(resolved)?.watcher;
12849
+ if (!openPolicy.databasePath)
12850
+ await suspendFileWatcher(resolved);
12748
12851
  try {
12749
- const reconciliation = await engine.reconcileCandidate(candidate, resolved, undefined, {
12852
+ invalidateResolutionCaches(repoPath);
12853
+ const indexed = [];
12854
+ const unchanged = [];
12855
+ let scipReport;
12856
+ let sarifReport;
12857
+ const progress = createIndexProgressReporter(options?.onProgress);
12858
+ progress("starting", 0, "Opening local graph database");
12859
+ const db = await openDb(repoPath, {
12750
12860
  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, {
12861
+ watcher: "disabled",
12862
+ // The explicit lifecycle operation owns all content work. Otherwise
12863
+ // getOrInitDb's freshness guard can perform a full, invisible
12864
+ // reconciliation before a targeted index even starts.
12760
12865
  skipContentReconciliation: true,
12761
12866
  skipFreshnessGuard: true,
12762
- watcher: openPolicy.watcher,
12763
12867
  });
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,
12868
+ let completionMessage = "Local graph is current";
12869
+ if (files && files.length > 0) {
12870
+ progress("indexing-files", 0, `Checking ${files.length.toLocaleString()} requested files`, {
12871
+ phaseTotal: files.length,
12795
12872
  });
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,
12818
- });
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);
12830
- }
12831
- else if (fs.existsSync(absPath)) {
12832
- await indexFile(absPath, relativePath, repoPath, db);
12833
- recordIndexState(db, repoPath, relativePath);
12834
- }
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;");
12873
+ for (const [fileIndex, file] of files.entries()) {
12874
+ const relativePath = path.isAbsolute(file) ? path.relative(repoPath, file) : file;
12875
+ const absPath = path.resolve(repoPath, relativePath);
12876
+ const needsUpdate = clean || fileDriftedFromIndexState(db, repoPath, relativePath);
12877
+ if (!needsUpdate) {
12878
+ unchanged.push(relativePath);
12844
12879
  }
12845
- catch (e) {
12846
- db.run("ROLLBACK;");
12847
- console.error(`Error deleting file from index: ${relativePath}`, e);
12880
+ else if (fs.existsSync(absPath)) {
12881
+ await indexFile(absPath, relativePath, repoPath, db);
12882
+ recordIndexState(db, repoPath, relativePath);
12848
12883
  }
12849
- removeIndexState(db, relativePath);
12884
+ else {
12885
+ // File was deleted
12886
+ db.run("BEGIN TRANSACTION;");
12887
+ try {
12888
+ deleteSymbolsForFile(db, relativePath);
12889
+ db.run('DELETE FROM "references" WHERE callerFile = ?', [relativePath]);
12890
+ db.run("DELETE FROM dependencies WHERE fromFile = ?", [relativePath]);
12891
+ db.run("DELETE FROM mcp_tools WHERE filePath = ?", [relativePath]);
12892
+ db.run("COMMIT;");
12893
+ }
12894
+ catch (e) {
12895
+ db.run("ROLLBACK;");
12896
+ console.error(`Error deleting file from index: ${relativePath}`, e);
12897
+ }
12898
+ removeIndexState(db, relativePath);
12899
+ }
12900
+ if (needsUpdate)
12901
+ indexed.push(relativePath);
12902
+ progress("indexing-files", fileIndex + 1, `Checking ${files.length.toLocaleString()} requested files`, { phaseTotal: files.length });
12850
12903
  }
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++;
12865
- }
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}`;
12873
- }
12874
- else {
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;
12904
+ const refreshedFlows = await refreshFlowsForApexFiles(db, repoPath, indexed);
12905
+ for (const flow of refreshedFlows)
12906
+ if (!indexed.includes(flow))
12907
+ indexed.push(flow);
12908
+ if (indexed.length > 0) {
12909
+ progress("finalizing", 0, "Refreshing identities and semantic embeddings");
12910
+ persistSymbolIdentities(db, repoPath, indexed);
12911
+ reconcileTypeScriptDi(db, repoPath);
12912
+ await indexEmbeddings(db, repoPath, progress);
12913
+ indexGeneration++;
12883
12914
  }
12884
- }
12885
- catch (_) { }
12886
- if (!clean && hasSymbols) {
12887
- progress("collecting-files", 0, "Checking existing local graph for changes");
12888
- await reconcileIndex(repoPath, db, progress);
12915
+ const alreadyCurrent = unchanged.length === 1
12916
+ ? "1 requested file already current"
12917
+ : `${unchanged.length.toLocaleString()} requested files already current`;
12918
+ completionMessage =
12919
+ indexed.length === 0
12920
+ ? alreadyCurrent
12921
+ : `Indexed ${indexed.length.toLocaleString()} requested file(s); ${alreadyCurrent}`;
12889
12922
  }
12890
12923
  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))
12901
- indexed.push(file);
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))
12924
+ // Full re-index or incremental sync
12925
+ let hasSymbols = false;
12926
+ try {
12927
+ const countResult = db
12928
+ .query("SELECT COUNT(*) as count FROM symbols")
12929
+ .all();
12930
+ if (countResult && countResult.length > 0 && countResult[0].count > 0) {
12931
+ hasSymbols = true;
12932
+ }
12933
+ }
12934
+ catch (_) { }
12935
+ if (!clean && hasSymbols) {
12936
+ progress("collecting-files", 0, "Checking existing local graph for changes");
12937
+ await reconcileIndex(repoPath, db, progress);
12938
+ }
12939
+ else {
12940
+ await indexRepo(repoPath, db, progress, options?.skipEmbeddings === true);
12941
+ }
12942
+ // Reuse the same candidate set as indexing/reconciliation. This keeps
12943
+ // LWC HTML/CSS bundle members visible in the response without admitting
12944
+ // arbitrary web assets to the source index.
12945
+ // Appended in a loop, not `push(...files)`: spreading an array as
12946
+ // arguments overflows the call stack once a repository is large
12947
+ // enough. Observed on a 40k-file checkout, which is well within the
12948
+ // range knodin is meant to index.
12949
+ for (const file of collectRepoFiles(repoPath))
12929
12950
  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, {
12958
- audit: "deep",
12959
- ignoreActiveOperation: true,
12960
- });
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")) {
12967
- setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
12968
- setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
12969
- recordFreshnessBaseline(repoPath, db);
12970
- statusCache.delete(databaseCacheKey(repoPath, openPolicy.databasePath));
12971
- health = await engine.status(repoPath, {
12951
+ completionMessage = `${indexed.length.toLocaleString()} repository file(s) are current`;
12952
+ }
12953
+ if (options?.scip) {
12954
+ const facts = readScipIndex(repoPath, options.scip);
12955
+ const affectedFiles = persistScipFacts(db, repoPath, facts);
12956
+ progress("finalizing", 0, "Refreshing SCIP identities and semantic embeddings");
12957
+ await indexEmbeddings(db, repoPath, progress);
12958
+ indexGeneration++;
12959
+ const bounds = { ...SCIP_DEFAULT_LIMITS, ...options.scip };
12960
+ scipReport = {
12961
+ provenance: "scip",
12962
+ inputPath: facts.inputPath,
12963
+ bytes: facts.bytes,
12964
+ files: facts.files,
12965
+ facts: facts.facts,
12966
+ languages: facts.languages,
12967
+ indexers: facts.indexers,
12968
+ elapsedMs: facts.elapsedMs,
12969
+ bounds: {
12970
+ maxBytes: bounds.maxBytes,
12971
+ maxFiles: bounds.maxFiles,
12972
+ maxFacts: bounds.maxFacts,
12973
+ timeoutMs: bounds.timeoutMs,
12974
+ },
12975
+ };
12976
+ for (const file of affectedFiles)
12977
+ if (!indexed.includes(file))
12978
+ indexed.push(file);
12979
+ }
12980
+ if (options?.sarif) {
12981
+ const facts = readSarifLog(repoPath, options.sarif);
12982
+ persistSarifFindings(db, facts);
12983
+ // Findings are not graph facts, so no re-embedding and no generation
12984
+ // bump: the graph is unchanged and existing answers stay valid.
12985
+ const bounds = { ...SARIF_DEFAULT_LIMITS, ...options.sarif };
12986
+ sarifReport = {
12987
+ provenance: "sarif",
12988
+ inputPath: facts.inputPath,
12989
+ bytes: facts.bytes,
12990
+ tools: facts.tools,
12991
+ rules: facts.rules,
12992
+ files: facts.files,
12993
+ findings: facts.findings.length,
12994
+ skippedUnresolved: facts.skippedUnresolved,
12995
+ elapsedMs: facts.elapsedMs,
12996
+ bounds: {
12997
+ maxBytes: bounds.maxBytes,
12998
+ maxFindings: bounds.maxFindings,
12999
+ timeoutMs: bounds.timeoutMs,
13000
+ },
13001
+ };
13002
+ }
13003
+ progress("verifying", 0, "Verifying local graph health", {
13004
+ phaseTotal: 1,
13005
+ });
13006
+ let health = await engine.status(repoPath, {
12972
13007
  audit: "deep",
12973
13008
  ignoreActiveOperation: true,
12974
13009
  });
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) {
13010
+ const contentVerified = health.coverage.sourceFiles === health.coverage.indexedFiles &&
13011
+ health.missing.files.length === 0 &&
13012
+ health.missing.records.length === 0 &&
13013
+ Object.values(health.orphaned).every((count) => count === 0);
13014
+ if (contentVerified &&
13015
+ (health.freshness.state === "stale-head" || health.freshness.state === "unknown")) {
12984
13016
  setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
12985
13017
  setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
12986
13018
  recordFreshnessBaseline(repoPath, db);
13019
+ statusCache.delete(databaseCacheKey(repoPath, openPolicy.databasePath));
13020
+ health = await engine.status(repoPath, {
13021
+ audit: "deep",
13022
+ ignoreActiveOperation: true,
13023
+ });
12987
13024
  }
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);
13025
+ // The postcondition belongs to this index result. Do not turn the next
13026
+ // explicit `status` call into a cache hit against an internal audit.
13027
+ statusCache.delete(databaseCacheKey(repoPath, openPolicy.databasePath));
13028
+ const issueCount = health.missing.files.length + health.missing.records.length;
13029
+ if (health.status === "healthy") {
13030
+ // Explicit indexing counts as a successful reconciliation only after
13031
+ // the audit proves that no requested or unrequested drift remains.
13032
+ if (files && files.length > 0) {
13033
+ setMeta(db, "lastIndexedHead", gitHead(repoPath) ?? "");
13034
+ setMeta(db, "lastSuccessfulReconciliation", new Date().toISOString());
13035
+ recordFreshnessBaseline(repoPath, db);
13036
+ }
13037
+ noteFreshness(repoPath, "fresh");
13038
+ // Test engines use an in-memory graph and must not create untracked
13039
+ // repository artifacts that pollute Git-diff/review fixtures.
13040
+ if (!process.env.VITEST && process.env.NODE_ENV !== "test")
13041
+ publishStructuralSnapshot(repoPath, db);
13042
+ }
13043
+ else {
13044
+ freshnessProbes.delete(path.resolve(repoPath));
13045
+ }
13046
+ progress("verifying", 1, health.status === "healthy"
13047
+ ? "Local graph health verified"
13048
+ : `Local graph still has ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: 1 });
13049
+ const completed = files?.length ?? indexed.length;
13050
+ progress("completed", completed, health.status === "healthy"
13051
+ ? completionMessage
13052
+ : `${completionMessage}; verification found ${issueCount.toLocaleString()} issue(s)`, { phaseTotal: completed });
13053
+ return {
13054
+ indexed,
13055
+ unchanged,
13056
+ scip: scipReport,
13057
+ sarif: sarifReport,
13058
+ verification: {
13059
+ status: health.status,
13060
+ issueCount,
13061
+ missing: health.missing,
13062
+ verifiedAt: health.verification.verifiedAt,
13063
+ },
13064
+ };
13065
+ }
13066
+ finally {
13067
+ const activeDb = dbInstances.get(resolved);
13068
+ if (!openPolicy.databasePath &&
13069
+ activeDb &&
13070
+ openPolicy.watcher === "enabled" &&
13071
+ watcherStateBefore !== "scale-limited" &&
13072
+ watcherStateBefore !== "resource-limited")
13073
+ startFileWatcher(resolved, activeDb, openPolicy.watcherFileLimit);
12993
13074
  }
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
13075
  }
13017
13076
  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);
13077
+ indexCoordination.release();
13025
13078
  }
13026
13079
  },
13027
13080
  async search(query, repoPath, limit, options = {}) {
@@ -15070,7 +15123,11 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
15070
15123
  });
15071
15124
  diskFingerprint = resourceFingerprint(inputs);
15072
15125
  if (!diskDrifted)
15073
- setBoundedCache(resourceCorpusCache, resolvedRepoPath, { generation: indexGeneration, inputs, fingerprint: diskFingerprint }, RESOURCE_CORPUS_CACHE_LIMIT);
15126
+ setBoundedCache(resourceCorpusCache, resolvedRepoPath, {
15127
+ generation: indexGeneration,
15128
+ inputs,
15129
+ fingerprint: diskFingerprint,
15130
+ }, RESOURCE_CORPUS_CACHE_LIMIT);
15074
15131
  }
15075
15132
  // Disk state is part of the cache identity. A generation-only key can
15076
15133
  // return a previously fresh result during the freshness-probe TTL after
@@ -15080,7 +15137,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
15080
15137
  const cached = resourceReachabilityCache.get(cacheKey);
15081
15138
  if (cached?.generation === indexGeneration) {
15082
15139
  const result = structuredClone(cached.result);
15083
- return finish([], { count: result.paths.length, resourceReachability: result });
15140
+ return finish([], {
15141
+ count: result.paths.length,
15142
+ resourceReachability: result,
15143
+ });
15084
15144
  }
15085
15145
  const result = analyzeResourceReachability(inputs, {
15086
15146
  maxItems: cap,
@@ -15090,7 +15150,10 @@ export function createEngine(openPolicy = DEFAULT_ENGINE_OPEN_POLICY) {
15090
15150
  expectedFingerprint: diskDrifted ? `indexed:${diskFingerprint}` : diskFingerprint,
15091
15151
  });
15092
15152
  setBoundedCache(resourceReachabilityCache, cacheKey, { generation: indexGeneration, result: structuredClone(result) }, RESOURCE_REACHABILITY_CACHE_LIMIT);
15093
- return finish([], { count: result.paths.length, resourceReachability: result });
15153
+ return finish([], {
15154
+ count: result.paths.length,
15155
+ resourceReachability: result,
15156
+ });
15094
15157
  }
15095
15158
  case "flow_analysis": {
15096
15159
  // C8 deliberately does not persist a PDG. This is a bounded, local