@wrongstack/tools 0.306.2 → 0.306.4

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/tool-tier.js CHANGED
@@ -1202,8 +1202,9 @@ async function resolveRealInsideRoot(absPath, ctx) {
1202
1202
  }
1203
1203
  throw err;
1204
1204
  }
1205
- if (isInsideAny(real, realRoots)) {
1206
- return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
1205
+ const candidate = pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
1206
+ if (isInsideAny(candidate, realRoots)) {
1207
+ return candidate;
1207
1208
  }
1208
1209
  throw new Error(
1209
1210
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
@@ -7737,7 +7738,7 @@ var auditTool = {
7737
7738
  "audit: `fix: true` is not supported \u2014 this tool is read-only (mutating: false). To remediate vulnerabilities, upgrade the affected packages with the `install` tool (or `language_package` for non-JS ecosystems)."
7738
7739
  );
7739
7740
  }
7740
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
7741
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
7741
7742
  const bridge = await tryLegacyPackageOperation("package-audit", {
7742
7743
  cwd,
7743
7744
  projectRoot: ctx.projectRoot,
@@ -11133,6 +11134,85 @@ function runSqliteWithRetry(fn) {
11133
11134
  throw lastError;
11134
11135
  }
11135
11136
 
11137
+ // src/codebase-index/vector-search.ts
11138
+ var RRF_K = 60;
11139
+ var VECTOR_DIMENSIONS = 384;
11140
+ var NGRAM_SIZE = 3;
11141
+ function embedText(text) {
11142
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
11143
+ const normalized = text.toLowerCase().trim();
11144
+ if (normalized.length < NGRAM_SIZE) {
11145
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
11146
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
11147
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
11148
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11149
+ vec[bucket] += 1;
11150
+ }
11151
+ } else {
11152
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
11153
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
11154
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
11155
+ vec[bucket] += 1;
11156
+ }
11157
+ }
11158
+ let norm = 0;
11159
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11160
+ norm += vec[i] * vec[i];
11161
+ }
11162
+ norm = Math.sqrt(norm);
11163
+ if (norm > 0) {
11164
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
11165
+ vec[i] /= norm;
11166
+ }
11167
+ }
11168
+ return vec;
11169
+ }
11170
+ function hashNgram(str) {
11171
+ let hash = 2166136261;
11172
+ for (let i = 0; i < str.length; i++) {
11173
+ hash ^= str.charCodeAt(i);
11174
+ hash = Math.imul(hash, 16777619);
11175
+ }
11176
+ return hash >>> 0;
11177
+ }
11178
+ function cosineSimilarity(a, b) {
11179
+ let dot = 0;
11180
+ const len = Math.min(a.length, b.length);
11181
+ for (let i = 0; i < len; i++) {
11182
+ dot += a[i] * b[i];
11183
+ }
11184
+ return dot;
11185
+ }
11186
+ function encodeVector(vec) {
11187
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
11188
+ }
11189
+ function decodeVector(buf) {
11190
+ const view = new DataView(
11191
+ buf.buffer,
11192
+ buf.byteOffset,
11193
+ buf.byteLength
11194
+ );
11195
+ const copy = new Float32Array(buf.byteLength / 4);
11196
+ for (let i = 0; i < copy.length; i++) {
11197
+ copy[i] = view.getFloat32(i * 4, true);
11198
+ }
11199
+ return copy;
11200
+ }
11201
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
11202
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
11203
+ const scored = [];
11204
+ for (const id of allIds) {
11205
+ const bm25Rank = bm25Ranks.get(id);
11206
+ const vecRank = vectorRanks.get(id);
11207
+ let score = 0;
11208
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
11209
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
11210
+ scored.push([id, score]);
11211
+ }
11212
+ scored.sort((a, b) => b[1] - a[1]);
11213
+ return scored;
11214
+ }
11215
+
11136
11216
  // src/codebase-index/writer-admin.ts
11137
11217
  import * as fs9 from "node:fs";
11138
11218
  import * as path14 from "node:path";
@@ -12494,90 +12574,19 @@ var StorePool = class {
12494
12574
  }
12495
12575
  };
12496
12576
 
