@wrongstack/tools 0.296.4 → 0.298.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +128 -65
  4. package/dist/bash.js +20 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +883 -351
  8. package/dist/codebase-index/index.js +95 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +76 -16
  11. package/dist/codebase-index/worker.js +36 -13
  12. package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
  13. package/dist/codebase-index/writer.d.ts +13 -1
  14. package/dist/design.d.ts +1 -1
  15. package/dist/design.js +4 -3
  16. package/dist/diff.js +11 -1
  17. package/dist/e2e.js +9 -6
  18. package/dist/edit.js +9 -6
  19. package/dist/exec.js +562 -516
  20. package/dist/format.js +131 -65
  21. package/dist/git.js +22 -2
  22. package/dist/glob.js +11 -5
  23. package/dist/grep.js +160 -104
  24. package/dist/index.js +834 -290
  25. package/dist/install.js +135 -68
  26. package/dist/json.js +64 -18
  27. package/dist/kanban-board-inputs.d.ts +21 -4
  28. package/dist/kanban.js +20 -5
  29. package/dist/languages/index.js +143 -71
  30. package/dist/lint.js +128 -65
  31. package/dist/next-steps.d.ts +23 -0
  32. package/dist/next-steps.js +4 -0
  33. package/dist/outdated.js +101 -26
  34. package/dist/pack.js +883 -351
  35. package/dist/patch.js +11 -1
  36. package/dist/plan.js +32 -10
  37. package/dist/process-registry.d.ts +9 -0
  38. package/dist/process-registry.js +15 -0
  39. package/dist/ps-slash.js +15 -0
  40. package/dist/read.js +104 -23
  41. package/dist/replace.js +11 -3
  42. package/dist/scaffold.js +9 -6
  43. package/dist/session-kanban.js +21 -5
  44. package/dist/task.js +3 -1
  45. package/dist/test.js +128 -65
  46. package/dist/todo.js +3 -1
  47. package/dist/tool-tier.js +883 -351
  48. package/dist/tool-use.d.ts +1 -1
  49. package/dist/tool-use.js +28 -4
  50. package/dist/tree.d.ts +1 -0
  51. package/dist/tree.js +44 -8
  52. package/dist/typecheck.js +128 -65
  53. package/dist/write.js +9 -6
  54. package/package.json +5 -4
  55. package/dist/_concurrency.d.ts.map +0 -1
  56. package/dist/_danger-detect.d.ts.map +0 -1
  57. package/dist/_edit-match.d.ts.map +0 -1
  58. package/dist/_env.d.ts.map +0 -1
  59. package/dist/_fetch-guard.d.ts.map +0 -1
  60. package/dist/_output-spool.d.ts.map +0 -1
  61. package/dist/_redact-command.d.ts.map +0 -1
  62. package/dist/_regex.d.ts.map +0 -1
  63. package/dist/_session-shell.d.ts.map +0 -1
  64. package/dist/_shell-pick.d.ts.map +0 -1
  65. package/dist/_spawn-stream.d.ts.map +0 -1
  66. package/dist/_syntax-check.d.ts.map +0 -1
  67. package/dist/_util.d.ts.map +0 -1
  68. package/dist/_win32-resolve.d.ts.map +0 -1
  69. package/dist/audit.d.ts.map +0 -1
  70. package/dist/audit.js.map +0 -7
  71. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  72. package/dist/auto-proceed-loop-guard.js.map +0 -7
  73. package/dist/bash-kill-guard.d.ts.map +0 -1
  74. package/dist/bash.d.ts.map +0 -1
  75. package/dist/bash.js.map +0 -7
  76. package/dist/batch-tool-use.d.ts.map +0 -1
  77. package/dist/batch-tool-use.js.map +0 -7
  78. package/dist/browser/artifacts.d.ts.map +0 -1
  79. package/dist/browser/index.d.ts.map +0 -1
  80. package/dist/browser/index.js.map +0 -7
  81. package/dist/browser/manager.d.ts.map +0 -1
  82. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  83. package/dist/browser/security.d.ts.map +0 -1
  84. package/dist/browser/tools.d.ts.map +0 -1
  85. package/dist/browser/types.d.ts.map +0 -1
  86. package/dist/builtin.d.ts.map +0 -1
  87. package/dist/builtin.js.map +0 -7
  88. package/dist/circuit-breaker.d.ts.map +0 -1
  89. package/dist/circuit-breaker.js.map +0 -7
  90. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  91. package/dist/codebase-index/bm25.d.ts.map +0 -1
  92. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  93. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  94. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  95. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  96. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  97. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  98. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  99. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/index-service.d.ts.map +0 -1
  101. package/dist/codebase-index/index.d.ts.map +0 -1
  102. package/dist/codebase-index/index.js.map +0 -7
  103. package/dist/codebase-index/indexer.d.ts.map +0 -1
  104. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  105. package/dist/codebase-index/languages.d.ts.map +0 -1
  106. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  107. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  109. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  110. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  111. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  112. package/dist/codebase-index/project-server.d.ts.map +0 -1
  113. package/dist/codebase-index/project-server.js.map +0 -7
  114. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  115. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  116. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  117. package/dist/codebase-index/schema.d.ts.map +0 -1
  118. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  119. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  120. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  121. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  122. package/dist/codebase-index/worker.d.ts.map +0 -1
  123. package/dist/codebase-index/worker.js.map +0 -7
  124. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  129. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  130. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  131. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  132. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  133. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  134. package/dist/codebase-index/writer.d.ts.map +0 -1
  135. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  136. package/dist/design.d.ts.map +0 -1
  137. package/dist/design.js.map +0 -7
  138. package/dist/diff.d.ts.map +0 -1
  139. package/dist/diff.js.map +0 -7
  140. package/dist/document.d.ts.map +0 -1
  141. package/dist/document.js.map +0 -7
  142. package/dist/e2e.d.ts.map +0 -1
  143. package/dist/e2e.js.map +0 -7
  144. package/dist/edit.d.ts.map +0 -1
  145. package/dist/edit.js.map +0 -7
  146. package/dist/exec-kill-guard.d.ts.map +0 -1
  147. package/dist/exec.d.ts.map +0 -1
  148. package/dist/exec.js.map +0 -7
  149. package/dist/fetch.d.ts.map +0 -1
  150. package/dist/fetch.js.map +0 -7
  151. package/dist/format.d.ts.map +0 -1
  152. package/dist/format.js.map +0 -7
  153. package/dist/git.d.ts.map +0 -1
  154. package/dist/git.js.map +0 -7
  155. package/dist/glob.d.ts.map +0 -1
  156. package/dist/glob.js.map +0 -7
  157. package/dist/grep.d.ts.map +0 -1
  158. package/dist/grep.js.map +0 -7
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js.map +0 -7
  161. package/dist/install.d.ts.map +0 -1
  162. package/dist/install.js.map +0 -7
  163. package/dist/json.d.ts.map +0 -1
  164. package/dist/json.js.map +0 -7
  165. package/dist/kanban-board-inputs.d.ts.map +0 -1
  166. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  167. package/dist/kanban-detail-actions.d.ts.map +0 -1
  168. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  169. package/dist/kanban-presence.d.ts.map +0 -1
  170. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  171. package/dist/kanban-task-inputs.d.ts.map +0 -1
  172. package/dist/kanban-tool-results.d.ts.map +0 -1
  173. package/dist/kanban-tool-schema.d.ts.map +0 -1
  174. package/dist/kanban-tool-types.d.ts.map +0 -1
  175. package/dist/kanban.d.ts.map +0 -1
  176. package/dist/kanban.js.map +0 -7
  177. package/dist/languages/detect.d.ts.map +0 -1
  178. package/dist/languages/diagnostics.d.ts.map +0 -1
  179. package/dist/languages/execute-tool.d.ts.map +0 -1
  180. package/dist/languages/execute.d.ts.map +0 -1
  181. package/dist/languages/index.d.ts.map +0 -1
  182. package/dist/languages/index.js.map +0 -7
  183. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  184. package/dist/languages/package-tool.d.ts.map +0 -1
  185. package/dist/languages/plan.d.ts.map +0 -1
  186. package/dist/languages/profile-helpers.d.ts.map +0 -1
  187. package/dist/languages/profiles/additional.d.ts.map +0 -1
  188. package/dist/languages/profiles/primary.d.ts.map +0 -1
  189. package/dist/languages/registry.d.ts.map +0 -1
  190. package/dist/languages/tool.d.ts.map +0 -1
  191. package/dist/languages/types.d.ts.map +0 -1
  192. package/dist/lint.d.ts.map +0 -1
  193. package/dist/lint.js.map +0 -7
  194. package/dist/logs.d.ts.map +0 -1
  195. package/dist/logs.js.map +0 -7
  196. package/dist/memory.d.ts.map +0 -1
  197. package/dist/memory.js.map +0 -7
  198. package/dist/mode.d.ts.map +0 -1
  199. package/dist/mode.js.map +0 -7
  200. package/dist/next-steps.d.ts.map +0 -1
  201. package/dist/next-steps.js.map +0 -7
  202. package/dist/outdated.d.ts.map +0 -1
  203. package/dist/outdated.js.map +0 -7
  204. package/dist/pack.d.ts.map +0 -1
  205. package/dist/pack.js.map +0 -7
  206. package/dist/patch.d.ts.map +0 -1
  207. package/dist/patch.js.map +0 -7
  208. package/dist/plan.d.ts.map +0 -1
  209. package/dist/plan.js.map +0 -7
  210. package/dist/process-guardian.d.ts.map +0 -1
  211. package/dist/process-registry-persistent.d.ts.map +0 -1
  212. package/dist/process-registry.d.ts.map +0 -1
  213. package/dist/process-registry.js.map +0 -7
  214. package/dist/ps-slash.d.ts.map +0 -1
  215. package/dist/ps-slash.js.map +0 -7
  216. package/dist/read.d.ts.map +0 -1
  217. package/dist/read.js.map +0 -7
  218. package/dist/replace.d.ts.map +0 -1
  219. package/dist/replace.js.map +0 -7
  220. package/dist/scaffold.d.ts.map +0 -1
  221. package/dist/scaffold.js.map +0 -7
  222. package/dist/search.d.ts.map +0 -1
  223. package/dist/search.js.map +0 -7
  224. package/dist/session-kanban.d.ts.map +0 -1
  225. package/dist/session-kanban.js.map +0 -7
  226. package/dist/set-working-dir.d.ts.map +0 -1
  227. package/dist/skill.d.ts.map +0 -1
  228. package/dist/skill.js.map +0 -7
  229. package/dist/spawn-background.d.ts.map +0 -1
  230. package/dist/task.d.ts.map +0 -1
  231. package/dist/task.js.map +0 -7
  232. package/dist/test.d.ts.map +0 -1
  233. package/dist/test.js.map +0 -7
  234. package/dist/todo.d.ts.map +0 -1
  235. package/dist/todo.js.map +0 -7
  236. package/dist/tool-diff.d.ts.map +0 -1
  237. package/dist/tool-diff.js.map +0 -7
  238. package/dist/tool-help.d.ts.map +0 -1
  239. package/dist/tool-help.js.map +0 -7
  240. package/dist/tool-icon-map.d.ts.map +0 -1
  241. package/dist/tool-icons.d.ts.map +0 -1
  242. package/dist/tool-icons.js.map +0 -7
  243. package/dist/tool-search.d.ts.map +0 -1
  244. package/dist/tool-search.js.map +0 -7
  245. package/dist/tool-summary.d.ts.map +0 -1
  246. package/dist/tool-summary.js.map +0 -7
  247. package/dist/tool-tier.d.ts.map +0 -1
  248. package/dist/tool-tier.js.map +0 -7
  249. package/dist/tool-use.d.ts.map +0 -1
  250. package/dist/tool-use.js.map +0 -7
  251. package/dist/tree.d.ts.map +0 -1
  252. package/dist/tree.js.map +0 -7
  253. package/dist/typecheck.d.ts.map +0 -1
  254. package/dist/typecheck.js.map +0 -7
  255. package/dist/win32.d.ts.map +0 -1
  256. package/dist/win32.js.map +0 -7
  257. package/dist/write.d.ts.map +0 -1
  258. package/dist/write.js.map +0 -7
