@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/patch.js CHANGED
@@ -55,8 +55,9 @@ async function resolveRealInsideRoot(absPath, ctx) {
55
55
  }
56
56
  throw err;
57
57
  }
58
- if (isInsideAny(real, realRoots)) {
59
- return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
58
+ const candidate = pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
59
+ if (isInsideAny(candidate, realRoots)) {
60
+ return candidate;
60
61
  }
61
62
  throw new Error(
62
63
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
package/dist/read.js CHANGED
@@ -2753,8 +2753,9 @@ async function resolveRealInsideRoot(absPath, ctx) {
2753
2753
  }
2754
2754
  throw err;
2755
2755
  }
2756
- if (isInsideAny(real, realRoots)) {
2757
- return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
2756
+ const candidate = pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
2757
+ if (isInsideAny(candidate, realRoots)) {
2758
+ return candidate;
2758
2759
  }
2759
2760
  throw new Error(
2760
2761
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
@@ -2858,6 +2859,7 @@ var indexCircuitBreaker = new IndexCircuitBreaker();
2858
2859
  // src/codebase-index/indexer.ts
2859
2860
  import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
2860
2861
  import { execFile } from "node:child_process";
2862
+ import { createHash as createHash2 } from "node:crypto";
2861
2863
  import * as fs9 from "node:fs/promises";
2862
2864
  import { availableParallelism } from "node:os";
2863
2865
  import * as path13 from "node:path";
@@ -4178,6 +4180,85 @@ function runSqliteWithRetry(fn) {
4178
4180
  throw lastError;
4179
4181
  }
4180
4182
 
4183
+ // src/codebase-index/vector-search.ts
4184
+ var RRF_K = 60;
4185
+ var VECTOR_DIMENSIONS = 384;
4186
+ var NGRAM_SIZE = 3;
4187
+ function embedText(text) {
4188
+ const vec = new Float32Array(VECTOR_DIMENSIONS);
4189
+ const normalized = text.toLowerCase().trim();
4190
+ if (normalized.length < NGRAM_SIZE) {
4191
+ const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
4192
+ for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
4193
+ const ngram = padded.slice(i, i + NGRAM_SIZE);
4194
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4195
+ vec[bucket] += 1;
4196
+ }
4197
+ } else {
4198
+ for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
4199
+ const ngram = normalized.slice(i, i + NGRAM_SIZE);
4200
+ const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
4201
+ vec[bucket] += 1;
4202
+ }
4203
+ }
4204
+ let norm = 0;
4205
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4206
+ norm += vec[i] * vec[i];
4207
+ }
4208
+ norm = Math.sqrt(norm);
4209
+ if (norm > 0) {
4210
+ for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
4211
+ vec[i] /= norm;
4212
+ }
4213
+ }
4214
+ return vec;
4215
+ }
4216
+ function hashNgram(str) {
4217
+ let hash = 2166136261;
4218
+ for (let i = 0; i < str.length; i++) {
4219
+ hash ^= str.charCodeAt(i);
4220
+ hash = Math.imul(hash, 16777619);
4221
+ }
4222
+ return hash >>> 0;
4223
+ }
4224
+ function cosineSimilarity(a, b) {
4225
+ let dot = 0;
4226
+ const len = Math.min(a.length, b.length);
4227
+ for (let i = 0; i < len; i++) {
4228
+ dot += a[i] * b[i];
4229
+ }
4230
+ return dot;
4231
+ }
4232
+ function encodeVector(vec) {
4233
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
4234
+ }
4235
+ function decodeVector(buf) {
4236
+ const view = new DataView(
4237
+ buf.buffer,
4238
+ buf.byteOffset,
4239
+ buf.byteLength
4240
+ );
4241
+ const copy = new Float32Array(buf.byteLength / 4);
4242
+ for (let i = 0; i < copy.length; i++) {
4243
+ copy[i] = view.getFloat32(i * 4, true);
4244
+ }
4245
+ return copy;
4246
+ }
4247
+ function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
4248
+ const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
4249
+ const scored = [];
4250
+ for (const id of allIds) {
4251
+ const bm25Rank = bm25Ranks.get(id);
4252
+ const vecRank = vectorRanks.get(id);
4253
+ let score = 0;
4254
+ if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
4255
+ if (vecRank !== void 0) score += 1 / (k + vecRank);
4256
+ scored.push([id, score]);
4257
+ }
4258
+ scored.sort((a, b) => b[1] - a[1]);
4259
+ return scored;
4260
+ }
4261
+
4181
4262
  // src/codebase-index/writer-admin.ts
