@wrongstack/tools 0.293.0 → 0.295.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/dist/_env.d.ts +1 -1
  2. package/dist/_env.d.ts.map +1 -1
  3. package/dist/_fetch-guard.d.ts.map +1 -1
  4. package/dist/_spawn-stream.d.ts +1 -1
  5. package/dist/_spawn-stream.d.ts.map +1 -1
  6. package/dist/_util.d.ts +1 -1
  7. package/dist/_util.d.ts.map +1 -1
  8. package/dist/audit.d.ts +1 -1
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +81 -16
  11. package/dist/audit.js.map +2 -2
  12. package/dist/auto-proceed-loop-guard.d.ts +31 -0
  13. package/dist/auto-proceed-loop-guard.d.ts.map +1 -1
  14. package/dist/auto-proceed-loop-guard.js +24 -1
  15. package/dist/auto-proceed-loop-guard.js.map +2 -2
  16. package/dist/bash.d.ts +1 -1
  17. package/dist/bash.d.ts.map +1 -1
  18. package/dist/bash.js +105 -75
  19. package/dist/bash.js.map +3 -3
  20. package/dist/batch-tool-use.d.ts +1 -1
  21. package/dist/batch-tool-use.d.ts.map +1 -1
  22. package/dist/batch-tool-use.js +1 -1
  23. package/dist/batch-tool-use.js.map +1 -1
  24. package/dist/browser/index.js +10 -1
  25. package/dist/browser/index.js.map +2 -2
  26. package/dist/browser/manager.d.ts +2 -0
  27. package/dist/browser/manager.d.ts.map +1 -1
  28. package/dist/browser/tools.d.ts +1 -1
  29. package/dist/browser/tools.d.ts.map +1 -1
  30. package/dist/builtin.d.ts +1 -1
  31. package/dist/builtin.d.ts.map +1 -1
  32. package/dist/builtin.js +1650 -640
  33. package/dist/builtin.js.map +4 -4
  34. package/dist/codebase-index/background-indexer.d.ts +2 -2
  35. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  36. package/dist/codebase-index/bm25.d.ts +3 -1
  37. package/dist/codebase-index/bm25.d.ts.map +1 -1
  38. package/dist/codebase-index/codebase-index-tool.d.ts +1 -1
  39. package/dist/codebase-index/codebase-index-tool.d.ts.map +1 -1
  40. package/dist/codebase-index/codebase-search-tool.d.ts +1 -1
  41. package/dist/codebase-index/codebase-search-tool.d.ts.map +1 -1
  42. package/dist/codebase-index/codebase-stats-tool.d.ts +1 -1
  43. package/dist/codebase-index/codebase-stats-tool.d.ts.map +1 -1
  44. package/dist/codebase-index/generic-parser.d.ts +34 -0
  45. package/dist/codebase-index/generic-parser.d.ts.map +1 -0
  46. package/dist/codebase-index/go-parser.d.ts +1 -1
  47. package/dist/codebase-index/go-parser.d.ts.map +1 -1
  48. package/dist/codebase-index/index.d.ts +4 -2
  49. package/dist/codebase-index/index.d.ts.map +1 -1
  50. package/dist/codebase-index/index.js +1068 -372
  51. package/dist/codebase-index/index.js.map +4 -4
  52. package/dist/codebase-index/indexer.d.ts +6 -1
  53. package/dist/codebase-index/indexer.d.ts.map +1 -1
  54. package/dist/codebase-index/json-parser.d.ts +1 -1
  55. package/dist/codebase-index/languages.d.ts +25 -0
  56. package/dist/codebase-index/languages.d.ts.map +1 -0
  57. package/dist/codebase-index/py-parser.d.ts +9 -1
  58. package/dist/codebase-index/py-parser.d.ts.map +1 -1
  59. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  60. package/dist/codebase-index/rs-parser.d.ts +1 -1
  61. package/dist/codebase-index/rs-parser.d.ts.map +1 -1
  62. package/dist/codebase-index/schema.d.ts +9 -2
  63. package/dist/codebase-index/schema.d.ts.map +1 -1
  64. package/dist/codebase-index/spawn-gate.d.ts +15 -0
  65. package/dist/codebase-index/spawn-gate.d.ts.map +1 -0
  66. package/dist/codebase-index/ts-parser.d.ts +2 -2
  67. package/dist/codebase-index/ts-parser.d.ts.map +1 -1
  68. package/dist/codebase-index/worker.js +993 -348
  69. package/dist/codebase-index/worker.js.map +4 -4
  70. package/dist/codebase-index/writer.d.ts +83 -4
  71. package/dist/codebase-index/writer.d.ts.map +1 -1
  72. package/dist/codebase-index/yaml-parser.d.ts +1 -1
  73. package/dist/design.d.ts +5 -2
  74. package/dist/design.d.ts.map +1 -1
  75. package/dist/design.js +36 -3
  76. package/dist/design.js.map +2 -2
  77. package/dist/diff.d.ts +1 -1
  78. package/dist/diff.d.ts.map +1 -1
  79. package/dist/diff.js +2 -2
  80. package/dist/diff.js.map +1 -1
  81. package/dist/document.d.ts +1 -1
  82. package/dist/document.d.ts.map +1 -1
  83. package/dist/document.js +1 -1
  84. package/dist/document.js.map +1 -1
  85. package/dist/e2e.d.ts +1 -1
  86. package/dist/e2e.d.ts.map +1 -1
  87. package/dist/e2e.js +1 -1
  88. package/dist/e2e.js.map +1 -1
  89. package/dist/edit.d.ts +1 -1
  90. package/dist/edit.d.ts.map +1 -1
  91. package/dist/edit.js +3 -3
  92. package/dist/edit.js.map +2 -2
  93. package/dist/exec.d.ts +1 -1
  94. package/dist/exec.d.ts.map +1 -1
  95. package/dist/exec.js +99 -44
  96. package/dist/exec.js.map +3 -3
  97. package/dist/fetch.d.ts +1 -1
  98. package/dist/fetch.d.ts.map +1 -1
  99. package/dist/fetch.js +4 -5
  100. package/dist/fetch.js.map +2 -2
  101. package/dist/format.d.ts +1 -1
  102. package/dist/format.d.ts.map +1 -1
  103. package/dist/format.js +81 -16
  104. package/dist/format.js.map +2 -2
  105. package/dist/git.d.ts +1 -1
  106. package/dist/git.d.ts.map +1 -1
  107. package/dist/git.js +3 -2
  108. package/dist/git.js.map +2 -2
  109. package/dist/glob.d.ts +1 -1
  110. package/dist/glob.d.ts.map +1 -1
  111. package/dist/glob.js +81 -31
  112. package/dist/glob.js.map +4 -4
  113. package/dist/grep.d.ts +1 -1
  114. package/dist/grep.d.ts.map +1 -1
  115. package/dist/grep.js +90 -22
  116. package/dist/grep.js.map +4 -4
  117. package/dist/index.d.ts +5 -4
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +1895 -771
  120. package/dist/index.js.map +4 -4
  121. package/dist/install.d.ts +1 -1
  122. package/dist/install.d.ts.map +1 -1
  123. package/dist/install.js +85 -17
  124. package/dist/install.js.map +2 -2
  125. package/dist/json.d.ts +1 -1
  126. package/dist/json.d.ts.map +1 -1
  127. package/dist/json.js +2 -2
  128. package/dist/json.js.map +2 -2
  129. package/dist/kanban-evidence-bridge.d.ts +21 -0
  130. package/dist/kanban-evidence-bridge.d.ts.map +1 -0
  131. package/dist/kanban.d.ts +20 -3
  132. package/dist/kanban.d.ts.map +1 -1
  133. package/dist/kanban.js +256 -32
  134. package/dist/kanban.js.map +3 -3
  135. package/dist/languages/execute-tool.d.ts +1 -1
  136. package/dist/languages/execute-tool.d.ts.map +1 -1
  137. package/dist/languages/execute.d.ts +1 -1
  138. package/dist/languages/execute.d.ts.map +1 -1
  139. package/dist/languages/index.d.ts +1 -1
  140. package/dist/languages/index.d.ts.map +1 -1
  141. package/dist/languages/index.js +99 -17
  142. package/dist/languages/index.js.map +2 -2
  143. package/dist/languages/package-tool.d.ts +3 -4
  144. package/dist/languages/package-tool.d.ts.map +1 -1
  145. package/dist/languages/plan.d.ts.map +1 -1
  146. package/dist/languages/profiles/additional.d.ts.map +1 -1
  147. package/dist/languages/profiles/primary.d.ts.map +1 -1
  148. package/dist/languages/tool.d.ts +1 -1
  149. package/dist/languages/tool.d.ts.map +1 -1
  150. package/dist/lint.d.ts +1 -1
  151. package/dist/lint.d.ts.map +1 -1
  152. package/dist/lint.js +81 -16
  153. package/dist/lint.js.map +2 -2
  154. package/dist/logs.d.ts +1 -1
  155. package/dist/logs.d.ts.map +1 -1
  156. package/dist/logs.js +2 -2
  157. package/dist/logs.js.map +1 -1
  158. package/dist/memory.d.ts +1 -1
  159. package/dist/memory.d.ts.map +1 -1
  160. package/dist/memory.js +1 -1
  161. package/dist/memory.js.map +1 -1
  162. package/dist/mode.d.ts +1 -1
  163. package/dist/mode.d.ts.map +1 -1
  164. package/dist/mode.js.map +1 -1
  165. package/dist/outdated.d.ts +1 -1
  166. package/dist/outdated.d.ts.map +1 -1
  167. package/dist/outdated.js +101 -19
  168. package/dist/outdated.js.map +2 -2
  169. package/dist/pack.d.ts +1 -1
  170. package/dist/pack.js +1650 -640
  171. package/dist/pack.js.map +4 -4
  172. package/dist/patch.d.ts +1 -1
  173. package/dist/patch.d.ts.map +1 -1
  174. package/dist/patch.js +2 -2
  175. package/dist/patch.js.map +1 -1
  176. package/dist/plan.d.ts +1 -1
  177. package/dist/plan.d.ts.map +1 -1
  178. package/dist/plan.js +7 -7
  179. package/dist/plan.js.map +2 -2
  180. package/dist/process-guardian.d.ts +7 -0
  181. package/dist/process-guardian.d.ts.map +1 -1
  182. package/dist/process-registry-persistent.d.ts +5 -0
  183. package/dist/process-registry-persistent.d.ts.map +1 -1
  184. package/dist/process-registry.d.ts +9 -1
  185. package/dist/process-registry.d.ts.map +1 -1
  186. package/dist/process-registry.js +18 -3
  187. package/dist/process-registry.js.map +2 -2
  188. package/dist/ps-slash.js +93 -38
  189. package/dist/ps-slash.js.map +2 -2
  190. package/dist/read.d.ts +1 -1
  191. package/dist/read.d.ts.map +1 -1
  192. package/dist/read.js +3 -2
  193. package/dist/read.js.map +2 -2
  194. package/dist/replace.d.ts +1 -1
  195. package/dist/replace.d.ts.map +1 -1
  196. package/dist/replace.js +4 -4
  197. package/dist/replace.js.map +2 -2
  198. package/dist/scaffold.d.ts +1 -1
  199. package/dist/scaffold.d.ts.map +1 -1
  200. package/dist/scaffold.js +2 -2
  201. package/dist/scaffold.js.map +1 -1
  202. package/dist/search.d.ts +1 -1
  203. package/dist/search.d.ts.map +1 -1
  204. package/dist/search.js +4 -4
  205. package/dist/search.js.map +2 -2
  206. package/dist/session-kanban.d.ts +4 -1
  207. package/dist/session-kanban.d.ts.map +1 -1
  208. package/dist/session-kanban.js +23 -15
  209. package/dist/session-kanban.js.map +2 -2
  210. package/dist/set-working-dir.d.ts +1 -1
  211. package/dist/set-working-dir.d.ts.map +1 -1
  212. package/dist/skill.d.ts +1 -1
  213. package/dist/skill.d.ts.map +1 -1
  214. package/dist/skill.js +2 -5
  215. package/dist/skill.js.map +2 -2
  216. package/dist/spawn-background.d.ts.map +1 -1
  217. package/dist/task.d.ts +3 -2
  218. package/dist/task.d.ts.map +1 -1
  219. package/dist/task.js +6 -8
  220. package/dist/task.js.map +2 -2
  221. package/dist/test.d.ts +1 -1
  222. package/dist/test.d.ts.map +1 -1
  223. package/dist/test.js +81 -16
  224. package/dist/test.js.map +2 -2
  225. package/dist/todo.d.ts +2 -1
  226. package/dist/todo.d.ts.map +1 -1
  227. package/dist/todo.js +4 -4
  228. package/dist/todo.js.map +2 -2
  229. package/dist/tool-help.d.ts +1 -1
  230. package/dist/tool-help.d.ts.map +1 -1
  231. package/dist/tool-help.js.map +1 -1
  232. package/dist/tool-search.d.ts +1 -1
  233. package/dist/tool-search.d.ts.map +1 -1
  234. package/dist/tool-search.js.map +1 -1
  235. package/dist/tool-tier.d.ts +16 -0
  236. package/dist/tool-tier.d.ts.map +1 -0
  237. package/dist/tool-tier.js +22402 -0
  238. package/dist/tool-tier.js.map +7 -0
  239. package/dist/tool-use.d.ts +1 -1
  240. package/dist/tool-use.d.ts.map +1 -1
  241. package/dist/tool-use.js.map +1 -1
  242. package/dist/tree.d.ts +1 -1
  243. package/dist/tree.d.ts.map +1 -1
  244. package/dist/tree.js +3 -15
  245. package/dist/tree.js.map +2 -2
  246. package/dist/typecheck.d.ts +1 -1
  247. package/dist/typecheck.d.ts.map +1 -1
  248. package/dist/typecheck.js +81 -16
  249. package/dist/typecheck.js.map +2 -2
  250. package/dist/win32.d.ts +10 -0
  251. package/dist/win32.d.ts.map +1 -0
  252. package/dist/win32.js +67 -0
  253. package/dist/win32.js.map +7 -0
  254. package/dist/write.d.ts +1 -1
  255. package/dist/write.d.ts.map +1 -1
  256. package/dist/write.js +3 -2
  257. package/dist/write.js.map +2 -2
  258. package/package.json +11 -3