12497
- // src/codebase-index/vector-search.ts
12498
- var RRF_K = 60;
12499
- var VECTOR_DIMENSIONS = 384;
12500
- var NGRAM_SIZE = 3;
12501
- function embedText(text) {
12502
- const vec = new Float32Array(VECTOR_DIMENSIONS);
12503
- const normalized = text.toLowerCase().trim();
12504
- if (normalized.length < NGRAM_SIZE) {
12505
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
12506
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
12507
- const ngram = padded.slice(i, i + NGRAM_SIZE);
12508
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12509
- vec[bucket] += 1;
12510
- }
12511
- } else {
12512
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
12513
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
12514
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
12515
- vec[bucket] += 1;
12516
- }
12517
- }
12518
- let norm = 0;
12519
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12520
- norm += vec[i] * vec[i];
12521
- }
12522
- norm = Math.sqrt(norm);
12523
- if (norm > 0) {
12524
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
12525
- vec[i] /= norm;
12526
- }
12527
- }
12528
- return vec;
12529
- }
12530
- function hashNgram(str) {
12531
- let hash = 2166136261;
12532
- for (let i = 0; i < str.length; i++) {
12533
- hash ^= str.charCodeAt(i);
12534
- hash = Math.imul(hash, 16777619);
12535
- }
12536
- return hash >>> 0;
12537
- }
12538
- function cosineSimilarity(a, b) {
12539
- let dot = 0;
12540
- const len = Math.min(a.length, b.length);
12541
- for (let i = 0; i < len; i++) {
12542
- dot += a[i] * b[i];
12543
- }
12544
- return dot;
12545
- }
12546
- function encodeVector(vec) {
12547
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
12548
- }
12549
- function decodeVector(buf) {
12550
- const view = new DataView(
12551
- buf.buffer,
12552
- buf.byteOffset,
12553
- buf.byteLength
12554
- );
12555
- const copy = new Float32Array(buf.byteLength / 4);
12556
- for (let i = 0; i < copy.length; i++) {
12557
- copy[i] = view.getFloat32(i * 4, true);
12558
- }
12559
- return copy;
12560
- }
12561
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
12562
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
12563
- const scored = [];
12564
- for (const id of allIds) {
12565
- const bm25Rank = bm25Ranks.get(id);
12566
- const vecRank = vectorRanks.get(id);
12567
- let score = 0;
12568
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
12569
- if (vecRank !== void 0) score += 1 / (k + vecRank);
12570
- scored.push([id, score]);
12571
- }
12572
- scored.sort((a, b) => b[1] - a[1]);
12573
- return scored;
12574
- }
12575
-
12576
12577
  // src/codebase-index/writer.ts
12577
12578
  var DB_FILE2 = "index.db";
12578
12579
  var MAX_STATEMENT_CACHE = 128;
