@bevel-software/platform-core-backend 0.28.1 → 0.30.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 (274) hide show
  1. package/agent-guide/access-control.md +15 -0
  2. package/agent-guide/skills.md +8 -0
  3. package/dist/__tests__/test-repositories.d.ts +3 -0
  4. package/dist/__tests__/test-repositories.d.ts.map +1 -0
  5. package/dist/__tests__/test-repositories.js +18 -0
  6. package/dist/__tests__/test-repositories.js.map +1 -0
  7. package/dist/core/catalog-revision.d.ts.map +1 -1
  8. package/dist/core/catalog-revision.js +4 -2
  9. package/dist/core/catalog-revision.js.map +1 -1
  10. package/dist/core/compose-embed.d.ts +35 -0
  11. package/dist/core/compose-embed.d.ts.map +1 -0
  12. package/dist/core/compose-embed.js +14 -0
  13. package/dist/core/compose-embed.js.map +1 -0
  14. package/dist/core/core-ports.d.ts +10 -0
  15. package/dist/core/core-ports.d.ts.map +1 -1
  16. package/dist/core/core-ports.js.map +1 -1
  17. package/dist/core/create-core-server.d.ts.map +1 -1
  18. package/dist/core/create-core-server.js +8 -2
  19. package/dist/core/create-core-server.js.map +1 -1
  20. package/dist/core/create-core-services.d.ts +4 -1
  21. package/dist/core/create-core-services.d.ts.map +1 -1
  22. package/dist/core/create-core-services.js +57 -18
  23. package/dist/core/create-core-services.js.map +1 -1
  24. package/dist/core/lifecycle.d.ts +19 -6
  25. package/dist/core/lifecycle.d.ts.map +1 -1
  26. package/dist/core/lifecycle.js +17 -6
  27. package/dist/core/lifecycle.js.map +1 -1
  28. package/dist/modules/access/access-control.interface.d.ts +20 -0
  29. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  30. package/dist/modules/access/access-control.service.d.ts +68 -15
  31. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  32. package/dist/modules/access/access-control.service.js +282 -58
  33. package/dist/modules/access/access-control.service.js.map +1 -1
  34. package/dist/modules/access/access.routes.d.ts.map +1 -1
  35. package/dist/modules/access/access.routes.js +20 -6
  36. package/dist/modules/access/access.routes.js.map +1 -1
  37. package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
  38. package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
  39. package/dist/modules/auth/index.d.ts +15 -0
  40. package/dist/modules/auth/index.d.ts.map +1 -0
  41. package/dist/modules/auth/index.js +20 -0
  42. package/dist/modules/auth/index.js.map +1 -0
  43. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  44. package/dist/modules/diff/diff.service.js +7 -1
  45. package/dist/modules/diff/diff.service.js.map +1 -1
  46. package/dist/modules/embed/embed.interface.d.ts +22 -0
  47. package/dist/modules/embed/embed.interface.d.ts.map +1 -1
  48. package/dist/modules/embed/embed.service.d.ts +28 -14
  49. package/dist/modules/embed/embed.service.d.ts.map +1 -1
  50. package/dist/modules/embed/embed.service.js +38 -17
  51. package/dist/modules/embed/embed.service.js.map +1 -1
  52. package/dist/modules/embed/embed.tools.d.ts.map +1 -1
  53. package/dist/modules/embed/embed.tools.js +10 -2
  54. package/dist/modules/embed/embed.tools.js.map +1 -1
  55. package/dist/modules/kb-fs/git-dir.d.ts +21 -0
  56. package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
  57. package/dist/modules/kb-fs/git-dir.js +58 -0
  58. package/dist/modules/kb-fs/git-dir.js.map +1 -0
  59. package/dist/modules/kb-fs/index.d.ts +9 -0
  60. package/dist/modules/kb-fs/index.d.ts.map +1 -0
  61. package/dist/modules/kb-fs/index.js +3 -0
  62. package/dist/modules/kb-fs/index.js.map +1 -0
  63. package/dist/modules/kb-fs/ref-files.d.ts +63 -0
  64. package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
  65. package/dist/modules/kb-fs/ref-files.js +249 -0
  66. package/dist/modules/kb-fs/ref-files.js.map +1 -0
  67. package/dist/modules/plugins/join-request-jobs.service.d.ts.map +1 -1
  68. package/dist/modules/plugins/join-request-jobs.service.js +43 -8
  69. package/dist/modules/plugins/join-request-jobs.service.js.map +1 -1
  70. package/dist/modules/plugins/teams.routes.d.ts +4 -0
  71. package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
  72. package/dist/modules/plugins/teams.routes.js +9 -1
  73. package/dist/modules/plugins/teams.routes.js.map +1 -1
  74. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  75. package/dist/modules/skills/skills.tools.d.ts +8 -1
  76. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  77. package/dist/modules/skills/skills.tools.js +115 -11
  78. package/dist/modules/skills/skills.tools.js.map +1 -1
  79. package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
  80. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
  81. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  82. package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
  83. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  84. package/dist/modules/tool-helpers/default-branch-write.d.ts +69 -0
  85. package/dist/modules/tool-helpers/default-branch-write.d.ts.map +1 -0
  86. package/dist/modules/tool-helpers/default-branch-write.js +63 -0
  87. package/dist/modules/tool-helpers/default-branch-write.js.map +1 -0
  88. package/dist/modules/tool-helpers/index.d.ts +1 -0
  89. package/dist/modules/tool-helpers/index.d.ts.map +1 -1
  90. package/dist/modules/tool-helpers/index.js +1 -0
  91. package/dist/modules/tool-helpers/index.js.map +1 -1
  92. package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
  93. package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
  94. package/dist/modules/workflow/file-lock.service.d.ts +27 -5
  95. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  96. package/dist/modules/workflow/file-lock.service.js +70 -18
  97. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  98. package/dist/modules/workflow/git/git.service.d.ts +42 -0
  99. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  100. package/dist/modules/workflow/git/git.service.js +201 -24
  101. package/dist/modules/workflow/git/git.service.js.map +1 -1
  102. package/dist/modules/workflow/index.d.ts +8 -0
  103. package/dist/modules/workflow/index.d.ts.map +1 -0
  104. package/dist/modules/workflow/index.js +8 -0
  105. package/dist/modules/workflow/index.js.map +1 -0
  106. package/dist/modules/workflow/pending-commits.service.d.ts +2 -17
  107. package/dist/modules/workflow/pending-commits.service.d.ts.map +1 -1
  108. package/dist/modules/workflow/pending-commits.service.js +4 -24
  109. package/dist/modules/workflow/pending-commits.service.js.map +1 -1
  110. package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
  111. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  112. package/dist/modules/workflow/workflow.routes.js +20 -1
  113. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  114. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  115. package/dist/modules/workflow/workflow.service.js +5 -3
  116. package/dist/modules/workflow/workflow.service.js.map +1 -1
  117. package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
  118. package/dist/modules/workspace/agent-access.gate.js +2 -2
  119. package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
  120. package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
  121. package/dist/modules/workspace/bounded-matcher.js +261 -0
  122. package/dist/modules/workspace/bounded-matcher.js.map +1 -0
  123. package/dist/modules/workspace/fetch-instant.d.ts +9 -0
  124. package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
  125. package/dist/modules/workspace/fetch-instant.js +13 -0
  126. package/dist/modules/workspace/fetch-instant.js.map +1 -0
  127. package/dist/modules/workspace/index.d.ts +17 -0
  128. package/dist/modules/workspace/index.d.ts.map +1 -0
  129. package/dist/modules/workspace/index.js +16 -0
  130. package/dist/modules/workspace/index.js.map +1 -0
  131. package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
  132. package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
  133. package/dist/modules/workspace/kb-repository.contract.js +19 -0
  134. package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
  135. package/dist/modules/workspace/kb-repository.d.ts +236 -0
  136. package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
  137. package/dist/modules/workspace/kb-repository.js +811 -0
  138. package/dist/modules/workspace/kb-repository.js.map +1 -0
  139. package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
  140. package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
  141. package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
  142. package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
  143. package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
  144. package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
  145. package/dist/modules/workspace/routine-write-policy.js +0 -5
  146. package/dist/modules/workspace/routine-write-policy.js.map +1 -1
  147. package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
  148. package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
  149. package/dist/modules/workspace/set-aside-clone.js +139 -0
  150. package/dist/modules/workspace/set-aside-clone.js.map +1 -1
  151. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
  152. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  153. package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
  154. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  155. package/dist/modules/workspace/workspace.service.d.ts +68 -32
  156. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  157. package/dist/modules/workspace/workspace.service.js +240 -161
  158. package/dist/modules/workspace/workspace.service.js.map +1 -1
  159. package/dist/modules/workspace/workspace.tools.d.ts +13 -4
  160. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  161. package/dist/modules/workspace/workspace.tools.js +268 -356
  162. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  163. package/dist/shared/workspace-id.d.ts +17 -0
  164. package/dist/shared/workspace-id.d.ts.map +1 -1
  165. package/dist/shared/workspace-id.js +24 -0
  166. package/dist/shared/workspace-id.js.map +1 -1
  167. package/dist/test-setup.js +4 -0
  168. package/dist/test-setup.js.map +1 -1
  169. package/mcp-app/page.html +142 -8
  170. package/package.json +3 -3
  171. package/src/__tests__/test-repositories.ts +22 -0
  172. package/src/__tests__/workspace-service.ts +69 -0
  173. package/src/core/__tests__/compose-embed.test.ts +65 -0
  174. package/src/core/__tests__/lifecycle.test.ts +45 -26
  175. package/src/core/catalog-revision.ts +4 -2
  176. package/src/core/compose-embed.ts +50 -0
  177. package/src/core/core-ports.ts +10 -0
  178. package/src/core/create-core-server.ts +8 -2
  179. package/src/core/create-core-services.ts +63 -24
  180. package/src/core/lifecycle.ts +34 -12
  181. package/src/modules/access/__tests__/access-control.atref-cache.test.ts +443 -25
  182. package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
  183. package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
  184. package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
  185. package/src/modules/access/access-control.interface.ts +22 -0
  186. package/src/modules/access/access-control.service.ts +302 -58
  187. package/src/modules/access/access.routes.ts +22 -6
  188. package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
  189. package/src/modules/auth/index.ts +29 -0
  190. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
  191. package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
  192. package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
  193. package/src/modules/diff/diff.service.ts +6 -4
  194. package/src/modules/embed/__tests__/embed.service.test.ts +76 -3
  195. package/src/modules/embed/__tests__/embed.tools.test.ts +29 -0
  196. package/src/modules/embed/embed.interface.ts +24 -0
  197. package/src/modules/embed/embed.service.ts +44 -19
  198. package/src/modules/embed/embed.tools.ts +10 -2
  199. package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
  200. package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
  201. package/src/modules/kb-fs/git-dir.ts +53 -0
  202. package/src/modules/kb-fs/index.ts +8 -0
  203. package/src/modules/kb-fs/ref-files.ts +279 -0
  204. package/src/modules/plugins/__tests__/join-request-jobs.service.test.ts +73 -6
  205. package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
  206. package/src/modules/plugins/join-request-jobs.service.ts +45 -8
  207. package/src/modules/plugins/teams.routes.ts +14 -1
  208. package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
  209. package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
  210. package/src/modules/skills/__tests__/branch-skills.tools.test.ts +1 -1
  211. package/src/modules/skills/skills.contract.ts +4 -0
  212. package/src/modules/skills/skills.tools.ts +120 -10
  213. package/src/modules/tool-auth/internal-token.service.ts +1 -1
  214. package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
  215. package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
  216. package/src/modules/tool-helpers/__tests__/can-write.tools.test.ts +581 -0
  217. package/src/modules/tool-helpers/default-branch-write.ts +90 -0
  218. package/src/modules/tool-helpers/index.ts +5 -0
  219. package/src/modules/tool-helpers/tool-handler.ts +2 -2
  220. package/src/modules/tool-helpers/tool.contract.ts +2 -2
  221. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +42 -3
  222. package/src/modules/workflow/__tests__/fake-file-lock-db.ts +13 -2
  223. package/src/modules/workflow/__tests__/workflow.routes.lock-path.test.ts +168 -1
  224. package/src/modules/workflow/__tests__/workflow.service.delete-branch.test.ts +102 -6
  225. package/src/modules/workflow/file-lock.service.ts +79 -18
  226. package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
  227. package/src/modules/workflow/git/__tests__/git.service.readFileAtRef.test.ts +42 -0
  228. package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
  229. package/src/modules/workflow/git/git.service.ts +207 -22
  230. package/src/modules/workflow/index.ts +7 -0
  231. package/src/modules/workflow/pending-commits.service.ts +4 -23
  232. package/src/modules/workflow/workflow-hooks.ts +1 -1
  233. package/src/modules/workflow/workflow.routes.ts +24 -1
  234. package/src/modules/workflow/workflow.service.ts +3 -2
  235. package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
  236. package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
  237. package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
  238. package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
  239. package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
  240. package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
  241. package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
  242. package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
  243. package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
  244. package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
  245. package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
  246. package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
  247. package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
  248. package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
  249. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
  250. package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
  251. package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
  252. package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
  253. package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
  254. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
  255. package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
  256. package/src/modules/workspace/__tests__/workspace.tools.test.ts +174 -309
  257. package/src/modules/workspace/agent-access.gate.ts +2 -2
  258. package/src/modules/workspace/bounded-matcher.ts +328 -0
  259. package/src/modules/workspace/fetch-instant.ts +13 -0
  260. package/src/modules/workspace/index.ts +26 -0
  261. package/src/modules/workspace/kb-repository.contract.ts +124 -0
  262. package/src/modules/workspace/kb-repository.ts +844 -0
  263. package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
  264. package/src/modules/workspace/routine-write-policy.ts +0 -15
  265. package/src/modules/workspace/set-aside-clone.ts +140 -0
  266. package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
  267. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
  268. package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
  269. package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
  270. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
  271. package/src/modules/workspace/workspace.service.ts +247 -169
  272. package/src/modules/workspace/workspace.tools.ts +299 -385
  273. package/src/shared/workspace-id.ts +24 -0
  274. package/src/test-setup.ts +5 -0