package/dist/pack.js CHANGED
@@ -17,7 +17,7 @@ var __export = (target, all) => {
17
17
  import { createWriteStream, mkdirSync } from "node:fs";
18
18
  import * as fsp from "node:fs/promises";
19
19
  import * as path from "node:path";
20
- import { wstackGlobalRoot } from "@wrongstack/core";
20
+ import { wstackGlobalRoot } from "@wrongstack/core/utils";
21
21
  function toolOutputDir() {
22
22
  return path.join(wstackGlobalRoot(), "tool-output");
23
23
  }
@@ -325,7 +325,7 @@ var init_circuit_breaker = __esm({
325
325
  });
326
326
 
327
327
  // src/_redact-command.ts
328
- import { expectDefined } from "@wrongstack/core";
328
+ import { expectDefined } from "@wrongstack/core/utils";
329
329
  function redactCommand(cmd) {
330
330
  let result = cmd;
331
331
  for (const pattern of SENSITIVE_FLAG_PATTERNS) {
@@ -439,7 +439,12 @@ var init_process_registry = __esm({
439
439
  this.breaker.setEnabled(false);
440
440
  }
441
441
  register(info) {
442
- this.processes.set(info.pid, { ...info, killed: false, protected: info.protected ?? false });
442
+ this.processes.set(info.pid, {
443
+ ...info,
444
+ killed: false,
445
+ protected: info.protected ?? false,
446
+ background: info.background ?? false
447
+ });
443
448
  }
444
449
  _isSafeSignalPid(pid) {
445
450
  return Number.isInteger(pid) && pid > 1 && pid !== process.pid && pid !== process.ppid;
@@ -492,12 +497,21 @@ var init_process_registry = __esm({
492
497
  }
493
498
  return n;
494
499
  }
500
+ /** Count of active jobs explicitly launched in background mode. */
501
+ get activeBackgroundCount() {
502
+ let n = 0;
503
+ for (const p of this.processes.values()) {
504
+ if (p.background && !p.killed) n++;
505
+ }
506
+ return n;
507
+ }
495
508
  /**
496
509
  * Combined stats for observability — used by /ps and the TUI status bar.
497
510
  */
498
511
  stats() {
499
512
  return {
500
513
  activeCount: this.activeCount,
514
+ backgroundCount: this.activeBackgroundCount,
501
515
  totalCount: this.processes.size,
502
516
  breaker: this.breaker.snapshot()
503
517
  };
@@ -598,7 +612,7 @@ var init_process_registry = __esm({
598
612
  this.autoKillTimer = setTimeout(() => {
599
613
  this.autoKillTimer = null;
600
614
  this.autoKillArmedAt = null;
601
- this.killAll({ force: false });
615
+ this.killAll({ force: false, preserveBackground: true });
602
616
  this.breaker.forceReset();
603
617
  this._emitBreakerCountdown();
604
618
  }, this.autoKillResetMs);
@@ -638,6 +652,7 @@ var init_process_registry = __esm({
638
652
  if (!p) return false;
639
653
  if (p.killed) return true;
640
654
  if (p.protected) return false;
655
+ if (opts.preserveBackground && p.background) return false;
641
656
  const { force = false, graceMs = DEFAULT_GRACE_MS } = opts;
642
657
  const isWin5 = os.platform() === "win32";
643
658
  if (isWin5) {
@@ -807,11 +822,11 @@ var init_win32_resolve = __esm({
807
822
  // src/_spawn-stream.ts
808
823
  import { spawn as spawn2 } from "node:child_process";
809
824
  import {
810
- buildChildEnv,
811
825
  emitProcessCompleted,
812
826
  emitProcessOutput,
813
827
  emitProcessStarted
814
- } from "@wrongstack/core";
828
+ } from "@wrongstack/core/observability";
829
+ import { buildChildEnv } from "@wrongstack/core/utils";
815
830
  async function* spawnStream(opts) {
816
831
  const max = opts.maxBytes ?? 2e5;
817
832
  const flushAt = opts.flushBytes ?? 4 * 1024;
@@ -1023,7 +1038,7 @@ var init_spawn_stream = __esm({
1023
1038
  import { createHash } from "node:crypto";
1024
1039
  import * as fsp2 from "node:fs/promises";
1025
1040
  import * as path3 from "node:path";
1026
- import * as Core from "@wrongstack/core";
1041
+ import * as Core from "@wrongstack/core/utils";
1027
1042
  function sha256hex(content) {
1028
1043
  return createHash("sha256").update(content, "utf8").digest("hex");
1029
1044
  }
@@ -1067,16 +1082,16 @@ async function assertRealInsideRoot(absPath, ctx) {
1067
1082
  const realRoots = await Promise.all(
1068
1083
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1069
1084
  );
1070
- let probe = absPath;
1085
+ let probe2 = absPath;
1071
1086
  for (; ; ) {
1072
1087
  let real;
1073
1088
  try {
1074
- real = await fsp2.realpath(probe);
1089
+ real = await fsp2.realpath(probe2);
1075
1090
  } catch (err) {
1076
1091
  if (err.code === "ENOENT") {
1077
- const parent = path3.dirname(probe);
1078
- if (parent === probe) return;
1079
- probe = parent;
1092
+ const parent = path3.dirname(probe2);
1093
+ if (parent === probe2) return;
1094
+ probe2 = parent;
1080
1095
  continue;
1081
1096
  }
1082
1097
  throw err;
@@ -1264,7 +1279,20 @@ function pythonProfile() {
1264
1279
  ]),
1265
1280
  ignoredDirectories: IGNORES,
1266
1281
  packageManagers: Object.freeze(["pip", "poetry", "pipenv", "uv"]),
1267
- executables: Object.freeze(["python", "python3", "ruff", "black", "pytest", "mypy", "pyright"]),
1282
+ executables: Object.freeze([
1283
+ "python",
1284
+ "python3",
1285
+ "pip",
1286
+ "pip3",
1287
+ "poetry",
1288
+ "pipenv",
1289
+ "uv",
1290
+ "ruff",
1291
+ "black",
1292
+ "pytest",
1293
+ "mypy",
1294
+ "pyright"
1295
+ ]),
1268
1296
  operations: Object.freeze({
1269
1297
  syntax: async (ctx) => {
1270
1298
  const py = ctx.options.target ? "python3" : "python3";
@@ -1324,6 +1352,14 @@ function pythonProfile() {
1324
1352
  network: true
1325
1353
  });
1326
1354
  },
1355
+ "package-remove": async (ctx) => {
1356
+ const names = packageNames(ctx);
1357
+ return names.length === 0 ? unavailable(ctx, "package-remove", "At least one package name is required.") : processPlan(ctx, "package-remove", "pip", ["uninstall", "--yes", ...names], {
1358
+ parser: "package-text",
1359
+ reason: "Remove validated Python packages.",
1360
+ mutating: true
1361
+ });
1362
+ },
1327
1363
  "package-audit": async (ctx) => processPlan(ctx, "package-audit", "pip", ["audit"], {
1328
1364
  parser: "pip-audit",
1329
1365
  reason: "Audit Python dependencies for vulnerabilities.",
@@ -1935,6 +1971,19 @@ function typescriptProfile() {
1935
1971
  network: true
1936
1972
  });
1937
1973
  },
1974
+ "package-remove": async (ctx) => {
1975
+ const names = packageNames(ctx);
1976
+ if (names.length === 0)
1977
+ return unavailable(ctx, "package-remove", "At least one package name is required.");
1978
+ const manager = ctx.workspace.packageManager ?? "npm";
1979
+ const args = manager === "npm" ? ["uninstall", "--ignore-scripts", ...names] : ["remove", "--ignore-scripts", ...names];
1980
+ return processPlan(ctx, "package-remove", manager, args, {
1981
+ parser: "package-text",
1982
+ reason: `Remove validated packages with ${manager} and lifecycle scripts disabled.`,
1983
+ mutating: true,
1984
+ network: true
1985
+ });
1986
+ },
1938
1987
  "package-audit": async (ctx) => {
1939
1988
  const manager = ctx.workspace.packageManager ?? "npm";
1940
1989
  return processPlan(ctx, "package-audit", manager, ["audit", "--json"], {
@@ -2387,13 +2436,28 @@ var init_primary = __esm({
2387
2436
  }),
2388
2437
  "package-add": async (ctx) => {
2389
2438
  const names = packageNames(ctx);
2390
- return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : processPlan(ctx, "package-add", "dotnet", ["add", "package", ...names], {
2391
- parser: "dotnet-package",
2392
- reason: "Add validated NuGet packages.",
2393
- mutating: true,
2394
- network: true,
2395
- executesProjectCode: true
2396
- });
2439
+ const [spec] = names;
2440
+ const versionAt = spec?.lastIndexOf("@") ?? -1;
2441
+ const packageName = versionAt > 0 ? spec?.slice(0, versionAt) : spec;
2442
+ const packageVersion = versionAt > 0 ? spec?.slice(versionAt + 1) : void 0;
2443
+ return names.length === 0 ? unavailable(ctx, "package-add", "At least one NuGet package is required.") : names.length > 1 ? unavailable(ctx, "package-add", "NuGet package changes run one package at a time.") : processPlan(
2444
+ ctx,
2445
+ "package-add",
2446
+ "dotnet",
2447
+ [
2448
+ "add",
2449
+ "package",
2450
+ packageName ?? "",
2451
+ ...packageVersion ? ["--version", packageVersion] : []
2452
+ ],
2453
+ {
2454
+ parser: "dotnet-package",
2455
+ reason: "Add validated NuGet packages.",
2456
+ mutating: true,
2457
+ network: true,
2458
+ executesProjectCode: true
2459
+ }
2460
+ );
2397
2461
  },
2398
2462
  "package-remove": async (ctx) => {
2399
2463
  const names = packageNames(ctx);
@@ -2605,8 +2669,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2605
2669
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2606
2670
  }
2607
2671
  }
2608
- function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2609
- const lower = basename10.toLowerCase();
2672
+ function collectFileEvidence(directory, fullPath, basename11, profiles, state) {
2673
+ const lower = basename11.toLowerCase();
2610
2674
  const extension = path4.extname(lower);
2611
2675
  for (const profile of profiles) {
2612
2676
  const detector = profile.detectors.find(
@@ -2617,7 +2681,7 @@ function collectFileEvidence(directory, fullPath, basename10, profiles, state) {
2617
2681
  candidate.evidence.push({
2618
2682
  kind: detector.kind,
2619
2683
  path: fullPath,
2620
- value: basename10,
2684
+ value: basename11,
2621
2685
  weight: detector.weight
2622
2686
  });
2623
2687
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3483,7 +3547,7 @@ function validateOperationOptions(operation, packages) {
3483
3547
  if (value.startsWith(".") || value.startsWith("/") || /^[A-Za-z]:/.test(value) || value.includes("://")) {
3484
3548
  throw new Error(`Package paths and URLs are not supported: "${value}"`);
3485
3549
  }
3486
- if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value)) {
3550
+ if (!PACKAGE_NAME_RE.test(value) && !COMPOSER_PACKAGE_RE.test(value) && !GO_MODULE_RE.test(value) && !PYTHON_PACKAGE_RE.test(value)) {
3487
3551
  throw new Error(`Invalid package identifier "${value}"`);
3488
3552
  }
3489
3553
  }
@@ -3505,7 +3569,7 @@ function isInside2(candidate, root) {
3505
3569
  const relative12 = path6.relative(path6.resolve(root), path6.resolve(candidate));
3506
3570
  return relative12 === "" || !relative12.startsWith("..") && !path6.isAbsolute(relative12);
3507
3571
  }
3508
- var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE;
3572
+ var MAX_ARGUMENTS, MAX_ARGUMENT_LENGTH, PACKAGE_NAME_RE, COMPOSER_PACKAGE_RE, GO_MODULE_RE, PYTHON_PACKAGE_RE;
3509
3573
  var init_plan = __esm({
3510
3574
  "src/languages/plan.ts"() {
3511
3575
  "use strict";
@@ -3516,6 +3580,7 @@ var init_plan = __esm({
3516
3580
  PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3517
3581
  COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3518
3582
  GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3583
+ PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3519
3584
  }
3520
3585
  });
3521
3586
 
@@ -3934,15 +3999,15 @@ async function changedPaths(before, after, beforeSizes, afterSizes) {
3934
3999
  const beforeSet = new Set(before);
3935
4000
  const afterSet = new Set(after);
3936
4001
  const changed = /* @__PURE__ */ new Set();
3937
- for (const path33 of after) {
3938
- if (!beforeSet.has(path33)) changed.add(path33);
4002
+ for (const path34 of after) {
4003
+ if (!beforeSet.has(path34)) changed.add(path34);
3939
4004
  }
3940
- for (const path33 of before) {
3941
- if (!afterSet.has(path33)) changed.add(path33);
4005
+ for (const path34 of before) {
4006
+ if (!afterSet.has(path34)) changed.add(path34);
3942
4007
  }
3943
4008
  if (beforeSizes && afterSizes) {
3944
- for (const path33 of after) {
3945
- if (beforeSizes.get(path33) !== afterSizes.get(path33)) changed.add(path33);
4009
+ for (const path34 of after) {
4010
+ if (beforeSizes.get(path34) !== afterSizes.get(path34)) changed.add(path34);
3946
4011
  }
3947
4012
  }
3948
4013
  return [...changed].sort();
@@ -4417,7 +4482,6 @@ var init_package_tool = __esm({
4417
4482
  const operationOptions2 = {
4418
4483
  ...input.names ? { packages: input.names } : {},
4419
4484
  ...input.scope ? { packageScope: input.scope } : {},
4420
- ...input.dryRun !== void 0 ? { allowScripts: input.dryRun } : { allowScripts: false },
4421
4485
  ...input.allowScripts !== void 0 ? { allowScripts: input.allowScripts } : {}
4422
4486
  };
4423
4487
  const planResult = await planLanguageOperation({
@@ -4446,6 +4510,24 @@ var init_package_tool = __esm({
4446
4510
  };
4447
4511
  return;
4448
4512
  }
4513
+ if (input.dryRun) {
4514
+ yield {
4515
+ type: "final",
4516
+ output: {
4517
+ status: "passed",
4518
+ language: planResult.workspace.language,
4519
+ workspace: planResult.workspace.root,
4520
+ operation: planResult.plan.operation,
4521
+ mutations: [],
4522
+ vulnerabilities: [],
4523
+ outdated: [],
4524
+ manifestsChanged: [],
4525
+ lockfilesChanged: [],
4526
+ output: `Dry run: ${planResult.plan.command ?? "internal"} ${planResult.plan.args.join(" ")}`.trim()
4527
+ }
4528
+ };
4529
+ return;
4530
+ }
4449
4531
  const runner = executePackagePlan({
4450
4532
  projectRoot: ctx.projectRoot,
4451
4533
  workspace: planResult.workspace,
@@ -4948,10 +5030,10 @@ import {
4948
5030
  emitProcessCompleted as emitProcessCompleted2,
4949
5031
  emitProcessOutput as emitProcessOutput2,
4950
5032
  emitProcessStarted as emitProcessStarted2
4951
- } from "@wrongstack/core";
5033
+ } from "@wrongstack/core/observability";
4952
5034
 
4953
5035
  // src/_env.ts
4954
- import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core";
5036
+ import { buildChildEnv as buildChildEnv2 } from "@wrongstack/core/utils";
4955
5037
 
4956
5038
  // src/bash.ts
4957
5039
  init_output_spool();
@@ -4965,8 +5047,9 @@ import * as os3 from "node:os";
4965
5047
  init_process_registry();
4966
5048
  import * as fs6 from "node:fs/promises";
4967
5049
  import * as os2 from "node:os";
5050
+ import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
4968
5051
  import * as path9 from "node:path";
4969
- var REGISTRY_FILE = ".wrongstack/process-registry.json";
5052
+ var REGISTRY_FILE = "process-registry.json";
4970
5053
  function toErrorMessage(err) {
4971
5054
  return err instanceof Error ? err.message : String(err);
4972
5055
  }
@@ -4984,7 +5067,8 @@ function emitStructuredLog(level, event, message, error) {
4984
5067
  }
4985
5068
  var HEARTBEAT_INTERVAL_MS = 5e3;
4986
5069
  var STALE_THRESHOLD_MS = 3e4;
4987
- var LOCKFILE = ".wrongstack/.process-registry.lock";
5070
+ var LOCK_STALE_MS = 3e4;
5071
+ var LOCKFILE = ".process-registry.lock";
4988
5072
  function generateInstanceId() {
4989
5073
  const hostname2 = os2.hostname();
4990
5074
  const pid = process.pid;
@@ -5012,21 +5096,26 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
5012
5096
  try {
5013
5097
  const content = await fs6.readFile(lockfilePath, "utf-8");
5014
5098
  const parts = content.split(":");
5015
- const lockPidStr = parts[0] ?? "0";
5016
- const lockPid = parseInt(lockPidStr, 10);
5017
- if (process.platform !== "win32") {
5099
+ const lockPid = parseInt(parts[0] ?? "0", 10);
5100
+ const lockTs = Number(parts[parts.length - 1]);
5101
+ const staleByAge = Number.isFinite(lockTs) && Date.now() - lockTs > LOCK_STALE_MS;
5102
+ let holderDead = false;
5103
+ if (process.platform !== "win32" && Number.isFinite(lockPid) && lockPid > 0) {
5018
5104
  try {
5019
5105
  process.kill(lockPid, 0);
5020
5106
  } catch {
5021
- await fs6.unlink(lockfilePath);
5022
- continue;
5107
+ holderDead = true;
5023
5108
  }
5024
5109
  }
5025
- } catch {
5026
- try {
5027
- await fs6.unlink(lockfilePath);
5028
- } catch {
5110
+ if (holderDead || staleByAge) {
5111
+ await fs6.unlink(lockfilePath).catch(() => {
5112
+ });
5113
+ continue;
5029
5114
  }
5115
+ } catch {
5116
+ await fs6.unlink(lockfilePath).catch(() => {
5117
+ });
5118
+ continue;
5030
5119
  }
5031
5120
  await new Promise((r) => setTimeout(r, 100));
5032
5121
  continue;
@@ -5036,25 +5125,35 @@ async function acquireLock(lockfilePath, timeoutMs = 5e3) {
5036
5125
  }
5037
5126
  throw new Error(`Failed to acquire lock after ${timeoutMs}ms`);
5038
5127
  }
5128
+ function freshRegistryData() {
5129
+ return {
5130
+ version: 1,
5131
+ instances: /* @__PURE__ */ new Map(),
5132
+ protectedPatterns: ["wrongstack", "node"],
5133
+ lastCleanup: Date.now()
5134
+ };
5135
+ }
5039
5136
  async function readRegistryFile(filePath) {
5137
+ let content;
5040
5138
  try {
5041
- const content = await fs6.readFile(filePath, "utf-8");
5042
- const parsed = JSON.parse(content);
5043
- if (parsed.instances && Array.isArray(parsed.instances)) {
5044
- parsed.instances = new Map(parsed.instances);
5045
- }
5046
- return parsed;
5139
+ content = await fs6.readFile(filePath, "utf-8");
5047
5140
  } catch (err) {
5048
- if (isNodeError(err) && err.code === "ENOENT") {
5049
- return {
5050
- version: 1,
5051
- instances: /* @__PURE__ */ new Map(),
5052
- protectedPatterns: ["wrongstack", "node"],
5053
- lastCleanup: Date.now()
5054
- };
5055
- }
5141
+ if (isNodeError(err) && err.code === "ENOENT") return freshRegistryData();
5056
5142
  throw err;
5057
5143
  }
5144
+ try {
5145
+ const parsed = JSON.parse(content);
5146
+ if (!parsed || typeof parsed !== "object") return freshRegistryData();
5147
+ const base = freshRegistryData();
5148
+ return {
5149
+ version: 1,
5150
+ instances: Array.isArray(parsed.instances) ? new Map(parsed.instances) : base.instances,
5151
+ protectedPatterns: Array.isArray(parsed.protectedPatterns) ? parsed.protectedPatterns : base.protectedPatterns,
5152
+ lastCleanup: typeof parsed.lastCleanup === "number" ? parsed.lastCleanup : base.lastCleanup
5153
+ };
5154
+ } catch {
5155
+ return freshRegistryData();
5156
+ }
5058
5157
  }
5059
5158
  async function writeRegistryFile(filePath, data) {
5060
5159
  const tmpPath = `${filePath}.tmp.${process.pid}`;
@@ -5073,12 +5172,18 @@ var PersistentProcessRegistry = class {
5073
5172
  lockPath;
5074
5173
  baseRegistry;
5075
5174
  heartbeatInterval = null;
5175
+ cleanupInterval = null;
5176
+ heartbeatRunning = false;
5177
+ cleanupRunning = false;
5076
5178
  isShuttingDown = false;
5179
+ onProcessExit = () => {
5180
+ void this.syncToPersistent();
5181
+ };
5077
5182
  constructor(baseRegistry) {
5078
5183
  this.instanceId = generateInstanceId();
5079
- const homeDir = os2.homedir();
5080
- this.registryPath = path9.join(homeDir, REGISTRY_FILE);
5081
- this.lockPath = path9.join(homeDir, LOCKFILE);
5184
+ const globalRoot = wstackGlobalRoot3();
5185
+ this.registryPath = path9.join(globalRoot, REGISTRY_FILE);
5186
+ this.lockPath = path9.join(globalRoot, LOCKFILE);
5082
5187
  this.baseRegistry = baseRegistry ?? getProcessRegistry();
5083
5188
  this.ensureDirectory().catch((err) => {
5084
5189
  emitStructuredLog("warn", "process_registry.dir_create_failed", "PersistentProcessRegistry: failed to create .wrongstack directory", err);
@@ -5097,16 +5202,18 @@ var PersistentProcessRegistry = class {
5097
5202
  */
5098
5203
  start() {
5099
5204
  if (this.heartbeatInterval) return;
5100
- this.syncToPersistent();
5205
+ this.isShuttingDown = false;
5206
+ this.heartbeat();
5101
5207
  this.heartbeatInterval = setInterval(() => {
5102
5208
  this.heartbeat();
5103
5209
  }, HEARTBEAT_INTERVAL_MS);
5104
5210
  this.heartbeatInterval.unref?.();
5105
- setInterval(() => {
5106
- this.cleanupStaleEntries();
5107
- }, STALE_THRESHOLD_MS).unref?.();
5211
+ this.cleanupInterval = setInterval(() => {
5212
+ this.cleanup();
5213
+ }, STALE_THRESHOLD_MS);
5214
+ this.cleanupInterval.unref?.();
5108
5215
  this.registerMainProcess();
5109
- process.on("exit", () => this.syncToPersistent());
5216
+ process.on("exit", this.onProcessExit);
5110
5217
  }
5111
5218
  /**
5112
5219
  * Stop the heartbeat and clean up.
@@ -5117,7 +5224,12 @@ var PersistentProcessRegistry = class {
5117
5224
  clearInterval(this.heartbeatInterval);
5118
5225
  this.heartbeatInterval = null;
5119
5226
  }
5120
- this.syncToPersistent();
5227
+ if (this.cleanupInterval) {
5228
+ clearInterval(this.cleanupInterval);
5229
+ this.cleanupInterval = null;
5230
+ }
5231
+ process.off("exit", this.onProcessExit);
5232
+ void this.syncToPersistent();
5121
5233
  }
5122
5234
  /**
5123
5235
  * Register the main WrongStack process as protected.
@@ -5201,8 +5313,18 @@ var PersistentProcessRegistry = class {
5201
5313
  * Send heartbeat to mark all this instance's processes as alive.
5202
5314
  */
5203
5315
  heartbeat() {
5204
- if (this.isShuttingDown) return;
5205
- this.syncToPersistent();
5316
+ if (this.isShuttingDown || this.heartbeatRunning) return;
5317
+ this.heartbeatRunning = true;
5318
+ void this.syncToPersistent().finally(() => {
5319
+ this.heartbeatRunning = false;
5320
+ });
5321
+ }
5322
+ cleanup() {
5323
+ if (this.isShuttingDown || this.cleanupRunning) return;
5324
+ this.cleanupRunning = true;
5325
+ void this.cleanupStaleEntries().finally(() => {
5326
+ this.cleanupRunning = false;
5327
+ });
5206
5328
  }
5207
5329
  /**
5208
5330
  * Sync this instance's processes to the persistent registry.
@@ -5918,14 +6040,12 @@ var bashTool = {
5918
6040
  const detached = !isWin5;
5919
6041
  const startedAt = Date.now();
5920
6042
  if (input.background) {
5921
- let buf2 = "";
5922
- let truncated = false;
5923
6043
  const child2 = spawn3(shell, args, {
5924
6044
  cwd: ctx.projectRoot,
5925
6045
  env,
5926
6046
  // PowerShell takes the script on stdin (no argv quoting); cmd.exe
5927
6047
  // and POSIX shells ignore stdin when given the command inline.
5928
- stdio: [plan.useStdin ? "pipe" : "ignore", "pipe", "pipe"],
6048
+ stdio: [plan.useStdin ? "pipe" : "ignore", "ignore", "ignore"],
5929
6049
  // win32: CreateProcess IGNORES CREATE_NO_WINDOW (windowsHide) when
5930
6050
  // DETACHED_PROCESS (detached: true) is set, so the console-less
5931
6051
  // cmd.exe's grandchildren (node, dev servers) each allocate a fresh
@@ -5944,8 +6064,8 @@ var bashTool = {
5944
6064
  }
5945
6065
  }
5946
6066
  const pid2 = child2.pid;
5947
- let stdoutBytes2 = 0;
5948
- let stderrBytes2 = 0;
6067
+ const stdoutBytes2 = 0;
6068
+ const stderrBytes2 = 0;
5949
6069
  let telemetryCompleted2 = false;
5950
6070
  emitProcessStarted2({
5951
6071
  ...pid2 !== void 0 ? { pid: pid2 } : {},
@@ -5978,41 +6098,17 @@ var bashTool = {
5978
6098
  startedAt: Date.now(),
5979
6099
  sessionId: ctx.session?.id,
5980
6100
  child: child2,
5981
- processGroupLeader: detached && child2.pid === pid2
6101
+ processGroupLeader: detached && child2.pid === pid2,
6102
+ background: true
5982
6103
  });
5983
6104
  child2.on("close", () => registry.unregister(pid2));
5984
6105
  }
5985
- const onBgData = (chunk, stream) => {
5986
- if (stream === "stdout") stdoutBytes2 += chunk.byteLength;
5987
- else stderrBytes2 += chunk.byteLength;
5988
- emitProcessOutput2({ pid: pid2, stream, chunk });
5989
- if (truncated) return;
5990
- const remain = MAX_OUTPUT - buf2.length;
5991
- if (remain > 0) {
5992
- buf2 += chunk.toString().slice(0, remain);
5993
- }
5994
- if (buf2.length >= MAX_OUTPUT) {
5995
- truncated = true;
5996
- child2.stdout?.off("data", onBgStdout);
5997
- child2.stderr?.off("data", onBgStderr);
5998
- }
5999
- };
6000
- const onBgStdout = (chunk) => onBgData(chunk, "stdout");
6001
- const onBgStderr = (chunk) => onBgData(chunk, "stderr");
6002
- child2.stdout?.on("data", onBgStdout);
6003
- child2.stderr?.on("data", onBgStderr);
6004
- const cleanupBackground = () => {
6005
- child2.stdout?.off("data", onBgStdout);
6006
- child2.stderr?.off("data", onBgStderr);
6007
- };
6008
6106
  child2.on("error", () => {
6009
- cleanupBackground();
6010
6107
  if (typeof pid2 === "number") registry.unregister(pid2);
6011
6108
  registry.afterCall(Date.now() - startedAt, true, bypassBreaker);
6012
6109
  completeBackground(1);
6013
6110
  });
6014
6111
  child2.on("close", (code, signal) => {
6015
- cleanupBackground();
6016
6112
  registry.afterCall(Date.now() - startedAt, false, bypassBreaker);
6017
6113
  completeBackground(code ?? (signal ? 1 : 0), signal ?? void 0);
6018
6114
  });
@@ -6020,7 +6116,7 @@ var bashTool = {
6020
6116
  yield {
6021
6117
  type: "final",
6022
6118
  output: {
6023
- output: normalizeCommandOutput(buf2),
6119
+ output: "",
6024
6120
  exit_code: null,
6025
6121
  timed_out: false,
6026
6122
  pid: pid2
@@ -6291,6 +6387,7 @@ ${hint}` : ""),
6291
6387
  for (const entry of registry.bySession(sessionId)) {
6292
6388
  if (entry.name !== "bash") continue;
6293
6389
  if (entry.child.exitCode !== null) continue;
6390
+ if (entry.background) continue;
6294
6391
  if (entry.protected) continue;
6295
6392
  registry.kill(entry.pid, { force: true });
6296
6393
  }
@@ -6300,7 +6397,7 @@ ${hint}` : ""),
6300
6397
  // src/batch-tool-use.ts
6301
6398
  import {
6302
6399
  GOVERNED_TOOL_EXECUTOR_META_KEY
6303
- } from "@wrongstack/core";
6400
+ } from "@wrongstack/core/types";
6304
6401
  var batchToolUseTool = {
6305
6402
  name: "batch_tool_use",
6306
6403
  category: "Meta",
@@ -6423,7 +6520,7 @@ async function executeSingle(call, ctx, governedExecute) {
6423
6520
 
6424
6521
  // src/browser/tools.ts
6425
6522
  import * as path12 from "node:path";
6426
- import { ToolCapabilities } from "@wrongstack/core";
6523
+ import { ToolCapabilities } from "@wrongstack/core/security";
6427
6524
  import { resolveWstackPaths } from "@wrongstack/core/utils";
6428
6525
 
6429
6526
  // src/browser/manager.ts
@@ -7085,6 +7182,10 @@ var BrowserSessionManager = class {
7085
7182
  const ids = [...this.sessions.values()].filter((session) => session.ownerId === ownerId).map((session) => session.id);
7086
7183
  for (const id of ids) await this.close(id, ownerId).catch(() => void 0);
7087
7184
  }
7185
+ /** True when the manager owns no live browser contexts. */
7186
+ isIdle() {
7187
+ return this.sessions.size === 0;
7188
+ }
7088
7189
  async dispose() {
7089
7190
  for (const session of [...this.sessions.values()]) {
7090
7191
  await session.context.close().catch(() => void 0);
@@ -7266,12 +7367,17 @@ function managerFor(ctx) {
7266
7367
  if (cleanupSignalByContext.get(ctx) !== runSignal) {
7267
7368
  cleanupSignalByContext.set(ctx, runSignal);
7268
7369
  const managerForRun = manager;
7370
+ const rootForRun = root;
7269
7371
  const ownerForRun = owner(ctx);
7270
7372
  ctx.registerAbortHook?.(async () => {
7271
7373
  if (cleanupSignalByContext.get(ctx) === runSignal) {
7272
7374
  cleanupSignalByContext.delete(ctx);
7273
7375
  }
7274
7376
  await managerForRun.closeOwner(ownerForRun);
7377
+ if (managerForRun.isIdle() && managers.get(rootForRun) === managerForRun) {
7378
+ managers.delete(rootForRun);
7379
+ await managerForRun.dispose();
7380
+ }
7275
7381
  });
7276
7382
  }
7277
7383
  return manager;
@@ -7654,7 +7760,7 @@ for (const tool of browserTools) tool.icon = "web";
7654
7760
  for (const tool of browserTools) tool.timeoutMs ??= 6e4;
7655
7761
 
7656
7762
  // src/codebase-index/writer.ts
7657
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
7763
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
7658
7764
 
7659
7765
  // src/codebase-index/circuit-breaker.ts
7660
7766
  var CircuitOpenError = class extends Error {
@@ -7741,7 +7847,7 @@ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
7741
7847
  import { createRequire } from "node:module";
7742
7848
  import * as fs9 from "node:fs";
7743
7849
  import * as path13 from "node:path";
7744
- import { resolveWstackPaths as resolveWstackPaths2 } from "@wrongstack/core";
7850
+ import { resolveWstackPaths as resolveWstackPaths2, sqliteCachePragmas } from "@wrongstack/core/utils";
7745
7851
 
7746
7852
  // src/codebase-index/schema.ts
7747
7853
  var SCHEMA_VERSION = 3;
@@ -7797,23 +7903,13 @@ function buildBm25Index(docs) {
7797
7903
  const tokens = tokenise(d.text);
7798
7904
  return { id: d.id, tokens, raw: d.text, len: tokens.length };
7799
7905
  });
7800
- const df = {};
7801
- for (const doc of documents) {
7802
- const seen = /* @__PURE__ */ new Set();
7803
- for (const t of doc.tokens) {
7804
- if (!seen.has(t)) {
7805
- df[t] = (df[t] ?? 0) + 1;
7806
- seen.add(t);
7807
- }
7808
- }
7809
- }
7810
7906
  const N = documents.length;
7811
7907
  const totalLen = documents.reduce((sum, d) => sum + d.len, 0);
7812
7908
  const avgLen = N === 0 ? 0 : totalLen / N;
7813
- return new Bm25Index(documents, df, N, avgLen);
7909
+ return new Bm25Index(documents, N, avgLen);
7814
7910
  }
7815
7911
  var Bm25Index = class {
7816
- constructor(documents, _df, N, avgLen) {
7912
+ constructor(documents, N, avgLen) {
7817
7913
  this.documents = documents;
7818
7914
  this.N = N;
7819
7915
  this.safeAvgLen = avgLen === 0 ? 1 : avgLen;
@@ -7821,9 +7917,19 @@ var Bm25Index = class {
7821
7917
  documents;
7822
7918
  N;
7823
7919
  safeAvgLen;
7920
+ /** Lazily-built id→doc index so getDoc is O(1) instead of an O(D) linear find. */
7921
+ _byId;
7824
7922
  score(query, filter) {
7825
7923
  const qTokens = tokenise(query);
7826
7924
  if (qTokens.length === 0) return [];
7925
+ const dfByTerm = /* @__PURE__ */ new Map();
7926
+ for (const qTerm of qTokens) {
7927
+ let dfVal = 0;
7928
+ for (const candidate of this.documents) {
7929
+ if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7930
+ }
7931
+ dfByTerm.set(qTerm, dfVal);
7932
+ }
7827
7933
  const results = [];
7828
7934
  for (const doc of this.documents) {
7829
7935
  if (filter && !filter(doc.id)) continue;
@@ -7834,10 +7940,7 @@ var Bm25Index = class {
7834
7940
  if (t.startsWith(qTerm)) tf++;
7835
7941
  }
7836
7942
  if (tf === 0) continue;
7837
- let dfVal = 0;
7838
- for (const candidate of this.documents) {
7839
- if (candidate.tokens.some((t) => t.startsWith(qTerm))) dfVal++;
7840
- }
7943
+ const dfVal = dfByTerm.get(qTerm) ?? 0;
7841
7944
  if (dfVal === 0) continue;
7842
7945
  const idf = Math.log((this.N - dfVal + 0.5) / (dfVal + 0.5) + 1);
7843
7946
  const lenRatio = B * (doc.len / this.safeAvgLen);
@@ -7849,7 +7952,10 @@ var Bm25Index = class {
7849
7952
  return results;
7850
7953
  }
7851
7954
  getDoc(id) {
7852
- return this.documents.find((d) => d.id === id);
7955
+ if (!this._byId) {
7956
+ this._byId = new Map(this.documents.map((d) => [d.id, d]));
7957
+ }
7958
+ return this._byId.get(id);
7853
7959
  }
7854
7960
  extractSnippet(docId, queryTokens, radius = 40) {
7855
7961
  const doc = this.getDoc(docId);
@@ -8011,6 +8117,25 @@ var IndexStore = class _IndexStore {
8011
8117
  * and reuse it. Cleared in {@link close} when the connection is torn down.
8012
8118
  */
8013
8119
  stmtCache = /* @__PURE__ */ new Map();
8120
+ /**
8121
+ * Cached full-corpus BM25 index for the FTS5-unavailable fallback path.
8122
+ * Built lazily on the first `searchRankedFallback` call and invalidated
8123
+ * (via `bm25Dirty`) whenever the `symbols` table is mutated. Computing
8124
+ * IDF over the full corpus is also more correct than the old per-query
8125
+ * candidate-subset IDF.
8126
+ *
8127
+ * Cache-lifecycle invariants (single source of truth lives at the
8128
+ * `invalidateBm25()` helper — see its docblock for the "every mutation
8129
+ * MUST call this" contract):
8130
+ * - declaration: this field + `bm25Dirty` (here)
8131
+ * - invalidation: `invalidateBm25()` flips the flag and nulls the cache
8132
+ * - build: `getOrBuildBm25()` rebuilds against current `symbols` rows
8133
+ * - teardown: `close()` resets the flag and nulls the cache
8134
+ */
8135
+ bm25Cache = null;
8136
+ // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
8137
+ // an empty or pre-existing corpus makes a stale IDF table meaningless.
8138
+ bm25Dirty = true;
8014
8139
  /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
8015
8140
  stmt(sql) {
8016
8141
  let s = this.stmtCache.get(sql);
@@ -8058,7 +8183,15 @@ var IndexStore = class _IndexStore {
8058
8183
  this.db = new Database(path13.join(this.indexDir, DB_FILE));
8059
8184
  try {
8060
8185
  this.db.exec("PRAGMA journal_mode = WAL");
8061
- this.db.exec("PRAGMA busy_timeout = 5000");
8186
+ this.db.exec("PRAGMA synchronous = NORMAL");
8187
+ this.db.exec("PRAGMA busy_timeout = 15000");
8188
+ this.db.exec("PRAGMA temp_store = MEMORY");
8189
+ const cache2 = sqliteCachePragmas();
8190
+ this.db.exec(`PRAGMA cache_size = -${cache2.cacheSizeKiB}`);
8191
+ this.db.exec(`PRAGMA mmap_size = ${cache2.mmapBytes}`);
8192
+ this.db.exec("PRAGMA foreign_keys = ON");
8193
+ this.db.exec("PRAGMA journal_size_limit = 67108864");
8194
+ this.db.exec("PRAGMA wal_autocheckpoint = 1000");
8062
8195
  } catch {
8063
8196
  }
8064
8197
  this.initSchema();
@@ -8070,7 +8203,7 @@ var IndexStore = class _IndexStore {
8070
8203
  value TEXT NOT NULL
8071
8204
  );
8072
8205
  `);
8073
- const storedRows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all("version");
8206
+ const storedRows = this.stmt("SELECT value FROM metadata WHERE key = ?").all("version");
8074
8207
  const storedVersion = storedRows.length ? Number(storedRows[0]?.value) : null;
8075
8208
  if (storedVersion !== null && storedVersion !== SCHEMA_VERSION) {
8076
8209
  this.db.exec(`
@@ -8079,9 +8212,9 @@ var IndexStore = class _IndexStore {
8079
8212
  DROP TABLE IF EXISTS refs;
8080
8213
  `);
8081
8214
  this.db.exec("DROP TABLE IF EXISTS symbols_fts");
8082
- this.db.prepare("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8215
+ this.stmt("UPDATE metadata SET value = ? WHERE key = ?").run(String(SCHEMA_VERSION), "version");
8083
8216
  } else if (storedVersion === null) {
8084
- this.db.prepare("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8217
+ this.stmt("INSERT INTO metadata(key, value) VALUES (?, ?)").run("version", String(SCHEMA_VERSION));
8085
8218
  }
8086
8219
  this.db.exec(`
8087
8220
  CREATE TABLE IF NOT EXISTS files (
@@ -8112,6 +8245,7 @@ var IndexStore = class _IndexStore {
8112
8245
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file ON symbols(file)");
8113
8246
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_lang_kind ON symbols(lang, kind)");
8114
8247
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_file_fk ON symbols(file_fk)");
8248
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_s_name_id ON symbols(name, id)");
8115
8249
  this.db.exec(`
8116
8250
  CREATE TABLE IF NOT EXISTS refs (
8117
8251
  id INTEGER PRIMARY KEY,
@@ -8132,65 +8266,167 @@ var IndexStore = class _IndexStore {
8132
8266
  );
8133
8267
  this.ftsAvailable = true;
8134
8268
  const symbolCount = Number(
8135
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8269
+ this.stmt("SELECT COUNT(*) AS n FROM symbols").get()?.n ?? 0
8136
8270
  );
8137
8271
  const ftsCount = Number(
8138
- this.db.prepare("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8272
+ this.stmt("SELECT COUNT(*) AS n FROM symbols_fts").get()?.n ?? 0
8139
8273
  );
8140
8274
  if (symbolCount !== ftsCount) {
8141
8275
  this.db.exec("DELETE FROM symbols_fts");
8142
- const insert = this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)");
8143
- const rows = this.db.prepare("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8144
- for (const row of rows) {
8145
- insert.run(row.id, buildIndexableText(row.name, row.signature, row.doc_comment));
8146
- }
8276
+ const rows = this.stmt("SELECT id, name, signature, doc_comment FROM symbols ORDER BY id").all();
8277
+ this.bulkInsertFts(
8278
+ rows.map((row) => ({
8279
+ id: row.id,
8280
+ text: buildIndexableText(row.name, row.signature, row.doc_comment)
8281
+ }))
8282
+ );
8283
+ this.invalidateBm25();
8147
8284
  }
8148
8285
  } catch {
8149
8286
  this.ftsAvailable = false;
8150
8287
  }
8288
+ this.ensureNextSymbolIdSeeded();
8289
+ }
8290
+ // ─── ID allocation & bulk helpers ────────────────────────────────────────────
8291
+ static NEXT_SYMBOL_ID_KEY = "next_symbol_id";
8292
+ /** Stay under typical SQLite SQLITE_MAX_VARIABLE_NUMBER (often 999). */
8293
+ static MAX_SQL_VARS = 900;
8294
+ /**
8295
+ * Ensure `metadata.next_symbol_id` exists. Safe to call outside a write
8296
+ * transaction on open; the first concurrent writer under BEGIN IMMEDIATE
8297
+ * re-reads and advances the counter atomically.
8298
+ */
8299
+ ensureNextSymbolIdSeeded() {
8300
+ const existing = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8301
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8302
+ );
8303
+ if (existing?.value !== void 0) return;
8304
+ const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8305
+ const next = (maxRows[0]?.m ?? 0) + 1;
8306
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8307
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8308
+ String(next)
8309
+ );
8310
+ }
8311
+ /**
8312
+ * Reserve `count` consecutive symbol ids. MUST run inside BEGIN IMMEDIATE
8313
+ * so concurrent indexers cannot hand out overlapping ranges.
8314
+ */
8315
+ allocateSymbolIds(count) {
8316
+ if (count <= 0) return this.getMaxSymbolId() + 1;
8317
+ this.ensureNextSymbolIdSeeded();
8318
+ const row = this.stmt("SELECT value FROM metadata WHERE key = ?").get(
8319
+ _IndexStore.NEXT_SYMBOL_ID_KEY
8320
+ );
8321
+ const start = Math.max(1, Number(row?.value ?? 1) || 1);
8322
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES (?, ?)").run(
8323
+ _IndexStore.NEXT_SYMBOL_ID_KEY,
8324
+ String(start + count)
8325
+ );
8326
+ return start;
8327
+ }
8328
+ /** Multi-row INSERT for symbols — amortizes prepare/bind overhead in large batches. */
8329
+ bulkInsertSymbols(rows) {
8330
+ if (rows.length === 0) return;
8331
+ const cols = 12;
8332
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8333
+ for (let i = 0; i < rows.length; i += chunkSize) {
8334
+ const chunk = rows.slice(i, i + chunkSize);
8335
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
8336
+ const stmt = this.stmt(
8337
+ `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8338
+ VALUES ${placeholders}`
8339
+ );
8340
+ const binds = [];
8341
+ for (const r of chunk) {
8342
+ binds.push(
8343
+ r.id,
8344
+ r.lang,
8345
+ r.kind,
8346
+ r.name,
8347
+ r.file,
8348
+ r.line,
8349
+ r.col,
8350
+ r.signature,
8351
+ r.docComment,
8352
+ r.scope,
8353
+ r.text,
8354
+ r.file
8355
+ );
8356
+ }
8357
+ stmt.run(...binds);
8358
+ }
8359
+ }
8360
+ bulkInsertFts(rows) {
8361
+ if (!this.ftsAvailable || rows.length === 0) return;
8362
+ const cols = 2;
8363
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8364
+ for (let i = 0; i < rows.length; i += chunkSize) {
8365
+ const chunk = rows.slice(i, i + chunkSize);
8366
+ const placeholders = chunk.map(() => "(?, ?)").join(", ");
8367
+ const stmt = this.stmt(`INSERT INTO symbols_fts(rowid, text) VALUES ${placeholders}`);
8368
+ const binds = [];
8369
+ for (const r of chunk) binds.push(r.id, r.text);
8370
+ stmt.run(...binds);
8371
+ }
8372
+ }
8373
+ bulkInsertRefs(refs) {
8374
+ if (refs.length === 0) return;
8375
+ const cols = 5;
8376
+ const chunkSize = Math.max(1, Math.floor(_IndexStore.MAX_SQL_VARS / cols));
8377
+ for (let i = 0; i < refs.length; i += chunkSize) {
8378
+ const chunk = refs.slice(i, i + chunkSize);
8379
+ const placeholders = chunk.map(() => "(?, ?, ?, ?, ?)").join(", ");
8380
+ const stmt = this.stmt(
8381
+ `INSERT INTO refs(from_id, to_name, to_id, call_type, line) VALUES ${placeholders}`
8382
+ );
8383
+ const binds = [];
8384
+ for (const ref of chunk) {
8385
+ binds.push(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8386
+ }
8387
+ stmt.run(...binds);
8388
+ }
8151
8389
  }
8152
8390
  // ─── Symbol CRUD ─────────────────────────────────────────────────────────────
8153
8391
  /**
8154
8392
  * Insert symbols, assigning IDs atomically inside `BEGIN IMMEDIATE` /
8155
- * `COMMIT`. The ID allocation (`SELECT MAX(id)`) and all `INSERT`s share
8156
- * the same transaction, preventing UNIQUE constraint violations when two
8157
- * processes index concurrently (each would see a different `MAX(id)` and
8158
- * neither can insert with the other's IDs).
8393
+ * `COMMIT`. Id ranges come from the `next_symbol_id` metadata counter
8394
+ * (O(1)); multi-row INSERT amortizes bind overhead for large files.
8159
8395
  *
8160
8396
  * @returns The symbols array with `id` fields populated so the caller can
8161
8397
  * use them for refs without re-reading from the DB.
8162
8398
  */
8163
8399
  insertSymbols(symbols) {
8400
+ this.invalidateBm25();
8164
8401
  return this.runWithRetry(() => {
8165
8402
  this.db.exec("BEGIN IMMEDIATE");
8166
8403
  try {
8167
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8168
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8169
- const stmt = this.db.prepare(
8170
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8171
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8172
- );
8173
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
8404
+ let nextId = this.allocateSymbolIds(symbols.length);
8174
8405
  const result = [];
8406
+ const bulk = [];
8407
+ const ftsRows = [];
8175
8408
  for (const s of symbols) {
8176
8409
  const id = nextId++;
8177
- stmt.run(
8410
+ bulk.push({
8178
8411
  id,
8179
- s.lang,
8180
- s.kind,
8181
- s.name,
8182
- s.file,
8183
- s.line,
8184
- s.col,
8185
- s.signature,
8186
- s.docComment,
8187
- s.scope,
8188
- s.text,
8189
- s.file
8190
- );
8191
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8412
+ lang: s.lang,
8413
+ kind: s.kind,
8414
+ name: s.name,
8415
+ file: s.file,
8416
+ line: s.line,
8417
+ col: s.col,
8418
+ signature: s.signature,
8419
+ docComment: s.docComment,
8420
+ scope: s.scope,
8421
+ text: s.text
8422
+ });
8423
+ if (this.ftsAvailable) {
8424
+ ftsRows.push({ id, text: buildIndexableText(s.name, s.signature, s.docComment) });
8425
+ }
8192
8426
  result.push({ ...s, id });
8193
8427
  }
8428
+ this.bulkInsertSymbols(bulk);
8429
+ this.bulkInsertFts(ftsRows);
8194
8430
  this.db.exec("COMMIT");
8195
8431
  return result;
8196
8432
  } catch (err) {
@@ -8200,6 +8436,7 @@ var IndexStore = class _IndexStore {
8200
8436
  });
8201
8437
  }
8202
8438
  deleteSymbolsForFile(file) {
8439
+ this.invalidateBm25();
8203
8440
  this.runWithRetry(() => {
8204
8441
  if (this.ftsAvailable) {
8205
8442
  this.stmt(
@@ -8215,17 +8452,20 @@ var IndexStore = class _IndexStore {
8215
8452
  * dropped the `files` row, leaving its symbols orphaned but still searchable.
8216
8453
  */
8217
8454
  deleteFile(file) {
8455
+ this.invalidateBm25();
8218
8456
  this.runWithRetry(() => {
8219
8457
  this.db.exec("BEGIN IMMEDIATE");
8220
8458
  try {
8221
8459
  if (this.ftsAvailable) {
8222
- this.db.prepare(
8460
+ this.stmt(
8223
8461
  "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8224
8462
  ).run(file);
8225
8463
  }
8226
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)").run(file);
8227
- this.db.prepare("DELETE FROM symbols WHERE file_fk = ?").run(file);
8228
- this.db.prepare("DELETE FROM files WHERE file = ?").run(file);
8464
+ this.stmt(
8465
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8466
+ ).run(file);
8467
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(file);
8468
+ this.stmt("DELETE FROM files WHERE file = ?").run(file);
8229
8469
  this.db.exec("COMMIT");
8230
8470
  } catch (err) {
8231
8471
  this.db.exec("ROLLBACK");
@@ -8262,7 +8502,7 @@ var IndexStore = class _IndexStore {
8262
8502
  };
8263
8503
  }
8264
8504
  getAllFileMetas() {
8265
- return this.db.prepare("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8505
+ return this.stmt("SELECT file, lang, mtime_ms, symbol_count, last_indexed FROM files").all().map((r) => ({
8266
8506
  file: r.file,
8267
8507
  lang: r.lang,
8268
8508
  mtimeMs: r.mtime_ms,
@@ -8271,7 +8511,32 @@ var IndexStore = class _IndexStore {
8271
8511
  }));
8272
8512
  }
8273
8513
  // ─── Search ──────────────────────────────────────────────────────────────────
8274
- search(query, filter) {
8514
+ search(query, filter, opts) {
8515
+ const built = this.buildSearchWhere(query, filter);
8516
+ if (built === null) return [];
8517
+ const { where, values } = built;
8518
+ const limit = typeof opts?.limit === "number" && Number.isFinite(opts.limit) ? Math.max(0, Math.trunc(opts.limit)) : void 0;
8519
+ const limitSql = limit !== void 0 ? " LIMIT ?" : "";
8520
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
8521
+ const binds = limit !== void 0 ? [...values, limit] : values;
8522
+ const rows = this.stmt(sql).all(...binds);
8523
+ return rows.map((r) => ({
8524
+ id: r.id,
8525
+ lang: r.lang,
8526
+ kind: r.kind,
8527
+ name: r.name,
8528
+ file: r.file,
8529
+ line: r.line,
8530
+ col: r.col,
8531
+ signature: r.signature,
8532
+ docComment: r.doc_comment,
8533
+ score: 0,
8534
+ snippet: "",
8535
+ lspKind: filter?.lspKind
8536
+ }));
8537
+ }
8538
+ /** Shared WHERE builder for {@link search} / empty-query ranked totals. */
8539
+ buildSearchWhere(query, filter) {
8275
8540
  const conditions = [];
8276
8541
  const values = [];
8277
8542
  let effectiveKind = filter?.kind;
@@ -8280,7 +8545,7 @@ var IndexStore = class _IndexStore {
8280
8545
  if (mapped !== null) {
8281
8546
  effectiveKind = mapped;
8282
8547
  } else {
8283
- return [];
8548
+ return null;
8284
8549
  }
8285
8550
  }
8286
8551
  if (effectiveKind) {
@@ -8302,23 +8567,15 @@ var IndexStore = class _IndexStore {
8302
8567
  for (const t of tokens) values.push(`%${t}%`);
8303
8568
  }
8304
8569
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
8305
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}`;
8306
- const stmt = this.db.prepare(sql);
8307
- const rows = stmt.all(...values);
8308
- return rows.map((r) => ({
8309
- id: r.id,
8310
- lang: r.lang,
8311
- kind: r.kind,
8312
- name: r.name,
8313
- file: r.file,
8314
- line: r.line,
8315
- col: r.col,
8316
- signature: r.signature,
8317
- docComment: r.doc_comment,
8318
- score: 0,
8319
- snippet: "",
8320
- lspKind: filter?.lspKind
8321
- }));
8570
+ return { where, values };
8571
+ }
8572
+ countSearch(query, filter) {
8573
+ const built = this.buildSearchWhere(query, filter);
8574
+ if (built === null) return 0;
8575
+ const row = this.stmt(`SELECT COUNT(*) AS n FROM symbols ${built.where}`).get(
8576
+ ...built.values
8577
+ );
8578
+ return Number(row?.n ?? 0);
8322
8579
  }
8323
8580
  /**
8324
8581
  * Ranked search — the one-stop query the codebase-search tool and plug-lsp
@@ -8359,12 +8616,12 @@ var IndexStore = class _IndexStore {
8359
8616
  values.push(`%${escapeLike(filter.file.replace(/\\/g, "/"))}%`);
8360
8617
  }
8361
8618
  const where = conditions.join(" AND ");
8362
- const countRows = this.db.prepare(
8619
+ const countRows = this.stmt(
8363
8620
  `SELECT COUNT(*) AS n FROM symbols_fts JOIN symbols s ON s.id = symbols_fts.rowid WHERE ${where}`
8364
8621
  ).all(...values);
8365
8622
  const total = countRows[0] ? Number(countRows[0].n) : 0;
8366
8623
  if (total === 0) return { results: [], total: 0 };
8367
- const rows = this.db.prepare(
8624
+ const rows = this.stmt(
8368
8625
  `SELECT s.id, s.lang, s.kind, s.name, s.file, s.line, s.col, s.signature, s.doc_comment,
8369
8626
  -bm25(symbols_fts) AS score,
8370
8627
  snippet(symbols_fts, 0, '', '', '\u2026', 12) AS snippet
@@ -8397,20 +8654,60 @@ var IndexStore = class _IndexStore {
8397
8654
  total
8398
8655
  };
8399
8656
  }
8657
+ /**
8658
+ * Invalidate the cached BM25 index.
8659
+ *
8660
+ * **Contract: every method that mutates `symbols` MUST call this before
8661
+ * returning.** (`refs` mutations do not affect the BM25 fallback because
8662
+ * the corpus is built from `symbols.text` via `getAllIndexable()` and the
8663
+ * BM25 score is filtered by the LIKE-selected candidate set in
8664
+ * `searchRankedFallback`.) Today the call sites are `repairDrift`,
8665
+ * `insertSymbols`, `deleteSymbolsForFile`, `deleteFile`, `clearAll`, and
8666
+ * `commitBatch`. A future mutation that adds a new write path (e.g.
8667
+ * `renameFile`, `updateSignature`) MUST also call this — otherwise the
8668
+ * FTS5-unavailable fallback will serve stale search results. The
8669
+ * `close()` reset at L1820-1821 tears the cache down on store shutdown,
8670
+ * which is the only legitimate place that flips the flag outside this
8671
+ * helper.
8672
+ *
8673
+ * Called *before* `runWithRetry` on purpose: if the write fails all
8674
+ * retries the flag stays set, forcing a rebuild on the next search rather
8675
+ * than trusting a cache that may not reflect the intended mutation.
8676
+ * Do not move this inside the retry closure.
8677
+ */
8678
+ invalidateBm25() {
8679
+ this.bm25Dirty = true;
8680
+ this.bm25Cache = null;
8681
+ }
8682
+ /**
8683
+ * Return the cached full-corpus BM25 index, rebuilding it only when the
8684
+ * symbols table has been mutated since the last build. The full-corpus IDF
8685
+ * is more correct than the old per-query candidate-subset IDF, and the
8686
+ * amortized build cost drops from O(symbols × tokens) per search to once
8687
+ * per write batch.
8688
+ *
8689
+ * Note: the first call after a long idle (or on a freshly opened store)
8690
+ * pays the full corpus rebuild synchronously on the search path. For a
8691
+ * 5 500+ symbol corpus this is a visible one-time latency spike.
8692
+ */
8693
+ getOrBuildBm25() {
8694
+ if (this.bm25Cache && !this.bm25Dirty) return this.bm25Cache;
8695
+ const docs = this.getAllIndexable();
8696
+ this.bm25Cache = buildBm25Index(docs);
8697
+ this.bm25Dirty = false;
8698
+ return this.bm25Cache;
8699
+ }
8400
8700
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
8401
8701
  searchRankedFallback(query, filter, limit) {
8402
- const candidates = this.search(query, filter);
8403
- if (candidates.length === 0) return { results: [], total: 0 };
8404
8702
  if (!query.trim()) {
8405
- return { results: candidates.slice(0, limit), total: candidates.length };
8703
+ const total = this.countSearch(query, filter);
8704
+ if (total === 0) return { results: [], total: 0 };
8705
+ return { results: this.search(query, filter, { limit }), total };
8406
8706
  }
8707
+ const candidates = this.search(query, filter);
8708
+ if (candidates.length === 0) return { results: [], total: 0 };
8407
8709
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
8408
- const bm25 = buildBm25Index(
8409
- candidates.map((c) => ({
8410
- id: c.id,
8411
- text: buildIndexableText(c.name, c.signature, c.docComment)
8412
- }))
8413
- );
8710
+ const bm25 = this.getOrBuildBm25();
8414
8711
  const scored = bm25.score(query, (id) => candidateById.has(id));
8415
8712
  const q = query.trim().toLowerCase();
8416
8713
  const rank = (id) => {
@@ -8452,7 +8749,7 @@ var IndexStore = class _IndexStore {
8452
8749
  // ─── Stats ───────────────────────────────────────────────────────────────────
8453
8750
  getStats() {
8454
8751
  const sizeBytes = this.sizeBytes();
8455
- const lastRows = this.db.prepare("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8752
+ const lastRows = this.stmt("SELECT value FROM metadata WHERE key = 'last_indexed'").all();
8456
8753
  const lastIndexed = lastRows.length ? Number(lastRows[0]?.value) : null;
8457
8754
  const totalRows = this.stmt("SELECT COUNT(*) FROM symbols").all();
8458
8755
  const totalSymbols = totalRows[0] ? Number(totalRows[0]["COUNT(*)"]) : 0;
@@ -8477,19 +8774,20 @@ var IndexStore = class _IndexStore {
8477
8774
  }
8478
8775
  setLastIndexed(ts2) {
8479
8776
  this.runWithRetry(() => {
8480
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8777
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES('last_indexed', ?)").run(String(ts2));
8481
8778
  });
8482
8779
  }
8483
8780
  getMetadata(key) {
8484
- const rows = this.db.prepare("SELECT value FROM metadata WHERE key = ?").all(key);
8781
+ const rows = this.stmt("SELECT value FROM metadata WHERE key = ?").all(key);
8485
8782
  return rows[0]?.value;
8486
8783
  }
8487
8784
  setMetadata(key, value) {
8488
8785
  this.runWithRetry(() => {
8489
- this.db.prepare("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8786
+ this.stmt("INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)").run(key, value);
8490
8787
  });
8491
8788
  }
8492
8789
  clearAll() {
8790
+ this.invalidateBm25();
8493
8791
  this.runWithRetry(() => {
8494
8792
  this.db.exec("BEGIN IMMEDIATE");
8495
8793
  try {
@@ -8516,13 +8814,7 @@ var IndexStore = class _IndexStore {
8516
8814
  this.runWithRetry(() => {
8517
8815
  this.stmt("DELETE FROM refs WHERE from_id = ?").run(fromId);
8518
8816
  if (refs.length === 0) return;
8519
- const stmt = this.db.prepare(
8520
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8521
- VALUES (?, ?, ?, ?, ?)`
8522
- );
8523
- for (const ref of refs) {
8524
- stmt.run(fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8525
- }
8817
+ this.bulkInsertRefs(refs.map((ref) => ({ ...ref, fromId })));
8526
8818
  });
8527
8819
  }
8528
8820
  /**
@@ -8539,13 +8831,7 @@ var IndexStore = class _IndexStore {
8539
8831
  insertRefsBatch(refs) {
8540
8832
  if (refs.length === 0) return;
8541
8833
  this.runWithRetry(() => {
8542
- const stmt = this.db.prepare(
8543
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8544
- VALUES (?, ?, ?, ?, ?)`
8545
- );
8546
- for (const ref of refs) {
8547
- stmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8548
- }
8834
+ this.bulkInsertRefs(refs);
8549
8835
  });
8550
8836
  }
8551
8837
  /**
@@ -8570,65 +8856,61 @@ var IndexStore = class _IndexStore {
8570
8856
  if (entries.length === 0 && (options.deleteForFiles?.length ?? 0) === 0) {
8571
8857
  return [];
8572
8858
  }
8859
+ this.invalidateBm25();
8573
8860
  return this.runWithRetry(() => {
8574
8861
  this.db.exec("BEGIN IMMEDIATE");
8575
8862
  try {
8576
8863
  if (options.deleteForFiles && options.deleteForFiles.length > 0) {
8577
8864
  const placeholders = options.deleteForFiles.map(() => "?").join(",");
8578
8865
  if (this.ftsAvailable) {
8579
- this.db.prepare(
8866
+ this.stmt(
8580
8867
  `DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8581
8868
  ).run(...options.deleteForFiles);
8582
8869
  }
8583
- this.db.prepare(
8870
+ this.stmt(
8584
8871
  `DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file IN (${placeholders}))`
8585
8872
  ).run(...options.deleteForFiles);
8586
- this.db.prepare(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8873
+ this.stmt(`DELETE FROM symbols WHERE file IN (${placeholders})`).run(...options.deleteForFiles);
8587
8874
  }
8588
- const maxRows = this.stmt("SELECT MAX(id) AS m FROM symbols").all();
8589
- let nextId = (maxRows[0]?.m ?? 0) + 1;
8590
- const symStmt = this.db.prepare(
8591
- `INSERT INTO symbols(id, lang, kind, name, file, line, col, signature, doc_comment, scope, text, file_fk)
8592
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
8593
- );
8594
- const ftsStmt = this.ftsAvailable ? this.db.prepare("INSERT INTO symbols_fts(rowid, text) VALUES (?, ?)") : null;
8875
+ const totalSymbols = entries.reduce((n, e) => n + e.symbols.length, 0);
8876
+ let nextId = this.allocateSymbolIds(totalSymbols);
8595
8877
  const allInserted = [];
8596
8878
  const refsToInsert = [];
8879
+ const bulkSyms = [];
8880
+ const ftsRows = [];
8597
8881
  for (const entry of entries) {
8598
8882
  const insertedForEntry = [];
8599
8883
  for (const s of entry.symbols) {
8600
8884
  const id = nextId++;
8601
- symStmt.run(
8885
+ bulkSyms.push({
8602
8886
  id,
8603
- s.lang,
8604
- s.kind,
8605
- s.name,
8606
- s.file,
8607
- s.line,
8608
- s.col,
8609
- s.signature,
8610
- s.docComment,
8611
- s.scope,
8612
- s.text,
8613
- s.file
8614
- );
8615
- ftsStmt?.run(id, buildIndexableText(s.name, s.signature, s.docComment));
8887
+ lang: s.lang,
8888
+ kind: s.kind,
8889
+ name: s.name,
8890
+ file: s.file,
8891
+ line: s.line,
8892
+ col: s.col,
8893
+ signature: s.signature,
8894
+ docComment: s.docComment,
8895
+ scope: s.scope,
8896
+ text: s.text
8897
+ });
8898
+ if (this.ftsAvailable) {
8899
+ ftsRows.push({
8900
+ id,
8901
+ text: buildIndexableText(s.name, s.signature, s.docComment)
8902
+ });
8903
+ }
8616
8904
  const inserted = { ...s, id };
8617
8905
  allInserted.push(inserted);
8618
8906
  insertedForEntry.push(inserted);
8619
8907
  }
8620
8908
  refsToInsert.push(...assignRefsToSymbols(entry.refs, insertedForEntry));
8621
8909
  }
8622
- if (refsToInsert.length > 0) {
8623
- const refStmt = this.db.prepare(
8624
- `INSERT INTO refs(from_id, to_name, to_id, call_type, line)
8625
- VALUES (?, ?, ?, ?, ?)`
8626
- );
8627
- for (const ref of refsToInsert) {
8628
- refStmt.run(ref.fromId, ref.toName, ref.toId ?? null, ref.callType, ref.line);
8629
- }
8630
- }
8631
- const upsertStmt = this.db.prepare(
8910
+ this.bulkInsertSymbols(bulkSyms);
8911
+ this.bulkInsertFts(ftsRows);
8912
+ this.bulkInsertRefs(refsToInsert);
8913
+ const upsertStmt = this.stmt(
8632
8914
  `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8633
8915
  VALUES (?, ?, ?, ?, ?)
8634
8916
  ON CONFLICT(file) DO UPDATE SET
@@ -8655,7 +8937,7 @@ var IndexStore = class _IndexStore {
8655
8937
  */
8656
8938
  deleteRefsForFile(file) {
8657
8939
  this.runWithRetry(() => {
8658
- this.db.prepare("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8940
+ this.stmt("DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file = ?)").run(file);
8659
8941
  });
8660
8942
  }
8661
8943
  /**
@@ -8668,20 +8950,75 @@ var IndexStore = class _IndexStore {
8668
8950
  */
8669
8951
  resolveRefs() {
8670
8952
  return this.runWithRetry(() => {
8671
- const result = this.db.prepare(
8672
- `UPDATE refs SET to_id = (
8673
- SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
8674
- ) WHERE to_id IS NULL AND to_name IS NOT NULL
8675
- AND to_name IN (SELECT name FROM symbols)`
8676
- ).run();
8677
- return result.changes ?? 0;
8953
+ try {
8954
+ const result = this.stmt(
8955
+ `UPDATE refs
8956
+ SET to_id = s.id
8957
+ FROM (
8958
+ SELECT name, MIN(id) AS id FROM symbols GROUP BY name
8959
+ ) AS s
8960
+ WHERE refs.to_id IS NULL
8961
+ AND refs.to_name IS NOT NULL
8962
+ AND refs.to_name = s.name`
8963
+ ).run();
8964
+ return result.changes ?? 0;
8965
+ } catch {
8966
+ const result = this.stmt(
8967
+ `UPDATE refs SET to_id = (
8968
+ SELECT id FROM symbols WHERE name = refs.to_name LIMIT 1
8969
+ ) WHERE to_id IS NULL AND to_name IS NOT NULL
8970
+ AND to_name IN (SELECT name FROM symbols)`
8971
+ ).run();
8972
+ return result.changes ?? 0;
8973
+ }
8678
8974
  });
8679
8975
  }
8976
+ /**
8977
+ * Clear symbols/refs for a file and mark it as indexed with zero symbols.
8978
+ * Used by the indexer for empty-parse results so three writes share one txn.
8979
+ */
8980
+ replaceEmptyFile(meta) {
8981
+ this.invalidateBm25();
8982
+ this.runWithRetry(() => {
8983
+ this.db.exec("BEGIN IMMEDIATE");
8984
+ try {
8985
+ if (this.ftsAvailable) {
8986
+ this.stmt(
8987
+ "DELETE FROM symbols_fts WHERE rowid IN (SELECT id FROM symbols WHERE file_fk = ?)"
8988
+ ).run(meta.file);
8989
+ }
8990
+ this.stmt(
8991
+ "DELETE FROM refs WHERE from_id IN (SELECT id FROM symbols WHERE file_fk = ?)"
8992
+ ).run(meta.file);
8993
+ this.stmt("DELETE FROM symbols WHERE file_fk = ?").run(meta.file);
8994
+ this.stmt(
8995
+ `INSERT INTO files(file, lang, mtime_ms, symbol_count, last_indexed)
8996
+ VALUES (?, ?, ?, ?, ?)
8997
+ ON CONFLICT(file) DO UPDATE SET
8998
+ lang = excluded.lang,
8999
+ mtime_ms = excluded.mtime_ms,
9000
+ symbol_count = excluded.symbol_count,
9001
+ last_indexed = excluded.last_indexed`
9002
+ ).run(meta.file, meta.lang, meta.mtimeMs, meta.symbolCount, meta.lastIndexed);
9003
+ this.db.exec("COMMIT");
9004
+ } catch (err) {
9005
+ this.db.exec("ROLLBACK");
9006
+ throw err;
9007
+ }
9008
+ });
9009
+ }
9010
+ /** Best-effort query planner refresh after a large reindex. */
9011
+ optimize() {
9012
+ try {
9013
+ this.db.exec("PRAGMA optimize");
9014
+ } catch {
9015
+ }
9016
+ }
8680
9017
  /**
8681
9018
  * Find all references TO a given symbol (who calls / uses this symbol?).
8682
9019
  */
8683
9020
  findRefsTo(symbolId) {
8684
- return this.db.prepare(
9021
+ return this.stmt(
8685
9022
  "SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE to_id = ? OR to_name = (SELECT name FROM symbols WHERE id = ?)"
8686
9023
  ).all(symbolId, symbolId).map((r) => ({
8687
9024
  id: r.id,
@@ -8696,7 +9033,7 @@ var IndexStore = class _IndexStore {
8696
9033
  * Find all references FROM a given symbol (what does this symbol call/use?).
8697
9034
  */
8698
9035
  findRefsFrom(symbolId) {
8699
- return this.db.prepare("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
9036
+ return this.stmt("SELECT id, from_id, to_name, to_id, call_type, line FROM refs WHERE from_id = ?").all(symbolId).map((r) => ({
8700
9037
  id: r.id,
8701
9038
  fromId: r.from_id,
8702
9039
  toName: r.to_name,
@@ -8769,61 +9106,77 @@ var IndexStore = class _IndexStore {
8769
9106
  * symbol resolved in package B). Node metadata includes symbol/file counts.
8770
9107
  */
8771
9108
  getPackageGraph() {
8772
- const symbols = this.db.prepare("SELECT file, id FROM symbols ORDER BY id").all();
9109
+ const fileCounts = this.stmt(
9110
+ "SELECT file, COUNT(*) AS n FROM symbols GROUP BY file"
9111
+ ).all();
8773
9112
  const pkgNodes = /* @__PURE__ */ new Map();
8774
9113
  const fileToPkg = /* @__PURE__ */ new Map();
8775
- const symbolToPkg = /* @__PURE__ */ new Map();
8776
- for (const s of symbols) {
8777
- const pkg = _IndexStore.derivePackage(s.file) ?? "(root)";
8778
- fileToPkg.set(s.file, pkg);
8779
- symbolToPkg.set(s.id, pkg);
9114
+ for (const { file, n } of fileCounts) {
9115
+ const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9116
+ fileToPkg.set(file, pkg);
8780
9117
  const node = pkgNodes.get(pkg);
8781
9118
  if (node) {
8782
- node.symbolCount = (node.symbolCount ?? 0) + 1;
9119
+ node.symbolCount = (node.symbolCount ?? 0) + n;
8783
9120
  } else {
8784
9121
  pkgNodes.set(pkg, {
8785
9122
  id: `pkg:${pkg}`,
8786
9123
  label: pkg,
8787
9124
  kind: "package",
8788
9125
  package: pkg,
8789
- symbolCount: 1,
9126
+ symbolCount: n,
8790
9127
  fileCount: 0
8791
9128
  });
8792
9129
  }
8793
9130
  }
8794
- const files = this.db.prepare("SELECT DISTINCT file FROM files").all();
9131
+ const files = this.stmt("SELECT DISTINCT file FROM files").all();
8795
9132
  for (const { file } of files) {
8796
9133
  const pkg = _IndexStore.derivePackage(file) ?? "(root)";
9134
+ fileToPkg.set(file, pkg);
8797
9135
  const node = pkgNodes.get(pkg);
8798
9136
  if (node) node.fileCount = (node.fileCount ?? 0) + 1;
9137
+ else {
9138
+ pkgNodes.set(pkg, {
9139
+ id: `pkg:${pkg}`,
9140
+ label: pkg,
9141
+ kind: "package",
9142
+ package: pkg,
9143
+ symbolCount: 0,
9144
+ fileCount: 1
9145
+ });
9146
+ }
8799
9147
  }
8800
- const refRows = this.db.prepare(
8801
- `SELECT r.from_id, r.to_id, r.call_type
9148
+ const refRows = this.stmt(
9149
+ `SELECT r.call_type, sf.file AS from_file, st.file AS to_file, COUNT(*) AS n
8802
9150
  FROM refs r
8803
- WHERE r.to_id IS NOT NULL`
9151
+ JOIN symbols sf ON sf.id = r.from_id
9152
+ JOIN symbols st ON st.id = r.to_id
9153
+ WHERE r.to_id IS NOT NULL AND r.call_type != 'import'
9154
+ GROUP BY r.call_type, sf.file, st.file`
8804
9155
  ).all();
8805
9156
  const edgeMap = /* @__PURE__ */ new Map();
8806
9157
  for (const r of refRows) {
8807
- if (r.call_type === "import") continue;
8808
- const fromPkg = symbolToPkg.get(r.from_id);
8809
- const toPkg = symbolToPkg.get(r.to_id);
8810
- if (!fromPkg || !toPkg || fromPkg === toPkg) continue;
9158
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
9159
+ const toPkg = fileToPkg.get(r.to_file) ?? _IndexStore.derivePackage(r.to_file) ?? "(root)";
9160
+ if (fromPkg === toPkg) continue;
8811
9161
  const key = `${fromPkg}\0${toPkg}`;
8812
9162
  let e = edgeMap.get(key);
8813
9163
  if (!e) {
8814
9164
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
8815
9165
  edgeMap.set(key, e);
8816
9166
  }
8817
- e.weight++;
8818
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9167
+ const n = Number(r.n) || 0;
9168
+ e.weight += n;
9169
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
8819
9170
  }
8820
- const importRows = this.db.prepare(
8821
- `SELECT r.from_id, r.to_name
9171
+ const importRows = this.stmt(
9172
+ `SELECT r.to_name, s.file AS from_file, COUNT(*) AS n
8822
9173
  FROM refs r
8823
- WHERE r.call_type = 'import'`
9174
+ JOIN symbols s ON s.id = r.from_id
9175
+ WHERE r.call_type = 'import'
9176
+ GROUP BY r.to_name, s.file`
8824
9177
  ).all();
8825
9178
  for (const r of importRows) {
8826
- const fromPkg = symbolToPkg.get(r.from_id);
9179
+ const fromPkg = fileToPkg.get(r.from_file) ?? _IndexStore.derivePackage(r.from_file) ?? "(root)";
8827
9180
  const toPkg = _IndexStore.packageFromImport(r.to_name);
8828
9181
  if (!fromPkg || !toPkg || fromPkg === toPkg || !pkgNodes.has(toPkg)) continue;
8829
9182
  const key = `${fromPkg}\0${toPkg}`;
@@ -8832,8 +9185,9 @@ var IndexStore = class _IndexStore {
8832
9185
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8833
9186
  edgeMap.set(key, edge);
8834
9187
  }
8835
- edge.weight++;
8836
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9188
+ const n = Number(r.n) || 0;
9189
+ edge.weight += n;
9190
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
8837
9191
  }
8838
9192
  const edges = [];
8839
9193
  for (const [key, e] of edgeMap) {
@@ -8860,12 +9214,12 @@ var IndexStore = class _IndexStore {
8860
9214
  * derived from cross-file symbol references within the package.
8861
9215
  */
8862
9216
  getFileGraph(packageFilter) {
8863
- const allFiles = this.db.prepare("SELECT DISTINCT file FROM symbols").all();
9217
+ const allFiles = this.stmt("SELECT DISTINCT file FROM symbols").all();
8864
9218
  const pkgFilePaths = allFiles.filter((f) => (_IndexStore.derivePackage(f.file) ?? "(root)") === packageFilter).map((f) => f.file);
8865
9219
  const localFiles = new Set(pkgFilePaths);
8866
9220
  if (localFiles.size === 0) return { nodes: [], edges: [] };
8867
9221
  const filePlaceholders = [...localFiles].map(() => "?").join(",");
8868
- const pkgSyms = this.db.prepare(
9222
+ const pkgSyms = this.stmt(
8869
9223
  `SELECT file, id, name, kind, lang, line FROM symbols WHERE file IN (${filePlaceholders}) ORDER BY id`
8870
9224
  ).all(...pkgFilePaths);
8871
9225
  const fileNodes = /* @__PURE__ */ new Map();
@@ -8897,12 +9251,13 @@ var IndexStore = class _IndexStore {
8897
9251
  ensureFileNode(file);
8898
9252
  }
8899
9253
  const indexedFiles = new Set(allFiles.map((f) => f.file));
8900
- const refRows = this.db.prepare(
8901
- `SELECT r.from_id, r.to_id, r.call_type
9254
+ const refRows = this.stmt(
9255
+ `SELECT r.from_id, r.to_id, r.call_type, COUNT(*) AS n
8902
9256
  FROM refs r
8903
9257
  WHERE (r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
8904
9258
  OR r.to_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders})))
8905
- AND r.to_id IS NOT NULL`
9259
+ AND r.to_id IS NOT NULL
9260
+ GROUP BY r.from_id, r.to_id, r.call_type`
8906
9261
  ).all(...pkgFilePaths, ...pkgFilePaths);
8907
9262
  const knownSymIds = new Set(pkgSyms.map((s) => s.id));
8908
9263
  const crossRefIds = /* @__PURE__ */ new Set();
@@ -8912,7 +9267,7 @@ var IndexStore = class _IndexStore {
8912
9267
  }
8913
9268
  if (crossRefIds.size > 0) {
8914
9269
  const crossPlaceholders = [...crossRefIds].map(() => "?").join(",");
8915
- const extras = this.db.prepare(
9270
+ const extras = this.stmt(
8916
9271
  `SELECT id, file FROM symbols WHERE id IN (${crossPlaceholders})`
8917
9272
  ).all(...crossRefIds);
8918
9273
  for (const x of extras) {
@@ -8937,14 +9292,16 @@ var IndexStore = class _IndexStore {
8937
9292
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
8938
9293
  edgeMap.set(key, e);
8939
9294
  }
8940
- e.weight++;
8941
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9295
+ const n = Number(r.n) || 0;
9296
+ e.weight += n;
9297
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
8942
9298
  }
8943
- const importRows = this.db.prepare(
8944
- `SELECT r.from_id, r.to_name
9299
+ const importRows = this.stmt(
9300
+ `SELECT r.from_id, r.to_name, COUNT(*) AS n
8945
9301
  FROM refs r
8946
9302
  WHERE r.call_type = 'import'
8947
- AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))`
9303
+ AND r.from_id IN (SELECT id FROM symbols WHERE file IN (${filePlaceholders}))
9304
+ GROUP BY r.from_id, r.to_name`
8948
9305
  ).all(...pkgFilePaths);
8949
9306
  for (const r of importRows) {
8950
9307
  const fromFile = symToFile.get(r.from_id);
@@ -8959,8 +9316,9 @@ var IndexStore = class _IndexStore {
8959
9316
  edge = { weight: 0, types: /* @__PURE__ */ new Map() };
8960
9317
  edgeMap.set(key, edge);
8961
9318
  }
8962
- edge.weight++;
8963
- edge.types.set("import", (edge.types.get("import") ?? 0) + 1);
9319
+ const n = Number(r.n) || 0;
9320
+ edge.weight += n;
9321
+ edge.types.set("import", (edge.types.get("import") ?? 0) + n);
8964
9322
  }
8965
9323
  const edges = [];
8966
9324
  for (const [key, e] of edgeMap) {
@@ -8987,7 +9345,7 @@ var IndexStore = class _IndexStore {
8987
9345
  * derived from intra-file and cross-file symbol references (who calls whom).
8988
9346
  */
8989
9347
  getSymbolGraph(fileFilter) {
8990
- const syms = this.db.prepare(
9348
+ const syms = this.stmt(
8991
9349
  "SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE file = ? ORDER BY line, id"
8992
9350
  ).all(fileFilter);
8993
9351
  if (syms.length === 0) return { nodes: [], edges: [] };
@@ -9007,17 +9365,25 @@ var IndexStore = class _IndexStore {
9007
9365
  scope: s.scope,
9008
9366
  external: s.file !== fileFilter
9009
9367
  });
9010
- const symIds = new Set(syms.map((s) => s.id));
9011
- const refRows = this.db.prepare(
9012
- `SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9013
- FROM refs r
9014
- WHERE r.from_id IN (SELECT id FROM symbols WHERE file = ?)
9015
- OR r.to_id IN (SELECT id FROM symbols WHERE file = ?)`
9368
+ const refRows = this.stmt(
9369
+ `SELECT from_id, to_id, call_type, COUNT(*) AS n
9370
+ FROM (
9371
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9372
+ FROM refs r
9373
+ JOIN symbols s ON s.id = r.from_id
9374
+ WHERE s.file = ?
9375
+ UNION
9376
+ SELECT r.from_id, r.to_id, r.to_name, r.call_type, r.line
9377
+ FROM refs r
9378
+ JOIN symbols s ON s.id = r.to_id
9379
+ WHERE s.file = ?
9380
+ )
9381
+ WHERE to_id IS NOT NULL
9382
+ GROUP BY from_id, to_id, call_type`
9016
9383
  ).all(fileFilter, fileFilter);
9017
9384
  const edgeMap = /* @__PURE__ */ new Map();
9018
9385
  for (const r of refRows) {
9019
- if (r.to_id === null) continue;
9020
- if (!symIds.has(r.from_id) && !symIds.has(r.to_id)) continue;
9386
+ if (r.to_id == null) continue;
9021
9387
  relatedIds.add(r.from_id);
9022
9388
  relatedIds.add(r.to_id);
9023
9389
  const key = `${r.from_id}\0${r.to_id}`;
@@ -9026,8 +9392,9 @@ var IndexStore = class _IndexStore {
9026
9392
  e = { weight: 0, types: /* @__PURE__ */ new Map() };
9027
9393
  edgeMap.set(key, e);
9028
9394
  }
9029
- e.weight++;
9030
- e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + 1);
9395
+ const n = Number(r.n) || 0;
9396
+ e.weight += n;
9397
+ e.types.set(r.call_type, (e.types.get(r.call_type) ?? 0) + n);
9031
9398
  }
9032
9399
  const edges = [];
9033
9400
  for (const [key, e] of edgeMap) {
@@ -9051,7 +9418,7 @@ var IndexStore = class _IndexStore {
9051
9418
  const missingIds = [...relatedIds].filter((id) => !loadedIds.has(id));
9052
9419
  if (missingIds.length > 0) {
9053
9420
  const placeholders = missingIds.map(() => "?").join(",");
9054
- const extras = this.db.prepare(
9421
+ const extras = this.stmt(
9055
9422
  `SELECT id, name, kind, lang, file, line, signature, scope FROM symbols WHERE id IN (${placeholders})`
9056
9423
  ).all(...missingIds);
9057
9424
  for (const s of extras) symById.set(s.id, s);
@@ -9065,6 +9432,8 @@ var IndexStore = class _IndexStore {
9065
9432
  }
9066
9433
  close() {
9067
9434
  this.stmtCache.clear();
9435
+ this.bm25Dirty = true;
9436
+ this.bm25Cache = null;
9068
9437
  try {
9069
9438
  this.db.close();
9070
9439
  } catch {
@@ -9078,14 +9447,118 @@ import { fileURLToPath } from "node:url";
9078
9447
  import { Worker } from "node:worker_threads";
9079
9448
 
9080
9449
  // src/codebase-index/indexer.ts
9081
- import { expectDefined as expectDefined6 } from "@wrongstack/core";
9450
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9082
9451
  import * as fs14 from "node:fs/promises";
9083
- import * as path19 from "node:path";
9452
+ import * as path20 from "node:path";
9084
9453
  import { availableParallelism } from "node:os";
9085
- import { compileGlob as compileGlob2 } from "@wrongstack/core";
9454
+ import { DEFAULT_WALK_IGNORE_DIRS, indexParallelBatchSize, isFrugalPerf } from "@wrongstack/core/utils";
9086
9455
 
9087
9456
  // src/codebase-index/ts-parser.ts
9088
9457
  import * as ts from "@typescript/typescript6";
9458
+
9459
+ // src/codebase-index/languages.ts
9460
+ import * as path14 from "node:path";
9461
+ var EXT_TO_LANG = {
9462
+ // TypeScript / JavaScript (first-class TS compiler API)
9463
+ ".ts": "ts",
9464
+ ".mts": "ts",
9465
+ ".cts": "ts",
9466
+ ".tsx": "tsx",
9467
+ ".js": "js",
9468
+ ".mjs": "js",
9469
+ ".cjs": "js",
9470
+ ".jsx": "jsx",
9471
+ // First-class native/spawn parsers (+ regex fallback)
9472
+ ".go": "go",
9473
+ ".py": "py",
9474
+ ".pyi": "py",
9475
+ ".pyw": "py",
9476
+ ".rs": "rs",
9477
+ ".json": "json",
9478
+ ".jsonc": "json",
9479
+ ".yaml": "yaml",
9480
+ ".yml": "yaml",
9481
+ // C family
9482
+ ".c": "c",
9483
+ ".h": "c",
9484
+ ".cc": "cpp",
9485
+ ".cpp": "cpp",
9486
+ ".cxx": "cpp",
9487
+ ".hh": "cpp",
9488
+ ".hpp": "cpp",
9489
+ ".hxx": "cpp",
9490
+ // JVM / .NET
9491
+ ".java": "java",
9492
+ ".cs": "csharp",
9493
+ ".kt": "kotlin",
9494
+ ".kts": "kotlin",
9495
+ ".scala": "scala",
9496
+ ".sc": "scala",
9497
+ // Scripting
9498
+ ".php": "php",
9499
+ ".rb": "ruby",
9500
+ ".swift": "swift",
9501
+ ".dart": "dart",
9502
+ ".lua": "lua",
9503
+ ".r": "r",
9504
+ ".R": "r",
9505
+ ".pl": "other",
9506
+ ".pm": "other",
9507
+ // Systems / functional
9508
+ ".zig": "zig",
9509
+ ".ex": "elixir",
9510
+ ".exs": "elixir",
9511
+ ".hs": "haskell",
9512
+ ".lhs": "haskell",
9513
+ // Shell / data / docs / web
9514
+ ".sh": "shell",
9515
+ ".bash": "shell",
9516
+ ".zsh": "shell",
9517
+ ".ps1": "shell",
9518
+ ".sql": "sql",
9519
+ ".md": "md",
9520
+ ".mdx": "md",
9521
+ ".toml": "toml",
9522
+ ".html": "html",
9523
+ ".htm": "html",
9524
+ ".css": "css",
9525
+ ".scss": "css",
9526
+ ".less": "css",
9527
+ ".vue": "vue",
9528
+ ".svelte": "svelte",
9529
+ ".proto": "proto",
9530
+ ".graphql": "graphql",
9531
+ ".gql": "graphql"
9532
+ };
9533
+ var INDEXABLE_EXTENSIONS = Object.freeze(
9534
+ [...new Set(Object.keys(EXT_TO_LANG).map((e) => e.toLowerCase()))].sort()
9535
+ );
9536
+ var SPECIAL_FILENAMES = {
9537
+ makefile: "other",
9538
+ gnumakefile: "other",
9539
+ dockerfile: "other",
9540
+ "docker-compose.yml": "yaml",
9541
+ "docker-compose.yaml": "yaml",
9542
+ "cmakelists.txt": "other",
9543
+ gemfile: "ruby",
9544
+ rakefile: "ruby",
9545
+ procfile: "other",
9546
+ justfile: "other"
9547
+ };
9548
+ function detectLang(file) {
9549
+ const base = path14.basename(file);
9550
+ const lowerBase = base.toLowerCase();
9551
+ if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
9552
+ return "ts";
9553
+ }
9554
+ const special = SPECIAL_FILENAMES[lowerBase];
9555
+ if (special) return special;
9556
+ const ext = path14.extname(base).toLowerCase();
9557
+ if (!ext) return null;
9558
+ return EXT_TO_LANG[ext] ?? null;
9559
+ }
9560
+
9561
+ // src/codebase-index/ts-parser.ts
9089
9562
  var KIND_MAP = {
9090
9563
  [ts.SyntaxKind.ClassDeclaration]: "class",
9091
9564
  [ts.SyntaxKind.InterfaceDeclaration]: "interface",
@@ -9112,32 +9585,6 @@ function kindOf(node) {
9112
9585
  if (ts.isModuleDeclaration(node)) return "namespace";
9113
9586
  return KIND_MAP[node.kind] ?? null;
9114
9587
  }
9115
- function extToLang(ext) {
9116
- switch (ext) {
9117
- case ".ts":
9118
- return "ts";
9119
- case ".tsx":
9120
- return "tsx";
9121
- case ".js":
9122
- return "js";
9123
- case ".jsx":
9124
- return "jsx";
9125
- case ".go":
9126
- return "go";
9127
- case ".py":
9128
- return "py";
9129
- case ".rs":
9130
- return "rs";
9131
- case ".json":
9132
- return "json";
9133
- case ".yaml":
9134
- return "yaml";
9135
- case ".yml":
9136
- return "yaml";
9137
- default:
9138
- return null;
9139
- }
9140
- }
9141
9588
  function getSignature(printer, node, sourceFile) {
9142
9589
  const raw = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
9143
9590
  return raw.replace(/\s+/g, " ").slice(0, 500);
@@ -9260,22 +9707,30 @@ function deduplicateRefs(refs) {
9260
9707
  return true;
9261
9708
  });
9262
9709
  }
9263
- function detectLang(file) {
9264
- const idx = file.lastIndexOf(".");
9265
- if (idx < 0) return null;
9266
- return extToLang(file.slice(idx));
9267
- }
9268
9710
 
9269
9711
  // src/codebase-index/go-parser.ts
9270
9712
  init_win32_resolve();
9271
9713
  import { spawn as spawn4 } from "node:child_process";
9272
9714
  import * as os5 from "node:os";
9273
- import * as path14 from "node:path";
9715
+ import * as path15 from "node:path";
9274
9716
  import * as fs10 from "node:fs/promises";
9717
+
9718
+ // src/codebase-index/spawn-gate.ts
9719
+ var chain = Promise.resolve();
9720
+ function withSpawnGate(fn) {
9721
+ const run = chain.then(fn, fn);
9722
+ chain = run.then(
9723
+ () => void 0,
9724
+ () => void 0
9725
+ );
9726
+ return run;
9727
+ }
9728
+
9729
+ // src/codebase-index/go-parser.ts
9275
9730
  async function parseSymbols2(opts) {
9276
9731
  const { file, content, lang } = opts;
9277
9732
  try {
9278
- const parsed = await syncGoParse(file, content, lang);
9733
+ const parsed = await withSpawnGate(() => syncGoParse(file, content, lang));
9279
9734
  if (parsed.symbols.length > 0) {
9280
9735
  return parsed;
9281
9736
  }
@@ -9571,11 +10026,16 @@ func formatType(t ast.Expr) string {
9571
10026
  }
9572
10027
  }
9573
10028
  `;
10029
+ var _cachedGoScriptPath = null;
9574
10030
  async function syncGoParse(filePath, content, lang) {
9575
- const tmpDir = await fs10.mkdtemp(path14.join(os5.tmpdir(), "ws-go-parse-"));
9576
10031
  try {
9577
- const scriptPath = path14.join(tmpDir, "parse.go");
9578
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10032
+ let scriptPath = _cachedGoScriptPath;
10033
+ if (!scriptPath) {
10034
+ const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
10035
+ scriptPath = path15.join(tmpDir, "parse.go");
10036
+ await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
10037
+ _cachedGoScriptPath = scriptPath;
10038
+ }
9579
10039
  const goBinary = resolveWin32Command("go");
9580
10040
  const goResult = await new Promise(
9581
10041
  (resolve14, reject) => {
@@ -9632,24 +10092,312 @@ async function syncGoParse(filePath, content, lang) {
9632
10092
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9633
10093
  } catch {
9634
10094
  return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9635
- } finally {
9636
- await fs10.rm(tmpDir, { recursive: true, force: true });
9637
10095
  }
9638
10096
  }
9639
10097
 
9640
10098
  // src/codebase-index/py-parser.ts
9641
10099
  init_win32_resolve();
9642
- import { spawn as spawn5, spawnSync } from "node:child_process";
10100
+ import { spawn as spawn5 } from "node:child_process";
9643
10101
  import * as fs11 from "node:fs/promises";
9644
10102
  import * as os6 from "node:os";
9645
- import * as path15 from "node:path";
10103
+ import * as path16 from "node:path";
10104
+
10105
+ // src/codebase-index/generic-parser.ts
10106
+ var C_LIKE = [
10107
+ { re: /\b(?:class|struct|enum|interface|union)\s+([A-Za-z_]\w*)/g, kind: "class" },
10108
+ {
10109
+ re: /\b(?:public|private|protected|static|final|async|override|virtual|inline|export)?\s*(?:[\w:<>[\]\s*&]+)\s+([A-Za-z_]\w*)\s*\([^;{]*\)\s*(?:const)?\s*[{;]/g,
10110
+ kind: "function"
10111
+ },
10112
+ { re: /\b(?:namespace)\s+([A-Za-z_]\w*)/g, kind: "namespace" }
10113
+ ];
10114
+ var LANG_PATTERNS = {
10115
+ py: [
10116
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10117
+ { re: /^class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10118
+ { re: /^([A-Za-z_]\w*)\s*=/gm, kind: "var" }
10119
+ ],
10120
+ go: [
10121
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(/gm, kind: "function" },
10122
+ { re: /^type\s+([A-Za-z_]\w*)\b/gm, kind: "type" },
10123
+ { re: /^(?:const|var)\s+([A-Za-z_]\w*)\b/gm, kind: "const" },
10124
+ { re: /^package\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10125
+ ],
10126
+ rs: [
10127
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10128
+ { re: /\bstruct\s+([A-Za-z_]\w*)/g, kind: "struct" },
10129
+ { re: /\benum\s+([A-Za-z_]\w*)/g, kind: "enum" },
10130
+ { re: /\btrait\s+([A-Za-z_]\w*)/g, kind: "trait" },
10131
+ { re: /\bimpl(?:\s*<[^>]+>)?\s+([A-Za-z_]\w*)/g, kind: "impl" },
10132
+ { re: /\b(?:const|static)\s+([A-Za-z_]\w*)/g, kind: "const" },
10133
+ { re: /\bmod\s+([A-Za-z_]\w*)/g, kind: "mod" }
10134
+ ],
10135
+ c: C_LIKE,
10136
+ cpp: C_LIKE,
10137
+ java: [
10138
+ { re: /\b(?:class|interface|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10139
+ {
10140
+ re: /\b(?:public|private|protected|static|final|abstract|synchronized|native|default|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10141
+ kind: "method"
10142
+ }
10143
+ ],
10144
+ csharp: [
10145
+ { re: /\b(?:class|interface|struct|enum|record)\s+([A-Za-z_]\w*)/g, kind: "class" },
10146
+ { re: /\bnamespace\s+([A-Za-z_.\w]+)/g, kind: "namespace" },
10147
+ {
10148
+ re: /\b(?:public|private|protected|internal|static|async|override|virtual|\s)+\s*[\w.<>,[\]\s]+\s+([A-Za-z_]\w*)\s*\(/g,
10149
+ kind: "method"
10150
+ }
10151
+ ],
10152
+ php: [
10153
+ { re: /\bfunction\s+([A-Za-z_]\w*)/g, kind: "function" },
10154
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" },
10155
+ { re: /\binterface\s+([A-Za-z_]\w*)/g, kind: "interface" },
10156
+ { re: /\bnamespace\s+([A-Za-z_\\]+)/g, kind: "namespace" }
10157
+ ],
10158
+ ruby: [
10159
+ { re: /^\s*def\s+(?:self\.)?([A-Za-z_]\w*[!?]?)/gm, kind: "function" },
10160
+ { re: /^\s*class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10161
+ { re: /^\s*module\s+([A-Za-z_]\w*)/gm, kind: "namespace" }
10162
+ ],
10163
+ swift: [
10164
+ { re: /\b(?:func)\s+([A-Za-z_]\w*)/g, kind: "function" },
10165
+ { re: /\b(?:class|struct|enum|protocol|actor)\s+([A-Za-z_]\w*)/g, kind: "class" }
10166
+ ],
10167
+ kotlin: [
10168
+ { re: /\b(?:fun)\s+([A-Za-z_]\w*)/g, kind: "function" },
10169
+ { re: /\b(?:class|interface|object|enum\s+class|data\s+class)\s+([A-Za-z_]\w*)/g, kind: "class" }
10170
+ ],
10171
+ scala: [
10172
+ { re: /\b(?:def)\s+([A-Za-z_]\w*)/g, kind: "function" },
10173
+ { re: /\b(?:class|object|trait|enum)\s+([A-Za-z_]\w*)/g, kind: "class" }
10174
+ ],
10175
+ shell: [
10176
+ { re: /^(?:function\s+)?([A-Za-z_]\w*)\s*\(\)\s*\{/gm, kind: "function" },
10177
+ { re: /^([A-Za-z_][\w]*)\s*\(\)\s*\{/gm, kind: "function" }
10178
+ ],
10179
+ sql: [
10180
+ { re: /\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|VIEW|INDEX|FUNCTION|PROCEDURE|TRIGGER)\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_"][\w."]*)/gi, kind: "type" }
10181
+ ],
10182
+ md: [
10183
+ { re: /^(#{1,6})\s+(.+)$/gm, kind: "namespace" }
10184
+ ],
10185
+ toml: [
10186
+ { re: /^\[([^\]]+)\]/gm, kind: "namespace" }
10187
+ ],
10188
+ html: [
10189
+ { re: /\bid\s*=\s*["']([^"']+)["']/gi, kind: "property" },
10190
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10191
+ ],
10192
+ css: [
10193
+ { re: /^\s*([.#]?[A-Za-z_][\w-]*)\s*\{/gm, kind: "type" },
10194
+ { re: /@(?:keyframes|media|supports)\s+([^{\s]+)/g, kind: "namespace" }
10195
+ ],
10196
+ vue: [
10197
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" },
10198
+ { re: /<(?:script|template|style)\b/gi, kind: "namespace" }
10199
+ ],
10200
+ svelte: [
10201
+ { re: /\b(?:function|const|let|var|class|export\s+(?:default\s+)?(?:function|class|const))\s+([A-Za-z_]\w*)/g, kind: "function" }
10202
+ ],
10203
+ dart: [
10204
+ { re: /\b(?:class|enum|mixin|extension)\s+([A-Za-z_]\w*)/g, kind: "class" },
10205
+ { re: /\b([A-Za-z_]\w*)\s*\([^;]*\)\s*(?:async\s*)?\{/g, kind: "function" }
10206
+ ],
10207
+ lua: [
10208
+ { re: /\bfunction\s+([A-Za-z_.:]\w*)/g, kind: "function" },
10209
+ { re: /\blocal\s+function\s+([A-Za-z_]\w*)/g, kind: "function" }
10210
+ ],
10211
+ r: [
10212
+ { re: /([A-Za-z.]\w*)\s*<-\s*function\s*\(/g, kind: "function" },
10213
+ { re: /([A-Za-z.]\w*)\s*=\s*function\s*\(/g, kind: "function" }
10214
+ ],
10215
+ proto: [
10216
+ { re: /\b(?:message|service|enum)\s+([A-Za-z_]\w*)/g, kind: "type" },
10217
+ { re: /\brpc\s+([A-Za-z_]\w*)/g, kind: "function" }
10218
+ ],
10219
+ graphql: [
10220
+ { re: /\b(?:type|interface|enum|input|union|scalar)\s+([A-Za-z_]\w*)/g, kind: "type" },
10221
+ { re: /\b(?:query|mutation|subscription)\s+([A-Za-z_]\w*)/g, kind: "function" }
10222
+ ],
10223
+ zig: [
10224
+ { re: /\b(?:fn|pub\s+fn)\s+([A-Za-z_]\w*)/g, kind: "function" },
10225
+ { re: /\b(?:const|var)\s+([A-Za-z_]\w*)/g, kind: "const" },
10226
+ { re: /\b(?:struct|enum|union)\s*\{/g, kind: "type" }
10227
+ ],
10228
+ elixir: [
10229
+ { re: /\bdef(?:p|macro|macrop)?\s+([A-Za-z_]\w*[!?]?)/g, kind: "function" },
10230
+ { re: /\bdefmodule\s+([A-Za-z_.]\w*)/g, kind: "namespace" }
10231
+ ],
10232
+ haskell: [
10233
+ { re: /^([A-Za-z_]\w*)\s*::/gm, kind: "function" },
10234
+ { re: /\bdata\s+([A-Za-z_]\w*)/g, kind: "type" },
10235
+ { re: /\btype\s+(?:family\s+)?([A-Za-z_]\w*)/g, kind: "type" },
10236
+ { re: /\bclass\s+([A-Za-z_]\w*)/g, kind: "class" }
10237
+ ],
10238
+ other: [
10239
+ { re: /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/gm, kind: "function" },
10240
+ { re: /^(?:export\s+)?class\s+([A-Za-z_]\w*)/gm, kind: "class" },
10241
+ { re: /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_]\w*)/gm, kind: "const" },
10242
+ { re: /^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm, kind: "function" },
10243
+ { re: /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/gm, kind: "function" },
10244
+ { re: /\bfn\s+([A-Za-z_]\w*)/g, kind: "function" },
10245
+ { re: /^(?:target|PHONY)\s*:/gm, kind: "namespace" }
10246
+ ]
10247
+ };
10248
+ var KEYWORDS = /* @__PURE__ */ new Set([
10249
+ "if",
10250
+ "else",
10251
+ "for",
10252
+ "while",
10253
+ "switch",
10254
+ "case",
10255
+ "return",
10256
+ "break",
10257
+ "continue",
10258
+ "new",
10259
+ "delete",
10260
+ "typeof",
10261
+ "instanceof",
10262
+ "void",
10263
+ "null",
10264
+ "true",
10265
+ "false",
10266
+ "this",
10267
+ "super",
10268
+ "import",
10269
+ "export",
10270
+ "from",
10271
+ "as",
10272
+ "default",
10273
+ "public",
10274
+ "private",
10275
+ "protected",
10276
+ "static",
10277
+ "final",
10278
+ "class",
10279
+ "struct",
10280
+ "enum",
10281
+ "interface",
10282
+ "function",
10283
+ "def",
10284
+ "fn",
10285
+ "func",
10286
+ "var",
10287
+ "let",
10288
+ "const",
10289
+ "package",
10290
+ "namespace",
10291
+ "module",
10292
+ "using",
10293
+ "include",
10294
+ "require",
10295
+ "select",
10296
+ "from",
10297
+ "where",
10298
+ "and",
10299
+ "or",
10300
+ "not",
10301
+ "in",
10302
+ "is",
10303
+ "try",
10304
+ "catch",
10305
+ "finally",
10306
+ "throw",
10307
+ "async",
10308
+ "await",
10309
+ "yield"
10310
+ ]);
10311
+ function patternsFor(lang) {
10312
+ return LANG_PATTERNS[lang] ?? LANG_PATTERNS.other ?? [];
10313
+ }
10314
+ function looksBinary(content) {
10315
+ if (content.includes("\0")) return true;
10316
+ const sample = content.slice(0, 2048);
10317
+ if (sample.length === 0) return false;
10318
+ let bad = 0;
10319
+ for (let i = 0; i < sample.length; i++) {
10320
+ const c = sample.charCodeAt(i);
10321
+ if (c === 9 || c === 10 || c === 13) continue;
10322
+ if (c < 32 || c === 127) bad++;
10323
+ }
10324
+ return bad / sample.length > 0.1;
10325
+ }
10326
+ function lineColAt(content, index) {
10327
+ let line = 1;
10328
+ let lastNl = -1;
10329
+ for (let i = 0; i < index && i < content.length; i++) {
10330
+ if (content.charCodeAt(i) === 10) {
10331
+ line++;
10332
+ lastNl = i;
10333
+ }
10334
+ }
10335
+ return { line, col: index - lastNl };
10336
+ }
10337
+ var GENERIC_MAX_SYMBOLS_DEFAULT = 500;
10338
+ var GENERIC_MAX_FILE_CHARS = 512 * 1024;
10339
+ function parseGeneric2(opts) {
10340
+ const { file, lang } = opts;
10341
+ const maxSymbols = opts.maxSymbols ?? GENERIC_MAX_SYMBOLS_DEFAULT;
10342
+ const mtimeMs = Date.now();
10343
+ if (!opts.content || looksBinary(opts.content)) {
10344
+ return { file, lang, symbols: [], mtimeMs };
10345
+ }
10346
+ const content = opts.content.length > GENERIC_MAX_FILE_CHARS ? opts.content.slice(0, GENERIC_MAX_FILE_CHARS) : opts.content;
10347
+ const patterns = patternsFor(lang);
10348
+ const symbols = [];
10349
+ const seen = /* @__PURE__ */ new Set();
10350
+ for (const pattern of patterns) {
10351
+ const re = new RegExp(pattern.re.source, pattern.re.flags.includes("g") ? pattern.re.flags : `${pattern.re.flags}g`);
10352
+ re.lastIndex = 0;
10353
+ for (const match of content.matchAll(re)) {
10354
+ if (symbols.length >= maxSymbols) break;
10355
+ let name = (match[1] ?? match[2] ?? "").trim();
10356
+ if (lang === "md" && match[2]) name = match[2].trim();
10357
+ if (!name || name.length > 200) continue;
10358
+ name = name.replace(/^#+\s*/, "").replace(/["'`]/g, "");
10359
+ if (!name || KEYWORDS.has(name.toLowerCase())) continue;
10360
+ if (!/^[A-Za-z_#.@/\w][\w.\-:/#!?]*$/.test(name) && lang !== "md" && lang !== "toml") {
10361
+ continue;
10362
+ }
10363
+ const { line, col } = lineColAt(content, match.index);
10364
+ const key = `${name}\0${line}\0${pattern.kind}`;
10365
+ if (seen.has(key)) continue;
10366
+ seen.add(key);
10367
+ const nl = content.indexOf("\n", match.index);
10368
+ const lineText = content.slice(match.index, nl === -1 ? content.length : nl);
10369
+ const signature = (lineText || name).trim().slice(0, 500);
10370
+ symbols.push({
10371
+ id: 0,
10372
+ lang,
10373
+ kind: lang === "md" ? "namespace" : pattern.kind,
10374
+ name: name.slice(0, 200),
10375
+ file,
10376
+ line,
10377
+ col,
10378
+ signature,
10379
+ docComment: "",
10380
+ scope: "",
10381
+ text: `${name} ${signature}`.trim().slice(0, 1e3)
10382
+ });
10383
+ }
10384
+ if (symbols.length >= maxSymbols) break;
10385
+ }
10386
+ return { file, lang, symbols, mtimeMs };
10387
+ }
9646
10388
  async function parseSymbols3(opts) {
10389
+ return parseGeneric2(opts);
10390
+ }
10391
+
10392
+ // src/codebase-index/py-parser.ts
10393
+ async function parseSymbols4(opts) {
9647
10394
  const { file, content, lang } = opts;
9648
10395
  try {
9649
- return await syncPyParse(file, content, lang);
10396
+ const native = await withSpawnGate(() => syncPyParse(file, content, lang));
10397
+ if (native !== null) return native;
9650
10398
  } catch {
9651
- return { file, lang, symbols: [], mtimeMs: Date.now() };
9652
10399
  }
10400
+ return parseGeneric2({ file, content, lang: lang === "py" ? "py" : lang });
9653
10401
  }
9654
10402
  var PY_PARSE_SCRIPT = `import ast, json, sys, os
9655
10403
 
@@ -9854,20 +10602,37 @@ visitor.visit(tree)
9854
10602
 
9855
10603
  print(json.dumps([s.to_dict() for s in syms]))
9856
10604
  `;
9857
- function resolvePython() {
10605
+ async function resolvePython() {
9858
10606
  const candidates = process.platform === "win32" ? ["python3", "python", "py"] : ["python3", "python"];
9859
10607
  for (const name of candidates) {
9860
10608
  const resolved = resolveWin32Command(name);
9861
- const result = spawnSync(resolved, ["--version"], {
9862
- stdio: "pipe",
9863
- timeout: 5e3
9864
- });
9865
- if (result.error) continue;
9866
- if (result.status !== 0) continue;
10609
+ if (!await commandIsAvailable(resolved)) continue;
9867
10610
  return resolved;
9868
10611
  }
9869
10612
  return null;
9870
10613
  }
10614
+ function commandIsAvailable(command) {
10615
+ return new Promise((resolve14) => {
10616
+ let settled = false;
10617
+ const proc = spawn5(command, ["--version"], {
10618
+ stdio: "ignore",
10619
+ windowsHide: true
10620
+ });
10621
+ const finish = (available) => {
10622
+ if (settled) return;
10623
+ settled = true;
10624
+ clearTimeout(timer);
10625
+ resolve14(available);
10626
+ };
10627
+ const timer = setTimeout(() => {
10628
+ proc.kill("SIGKILL");
10629
+ finish(false);
10630
+ }, 5e3);
10631
+ timer.unref?.();
10632
+ proc.once("error", () => finish(false));
10633
+ proc.once("close", (code) => finish(code === 0));
10634
+ });
10635
+ }
9871
10636
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9872
10637
  return new Promise((resolve14, reject) => {
9873
10638
  let settled = false;
@@ -9903,21 +10668,20 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
9903
10668
  });
9904
10669
  }
9905
10670
  var _cachedScriptPath = null;
9906
- var _cachedPyBinary = null;
10671
+ var cachedPyBinary;
9907
10672
  async function syncPyParse(filePath, content, lang) {
9908
10673
  try {
9909
10674
  if (!_cachedScriptPath) {
9910
- const tmpDir = path15.join(os6.tmpdir(), "ws-py-parse");
10675
+ const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
9911
10676
  await fs11.mkdir(tmpDir, { recursive: true });
9912
- _cachedScriptPath = path15.join(tmpDir, "parse.py");
10677
+ _cachedScriptPath = path16.join(tmpDir, "parse.py");
9913
10678
  await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
9914
10679
  }
9915
- if (!_cachedPyBinary) {
9916
- _cachedPyBinary = resolvePython();
9917
- if (!_cachedPyBinary) return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
9918
- }
10680
+ cachedPyBinary ??= resolvePython();
10681
+ const pyBinary = await cachedPyBinary;
10682
+ if (!pyBinary) return null;
9919
10683
  const { code, stdout } = await spawnPyParser(
9920
- _cachedPyBinary,
10684
+ pyBinary,
9921
10685
  _cachedScriptPath,
9922
10686
  filePath,
9923
10687
  content
@@ -9941,31 +10705,40 @@ async function syncPyParse(filePath, content, lang) {
9941
10705
  }));
9942
10706
  return { file: filePath, lang, symbols, mtimeMs: Date.now() };
9943
10707
  } catch {
9944
- return { file: filePath, lang, symbols: [], mtimeMs: Date.now() };
10708
+ return null;
9945
10709
  }
9946
10710
  }
9947
10711
 
9948
10712
  // src/codebase-index/rs-parser.ts
9949
10713
  init_win32_resolve();
9950
- import { expectDefined as expectDefined3 } from "@wrongstack/core";
9951
- import { execFileSync, spawn as spawn6 } from "node:child_process";
10714
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
10715
+ import { execFile, spawn as spawn6 } from "node:child_process";
9952
10716
  import * as fs12 from "node:fs/promises";
9953
- import * as path16 from "node:path";
9954
- async function parseSymbols4(opts) {
10717
+ import * as path17 from "node:path";
10718
+ async function parseSymbols5(opts) {
9955
10719
  const { file, content, lang } = opts;
9956
- const nativeAvailable = checkNativeParser();
10720
+ const nativeAvailable = await checkNativeParser();
9957
10721
  if (nativeAvailable) {
9958
- const result = await tryNativeParse(file, content);
10722
+ const result = await withSpawnGate(() => tryNativeParse(file, content));
9959
10723
  if (result) return result;
9960
10724
  }
9961
10725
  return regexParse({ file, content, lang });
9962
10726
  }
10727
+ var nativeParserAvailability;
10728
+ function probe(command, args) {
10729
+ return new Promise((resolve14, reject) => {
10730
+ execFile(command, args, { timeout: 1e4, windowsHide: true }, (error) => {
10731
+ if (error) reject(error);
10732
+ else resolve14();
10733
+ });
10734
+ });
10735
+ }
9963
10736
  function checkNativeParser() {
9964
- try {
9965
- execFileSync("rustc", ["--version"], { stdio: "pipe", windowsHide: true });
9966
- const toolsDir = path16.join(process.cwd(), "tools");
10737
+ nativeParserAvailability ??= (async () => {
9967
10738
  try {
9968
- execFileSync(
10739
+ await probe("rustc", ["--version"]);
10740
+ const toolsDir = path17.join(process.cwd(), "tools");
10741
+ await probe(
9969
10742
  "cargo",
9970
10743
  [
9971
10744
  "metadata",
@@ -9973,23 +10746,21 @@ function checkNativeParser() {
9973
10746
  "--format-version",
9974
10747
  "1",
9975
10748
  "--manifest-path",
9976
- path16.join(toolsDir, "Cargo.toml")
9977
- ],
9978
- { stdio: "pipe", windowsHide: true }
10749
+ path17.join(toolsDir, "Cargo.toml")
10750
+ ]
9979
10751
  );
9980
10752
  return true;
9981
10753
  } catch {
9982
10754
  return false;
9983
10755
  }
9984
- } catch {
9985
- return false;
9986
- }
10756
+ })();
10757
+ return nativeParserAvailability;
9987
10758
  }
9988
10759
  async function tryNativeParse(file, content) {
9989
10760
  try {
9990
- const toolsDir = path16.join(process.cwd(), "tools");
9991
- const crateDir = path16.join(toolsDir, "syn-parser");
9992
- const tmpFile = path16.join(crateDir, "src", "input.rs");
10761
+ const toolsDir = path17.join(process.cwd(), "tools");
10762
+ const crateDir = path17.join(toolsDir, "syn-parser");
10763
+ const tmpFile = path17.join(crateDir, "src", "input.rs");
9993
10764
  await fs12.writeFile(tmpFile, content, "utf8");
9994
10765
  const cargoBinary = resolveWin32Command("cargo");
9995
10766
  const result = await new Promise(
@@ -9997,7 +10768,7 @@ async function tryNativeParse(file, content) {
9997
10768
  let settled = false;
9998
10769
  const proc = spawn6(
9999
10770
  cargoBinary,
10000
- ["run", "--manifest-path", path16.join(toolsDir, "Cargo.toml")],
10771
+ ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
10001
10772
  {
10002
10773
  cwd: process.cwd(),
10003
10774
  stdio: ["pipe", "pipe", "pipe"],
@@ -10111,9 +10882,9 @@ function regexParse(opts) {
10111
10882
  }
10112
10883
 
10113
10884
  // src/codebase-index/json-parser.ts
10114
- import { expectDefined as expectDefined4 } from "@wrongstack/core";
10115
- import * as path17 from "node:path";
10116
- function parseSymbols5(opts) {
10885
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
10886
+ import * as path18 from "node:path";
10887
+ function parseSymbols6(opts) {
10117
10888
  const { file, content, lang } = opts;
10118
10889
  try {
10119
10890
  return regexParse2({ file, content, lang });
@@ -10124,9 +10895,9 @@ function parseSymbols5(opts) {
10124
10895
  function regexParse2(opts) {
10125
10896
  const { file, content, lang } = opts;
10126
10897
  const symbols = [];
10127
- const basename10 = path17.basename(file).toLowerCase();
10128
- const isPackageJson = basename10 === "package.json";
10129
- const isTsconfig = basename10 === "tsconfig.json" || basename10 === "tsconfig.build.json";
10898
+ const basename11 = path18.basename(file).toLowerCase();
10899
+ const isPackageJson = basename11 === "package.json";
10900
+ const isTsconfig = basename11 === "tsconfig.json" || basename11 === "tsconfig.build.json";
10130
10901
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
10131
10902
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
10132
10903
  const lines = content.split("\n");
@@ -10150,11 +10921,11 @@ function regexParse2(opts) {
10150
10921
  const line = lineFromOffset(offset);
10151
10922
  symbols.push(
10152
10923
  makeSymbol({
10153
- name: path17.basename(file),
10924
+ name: path18.basename(file),
10154
10925
  kind: "object",
10155
10926
  line,
10156
10927
  col: 0,
10157
- signature: `"${path17.basename(file)}" = { ... }`,
10928
+ signature: `"${path18.basename(file)}" = { ... }`,
10158
10929
  file,
10159
10930
  lang
10160
10931
  })
@@ -10316,8 +11087,8 @@ function makeSymbol(opts) {
10316
11087
  }
10317
11088
 
10318
11089
  // src/codebase-index/yaml-parser.ts
10319
- import { expectDefined as expectDefined5, truncate } from "@wrongstack/core";
10320
- function parseSymbols6(opts) {
11090
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
11091
+ function parseSymbols7(opts) {
10321
11092
  const { file, content, lang } = opts;
10322
11093
  try {
10323
11094
  return regexParse3({ file, content, lang });
@@ -10466,8 +11237,8 @@ function makeSymbol2(opts) {
10466
11237
 
10467
11238
  // src/codebase-index/gitignore.ts
10468
11239
  import * as fs13 from "node:fs/promises";
10469
- import * as path18 from "node:path";
10470
- import { compileGlob } from "@wrongstack/core";
11240
+ import * as path19 from "node:path";
11241
+ import { compileGlob } from "@wrongstack/core/utils";
10471
11242
  function globBody(glob) {
10472
11243
  return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
10473
11244
  }
@@ -10512,7 +11283,7 @@ function compileGitignore(lines) {
10512
11283
  async function loadGitignoreMatcher(projectRoot) {
10513
11284
  let lines = [];
10514
11285
  try {
10515
- const raw = await fs13.readFile(path18.join(projectRoot, ".gitignore"), "utf8");
11286
+ const raw = await fs13.readFile(path19.join(projectRoot, ".gitignore"), "utf8");
10516
11287
  lines = raw.split("\n");
10517
11288
  } catch {
10518
11289
  }
@@ -10521,7 +11292,9 @@ async function loadGitignoreMatcher(projectRoot) {
10521
11292
 
10522
11293
  // src/codebase-index/indexer.ts
10523
11294
  var YIELD_EVERY_N = 50;
10524
- var PARALLEL_BATCH = Math.min(availableParallelism() * 4, 40);
11295
+ function resolveParallelBatch() {
11296
+ return indexParallelBatchSize(availableParallelism());
11297
+ }
10525
11298
  function yieldEventLoop() {
10526
11299
  return new Promise((resolve14) => setImmediate(resolve14));
10527
11300
  }
@@ -10533,22 +11306,12 @@ function throwIfAborted(signal) {
10533
11306
  function isAbortError(err) {
10534
11307
  return err instanceof DOMException && err.name === "AbortError";
10535
11308
  }
10536
- var DEFAULT_IGNORE = [
10537
- "node_modules",
10538
- ".git",
10539
- "dist",
10540
- "build",
10541
- ".next",
10542
- "coverage",
10543
- ".turbo",
10544
- "__snapshots__",
10545
- ".nyc_output"
10546
- ];
11309
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
10547
11310
  var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
10548
11311
  var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
10549
11312
  function isWithinProject(projectRoot, file) {
10550
- const rel = path19.relative(projectRoot, file);
10551
- return rel !== "" && !rel.startsWith(`..${path19.sep}`) && rel !== ".." && !path19.isAbsolute(rel);
11313
+ const rel = path20.relative(projectRoot, file);
11314
+ return rel !== "" && !rel.startsWith(`..${path20.sep}`) && rel !== ".." && !path20.isAbsolute(rel);
10552
11315
  }
10553
11316
  function isMissingPathError(err) {
10554
11317
  const code = err?.code;
@@ -10559,18 +11322,7 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10559
11322
  const errors = [];
10560
11323
  let complete = true;
10561
11324
  const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
10562
- const globs = [
10563
- { ext: ".ts", pat: compileGlob2("**/*.ts") },
10564
- { ext: ".tsx", pat: compileGlob2("**/*.tsx") },
10565
- { ext: ".js", pat: compileGlob2("**/*.js") },
10566
- { ext: ".jsx", pat: compileGlob2("**/*.jsx") },
10567
- { ext: ".go", pat: compileGlob2("**/*.go") },
10568
- { ext: ".py", pat: compileGlob2("**/*.py") },
10569
- { ext: ".rs", pat: compileGlob2("**/*.rs") },
10570
- { ext: ".json", pat: compileGlob2("**/*.json") },
10571
- { ext: ".yaml", pat: compileGlob2("**/*.yaml") },
10572
- { ext: ".yml", pat: compileGlob2("**/*.yml") }
10573
- ];
11325
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
10574
11326
  let dirCount = 0;
10575
11327
  const walk = async (dir) => {
10576
11328
  throwIfAborted(signal);
@@ -10589,19 +11341,16 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10589
11341
  dirCount++;
10590
11342
  for (const e of entries) {
10591
11343
  if (ignoreSet.has(e.name)) continue;
10592
- const full = path19.join(dir, e.name);
10593
- const rel = path19.relative(projectRoot, full).replace(/\\/g, "/");
11344
+ const full = path20.join(dir, e.name);
11345
+ const rel = path20.relative(projectRoot, full).replace(/\\/g, "/");
10594
11346
  if (e.isDirectory()) {
10595
11347
  if (isGitIgnored(rel, true)) continue;
10596
11348
  await walk(full);
10597
11349
  } else if (e.isFile()) {
10598
11350
  if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
10599
- const ext = path19.extname(e.name).toLowerCase();
10600
- for (const { ext: extName, pat } of globs) {
10601
- if (ext === extName && (pat.test(rel) || pat.test(e.name))) {
10602
- results.push(full);
10603
- break;
10604
- }
11351
+ const ext = path20.extname(e.name).toLowerCase();
11352
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
11353
+ results.push(full);
10605
11354
  }
10606
11355
  }
10607
11356
  }
@@ -10610,24 +11359,25 @@ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
10610
11359
  return { files: results, complete, errors };
10611
11360
  }
10612
11361
  async function parseFile(file, content, lang) {
10613
- switch (lang) {
11362
+ const symbolLang = lang;
11363
+ switch (symbolLang) {
10614
11364
  case "ts":
10615
11365
  case "tsx":
10616
11366
  case "js":
10617
11367
  case "jsx":
10618
- return parseSymbols({ file, content, lang });
11368
+ return parseSymbols({ file, content, lang: symbolLang });
10619
11369
  case "go":
10620
11370
  return parseSymbols2({ file, content, lang: "go" });
10621
11371
  case "py":
10622
- return parseSymbols3({ file, content, lang: "py" });
11372
+ return parseSymbols4({ file, content, lang: "py" });
10623
11373
  case "rs":
10624
- return parseSymbols4({ file, content, lang: "rs" });
11374
+ return parseSymbols5({ file, content, lang: "rs" });
10625
11375
  case "json":
10626
- return parseSymbols5({ file, content, lang: "json" });
11376
+ return parseSymbols6({ file, content, lang: "json" });
10627
11377
  case "yaml":
10628
- return parseSymbols6({ file, content, lang: "yaml" });
11378
+ return parseSymbols7({ file, content, lang: "yaml" });
10629
11379
  default:
10630
- return { file, lang, symbols: [], mtimeMs: Date.now() };
11380
+ return parseSymbols3({ file, content, lang: symbolLang });
10631
11381
  }
10632
11382
  }
10633
11383
  function assignRefsToSymbols2(refs, symbols) {
@@ -10675,10 +11425,10 @@ async function runIndexerWithStore(store, opts) {
10675
11425
  let discoveredFiles = null;
10676
11426
  let discoveryComplete = true;
10677
11427
  if (opts.files && opts.files.length > 0) {
10678
- files = opts.files.map((f) => path19.resolve(projectRoot, f)).filter((f) => {
11428
+ files = opts.files.map((f) => path20.resolve(projectRoot, f)).filter((f) => {
10679
11429
  if (!isWithinProject(projectRoot, f)) return false;
10680
- const rel = path19.relative(projectRoot, f).replace(/\\/g, "/");
10681
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path19.basename(f)) && !isGitIgnored(rel, false);
11430
+ const rel = path20.relative(projectRoot, f).replace(/\\/g, "/");
11431
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path20.basename(f)) && !isGitIgnored(rel, false);
10682
11432
  });
10683
11433
  } else {
10684
11434
  const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
@@ -10699,15 +11449,19 @@ async function runIndexerWithStore(store, opts) {
10699
11449
  if (!force) {
10700
11450
  for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
10701
11451
  }
11452
+ const parallelBatch = resolveParallelBatch();
10702
11453
  let filesSinceLastYield = 0;
10703
- for (let batchStart = 0; batchStart < files.length; batchStart += PARALLEL_BATCH) {
10704
- const batchEnd = Math.min(batchStart + PARALLEL_BATCH, files.length);
11454
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11455
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
10705
11456
  const batchFiles = files.slice(batchStart, batchEnd);
10706
11457
  opts.onProgress?.(batchEnd, files.length);
10707
11458
  filesSinceLastYield += batchFiles.length;
10708
11459
  if (filesSinceLastYield >= YIELD_EVERY_N) {
10709
11460
  filesSinceLastYield = 0;
10710
11461
  await yieldEventLoop();
11462
+ if (isFrugalPerf()) {
11463
+ await new Promise((r) => setTimeout(r, 8));
11464
+ }
10711
11465
  throwIfAborted(signal);
10712
11466
  }
10713
11467
  const statOpts = signal ? { signal } : {};
@@ -10811,9 +11565,7 @@ async function runIndexerWithStore(store, opts) {
10811
11565
  continue;
10812
11566
  }
10813
11567
  if (parsed.symbols.length === 0) {
10814
- store.deleteRefsForFile(file);
10815
- store.deleteSymbolsForFile(file);
10816
- store.upsertFile({
11568
+ store.replaceEmptyFile({
10817
11569
  file,
10818
11570
  lang,
10819
11571
  mtimeMs: Math.floor(stat17.mtimeMs),
@@ -10882,6 +11634,7 @@ async function runIndexerWithStore(store, opts) {
10882
11634
  }
10883
11635
  store.resolveRefs();
10884
11636
  store.setMetadata("relation_graph_version", relationGraphVersion);
11637
+ store.optimize();
10885
11638
  const durationMs = Date.now() - startMs;
10886
11639
  store.setLastIndexed(Date.now());
10887
11640
  return {
@@ -11124,10 +11877,10 @@ async function callInline(op, args, opts) {
11124
11877
  opts.signal?.removeEventListener("abort", onOuterAbort);
11125
11878
  }
11126
11879
  }
11127
- var chain = Promise.resolve();
11880
+ var chain2 = Promise.resolve();
11128
11881
  function withMutex(job) {
11129
- const run = chain.then(job, job);
11130
- chain = run.then(
11882
+ const run = chain2.then(job, job);
11883
+ chain2 = run.then(
11131
11884
  () => void 0,
11132
11885
  () => void 0
11133
11886
  );
@@ -11476,7 +12229,7 @@ var codebaseStatsTool = {
11476
12229
 
11477
12230
  // src/design.ts
11478
12231
  import * as fs16 from "node:fs/promises";
11479
- import * as path20 from "node:path";
12232
+ import * as path21 from "node:path";
11480
12233
  import {
11481
12234
  applyTokenOverrides,
11482
12235
  getDesignKitLoader,
@@ -11485,23 +12238,24 @@ import {
11485
12238
  materializeTokens,
11486
12239
  recordKitChoice,
11487
12240
  recordOverrides,
12241
+ resolveSemanticTune,
11488
12242
  runDesignVerify,
11489
12243
  setActiveKit,
11490
12244
  setDesignOverrides
11491
- } from "@wrongstack/core";
12245
+ } from "@wrongstack/core/design";
11492
12246
  async function resolveReal(p) {
11493
- const resolved = path20.resolve(p);
11494
- let probe = resolved;
12247
+ const resolved = path21.resolve(p);
12248
+ let probe2 = resolved;
11495
12249
  const missing = [];
11496
12250
  for (; ; ) {
11497
12251
  try {
11498
- return path20.resolve(await fs16.realpath(probe), ...missing);
12252
+ return path21.resolve(await fs16.realpath(probe2), ...missing);
11499
12253
  } catch (err) {
11500
12254
  if (err.code === "ENOENT") {
11501
- const parent = path20.dirname(probe);
11502
- if (parent === probe) return resolved;
11503
- missing.unshift(path20.basename(probe));
11504
- probe = parent;
12255
+ const parent = path21.dirname(probe2);
12256
+ if (parent === probe2) return resolved;
12257
+ missing.unshift(path21.basename(probe2));
12258
+ probe2 = parent;
11505
12259
  continue;
11506
12260
  }
11507
12261
  return resolved;
@@ -11531,8 +12285,8 @@ var designTool = {
11531
12285
  properties: {
11532
12286
  action: {
11533
12287
  type: "string",
11534
- enum: ["list", "use", "foundations", "set", "materialize", "verify"],
11535
- description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; materialize = write tokens to a theme file; verify = scan UI for off-palette colors. Default: list."
12288
+ enum: ["list", "use", "foundations", "set", "tune", "materialize", "verify"],
12289
+ description: "list = menu; use = load+pin a kit; foundations = baseline; set = override colors/tokens; tune = high-level knobs (radius/density/font/motion); materialize = write tokens to a theme file; verify = scan UI for token drift. Default: list."
11536
12290
  },
11537
12291
  kit: {
11538
12292
  type: "string",
@@ -11548,6 +12302,16 @@ var designTool = {
11548
12302
  description: 'Token overrides for "set"/"use": { "primary": "oklch(\u2026)", "dark.bg": "#111" }. Bare key = both themes; "light."/"dark." prefix = that theme only. Empty value clears an override.',
11549
12303
  additionalProperties: { type: "string" }
11550
12304
  },
12305
+ tune: {
12306
+ type: "object",
12307
+ description: 'High-level knobs for "tune": { "radius": "lg", "density": "compact", "font": "Space Grotesk", "motion": "snappy" }. radius = none|sm|md|lg|xl|full or a base length; density = compact|cozy|comfortable; motion = snappy|smooth|none. Resolved to concrete token overrides.',
12308
+ properties: {
12309
+ radius: { type: "string" },
12310
+ density: { type: "string" },
12311
+ font: { type: "string" },
12312
+ motion: { type: "string" }
12313
+ }
12314
+ },
11551
12315
  out: {
11552
12316
  type: "string",
11553
12317
  description: "Materialize output path (project-relative). Defaults to a per-stack convention."
@@ -11645,6 +12409,28 @@ ${body}`
11645
12409
  These win over kit tokens. Run \`design {action:"materialize"}\` to write them to a theme file.`
11646
12410
  };
11647
12411
  }
12412
+ if (action === "tune") {
12413
+ const patch = resolveSemanticTune(input.tune ?? {});
12414
+ if (Object.keys(patch).length === 0) {
12415
+ return {
12416
+ action,
12417
+ output: 'No recognized knobs. Pass tune:{ radius:"lg", density:"compact", font:"\u2026", motion:"snappy" }.'
12418
+ };
12419
+ }
12420
+ const merged = await recordOverrides(ctx.projectRoot, patch, (/* @__PURE__ */ new Date()).toISOString());
12421
+ if (!merged) {
12422
+ return {
12423
+ action,
12424
+ output: 'No active kit. Pick one first: `design {action:"use", kit:"<id>"}`.'
12425
+ };
12426
+ }
12427
+ setDesignOverrides(ctx, merged);
12428
+ return {
12429
+ action,
12430
+ output: `Tuned. Resolved ${Object.keys(patch).length} token override(s): ${Object.entries(patch).map(([k, v]) => `${k}=${v}`).join(", ")}
12431
+ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme file.`
12432
+ };
12433
+ }
11648
12434
  if (action === "materialize") {
11649
12435
  const active = await loadActiveKit(ctx.projectRoot);
11650
12436
  if (!active) {
@@ -11666,11 +12452,11 @@ These win over kit tokens. Run \`design {action:"materialize"}\` to write them t
11666
12452
  outPath: input.out
11667
12453
  });
11668
12454
  const root = await resolveReal(ctx.projectRoot);
11669
- const absResolved = path20.isAbsolute(result.path) ? path20.resolve(result.path) : path20.resolve(path20.join(ctx.projectRoot, result.path));
11670
- const absParent = await resolveReal(path20.dirname(absResolved));
11671
- const abs = path20.join(absParent, path20.basename(absResolved));
11672
- const rel = path20.relative(root, abs);
11673
- if (rel.startsWith("..") || path20.isAbsolute(rel)) {
12455
+ const absResolved = path21.isAbsolute(result.path) ? path21.resolve(result.path) : path21.resolve(path21.join(ctx.projectRoot, result.path));
12456
+ const absParent = await resolveReal(path21.dirname(absResolved));
12457
+ const abs = path21.join(absParent, path21.basename(absResolved));
12458
+ const rel = path21.relative(root, abs);
12459
+ if (rel.startsWith("..") || path21.isAbsolute(rel)) {
11674
12460
  throw new Error(
11675
12461
  `design: materialize path "${result.path}" would escape the project root`
11676
12462
  );
@@ -11694,7 +12480,7 @@ ${result.content}
11694
12480
  \`\`\``
11695
12481
  };
11696
12482
  }
11697
- await fs16.mkdir(path20.dirname(abs), { recursive: true });
12483
+ await fs16.mkdir(path21.dirname(abs), { recursive: true });
11698
12484
  await fs16.writeFile(abs, result.content);
11699
12485
  return {
11700
12486
  action,
@@ -11746,8 +12532,8 @@ init_util();
11746
12532
  import { spawn as spawn7 } from "node:child_process";
11747
12533
  import { statSync as statSync2 } from "node:fs";
11748
12534
  import * as fs17 from "node:fs/promises";
11749
- import * as path21 from "node:path";
11750
- import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core";
12535
+ import * as path22 from "node:path";
12536
+ import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
11751
12537
  var diffTool = {
11752
12538
  name: "diff",
11753
12539
  category: "Filesystem",
@@ -11830,11 +12616,11 @@ function findGitDir(cwd) {
11830
12616
  let dir = cwd;
11831
12617
  for (let i = 0; i < 20; i++) {
11832
12618
  try {
11833
- const stat17 = statSync2(path21.join(dir, ".git"));
12619
+ const stat17 = statSync2(path22.join(dir, ".git"));
11834
12620
  if (stat17.isDirectory()) return dir;
11835
12621
  } catch {
11836
12622
  }
11837
- const parent = path21.dirname(dir);
12623
+ const parent = path22.dirname(dir);
11838
12624
  if (parent === dir) break;
11839
12625
  dir = parent;
11840
12626
  }
@@ -12048,7 +12834,7 @@ function processFile(content, absPath, _style, _overwrite, target) {
12048
12834
  // src/e2e.ts
12049
12835
  init_util();
12050
12836
  import { open, readdir as readdir5 } from "node:fs/promises";
12051
- import * as path22 from "node:path";
12837
+ import * as path23 from "node:path";
12052
12838
  async function readBoundedText(filePath, maxBytes) {
12053
12839
  let handle;
12054
12840
  try {
@@ -12098,8 +12884,8 @@ var MAX_PACKAGE_BYTES = 512 * 1024;
12098
12884
  var MAX_CONFIG_BYTES = 512 * 1024;
12099
12885
  var MAX_SPEC_SAMPLES = 20;
12100
12886
  function relativePath(root, target) {
12101
- const value = path22.relative(root, target) || ".";
12102
- return value.split(path22.sep).join("/");
12887
+ const value = path23.relative(root, target) || ".";
12888
+ return value.split(path23.sep).join("/");
12103
12889
  }
12104
12890
  function escapeRegExp(value) {
12105
12891
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -12171,7 +12957,7 @@ async function scanWorkspace(root, maxDepth, signal) {
12171
12957
  }
12172
12958
  for (const entry of entries) {
12173
12959
  signal.throwIfAborted();
12174
- const absolutePath = path22.join(current.directory, entry.name);
12960
+ const absolutePath = path23.join(current.directory, entry.name);
12175
12961
  if (entry.isFile()) {
12176
12962
  if (entry.name === "package.json") result.packageFiles.push(absolutePath);
12177
12963
  const framework = CONFIG_NAMES.get(entry.name);
@@ -12232,9 +13018,9 @@ async function detectPackageManager3(projectRoot, scanRoot, declared) {
12232
13018
  if (names.has("bun.lock") || names.has("bun.lockb")) return "bun";
12233
13019
  if (names.has("package-lock.json") || names.has("npm-shrinkwrap.json")) return "npm";
12234
13020
  if (directory === scanRoot) break;
12235
- const parent = path22.dirname(directory);
12236
- const relativeParent = path22.relative(scanRoot, parent);
12237
- if (parent === directory || relativeParent.startsWith("..") || path22.isAbsolute(relativeParent)) {
13021
+ const parent = path23.dirname(directory);
13022
+ const relativeParent = path23.relative(scanRoot, parent);
13023
+ if (parent === directory || relativeParent.startsWith("..") || path23.isAbsolute(relativeParent)) {
12238
13024
  break;
12239
13025
  }
12240
13026
  directory = parent;
@@ -12279,7 +13065,7 @@ function isSpec(framework, filename) {
12279
13065
  return /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/i.test(filename);
12280
13066
  }
12281
13067
  async function collectSpecs(root, framework, testDirectory, signal) {
12282
- const roots = testDirectory ? [path22.resolve(root, testDirectory)] : framework === "cypress" ? [path22.join(root, "cypress", "e2e"), path22.join(root, "cypress", "integration")] : [path22.join(root, "tests")];
13068
+ const roots = testDirectory ? [path23.resolve(root, testDirectory)] : framework === "cypress" ? [path23.join(root, "cypress", "e2e"), path23.join(root, "cypress", "integration")] : [path23.join(root, "tests")];
12283
13069
  const samples = [];
12284
13070
  let count = 0;
12285
13071
  let scanned = 0;
@@ -12298,7 +13084,7 @@ async function collectSpecs(root, framework, testDirectory, signal) {
12298
13084
  }
12299
13085
  for (const entry of entries) {
12300
13086
  signal.throwIfAborted();
12301
- const target = path22.join(directory, entry.name);
13087
+ const target = path23.join(directory, entry.name);
12302
13088
  if (entry.isDirectory() && !SKIP_DIRECTORIES.has(entry.name)) queue.push(target);
12303
13089
  else if (entry.isFile() && isSpec(framework, entry.name)) {
12304
13090
  count += 1;
@@ -12318,7 +13104,7 @@ function nearestPackage(projectRoot, packagesByDirectory, scanRoot) {
12318
13104
  const found = packagesByDirectory.get(directory);
12319
13105
  if (found) return found;
12320
13106
  if (directory === scanRoot) return void 0;
12321
- const parent = path22.dirname(directory);
13107
+ const parent = path23.dirname(directory);
12322
13108
  if (parent === directory || relativePath(scanRoot, parent).startsWith("..")) return void 0;
12323
13109
  directory = parent;
12324
13110
  }
@@ -12329,13 +13115,13 @@ async function discoverE2EProjects(root, options) {
12329
13115
  const packages = (await Promise.all(scan.packageFiles.map(readPackageInfo))).filter(
12330
13116
  (info) => Boolean(info)
12331
13117
  );
12332
- const packagesByDirectory = new Map(packages.map((info) => [path22.dirname(info.path), info]));
13118
+ const packagesByDirectory = new Map(packages.map((info) => [path23.dirname(info.path), info]));
12333
13119
  const candidates = /* @__PURE__ */ new Map();
12334
13120
  for (const config of scan.configs) {
12335
13121
  if (options.framework && options.framework !== "all" && config.framework !== options.framework) {
12336
13122
  continue;
12337
13123
  }
12338
- const projectRoot = path22.dirname(config.absolutePath);
13124
+ const projectRoot = path23.dirname(config.absolutePath);
12339
13125
  candidates.set(`${config.framework}:${projectRoot}`, {
12340
13126
  framework: config.framework,
12341
13127
  root: projectRoot,
@@ -12343,7 +13129,7 @@ async function discoverE2EProjects(root, options) {
12343
13129
  });
12344
13130
  }
12345
13131
  for (const info of packages) {
12346
- const projectRoot = path22.dirname(info.path);
13132
+ const projectRoot = path23.dirname(info.path);
12347
13133
  for (const framework of frameworkFromPackage(info)) {
12348
13134
  if (options.framework && options.framework !== "all" && framework !== options.framework)
12349
13135
  continue;
@@ -12359,10 +13145,10 @@ async function discoverE2EProjects(root, options) {
12359
13145
  const scripts = matchingScripts(info, candidate.framework);
12360
13146
  const manager = await detectPackageManager3(candidate.root, root, info?.packageManager);
12361
13147
  const testDirectory = candidate.framework === "playwright" ? staticString(source, "testDir") : void 0;
12362
- const resolvedTestDirectory = testDirectory ? path22.resolve(candidate.root, testDirectory) : void 0;
12363
- const relativeTestDirectory = resolvedTestDirectory ? path22.relative(root, resolvedTestDirectory) : void 0;
13148
+ const resolvedTestDirectory = testDirectory ? path23.resolve(candidate.root, testDirectory) : void 0;
13149
+ const relativeTestDirectory = resolvedTestDirectory ? path23.relative(root, resolvedTestDirectory) : void 0;
12364
13150
  const unsafeTestDirectory = Boolean(
12365
- relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path22.isAbsolute(relativeTestDirectory))
13151
+ relativeTestDirectory && (relativeTestDirectory.startsWith("..") || path23.isAbsolute(relativeTestDirectory))
12366
13152
  );
12367
13153
  const specs = options.includeSpecs === false || unsafeTestDirectory ? { count: 0, samples: [], truncated: false } : await collectSpecs(candidate.root, candidate.framework, testDirectory, options.signal);
12368
13154
  const warnings = [];
@@ -12467,10 +13253,10 @@ import {
12467
13253
  atomicWrite as atomicWrite2,
12468
13254
  detectNewlineStyle,
12469
13255
  normalizeToLf,
12470
- ToolValidationError,
12471
13256
  toStyle,
12472
13257
  unifiedDiff
12473
- } from "@wrongstack/core";
13258
+ } from "@wrongstack/core/utils";
13259
+ import { ToolValidationError } from "@wrongstack/core/types";
12474
13260
 
12475
13261
  // src/_edit-match.ts
12476
13262
  var TIER_LABEL = {
@@ -12698,7 +13484,7 @@ function prefixSimilarity(a, b) {
12698
13484
  }
12699
13485
 
12700
13486
  // src/_syntax-check.ts
12701
- import * as path23 from "node:path";
13487
+ import * as path24 from "node:path";
12702
13488
  var TS_LIKE = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"]);
12703
13489
  var MAX_CHECK_CHARS = 15e5;
12704
13490
  var MAX_ERRORS = 5;
@@ -12722,15 +13508,15 @@ async function checkSyntax(filePath, content, previousContent) {
12722
13508
  return { errors, preExisting };
12723
13509
  }
12724
13510
  function isJsoncFile(filePath) {
12725
- const base = path23.basename(filePath).toLowerCase();
13511
+ const base = path24.basename(filePath).toLowerCase();
12726
13512
  if (base.endsWith(".jsonc")) return true;
12727
13513
  if (/^(tsconfig|jsconfig)([.-].*)?\.json$/.test(base)) return true;
12728
- const dir = path23.basename(path23.dirname(filePath)).toLowerCase();
13514
+ const dir = path24.basename(path24.dirname(filePath)).toLowerCase();
12729
13515
  return dir === ".vscode";
12730
13516
  }
12731
13517
  async function parseErrors(filePath, content) {
12732
13518
  if (content.length > MAX_CHECK_CHARS) return void 0;
12733
- const ext = path23.extname(filePath).toLowerCase();
13519
+ const ext = path24.extname(filePath).toLowerCase();
12734
13520
  if (ext === ".json" || ext === ".jsonc") {
12735
13521
  try {
12736
13522
  JSON.parse(content);
@@ -12757,7 +13543,7 @@ async function parseErrors(filePath, content) {
12757
13543
  ts2.ScriptKind.JSX
12758
13544
  );
12759
13545
  const sourceFile = ts2.createSourceFile(
12760
- path23.basename(filePath),
13546
+ path24.basename(filePath),
12761
13547
  content,
12762
13548
  ts2.ScriptTarget.Latest,
12763
13549
  /* setParentNodes */
@@ -12992,7 +13778,7 @@ import {
12992
13778
  emitProcessCompleted as emitProcessCompleted3,
12993
13779
  emitProcessOutput as emitProcessOutput3,
12994
13780
  emitProcessStarted as emitProcessStarted3
12995
- } from "@wrongstack/core";
13781
+ } from "@wrongstack/core/observability";
12996
13782
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
12997
13783
  init_output_spool();
12998
13784
  init_util();
@@ -13282,7 +14068,7 @@ function levelRank(level) {
13282
14068
 
13283
14069
  // src/exec-kill-guard.ts
13284
14070
  import * as os7 from "node:os";
13285
- import * as path24 from "node:path";
14071
+ import * as path25 from "node:path";
13286
14072
  var isWin3 = os7.platform() === "win32";
13287
14073
  async function checkExecKillCommand(cmd, args) {
13288
14074
  if (!cmd) return { blocked: false };
@@ -13495,7 +14281,7 @@ async function checkKillTarget(target) {
13495
14281
  reason: `Blocked: kill ${target.signal} '${target.name}' targets a WrongStack process name.`
13496
14282
  };
13497
14283
  }
13498
- const currentImage = path24.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
14284
+ const currentImage = path25.basename(process.execPath).toLowerCase().replace(/\.exe$/, "");
13499
14285
  const targetsNodeRuntime = nameLower === "node" || nameLower.startsWith("node");
13500
14286
  if (targetsNodeRuntime && currentImage === "node") {
13501
14287
  return {
@@ -14282,7 +15068,7 @@ var execTool = {
14282
15068
  command: cmd,
14283
15069
  args: input.args ?? [],
14284
15070
  stdout: "",
14285
- stderr: `Command "${cmd}" not in allowlist. Add it to your ~/.wrongstack/config.json under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
15071
+ stderr: `Command "${cmd}" not in allowlist. Add it to your active profile config (~/.wrongstack/profiles/<name>/config.json) under "tools": { "exec": { "allow": ["${cmd}"] } }, or use the bash tool for one-off arbitrary commands.`,
14286
15072
  exitCode: 1,
14287
15073
  truncated: false,
14288
15074
  allowed: false,
@@ -14494,18 +15280,17 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
14494
15280
  }
14495
15281
 
14496
15282
  // src/fetch.ts
14497
- import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core";
15283
+ import { FetchError as FetchError2, ToolError, ToolValidationError as ToolValidationError3 } from "@wrongstack/core/types";
14498
15284
  import TurndownService from "turndown";
14499
15285
 
14500
15286
  // src/_fetch-guard.ts
14501
15287
  import * as dns2 from "node:dns/promises";
14502
15288
  import * as net3 from "node:net";
14503
15289
  import {
14504
- FetchError,
14505
- ToolValidationError as ToolValidationError2,
14506
15290
  isPrivateIPv4 as isPrivateIPv42,
14507
15291
  isPrivateIPv6 as isPrivateIPv62
14508
- } from "@wrongstack/core";
15292
+ } from "@wrongstack/core/utils";
15293
+ import { FetchError, ToolValidationError as ToolValidationError2 } from "@wrongstack/core/types";
14509
15294
  import { Agent, fetch as undiciFetch } from "undici";
14510
15295
  var nativeGlobalFetch = globalThis.fetch;
14511
15296
  var ALLOW_PRIVATE = process.env["WRONGSTACK_FETCH_ALLOW_PRIVATE"] === "1";
@@ -15003,7 +15788,8 @@ init_util();
15003
15788
  import { spawn as spawn9 } from "node:child_process";
15004
15789
  import { statSync as statSync3 } from "node:fs";
15005
15790
  import { dirname as dirname12, resolve as resolve11, sep as sep5 } from "node:path";
15006
- import { assessCommitSafety, buildChildEnv as buildChildEnv4 } from "@wrongstack/core";
15791
+ import { assessCommitSafety } from "@wrongstack/core/coordination";
15792
+ import { buildChildEnv as buildChildEnv4 } from "@wrongstack/core/utils";
15007
15793
  var TIMEOUT_MS2 = 3e4;
15008
15794
  var MAX_OUTPUT3 = 1e5;
15009
15795
  var gitTool = {
@@ -15283,8 +16069,8 @@ function runGit2(args, cwd, signal) {
15283
16069
 
15284
16070
  // src/glob.ts
15285
16071
  import * as fs20 from "node:fs/promises";
15286
- import * as path25 from "node:path";
15287
- import { compileGlob as compileGlob3 } from "@wrongstack/core";
16072
+ import * as path26 from "node:path";
16073
+ import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
15288
16074
 
15289
16075
  // src/_concurrency.ts
15290
16076
  async function mapWithConcurrency(items, limit, fn) {
@@ -15305,7 +16091,7 @@ async function mapWithConcurrency(items, limit, fn) {
15305
16091
 
15306
16092
  // src/glob.ts
15307
16093
  init_util();
15308
- var DEFAULT_IGNORE2 = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".turbo"];
16094
+ var DEFAULT_IGNORE2 = DEFAULT_WALK_IGNORE_DIRS2;
15309
16095
  var WALK_CONCURRENCY = 16;
15310
16096
  var globTool = {
15311
16097
  name: "glob",
@@ -15345,8 +16131,8 @@ var globTool = {
15345
16131
  const signal = opts?.signal;
15346
16132
  const base = input.path ? await safeResolveReal(input.path, ctx) : ctx.cwd;
15347
16133
  const limit = Math.max(1, Math.min(input.limit ?? 1e3, 5e3));
15348
- const ignored = await readGitignore(base);
15349
- const re = compileGlob3(input.pattern);
16134
+ const isGitIgnored = await loadGitignoreMatcher(base);
16135
+ const re = compileGlob2(input.pattern);
15350
16136
  const results = [];
15351
16137
  let truncated = false;
15352
16138
  const pushResult = async (full) => {
@@ -15385,12 +16171,13 @@ var globTool = {
15385
16171
  for (const e of entries) {
15386
16172
  const name = e.name;
15387
16173
  if (DEFAULT_IGNORE2.includes(name)) continue;
15388
- if (ignored.includes(name)) continue;
15389
16174
  const rel = relPrefix ? `${relPrefix}/${name}` : name;
15390
- const full = path25.join(dir, name);
16175
+ const full = path26.join(dir, name);
15391
16176
  if (e.isDirectory()) {
16177
+ if (isGitIgnored(rel, true)) continue;
15392
16178
  subdirs.push({ full, rel });
15393
16179
  } else if (e.isFile()) {
16180
+ if (isGitIgnored(rel, false)) continue;
15394
16181
  re.lastIndex = 0;
15395
16182
  const relMatch = re.test(rel);
15396
16183
  re.lastIndex = 0;
@@ -15402,10 +16189,12 @@ var globTool = {
15402
16189
  try {
15403
16190
  const st = await fs20.stat(full);
15404
16191
  if (st.isDirectory()) {
16192
+ if (isGitIgnored(rel, true)) continue;
15405
16193
  const real = await fs20.realpath(full);
15406
16194
  await assertRealInsideRoot(real, ctx);
15407
16195
  subdirs.push({ full, rel });
15408
16196
  } else if (st.isFile()) {
16197
+ if (isGitIgnored(rel, false)) continue;
15409
16198
  const real = await fs20.realpath(full);
15410
16199
  await assertRealInsideRoot(real, ctx);
15411
16200
  re.lastIndex = 0;
@@ -15429,21 +16218,14 @@ var globTool = {
15429
16218
  return { files: results.map((r) => r.rel), truncated };
15430
16219
  }
15431
16220
  };
15432
- async function readGitignore(dir) {
15433
- try {
15434
- const raw = await fs20.readFile(path25.join(dir, ".gitignore"), "utf8");
15435
- return raw.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
15436
- } catch {
15437
- return [];
15438
- }
15439
- }
15440
16221
 
15441
16222
  // src/grep.ts
15442
- import { expectDefined as expectDefined7 } from "@wrongstack/core";
16223
+ import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
15443
16224
  import { spawn as spawn10 } from "node:child_process";
15444
16225
  import * as fs21 from "node:fs/promises";
15445
- import * as path26 from "node:path";
15446
- import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob4, ToolValidationError as ToolValidationError4 } from "@wrongstack/core";
16226
+ import * as path27 from "node:path";
16227
+ import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
16228
+ import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob3, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3 } from "@wrongstack/core/utils";
15447
16229
 
15448
16230
  // src/_regex.ts
15449
16231
  var MAX_PATTERN_LEN = 256;
@@ -15492,7 +16274,7 @@ function capSubject(line) {
15492
16274
 
15493
16275
  // src/grep.ts
15494
16276
  init_util();
15495
- var DEFAULT_IGNORE3 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
16277
+ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
15496
16278
  var NATIVE_SCAN_CONCURRENCY = 32;
15497
16279
  var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
15498
16280
  var NATIVE_MAX_FILE_BYTES = 1e6;
@@ -15609,6 +16391,10 @@ async function* runRgStream(input, base, mode, limit, signal) {
15609
16391
  for (const ignored of DEFAULT_IGNORE3) {
15610
16392
  args.push("--glob", `!${ignored}/**`, "--glob", `!**/${ignored}/**`);
15611
16393
  }
16394
+ const gitignorePath = path27.join(base, ".gitignore");
16395
+ if (await fs21.access(gitignorePath).then(() => true, () => false)) {
16396
+ args.push("--ignore-file", gitignorePath);
16397
+ }
15612
16398
  if (input.glob) args.push("--glob", input.glob);
15613
16399
  args.push("--", input.pattern, base);
15614
16400
  const matches = [];
@@ -15733,7 +16519,8 @@ async function runNative(input, base, mode, limit, signal) {
15733
16519
  });
15734
16520
  }
15735
16521
  const re = compiled.regex;
15736
- const globRe = input.glob ? compileGlob4(input.glob) : null;
16522
+ const globRe = input.glob ? compileGlob3(input.glob) : null;
16523
+ const isGitIgnored = await loadGitignoreMatcher(base);
15737
16524
  const matches = [];
15738
16525
  const countOnlyFirstHit = mode === "count" && limit === 1;
15739
16526
  const maxBytes = mode === "content" ? NATIVE_MAX_FILE_BYTES : Math.min(NATIVE_MAX_FILE_BYTES, 256 * 1024);
@@ -15821,7 +16608,7 @@ async function runNative(input, base, mode, limit, signal) {
15821
16608
  } catch {
15822
16609
  }
15823
16610
  };
15824
- const walk = async (dir) => {
16611
+ const walk = async (dir, relPrefix) => {
15825
16612
  if (stopped || signal.aborted) return;
15826
16613
  let entries;
15827
16614
  try {
@@ -15835,17 +16622,24 @@ async function runNative(input, base, mode, limit, signal) {
15835
16622
  if (stopped) return;
15836
16623
  if (DEFAULT_IGNORE3.includes(e.name)) continue;
15837
16624
  if (e.isSymbolicLink()) continue;
15838
- const full = path26.join(dir, e.name);
16625
+ const rel = relPrefix ? `${relPrefix}/${e.name}` : e.name;
16626
+ const full = path27.join(dir, e.name);
15839
16627
  if (e.isDirectory()) {
15840
- subdirs.push(full);
16628
+ if (isGitIgnored(rel, true)) continue;
16629
+ subdirs.push({ full, rel });
15841
16630
  } else if (e.isFile()) {
16631
+ if (isGitIgnored(rel, false)) continue;
15842
16632
  files.push({ full, name: e.name });
15843
16633
  }
15844
16634
  }
15845
16635
  await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
15846
- await mapWithConcurrency(subdirs, Math.min(16, NATIVE_SCAN_CONCURRENCY), walk);
16636
+ await mapWithConcurrency(
16637
+ subdirs,
16638
+ Math.min(16, NATIVE_SCAN_CONCURRENCY),
16639
+ ({ full, rel }) => walk(full, rel)
16640
+ );
15847
16641
  };
15848
- await walk(base);
16642
+ await walk(base, "");
15849
16643
  return {
15850
16644
  matches,
15851
16645
  count: total,
@@ -15859,7 +16653,10 @@ init_spawn_stream();
15859
16653
  init_util();
15860
16654
  init_legacy_bridge();
15861
16655
  import { join as join21 } from "node:path";
15862
- import { detectPackageEcosystem, recordPackageAction } from "@wrongstack/core";
16656
+ import {
16657
+ detectEcosystem as detectPackageEcosystem,
16658
+ recordPackageAction
16659
+ } from "@wrongstack/core/coordination";
15863
16660
  var installTool = {
15864
16661
  name: "install",
15865
16662
  category: "Package Management",
@@ -16045,7 +16842,7 @@ function resolveManifestPath(cwd, pkgManager) {
16045
16842
  // src/json.ts
16046
16843
  init_util();
16047
16844
  import * as fs22 from "node:fs/promises";
16048
- import { deepMerge as deepMergeCore } from "@wrongstack/core";
16845
+ import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
16049
16846
  var jsonTool = {
16050
16847
  name: "json",
16051
16848
  category: "Data",
@@ -16423,56 +17220,56 @@ function jmespathSearch(data, query) {
16423
17220
  }
16424
17221
  function validateJsonSchema(data, schema) {
16425
17222
  const errors = [];
16426
- function check(value, s, path33) {
17223
+ function check(value, s, path34) {
16427
17224
  if (s["type"]) {
16428
17225
  const expectedType = s["type"];
16429
17226
  const actualType = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
16430
17227
  if (expectedType === "integer") {
16431
- if (!Number.isInteger(value)) errors.push(`${path33}: expected integer, got ${actualType}`);
17228
+ if (!Number.isInteger(value)) errors.push(`${path34}: expected integer, got ${actualType}`);
16432
17229
  } else if (expectedType !== actualType) {
16433
- errors.push(`${path33}: expected ${expectedType}, got ${actualType}`);
17230
+ errors.push(`${path34}: expected ${expectedType}, got ${actualType}`);
16434
17231
  }
16435
17232
  }
16436
17233
  if (typeof value === "string" && s["format"] === "uri" && value) {
16437
17234
  try {
16438
17235
  new URL(value);
16439
17236
  } catch {
16440
- errors.push(`${path33}: not a valid URI`);
17237
+ errors.push(`${path34}: not a valid URI`);
16441
17238
  }
16442
17239
  }
16443
17240
  if (typeof value === "string" && s["pattern"]) {
16444
17241
  const re = new RegExp(s["pattern"]);
16445
- if (!re.test(value)) errors.push(`${path33}: does not match pattern ${s["pattern"]}`);
17242
+ if (!re.test(value)) errors.push(`${path34}: does not match pattern ${s["pattern"]}`);
16446
17243
  }
16447
17244
  if (typeof value === "string" && s["minLength"] !== void 0 && value.length < s["minLength"]) {
16448
- errors.push(`${path33}: string too short (min ${s["minLength"]})`);
17245
+ errors.push(`${path34}: string too short (min ${s["minLength"]})`);
16449
17246
  }
16450
17247
  if (typeof value === "string" && s["maxLength"] !== void 0 && value.length > s["maxLength"]) {
16451
- errors.push(`${path33}: string too long (max ${s["maxLength"]})`);
17248
+ errors.push(`${path34}: string too long (max ${s["maxLength"]})`);
16452
17249
  }
16453
17250
  if (typeof value === "number" && s["minimum"] !== void 0 && value < s["minimum"]) {
16454
- errors.push(`${path33}: below minimum ${s["minimum"]}`);
17251
+ errors.push(`${path34}: below minimum ${s["minimum"]}`);
16455
17252
  }
16456
17253
  if (typeof value === "number" && s["maximum"] !== void 0 && value > s["maximum"]) {
16457
- errors.push(`${path33}: above maximum ${s["maximum"]}`);
17254
+ errors.push(`${path34}: above maximum ${s["maximum"]}`);
16458
17255
  }
16459
17256
  if (Array.isArray(value) && s["items"] && Array.isArray(s["items"])) {
16460
17257
  for (let i = 0; i < value.length; i++) {
16461
- check(value[i], s["items"], `${path33}[${i}]`);
17258
+ check(value[i], s["items"], `${path34}[${i}]`);
16462
17259
  }
16463
17260
  }
16464
17261
  if (typeof value === "object" && value !== null && !Array.isArray(value) && s["properties"]) {
16465
17262
  const props = s["properties"];
16466
17263
  for (const [k, propSchema] of Object.entries(props)) {
16467
- check(value[k], propSchema, `${path33}.${k}`);
17264
+ check(value[k], propSchema, `${path34}.${k}`);
16468
17265
  }
16469
17266
  }
16470
17267
  }
16471
17268
  check(data, schema, "$");
16472
17269
  return { valid: errors.length === 0, errors };
16473
17270
  }
16474
- function simpleQuery(data, path33) {
16475
- const parts = path33.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
17271
+ function simpleQuery(data, path34) {
17272
+ const parts = path34.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
16476
17273
  let current = data;
16477
17274
  for (const part of parts) {
16478
17275
  if (current === null || current === void 0) return void 0;
@@ -16523,7 +17320,8 @@ function toYaml(data, indent = 0) {
16523
17320
 
16524
17321
  // src/kanban.ts
16525
17322
  import { randomUUID } from "node:crypto";
16526
- import { deserializeTaskGraph as deserializeTaskGraph2, loadTasks as loadTasks2, serializeTaskGraph } from "@wrongstack/core";
17323
+ import { deserializeTaskGraph as deserializeTaskGraph2, serializeTaskGraph } from "@wrongstack/core/tasking";
17324
+ import { loadTasks as loadTasks2 } from "@wrongstack/core/storage";
16527
17325
  import {
16528
17326
  addCheckToTask,
16529
17327
  addColumn,
@@ -16570,18 +17368,45 @@ import {
16570
17368
  updateColumn,
16571
17369
  updateGoalMetricOnTask,
16572
17370
  updateTask,
16573
- updateTaskAssignment
17371
+ updateTaskAssignment,
17372
+ verifyTaskCompletion,
17373
+ finalizeTaskCompletion,
17374
+ assessTaskAtomicity,
17375
+ proposeTaskDecomposition
16574
17376
  } from "@wrongstack/kanban";
16575
17377
 
17378
+ // src/kanban-evidence-bridge.ts
17379
+ import { recordCompletedWorkEvidence } from "@wrongstack/core/utils";
17380
+ function kanbanEvidenceKey(boardId, taskId) {
17381
+ return `kanban:${boardId}:${taskId}`;
17382
+ }
17383
+ function kanbanEvidencePointer(boardId, taskId) {
17384
+ return `kanban://${boardId}/${taskId}#verificationReport`;
17385
+ }
17386
+ function recordKanbanVerificationEvidence(ctx, report) {
17387
+ try {
17388
+ const passed = report.checks.filter((check) => check.status === "passed").length;
17389
+ const completedAt = Date.parse(report.completedAt);
17390
+ recordCompletedWorkEvidence(ctx, {
17391
+ key: kanbanEvidenceKey(report.boardId, report.taskId),
17392
+ source: "verification",
17393
+ summary: `${report.taskTitle} \u2014 verification ${report.verdict} (${passed}/${report.checks.length} checks)`,
17394
+ ...Number.isFinite(completedAt) ? { completedAt } : {},
17395
+ evidence: kanbanEvidencePointer(report.boardId, report.taskId)
17396
+ });
17397
+ } catch {
17398
+ }
17399
+ }
17400
+
16576
17401
  // src/session-kanban.ts
17402
+ import { GlobalMailbox } from "@wrongstack/core/coordination";
16577
17403
  import {
16578
- deserializeTaskGraph,
16579
- GlobalMailbox,
16580
17404
  loadPlan,
16581
17405
  loadTasks,
16582
17406
  mutatePlan,
16583
17407
  mutateTasks
16584
- } from "@wrongstack/core";
17408
+ } from "@wrongstack/core/storage";
17409
+ import { deserializeTaskGraph } from "@wrongstack/core/tasking";
16585
17410
  import { resolveWstackPaths as resolveWstackPaths3 } from "@wrongstack/core/utils";
16586
17411
  import {
16587
17412
  createBoard,
@@ -16802,8 +17627,8 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
16802
17627
  var kanbanTool = {
16803
17628
  name: "kanban",
16804
17629
  category: "Project",
16805
- description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence.",
16806
- usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done.",
17630
+ description: "Manage and audit project-scoped multi-kanban boards stored under .wrongstack/kanbans. Managed cards enforce fully specified details and adjacent Backlog \u2192 Todo \u2192 Running \u2192 Review \u2192 Done transitions with persistent comments and evidence. Successful board access records live agent/session presence. Use verify_completion to validate a task against its success criteria and persist the verification report. Use split_atomic to split a task into child tasks with parent.atomic=true in a single atomic board mutation, enforcing subtree verification before the parent can complete.",
17631
+ usageHint: "Use this for durable project kanban state. Reassess with get_board whenever evidence changes; agents may add, update, split, merge, reprioritize, or remove tasks so the board remains a live plan. Presence includes active/last-seen session and agent metadata. For managed boards, fully fill card details, use transition_task after every material step, attach truthful evidence, and never use update_task/move_task to bypass lifecycle guards. Worker completion enters Review; only passed acceptance criteria plus review evidence allow Done. Use verify_completion to generate a verification report (persisted automatically) and split_atomic to atomically create child subtasks with the atomic flag pre-set.",
16807
17632
  permission: "confirm",
16808
17633
  mutating: true,
16809
17634
  capabilities: ["fs.write"],
@@ -16859,7 +17684,11 @@ var kanbanTool = {
16859
17684
  "add_check",
16860
17685
  "update_check",
16861
17686
  "add_note",
16862
- "add_link"
17687
+ "add_link",
17688
+ "verify_completion",
17689
+ "split_atomic",
17690
+ "assess_atomicity",
17691
+ "propose_decomposition"
16863
17692
  ]
16864
17693
  },
16865
17694
  boardId: { type: "string" },
@@ -16914,6 +17743,7 @@ var kanbanTool = {
16914
17743
  runTaskId: { type: "string" },
16915
17744
  lastResult: { type: "string" },
16916
17745
  error: { type: "string" },
17746
+ expectedLeaseId: { type: "string" },
16917
17747
  assignmentStatus: {
16918
17748
  type: "string",
16919
17749
  enum: ["assigned", "queued", "running", "completed", "failed", "cancelled"]
@@ -16948,6 +17778,7 @@ var kanbanTool = {
16948
17778
  lastFailureKind: { type: "string" },
16949
17779
  dependsOn: { type: "array", items: { type: "string" } },
16950
17780
  estimatedHours: { type: "number" },
17781
+ actualHours: { type: "number" },
16951
17782
  taskGraph: { type: "object" },
16952
17783
  graphId: { type: "string" },
16953
17784
  specId: { type: "string" },
@@ -16993,7 +17824,24 @@ var kanbanTool = {
16993
17824
  includeCompletedTasks: { type: "boolean" },
16994
17825
  preserveAssignment: { type: "boolean" },
16995
17826
  preserveDependencies: { type: "boolean" },
16996
- moveTasksToColumnId: { type: "string" }
17827
+ moveTasksToColumnId: { type: "string" },
17828
+ atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
17829
+ atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
17830
+ gateEnforcement: { type: "string", enum: ["strict", "soft", "off"] },
17831
+ subtasks: {
17832
+ type: "array",
17833
+ minItems: 2,
17834
+ items: {
17835
+ type: "object",
17836
+ properties: {
17837
+ title: { type: "string" },
17838
+ description: { type: "string" },
17839
+ successCriteria: { type: "array", items: { type: "string" } },
17840
+ dependsOnIndex: { type: "array", items: { type: "number" } }
17841
+ },
17842
+ required: ["title"]
17843
+ }
17844
+ }
16997
17845
  },
16998
17846
  required: ["action"]
16999
17847
  },
@@ -17033,7 +17881,14 @@ var kanbanTool = {
17033
17881
  title: input.title,
17034
17882
  ...input.description !== void 0 ? { description: input.description } : {},
17035
17883
  ...input.tags !== void 0 ? { tags: input.tags } : {},
17036
- ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {}
17884
+ ...input.generatedBy !== void 0 ? { generatedBy: input.generatedBy } : {},
17885
+ ...input.atomicityMode !== void 0 ? {
17886
+ atomicity: {
17887
+ mode: input.atomicityMode,
17888
+ decomposition: input.atomicityDecomposition ?? "propose"
17889
+ }
17890
+ } : {},
17891
+ ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
17037
17892
  });
17038
17893
  return { ok: true, message: `Board created: ${board.title}`, board };
17039
17894
  }
@@ -17042,7 +17897,14 @@ var kanbanTool = {
17042
17897
  const board = await updateBoard2(projectRoot, input.boardId, {
17043
17898
  ...input.title !== void 0 ? { title: input.title } : {},
17044
17899
  ...input.description !== void 0 ? { description: input.description } : {},
17045
- ...input.tags !== void 0 ? { tags: input.tags } : {}
17900
+ ...input.tags !== void 0 ? { tags: input.tags } : {},
17901
+ ...input.atomicityMode !== void 0 ? {
17902
+ atomicity: {
17903
+ mode: input.atomicityMode,
17904
+ decomposition: input.atomicityDecomposition ?? "propose"
17905
+ }
17906
+ } : {},
17907
+ ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
17046
17908
  });
17047
17909
  return board ? okBoard(board, "Board updated.") : fail("Board not found.");
17048
17910
  }
@@ -17250,30 +18112,18 @@ var kanbanTool = {
17250
18112
  case "add_task": {
17251
18113
  if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
17252
18114
  const result2 = await addTask(projectRoot, input.boardId, taskInput(input));
17253
- return result2 ? okTask(result2.board, result2.task, "Task added.") : fail("Board not found.");
18115
+ if (!result2) return fail("Board not found.");
18116
+ return okTask(
18117
+ result2.board,
18118
+ result2.task,
18119
+ `Task added.${atomicityNudge(result2.task)}`
18120
+ );
17254
18121
  }
17255
18122
  case "split_task": {
17256
18123
  if (!input.boardId || !input.taskId || !input.childTitles?.length) {
17257
18124
  return fail("split_task requires boardId, taskId, and childTitles.");
17258
18125
  }
17259
- const result2 = await splitTask(projectRoot, input.boardId, input.taskId, {
17260
- titles: input.childTitles,
17261
- ...input.targetColumnId !== void 0 ? { columnId: input.targetColumnId } : {},
17262
- ...input.inheritAssignment !== void 0 ? { inheritAssignment: input.inheritAssignment } : {},
17263
- ...input.inheritLabels !== void 0 ? { inheritLabels: input.inheritLabels } : {},
17264
- ...input.inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria: input.inheritSuccessCriteria } : {},
17265
- ...input.inheritGoalMetrics !== void 0 ? { inheritGoalMetrics: input.inheritGoalMetrics } : {},
17266
- ...input.inheritDependencies !== void 0 ? { inheritDependencies: input.inheritDependencies } : {},
17267
- ...input.chainChildren !== void 0 ? { chainChildren: input.chainChildren } : {},
17268
- ...input.rewireDependents !== void 0 ? { rewireDependents: input.rewireDependents } : {}
17269
- });
17270
- return result2 ? {
17271
- ok: true,
17272
- message: `${result2.children.length} child task(s) created.`,
17273
- board: result2.board,
17274
- task: result2.parent,
17275
- children: result2.children
17276
- } : fail("Task not found.");
18126
+ return handleSplitTask(projectRoot, input, {});
17277
18127
  }
17278
18128
  case "merge_tasks": {
17279
18129
  if (!input.boardId || !input.taskIds?.length || !input.title) {
@@ -17347,6 +18197,19 @@ var kanbanTool = {
17347
18197
  "transition_task requires boardId, taskId, lifecycleStage, author, and transitionComment."
17348
18198
  );
17349
18199
  }
18200
+ if (input.lifecycleStage === "done") {
18201
+ const boardBefore = await getBoard2(projectRoot, input.boardId);
18202
+ const taskBefore = boardBefore ? await getTask(projectRoot, input.boardId, input.taskId) : null;
18203
+ if (boardBefore && taskBefore && !taskBefore.verificationReport && (taskBefore.atomic || Boolean(taskBefore.successCriteria?.length))) {
18204
+ const preGate = await verifyTaskCompletion(projectRoot, input.boardId, taskBefore.id, {
18205
+ persist: false
18206
+ });
18207
+ await updateTask(projectRoot, input.boardId, taskBefore.id, {
18208
+ verificationReport: preGate.report,
18209
+ successCriteria: preGate.task.successCriteria
18210
+ });
18211
+ }
18212
+ }
17350
18213
  const result2 = await transitionTask(projectRoot, input.boardId, input.taskId, {
17351
18214
  to: input.lifecycleStage,
17352
18215
  actor: input.author,
@@ -17361,6 +18224,9 @@ var kanbanTool = {
17361
18224
  } : {},
17362
18225
  patch: taskPatch(input)
17363
18226
  });
18227
+ if (result2 && input.lifecycleStage === "done" && result2.task.verificationReport) {
18228
+ recordKanbanVerificationEvidence(ctx, result2.task.verificationReport);
18229
+ }
17364
18230
  return result2 ? okTask(result2.board, result2.task, `Task advanced to ${result2.transition.to}.`) : fail("Board or task not found.");
17365
18231
  }
17366
18232
  case "move_task": {
@@ -17474,7 +18340,31 @@ var kanbanTool = {
17474
18340
  // is atomic inside updateTaskAssignment's mutateBoard lock.
17475
18341
  input.expectedLeaseId !== void 0 ? { expectedLeaseId: input.expectedLeaseId } : {}
17476
18342
  );
17477
- return board ? okBoard(board, "Assignment updated.") : fail("Task not found.");
18343
+ if (!board) return fail("Task not found.");
18344
+ if (assignmentStatus === "completed") {
18345
+ const envGate = readEnvGateEnforcement();
18346
+ const finalized = await finalizeTaskCompletion(projectRoot, board.id, input.taskId, {
18347
+ ...board.completionGate === void 0 && envGate !== void 0 ? { enforcement: envGate } : {},
18348
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
18349
+ });
18350
+ if (finalized) {
18351
+ if (finalized.gate.report) {
18352
+ recordKanbanVerificationEvidence(ctx, finalized.gate.report);
18353
+ }
18354
+ const gateSummary = {
18355
+ enforcement: finalized.gate.enforcement,
18356
+ allowed: finalized.gate.allowed,
18357
+ verdict: finalized.gate.verdict,
18358
+ issues: finalized.gate.issues.map((issue) => issue.message)
18359
+ };
18360
+ const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "skipped" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
18361
+ return {
18362
+ ...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
18363
+ gate: gateSummary
18364
+ };
18365
+ }
18366
+ }
18367
+ return okBoard(board, "Assignment updated.");
17478
18368
  }
17479
18369
  case "heartbeat_assignment": {
17480
18370
  if (!input.boardId || !input.taskId) {
@@ -17638,6 +18528,83 @@ var kanbanTool = {
17638
18528
  });
17639
18529
  return board ? okBoard(board, "Link added.") : fail("Task not found.");
17640
18530
  }
18531
+ case "assess_atomicity": {
18532
+ if (!input.boardId || !input.taskId) {
18533
+ return fail("assess_atomicity requires boardId and taskId.");
18534
+ }
18535
+ const result2 = await assessTaskAtomicity(projectRoot, input.boardId, input.taskId, {
18536
+ assessedBy: "agent",
18537
+ ...ctx.agentId !== void 0 ? { eventContext: { actor: ctx.agentId } } : {}
18538
+ });
18539
+ if (!result2) return fail("Task not found.");
18540
+ const failing = result2.assessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason);
18541
+ const guidance = result2.assessment.verdict === "needs_decomposition" ? ` This task should be split before dispatch \u2014 call propose_decomposition with 2+ subtasks (each with one verifiable success criterion). Reasons: ${failing.join(" | ")}` : result2.assessment.verdict === "composite" ? " Container task: work happens in its children; it is verified via subtask aggregation." : "";
18542
+ return okTask(
18543
+ result2.board,
18544
+ result2.task,
18545
+ `Atomicity verdict: ${result2.assessment.verdict} (score ${result2.assessment.score}).${guidance}`
18546
+ );
18547
+ }
18548
+ case "propose_decomposition": {
18549
+ if (!input.boardId || !input.taskId || !input.subtasks?.length) {
18550
+ return fail("propose_decomposition requires boardId, taskId, and subtasks (2+).");
18551
+ }
18552
+ if (input.subtasks.length < 2) {
18553
+ return fail("propose_decomposition requires at least two subtasks.");
18554
+ }
18555
+ const invalid = input.subtasks.find(
18556
+ (subtask) => typeof subtask?.title !== "string" || !subtask.title.trim()
18557
+ );
18558
+ if (invalid) return fail("Every proposed subtask needs a non-blank title.");
18559
+ const result2 = await proposeTaskDecomposition(
18560
+ projectRoot,
18561
+ input.boardId,
18562
+ input.taskId,
18563
+ {
18564
+ subtasks: input.subtasks,
18565
+ ...input.note !== void 0 ? { rationale: input.note } : {},
18566
+ ...ctx.agentId !== void 0 ? { proposedBy: ctx.agentId } : {}
18567
+ },
18568
+ ctx.agentId !== void 0 ? { actor: ctx.agentId } : {}
18569
+ );
18570
+ if (!result2) return fail("Task not found.");
18571
+ const message = result2.proposal.status === "applied" ? `Decomposition applied: ${result2.proposal.appliedChildTaskIds?.length ?? 0} child tasks created (parent marked atomic).` : 'Decomposition proposal recorded \u2014 awaiting approval (board policy is "propose"). It can be approved from the WebUI or via update_task.';
18572
+ return okTask(result2.board, result2.task, message);
18573
+ }
18574
+ case "verify_completion": {
18575
+ if (!input.boardId || !input.taskId) {
18576
+ return fail("verify_completion requires boardId and taskId.");
18577
+ }
18578
+ const verResult = await verifyTaskCompletion(projectRoot, input.boardId, input.taskId);
18579
+ const persistedBoard = await updateTask(projectRoot, input.boardId, input.taskId, {
18580
+ verificationReport: verResult.report,
18581
+ successCriteria: verResult.task.successCriteria
18582
+ });
18583
+ if (!persistedBoard) {
18584
+ return {
18585
+ ok: false,
18586
+ verdict: verResult.report.verdict,
18587
+ message: `Verification succeeded but persist failed: ${verResult.report.markdownSummary}. Board may be stale \u2014 re-run verify_completion.`,
18588
+ board: verResult.board
18589
+ };
18590
+ }
18591
+ recordKanbanVerificationEvidence(ctx, verResult.report);
18592
+ const freshTask = persistedBoard.tasks?.find((t) => t.id === input.taskId);
18593
+ const deterministicVerdicts = ["passed", "failed", "needs_human", "incomplete"];
18594
+ return {
18595
+ ok: deterministicVerdicts.includes(verResult.report.verdict),
18596
+ verdict: verResult.report.verdict,
18597
+ message: verResult.report.markdownSummary,
18598
+ board: persistedBoard,
18599
+ task: freshTask ?? verResult.task
18600
+ };
18601
+ }
18602
+ case "split_atomic": {
18603
+ if (!input.boardId || !input.taskId || !input.childTitles?.length) {
18604
+ return fail("split_atomic requires boardId, taskId, and childTitles (at least one).");
18605
+ }
18606
+ return handleSplitTask(projectRoot, input, { atomic: true });
18607
+ }
17641
18608
  default:
17642
18609
  return fail(`Unknown kanban action: ${input.action}`);
17643
18610
  }
@@ -17648,6 +18615,15 @@ var kanbanTool = {
17648
18615
  }
17649
18616
  }
17650
18617
  };
18618
+ function atomicityNudge(task) {
18619
+ if (task.atomicityAssessment?.verdict !== "needs_decomposition") return "";
18620
+ const reasons = task.atomicityAssessment.criteria.filter((entry) => entry.score < 1).map((entry) => entry.reason).join(" | ");
18621
+ return ` Atomicity: needs_decomposition (score ${task.atomicityAssessment.score}) \u2014 call propose_decomposition with 2+ subtasks before dispatch. Reasons: ${reasons}`;
18622
+ }
18623
+ function readEnvGateEnforcement() {
18624
+ const raw = process.env["WRONGSTACK_KANBAN_GATE"]?.trim().toLowerCase();
18625
+ return raw === "strict" || raw === "soft" || raw === "off" ? raw : void 0;
18626
+ }
17651
18627
  function fail(message) {
17652
18628
  return { ok: false, message };
17653
18629
  }
@@ -17657,6 +18633,50 @@ function okBoard(board, message = "Board loaded.") {
17657
18633
  function okTask(board, task, message) {
17658
18634
  return { ok: true, message, board, task };
17659
18635
  }
18636
+ async function handleSplitTask(projectRoot, input, extraSplitOptions) {
18637
+ const boardId = input.boardId;
18638
+ const taskId = input.taskId;
18639
+ const childTitles = input.childTitles;
18640
+ if (!boardId || !taskId || !childTitles?.length) {
18641
+ return fail("split requires boardId, taskId, and at least one childTitles.");
18642
+ }
18643
+ const {
18644
+ targetColumnId,
18645
+ inheritAssignment,
18646
+ inheritLabels,
18647
+ inheritSuccessCriteria,
18648
+ inheritGoalMetrics,
18649
+ inheritDependencies,
18650
+ chainChildren,
18651
+ rewireDependents
18652
+ } = input;
18653
+ const result = await splitTask(projectRoot, boardId, taskId, {
18654
+ titles: childTitles,
18655
+ ...extraSplitOptions,
18656
+ ...targetColumnId !== void 0 ? { columnId: targetColumnId } : {},
18657
+ ...inheritAssignment !== void 0 ? { inheritAssignment } : {},
18658
+ ...inheritLabels !== void 0 ? { inheritLabels } : {},
18659
+ ...inheritSuccessCriteria !== void 0 ? { inheritSuccessCriteria } : {},
18660
+ ...inheritGoalMetrics !== void 0 ? { inheritGoalMetrics } : {},
18661
+ ...inheritDependencies !== void 0 ? { inheritDependencies } : {},
18662
+ ...chainChildren !== void 0 ? { chainChildren } : {},
18663
+ ...rewireDependents !== void 0 ? { rewireDependents } : {}
18664
+ });
18665
+ if (!result) return fail("Task not found.");
18666
+ const freshParent = result.board.tasks?.find((t) => t.id === taskId);
18667
+ if (!freshParent) {
18668
+ return fail(
18669
+ `Split succeeded but parent ${taskId} not found in returned board. Children: [${result.children.map((c) => c.id).join(", ")}].`
18670
+ );
18671
+ }
18672
+ return {
18673
+ ok: true,
18674
+ message: `${result.children.length} child task(s) created.`,
18675
+ board: result.board,
18676
+ task: freshParent,
18677
+ children: result.children
18678
+ };
18679
+ }
17660
18680
  async function requireBoard(projectRoot, boardId) {
17661
18681
  return boardId ? getBoard2(projectRoot, boardId) : null;
17662
18682
  }
@@ -17758,7 +18778,8 @@ function taskPatch(input) {
17758
18778
  labels: input.labels,
17759
18779
  assignedAgent: input.agentId,
17760
18780
  ...mergedDependsOn(input) ? { dependsOn: mergedDependsOn(input) } : {},
17761
- ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {}
18781
+ ...input.estimatedHours !== void 0 ? { estimatedHours: input.estimatedHours } : {},
18782
+ ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
17762
18783
  };
17763
18784
  }
17764
18785
  function assignmentInput(input) {
@@ -17940,7 +18961,7 @@ async function detectLinter(cwd) {
17940
18961
 
17941
18962
  // src/logs.ts
17942
18963
  import { spawn as spawn11 } from "node:child_process";
17943
- import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core";
18964
+ import { buildChildEnv as buildChildEnv6 } from "@wrongstack/core/utils";
17944
18965
  init_util();
17945
18966
  var logsTool = {
17946
18967
  name: "logs",
@@ -18079,7 +19100,7 @@ async function dockerLogs(service, lines, filterRe, cwd, signal, since) {
18079
19100
  }
18080
19101
  var DOCKER_LOGS_TIMEOUT_MS = 3e3;
18081
19102
  var MAX_TAIL_LINES = 1e5;
18082
- async function fileLogs(path33, lines, filterRe, stream) {
19103
+ async function fileLogs(path34, lines, filterRe, stream) {
18083
19104
  const { createInterface } = await import("node:readline");
18084
19105
  const { createReadStream: createReadStream2 } = await import("node:fs");
18085
19106
  const entries = [];
@@ -18088,7 +19109,7 @@ async function fileLogs(path33, lines, filterRe, stream) {
18088
19109
  let writeIdx = 0;
18089
19110
  let totalLines = 0;
18090
19111
  const rl = createInterface({
18091
- input: createReadStream2(path33),
19112
+ input: createReadStream2(path34),
18092
19113
  crlfDelay: Number.POSITIVE_INFINITY
18093
19114
  });
18094
19115
  for await (const line of rl) {
@@ -18109,7 +19130,7 @@ async function fileLogs(path33, lines, filterRe, stream) {
18109
19130
  if (parsed) entries.push(parsed);
18110
19131
  }
18111
19132
  return {
18112
- source: path33,
19133
+ source: path34,
18113
19134
  entries,
18114
19135
  total: entries.length,
18115
19136
  truncated: totalLines > effLines,
@@ -18156,7 +19177,7 @@ function parseLine(line) {
18156
19177
  init_util();
18157
19178
  init_win32_resolve();
18158
19179
  import { spawn as spawn12 } from "node:child_process";
18159
- import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core";
19180
+ import { buildChildEnv as buildChildEnv7 } from "@wrongstack/core/utils";
18160
19181
  var outdatedTool = {
18161
19182
  name: "outdated",
18162
19183
  category: "Package Management",
@@ -18341,8 +19362,8 @@ init_util();
18341
19362
  import { spawn as spawn13 } from "node:child_process";
18342
19363
  import * as fs23 from "node:fs/promises";
18343
19364
  import * as os8 from "node:os";
18344
- import * as path27 from "node:path";
18345
- import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core";
19365
+ import * as path28 from "node:path";
19366
+ import { buildChildEnv as buildChildEnv8 } from "@wrongstack/core/utils";
18346
19367
  var patchTool = {
18347
19368
  name: "patch",
18348
19369
  category: "Filesystem",
@@ -18377,9 +19398,9 @@ var patchTool = {
18377
19398
  for (const t of targets) {
18378
19399
  const stripped = stripPathComponents(t, strip);
18379
19400
  if (!stripped) continue;
18380
- const candidate = path27.resolve(dir, stripped);
18381
- const rel = path27.relative(ctx.projectRoot, candidate);
18382
- if (rel.startsWith("..") || path27.isAbsolute(rel)) {
19401
+ const candidate = path28.resolve(dir, stripped);
19402
+ const rel = path28.relative(ctx.projectRoot, candidate);
19403
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) {
18383
19404
  return {
18384
19405
  applied: 0,
18385
19406
  rejected: 1,
@@ -18396,11 +19417,11 @@ var patchTool = {
18396
19417
  beforeContents.set(target, await readTextForTracking(target));
18397
19418
  }
18398
19419
  }
18399
- const tmpDir = await fs23.mkdtemp(path27.join(os8.tmpdir(), ".wstack_patch_"));
19420
+ const tmpDir = await fs23.mkdtemp(path28.join(os8.tmpdir(), ".wstack_patch_"));
18400
19421
  try {
18401
19422
  await fs23.chmod(tmpDir, 448).catch(() => {
18402
19423
  });
18403
- const patchFile = path27.join(tmpDir, "in.diff");
19424
+ const patchFile = path28.join(tmpDir, "in.diff");
18404
19425
  await fs23.writeFile(patchFile, input.patch, { mode: 384 });
18405
19426
  const args = [`-p${strip}`, "--merge", ...dryRun ? ["--dry-run"] : [], "-i", patchFile];
18406
19427
  const result = await runPatch(args, dir, opts.signal);
@@ -18506,11 +19527,11 @@ import {
18506
19527
  mutatePlan as mutatePlan2,
18507
19528
  removePlanItem,
18508
19529
  setPlanItemStatus
18509
- } from "@wrongstack/core";
19530
+ } from "@wrongstack/core/storage";
18510
19531
  import {
18511
- mutateTasks as mutateTasks2,
18512
- formatTaskList
18513
- } from "@wrongstack/core";
19532
+ mutateTasks as mutateTasks2
19533
+ } from "@wrongstack/core/storage";
19534
+ import { formatTaskList } from "@wrongstack/core/utils";
18514
19535
  import { randomUUID as randomUUID2 } from "node:crypto";
18515
19536
  var planTool = {
18516
19537
  name: "plan",
@@ -18777,7 +19798,8 @@ function mkResult(plan, ok, message, todos) {
18777
19798
  // src/read.ts
18778
19799
  init_util();
18779
19800
  import * as fs24 from "node:fs/promises";
18780
- import { FsError, toErrorMessage as toErrorMessage4, ToolValidationError as ToolValidationError5 } from "@wrongstack/core";
19801
+ import { FsError, ToolValidationError as ToolValidationError5 } from "@wrongstack/core/types";
19802
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
18781
19803
  var MAX_BYTES2 = 5 * 1024 * 1024;
18782
19804
  var readTool = {
18783
19805
  name: "read",
@@ -18983,20 +20005,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
18983
20005
  }
18984
20006
 
18985
20007
  // src/replace.ts
18986
- import { expectDefined as expectDefined8 } from "@wrongstack/core";
20008
+ import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
18987
20009
  import { spawn as spawn14 } from "node:child_process";
18988
20010
  import * as fs25 from "node:fs/promises";
18989
- import * as path28 from "node:path";
20011
+ import * as path29 from "node:path";
18990
20012
  import {
18991
20013
  atomicWrite as atomicWrite3,
18992
20014
  buildChildEnv as buildChildEnv9,
18993
- compileGlob as compileGlob5,
20015
+ compileGlob as compileGlob4,
18994
20016
  detectNewlineStyle as detectNewlineStyle2,
18995
20017
  normalizeToLf as normalizeToLf2,
18996
20018
  toStyle as toStyle2,
18997
- ToolValidationError as ToolValidationError6,
18998
20019
  unifiedDiff as unifiedDiff2
18999
- } from "@wrongstack/core";
20020
+ } from "@wrongstack/core/utils";
20021
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
19000
20022
  init_util();
19001
20023
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
19002
20024
  var replaceTool = {
@@ -19055,7 +20077,7 @@ var replaceTool = {
19055
20077
  });
19056
20078
  }
19057
20079
  const re = compiled.regex;
19058
- const globRe = input.glob ? compileGlob5(input.glob) : null;
20080
+ const globRe = input.glob ? compileGlob4(input.glob) : null;
19059
20081
  const dryRun = input.dry_run ?? true;
19060
20082
  const filesInput = Array.isArray(input.files) ? input.files.join(",") : input.files;
19061
20083
  const fileList = await resolveFiles2(filesInput, ctx, globRe);
@@ -19075,8 +20097,8 @@ var replaceTool = {
19075
20097
  } catch {
19076
20098
  continue;
19077
20099
  }
19078
- const rel = path28.relative(realRoot, realPath);
19079
- if (rel.startsWith("..") || path28.isAbsolute(rel)) continue;
20100
+ const rel = path29.relative(realRoot, realPath);
20101
+ if (rel.startsWith("..") || path29.isAbsolute(rel)) continue;
19080
20102
  const stat17 = await fs25.stat(realPath).catch(() => null);
19081
20103
  if (!stat17?.isFile()) continue;
19082
20104
  let content;
@@ -19195,7 +20217,7 @@ function spawnRgFind(pattern, base) {
19195
20217
  }
19196
20218
  async function globNative(pattern, base, extraGlob) {
19197
20219
  const results = [];
19198
- const globRe = compileGlob5(pattern);
20220
+ const globRe = compileGlob4(pattern);
19199
20221
  const walk = async (dir) => {
19200
20222
  let entries;
19201
20223
  try {
@@ -19205,7 +20227,7 @@ async function globNative(pattern, base, extraGlob) {
19205
20227
  }
19206
20228
  for (const e of entries) {
19207
20229
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
19208
- const full = path28.join(dir, e.name);
20230
+ const full = path29.join(dir, e.name);
19209
20231
  try {
19210
20232
  const stat17 = await fs25.lstat(full);
19211
20233
  if (stat17.isSymbolicLink()) continue;
@@ -19232,8 +20254,8 @@ async function globNative(pattern, base, extraGlob) {
19232
20254
  // src/scaffold.ts
19233
20255
  init_util();
19234
20256
  import * as fs26 from "node:fs/promises";
19235
- import * as path29 from "node:path";
19236
- import { atomicWrite as atomicWrite4 } from "@wrongstack/core";
20257
+ import * as path30 from "node:path";
20258
+ import { atomicWrite as atomicWrite4 } from "@wrongstack/core/utils";
19237
20259
  var BUILT_IN_TEMPLATES = {
19238
20260
  "npm-package": {
19239
20261
  description: "Basic npm package with ESM",
@@ -19382,16 +20404,16 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
19382
20404
  let filesCreated = 0;
19383
20405
  for (const [filePath, content] of Object.entries(templateFiles)) {
19384
20406
  const resolvedPath = substituteVars(filePath, name, vars);
19385
- const joinedPath = path29.join(cwd, resolvedPath);
19386
- const root = path29.resolve(ctx.projectRoot);
19387
- const target = path29.resolve(joinedPath);
19388
- const rel = path29.relative(root, target);
19389
- if (rel.startsWith("..") || path29.isAbsolute(rel)) {
20407
+ const joinedPath = path30.join(cwd, resolvedPath);
20408
+ const root = path30.resolve(ctx.projectRoot);
20409
+ const target = path30.resolve(joinedPath);
20410
+ const rel = path30.relative(root, target);
20411
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) {
19390
20412
  throw new Error(`scaffold: generated path "${resolvedPath}" would escape project root`);
19391
20413
  }
19392
20414
  const fullPath = target;
19393
20415
  if (!dryRun) {
19394
- await fs26.mkdir(path29.dirname(fullPath), { recursive: true });
20416
+ await fs26.mkdir(path30.dirname(fullPath), { recursive: true });
19395
20417
  await atomicWrite4(fullPath, substituteVars(content, name, vars));
19396
20418
  }
19397
20419
  files.push(resolvedPath);
@@ -19420,7 +20442,8 @@ function substituteVars(content, name, vars) {
19420
20442
  }
19421
20443
 
19422
20444
  // src/search.ts
19423
- import { expectDefined as expectDefined9, FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core";
20445
+ import { FetchError as FetchError3, ToolValidationError as ToolValidationError7 } from "@wrongstack/core/types";
20446
+ import { expectDefined as expectDefined9 } from "@wrongstack/core/utils";
19424
20447
  import { toErrorMessage as toErrorMessage5 } from "@wrongstack/core/utils";
19425
20448
  var DEFAULT_NUM = 10;
19426
20449
  var MAX_RESULTS = 50;
@@ -19870,15 +20893,13 @@ var setWorkingDirTool = {
19870
20893
  import {
19871
20894
  computeTaskItemProgress,
19872
20895
  formatTaskList as formatTaskList2
19873
- } from "@wrongstack/core";
19874
- import {
19875
- mutateTasks as mutateTasks3
19876
- } from "@wrongstack/core";
20896
+ } from "@wrongstack/core/utils";
20897
+ import { mutateTasks as mutateTasks3 } from "@wrongstack/core/storage";
19877
20898
  import {
19878
20899
  addPlanItem as addPlanItem2,
19879
20900
  mutatePlan as mutatePlan3,
19880
20901
  formatPlan as formatPlan2
19881
- } from "@wrongstack/core";
20902
+ } from "@wrongstack/core/storage";
19882
20903
  import { randomUUID as randomUUID3 } from "node:crypto";
19883
20904
  function findTaskIndex(tasks, query) {
19884
20905
  const asNum = Number.parseInt(query, 10);
@@ -20248,7 +21269,7 @@ ${formatTaskList2(file.tasks)}` : file.tasks.length > 0 ? formatTaskList2(file.t
20248
21269
  init_spawn_stream();
20249
21270
  init_util();
20250
21271
  init_legacy_bridge();
20251
- import * as path30 from "node:path";
21272
+ import * as path31 from "node:path";
20252
21273
  var testTool = {
20253
21274
  name: "test",
20254
21275
  category: "Code Quality",
@@ -20355,7 +21376,7 @@ async function detectRunner(cwd) {
20355
21376
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
20356
21377
  for (const f of candidates) {
20357
21378
  try {
20358
- await stat17(path30.join(cwd, f));
21379
+ await stat17(path31.join(cwd, f));
20359
21380
  if (f.includes("vitest")) return "vitest";
20360
21381
  if (f.includes("jest")) return "jest";
20361
21382
  if (f.includes("mocha")) return "mocha";
@@ -20432,7 +21453,7 @@ function parseResult(runner, result, duration) {
20432
21453
  }
20433
21454
 
20434
21455
  // src/todo.ts
20435
- import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core";
21456
+ import { loadPlan as loadPlan2, loadTasks as loadTasks3, savePlan, saveTasks, setPlanItemStatus as setPlanItemStatus2 } from "@wrongstack/core/storage";
20436
21457
  var todoTool = {
20437
21458
  name: "todo",
20438
21459
  category: "Session",
@@ -20824,22 +21845,10 @@ var toolUseTool = {
20824
21845
 
20825
21846
  // src/tree.ts
20826
21847
  init_util();
20827
- import { expectDefined as expectDefined10 } from "@wrongstack/core";
21848
+ import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
20828
21849
  import * as fs28 from "node:fs/promises";
20829
- import * as path31 from "node:path";
20830
- var DEFAULT_IGNORE5 = [
20831
- "node_modules",
20832
- ".git",
20833
- "dist",
20834
- "build",
20835
- ".next",
20836
- "coverage",
20837
- "__pycache__",
20838
- ".wrongstack",
20839
- ".ssh",
20840
- ".gnupg",
20841
- ".aws"
20842
- ];
21850
+ import * as path32 from "node:path";
21851
+ var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
20843
21852
  var treeTool = {
20844
21853
  name: "tree",
20845
21854
  category: "Filesystem",
@@ -20997,7 +22006,7 @@ async function walkDir(dir, depth, opts) {
20997
22006
  opts.lines.push(opts.prefix + branch + displayName);
20998
22007
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
20999
22008
  const childPrefix = opts.prefix + connector;
21000
- await walkDir(path31.join(dir, entry.name), depth + 1, {
22009
+ await walkDir(path32.join(dir, entry.name), depth + 1, {
21001
22010
  ...opts,
21002
22011
  prefix: childPrefix,
21003
22012
  isLast
@@ -21010,7 +22019,7 @@ async function walkDir(dir, depth, opts) {
21010
22019
  init_spawn_stream();
21011
22020
  init_util();
21012
22021
  init_legacy_bridge();
21013
- import * as path32 from "node:path";
22022
+ import * as path33 from "node:path";
21014
22023
  var typecheckTool = {
21015
22024
  name: "typecheck",
21016
22025
  category: "Code Quality",
@@ -21113,8 +22122,8 @@ async function findTsConfig(cwd) {
21113
22122
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
21114
22123
  for (const f of candidates) {
21115
22124
  try {
21116
- const s = await stat17(path32.join(cwd, f));
21117
- if (s.isFile()) return path32.join(cwd, f);
22125
+ const s = await stat17(path33.join(cwd, f));
22126
+ if (s.isFile()) return path33.join(cwd, f);
21118
22127
  } catch {
21119
22128
  }
21120
22129
  }
@@ -21123,7 +22132,8 @@ async function findTsConfig(cwd) {
21123
22132
 
21124
22133
  // src/write.ts
21125
22134
  import * as fs29 from "node:fs/promises";
21126
- import { atomicWrite as atomicWrite5, ToolValidationError as ToolValidationError8, unifiedDiff as unifiedDiff3 } from "@wrongstack/core";
22135
+ import { ToolValidationError as ToolValidationError8 } from "@wrongstack/core/types";
22136
+ import { atomicWrite as atomicWrite5, unifiedDiff as unifiedDiff3 } from "@wrongstack/core/utils";
21127
22137
  init_util();
21128
22138
  var writeTool = {
21129
22139
  name: "write",