4182
4263
  import * as fs7 from "node:fs";
4183
4264
  import * as path11 from "node:path";
@@ -5237,90 +5318,19 @@ var StorePool = class {
5237
5318
  }
5238
5319
  };
5239
5320
 
5240
- // src/codebase-index/vector-search.ts
5241
- var RRF_K = 60;
5242
- var VECTOR_DIMENSIONS = 384;
5243
- var NGRAM_SIZE = 3;
5244
- function embedText(text) {
5245
- const vec = new Float32Array(VECTOR_DIMENSIONS);
5246
- const normalized = text.toLowerCase().trim();
5247
- if (normalized.length < NGRAM_SIZE) {
5248
- const padded = ` ${normalized} `.slice(0, Math.max(NGRAM_SIZE, normalized.length + 2));
5249
- for (let i = 0; i <= padded.length - NGRAM_SIZE; i++) {
5250
- const ngram = padded.slice(i, i + NGRAM_SIZE);
5251
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5252
- vec[bucket] += 1;
5253
- }
5254
- } else {
5255
- for (let i = 0; i <= normalized.length - NGRAM_SIZE; i++) {
5256
- const ngram = normalized.slice(i, i + NGRAM_SIZE);
5257
- const bucket = hashNgram(ngram) % VECTOR_DIMENSIONS;
5258
- vec[bucket] += 1;
5259
- }
5260
- }
5261
- let norm = 0;
5262
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5263
- norm += vec[i] * vec[i];
5264
- }
5265
- norm = Math.sqrt(norm);
5266
- if (norm > 0) {
5267
- for (let i = 0; i < VECTOR_DIMENSIONS; i++) {
5268
- vec[i] /= norm;
5269
- }
5270
- }
5271
- return vec;
5272
- }
5273
- function hashNgram(str) {
5274
- let hash = 2166136261;
5275
- for (let i = 0; i < str.length; i++) {
5276
- hash ^= str.charCodeAt(i);
5277
- hash = Math.imul(hash, 16777619);
5278
- }
5279
- return hash >>> 0;
5280
- }
5281
- function cosineSimilarity(a, b) {
5282
- let dot = 0;
5283
- const len = Math.min(a.length, b.length);
5284
- for (let i = 0; i < len; i++) {
5285
- dot += a[i] * b[i];
5286
- }
5287
- return dot;
5288
- }
5289
- function encodeVector(vec) {
5290
- return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
5291
- }
5292
- function decodeVector(buf) {
5293
- const view = new DataView(
5294
- buf.buffer,
5295
- buf.byteOffset,
5296
- buf.byteLength
5297
- );
5298
- const copy = new Float32Array(buf.byteLength / 4);
5299
- for (let i = 0; i < copy.length; i++) {
5300
- copy[i] = view.getFloat32(i * 4, true);
5301
- }
5302
- return copy;
5303
- }
5304
- function reciprocalRankFusion(bm25Ranks, vectorRanks, k = RRF_K) {
5305
- const allIds = /* @__PURE__ */ new Set([...bm25Ranks.keys(), ...vectorRanks.keys()]);
5306
- const scored = [];
5307
- for (const id of allIds) {
5308
- const bm25Rank = bm25Ranks.get(id);
5309
- const vecRank = vectorRanks.get(id);
5310
- let score = 0;
5311
- if (bm25Rank !== void 0) score += 1 / (k + bm25Rank);
5312
- if (vecRank !== void 0) score += 1 / (k + vecRank);
5313
- scored.push([id, score]);
5314
- }
5315
- scored.sort((a, b) => b[1] - a[1]);
5316
- return scored;
5317
- }
5318
-
5319
5321
  // src/codebase-index/writer.ts