@@ -149,8 +149,8 @@ export async function notifyAgentRead(
149
149
  * that throws refuses it with its own message and status; with none registered
150
150
  * this is a no-op.
151
151
  *
152
- * `wsPath` is omitted by the one write-capable tool with no resolvable target,
153
- * `execute_command` — the hook then decides at the coarser session level.
152
+ * `wsPath` is omitted by a write-capable operation with no resolvable
153
+ * target — the hook then decides at the coarser session level.
154
154
  */
155
155
  export async function assertAgentWriteAllowed(
156
156
  gate: AgentAccessGate,
@@ -0,0 +1,328 @@
1
+ import { Worker } from 'node:worker_threads';
2
+ import { ToolError } from '../tool-helpers/tool.contract.js';
3
+ import {
4
+ type IPatternMatcher,
5
+ type IPatternSearch,
6
+ PatternTooExpensiveError,
7
+ SearchBusyError,
8
+ } from './pattern-matcher.contract.js';
9
+
10
+ /**
11
+ * The one place a CALLER'S regular expression is run (see
12
+ * {@link IPatternMatcher}).
13
+ *
14
+ * The pattern never runs on the request thread: the lines go to a worker, the
15
+ * worker answers which of them match, and a search whose matching outlives its
16
+ * budget has its worker terminated (which interrupts even a regex
17
+ * mid-backtrack) and is answered with {@link PatternTooExpensiveError}.
18
+ * Nothing partial is returned: a search that blew its budget answers the 400
19
+ * alone.
20
+ *
21
+ * The semantics are the engine's own — the worker compiles the same source with
22
+ * the same flags and calls `test` on each line — so a pattern within budget
23
+ * matches exactly the lines it always did.
24
+ *
25
+ * The pool is this instance's own: the composition root builds one per graph
26
+ * and closes it when the graph stops, so no worker outlives what it served.
27
+ */
28
+ export interface WorkerPatternMatcherOptions {
29
+ /** Matching time one search may spend, summed over every file it searches. Default 5 s. */
30
+ budgetMs?: number;
31
+ /** Workers kept for searches at once; a search past this waits for one to free up. Default 4. */
32
+ poolMax?: number;
33
+ /**
34
+ * Searches that may wait for a worker at once. A waiting search holds the
35
+ * lines it was handed, so the queue is capped: past it a search is refused
36
+ * with a 503 rather than piling file contents up in memory. Default 16.
37
+ */
38
+ maxWaiting?: number;
39
+ /**
40
+ * Longest pattern accepted. The pattern is compiled on the request thread
41
+ * (that is how a syntax error earns its 400 there), so its length is what
42
+ * bounds that. Default 1000.
43
+ */
44
+ maxPatternLength?: number;
45
+ /**
46
+ * Characters handed to the worker in one message. Posting a message copies
47
+ * it on the request thread, so a large file goes over in pieces of about
48
+ * this size rather than in one copy that would stall every other request.
49
+ * A single line longer than this is not posted at all: it is written into
50
+ * shared memory this many characters at a time, yielding between pieces,
51
+ * and the worker reads it from there. Default 1 MB.
52
+ */
53
+ batchChars?: number;
54
+ /**
55
+ * Lines handed to the worker in one message. Each line is an entry the
56
+ * copy has to walk whatever its length, so a file of empty lines is bounded
57
+ * by this, not by `batchChars`. Default 10,000.
58
+ */
59
+ batchLines?: number;
60
+ }
61
+
62
+ /**
63
+ * Slack on top of the remaining budget before a batch is given up on: the
64
+ * budget counts only the worker's own matching time, so handing the lines over
65
+ * and the answer back must not be what trips it.
66
+ */
67
+ const TRANSFER_GRACE_MS = 250;
68
+
69
+ /** The worker: compiles the pattern it is handed and answers the indices of the lines it matches. */
70
+ const WORKER_SOURCE = `
71
+ const { parentPort } = require('node:worker_threads');
72
+ const { performance } = require('node:perf_hooks');
73
+ let cached = { key: null, re: null };
74
+ parentPort.on('message', ({ source, flags, lines, shared, limit }) => {
75
+ try {
76
+ // One line too long to post, written into shared memory as raw UTF-16 code units.
77
+ if (shared) lines = [Buffer.from(shared).toString('utf16le')];
78
+ const key = flags + '/' + source;
79
+ if (cached.key !== key) cached = { key, re: new RegExp(source, flags) };
80
+ const re = cached.re;
81
+ const started = performance.now();
82
+ const hits = [];
83
+ for (let i = 0; i < lines.length && hits.length < limit; i++) {
84
+ re.lastIndex = 0;
85
+ if (re.test(lines[i])) hits.push(i);
86
+ }
87
+ parentPort.postMessage({ hits, elapsed: performance.now() - started });
88
+ } catch (err) {
89
+ parentPort.postMessage({ error: String(err && err.message ? err.message : err) });
90
+ }
91
+ });
92
+ `;
93
+
94
+ type Reply = { hits: number[]; elapsed: number } | { error: string };
95
+ /** What a search borrows from its pool: a worker, its return, and where a batch ends. */
96
+ type Lease = {
97
+ acquire(): Promise<Worker>;
98
+ release(w: Worker): void;
99
+ batchEnd(lines: string[], from: number): number;
100
+ readonly batchChars: number;
101
+ };
102
+ type Waiter = { resolve: (w: Worker) => void; reject: (err: Error) => void };
103
+
104
+ export class WorkerPatternMatcher implements IPatternMatcher {
105
+ private readonly budgetMs: number;
106
+ private readonly poolMax: number;
107
+ private readonly maxWaiting: number;
108
+ private readonly maxPatternLength: number;
109
+ private readonly batchChars: number;
110
+ private readonly batchLines: number;
111
+ private readonly idle: Worker[] = [];
112
+ private readonly all = new Set<Worker>();
113
+ private readonly waiting: Waiter[] = [];
114
+ private closed = false;
115
+ private readonly lease: Lease;
116
+
117
+ constructor(opts: WorkerPatternMatcherOptions = {}) {
118
+ this.budgetMs = option('budgetMs', opts.budgetMs ?? 5_000, 1, MAX_TIMER_MS - TRANSFER_GRACE_MS);
119
+ // A pool of none would leave every search waiting for a worker that never comes.
120
+ this.poolMax = option('poolMax', opts.poolMax ?? 4, 1);
121
+ // No queue at all is allowed: every search past the pool is refused busy.
122
+ this.maxWaiting = option('maxWaiting', opts.maxWaiting ?? 16, 0);
123
+ this.maxPatternLength = option('maxPatternLength', opts.maxPatternLength ?? 1_000, 1);
124
+ this.batchChars = option('batchChars', opts.batchChars ?? 1_000_000, 1);
125
+ this.batchLines = option('batchLines', opts.batchLines ?? 10_000, 1);
126
+ this.lease = {
127
+ acquire: () => this.acquire(),
128
+ release: (w) => this.release(w),
129
+ batchEnd: (lines, from) => this.batchEnd(lines, from),
130
+ batchChars: this.batchChars,
131
+ };
132
+ }
133
+
134
+ search(source: string, flags: string): IPatternSearch {
135
+ if (source.length > this.maxPatternLength) {
136
+ throw new ToolError(
137
+ `Pattern too long: ${source.length} characters, and a search pattern may have at most ${this.maxPatternLength}.`,
138
+ 400,
139
+ { code: 'pattern_too_long' },
140
+ );
141
+ }
142
+ try {
143
+ new RegExp(source, flags);
144
+ } catch (err) {
145
+ throw new ToolError(`Invalid regex: ${(err as Error).message}`, 400);
146
+ }
147
+ return new BoundedSearch(this.lease, source, flags, this.budgetMs);
148
+ }
149
+
150
+ async close(): Promise<void> {
151
+ this.closed = true;
152
+ for (const waiter of this.waiting.splice(0)) waiter.reject(new SearchBusyError());
153
+ const workers = [...this.all];
154
+ this.idle.length = 0;
155
+ await Promise.all(workers.map((w) => w.terminate()));
156
+ }
157
+
158
+ /**
159
+ * Where the batch starting at `from` ends: about `batchChars` characters on,
160
+ * at most `batchLines` lines on, and never empty. A batch of one line past
161
+ * `batchChars` is handed over through shared memory instead (see
162
+ * {@link BoundedSearch.matchBatch}).
163
+ */
164
+ private batchEnd(lines: string[], from: number): number {
165
+ let to = from;
166
+ let chars = 0;
167
+ const last = Math.min(lines.length, from + this.batchLines);
168
+ while (to < last && (to === from || chars + lines[to]!.length <= this.batchChars)) chars += lines[to++]!.length;
169
+ return to;
170
+ }
171
+
172
+ private async acquire(): Promise<Worker> {
173
+ if (this.closed) throw new SearchBusyError();
174
+ if (this.idle.length === 0 && this.all.size >= this.poolMax && this.waiting.length >= this.maxWaiting) {
175
+ throw new SearchBusyError();
176
+ }
177
+ const w =
178
+ this.idle.pop() ??
179
+ (this.all.size < this.poolMax
180
+ ? this.spawn()
181
+ : await new Promise<Worker>((resolve, reject) => this.waiting.push({ resolve, reject })));
182
+ w.ref();
183
+ return w;
184
+ }
185
+
186
+ private release(w: Worker): void {
187
+ const next = this.waiting.shift();
188
+ if (next) return next.resolve(w);
189
+ // An idle worker must not hold a process's exit open.
190
+ w.unref();
191
+ this.idle.push(w);
192
+ }
193
+
194
+ private spawn(): Worker {
195
+ const w = new Worker(WORKER_SOURCE, { eval: true });
196
+ this.all.add(w);
197
+ // A worker that dies — terminated over budget, or out of memory — leaves the pool.
198
+ w.once('exit', () => {
199
+ this.all.delete(w);
200
+ const at = this.idle.indexOf(w);
201
+ if (at >= 0) this.idle.splice(at, 1);
202
+ if (this.closed) return;
203
+ const next = this.waiting.shift();
204
+ if (next) next.resolve(this.spawn());
205
+ });
206
+ return w;
207
+ }
208
+ }
209
+
210
+ /** One search: a caller's pattern, matched off the event loop under one budget. */
211
+ class BoundedSearch implements IPatternSearch {
212
+ private spentMs = 0;
213
+
214
+ constructor(
215
+ private readonly pool: Lease,
216
+ private readonly source: string,
217
+ private readonly flags: string,
218
+ private readonly budgetMs: number,
219
+ ) {}
220
+
221
+ async matchingLines(lines: string[], limit: number): Promise<number[]> {
222
+ if (limit <= 0 || lines.length === 0) return [];
223
+ if (this.spentMs >= this.budgetMs) throw new PatternTooExpensiveError(this.source, this.budgetMs);
224
+ const w = await this.pool.acquire();
225
+ const hits: number[] = [];
226
+ try {
227
+ for (let from = 0; from < lines.length && hits.length < limit; ) {
228
+ const to = this.pool.batchEnd(lines, from);
229
+ for (const i of await this.matchBatch(w, lines.slice(from, to), limit - hits.length)) hits.push(from + i);
230
+ from = to;
231
+ }
232
+ } catch (err) {
233
+ // Terminating is what stops a regex mid-backtrack; the pool replaces it.
234
+ void w.terminate();
235
+ throw err;
236
+ }
237
+ this.pool.release(w);
238
+ return hits;
239
+ }
240
+
241
+ /**
242
+ * One handoff to the worker, charged to this search's budget. No handoff
243
+ * copies more than `batchChars` characters on the request thread at once: a
244
+ * line longer than that goes over in shared memory, not in the message.
245
+ */
246
+ private async matchBatch(w: Worker, lines: string[], limit: number): Promise<number[]> {
247
+ if (this.spentMs >= this.budgetMs) throw new PatternTooExpensiveError(this.source, this.budgetMs);
248
+ const oversized = lines.length === 1 && lines[0]!.length > this.pool.batchChars;
249
+ const payload = oversized ? { shared: await shareLine(w, lines[0]!, this.pool.batchChars) } : { lines };
250
+ const remaining = this.budgetMs - this.spentMs;
251
+ const reply = await run(w, { source: this.source, flags: this.flags, ...payload, limit }, remaining + TRANSFER_GRACE_MS);
252
+ if (reply === 'timeout') {
253
+ this.spentMs = this.budgetMs;
254
+ throw new PatternTooExpensiveError(this.source, this.budgetMs);
255
+ }
256
+ if ('error' in reply) throw new ToolError(`Invalid regex: ${reply.error}`, 400);
257
+ this.spentMs += reply.elapsed;
258
+ if (this.spentMs > this.budgetMs) throw new PatternTooExpensiveError(this.source, this.budgetMs);
259
+ return reply.hits;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * A line as raw UTF-16 code units in shared memory, which a message hands the
265
+ * worker without copying. It is written `chunkChars` at a time with a turn of
266
+ * the event loop between, so a line of any length never holds the request
267
+ * thread for longer than one chunk. Raw code units keep the line exact, lone
268
+ * surrogates included.
269
+ *
270
+ * The worker it is meant for can be terminated by `close()` during those turns,
271
+ * before {@link run} listens for its exit; a message posted to it then would
272
+ * never be answered. So its exit is watched here too: writing stops and the
273
+ * search is answered busy, as one terminated under `run` is.
274
+ */
275
+ async function shareLine(w: Worker, line: string, chunkChars: number): Promise<SharedArrayBuffer> {
276
+ let exited = false;
277
+ const onExit = () => {
278
+ exited = true;
279
+ };
280
+ w.once('exit', onExit);
281
+ try {
282
+ const shared = new SharedArrayBuffer(line.length * 2);
283
+ const bytes = Buffer.from(shared);
284
+ for (let at = 0; at < line.length && !exited; at += chunkChars) {
285
+ if (at > 0) await new Promise<void>((resolve) => setImmediate(resolve));
286
+ bytes.write(line.slice(at, at + chunkChars), at * 2, 'utf16le');
287
+ }
288
+ if (exited) throw new SearchBusyError();
289
+ return shared;
290
+ } finally {
291
+ w.off('exit', onExit);
292
+ }
293
+ }
294
+
295
+ function run(w: Worker, message: object, timeoutMs: number): Promise<Reply | 'timeout'> {
296
+ return new Promise((resolve, reject) => {
297
+ const settle = (fn: () => void) => {
298
+ clearTimeout(timer);
299
+ w.off('message', onMessage);
300
+ w.off('error', onError);
301
+ w.off('exit', onExit);
302
+ fn();
303
+ };
304
+ const onMessage = (reply: Reply) => settle(() => resolve(reply));
305
+ const onError = (err: Error) => settle(() => reject(err));
306
+ // Terminated under the search by `close()`: answered as busy, not left hanging until the timer.
307
+ const onExit = () => settle(() => reject(new SearchBusyError()));
308
+ const timer = setTimeout(() => settle(() => resolve('timeout')), timeoutMs);
309
+ w.on('message', onMessage);
310
+ w.on('error', onError);
311
+ w.on('exit', onExit);
312
+ w.postMessage(message);
313
+ });
314
+ }
315
+
316
+ /**
317
+ * The longest delay `setTimeout` honours. A longer one fires after 1 ms, so a
318
+ * budget past this (less the grace added to it) would stop every search at once.
319
+ */
320
+ const MAX_TIMER_MS = 2_147_483_647;
321
+
322
+ /** A sizing option as given, refused at construction unless it is an integer from `min` to `max`. */
323
+ function option(name: string, value: number, min: number, max = Number.MAX_SAFE_INTEGER): number {
324
+ if (!Number.isInteger(value) || value < min || value > max) {
325
+ throw new RangeError(`WorkerPatternMatcher: ${name} must be an integer from ${min} to ${max}, got ${value}.`);
326
+ }
327
+ return value;
328
+ }
@@ -0,0 +1,13 @@
1
+ let last = 0;
2
+
3
+ /**
4
+ * An instant on the one clock that fetch starts and `fetchedSince` instants
5
+ * are read from. Milliseconds, like `Date.now()`, but strictly increasing in
6
+ * call order: two readings never compare equal, so a fetch started in the
7
+ * same millisecond as a listing is still ordered before or after it, and
8
+ * "started after the instant" is a strict comparison that cannot be a tie.
9
+ */
10
+ export function fetchInstant(): number {
11
+ last = Math.max(Date.now(), last + 0.001);
12
+ return last;
13
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What the workspace module offers the other modules: how a file's bytes are
3
+ * read and told apart — the reader registry `read_file` dispatches on, and
4
+ * the raw content type a file is served with — so a module that serves a
5
+ * file answers exactly as a read of it would. The services are wired by the
6
+ * composition root.
7
+ */
8
+ export {
9
+ FileReaderRegistry,
10
+ type FileKind,
11
+ type FileReader,
12
+ type ReadResult,
13
+ } from './file-readers/file-reader.js';
14
+ export { OCTET_STREAM, rawExtensionOf, rawMimeFor } from './file-readers/raw-mime.js';
15
+ /**
16
+ * The knowledge base's one repository, every branch workspace a worktree of
17
+ * it. (Where a checkout's git directory is — its `.git` folder, or the
18
+ * folder its `.git` FILE names — is `kb-fs/git-dir.ts`.)
19
+ */
20
+ export {
21
+ KbRepository,
22
+ repositoryDirFor,
23
+ workingCopyShape,
24
+ type WorkingCopyShape,
25
+ } from './kb-repository.js';
26
+ export type { AddWorktreeOptions, FetchOptions, IKbRepository } from './kb-repository.contract.js';
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The knowledge base's one repository, every branch workspace a worktree of
3
+ * it — the contract its consumers (the workspace service, the KB startup
4
+ * phase, the set-aside of a moved repository, the git layer) depend on.
5
+ * `KbRepository` (`kb-repository.ts`) is the implementation, a bare
6
+ * repository driven by the git CLI; the composition root makes ONE and
7
+ * injects it everywhere, so that one fetch serves every workspace and the
8
+ * repository's administration — its creation, the writes to its one config
9
+ * file, its worktree list, a set-aside — is serialized in one place.
10
+ *
11
+ * Nothing else is serialized by the application: git was built for many
12
+ * worktrees of one repository, and protects the refs, `packed-refs` and the
13
+ * object store with its own locks. The worktree list (`worktrees/`) it does
14
+ * not lock, which is why adding, removing and pruning worktrees are
15
+ * administration here. Two worktrees run side by side, as two developers'
16
+ * worktrees of one repository do.
17
+ */
18
+
19
+ export interface FetchOptions {
20
+ /** A fetch that SUCCEEDED this recently is fresh enough; nothing runs. */
21
+ maxAgeMs: number;
22
+ /**
23
+ * An instant (`fetchInstant()`): a success counts as fresh only when
24
+ * its fetch started after it, however young it is (a fetch from before a listing may lack
25
+ * what the listing saw). A fetch in flight that started before it is
26
+ * waited for, not joined, and a fresh one runs.
27
+ */
28
+ fetchedSince?: number;
29
+ /** Skip the freshness window (the in-flight join still applies). */
30
+ force?: boolean;
31
+ /**
32
+ * A fetch that FAILED this recently is not retried: the answer is the
33
+ * failure. For pollers, so a host that is down is not asked again on every
34
+ * request. Absent, a failure is retried on the next call.
35
+ */
36
+ failureBackoffMs?: number;
37
+ }
38
+
39
+ export interface AddWorktreeOptions {
40
+ /** A converted clone whose branch was never pushed: its local ref is enough. */
41
+ allowLocalOnly?: boolean;
42
+ /** A conversion: a local ref behind the remote stays where it is. */
43
+ keepLocalTip?: boolean;
44
+ /**
45
+ * Reuse the repository's own fetch when it started after this instant
46
+ * (`fetchInstant()`)
47
+ * and knows the branch. An instant, not an age: it is compared inside the
48
+ * reservation, so time spent waiting for it cannot age a fetch out.
49
+ */
50
+ fetchedSince?: number;
51
+ }
52
+
53
+ export interface IKbRepository {
54
+ /** The bare repository's folder. */
55
+ readonly dir: string;
56
+ /**
57
+ * Run `fn` as the repository's one administrator for its duration: no
58
+ * creation of the repository, no write to its one config file (git refuses
59
+ * a second writer of it rather than waiting), no change to its worktree
60
+ * list (which git does not lock: a prune beside a `worktree add` can take
61
+ * the entry being made), and no set-aside runs beside it. Git operations
62
+ * in the worktrees are not held back — git's own locks cover them — so a
63
+ * caller that moves the repository's folders holds those off itself (the
64
+ * startup phase runs before anything is served; the workspace service
65
+ * wraps a set-aside in what surrounds one). Never nested: `fn` must not
66
+ * call `exclusive` again, nor a method that does (`ensure`,
67
+ * `refreshCredentials`, `writeConfig`, `addWorktree`, `removeWorktree`,
68
+ * `forgetWorktrees`, `prune`, `setOriginUrl`).
69
+ */
70
+ exclusive<T>(fn: () => Promise<T>): Promise<T>;
71
+
72
+ /** Whether the repository is on disk and finished (its first fetch landed). */
73
+ exists(): Promise<boolean>;
74
+ /** Create the repository and fetch it once when it is not there. Throws with git's words, redacted. */
75
+ ensure(): Promise<void>;
76
+ /** Re-stamp the credential helper when the deployment's credential changed. */
77
+ refreshCredentials(): Promise<void>;
78
+ /** Run `git config` writes against the shared configuration, one writer at a time. */
79
+ writeConfig(cwd: string, argLists: readonly string[][]): Promise<void>;
80
+ /**
81
+ * Garbage-collect the repository when git finds it worth it (`gc --auto`
82
+ * with git's default thresholds). Automatic collection is off on the
83
+ * repository — a commit in one worktree would otherwise collect beside
84
+ * every other worktree's work — so the startup phase runs this once, when
85
+ * nothing else does. Never throws.
86
+ */
87
+ maintain(): Promise<void>;
88
+ /** The last time the repository reached the remote and whether it worked; null before any attempt. */
89
+ lastContact(): { at: number; ok: boolean } | null;
90
+ /** Why the last fetch failed, redacted; null when it did not. */
91
+ lastFetchFailure(): string | null;
92
+ /**
93
+ * `git fetch --prune origin`, once for every workspace: throttled, and
94
+ * joined while in flight. Answers whether the remote-tracking refs are
95
+ * provably fresh. Never throws.
96
+ */
97
+ fetch(opts: FetchOptions): Promise<boolean>;
98
+ /** Forget what this process knows of the repository (it was set aside). */
99
+ forget(): void;
100
+ /** Add the worktree of `branch` at `checkoutDir`, copying no history. */
101
+ addWorktree(branch: string, checkoutDir: string, opts?: AddWorktreeOptions): Promise<void>;
102
+ /** Remove and prune the worktree at `checkoutDir`, with its branch's local ref; nothing else. */
103
+ removeWorktree(checkoutDir: string, branch: string): Promise<void>;
104
+ /** Prune worktrees whose folders were deleted by hand, and drop their branches' local refs. */
105
+ forgetWorktrees(branches: readonly string[]): Promise<void>;
106
+ /** `git worktree prune`. */
107
+ prune(): Promise<void>;
108
+ /** The folders of the repository's worktrees, as git lists them. */
109
+ listWorktrees(): Promise<string[]>;
110
+ /** Import the tip of `branch` from the full clone at `cloneDir` (unpushed commits included); answers the tip. */
111
+ importBranch(cloneDir: string, branch: string): Promise<string>;
112
+ /**
113
+ * Import the full clone's stash entries and its other local refs (not its own
114
+ * branch, not remote-tracking ones) under `refs/kb-converted/<workspaceId>/`,
115
+ * leaving the clone as it was; answers the refs written.
116
+ */
117
+ importLocalRefs(cloneDir: string, branch: string, workspaceId: string): Promise<string[]>;
118
+ /** The address the repository fetches from, or null when it cannot be read. */
119
+ originUrl(): Promise<string | null>;
120
+ /** Re-link a repository and its worktrees that were moved together; answers the worktrees that could not be. */
121
+ relinkMovedCopy(movedRepository: string, movedWorktrees: readonly string[]): Promise<string[]>;
122
+ /** Point the repository at `url`, forgetting the old address's fetch freshness. */
123
+ setOriginUrl(url: string): Promise<void>;
124
+ }