12579
12580
  var IndexStore = class _IndexStore {
12580
12581
  db;
12582
+ /**
12583
+ * True while an index run owns one outer SQLite transaction. Individual
12584
+ * writer methods normally protect themselves with BEGIN/COMMIT, but during
12585
+ * a refresh they join this transaction so readers observe either the last
12586
+ * completed index or the next completed index, never an in-between batch.
12587
+ */
12588
+ atomicIndexUpdateActive = false;
12589
+ writeSavepointSequence = 0;
12581
12590
  /** Absolute path to this project's index directory. */
12582
12591
  indexDir;
12583
12592
  /**
@@ -12658,6 +12667,51 @@ var IndexStore = class _IndexStore {
12658
12667
  runWithRetry(fn) {
12659
12668
  return runSqliteWithRetry(fn);
12660
12669
  }
12670
+ /** Run a complete index mutation as one WAL-visible publication. */
12671
+ async runAtomicIndexUpdate(job) {
12672
+ if (this.atomicIndexUpdateActive) return job();
12673
+ this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
12674
+ this.atomicIndexUpdateActive = true;
12675
+ try {
12676
+ const result = await job();
12677
+ this.db.exec("COMMIT");
12678
+ return result;
12679
+ } catch (error) {
12680
+ try {
12681
+ this.db.exec("ROLLBACK");
12682
+ } catch {
12683
+ }
12684
+ throw error;
12685
+ } finally {
12686
+ this.atomicIndexUpdateActive = false;
12687
+ }
12688
+ }
12689
+ /**
12690
+ * Begin a method-local transaction. Inside an atomic index publication a
12691
+ * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
12692
+ * when commitBatch falls back to per-file writes after one batch fails.
12693
+ */
12694
+ beginWriteTransaction() {
12695
+ if (this.atomicIndexUpdateActive) {
12696
+ const savepoint = `index_write_${++this.writeSavepointSequence}`;
12697
+ this.db.exec(`SAVEPOINT ${savepoint}`);
12698
+ return savepoint;
12699
+ }
12700
+ this.db.exec("BEGIN IMMEDIATE");
12701
+ return null;
12702
+ }
12703
+ commitWriteTransaction(savepoint) {
12704
+ if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
12705
+ else this.db.exec("COMMIT");
12706
+ }
12707
+ rollbackWriteTransaction(savepoint) {
12708
+ if (savepoint) {
12709
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
12710
+ this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
12711
+ } else {
12712
+ this.db.exec("ROLLBACK");
12713
+ }
12714
+ }
12661
12715
  /**
12662
12716
  * Mirror the in-process language→family map into SQLite.
12663
12717
  *
@@ -12892,7 +12946,7 @@ var IndexStore = class _IndexStore {
12892
12946
  insertSymbols(symbols) {
12893
12947
  this.invalidateBm25();
12894
12948
  return this.runWithRetry(() => {
12895
- this.db.exec("BEGIN IMMEDIATE");
12949
+ const ownsTransaction = this.beginWriteTransaction();
12896
12950
  try {
12897
12951
  let nextId = this.allocateSymbolIds(symbols.length);
12898
12952
  const result = [];
@@ -12919,7 +12973,9 @@ var IndexStore = class _IndexStore {
12919
12973
  }
12920
12974
  vectorRows.push({
12921
12975
  id,
12922
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
12976
+ vector: encodeVector(
12977
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
12978
+ )
12923
12979
  });
12924
12980
  result.push({ ...s, id });
12925
12981
  }
@@ -12937,10 +12993,10 @@ var IndexStore = class _IndexStore {
12937
12993
  vectorRows
12938
12994
  );
12939
12995
  }
12940
- this.db.exec("COMMIT");
12996
+ this.commitWriteTransaction(ownsTransaction);
12941
12997
  return result;
12942
12998
  } catch (err) {
12943
- this.db.exec("ROLLBACK");
12999
+ this.rollbackWriteTransaction(ownsTransaction);
12944
13000
  throw err;
12945
13001
  }
12946
13002
  });
@@ -12948,7 +13004,7 @@ var IndexStore = class _IndexStore {
12948
13004
  deleteSymbolsForFile(file) {
12949
13005
  this.invalidateBm25();
12950
13006
  this.runWithRetry(() => {
12951
- this.db.exec("BEGIN IMMEDIATE");
13007
+ const ownsTransaction = this.beginWriteTransaction();
12952
13008
  try {
12953
13009
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
12954
13010
  if (this.ftsAvailable) {
@@ -12963,9 +13019,9 @@ var IndexStore = class _IndexStore {
12963
13019
  }
12964
13020
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
12965
13021
  this.resolveRefsForNamesUnsafe(affectedNames);
12966
- this.db.exec("COMMIT");
13022
+ this.commitWriteTransaction(ownsTransaction);
12967
13023
  } catch (error) {
12968
- this.db.exec("ROLLBACK");
13024
+ this.rollbackWriteTransaction(ownsTransaction);
12969
13025
  throw error;
12970
13026
  }
12971
13027
  });
@@ -12978,7 +13034,7 @@ var IndexStore = class _IndexStore {
12978
13034
  deleteFile(file) {
12979
13035
  this.invalidateBm25();
12980
13036
  this.runWithRetry(() => {
12981
- this.db.exec("BEGIN IMMEDIATE");
13037
+ const ownsTransaction = this.beginWriteTransaction();
12982
13038
  try {
12983
13039
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
12984
13040
  if (this.ftsAvailable) {
@@ -12997,9 +13053,9 @@ var IndexStore = class _IndexStore {
12997
13053
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
12998
13054
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
12999
13055
  this.resolveRefsForNamesUnsafe(affectedNames);
13000
- this.db.exec("COMMIT");
13056
+ this.commitWriteTransaction(ownsTransaction);
13001
13057
  } catch (err) {
13002
- this.db.exec("ROLLBACK");
13058
+ this.rollbackWriteTransaction(ownsTransaction);
13003
13059
  throw err;
13004
13060
  }
13005
13061
  });
@@ -13386,7 +13442,7 @@ var IndexStore = class _IndexStore {
13386
13442
  clearAll() {
13387
13443
  this.invalidateBm25();
13388
13444
  this.runWithRetry(() => {
13389
- this.db.exec("BEGIN IMMEDIATE");
13445
+ const ownsTransaction = this.beginWriteTransaction();
13390
13446
  try {
13391
13447
  this.db.exec("DROP TABLE IF EXISTS refs");
13392
13448
  this.db.exec("DROP TABLE IF EXISTS symbols");
@@ -13394,15 +13450,15 @@ var IndexStore = class _IndexStore {
13394
13450
  this.db.exec("DROP TABLE IF EXISTS metadata");
13395
13451
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
13396
13452
  this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
13397
- this.db.exec("COMMIT");
13398
13453
  this.stmtCache.clear();
13399
13454
  this.initSchema();
13400
13455
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
13401
13456
  _IndexStore.NEXT_SYMBOL_ID_KEY,
13402
13457
  "1"
13403
13458
  );
13459
+ this.commitWriteTransaction(ownsTransaction);
13404
13460
  } catch (err) {
13405
- this.db.exec("ROLLBACK");
13461
+ this.rollbackWriteTransaction(ownsTransaction);
13406
13462
  throw err;
13407
13463
  }
13408
13464
  });
@@ -13464,7 +13520,7 @@ var IndexStore = class _IndexStore {
13464
13520
  }
13465
13521
  this.invalidateBm25();
13466
13522
  return this.runWithRetry(() => {
13467
- this.db.exec("BEGIN IMMEDIATE");
13523
+ const ownsTransaction = this.beginWriteTransaction();
13468
13524
  try {
13469
13525
  const affectedNames = /* @__PURE__ */ new Set();
13470
13526
  for (const entry of entries) {
@@ -13525,7 +13581,9 @@ var IndexStore = class _IndexStore {
13525
13581
  }
13526
13582
  vectorRows.push({
13527
13583
  id,
13528
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
13584
+ vector: encodeVector(
13585
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
13586
+ )
13529
13587
  });
13530
13588
  const inserted = { ...s, id };
13531
13589
  allInserted.push(inserted);
@@ -13570,10 +13628,10 @@ var IndexStore = class _IndexStore {
13570
13628
  );
13571
13629
  }
13572
13630
  this.resolveRefsForNamesUnsafe(affectedNames);
13573
- this.db.exec("COMMIT");
13631
+ this.commitWriteTransaction(ownsTransaction);
13574
13632
  return allInserted;
13575
13633
  } catch (err) {
13576
- this.db.exec("ROLLBACK");
13634
+ this.rollbackWriteTransaction(ownsTransaction);
13577
13635
  throw err;
13578
13636
  }