5320
5322
  var DB_FILE2 = "index.db";
5321
5323
  var MAX_STATEMENT_CACHE = 128;
5322
5324
  var IndexStore = class _IndexStore {
5323
5325
  db;
5326
+ /**
5327
+ * True while an index run owns one outer SQLite transaction. Individual
5328
+ * writer methods normally protect themselves with BEGIN/COMMIT, but during
5329
+ * a refresh they join this transaction so readers observe either the last
5330
+ * completed index or the next completed index, never an in-between batch.
5331
+ */
5332
+ atomicIndexUpdateActive = false;
5333
+ writeSavepointSequence = 0;
5324
5334
  /** Absolute path to this project's index directory. */
5325
5335
  indexDir;
5326
5336
  /**
@@ -5401,6 +5411,51 @@ var IndexStore = class _IndexStore {
5401
5411
  runWithRetry(fn) {
5402
5412
  return runSqliteWithRetry(fn);
5403
5413
  }
5414
+ /** Run a complete index mutation as one WAL-visible publication. */
5415
+ async runAtomicIndexUpdate(job) {
5416
+ if (this.atomicIndexUpdateActive) return job();
5417
+ this.runWithRetry(() => this.db.exec("BEGIN IMMEDIATE"));
5418
+ this.atomicIndexUpdateActive = true;
5419
+ try {
5420
+ const result = await job();
5421
+ this.db.exec("COMMIT");
5422
+ return result;
5423
+ } catch (error) {
5424
+ try {
5425
+ this.db.exec("ROLLBACK");
5426
+ } catch {
5427
+ }
5428
+ throw error;
5429
+ } finally {
5430
+ this.atomicIndexUpdateActive = false;
5431
+ }
5432
+ }
5433
+ /**
5434
+ * Begin a method-local transaction. Inside an atomic index publication a
5435
+ * SAVEPOINT preserves the old per-batch rollback boundary, which is needed
5436
+ * when commitBatch falls back to per-file writes after one batch fails.
5437
+ */
5438
+ beginWriteTransaction() {
5439
+ if (this.atomicIndexUpdateActive) {
5440
+ const savepoint = `index_write_${++this.writeSavepointSequence}`;
5441
+ this.db.exec(`SAVEPOINT ${savepoint}`);
5442
+ return savepoint;
5443
+ }
5444
+ this.db.exec("BEGIN IMMEDIATE");
5445
+ return null;
5446
+ }
5447
+ commitWriteTransaction(savepoint) {
5448
+ if (savepoint) this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
5449
+ else this.db.exec("COMMIT");
5450
+ }
5451
+ rollbackWriteTransaction(savepoint) {
5452
+ if (savepoint) {
5453
+ this.db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
5454
+ this.db.exec(`RELEASE SAVEPOINT ${savepoint}`);
5455
+ } else {
5456
+ this.db.exec("ROLLBACK");
5457
+ }
5458
+ }
5404
5459
  /**
5405
5460
  * Mirror the in-process language→family map into SQLite.
5406
5461
  *
@@ -5635,7 +5690,7 @@ var IndexStore = class _IndexStore {
5635
5690
  insertSymbols(symbols) {
5636
5691
  this.invalidateBm25();
5637
5692
  return this.runWithRetry(() => {
5638
- this.db.exec("BEGIN IMMEDIATE");
5693
+ const ownsTransaction = this.beginWriteTransaction();
5639
5694
  try {
5640
5695
  let nextId = this.allocateSymbolIds(symbols.length);
5641
5696
  const result = [];
@@ -5662,7 +5717,9 @@ var IndexStore = class _IndexStore {
5662
5717
  }
5663
5718
  vectorRows.push({
5664
5719
  id,
5665
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
5720
+ vector: encodeVector(
5721
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
5722
+ )
5666
5723
  });
5667
5724
  result.push({ ...s, id });
5668
5725
  }
@@ -5680,10 +5737,10 @@ var IndexStore = class _IndexStore {
5680
5737
  vectorRows
5681
5738
  );
5682
5739
  }
5683
- this.db.exec("COMMIT");
5740
+ this.commitWriteTransaction(ownsTransaction);
5684
5741
  return result;
5685
5742
  } catch (err) {
5686
- this.db.exec("ROLLBACK");
5743
+ this.rollbackWriteTransaction(ownsTransaction);
5687
5744
  throw err;
5688
5745
  }
5689
5746
  });
@@ -5691,7 +5748,7 @@ var IndexStore = class _IndexStore {
5691
5748
  deleteSymbolsForFile(file) {
5692
5749
  this.invalidateBm25();
5693
5750
  this.runWithRetry(() => {
5694
- this.db.exec("BEGIN IMMEDIATE");
5751
+ const ownsTransaction = this.beginWriteTransaction();
5695
5752
  try {
5696
5753
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
5697
5754
  if (this.ftsAvailable) {
@@ -5706,9 +5763,9 @@ var IndexStore = class _IndexStore {
5706
5763
  }
5707
5764
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
5708
5765
  this.resolveRefsForNamesUnsafe(affectedNames);
5709
- this.db.exec("COMMIT");
5766
+ this.commitWriteTransaction(ownsTransaction);
5710
5767
  } catch (error) {
5711
- this.db.exec("ROLLBACK");
5768
+ this.rollbackWriteTransaction(ownsTransaction);
5712
5769
  throw error;
5713
5770
  }
5714
5771
  });
@@ -5721,7 +5778,7 @@ var IndexStore = class _IndexStore {
5721
5778
  deleteFile(file) {
5722
5779
  this.invalidateBm25();
5723
5780
  this.runWithRetry(() => {
5724
- this.db.exec("BEGIN IMMEDIATE");
5781
+ const ownsTransaction = this.beginWriteTransaction();
5725
5782
  try {
5726
5783
  const affectedNames = this.invalidateIncomingRefsForFiles([file]);
5727
5784
  if (this.ftsAvailable) {
@@ -5740,9 +5797,9 @@ var IndexStore = class _IndexStore {
5740
5797
  this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
5741
5798
  this.stmt("DELETE FROM files WHERE file = ?").run(file);
5742
5799
  this.resolveRefsForNamesUnsafe(affectedNames);
5743
- this.db.exec("COMMIT");
5800
+ this.commitWriteTransaction(ownsTransaction);
5744
5801
  } catch (err) {
5745
- this.db.exec("ROLLBACK");
5802
+ this.rollbackWriteTransaction(ownsTransaction);
5746
5803
  throw err;
5747
5804
  }
5748
5805
  });
@@ -6129,7 +6186,7 @@ var IndexStore = class _IndexStore {
6129
6186
  clearAll() {
6130
6187
  this.invalidateBm25();
6131
6188
  this.runWithRetry(() => {
6132
- this.db.exec("BEGIN IMMEDIATE");
6189
+ const ownsTransaction = this.beginWriteTransaction();
6133
6190
  try {
6134
6191
  this.db.exec("DROP TABLE IF EXISTS refs");
6135
6192
  this.db.exec("DROP TABLE IF EXISTS symbols");
@@ -6137,15 +6194,15 @@ var IndexStore = class _IndexStore {
6137
6194
  this.db.exec("DROP TABLE IF EXISTS metadata");
6138
6195
  if (this.ftsAvailable) this.db.exec("DROP TABLE IF EXISTS symbols_fts");
6139
6196
  this.db.exec("DROP TABLE IF EXISTS symbol_vectors");
6140
- this.db.exec("COMMIT");
6141
6197
  this.stmtCache.clear();
6142
6198
  this.initSchema();
6143
6199
  this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
6144
6200
  _IndexStore.NEXT_SYMBOL_ID_KEY,
6145
6201
  "1"
6146
6202
  );
6203
+ this.commitWriteTransaction(ownsTransaction);
6147
6204
  } catch (err) {
6148
- this.db.exec("ROLLBACK");
6205
+ this.rollbackWriteTransaction(ownsTransaction);
6149
6206
  throw err;
6150
6207
  }
6151
6208
  });
@@ -6207,7 +6264,7 @@ var IndexStore = class _IndexStore {
6207
6264
  }
6208
6265
  this.invalidateBm25();
6209
6266
  return this.runWithRetry(() => {
6210
- this.db.exec("BEGIN IMMEDIATE");
6267
+ const ownsTransaction = this.beginWriteTransaction();
6211
6268
  try {
6212
6269
  const affectedNames = /* @__PURE__ */ new Set();
6213
6270
  for (const entry of entries) {
@@ -6268,7 +6325,9 @@ var IndexStore = class _IndexStore {
6268
6325
  }
6269
6326
  vectorRows.push({
6270
6327
  id,
6271
- vector: encodeVector(embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment)))
6328
+ vector: encodeVector(
6329
+ embedText(s.text || buildIndexableText(s.name, s.signature, s.docComment))
6330
+ )
6272
6331
  });
