@wrongstack/tools 0.269.0 → 0.272.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.
@@ -1,5 +1,5 @@
1
- import { t as IndexResult, S as Symbol, F as FileMeta, u as SymbolKind, v as SymbolLang, w as SearchResult, x as IndexStats, R as Ref } from '../background-indexer-CJ5JiV5i.js';
2
- export { C as CircuitOpenError, a as CircuitSnapshot, b as CircuitState, y as FileSymbols, I as IndexCircuitBreaker, c as IndexTimeoutError, z as SCHEMA_VERSION, d as cancelPendingReindexes, e as codebaseIndexStats, f as codebaseIndexTool, g as codebaseSearchTool, h as codebaseStatsTool, i as enqueueReindex, j as getIndexState, k as indexCircuitBreaker, l as isIndexReady, m as isIndexableFile, n as isIndexing, o as onIndexStateChange, r as resetIndexCircuitBreaker, p as runStartupIndex, s as searchCodebaseIndex, q as shutdownCodebaseIndexHost } from '../background-indexer-CJ5JiV5i.js';
1
+ import { I as IndexResult, S as Symbol, F as FileMeta, a as SymbolKind, b as SymbolLang, c as SearchResult, d as IndexStats, R as Ref } from '../background-indexer-BoTUw0EM.js';
2
+ export { C as CircuitOpenError, e as CircuitSnapshot, f as CircuitState, g as FileSymbols, h as IndexCircuitBreaker, i as IndexTimeoutError, j as SCHEMA_VERSION, k as cancelPendingReindexes, l as codebaseIndexStats, m as codebaseIndexTool, n as codebaseSearchTool, o as codebaseStatsTool, p as enqueueReindex, q as getIndexState, r as indexCircuitBreaker, s as isIndexReady, t as isIndexableFile, u as isIndexing, v as onIndexStateChange, w as resetIndexCircuitBreaker, x as runStartupIndex, y as searchCodebaseIndex, z as shutdownCodebaseIndexHost } from '../background-indexer-BoTUw0EM.js';
3
3
  import { Context } from '@wrongstack/core';
4
4
 
5
5
  interface IndexerOptions {
@@ -70,7 +70,17 @@ declare class IndexStore {
70
70
  indexDir?: string | undefined;
71
71
  });
72
72
  private initSchema;
73
- insertSymbols(symbols: Symbol[], nextId: number): number;
73
+ /**
74
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
75
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
76
+ * the same transaction, preventing UNIQUE constraint violations when two
77
+ * processes index concurrently (each would see a different `MAX(id)` and
78
+ * neither can insert with the other's IDs).
79
+ *
80
+ * @returns The symbols array with `id` fields populated so the caller can
81
+ * use them for refs without re-reading from the DB.
82
+ */
83
+ insertSymbols(symbols: Symbol[]): Symbol[];
74
84
  deleteSymbolsForFile(file: string): void;