@@ -2875,6 +2875,7 @@ var REFS_INDEX_SQL = [
2875
2875
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2876
2876
 
2877
2877
  // src/codebase-index/writer-search-helpers.ts
2878
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2878
2879
  function normalizeSearchLimit(limit) {
2879
2880
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2880
2881
  }
@@ -2904,8 +2905,8 @@ function buildWriterSearchWhere(query, filter) {
2904
2905
  }
2905
2906
  if (query.trim()) {
2906
2907
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2907
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2908
- for (const token of tokens) values.push(`%${token}%`);
2908
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2909
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2909
2910
  }
2910
2911
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2911
2912
  }
@@ -3014,6 +3015,7 @@ var StorePool = class {
3014
3015
 
3015
3016
  // src/codebase-index/writer.ts
3016
3017
  var DB_FILE2 = "index.db";
3018
+ var MAX_STATEMENT_CACHE = 128;
3017
3019
  var IndexStore = class _IndexStore {
3018
3020
  db;
3019
3021
  /** Absolute path to this project's index directory. */
@@ -3051,12 +3053,31 @@ var IndexStore = class _IndexStore {
3051
3053
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3052
3054
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3053
3055
  bm25Dirty = true;
3054
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3056
+ /**
3057
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3058
+ *
3059
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3060
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3061
+ * — so the SQL varies with the token count and a stream of differently-sized
3062
+ * queries grew the cache without limit. Sage's store already bounds its
3063
+ * equivalent at 128 (WS-096).
3064
+ *
3065
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3066
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3067
+ * itself rather than the reindex hot path.
3068
+ */
3055
3069
  stmt(sql) {
3056
- let s = this.stmtCache.get(sql);
3057
- if (s === void 0) {
3058
- s = this.db.prepare(sql);
3059
- this.stmtCache.set(sql, s);
3070
+ const cached = this.stmtCache.get(sql);
3071
+ if (cached !== void 0) {
3072
+ this.stmtCache.delete(sql);
3073
+ this.stmtCache.set(sql, cached);
3074
+ return cached;
3075
+ }
3076
+ const s = this.db.prepare(sql);
3077
+ this.stmtCache.set(sql, s);
3078
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3079
+ const oldest = this.stmtCache.keys().next();
3080
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3060
3081
  }
3061
3082
  return s;
3062
3083
  }
@@ -3329,7 +3350,7 @@ var IndexStore = class _IndexStore {
3329
3350
  const { where, values } = built;
3330
3351
  const limit = normalizeSearchLimit(opts?.limit);
3331
3352
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3332
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3353
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3333
3354
  const binds = limit !== void 0 ? [...values, limit] : values;
3334
3355
  const rows = this.stmt(sql).all(
3335
3356
  ...binds
@@ -3458,11 +3479,13 @@ var IndexStore = class _IndexStore {
3458
3479
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3459
3480
  searchRankedFallback(query, filter, limit) {
3460
3481
  if (!query.trim()) {
3461
- const total = this.countSearch(query, filter);
3462
- if (total === 0) return { results: [], total: 0 };
3463
- return { results: this.search(query, filter, { limit }), total };
3482
+ const total2 = this.countSearch(query, filter);
3483
+ if (total2 === 0) return { results: [], total: 0 };
3484
+ return { results: this.search(query, filter, { limit }), total: total2 };
3464
3485
  }
3465
- const candidates = this.search(query, filter);
3486
+ const total = this.countSearch(query, filter);
3487
+ if (total === 0) return { results: [], total: 0 };
3488
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3466
3489
  if (candidates.length === 0) return { results: [], total: 0 };
3467
3490
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3468
3491
  const bm25 = this.getOrBuildBm25();
@@ -3488,7 +3511,7 @@ var IndexStore = class _IndexStore {
3488
3511
  const c = expectDefined(candidateById.get(id));
3489
3512
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3490
3513
  });
3491
- return { results, total: candidates.length };
3514
+ return { results, total };
3492
3515
  }
3493
3516
  getAllIndexable() {
3494
3517
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -4082,6 +4105,8 @@ var ProjectServerConnection = class {
4082
4105
  connectResolve = null;
4083
4106
  connectReject = null;
4084
4107
  nextId = 1;
4108
+ /** WS-027: read from the owner-only metadata file — see currentAuthToken. */
4109
+ authToken;
4085
4110
  pending = /* @__PURE__ */ new Map();
4086
4111
  transition(status, options = {}) {
4087
4112
  const previous = connectionStates.get(this.endpoint);
@@ -4102,6 +4127,10 @@ var ProjectServerConnection = class {
4102
4127
  isConnected() {
4103
4128
  return this.socket !== null && !this.socket.destroyed && this.info !== null;
4104
4129
  }
4130
+ /** Safe LRU candidate: no request, connect, or health probe is in flight. */
4131
+ isEvictable() {
4132
+ return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
4133
+ }
4105
4134
  async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
4106
4135
  await this.ensureConnected(spawnIfMissing);
4107
4136
  if (this.healthCheck) return this.healthCheck;
@@ -4214,6 +4243,31 @@ var ProjectServerConnection = class {
4214
4243
  this.transition("offline");
4215
4244
  maybeStopHeartbeatLoop();
4216
4245
  }
4246
+ /**
4247
+ * WS-027: the per-process token, read from the daemon's owner-only metadata
4248
+ * file — never from the `hello` frame, which the daemon sends to every
4249
+ * socket that connects (the mistake WS-028 found in the SAGE daemon).
4250
+ *
4251
+ * Read lazily and re-read while unknown: the daemon starts listening before
4252
+ * it writes metadata (endpoint ownership has to be won first), and a
4253
+ * respawned daemon mints a new token, so a cached wrong one would be sticky.
4254
+ */
4255
+ currentAuthToken() {
4256
+ if (this.authToken === void 0) {
4257
+ try {
4258
+ const raw = fs4.readFileSync(
4259
+ projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
4260
+ "utf8"
4261
+ );
4262
+ const parsed = JSON.parse(raw);
4263
+ if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
4264
+ this.authToken = parsed.authToken;
4265
+ }
4266
+ } catch {
4267
+ }
4268
+ }
4269
+ return this.authToken;
4270
+ }
4217
4271
  request(message, options) {
4218
4272
  const socket = this.socket;
4219
4273
  if (!socket || socket.destroyed) {
@@ -4257,7 +4311,7 @@ var ProjectServerConnection = class {
4257
4311
  return;
4258
4312
  }
4259
4313
  }
4260
- this.write({ ...message, id });
4314
+ this.write({ ...message, id, authToken: this.currentAuthToken() });
4261
4315
  });
4262
4316
  }
4263
4317
  async ensureConnected(spawnIfMissing) {
@@ -4304,6 +4358,7 @@ var ProjectServerConnection = class {
4304
4358
  connectOnce() {
4305
4359
  this.socket?.destroy();
4306
4360
  this.socket = null;
4361
+ this.authToken = void 0;
4307
4362
  this.info = null;
4308
4363
  this.activity = null;
4309
4364
  this.health = null;
@@ -4500,7 +4555,26 @@ var ProjectServerConnection = class {
4500
4555
  }
4501
4556
  };
4502
4557
  var connections = /* @__PURE__ */ new Map();
4558
+ var MAX_CACHED_CONNECTIONS = 8;
4503
4559
  var heartbeatTimer;
4560
+ function forgetConnection(endpoint, connection) {
4561
+ if (connections.get(endpoint) === connection) connections.delete(endpoint);
4562
+ connection.close();
4563
+ connectionStates.delete(endpoint);
4564
+ if (latestConnectionState.endpoint !== endpoint) return;
4565
+ latestConnectionState = [...connectionStates.values()].at(-1) ?? {
4566
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
4567
+ connected: false
4568
+ };
4569
+ }
4570
+ function trimConnectionCache(protectedConnection) {
4571
+ if (connections.size <= MAX_CACHED_CONNECTIONS) return;
4572
+ for (const [endpoint, connection] of connections) {
4573
+ if (connections.size <= MAX_CACHED_CONNECTIONS) break;
4574
+ if (connection === protectedConnection || !connection.isEvictable()) continue;
4575
+ forgetConnection(endpoint, connection);
4576
+ }
4577
+ }
4504
4578
  function ensureHeartbeatLoop() {
4505
4579
  if (heartbeatTimer) return;
4506
4580
  heartbeatTimer = setInterval(() => {
@@ -4523,7 +4597,11 @@ function connectionFor(projectRoot, indexDir) {
4523
4597
  if (!connection) {
4524
4598
  connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
4525
4599
  connections.set(endpoint, connection);
4600
+ } else {
4601
+ connections.delete(endpoint);
4602
+ connections.set(endpoint, connection);
4526
4603
  }
4604
+ trimConnectionCache(connection);
4527
4605
  return connection;
4528
4606
  }
4529
4607
  function callProjectIndexServer(op, args, options) {
@@ -6243,8 +6321,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
6243
6321
  const content = fs12.readFileSync(epFile, "utf8");
6244
6322
  const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
6245
6323
  const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
6246
- let match;
6247
- while ((match = reExportRe.exec(strippedContent)) !== null) {
6324
+ for (; ; ) {
6325
+ const match = reExportRe.exec(strippedContent);
6326
+ if (match === null) break;
6248
6327
  const moduleSpec = match[1];
6249
6328
  const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
6250
6329
  for (const rf of resolvedFiles) {
@@ -1,6 +1,10 @@
1
1
  import type { OpName, OpShapes } from './worker-protocol.js';
2
2
  /** Hard ceiling for one newline-delimited IPC message (measured as JS characters). */
3
3
  export declare const PROJECT_INDEX_SERVER_MAX_FRAME_CHARS: number;
4
+ /**
5
+ * What the daemon tells a connecting client about itself. Carries NO secret —
6
+ * see {@link ProjectIndexServerMetadata}.
7
+ */
4
8
  export interface ProjectIndexServerInfo {
5
9
  protocolVersion: number;
6
10
  buildId: string;
@@ -41,11 +45,30 @@ export interface ProjectIndexServerHealth {
41
45
  oldestClientIdleMs?: number | undefined;
42
46
  activity: ProjectIndexServerActivity;
43
47
  }
48
+ /**
49
+ * The daemon's on-disk metadata file, written 0600. The auth token exists here
50
+ * and nowhere else — never in the `hello` frame, which the daemon sends to
51
+ * every socket that connects (the mistake WS-028 found in the SAGE daemon).
52
+ */
53
+ export interface ProjectIndexServerMetadata extends ProjectIndexServerInfo {
54
+ authToken: string;
55
+ }
56
+ /**
57
+ * WS-027: this daemon owns the project's code index — it reads every file in
58
+ * the repository and answers content queries over it — and it had no handshake
59
+ * credential at all. Any process that could open the socket could search the
60
+ * whole codebase through it or drive a reindex. `authToken` comes from the
61
+ * daemon's owner-only metadata file, so a caller must prove same-user access.
62
+ *
63
+ * `cancel` is ungated: it reaches only the sending connection's own in-flight
64
+ * request, so there is nothing to escalate.
65
+ */
44
66
  export type ProjectServerClientMessage = {
45
67
  type: 'request';
46
68
  id: number;
47
69
  op: OpName;
48
70
  args: OpShapes[OpName]['args'];
71
+ authToken?: string | undefined;
49
72
  } | {
50
73
  type: 'cancel';
51
74
  id: number;
@@ -54,13 +77,16 @@ export type ProjectServerClientMessage = {
54
77
  id: number;
55
78
  watchExternal: boolean;
56
79
  debounceMs: number;
80
+ authToken?: string | undefined;
57
81
  } | {
58
82
  type: 'ping';
59
83
  id: number;
84
+ authToken?: string | undefined;
60
85
  } | {
61
86
  type: 'shutdown';
62
87
  id: number;
63
88
  reason?: string | undefined;
89
+ authToken?: string | undefined;
64
90
  };
65
91
  export type ProjectServerMessage = ({
66
92
  type: 'hello';
@@ -1972,11 +1972,13 @@ var init_yaml_parser = __esm({
1972
1972
  });
1973
1973
 
1974
1974
  // src/codebase-index/project-server.ts
1975
+ import { randomBytes } from "node:crypto";
1975
1976
  import * as fs10 from "node:fs";
1976
1977
  import * as net from "node:net";
1977
1978
  import * as path13 from "node:path";
1978
1979
  import {
1979
1980
  DEFAULT_WALK_IGNORE_SET,
1981
+ startSharedHeapWatchdog,
1980
1982
  useDaemonPerfDefaults,
1981
1983
  watchProjectTree
1982
1984
  } from "@wrongstack/core/utils";
@@ -2938,6 +2940,7 @@ var REFS_INDEX_SQL = [
2938
2940
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2939
2941
 
2940
2942
  // src/codebase-index/writer-search-helpers.ts
2943
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2941
2944
  function normalizeSearchLimit(limit) {
2942
2945
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2943
2946
  }
@@ -2967,8 +2970,8 @@ function buildWriterSearchWhere(query, filter) {
2967
2970
  }
2968
2971
  if (query.trim()) {
2969
2972
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2970
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2971
- for (const token of tokens) values.push(`%${token}%`);
2973
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2974
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2972
2975
  }
2973
2976
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2974
2977
  }
@@ -3077,6 +3080,7 @@ var StorePool = class {
3077
3080
 
3078
3081
  // src/codebase-index/writer.ts
3079
3082
  var DB_FILE2 = "index.db";
3083
+ var MAX_STATEMENT_CACHE = 128;
3080
3084
  var IndexStore = class _IndexStore {
3081
3085
  db;
3082
3086
  /** Absolute path to this project's index directory. */
@@ -3114,12 +3118,31 @@ var IndexStore = class _IndexStore {
3114
3118
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3115
3119
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3116
3120
  bm25Dirty = true;
3117
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3121
+ /**
3122
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3123
+ *
3124
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3125
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3126
+ * — so the SQL varies with the token count and a stream of differently-sized
3127
+ * queries grew the cache without limit. Sage's store already bounds its
3128
+ * equivalent at 128 (WS-096).
3129
+ *
3130
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3131
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3132
+ * itself rather than the reindex hot path.
3133
+ */
3118
3134
  stmt(sql) {
3119
- let s = this.stmtCache.get(sql);
3120
- if (s === void 0) {
3121
- s = this.db.prepare(sql);
3122
- this.stmtCache.set(sql, s);
3135
+ const cached = this.stmtCache.get(sql);
3136
+ if (cached !== void 0) {
3137
+ this.stmtCache.delete(sql);
3138
+ this.stmtCache.set(sql, cached);
3139
+ return cached;
3140
+ }
3141
+ const s = this.db.prepare(sql);
3142
+ this.stmtCache.set(sql, s);
3143
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3144
+ const oldest = this.stmtCache.keys().next();
3145
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3123
3146
  }
3124
3147
  return s;
3125
3148
  }
@@ -3392,7 +3415,7 @@ var IndexStore = class _IndexStore {
3392
3415
  const { where, values } = built;
3393
3416
  const limit = normalizeSearchLimit(opts?.limit);
3394
3417
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3395
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3418
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3396
3419
  const binds = limit !== void 0 ? [...values, limit] : values;
3397
3420
  const rows = this.stmt(sql).all(
3398
3421
  ...binds
@@ -3521,11 +3544,13 @@ var IndexStore = class _IndexStore {
3521
3544
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3522
3545
  searchRankedFallback(query, filter, limit) {
3523
3546
  if (!query.trim()) {
3524
- const total = this.countSearch(query, filter);
3525
- if (total === 0) return { results: [], total: 0 };
3526
- return { results: this.search(query, filter, { limit }), total };
3547
+ const total2 = this.countSearch(query, filter);
3548
+ if (total2 === 0) return { results: [], total: 0 };
3549
+ return { results: this.search(query, filter, { limit }), total: total2 };
3527
3550
  }
3528
- const candidates = this.search(query, filter);
3551
+ const total = this.countSearch(query, filter);
3552
+ if (total === 0) return { results: [], total: 0 };
3553
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3529
3554
  if (candidates.length === 0) return { results: [], total: 0 };
3530
3555
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3531
3556
  const bm25 = this.getOrBuildBm25();
@@ -3551,7 +3576,7 @@ var IndexStore = class _IndexStore {
3551
3576
  const c = expectDefined4(candidateById.get(id));
3552
3577
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3553
3578
  });
3554
- return { results, total: candidates.length };
3579
+ return { results, total };
3555
3580
  }
3556
3581
  getAllIndexable() {
3557
3582
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -4586,6 +4611,11 @@ var clientLeaseMsRaw = Number(process.env["WRONGSTACK_INDEX_SERVER_CLIENT_LEASE_
4586
4611
  var clientLeaseMs = Number.isFinite(clientLeaseMsRaw) && clientLeaseMsRaw >= 100 ? clientLeaseMsRaw : DEFAULT_CLIENT_LEASE_MS;
4587
4612
  var clientLeaseSweepMs = Math.min(1e4, Math.max(100, Math.floor(clientLeaseMs / 3)));
4588
4613
  var startedAt = (/* @__PURE__ */ new Date()).toISOString();
4614
+ var authToken = randomBytes(16).toString("hex");
4615
+ var markMetadataWritten;
4616
+ var metadataWritten = new Promise((resolve4) => {
4617
+ markMetadataWritten = resolve4;
4618
+ });
4589
4619
  var serverInfo = {
4590
4620
  protocolVersion: PROJECT_INDEX_SERVER_PROTOCOL_VERSION,
4591
4621
  buildId: projectIndexServerBuildId(import.meta.url),
@@ -4617,6 +4647,20 @@ var indexActivity = {
4617
4647
  updatedAt: null,
4618
4648
  lastError: null
4619
4649
  };
4650
+ var stopMemoryWatchdog = startSharedHeapWatchdog({
4651
+ collectStats: () => ({
4652
+ surface: "codebase-index-project-server",
4653
+ clients: clients.size,
4654
+ activeRequests,
4655
+ activeWrites,
4656
+ queuedWrites,
4657
+ searchCache: searchCache.size,
4658
+ statsCache: statsCache.size,
4659
+ packageGraphCache: packageGraphCache.size,
4660
+ fileGraphCache: fileGraphCache.size,
4661
+ symbolGraphCache: symbolGraphCache.size
4662
+ })
4663
+ });
4620
4664
  var lastProgressBroadcastAt = 0;
4621
4665
  var externalWatcher;
4622
4666
  var DEFAULT_EXTERNAL_DEBOUNCE_MS = 400;
@@ -4846,6 +4890,16 @@ async function handleMessage(state, message) {
4846
4890
  state.cancel.get(message.id)?.();
4847
4891
  return;
4848
4892
  }
4893
+ if (message.authToken !== authToken) {
4894
+ send(state, {
4895
+ type: "response",
4896
+ id: message.id,
4897
+ ok: false,
4898
+ error: "Codebase-index IPC request rejected: missing or invalid authToken. Reconnect to refresh metadata (server.json#authToken).",
4899
+ errorName: "UnauthorizedIndexRequest"
4900
+ });
4901
+ return;
4902
+ }
4849
4903
  if (message.type === "ping") {
4850
4904
  send(state, { type: "response", id: message.id, ok: true, result: serverHealth() });
4851
4905
  return;
@@ -5032,7 +5086,8 @@ function removeMetadataIfOwned() {
5032
5086
  function writeMetadata() {
5033
5087
  fs10.mkdirSync(path13.dirname(metadataPath), { recursive: true });
5034
5088
  const temporary = `${metadataPath}.${process.pid}.tmp`;
5035
- fs10.writeFileSync(temporary, `${JSON.stringify(serverInfo, null, 2)}
5089
+ const metadata = { ...serverInfo, authToken };
5090
+ fs10.writeFileSync(temporary, `${JSON.stringify(metadata, null, 2)}
5036
5091
  `, { mode: 384 });
5037
5092
  try {
5038
5093
  fs10.renameSync(temporary, metadataPath);
@@ -5056,8 +5111,11 @@ var server = net.createServer((socket) => {
5056
5111
  lastSeenAt: Date.now()
5057
5112
  };
5058
5113
  clients.add(state);
5059
- send(state, { type: "hello", ...serverInfo });
5060
- send(state, { type: "index-state", state: indexActivity });
5114
+ void metadataWritten.then(() => {
5115
+ if (socket.destroyed) return;
5116
+ send(state, { type: "hello", ...serverInfo });
5117
+ send(state, { type: "index-state", state: indexActivity });
5118
+ });
5061
5119
  socket.on("data", (chunk) => consume(state, chunk));
5062
5120
  socket.on("close", () => {
5063
5121
  for (const cancel of state.cancel.values()) cancel();
@@ -5109,6 +5167,7 @@ async function stop(_reason) {
5109
5167
  } catch {
5110
5168
  }
5111
5169
  }
5170
+ await stopMemoryWatchdog();
5112
5171
  }
5113
5172
  ensureProjectIndexSocketDirectory(endpoint);
5114
5173
  server.once("error", (error) => {
@@ -5128,6 +5187,7 @@ server.listen(endpoint, () => {
5128
5187
  }
5129
5188
  }
5130
5189
  writeMetadata();
5190
+ markMetadataWritten?.();
5131
5191
  scheduleIdleStop();
5132
5192
  });
5133
5193
  process.once("SIGINT", () => void stop("SIGINT"));
@@ -2927,6 +2927,7 @@ var REFS_INDEX_SQL = [
2927
2927
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
2928
2928
 
2929
2929
  // src/codebase-index/writer-search-helpers.ts
2930
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
2930
2931
  function normalizeSearchLimit(limit) {
2931
2932
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
2932
2933
  }
@@ -2956,8 +2957,8 @@ function buildWriterSearchWhere(query, filter) {
2956
2957
  }
2957
2958
  if (query.trim()) {
2958
2959
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
2959
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
2960
- for (const token of tokens) values.push(`%${token}%`);
2960
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
2961
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
2961
2962
  }
2962
2963
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
2963
2964
  }
@@ -3066,6 +3067,7 @@ var StorePool = class {
3066
3067
 
3067
3068
  // src/codebase-index/writer.ts
3068
3069
  var DB_FILE2 = "index.db";
3070
+ var MAX_STATEMENT_CACHE = 128;
3069
3071
  var IndexStore = class _IndexStore {
3070
3072
  db;
3071
3073
  /** Absolute path to this project's index directory. */
@@ -3103,12 +3105,31 @@ var IndexStore = class _IndexStore {
3103
3105
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
3104
3106
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
3105
3107
  bm25Dirty = true;
3106
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
3108
+ /**
3109
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
3110
+ *
3111
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
3112
+ * the fallback search builder emits one `text LIKE ?` clause per query token
3113
+ * — so the SQL varies with the token count and a stream of differently-sized
3114
+ * queries grew the cache without limit. Sage's store already bounds its
3115
+ * equivalent at 128 (WS-096).
3116
+ *
3117
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
3118
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
3119
+ * itself rather than the reindex hot path.
3120
+ */
3107
3121
  stmt(sql) {
3108
- let s = this.stmtCache.get(sql);
3109
- if (s === void 0) {
3110
- s = this.db.prepare(sql);
3111
- this.stmtCache.set(sql, s);
3122
+ const cached = this.stmtCache.get(sql);
3123
+ if (cached !== void 0) {
3124
+ this.stmtCache.delete(sql);
3125
+ this.stmtCache.set(sql, cached);
3126
+ return cached;
3127
+ }
3128
+ const s = this.db.prepare(sql);
3129
+ this.stmtCache.set(sql, s);
3130
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
3131
+ const oldest = this.stmtCache.keys().next();
3132
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
3112
3133
  }
3113
3134
  return s;
3114
3135
  }
@@ -3381,7 +3402,7 @@ var IndexStore = class _IndexStore {
3381
3402
  const { where, values } = built;
3382
3403
  const limit = normalizeSearchLimit(opts?.limit);
3383
3404
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
3384
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
3405
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
3385
3406
  const binds = limit !== void 0 ? [...values, limit] : values;
3386
3407
  const rows = this.stmt(sql).all(
3387
3408
  ...binds
@@ -3510,11 +3531,13 @@ var IndexStore = class _IndexStore {
3510
3531
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
3511
3532
  searchRankedFallback(query, filter, limit) {
3512
3533
  if (!query.trim()) {
3513
- const total = this.countSearch(query, filter);
3514
- if (total === 0) return { results: [], total: 0 };
3515
- return { results: this.search(query, filter, { limit }), total };
3534
+ const total2 = this.countSearch(query, filter);
3535
+ if (total2 === 0) return { results: [], total: 0 };
3536
+ return { results: this.search(query, filter, { limit }), total: total2 };
3516
3537
  }
3517
- const candidates = this.search(query, filter);
3538
+ const total = this.countSearch(query, filter);
3539
+ if (total === 0) return { results: [], total: 0 };
3540
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
3518
3541
  if (candidates.length === 0) return { results: [], total: 0 };
3519
3542
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
3520
3543
  const bm25 = this.getOrBuildBm25();
@@ -3540,7 +3563,7 @@ var IndexStore = class _IndexStore {
3540
3563
  const c = expectDefined4(candidateById.get(id));
3541
3564
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3542
3565
  });
3543
- return { results, total: candidates.length };
3566
+ return { results, total };
3544
3567
  }
3545
3568
  getAllIndexable() {
3546
3569
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -19,6 +19,22 @@ export interface WriterSearchRow {
19
19
  score?: number;
20
20
  snippet?: string;
21
21
  }
22
+ /**
23
+ * Ceiling on how many LIKE candidates the FTS5-less ranked fallback pulls into
24
+ * memory before scoring (WS-031).
25
+ *
26
+ * The fallback ranks with an in-process BM25 pass, so it needs more than the
27
+ * caller's `limit` (≤100) to rank well — but it does not need the whole corpus.
28
+ * Without a bound, a broad query on a large index materializes every matching
29
+ * row as a JS object; this subsystem has a documented OOM history, and search
30
+ * runs on the request path.
31
+ *
32
+ * Set far above any query a human would type, so ranking quality is unchanged
33
+ * in practice: it bounds the pathological case, it does not shape normal ones.
34
+ * `total` is reported from a SQL `COUNT(*)` rather than the truncated array, so
35
+ * capping never makes the reported match count wrong.
36
+ */
37
+ export declare const SEARCH_CANDIDATE_SCAN_CAP = 5000;
22
38
  export declare function normalizeSearchLimit(limit: number | undefined): number | undefined;
23
39
  export declare function buildWriterSearchWhere(query: string, filter?: WriterSearchFilter | undefined): {
24
40
  where: string;
@@ -38,7 +38,19 @@ export declare class IndexStore {
38
38
  */
39
39
  private bm25Cache;
40
40
  private bm25Dirty;
41
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
41
+ /**
42
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
43
+ *
44
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
45
+ * the fallback search builder emits one `text LIKE ?` clause per query token
46
+ * — so the SQL varies with the token count and a stream of differently-sized
47
+ * queries grew the cache without limit. Sage's store already bounds its
48
+ * equivalent at 128 (WS-096).
49
+ *
50
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
51
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
52
+ * itself rather than the reindex hot path.
53
+ */
42
54
  private stmt;
43
55
  constructor(projectRoot: string, opts?: {
44
56
  indexDir?: string | undefined;
package/dist/design.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Tool } from '@wrongstack/core/types';
2
1
  import { type SemanticTune } from '@wrongstack/core/design';
2
+ import type { Tool } from '@wrongstack/core/types';
3
3
  type Overrides = Record<string, string>;
4
4
  interface DesignInput {
5
5
  action?: 'list' | 'use' | 'foundations' | 'set' | 'tune' | 'materialize' | 'verify' | undefined;
package/dist/design.js CHANGED
@@ -48,6 +48,9 @@ var designTool = {
48
48
  description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
49
49
  usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
50
50
  permission: "confirm",
51
+ // WS-046: gives permission decisions something to key on.
52
+ // The action performed; `out` is optional so it cannot be the subject.
53
+ subjectKey: "action",
51
54
  mutating: true,
52
55
  capabilities: ["fs.write"],
53
56
  timeoutMs: 15e3,
@@ -228,9 +231,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
228
231
  const abs = path.join(absParent, path.basename(absResolved));
229
232
  const rel = path.relative(root, abs);
230
233
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
231
- throw new Error(
232
- `design: materialize path "${result.path}" would escape the project root`
233
- );
234
+ throw new Error(`design: materialize path "${result.path}" would escape the project root`);
234
235
  }
235
236
  let exists = false;
236
237
  try {