13579
13637
  });
@@ -13650,7 +13708,7 @@ var IndexStore = class _IndexStore {
13650
13708
  replaceEmptyFile(meta) {
13651
13709
  this.invalidateBm25();
13652
13710
  this.runWithRetry(() => {
13653
- this.db.exec("BEGIN IMMEDIATE");
13711
+ const ownsTransaction = this.beginWriteTransaction();
13654
13712
  try {
13655
13713
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
13656
13714
  if (this.ftsAvailable) {
@@ -13685,9 +13743,9 @@ var IndexStore = class _IndexStore {
13685
13743
  meta.lastIndexed
13686
13744
  );
13687
13745
  this.resolveRefsForNamesUnsafe(affectedNames);
13688
- this.db.exec("COMMIT");
13746
+ this.commitWriteTransaction(ownsTransaction);
13689
13747
  } catch (err) {
13690
- this.db.exec("ROLLBACK");
13748
+ this.rollbackWriteTransaction(ownsTransaction);
13691
13749
  throw err;
13692
13750
  }
13693
13751
  });
@@ -14611,6 +14669,7 @@ import { Worker as Worker2 } from "node:worker_threads";
14611
14669
  // src/codebase-index/indexer.ts
14612
14670
  import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
14613
14671
  import { execFile } from "node:child_process";
14672
+ import { createHash as createHash5 } from "node:crypto";
14614
14673
  import * as fs18 from "node:fs/promises";
14615
14674
  import { availableParallelism } from "node:os";
14616
14675
  import * as path24 from "node:path";
@@ -15451,6 +15510,10 @@ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-l
15451
15510
  var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
15452
15511
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
15453
15512
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
15513
+ var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
15514
+ var IndexSourceChangedError = class extends Error {
15515
+ name = "IndexSourceChangedError";
15516
+ };
15454
15517
  function isWithinProject(projectRoot, file) {
15455
15518
  const rel = path24.relative(projectRoot, file);
15456
15519
  return rel !== "" && !rel.startsWith(`..${path24.sep}`) && rel !== ".." && !path24.isAbsolute(rel);
@@ -15487,7 +15550,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15487
15550
  if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
15488
15551
  throwIfAborted(signal);
15489
15552
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
15490
- const [output, statusOutput] = await Promise.all([
15553
+ const [output, statusOutput, stagedOutput] = await Promise.all([
15491
15554
  gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
15492
15555
  gitOutput(projectRoot, [
15493
15556
  "status",
@@ -15495,7 +15558,8 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15495
15558
  "-z",
15496
15559
  "--untracked-files=all",
15497
15560
  "--ignored=no"
15498
- ])
15561
+ ]),
15562
+ gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
15499
15563
  ]);
15500
15564
  throwIfAborted(signal);
15501
15565
  const dirty = /* @__PURE__ */ new Set();
@@ -15525,9 +15589,17 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
15525
15589
  const ext = path24.extname(relative12).toLowerCase();
15526
15590
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
15527
15591
  }
15592
+ const snapshot = createHash5("sha256").update(stagedOutput).update("\0").update(statusOutput);
15593
+ const indexedFiles = new Set(files);
15594
+ for (const dirtyFile of [...dirty].sort()) {
15595
+ if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
15596
+ snapshot.update("\0").update(dirtyFile).update("\0");
15597
+ snapshot.update(xxhash64String(await fs18.readFile(dirtyFile, "utf8")));
15598
+ }
15528
15599
  return {
15529
15600
  files,
15530
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
15601
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
15602
+ snapshotKey: snapshot.digest("hex")
15531
15603
  };
15532
15604
  } catch {
15533
15605
  return null;
@@ -15540,7 +15612,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
15540
15612
  files: gitFiles.files,
15541
15613
  complete: true,
15542
15614
  errors: [],
15543
- trustedUnchanged: gitFiles.trustedUnchanged
15615
+ trustedUnchanged: gitFiles.trustedUnchanged,
15616
+ snapshotKey: gitFiles.snapshotKey
15544
15617
  };
15545
15618
  }
15546
15619
  const results = [];
@@ -15627,6 +15700,17 @@ async function resolveProjectRelations(store, projectRoot, opts) {
15627
15700
  }
15628
15701
  }