75
85
  /**
76
86
  * Remove every trace of a file (refs, symbols, FTS rows, file meta). Used
@@ -2,13 +2,12 @@ import { resolveWstackPaths, expectDefined, compileGlob, truncate } from '@wrong
2
2
  import { toErrorMessage } from '@wrongstack/core/utils';
3
3
  import { createRequire } from 'node:module';
4
4
  import * as fs from 'node:fs';
5
- import { writeFileSync, mkdirSync } from 'node:fs';
6
5
  import * as path4 from 'node:path';
7
6
  import { fileURLToPath } from 'node:url';
8
7
  import { Worker } from 'node:worker_threads';
9
- import * as fs3 from 'node:fs/promises';
8
+ import * as fs6 from 'node:fs/promises';
10
9
  import * as ts from 'typescript';
11
- import { execFileSync, spawnSync } from 'node:child_process';
10
+ import { execFileSync, spawn } from 'node:child_process';
12
11
  import * as os from 'node:os';
13
12
 
14
13
  // src/codebase-index/writer.ts
@@ -428,33 +427,53 @@ var IndexStore = class {
428
427
  }
429
428
  }
430
429
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
431
- insertSymbols(symbols, nextId) {
430
+ /**
431
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
432
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
433
+ * the same transaction, preventing UNIQUE constraint violations when two
434
+ * processes index concurrently (each would see a different `MAX(id)` and
435
+ * neither can insert with the other's IDs).
436
+ *
437
+ * @returns The symbols array with `id` fields populated so the caller can
438
+ * use them for refs without re-reading from the DB.
439
+ */
440
+ insertSymbols(symbols) {
432
441
  return this.runWithRetry(() => {
433
- const stmt = this.db.prepare(
434
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
435
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
436
- );
437
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
438
- let id = nextId;
439
- for (const s of symbols) {
440
- stmt.run(
441
- id,
442
- s.lang,
443
- s.kind,
444
- s.name,
445
- s.file,
446
- s.line,
447
- s.col,
448
- s.signature,
449
- s.docComment,
450
- s.scope,
451
- s.text,
452
- s.file
442
+ this.db.exec("BEGIN IMMEDIATE");
443
+ try {
444
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
445
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
446
+ const stmt = this.db.prepare(
447
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
448
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
453
449
  );
454
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
455
- id++;
450
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
451
+ const result = [];
452
+ for (const s of symbols) {
453
+ const id = nextId++;
454
+ stmt.run(
455
+ id,
456
+ s.lang,
457
+ s.kind,
458
+ s.name,
459
+ s.file,
460
+ s.line,
461
+ s.col,
462
+ s.signature,
463
+ s.docComment,
464
+ s.scope,
465
+ s.text,
466
+ s.file
467
+ );
468
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
469
+ result.push({ ...s, id });
470
+ }
471
+ this.db.exec("COMMIT");
472
+ return result;
473
+ } catch (err) {
474
+ this.db.exec("ROLLBACK");
475
+ throw err;
456
476
  }
457
- return id;
458
477
  });
459
478
  }
460
479
  deleteSymbolsForFile(file) {
@@ -1008,10 +1027,10 @@ function detectLang(file) {
1008
1027
  if (idx < 0) return null;
1009
1028
  return extToLang(file.slice(idx));
1010
1029
  }
1011
- function parseSymbols2(opts) {
1030
+ async function parseSymbols2(opts) {
1012
1031
  const { file, content, lang } = opts;
1013
1032
  try {
1014
- return syncGoParse(file, content, lang);
1033
+ return await syncGoParse(file, content, lang);
1015
1034
  } catch {
1016
1035
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1017
1036
  }
@@ -1248,19 +1267,34 @@ func formatType(t ast.Expr) string {
1248
1267
  }
1249
1268
  }
1250
1269
  `;
1251
- function syncGoParse(filePath, content, lang) {
1270
+ async function syncGoParse(filePath, content, lang) {
1252
1271
  const tmpDir = path4.join(os.tmpdir(), "ws-go-parse");
1253
1272
  try {
1254
- mkdirSync(tmpDir, { recursive: true });
1273
+ await fs6.mkdir(tmpDir, { recursive: true });
1255
1274
  const scriptPath = path4.join(tmpDir, "parse.go");
1256
- writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
1257
- const stdout = execFileSync("go", ["run", scriptPath], {
1258
- input: content,
1259
- timeout: 15e3,
1260
- encoding: "utf8",
1275
+ await fs6.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
1276
+ const proc = spawn("go", ["run", scriptPath], {
1277
+ stdio: ["pipe", "pipe", "pipe"],
1261
1278
  windowsHide: true
1262
1279
  });
1263
- if (!stdout.trim()) {
1280
+ let stdout = "";
1281
+ proc.stdout?.on("data", (chunk) => {
1282
+ stdout += chunk.toString();
1283
+ });
1284
+ proc.stdin?.write(content);
1285
+ proc.stdin?.end();
1286
+ const { code } = await Promise.race([
1287
+ new Promise((resolve2) => {
1288
+ proc.on("close", (c) => resolve2({ code: c }));
1289
+ }),
1290
+ new Promise(
1291
+ (_, reject) => setTimeout(() => {
1292
+ proc.kill("SIGKILL");
1293
+ reject(new Error("timeout"));
1294
+ }, 15e3)
1295
+ )
1296
+ ]).catch(() => ({ code: -1 }));
1297
+ if (code !== 0 || !stdout.trim()) {
1264
1298
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1265
1299
  }
1266
1300
  const raw = JSON.parse(stdout.trim());
@@ -1282,10 +1316,10 @@ function syncGoParse(filePath, content, lang) {
1282
1316
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1283
1317
  }
1284
1318
  }
1285
- function parseSymbols3(opts) {
1319
+ async function parseSymbols3(opts) {
1286
1320
  const { file, lang } = opts;
1287
1321
  try {
1288
- return syncPyParse(file, lang);
1322
+ return await syncPyParse(file, lang);
1289
1323
  } catch {
1290
1324
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1291
1325
  }
@@ -1494,18 +1528,32 @@ visitor.visit(tree)
1494
1528
 
1495
1529
  print(json.dumps([s.to_dict() for s in syms]))
1496
1530
  `;
1497
- function syncPyParse(filePath, lang) {
1531
+ async function syncPyParse(filePath, lang) {
1498
1532
  try {
1499
1533
  const tmpDir = path4.join(os.tmpdir(), "ws-py-parse");
1500
- mkdirSync(tmpDir, { recursive: true });
1534
+ await fs6.mkdir(tmpDir, { recursive: true });
1501
1535
  const scriptPath = path4.join(tmpDir, "parse.py");
1502
- writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
1503
- const stdout = execFileSync("python", [scriptPath, filePath], {
1504
- timeout: 15e3,
1505
- encoding: "utf8",
1536
+ await fs6.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
1537
+ const proc = spawn("python", [scriptPath, filePath], {
1538
+ stdio: ["pipe", "pipe", "pipe"],
1506
1539
  windowsHide: true
1507
1540
  });
1508
- if (!stdout.trim()) {
1541
+ let stdout = "";
1542
+ proc.stdout?.on("data", (chunk) => {
1543
+ stdout += chunk.toString();
1544
+ });
1545
+ const { code } = await Promise.race([
1546
+ new Promise((resolve2) => {
1547
+ proc.on("close", (c) => resolve2({ code: c }));
1548
+ }),
1549
+ new Promise(
1550
+ (_, reject) => setTimeout(() => {
1551
+ proc.kill("SIGKILL");
1552
+ reject(new Error("timeout"));
1553
+ }, 15e3)
1554
+ )
1555
+ ]).catch(() => ({ code: -1 }));
1556
+ if (code !== 0 || !stdout.trim()) {
1509
1557
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1510
1558
  }
1511
1559
  const raw = JSON.parse(stdout.trim());
@@ -1527,11 +1575,11 @@ function syncPyParse(filePath, lang) {
1527
1575
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1528
1576
  }
1529
1577
  }
1530
- function parseSymbols4(opts) {
1578
+ async function parseSymbols4(opts) {
1531
1579
  const { file, content, lang } = opts;
1532
1580
  const nativeAvailable = checkNativeParser();
1533
1581
  if (nativeAvailable) {
1534
- const result = tryNativeParse(file, content);
1582
+ const result = await tryNativeParse(file, content);
1535
1583
  if (result) return result;
1536
1584
  }
1537
1585
  return regexParse({ file, content, lang });
@@ -1561,25 +1609,34 @@ function checkNativeParser() {
1561
1609
  return false;
1562
1610
  }
1563
1611
  }
1564
- function tryNativeParse(file, content) {
1612
+ async function tryNativeParse(file, content) {
1565
1613
  try {
1566
1614
  const toolsDir = path4.join(process.cwd(), "tools");
1567
1615
  const crateDir = path4.join(toolsDir, "syn-parser");
1568
1616
  const tmpFile = path4.join(crateDir, "src", "input.rs");
1569
- writeFileSync(tmpFile, content, "utf8");
1570
- const result = spawnSync(
1571
- "cargo",
1572
- ["run", "--manifest-path", path4.join(toolsDir, "Cargo.toml")],
1573
- {
1574
- cwd: process.cwd(),
1575
- encoding: "utf8",
1576
- timeout: 15e3,
1577
- stdio: ["pipe", "pipe", "pipe"],
1578
- windowsHide: true
1579
- }
1580
- );
1581
- if (result.status === 0 && result.stdout) {
1582
- const symbols = JSON.parse(result.stdout);
1617
+ await fs6.writeFile(tmpFile, content, "utf8");
1618
+ const proc = spawn("cargo", ["run", "--manifest-path", path4.join(toolsDir, "Cargo.toml")], {
1619
+ cwd: process.cwd(),
1620
+ stdio: ["pipe", "pipe", "pipe"],
1621
+ windowsHide: true
1622
+ });
1623
+ let stdout = "";
1624
+ proc.stdout?.on("data", (chunk) => {
1625
+ stdout += chunk.toString();
1626
+ });
1627
+ const { code } = await Promise.race([
1628
+ new Promise((resolve2) => {
1629
+ proc.on("close", (c) => resolve2({ code: c }));
1630
+ }),
1631
+ new Promise(
1632
+ (_, reject) => setTimeout(() => {
1633
+ proc.kill("SIGKILL");
1634
+ reject(new Error("timeout"));
1635
+ }, 15e3)
1636
+ )
1637
+ ]).catch(() => ({ code: -1 }));
1638
+ if (code === 0 && stdout.trim()) {
1639
+ const symbols = JSON.parse(stdout.trim());
1583
1640
  return {
1584
1641
  file,
1585
1642
  lang: "rs",
@@ -2048,7 +2105,7 @@ function compileGitignore(lines) {
2048
2105
  async function loadGitignoreMatcher(projectRoot) {
2049
2106
  let lines = [];
2050
2107
  try {
2051
- const raw = await fs3.readFile(path4.join(projectRoot, ".gitignore"), "utf8");
2108
+ const raw = await fs6.readFile(path4.join(projectRoot, ".gitignore"), "utf8");
2052
2109
  lines = raw.split("\n");
2053
2110
  } catch {
2054
2111
  }
@@ -2106,7 +2163,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2106
2163
  }
2107
2164
  let entries;
2108
2165
  try {
2109
- entries = await fs3.readdir(dir, { withFileTypes: true });
2166
+ entries = await fs6.readdir(dir, { withFileTypes: true });
2110
2167
  } catch {
2111
2168
  return;
2112
2169
  }
@@ -2204,7 +2261,7 @@ async function runIndexerWithStore(store, opts) {
2204
2261
  batchFiles.map(async (file) => {
2205
2262
  let stat2;
2206
2263
  try {
2207
- stat2 = await fs3.stat(file, statOpts);
2264
+ stat2 = await fs6.stat(file, statOpts);
2208
2265
  } catch (e) {
2209
2266
  if (isAbortError(e)) throw e;
2210
2267
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -2218,7 +2275,7 @@ async function runIndexerWithStore(store, opts) {
2218
2275
  }
2219
2276
  let content;
2220
2277
  try {
2221
- content = await fs3.readFile(file, { encoding: "utf8", signal });
2278
+ content = await fs6.readFile(file, { encoding: "utf8", signal });
2222
2279
  } catch (e) {
2223
2280
  if (isAbortError(e)) throw e;
2224
2281
  return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -2268,9 +2325,7 @@ async function runIndexerWithStore(store, opts) {
2268
2325
  filesIndexed++;
2269
2326
  continue;
2270
2327
  }
2271
- const nextId = store.getMaxSymbolId() + 1;
2272
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
2273
- store.insertSymbols(symbolsWithIds, nextId);
2328
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
2274
2329
  const count = symbolsWithIds.length;
2275
2330
  symbolsIndexed += count;
2276
2331
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -2299,7 +2354,7 @@ async function runIndexerWithStore(store, opts) {
2299
2354
  }
2300
2355
  for (const [file_] of existingMeta) {
2301
2356
  try {
2302
- await fs3.stat(file_);
2357
+ await fs6.stat(file_);
2303
2358
  } catch {
2304
2359
  store.deleteFile(file_);
2305
2360
  }
@@ -2572,10 +2627,10 @@ function debounceKey(indexDir, file) {
2572
2627
  function isIndexableFile(filePath) {
2573
2628
  return detectLang(filePath) !== null;
2574
2629
  }
2575
- function isUniqueConstraintError(err) {
2630
+ function isRecoverableConstraintError(err) {
2576
2631
  if (err instanceof Error) {
2577
2632
  const msg = err.message.toLowerCase();
2578
- return msg.includes("unique constraint") || msg.includes("UNIQUE constraint");
2633
+ return msg.includes("unique constraint") || msg.includes("constraint failed");
2579
2634
  }
2580
2635
  return false;
2581
2636
  }
@@ -2608,7 +2663,7 @@ async function runStartupIndex(opts) {
2608
2663
  return result;
2609
2664
  } catch (err) {
2610
2665
  _lastError = err instanceof Error ? err.message : String(err);
2611
- if (isUniqueConstraintError(err) && !opts.force) {
2666
+ if (isRecoverableConstraintError(err) && !opts.force) {
2612
2667
  _lastError = null;
2613
2668
  const rebuildResult = await runStartupIndex({
2614
2669
  ...opts,