6273
6332
  const inserted = { ...s, id };
6274
6333
  allInserted.push(inserted);
@@ -6313,10 +6372,10 @@ var IndexStore = class _IndexStore {
6313
6372
  );
6314
6373
  }
6315
6374
  this.resolveRefsForNamesUnsafe(affectedNames);
6316
- this.db.exec("COMMIT");
6375
+ this.commitWriteTransaction(ownsTransaction);
6317
6376
  return allInserted;
6318
6377
  } catch (err) {
6319
- this.db.exec("ROLLBACK");
6378
+ this.rollbackWriteTransaction(ownsTransaction);
6320
6379
  throw err;
6321
6380
  }
6322
6381
  });
@@ -6393,7 +6452,7 @@ var IndexStore = class _IndexStore {
6393
6452
  replaceEmptyFile(meta) {
6394
6453
  this.invalidateBm25();
6395
6454
  this.runWithRetry(() => {
6396
- this.db.exec("BEGIN IMMEDIATE");
6455
+ const ownsTransaction = this.beginWriteTransaction();
6397
6456
  try {
6398
6457
  const affectedNames = this.invalidateIncomingRefsForFiles([meta.file]);
6399
6458
  if (this.ftsAvailable) {
@@ -6428,9 +6487,9 @@ var IndexStore = class _IndexStore {
6428
6487
  meta.lastIndexed
6429
6488
  );
6430
6489
  this.resolveRefsForNamesUnsafe(affectedNames);
6431
- this.db.exec("COMMIT");
6490
+ this.commitWriteTransaction(ownsTransaction);
6432
6491
  } catch (err) {
6433
- this.db.exec("ROLLBACK");
6492
+ this.rollbackWriteTransaction(ownsTransaction);
6434
6493
  throw err;
6435
6494
  }
6436
6495
  });
@@ -6623,6 +6682,10 @@ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-l
6623
6682
  var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
6624
6683
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
6625
6684
  var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
6685
+ var GIT_SNAPSHOT_METADATA_KEY = "git_discovery_snapshot";
6686
+ var IndexSourceChangedError = class extends Error {
6687
+ name = "IndexSourceChangedError";
6688
+ };
6626
6689
  function isWithinProject(projectRoot, file) {
6627
6690
  const rel = path13.relative(projectRoot, file);
6628
6691
  return rel !== "" && !rel.startsWith(`..${path13.sep}`) && rel !== ".." && !path13.isAbsolute(rel);
@@ -6659,7 +6722,7 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6659
6722
  if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
6660
6723
  throwIfAborted(signal);
6661
6724
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
6662
- const [output, statusOutput] = await Promise.all([
6725
+ const [output, statusOutput, stagedOutput] = await Promise.all([
6663
6726
  gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
6664
6727
  gitOutput(projectRoot, [
6665
6728
  "status",
@@ -6667,7 +6730,8 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6667
6730
  "-z",
6668
6731
  "--untracked-files=all",
6669
6732
  "--ignored=no"
6670
- ])
6733
+ ]),
6734
+ gitOutput(projectRoot, ["ls-files", "--stage", "-z"])
6671
6735
  ]);
6672
6736
  throwIfAborted(signal);
6673
6737
  const dirty = /* @__PURE__ */ new Set();
@@ -6697,9 +6761,17 @@ async function findGitSourceFiles(projectRoot, ignore, signal) {
6697
6761
  const ext = path13.extname(relative3).toLowerCase();
6698
6762
  if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
6699
6763
  }
6764
+ const snapshot = createHash2("sha256").update(stagedOutput).update("\0").update(statusOutput);
6765
+ const indexedFiles = new Set(files);
6766
+ for (const dirtyFile of [...dirty].sort()) {
6767
+ if (!indexedFiles.has(dirtyFile) || deleted.has(dirtyFile)) continue;
6768
+ snapshot.update("\0").update(dirtyFile).update("\0");
6769
+ snapshot.update(xxhash64String(await fs9.readFile(dirtyFile, "utf8")));
6770
+ }
6700
6771
  return {
6701
6772
  files,
6702
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
6773
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file))),
6774
+ snapshotKey: snapshot.digest("hex")
6703
6775
  };
6704
6776
  } catch {
6705
6777
  return null;
@@ -6712,7 +6784,8 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
6712
6784
  files: gitFiles.files,
6713
6785
  complete: true,
6714
6786
  errors: [],
6715
- trustedUnchanged: gitFiles.trustedUnchanged
6787
+ trustedUnchanged: gitFiles.trustedUnchanged,
6788
+ snapshotKey: gitFiles.snapshotKey
6716
6789
  };