15629
15702
  async function runIndexerWithStore(store, opts) {
15703
+ let result;
15704
+ try {
15705
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
15706
+ } catch (error) {
15707
+ if (!(error instanceof IndexSourceChangedError)) throw error;
15708
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
15709
+ }
15710
+ if (!opts.files) store.compactIfNeeded();
15711
+ return result;
15712
+ }
15713
+ async function runIndexerAtomic(store, opts) {
15630
15714
  const { projectRoot, langs, ignore = [], signal } = opts;
15631
15715
  const relationGraphVersion = "2";
15632
15716
  const refResolutionVersion = "2";
@@ -15646,6 +15730,7 @@ async function runIndexerWithStore(store, opts) {
15646
15730
  let discoveredFiles = null;
15647
15731
  let discoveryComplete = true;
15648
15732
  let trustedUnchanged;
15733
+ let discoverySnapshotKey;
15649
15734
  if (opts.files && opts.files.length > 0) {
15650
15735
  files = opts.files.map((f) => path24.resolve(projectRoot, f)).filter((f) => {
15651
15736
  if (!isWithinProject(projectRoot, f)) return false;
@@ -15659,6 +15744,7 @@ async function runIndexerWithStore(store, opts) {
15659
15744
  discoveryComplete = discovery.complete;
15660
15745
  discoveredFiles = new Set(files);
15661
15746
  trustedUnchanged = discovery.trustedUnchanged;
15747
+ discoverySnapshotKey = discovery.snapshotKey;
15662
15748
  }
15663
15749
  if (langs && langs.length > 0) {
15664
15750
  const langSet = new Set(langs);
@@ -15672,6 +15758,8 @@ async function runIndexerWithStore(store, opts) {
15672
15758
  if (!force) {
15673
15759
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
15674
15760
  }
15761
+ const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
15762
+ if (!snapshotTrusted) trustedUnchanged = void 0;
15675
15763
  const totalFilesForProgress = files.length;
15676
15764
  let filesPreSkipped = 0;
15677
15765
  if (!force && trustedUnchanged) {
@@ -15734,9 +15822,6 @@ async function runIndexerWithStore(store, opts) {
15734
15822
  };
15735
15823
  }
15736
15824
  const meta = existingMeta.get(file);
15737
- if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
15738
- return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
15739
- }
15740
15825
  let content;
15741
15826
  try {
15742
15827
  content = await fs18.readFile(file, { encoding: "utf8", signal });
@@ -15965,9 +16050,18 @@ async function runIndexerWithStore(store, opts) {
15965
16050
  });
15966
16051
  store.setMetadata("ref_resolution_version", refResolutionVersion);
15967
16052
  store.setMetadata("relation_graph_version", relationGraphVersion);
16053
+ const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
16054
+ if (completeProjectScope && discoverySnapshotKey !== void 0) {
16055
+ const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
16056
+ if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
16057
+ throw new IndexSourceChangedError(
16058
+ "Project files changed during indexing; retrying before publishing the generation."
16059
+ );
16060
+ }
16061
+ store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
16062
+ }
15968
16063
  if (!opts.files || filesIndexed >= 50) store.optimize();
15969
16064
  store.setLastIndexed(Date.now());
15970
- if (!opts.files) store.compactIfNeeded();
15971
16065
  const durationMs = Date.now() - startMs;
15972
16066
  return {
15973
16067
  filesIndexed,
@@ -16252,6 +16346,13 @@ function callIndexOp(op, args, opts) {
16252
16346
  });
16253
16347
  }
16254
16348
  async function callInline(op, args, opts) {
16349
+ if (op !== "index" && _indexing) {
16350
+ const error = new Error(
16351
+ "Codebase index refresh in progress; retry after the completed generation is published."
16352
+ );
16353
+ error.name = "IndexRefreshInProgressError";
16354
+ throw error;
16355
+ }
16255
16356
  const ac = new AbortController();
16256
16357
  const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
16257
16358
  if (opts.signal?.aborted) onOuterAbort();
@@ -16479,12 +16580,12 @@ var codebaseSearchTool = {
16479
16580
  },
16480
16581
  async execute(input, ctx, execOpts) {
16481
16582
  const state = getIndexState();
16482
- if (state.indexing && !state.ready) {
16583
+ if (state.indexing) {
16483
16584
  return {
16484
16585
  results: [],
16485
16586
  total: 0,
16486
16587
  query: input.query,
16487
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16588
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16488
16589
  };
16489
16590
  }
16490
16591
  if (state.lastError) {
@@ -16665,12 +16766,12 @@ var codebaseIncomingCallsTool = {
16665
16766
  },
16666
16767
  async execute(input, ctx) {
16667
16768
  const state = getIndexState();
16668
- if (state.indexing && !state.ready) {
16769
+ if (state.indexing) {
16669
16770
  return {
16670
16771
  symbol: input.symbol,
16671
16772
  calls: [],
16672
16773
  total: 0,
16673
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16774
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16674
16775
  };
16675
16776
  }
16676
16777
  if (state.lastError) {
@@ -16687,16 +16788,14 @@ var codebaseIncomingCallsTool = {
16687
16788
  const transitive = input.transitive === true;
16688
16789
  let serviced;
16689
16790
  try {
16690
- serviced = await incomingCallsService2(
16691
- {
16692
- projectRoot: ctx.projectRoot,
16693
- indexDir: codebaseIndexDirOverride(ctx),
16694
- symbol: input.symbol,
16695
- file: input.file,
16696
- limit,
16697
- transitive
16698
- }
16699
- );
16791
+ serviced = await incomingCallsService2({
16792
+ projectRoot: ctx.projectRoot,
16793
+ indexDir: codebaseIndexDirOverride(ctx),
16794
+ symbol: input.symbol,
16795
+ file: input.file,
16796
+ limit,
16797
+ transitive
16798
+ });
16700
16799
  } catch (err) {
16701
16800
  return {
16702
16801
  symbol: input.symbol,
@@ -16735,10 +16834,14 @@ var codebaseIncomingCallsTool = {
16735
16834
  }
16736
16835
  const notes = [];
16737
16836
  if (totalMatches > limit) {
16738
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
16837
+ notes.push(
16838
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
16839
+ );
16739
16840
  }
16740
16841
  if (ambiguous) {
16741
- notes.push(`Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`);
16842
+ notes.push(
16843
+ `Symbol "${input.symbol}" exists in multiple files. Results include callers of all same-named symbols. Use codebase-search to find the exact file and pass it as \`file\`.`
16844
+ );
16742
16845
  }
16743
16846
  return {
16744
16847
  symbol: input.symbol,
@@ -16788,12 +16891,12 @@ var codebaseOutgoingCallsTool = {
16788
16891
  },
16789
16892
  async execute(input, ctx) {
16790
16893
  const state = getIndexState();
16791
- if (state.indexing && !state.ready) {
16894
+ if (state.indexing) {
16792
16895
  return {
16793
16896
  symbol: input.symbol,
16794
16897
  calls: [],
16795
16898
  total: 0,
16796
- indexStatus: `Indexing in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry in a moment.`
16899
+ indexStatus: `Index refresh in progress (${state.currentFile}/${state.totalFiles} files) \u2014 retry after the completed generation is published.`
16797
16900
  };
16798
16901
  }
16799
16902
  if (state.lastError) {
@@ -16810,16 +16913,14 @@ var codebaseOutgoingCallsTool = {
16810
16913
  const transitive = input.transitive === true;
16811
16914
  let serviced;
16812
16915
  try {
16813
- serviced = await outgoingCallsService2(
16814
- {
16815
- projectRoot: ctx.projectRoot,
16816
- indexDir: codebaseIndexDirOverride(ctx),
16817
- symbol: input.symbol,
16818
- file: input.file,
16819
- limit,
16820
- transitive
16821
- }
16822
- );
16916
+ serviced = await outgoingCallsService2({
16917
+ projectRoot: ctx.projectRoot,
16918
+ indexDir: codebaseIndexDirOverride(ctx),
16919
+ symbol: input.symbol,
16920
+ file: input.file,
16921
+ limit,
16922
+ transitive
16923
+ });
16823
16924
  } catch (err) {
16824
16925
  return {
16825
16926
  symbol: input.symbol,
@@ -16858,10 +16959,14 @@ var codebaseOutgoingCallsTool = {
16858
16959
  }
16859
16960
  const notes = [];
16860
16961
  if (totalMatches > limit) {
16861
- notes.push(`Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`);
16962
+ notes.push(
16963
+ `Results capped at ${limit} of ${totalMatches} call sites. Increase \`limit\` or use \`file\` to narrow.`
16964
+ );
16862
16965
  }
16863
16966
  if (unresolvedCount > 0) {
16864
- notes.push(`${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`);
16967
+ notes.push(
16968
+ `${unresolvedCount} unresolved reference(s) not shown \u2014 their targets could not be resolved during indexing.`
16969
+ );
16865
16970
  }
16866
16971
  return {
16867
16972
  symbol: input.symbol,
@@ -17958,7 +18063,7 @@ var documentTool = {
17958
18063
  }
17959
18064
  },
17960
18065
  async execute(input, ctx) {
17961
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
18066
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
17962
18067
  const style = input.style ?? "jsdoc";
17963
18068
  const results = [];
17964
18069
  let filesProcessed = 0;
@@ -17967,7 +18072,7 @@ var documentTool = {
17967
18072
  Array.isArray(input.files) ? input.files.join(",") : input.files,
17968
18073
  cwd,
17969
18074
  ctx
17970
- ) : input.path ? [safeResolve(input.path, ctx)] : [];
18075
+ ) : input.path ? [await safeResolveReal(input.path, ctx)] : [];
17971
18076
  for (const absPath of fileList) {
17972
18077
  try {
17973
18078
  const content = await fs23.readFile(absPath, "utf8");
@@ -21083,7 +21188,7 @@ var formatTool = {
21083
21188
  return final;
21084
21189
  },
21085
21190
  async *executeStream(input, ctx, opts) {
21086
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
21191
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
21087
21192
  const fixer = input.fixer ?? "auto";
21088
21193
  if (fixer === "auto" && !input.files) {
21089
21194
  const op = input.check ? "format-check" : "format-write";
@@ -22301,7 +22406,7 @@ var installTool = {
22301
22406
  return final;
22302
22407
  },
22303
22408
  async *executeStream(input, ctx, opts) {
22304
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
22409
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
22305
22410
  if (!input.global) {
22306
22411
  const pkgList2 = input.packages ? Array.isArray(input.packages) ? input.packages : input.packages.split(",").map((p) => p.trim()) : [];
22307
22412
  const bridge = await tryLegacyPackageOperation(
@@ -25477,7 +25582,7 @@ var lintTool = {
25477
25582
  return final;
25478
25583
  },
25479
25584
  async *executeStream(input, ctx, opts) {
25480
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
25585
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
25481
25586
  const linter = input.linter ?? "auto";
25482
25587
  if (linter === "auto" && !input.files) {
25483
25588
  const bridge = await tryLegacyCodeOperation("lint", {
@@ -25603,7 +25708,7 @@ var logsTool = {
25603
25708
  }
25604
25709
  },
25605
25710
  async execute(input, ctx, opts) {
25606
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
25711
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
25607
25712
  const lines = input.lines ?? 100;
25608
25713
  let filterRe = null;
25609
25714
  if (input.filter) {
@@ -25810,7 +25915,7 @@ var outdatedTool = {
25810
25915
  }
25811
25916
  },
25812
25917
  async execute(input, ctx, opts) {
25813
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
25918
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
25814
25919
  const manager = await detectPackageManager(cwd, ctx.projectRoot);
25815
25920
  if (manager === "npm") {
25816
25921
  try {
@@ -27524,7 +27629,7 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
27524
27629
  const parts = normalized.split(",").map((s) => s.trim()).filter(Boolean);
27525
27630
  const resolved = [];
27526
27631
  for (const p of parts) {
27527
- const absPath = safeResolve(p, ctx);
27632
+ const absPath = await safeResolveReal(p, ctx);
27528
27633
  if (extraGlob && !passesExtraGlob(extraGlob, path35.basename(absPath), absPath)) continue;
27529
27634
  const stat19 = await fs30.stat(absPath).catch(() => null);
27530
27635
  if (stat19?.isFile()) {
@@ -27762,7 +27867,7 @@ var scaffoldTool = {
27762
27867
  required: ["template", "name"]
27763
27868
  },
27764
27869
  async execute(input, ctx) {
27765
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
27870
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
27766
27871
  const name = input.name;
27767
27872
  const vars = { name, ...input.vars };
27768
27873
  const builtIn = BUILT_IN_TEMPLATES[input.template];
@@ -28850,7 +28955,7 @@ var testTool = {
28850
28955
  return final;
28851
28956
  },
28852
28957
  async *executeStream(input, ctx, opts) {
28853
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
28958
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
28854
28959
  const runner = input.runner ?? "auto";
28855
28960
  if (runner === "auto") {
28856
28961
  const bridge = await tryLegacyCodeOperation("test", {
@@ -29365,7 +29470,7 @@ var treeTool = {
29365
29470
  return final;
29366
29471
  },
29367
29472
  async *executeStream(input, ctx, opts) {
29368
- const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
29473
+ const basePath = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
29369
29474
  const maxDepth = input.depth ?? 3;
29370
29475
  const showFiles = input.show_files ?? true;
29371
29476
  const showDirs = input.show_dirs ?? true;
@@ -29545,7 +29650,7 @@ var typecheckTool = {
29545
29650
  return final;
29546
29651
  },
29547
29652
  async *executeStream(input, ctx, opts) {
29548
- const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
29653
+ const cwd = input.cwd ? await safeResolveReal(input.cwd, ctx) : ctx.cwd;
29549
29654
  const bridge = await tryLegacyCodeOperation("semantic", {
29550
29655
  cwd,
29551
29656
  projectRoot: ctx.projectRoot,
@@ -29582,7 +29687,7 @@ var typecheckTool = {
29582
29687
  cmdArgs = ["tsc", ...tscArgs];
29583
29688
  }
29584
29689
  } else {
29585
- const tsconfig = input.project ? safeResolve(input.project, ctx) : await findTsConfig(cwd);
29690
+ const tsconfig = input.project ? await safeResolveReal(input.project, ctx) : await findTsConfig(cwd);
29586
29691
  const tscArgs = ["--noEmit"];
29587
29692
  if (input.strict) tscArgs.push("--strict");
29588
29693
  if (tsconfig) tscArgs.push("--project", tsconfig);