@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,13 +1,12 @@
1
1
  import { parentPort } from 'node:worker_threads';
2
2
  import { expectDefined, resolveWstackPaths, compileGlob, truncate } from '@wrongstack/core';
3
- import * as fs3 from 'node:fs/promises';
3
+ import * as fs6 from 'node:fs/promises';
4
4
  import * as path4 from 'node:path';
5
5
  import { toErrorMessage } from '@wrongstack/core/utils';
6
6
  import { createRequire } from 'node:module';
7
7
  import * as fs from 'node:fs';
8
- import { writeFileSync, mkdirSync } from 'node:fs';
9
8
  import * as ts from 'typescript';
10
- import { execFileSync, spawnSync } from 'node:child_process';
9
+ import { execFileSync, spawn } from 'node:child_process';
11
10
  import * as os from 'node:os';
12
11
 
13
12
  // src/codebase-index/worker.ts
@@ -385,33 +384,53 @@ var IndexStore = class {
385
384
  }
386
385
  }
387
386
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
388
- insertSymbols(symbols, nextId) {
387
+ /**
388
+ * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
389
+ * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
390
+ * the same transaction, preventing UNIQUE constraint violations when two
391
+ * processes index concurrently (each would see a different `MAX(id)` and
392
+ * neither can insert with the other's IDs).
393
+ *
394
+ * @returns The symbols array with `id` fields populated so the caller can
395
+ * use them for refs without re-reading from the DB.
396
+ */
397
+ insertSymbols(symbols) {
389
398
  return this.runWithRetry(() => {
390
- const stmt = this.db.prepare(
391
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
392
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
393
- );
394
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
395
- let id = nextId;
396
- for (const s of symbols) {
397
- stmt.run(
398
- id,
399
- s.lang,
400
- s.kind,
401
- s.name,
402
- s.file,
403
- s.line,
404
- s.col,
405
- s.signature,
406
- s.docComment,
407
- s.scope,
408
- s.text,
409
- s.file
399
+ this.db.exec("BEGIN IMMEDIATE");
400
+ try {
401
+ const maxRows = this.db.prepare("SELECT MAX(id) AS m FROM symbols").all();
402
+ let nextId = (maxRows[0]?.m ?? 0) + 1;
403
+ const stmt = this.db.prepare(
404
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
405
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
410
406
  );
411
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
412
- id++;
407
+ const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
408
+ const result = [];
409
+ for (const s of symbols) {
410
+ const id = nextId++;
411
+ stmt.run(
412
+ id,
413
+ s.lang,
414
+ s.kind,
415
+ s.name,
416
+ s.file,
417
+ s.line,
418
+ s.col,
419
+ s.signature,
420
+ s.docComment,
421
+ s.scope,
422
+ s.text,
423
+ s.file
424
+ );
425
+ ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
426
+ result.push({ ...s, id });
427
+ }
428
+ this.db.exec("COMMIT");
429
+ return result;
430
+ } catch (err) {
431
+ this.db.exec("ROLLBACK");
432
+ throw err;
413
433
  }
414
- return id;
415
434
  });
416
435
  }
417
436
  deleteSymbolsForFile(file) {
@@ -965,10 +984,10 @@ function detectLang(file) {
965
984
  if (idx < 0) return null;
966
985
  return extToLang(file.slice(idx));
967
986
  }
968
- function parseSymbols2(opts) {
987
+ async function parseSymbols2(opts) {
969
988
  const { file, content, lang } = opts;
970
989
  try {
971
- return syncGoParse(file, content, lang);
990
+ return await syncGoParse(file, content, lang);
972
991
  } catch {
973
992
  return { file, lang, symbols: [], mtimeMs: Date.now() };
974
993
  }
@@ -1205,19 +1224,34 @@ func formatType(t ast.Expr) string {
1205
1224
  }
1206
1225
  }
1207
1226
  `;
1208
- function syncGoParse(filePath, content, lang) {
1227
+ async function syncGoParse(filePath, content, lang) {
1209
1228
  const tmpDir = path4.join(os.tmpdir(), "ws-go-parse");
1210
1229
  try {
1211
- mkdirSync(tmpDir, { recursive: true });
1230
+ await fs6.mkdir(tmpDir, { recursive: true });
1212
1231
  const scriptPath = path4.join(tmpDir, "parse.go");
1213
- writeFileSync(scriptPath, GO_PARSE_SCRIPT, "utf8");
1214
- const stdout = execFileSync("go", ["run", scriptPath], {
1215
- input: content,
1216
- timeout: 15e3,
1217
- encoding: "utf8",
1232
+ await fs6.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
1233
+ const proc = spawn("go", ["run", scriptPath], {
1234
+ stdio: ["pipe", "pipe", "pipe"],
1218
1235
  windowsHide: true
1219
1236
  });
1220
- if (!stdout.trim()) {
1237
+ let stdout = "";
1238
+ proc.stdout?.on("data", (chunk) => {
1239
+ stdout += chunk.toString();
1240
+ });
1241
+ proc.stdin?.write(content);
1242
+ proc.stdin?.end();
1243
+ const { code } = await Promise.race([
1244
+ new Promise((resolve2) => {
1245
+ proc.on("close", (c) => resolve2({ code: c }));
1246
+ }),
1247
+ new Promise(
1248
+ (_, reject) => setTimeout(() => {
1249
+ proc.kill("SIGKILL");
1250
+ reject(new Error("timeout"));
1251
+ }, 15e3)
1252
+ )
1253
+ ]).catch(() => ({ code: -1 }));
1254
+ if (code !== 0 || !stdout.trim()) {
1221
1255
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1222
1256
  }
1223
1257
  const raw = JSON.parse(stdout.trim());
@@ -1239,10 +1273,10 @@ function syncGoParse(filePath, content, lang) {
1239
1273
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1240
1274
  }
1241
1275
  }
1242
- function parseSymbols3(opts) {
1276
+ async function parseSymbols3(opts) {
1243
1277
  const { file, lang } = opts;
1244
1278
  try {
1245
- return syncPyParse(file, lang);
1279
+ return await syncPyParse(file, lang);
1246
1280
  } catch {
1247
1281
  return { file, lang, symbols: [], mtimeMs: Date.now() };
1248
1282
  }
@@ -1451,18 +1485,32 @@ visitor.visit(tree)
1451
1485
 
1452
1486
  print(json.dumps([s.to_dict() for s in syms]))
1453
1487
  `;
1454
- function syncPyParse(filePath, lang) {
1488
+ async function syncPyParse(filePath, lang) {
1455
1489
  try {
1456
1490
  const tmpDir = path4.join(os.tmpdir(), "ws-py-parse");
1457
- mkdirSync(tmpDir, { recursive: true });
1491
+ await fs6.mkdir(tmpDir, { recursive: true });
1458
1492
  const scriptPath = path4.join(tmpDir, "parse.py");
1459
- writeFileSync(scriptPath, PY_PARSE_SCRIPT, "utf8");
1460
- const stdout = execFileSync("python", [scriptPath, filePath], {
1461
- timeout: 15e3,
1462
- encoding: "utf8",
1493
+ await fs6.writeFile(scriptPath, PY_PARSE_SCRIPT, "utf8");
1494
+ const proc = spawn("python", [scriptPath, filePath], {
1495
+ stdio: ["pipe", "pipe", "pipe"],
1463
1496
  windowsHide: true
1464
1497
  });
1465
- if (!stdout.trim()) {
1498
+ let stdout = "";
1499
+ proc.stdout?.on("data", (chunk) => {
1500
+ stdout += chunk.toString();
1501
+ });
1502
+ const { code } = await Promise.race([
1503
+ new Promise((resolve2) => {
1504
+ proc.on("close", (c) => resolve2({ code: c }));
1505
+ }),
1506
+ new Promise(
1507
+ (_, reject) => setTimeout(() => {
1508
+ proc.kill("SIGKILL");
1509
+ reject(new Error("timeout"));
1510
+ }, 15e3)
1511
+ )
1512
+ ]).catch(() => ({ code: -1 }));
1513
+ if (code !== 0 || !stdout.trim()) {
1466
1514
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1467
1515
  }
1468
1516
  const raw = JSON.parse(stdout.trim());
@@ -1484,11 +1532,11 @@ function syncPyParse(filePath, lang) {
1484
1532
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
1485
1533
  }
1486
1534
  }
1487
- function parseSymbols4(opts) {
1535
+ async function parseSymbols4(opts) {
1488
1536
  const { file, content, lang } = opts;
1489
1537
  const nativeAvailable = checkNativeParser();
1490
1538
  if (nativeAvailable) {
1491
- const result = tryNativeParse(file, content);
1539
+ const result = await tryNativeParse(file, content);
1492
1540
  if (result) return result;
1493
1541
  }
1494
1542
  return regexParse({ file, content, lang });
@@ -1518,25 +1566,34 @@ function checkNativeParser() {
1518
1566
  return false;
1519
1567
  }
1520
1568
  }
1521
- function tryNativeParse(file, content) {
1569
+ async function tryNativeParse(file, content) {
1522
1570
  try {
1523
1571
  const toolsDir = path4.join(process.cwd(), "tools");
1524
1572
  const crateDir = path4.join(toolsDir, "syn-parser");
1525
1573
  const tmpFile = path4.join(crateDir, "src", "input.rs");
1526
- writeFileSync(tmpFile, content, "utf8");
1527
- const result = spawnSync(
1528
- "cargo",
1529
- ["run", "--manifest-path", path4.join(toolsDir, "Cargo.toml")],
1530
- {
1531
- cwd: process.cwd(),
1532
- encoding: "utf8",
1533
- timeout: 15e3,
1534
- stdio: ["pipe", "pipe", "pipe"],
1535
- windowsHide: true
1536
- }
1537
- );
1538
- if (result.status === 0 && result.stdout) {
1539
- const symbols = JSON.parse(result.stdout);
1574
+ await fs6.writeFile(tmpFile, content, "utf8");
1575
+ const proc = spawn("cargo", ["run", "--manifest-path", path4.join(toolsDir, "Cargo.toml")], {
1576
+ cwd: process.cwd(),
1577
+ stdio: ["pipe", "pipe", "pipe"],
1578
+ windowsHide: true
1579
+ });
1580
+ let stdout = "";
1581
+ proc.stdout?.on("data", (chunk) => {
1582
+ stdout += chunk.toString();
1583
+ });
1584
+ const { code } = await Promise.race([
1585
+ new Promise((resolve2) => {
1586
+ proc.on("close", (c) => resolve2({ code: c }));
1587
+ }),
1588
+ new Promise(
1589
+ (_, reject) => setTimeout(() => {
1590
+ proc.kill("SIGKILL");
1591
+ reject(new Error("timeout"));
1592
+ }, 15e3)
1593
+ )
1594
+ ]).catch(() => ({ code: -1 }));
1595
+ if (code === 0 && stdout.trim()) {
1596
+ const symbols = JSON.parse(stdout.trim());
1540
1597
  return {
1541
1598
  file,
1542
1599
  lang: "rs",
@@ -2005,7 +2062,7 @@ function compileGitignore(lines) {
2005
2062
  async function loadGitignoreMatcher(projectRoot) {
2006
2063
  let lines = [];
2007
2064
  try {
2008
- const raw = await fs3.readFile(path4.join(projectRoot, ".gitignore"), "utf8");
2065
+ const raw = await fs6.readFile(path4.join(projectRoot, ".gitignore"), "utf8");
2009
2066
  lines = raw.split("\n");
2010
2067
  } catch {
2011
2068
  }
@@ -2063,7 +2120,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
2063
2120
  }
2064
2121
  let entries;
2065
2122
  try {
2066
- entries = await fs3.readdir(dir, { withFileTypes: true });
2123
+ entries = await fs6.readdir(dir, { withFileTypes: true });
2067
2124
  } catch {
2068
2125
  return;
2069
2126
  }
@@ -2161,7 +2218,7 @@ async function runIndexerWithStore(store, opts) {
2161
2218
  batchFiles.map(async (file) => {
2162
2219
  let stat2;
2163
2220
  try {
2164
- stat2 = await fs3.stat(file, statOpts);
2221
+ stat2 = await fs6.stat(file, statOpts);
2165
2222
  } catch (e) {
2166
2223
  if (isAbortError(e)) throw e;
2167
2224
  return { file, stat: null, lang: "", parsed: null, error: `stat error: ${e instanceof Error ? e.message : String(e)}` };
@@ -2175,7 +2232,7 @@ async function runIndexerWithStore(store, opts) {
2175
2232
  }
2176
2233
  let content;
2177
2234
  try {
2178
- content = await fs3.readFile(file, { encoding: "utf8", signal });
2235
+ content = await fs6.readFile(file, { encoding: "utf8", signal });
2179
2236
  } catch (e) {
2180
2237
  if (isAbortError(e)) throw e;
2181
2238
  return { file, stat: stat2, lang, parsed: null, error: `read error: ${e instanceof Error ? e.message : String(e)}` };
@@ -2225,9 +2282,7 @@ async function runIndexerWithStore(store, opts) {
2225
2282
  filesIndexed++;
2226
2283
  continue;
2227
2284
  }
2228
- const nextId = store.getMaxSymbolId() + 1;
2229
- const symbolsWithIds = parsed.symbols.map((s, i) => ({ ...s, id: nextId + i }));
2230
- store.insertSymbols(symbolsWithIds, nextId);
2285
+ const symbolsWithIds = store.insertSymbols(parsed.symbols);
2231
2286
  const count = symbolsWithIds.length;
2232
2287
  symbolsIndexed += count;
2233
2288
  langStats[lang] = (langStats[lang] ?? 0) + count;
@@ -2256,7 +2311,7 @@ async function runIndexerWithStore(store, opts) {
2256
2311
  }
2257
2312
  for (const [file_] of existingMeta) {
2258
2313
  try {
2259
- await fs3.stat(file_);
2314
+ await fs6.stat(file_);
2260
2315
  } catch {
2261
2316
  store.deleteFile(file_);
2262
2317
  }