6717
6790
  }
6718
6791
  const results = [];
@@ -6799,6 +6872,17 @@ async function resolveProjectRelations(store, projectRoot, opts) {
6799
6872
  }
6800
6873
  }
6801
6874
  async function runIndexerWithStore(store, opts) {
6875
+ let result;
6876
+ try {
6877
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
6878
+ } catch (error) {
6879
+ if (!(error instanceof IndexSourceChangedError)) throw error;
6880
+ result = await store.runAtomicIndexUpdate(() => runIndexerAtomic(store, opts));
6881
+ }
6882
+ if (!opts.files) store.compactIfNeeded();
6883
+ return result;
6884
+ }
6885
+ async function runIndexerAtomic(store, opts) {
6802
6886
  const { projectRoot, langs, ignore = [], signal } = opts;
6803
6887
  const relationGraphVersion = "2";
6804
6888
  const refResolutionVersion = "2";
@@ -6818,6 +6902,7 @@ async function runIndexerWithStore(store, opts) {
6818
6902
  let discoveredFiles = null;
6819
6903
  let discoveryComplete = true;
6820
6904
  let trustedUnchanged;
6905
+ let discoverySnapshotKey;
6821
6906
  if (opts.files && opts.files.length > 0) {
6822
6907
  files = opts.files.map((f) => path13.resolve(projectRoot, f)).filter((f) => {
6823
6908
  if (!isWithinProject(projectRoot, f)) return false;
@@ -6831,6 +6916,7 @@ async function runIndexerWithStore(store, opts) {
6831
6916
  discoveryComplete = discovery.complete;
6832
6917
  discoveredFiles = new Set(files);
6833
6918
  trustedUnchanged = discovery.trustedUnchanged;
6919
+ discoverySnapshotKey = discovery.snapshotKey;
6834
6920
  }
6835
6921
  if (langs && langs.length > 0) {
6836
6922
  const langSet = new Set(langs);
@@ -6844,6 +6930,8 @@ async function runIndexerWithStore(store, opts) {
6844
6930
  if (!force) {
6845
6931
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
6846
6932
  }
6933
+ const snapshotTrusted = !force && discoverySnapshotKey !== void 0 && store.getMetadata(GIT_SNAPSHOT_METADATA_KEY) === discoverySnapshotKey;
6934
+ if (!snapshotTrusted) trustedUnchanged = void 0;
6847
6935
  const totalFilesForProgress = files.length;
6848
6936
  let filesPreSkipped = 0;
6849
6937
  if (!force && trustedUnchanged) {
@@ -6906,9 +6994,6 @@ async function runIndexerWithStore(store, opts) {
6906
6994
  };
6907
6995
  }
6908
6996
  const meta = existingMeta.get(file);
6909
- if (!force && meta && meta.mtimeMs === Math.floor(stat3.mtimeMs)) {
6910
- return { file, stat: stat3, lang, parsed: null, skippedMeta: meta };
6911
- }
6912
6997
  let content;
6913
6998
  try {
6914
6999
  content = await fs9.readFile(file, { encoding: "utf8", signal });
@@ -7137,9 +7222,18 @@ async function runIndexerWithStore(store, opts) {
7137
7222
  });
7138
7223
  store.setMetadata("ref_resolution_version", refResolutionVersion);
7139
7224
  store.setMetadata("relation_graph_version", relationGraphVersion);
7225
+ const completeProjectScope = !opts.files && (!langs || langs.length === 0) && (!opts.ignore || opts.ignore.length === 0);
7226
+ if (completeProjectScope && discoverySnapshotKey !== void 0) {
7227
+ const finalSnapshot = await findGitSourceFiles(projectRoot, ignore, signal);
7228
+ if (!finalSnapshot || finalSnapshot.snapshotKey !== discoverySnapshotKey) {
7229
+ throw new IndexSourceChangedError(
7230
+ "Project files changed during indexing; retrying before publishing the generation."
7231
+ );
7232
+ }
7233
+ store.setMetadata(GIT_SNAPSHOT_METADATA_KEY, errors.length === 0 ? discoverySnapshotKey : "");
7234
+ }
7140
7235
  if (!opts.files || filesIndexed >= 50) store.optimize();
7141
7236
  store.setLastIndexed(Date.now());
7142
- if (!opts.files) store.compactIfNeeded();
7143
7237
  const durationMs = Date.now() - startMs;
7144
7238
  return {
7145
7239
  filesIndexed,
@@ -7272,7 +7366,7 @@ function decodeBinaryFrame(payload) {
7272
7366
  }
7273
7367
 
7274
7368
  // src/codebase-index/project-server-endpoint.ts
7275
- import { createHash as createHash2 } from "node:crypto";
7369
+ import { createHash as createHash3 } from "node:crypto";
7276
7370
  import * as fs10 from "node:fs";
7277
7371
  import * as os3 from "node:os";
7278
7372
  import * as path14 from "node:path";
@@ -7290,7 +7384,7 @@ function projectIndexServerBuildId(entrypoint) {
7290
7384
  if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat3.mtimeMs && buildIdCache.size === stat3.size) {
7291
7385
  return buildIdCache.buildId;
7292
7386
  }
7293
- const buildId = createHash2("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
7387
+ const buildId = createHash3("sha256").update(fs10.readFileSync(file)).digest("hex").slice(0, 24);
7294
7388
  buildIdCache = { file, mtimeMs: stat3.mtimeMs, size: stat3.size, buildId };
7295
7389
  return buildId;
7296
7390
  } catch {
@@ -7303,7 +7397,7 @@ function normalizeLocalPath(value) {
7303
7397
  }
7304
7398
  function projectIndexServerKey(projectRoot, indexDir) {
7305
7399
  const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
7306
- return createHash2("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
7400
+ return createHash3("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
7307
7401
  }
7308
7402
  function projectIndexServerEndpoint(projectRoot, indexDir) {
7309
7403
  const key = projectIndexServerKey(projectRoot, indexDir);
@@ -8192,6 +8286,13 @@ function callIndexOp(op, args, opts) {
8192
8286
  });
8193
8287
  }
8194
8288
  async function callInline(op, args, opts) {
8289
+ if (op !== "index" && _indexing) {
8290
+ const error = new Error(
8291
+ "Codebase index refresh in progress; retry after the completed generation is published."
8292
+ );
8293
+ error.name = "IndexRefreshInProgressError";
8294
+ throw error;
8295
+ }
8195
8296
  const ac = new AbortController();
8196
8297
  const onOuterAbort = () => ac.abort(opts.signal?.reason ?? new Error("Indexing cancelled"));
8197
8298
  if (opts.signal?.aborted) onOuterAbort();
package/dist/replace.js CHANGED
@@ -139,6 +139,7 @@ var MAX_SUBJECT_LEN = 64 * 1024;
139
139
 
140
140
  // src/_util.ts
141
141
  import { createHash } from "node:crypto";
142
+ import * as fsp from "node:fs/promises";
142
143
  import * as path from "node:path";
143
144
  import * as Core from "@wrongstack/core/utils";
144
145
  function sha256hex(content) {
@@ -165,6 +166,40 @@ function ensureInsideRoot(absPath, ctx) {
165
166
  function safeResolve(input, ctx) {
166
167
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
167
168
  }
169
+ async function resolveRealInsideRoot(absPath, ctx) {
170
+ if (ctx.allowOutsideProjectRoot) return absPath;
171
+ const realRoots = await Promise.all(
172
+ allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
173
+ );
174
+ let probe = absPath;
175
+ const pendingTail = [];
176
+ for (; ; ) {
177
+ let real;
178
+ try {
179
+ real = await fsp.realpath(probe);
180
+ } catch (err) {
181
+ if (err.code === "ENOENT") {
182
+ const parent = path.dirname(probe);
183
+ if (parent === probe) return absPath;
184
+ pendingTail.unshift(path.basename(probe));
185
+ probe = parent;
186
+ continue;
187
+ }
188
+ throw err;
189
+ }
190
+ const candidate = pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
191
+ if (isInsideAny(candidate, realRoots)) {
192
+ return candidate;
193
+ }
194
+ throw new Error(
195
+ `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
196
+ );
197
+ }
198
+ }
199
+ async function safeResolveReal(input, ctx) {
200
+ const abs = safeResolve(input, ctx);
201
+ return await resolveRealInsideRoot(abs, ctx);
202
+ }
168
203
  function truncateDiffPayload(diff, maxBytes) {
169
204
  const total = Buffer.byteLength(diff, "utf8");
170
205
  if (total <= maxBytes) return { text: diff, truncated: false };
@@ -408,7 +443,7 @@ async function resolveFiles(filesInput, ctx, extraGlob) {
408
443
  const parts = normalized.split(",").map((s) => s.trim()).filter(Boolean);
409
444
  const resolved = [];
410
445
  for (const p of parts) {
411
- const absPath = safeResolve(p, ctx);
446
+ const absPath = await safeResolveReal(p, ctx);
412
447
  if (extraGlob && !passesExtraGlob(extraGlob, path2.basename(absPath), absPath)) continue;
413
448
  const stat2 = await fs.stat(absPath).catch(() => null);
414
449
  if (stat2?.isFile()) {