@animalabs/agent-framework 0.23.2 → 0.24.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 (338) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +60 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +92 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/code-execution/engine.d.ts +177 -0
  10. package/dist/src/code-execution/engine.d.ts.map +1 -0
  11. package/dist/src/code-execution/engine.js +494 -0
  12. package/dist/src/code-execution/engine.js.map +1 -0
  13. package/dist/src/code-execution/index.d.ts +5 -10
  14. package/dist/src/code-execution/index.d.ts.map +1 -1
  15. package/dist/src/code-execution/index.js +5 -9
  16. package/dist/src/code-execution/index.js.map +1 -1
  17. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  18. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  19. package/dist/src/code-execution/isolate/calls.js +145 -0
  20. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  21. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  22. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  24. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  26. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-main.js +132 -0
  28. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  30. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child.js +315 -0
  32. package/dist/src/code-execution/isolate/child.js.map +1 -0
  33. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  34. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/frames.js +210 -0
  36. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  37. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  38. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/output.js +143 -0
  40. package/dist/src/code-execution/isolate/output.js.map +1 -0
  41. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  42. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/prelude.js +401 -0
  44. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  45. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  46. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/protocol.js +150 -0
  48. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  49. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  50. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/render.js +214 -0
  52. package/dist/src/code-execution/isolate/render.js.map +1 -0
  53. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  54. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/runner.js +854 -0
  56. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  57. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  58. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/scripts.js +357 -0
  60. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  61. package/dist/src/code-execution/quickjs.wasm +0 -0
  62. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  63. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  64. package/dist/src/code-execution/tool-definition.js +176 -142
  65. package/dist/src/code-execution/tool-definition.js.map +1 -1
  66. package/dist/src/deadline-timer.d.ts +3 -0
  67. package/dist/src/deadline-timer.d.ts.map +1 -1
  68. package/dist/src/deadline-timer.js +1 -1
  69. package/dist/src/deadline-timer.js.map +1 -1
  70. package/dist/src/framework.d.ts +1027 -440
  71. package/dist/src/framework.d.ts.map +1 -1
  72. package/dist/src/framework.js +4093 -2238
  73. package/dist/src/framework.js.map +1 -1
  74. package/dist/src/gate/event-gate.d.ts +28 -1
  75. package/dist/src/gate/event-gate.d.ts.map +1 -1
  76. package/dist/src/gate/event-gate.js +88 -10
  77. package/dist/src/gate/event-gate.js.map +1 -1
  78. package/dist/src/held-writes.d.ts +16 -3
  79. package/dist/src/held-writes.d.ts.map +1 -1
  80. package/dist/src/held-writes.js +16 -3
  81. package/dist/src/held-writes.js.map +1 -1
  82. package/dist/src/index.d.ts +6 -6
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +9 -8
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/mcpl/channel-registry.d.ts +471 -75
  87. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  88. package/dist/src/mcpl/channel-registry.js +1673 -337
  89. package/dist/src/mcpl/channel-registry.js.map +1 -1
  90. package/dist/src/mcpl/conversation-router.d.ts +12 -7
  91. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  92. package/dist/src/mcpl/conversation-router.js +12 -7
  93. package/dist/src/mcpl/conversation-router.js.map +1 -1
  94. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  95. package/dist/src/mcpl/convert-block.js +9 -5
  96. package/dist/src/mcpl/convert-block.js.map +1 -1
  97. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  98. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  99. package/dist/src/mcpl/inbound-source.js +130 -0
  100. package/dist/src/mcpl/inbound-source.js.map +1 -1
  101. package/dist/src/mcpl/index.d.ts +2 -1
  102. package/dist/src/mcpl/index.d.ts.map +1 -1
  103. package/dist/src/mcpl/index.js +3 -2
  104. package/dist/src/mcpl/index.js.map +1 -1
  105. package/dist/src/mcpl/log-value.d.ts +6 -0
  106. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  107. package/dist/src/mcpl/log-value.js +8 -0
  108. package/dist/src/mcpl/log-value.js.map +1 -1
  109. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  110. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  111. package/dist/src/mcpl/modern-connection.js +168 -53
  112. package/dist/src/mcpl/modern-connection.js.map +1 -1
  113. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  114. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  115. package/dist/src/mcpl/prose-grammar.js +4 -5
  116. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  117. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  118. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  119. package/dist/src/mcpl/protocol-family.js +173 -16
  120. package/dist/src/mcpl/protocol-family.js.map +1 -1
  121. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  122. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  123. package/dist/src/mcpl/push-coalescer.js +227 -50
  124. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  125. package/dist/src/mcpl/push-handler.d.ts +6 -1
  126. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-handler.js +4 -1
  128. package/dist/src/mcpl/push-handler.js.map +1 -1
  129. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  130. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  131. package/dist/src/mcpl/reference-fetcher.js +4 -4
  132. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  133. package/dist/src/mcpl/server-connection.d.ts +50 -21
  134. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  135. package/dist/src/mcpl/server-connection.js +158 -74
  136. package/dist/src/mcpl/server-connection.js.map +1 -1
  137. package/dist/src/mcpl/tags.d.ts +24 -0
  138. package/dist/src/mcpl/tags.d.ts.map +1 -1
  139. package/dist/src/mcpl/tags.js +33 -0
  140. package/dist/src/mcpl/tags.js.map +1 -1
  141. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  142. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  143. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  144. package/dist/src/mcpl/transport.d.ts +55 -7
  145. package/dist/src/mcpl/transport.d.ts.map +1 -1
  146. package/dist/src/mcpl/transport.js +163 -34
  147. package/dist/src/mcpl/transport.js.map +1 -1
  148. package/dist/src/mcpl/types.d.ts +22 -6
  149. package/dist/src/mcpl/types.d.ts.map +1 -1
  150. package/dist/src/mcpl/types.js.map +1 -1
  151. package/dist/src/module-registry.d.ts +4 -2
  152. package/dist/src/module-registry.d.ts.map +1 -1
  153. package/dist/src/module-registry.js +100 -13
  154. package/dist/src/module-registry.js.map +1 -1
  155. package/dist/src/modules/discord/index.d.ts.map +1 -1
  156. package/dist/src/modules/discord/index.js +8 -13
  157. package/dist/src/modules/discord/index.js.map +1 -1
  158. package/dist/src/modules/history/index.d.ts.map +1 -1
  159. package/dist/src/modules/history/index.js +27 -4
  160. package/dist/src/modules/history/index.js.map +1 -1
  161. package/dist/src/modules/history/semantic.d.ts +15 -3
  162. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  163. package/dist/src/modules/history/semantic.js +27 -5
  164. package/dist/src/modules/history/semantic.js.map +1 -1
  165. package/dist/src/modules/index.d.ts +1 -1
  166. package/dist/src/modules/index.d.ts.map +1 -1
  167. package/dist/src/modules/index.js.map +1 -1
  168. package/dist/src/modules/unknown-tool.d.ts +4 -1
  169. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  170. package/dist/src/modules/unknown-tool.js +4 -1
  171. package/dist/src/modules/unknown-tool.js.map +1 -1
  172. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  173. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  174. package/dist/src/modules/workspace/branch-intent.js +3 -1
  175. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  176. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  177. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  178. package/dist/src/modules/workspace/edit-plan.js +794 -0
  179. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  180. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  181. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-text.js +584 -0
  183. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  184. package/dist/src/modules/workspace/effects.d.ts +5 -4
  185. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/effects.js +78 -25
  187. package/dist/src/modules/workspace/effects.js.map +1 -1
  188. package/dist/src/modules/workspace/index.d.ts +484 -61
  189. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/index.js +2173 -645
  191. package/dist/src/modules/workspace/index.js.map +1 -1
  192. package/dist/src/modules/workspace/observe.d.ts +213 -38
  193. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/observe.js +416 -36
  195. package/dist/src/modules/workspace/observe.js.map +1 -1
  196. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/reconcile.js +15 -8
  198. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  199. package/dist/src/modules/workspace/text.d.ts +32 -0
  200. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  201. package/dist/src/modules/workspace/text.js +67 -0
  202. package/dist/src/modules/workspace/text.js.map +1 -0
  203. package/dist/src/modules/workspace/types.d.ts +210 -6
  204. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/types.js.map +1 -1
  206. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  207. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  208. package/dist/src/modules/workspace/watcher.js.map +1 -1
  209. package/dist/src/operator-log.d.ts +52 -24
  210. package/dist/src/operator-log.d.ts.map +1 -1
  211. package/dist/src/operator-log.js.map +1 -1
  212. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  213. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  214. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  215. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  216. package/dist/src/recovery/offline-branch.d.ts +26 -1
  217. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  218. package/dist/src/recovery/offline-branch.js +164 -39
  219. package/dist/src/recovery/offline-branch.js.map +1 -1
  220. package/dist/src/recovery/recover-cli.js +23 -4
  221. package/dist/src/recovery/recover-cli.js.map +1 -1
  222. package/dist/src/send-tools.d.ts +11 -0
  223. package/dist/src/send-tools.d.ts.map +1 -0
  224. package/dist/src/send-tools.js +11 -0
  225. package/dist/src/send-tools.js.map +1 -0
  226. package/dist/src/tool-result-guard.d.ts +4 -3
  227. package/dist/src/tool-result-guard.d.ts.map +1 -1
  228. package/dist/src/tool-result-guard.js +5 -4
  229. package/dist/src/tool-result-guard.js.map +1 -1
  230. package/dist/src/tool-result-history.d.ts +1 -1
  231. package/dist/src/tool-result-history.js +1 -1
  232. package/dist/src/tune-out/coordinator.d.ts +21 -0
  233. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  234. package/dist/src/tune-out/coordinator.js +139 -37
  235. package/dist/src/tune-out/coordinator.js.map +1 -1
  236. package/dist/src/types/agent.d.ts +4 -0
  237. package/dist/src/types/agent.d.ts.map +1 -1
  238. package/dist/src/types/events.d.ts +62 -9
  239. package/dist/src/types/events.d.ts.map +1 -1
  240. package/dist/src/types/framework.d.ts +62 -58
  241. package/dist/src/types/framework.d.ts.map +1 -1
  242. package/dist/src/types/module.d.ts +40 -8
  243. package/dist/src/types/module.d.ts.map +1 -1
  244. package/dist/src/types/trace.d.ts +111 -10
  245. package/dist/src/types/trace.d.ts.map +1 -1
  246. package/dist/src/usage/types.d.ts +16 -0
  247. package/dist/src/usage/types.d.ts.map +1 -1
  248. package/dist/src/usage/usage-tracker.d.ts +14 -1
  249. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  250. package/dist/src/usage/usage-tracker.js +81 -41
  251. package/dist/src/usage/usage-tracker.js.map +1 -1
  252. package/package.json +6 -5
  253. package/src/agent.ts +93 -12
  254. package/src/api/server.ts +14 -12
  255. package/src/code-execution/engine.ts +595 -0
  256. package/src/code-execution/index.ts +5 -17
  257. package/src/code-execution/isolate/calls.ts +199 -0
  258. package/src/code-execution/isolate/child-entry.ts +5 -0
  259. package/src/code-execution/isolate/child-main.ts +132 -0
  260. package/src/code-execution/isolate/child.ts +327 -0
  261. package/src/code-execution/isolate/frames.ts +209 -0
  262. package/src/code-execution/isolate/output.ts +161 -0
  263. package/src/code-execution/isolate/prelude.ts +409 -0
  264. package/src/code-execution/isolate/protocol.ts +240 -0
  265. package/src/code-execution/isolate/render.ts +230 -0
  266. package/src/code-execution/isolate/runner.ts +911 -0
  267. package/src/code-execution/isolate/scripts.ts +479 -0
  268. package/src/code-execution/quickjs.wasm +0 -0
  269. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  270. package/src/code-execution/tool-definition.ts +236 -167
  271. package/src/deadline-timer.ts +1 -1
  272. package/src/framework.ts +4468 -2418
  273. package/src/gate/event-gate.ts +78 -11
  274. package/src/held-writes.ts +17 -3
  275. package/src/index.ts +30 -22
  276. package/src/mcpl/channel-registry.ts +1825 -368
  277. package/src/mcpl/conversation-router.ts +12 -7
  278. package/src/mcpl/convert-block.ts +9 -5
  279. package/src/mcpl/inbound-source.ts +131 -0
  280. package/src/mcpl/index.ts +4 -2
  281. package/src/mcpl/log-value.ts +9 -0
  282. package/src/mcpl/modern-connection.ts +162 -53
  283. package/src/mcpl/prose-grammar.ts +4 -5
  284. package/src/mcpl/protocol-family.ts +199 -21
  285. package/src/mcpl/push-coalescer.ts +306 -62
  286. package/src/mcpl/push-handler.ts +10 -2
  287. package/src/mcpl/reference-fetcher.ts +4 -4
  288. package/src/mcpl/server-connection.ts +180 -79
  289. package/src/mcpl/tags.ts +36 -0
  290. package/src/mcpl/tool-result-normalize.ts +2 -1
  291. package/src/mcpl/transport.ts +162 -33
  292. package/src/mcpl/types.ts +23 -6
  293. package/src/module-registry.ts +92 -13
  294. package/src/modules/discord/index.ts +8 -13
  295. package/src/modules/history/index.ts +25 -3
  296. package/src/modules/history/semantic.ts +29 -6
  297. package/src/modules/index.ts +1 -0
  298. package/src/modules/unknown-tool.ts +4 -1
  299. package/src/modules/workspace/branch-intent.ts +3 -1
  300. package/src/modules/workspace/edit-plan.ts +954 -0
  301. package/src/modules/workspace/edit-text.ts +618 -0
  302. package/src/modules/workspace/effects.ts +70 -19
  303. package/src/modules/workspace/index.ts +2247 -642
  304. package/src/modules/workspace/observe.ts +465 -56
  305. package/src/modules/workspace/reconcile.ts +14 -7
  306. package/src/modules/workspace/text.ts +68 -0
  307. package/src/modules/workspace/types.ts +211 -6
  308. package/src/modules/workspace/watcher.ts +3 -2
  309. package/src/operator-log.ts +47 -19
  310. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  311. package/src/recovery/offline-branch.ts +189 -55
  312. package/src/recovery/recover-cli.ts +25 -4
  313. package/src/send-tools.ts +11 -0
  314. package/src/tool-result-guard.ts +6 -5
  315. package/src/tool-result-history.ts +1 -1
  316. package/src/tune-out/coordinator.ts +163 -42
  317. package/src/types/agent.ts +4 -0
  318. package/src/types/events.ts +62 -9
  319. package/src/types/framework.ts +62 -58
  320. package/src/types/module.ts +40 -8
  321. package/src/types/trace.ts +121 -15
  322. package/src/usage/types.ts +16 -0
  323. package/src/usage/usage-tracker.ts +86 -43
  324. package/dist/src/code-execution/py-runner.d.ts +0 -191
  325. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  326. package/dist/src/code-execution/py-runner.js +0 -655
  327. package/dist/src/code-execution/py-runner.js.map +0 -1
  328. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  329. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  330. package/dist/src/code-execution/runtime-py.js +0 -677
  331. package/dist/src/code-execution/runtime-py.js.map +0 -1
  332. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  333. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  334. package/dist/src/mcpl/prose-stream-router.js +0 -291
  335. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  336. package/src/code-execution/py-runner.ts +0 -791
  337. package/src/code-execution/runtime-py.ts +0 -677
  338. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -0,0 +1,794 @@
1
+ /**
2
+ * Exact-text matching for edit and edit_many: where each edit's old text
3
+ * occurs in a file, what stops an edit applying, and the file's new bytes once
4
+ * every edit applies. Every edit is matched in the text as read, never in
5
+ * another edit's result, so the order of the edits doesn't matter and no edit
6
+ * can take away the text another needs. No I/O: the caller reads, stores and
7
+ * words the refusals.
8
+ *
9
+ * Ported from anima-research/hands-mcp's src/match.ts, where it serves the
10
+ * Edit and EditMany tools.
11
+ */
12
+ import { MAX_LINE_CHARS, hasBom, numbered, safeCut } from './text.js';
13
+ /**
14
+ * The file's text, or undefined when it isn't valid UTF-8: rewriting such a
15
+ * file would change bytes outside the edits.
16
+ */
17
+ export function decodeForEdit(bytes) {
18
+ const bom = hasBom(bytes);
19
+ let raw;
20
+ try {
21
+ // fatal: a byte sequence that isn't UTF-8 would come back as U+FFFD and be
22
+ // written over the file.
23
+ raw = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bom ? bytes.subarray(3) : bytes);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ return { raw, bom, ...lineEnds(raw) };
29
+ }
30
+ /**
31
+ * A file's text with every CRLF made LF, and where the CRs were: `crs` holds,
32
+ * ascending, the index in `value` of each LF that followed a CR. Matching works
33
+ * on `value`; rawIndex maps an index of it back to the file's own text, so a
34
+ * replacement touches only the bytes it replaces.
35
+ */
36
+ function lineEnds(raw) {
37
+ let lfs = 0;
38
+ for (let at = raw.indexOf('\n'); at !== -1; at = raw.indexOf('\n', at + 1))
39
+ lfs++;
40
+ if (!raw.includes('\r\n'))
41
+ return { value: raw, crs: [], lfs };
42
+ const parts = [];
43
+ const crs = [];
44
+ let length = 0;
45
+ let from = 0;
46
+ for (let at = raw.indexOf('\r\n'); at !== -1; at = raw.indexOf('\r\n', at + 2)) {
47
+ parts.push(raw.slice(from, at));
48
+ length += at - from;
49
+ crs.push(length);
50
+ from = at + 1; // keep the LF
51
+ }
52
+ parts.push(raw.slice(from));
53
+ return { value: parts.join(''), crs, lfs };
54
+ }
55
+ /**
56
+ * Straight and curly quotes compared as equal, for a second-chance match on
57
+ * prose edits. Each quote is one code unit either way, so offsets carry over.
58
+ */
59
+ function normalizeQuotes(value) {
60
+ return value.replace(/[‘’‚‛]/g, "'").replace(/[“”„‟]/g, '"');
61
+ }
62
+ /**
63
+ * The readings of an edit other than as given, where its old text carries
64
+ * what matching leaves out of a file's text: the BOM, which decodeForEdit
65
+ * takes off before the first line, at its start, or a line's CR, which
66
+ * matching reads as part of the line end, at its end. A character page shows
67
+ * both, and a line whose own text ends in a CR shows its end's CR too
68
+ * (shownLine). Each reading leaves out the CR, the BOM, or both, its new text
69
+ * read the same way, so a replacement keeps the file's one BOM and its CRLF
70
+ * line end, and may match only where what it leaves out is the file's: at the
71
+ * text's start for the BOM, and before a CRLF line end for the CR. None where
72
+ * the old text carries neither where the file has them. A reading that
73
+ * leaves nothing, of an old text that is the BOM or the CR alone, is one too:
74
+ * planFile finds it where what it leaves out is the file's.
75
+ */
76
+ function readingsOf(spec, file, crAt) {
77
+ const leading = file.bom && spec.oldText.startsWith('\uFEFF');
78
+ const trailing = file.crs.length > 0 && spec.oldText.endsWith('\r');
79
+ const read = (bom, cr, leaves) => {
80
+ const oldText = spec.oldText.slice(bom ? 1 : 0, cr ? -1 : undefined);
81
+ let newText = spec.newText;
82
+ if (bom && newText.startsWith('\uFEFF'))
83
+ newText = newText.slice(1);
84
+ if (cr && newText.endsWith('\r'))
85
+ newText = newText.slice(0, -1);
86
+ return [{
87
+ spec: { ...spec, oldText, newText },
88
+ // crs holds only LFs that followed a CR, so one there is a CRLF line end.
89
+ accept: (at) => (!bom || at === 0) && (!cr || crAt(at + oldText.length)),
90
+ leaves,
91
+ }];
92
+ };
93
+ return [
94
+ ...(trailing ? read(false, true, 'cr') : []),
95
+ ...(leading ? read(true, false, 'bom') : []),
96
+ ...(leading && trailing ? read(true, true, 'both') : []),
97
+ ];
98
+ }
99
+ export const COUNT_MAX = 5 * 1024 * 1024;
100
+ export const countBudget = (maxBytes = COUNT_MAX) => ({ left: maxBytes, size: maxBytes });
101
+ /**
102
+ * Where needle starts in haystack: every position, overlapping ones included,
103
+ * so "aa" starts twice in "aaa". The first `keep` positions are kept; `count`
104
+ * is all of them, unless occurrences overlap. Those can't each be replaced, so
105
+ * the edit is refused whatever their count, and the scan stops once it has
106
+ * found one more than the SHOWN_MATCHES a refusal shows, so a count it stopped
107
+ * at is more than those shown. Otherwise a needle that overlaps itself,
108
+ * such as a run of spaces, would be compared in full at every position of a
109
+ * long run of the same, at a cost of the run's length times the needle's.
110
+ * With `accept`, only the positions it accepts are occurrences, and the stop
111
+ * can't apply to the positions it passes over, so those are found in one
112
+ * pass over the haystack (everyOccurrence). The first `free` occurrences are
113
+ * counted freely; each past them takes one from `budget`, and the scan stops
114
+ * once it's spent, its count then a floor (`spent`).
115
+ */
116
+ function scan(haystack, needle, keep, accept, free = Infinity, budget) {
117
+ const starts = [];
118
+ let count = 0;
119
+ let overlapping = false;
120
+ let spent = false;
121
+ // indexOf finds the empty string at every position, never -1: callers refuse
122
+ // an empty old text, and this keeps any other caller from looping.
123
+ if (!needle)
124
+ return { starts, count, overlapping, atLeast: false, spent };
125
+ let previous = -Infinity;
126
+ // Takes the occurrence at `at`, and says whether to look for the next.
127
+ const take = (at) => {
128
+ if (accept && !accept(at))
129
+ return true;
130
+ if (at < previous + needle.length)
131
+ overlapping = true;
132
+ if (count < keep)
133
+ starts.push(at);
134
+ count++;
135
+ previous = at;
136
+ if (overlapping && count > SHOWN_MATCHES)
137
+ return false;
138
+ if (count > free) {
139
+ if (!budget || budget.left <= 0)
140
+ return !(spent = true);
141
+ budget.left--;
142
+ }
143
+ return true;
144
+ };
145
+ if (accept) {
146
+ // Run before the answer is made, since the answer reads what it counts.
147
+ const stopped = !everyOccurrence(haystack, needle, take);
148
+ return { starts, count, overlapping, atLeast: stopped, spent };
149
+ }
150
+ for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + 1)) {
151
+ if (!take(at))
152
+ return { starts, count, overlapping, atLeast: true, spent };
153
+ }
154
+ return { starts, count, overlapping, atLeast: false, spent };
155
+ }
156
+ /**
157
+ * Every position needle starts at in haystack, overlapping ones included,
158
+ * each given to `visit` in turn until it returns false, in one pass over the
159
+ * haystack however the needle repeats itself (Knuth–Morris–Pratt). False once
160
+ * `visit` returns false, even for an occurrence that ends the haystack, as
161
+ * scan's indexOf loop stops; true when every occurrence was visited.
162
+ */
163
+ function everyOccurrence(haystack, needle, visit) {
164
+ // border[i]: how long the needle's longest prefix is that also ends its
165
+ // first i + 1 code units, short of all of them.
166
+ const border = new Int32Array(needle.length);
167
+ for (let i = 1, k = 0; i < needle.length; i++) {
168
+ const c = needle.charCodeAt(i);
169
+ while (k > 0 && c !== needle.charCodeAt(k))
170
+ k = border[k - 1];
171
+ if (c === needle.charCodeAt(k))
172
+ k++;
173
+ border[i] = k;
174
+ }
175
+ for (let i = 0, k = 0; i < haystack.length; i++) {
176
+ const c = haystack.charCodeAt(i);
177
+ while (k > 0 && c !== needle.charCodeAt(k))
178
+ k = border[k - 1];
179
+ if (c === needle.charCodeAt(k))
180
+ k++;
181
+ if (k === needle.length) {
182
+ if (!visit(i - k + 1))
183
+ return false;
184
+ k = border[k - 1];
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+ /**
190
+ * The 1-based line of each index (ascending), in one pass over the text: it
191
+ * keeps the next line end it found, so once none remains it reads no further.
192
+ */
193
+ export function lineNumbersAt(value, indices) {
194
+ const lines = [];
195
+ let line = 1;
196
+ let next = value.indexOf('\n');
197
+ for (const index of indices) {
198
+ while (next !== -1 && next < index) {
199
+ line++;
200
+ next = value.indexOf('\n', next + 1);
201
+ }
202
+ lines.push(line);
203
+ }
204
+ return lines;
205
+ }
206
+ /** How many of the ascending values are below x. */
207
+ function countBelow(values, x) {
208
+ let lo = 0;
209
+ let hi = values.length;
210
+ while (lo < hi) {
211
+ const mid = (lo + hi) >> 1;
212
+ if (values[mid] < x)
213
+ lo = mid + 1;
214
+ else
215
+ hi = mid;
216
+ }
217
+ return lo;
218
+ }
219
+ /** Bigram counts of a string. */
220
+ function grams(value) {
221
+ const counts = new Map();
222
+ for (let i = 0; i + 1 < value.length; i++) {
223
+ const gram = value.slice(i, i + 2);
224
+ counts.set(gram, (counts.get(gram) ?? 0) + 1);
225
+ }
226
+ return counts;
227
+ }
228
+ /**
229
+ * Above this many UTF-16 code units of the text (line ends as LF) a miss gets
230
+ * no closest-region hint: scoring every line would hold the host's event loop.
231
+ */
232
+ export const HINT_MAX_CHARS = 2 * 1024 * 1024;
233
+ export const hintBudget = () => ({ left: HINT_MAX_CHARS });
234
+ /**
235
+ * The file region most like the first meaningful line of the old text,
236
+ * numbered for the retry, within the call's hint budget.
237
+ */
238
+ function closestRegion(value, needle, budget) {
239
+ if (value.length > HINT_MAX_CHARS) {
240
+ return `(No closest region for a file over ${HINT_MAX_CHARS.toLocaleString('en-US')} UTF-16 code units; find the text with grep.)`;
241
+ }
242
+ if (value.length > budget.left) {
243
+ return `(No closest region: this call's other misses used its ${HINT_MAX_CHARS.toLocaleString('en-US')} UTF-16 code units of search; find the text with grep.)`;
244
+ }
245
+ budget.left -= value.length;
246
+ // The first line with a bigram to score: a line of one character, such as
247
+ // a closing brace, has none, and no line would score above it. At most its
248
+ // first MAX_LINE_CHARS code units, as many as a hint shows of a line, so a
249
+ // long old text holds no more than that.
250
+ const first = needle
251
+ .split('\n')
252
+ .map((l) => l.trim())
253
+ .find((l) => l.length > 1) ?? needle.trim();
254
+ const probe = first.slice(0, safeCut(first, MAX_LINE_CHARS));
255
+ // Dice coefficient over character bigrams: cheap, order-sensitive enough for
256
+ // "closest line". Each line's bigrams are counted against the probe's, by
257
+ // a slot for each of the probe's, so what scoring holds follows the probe,
258
+ // not the longest line.
259
+ const slot = new Map();
260
+ const most = [];
261
+ for (const [gram, n] of grams(probe)) {
262
+ slot.set(gram, most.length);
263
+ most.push(n);
264
+ }
265
+ const met = new Int32Array(most.length);
266
+ const touched = [];
267
+ const probeTotal = Math.max(0, probe.length - 1);
268
+ const lines = value.split('\n');
269
+ // A final line end ends the last line rather than starting an empty one, as
270
+ // read numbers a file's lines.
271
+ if (lines.length > 1 && lines.at(-1) === '')
272
+ lines.pop();
273
+ let best = 0;
274
+ let bestScore = -1;
275
+ lines.forEach((line, i) => {
276
+ const trimmed = line.trim();
277
+ let shared = 0;
278
+ for (let at = 0; at + 1 < trimmed.length; at++) {
279
+ const k = slot.get(trimmed.slice(at, at + 2));
280
+ if (k === undefined)
281
+ continue;
282
+ if (met[k] === 0)
283
+ touched.push(k);
284
+ if (met[k] < most[k])
285
+ shared++;
286
+ met[k] = met[k] + 1;
287
+ }
288
+ for (const k of touched)
289
+ met[k] = 0;
290
+ touched.length = 0;
291
+ const total = probeTotal + Math.max(0, trimmed.length - 1);
292
+ const score = total ? (2 * shared) / total : 0;
293
+ if (score > bestScore) {
294
+ bestScore = score;
295
+ best = i;
296
+ }
297
+ });
298
+ if (bestScore <= 0)
299
+ return '';
300
+ const from = Math.max(0, best - 3);
301
+ const to = Math.min(lines.length, best + 4);
302
+ // Every line comes before a line feed but a last one the text doesn't end with.
303
+ const shown = lines.slice(from, to).map((line, k) => shownLine(line, from + k < lines.length - 1 || value.endsWith('\n')));
304
+ return `Closest region (line ${best + 1}, similarity ${bestScore.toFixed(2)}):\n${numbered(shown, from + 1)}`;
305
+ }
306
+ /**
307
+ * A line of the text edits are matched in, as an answer shows it: one whose
308
+ * own text ends in a CR, before a line feed, shows its line end's CR too, as
309
+ * read shows such a line. Matching reads a CR before a line feed in old text
310
+ * as part of the line end, so text copied across the line with one CR would
311
+ * match the same text with CRLF ends alone. In that text a CR before a line
312
+ * feed is always such a line's own, since lineEnds made each CRLF a line feed.
313
+ */
314
+ export function shownLine(text, beforeLf) {
315
+ return beforeLf && text.endsWith('\r') ? `${text}\r` : text;
316
+ }
317
+ /** How many matches a refusal names by line and shows. */
318
+ export const SHOWN_MATCHES = 20;
319
+ /** A matched line longer than this many UTF-16 code units is shown as an excerpt this long around its match. */
320
+ const EXCERPT_CHARS = 240;
321
+ /**
322
+ * The lines the first matches fall on, numbered as read numbers them, each
323
+ * line once: what a count refusal shows, so an over-broad match can be judged
324
+ * without another read. A long line, such as a paragraph kept on one line, is
325
+ * shown as an excerpt around its first match.
326
+ */
327
+ function matchedLines(value, starts, lines) {
328
+ const shown = [];
329
+ let last = 0;
330
+ starts.forEach((start, k) => {
331
+ const line = lines[k];
332
+ if (line === last)
333
+ return;
334
+ last = line;
335
+ const lineFrom = start === 0 ? 0 : value.lastIndexOf('\n', start - 1) + 1;
336
+ const newline = value.indexOf('\n', start);
337
+ const lineTo = newline === -1 ? value.length : newline;
338
+ let from = lineFrom;
339
+ let to = lineTo;
340
+ if (lineTo - lineFrom > EXCERPT_CHARS) {
341
+ from = Math.max(lineFrom, start - EXCERPT_CHARS / 3);
342
+ to = Math.min(lineTo, from + EXCERPT_CHARS);
343
+ from = Math.max(lineFrom, to - EXCERPT_CHARS);
344
+ // Never half of a surrogate pair at either cut.
345
+ from = safeCut(value, from);
346
+ to = safeCut(value, to);
347
+ }
348
+ shown.push(`${String(line).padStart(6)}\t${from > lineFrom ? '…' : ''}${shownLine(value.slice(from, to), to === lineTo && newline !== -1)}${to < lineTo ? '…' : ''}`);
349
+ });
350
+ return shown.join('\n');
351
+ }
352
+ /**
353
+ * Text built from many pieces, joined a batch at a time, so the pieces of a
354
+ * replaceAll with millions of occurrences aren't all held at once.
355
+ */
356
+ function textBuilder() {
357
+ const chunks = [];
358
+ let pieces = [];
359
+ const flush = () => {
360
+ chunks.push(pieces.join(''));
361
+ pieces = [];
362
+ };
363
+ return {
364
+ add: (piece) => {
365
+ if (!piece)
366
+ return;
367
+ pieces.push(piece);
368
+ if (pieces.length >= 4096)
369
+ flush();
370
+ },
371
+ text: () => {
372
+ flush();
373
+ return chunks.join('');
374
+ },
375
+ };
376
+ }
377
+ /** How many line feeds a string holds. */
378
+ function lineFeeds(value) {
379
+ let count = 0;
380
+ for (let at = value.indexOf('\n'); at !== -1; at = value.indexOf('\n', at + 1))
381
+ count++;
382
+ return count;
383
+ }
384
+ /**
385
+ * A file's edits applied together, or every problem that stops them. Each
386
+ * edit is matched in the file's text as read: its old text must occur the
387
+ * number of times it asks for, counting overlapping occurrences (so "aa"
388
+ * occurs twice in "aaa"), and occurrences that overlap one another can't each
389
+ * be replaced. An old text that carries the file's BOM at its start or a
390
+ * line's CR at its end is also read without the one, the other or both
391
+ * (readingsOf): one found in more than one reading, as given among them, is
392
+ * refused, since which it means can't be told, a reading in which the edit
393
+ * changes nothing included, and one found only in another reading, and there
394
+ * the same as its new text, has nothing to change. An edit that replaces
395
+ * every occurrence counts no reading in which it changes nothing or that
396
+ * leaves nothing, since edits keep line ends and the BOM. Only where no
397
+ * reading finds it is it matched again with straight and curly quotes
398
+ * treated as equal. Two edits' matches may touch but not overlap. What a
399
+ * call counts past what its edits need takes from its count budget
400
+ * (CountBudget). Only the replaced text changes: every other byte stays as
401
+ * it was, and each new text's line ends take, by position, those of the
402
+ * text it replaces. For each occurrence an edit replaces it holds numbers,
403
+ * not objects: the occurrence's start, and with several edits a key in one
404
+ * Float64Array. The
405
+ * new text is built a batch of pieces at a time, and of each edit's
406
+ * replacements it keeps only what a receipt names. Two bounds keep a call
407
+ * from holding more than its file: two or more edits whose asks, past what a
408
+ * refusal shows, come to more occurrences than the text is long keep no more
409
+ * starts than a refusal shows (one edit keeps the starts it finds, which the
410
+ * text's length bounds), and a file whose counts take it past `maxBytes` is
411
+ * refused as `tooLarge`, at least that many bytes, before its new text is
412
+ * built. One that the CRLF ends its new text takes, or quotes read as equal,
413
+ * take past it is built and returned, for its caller to refuse by its bytes.
414
+ */
415
+ export function planFile(file, bytes, specs, hints = hintBudget(), maxBytes = Infinity, counts = countBudget()) {
416
+ const { raw, value, crs, lfs } = file;
417
+ const crlf = crs.length > 0;
418
+ const mixed = crlf && crs.length < lfs;
419
+ const crAt = (lf) => {
420
+ const i = countBelow(crs, lf);
421
+ return i < crs.length && crs[i] === lf;
422
+ };
423
+ const problems = [];
424
+ // Each edit that matches, with where: the edit as matched (readForm's for an
425
+ // old text read without its BOM or CR), its starts, ascending, and how many.
426
+ const applied = [];
427
+ const quoteNormalized = [];
428
+ const readForms = [];
429
+ let quoted;
430
+ // Each edit keeps the starts it asks for, to replace them. What several ask
431
+ // for past the SHOWN_MATCHES each keeps for a refusal has to fit in the
432
+ // text, a character or more each; when it doesn't, they can't all apply
433
+ // (one finds fewer than it asks for, or two overlap), so they keep only
434
+ // what a refusal shows, and a call asking for millions of each doesn't
435
+ // hold millions for each.
436
+ const pastShown = specs.reduce((sum, spec) => sum + (spec.expected === 'all' ? 0 : Math.max(spec.expected - SHOWN_MATCHES, 0)), 0);
437
+ const crowded = specs.length > 1 && pastShown > value.length;
438
+ // The call's count budget holds as many as this text is long, at least:
439
+ // applyEdits sizes it to the larger of the mount's limit and the largest
440
+ // file the call names, and a caller that sizes it smaller has it raised
441
+ // here.
442
+ if (value.length > counts.size) {
443
+ counts.left += value.length - counts.size;
444
+ counts.size = value.length;
445
+ }
446
+ // Crowded edits whose counts stopped past what a refusal shows, and so
447
+ // can't be set against what they ask for, each with its count problem,
448
+ // made where the crowded check doesn't name it.
449
+ const uncounted = [];
450
+ for (const spec of specs) {
451
+ // Every occurrence when every one is replaced; otherwise enough to replace or to show.
452
+ const keep = crowded ? SHOWN_MATCHES : spec.expected === 'all' ? Infinity : Math.max(spec.expected, SHOWN_MATCHES);
453
+ // What it counts freely: every occurrence it replaces, and the one more
454
+ // that shows it finds more than it asks for. Past that, a count only makes
455
+ // a refusal exact, and takes from the call's budget (CountBudget). Crowded
456
+ // edits are refused whatever they find (below), so they count freely no
457
+ // more than a refusal shows, but for one that replaces every occurrence,
458
+ // which asks for as many as there are, and so counts them all.
459
+ const free = spec.expected === 'all' ? Infinity : crowded ? SHOWN_MATCHES : Math.max(spec.expected, SHOWN_MATCHES) + 1;
460
+ const budget = counts;
461
+ let matched = spec;
462
+ let { starts, count, overlapping, atLeast, spent } = scan(value, spec.oldText, keep, undefined, free, budget);
463
+ let normalized = false;
464
+ // An old text with the file's BOM at its start or a CR at its end reads
465
+ // more ways than as given: without either, or both, as a character page
466
+ // or a line shown with its CRLF end's CR gives it (readingsOf). A line
467
+ // whose own text ends in one CR, shown with its end's, reads as given as a
468
+ // line ending in two. Where more than one reading that changes something
469
+ // finds it, which the edit means can't be told, so it's refused; where one
470
+ // does, it's taken, before quotes are read as equal. That holds for one
471
+ // that changes nothing, its new text read the same way as its old: a copy
472
+ // of a line end's CR or the BOM, as a character page shows them, may mean
473
+ // them, which the file keeps, and alone it has nothing to change. Beside a
474
+ // match as given, a reading need only be found, and shown as a refusal
475
+ // shows it. A reading that leaves nothing, of an old text that is the BOM
476
+ // or a CR alone, is found where what it leaves out is the file's, the BOM
477
+ // at the text's start and a CR before each CRLF line end; it counts only
478
+ // beside another match, as an empty text can't be replaced. An edit that
479
+ // replaces every occurrence counts neither: edits keep line ends and the
480
+ // BOM, so where a reading changes nothing, or leaves nothing, it can only
481
+ // mean the occurrences that change.
482
+ const all = spec.expected === 'all';
483
+ const emptyAt = (leaves) => (leaves === 'cr' ? crs : file.bom && (leaves === 'bom' || crAt(0)) ? [0] : []);
484
+ const allReads = readingsOf(spec, file, crAt)
485
+ .filter((read) => read.spec.oldText || !all)
486
+ .map((read) => {
487
+ if (read.spec.oldText) {
488
+ return { read, found: count > 0 ? scan(value, read.spec.oldText, SHOWN_MATCHES, read.accept, SHOWN_MATCHES) : scan(value, read.spec.oldText, keep, read.accept, free, budget) };
489
+ }
490
+ const at = emptyAt(read.leaves);
491
+ return { read, found: { starts: at.slice(0, SHOWN_MATCHES), count: at.length, overlapping: false, atLeast: false, spent: false } };
492
+ })
493
+ .filter(({ found }) => found.count > 0);
494
+ const same = ({ read }) => read.spec.oldText === read.spec.newText;
495
+ const counted = all ? allReads.filter((reading) => !same(reading)) : allReads;
496
+ const others = (count > 0 ? 1 : 0) + counted.filter(({ read }) => read.spec.oldText).length;
497
+ const reads = counted.filter(({ read }) => read.spec.oldText || others > 0);
498
+ if (reads.length + (count > 0 ? 1 : 0) > 1) {
499
+ const where = (found) => ({ found: found.count, atLeast: found.atLeast, lines: lineNumbersAt(value, found.starts.slice(0, SHOWN_MATCHES)) });
500
+ problems.push({
501
+ kind: 'readings',
502
+ edit: spec.index,
503
+ bom: file.bom && spec.oldText.startsWith('\uFEFF'),
504
+ cr: crlf && spec.oldText.endsWith('\r'),
505
+ readings: [
506
+ ...(count > 0 ? [{ leaves: 'nothing', same: false, ...where({ starts, count, overlapping, atLeast, spent }) }] : []),
507
+ ...reads.map((reading) => ({ leaves: reading.read.leaves, same: same(reading), ...where(reading.found) })),
508
+ ],
509
+ });
510
+ continue;
511
+ }
512
+ if (count === 0 && reads.length === 1 && !same(reads[0])) {
513
+ const { read, found: where } = reads[0];
514
+ ({ starts, count, overlapping, atLeast, spent } = where);
515
+ matched = read.spec;
516
+ readForms.push(spec.index);
517
+ }
518
+ else if (count === 0 && reads.length < 2 && allReads.some((reading) => reading.read.spec.oldText && same(reading))) {
519
+ // Read as the file's own, the BOM or CR was all the edit changed. A
520
+ // reading that leaves nothing is never taken alone, so it misses.
521
+ problems.push({ kind: 'same', edit: spec.index });
522
+ continue;
523
+ }
524
+ if (count === 0) {
525
+ ({ starts, count, overlapping, atLeast, spent } = scan((quoted ??= normalizeQuotes(value)), normalizeQuotes(spec.oldText), keep, undefined, free, budget));
526
+ normalized = count > 0;
527
+ }
528
+ if (count === 0) {
529
+ problems.push({ kind: 'miss', edit: spec.index, hint: closestRegion(value, spec.oldText, hints), crlf });
530
+ continue;
531
+ }
532
+ // A count the budget stopped is a floor, and so isn't the one asked for
533
+ // even where it's that many, which only a crowded edit's can be.
534
+ if (overlapping || (spec.expected !== 'all' && (count !== spec.expected || spent))) {
535
+ const found = { count, atLeast, spent, overlapping, normalized, first: starts.slice(0, SHOWN_MATCHES) };
536
+ const problem = () => {
537
+ const lines = lineNumbersAt(value, found.first);
538
+ return {
539
+ kind: 'count',
540
+ edit: spec.index,
541
+ expected: spec.expected,
542
+ found: found.count,
543
+ atLeast: found.atLeast,
544
+ spent: found.spent,
545
+ lines,
546
+ shown: matchedLines(value, found.first, lines),
547
+ quoteNormalized: found.normalized,
548
+ overlapping: found.overlapping,
549
+ };
550
+ };
551
+ // A crowded edit whose count stopped at no more than it asks for may
552
+ // find what it asks for: the crowded check below names it, or else its
553
+ // count, as a floor.
554
+ if (crowded && spent && !overlapping && count <= spec.expected)
555
+ uncounted.push({ spec, problem });
556
+ else
557
+ problems.push(problem());
558
+ continue;
559
+ }
560
+ if (normalized)
561
+ quoteNormalized.push(spec.index);
562
+ applied.push({ spec: matched, starts, count });
563
+ }
564
+ if (crowded) {
565
+ // Where the edits that found what they ask for, and those whose counts
566
+ // stopped short of it, ask between them for more occurrences than the
567
+ // text is long, each takes one or more, so some overlap, though where
568
+ // isn't kept: they're named as crowded. Otherwise some edit found other
569
+ // than it asks for, and each that stopped short is named by its count,
570
+ // which says it's a floor.
571
+ // An edit that replaces every occurrence asks for as many as it found.
572
+ const asks = [
573
+ ...applied.map(({ spec, count }) => ({ edit: spec.index, asked: spec.expected === 'all' ? count : spec.expected })),
574
+ ...uncounted.map(({ spec }) => ({ edit: spec.index, asked: spec.expected })),
575
+ ].sort((a, b) => a.edit - b.edit);
576
+ const asked = asks.reduce((sum, ask) => sum + ask.asked, 0);
577
+ if (asked > value.length)
578
+ problems.push({ kind: 'crowded', edits: asks.map((ask) => ask.edit), asked, length: value.length });
579
+ // A loop, not a spread: a call's edits can be more than a spread passes.
580
+ else
581
+ for (const { problem } of uncounted)
582
+ problems.push(problem());
583
+ return { problems };
584
+ }
585
+ // Every replacement in file order. One edit's starts are that already; for
586
+ // several, each replacement is a number, its start times the edits plus
587
+ // the edit's slot, so sorting them sorts by start, then by the edit's place
588
+ // in the call's list.
589
+ applied.sort((a, b) => a.spec.index - b.spec.index);
590
+ const slots = applied.length;
591
+ const total = applied.reduce((sum, { starts }) => sum + starts.length, 0);
592
+ let order;
593
+ if (slots > 1) {
594
+ order = new Float64Array(total);
595
+ let k = 0;
596
+ applied.forEach(({ starts }, slot) => {
597
+ for (const start of starts)
598
+ order[k++] = start * slots + slot;
599
+ });
600
+ order.sort();
601
+ }
602
+ const slotOf = (k) => (order ? order[k] % slots : 0);
603
+ const startOf = (k) => (order ? (order[k] - (order[k] % slots)) / slots : applied[0].starts[k]);
604
+ // Overlapping matches are named at their first overlap, whatever order the
605
+ // list gave them in, and each edit at most once as the later of a pair, so
606
+ // the problems grow with the edits rather than their pairs: a thousand edits
607
+ // of one stretch are a thousand problems at most, not half a million. One
608
+ // edit's own occurrences don't overlap (scan), so only several can clash.
609
+ const named = new Set();
610
+ const pairs = new Set();
611
+ const clashes = [];
612
+ // A later replacement whose edit is already named can't name it again, so
613
+ // the first loop to meet it links past it (nextLater), and the loops after
614
+ // cross a run of such replacements in one step: many edits matching one
615
+ // text cost their replacements, not their pairs at every place.
616
+ const nextLater = slots > 1 ? new Int32Array(total + 1) : undefined;
617
+ for (let k = 0; nextLater && k <= total; k++)
618
+ nextLater[k] = k;
619
+ const laterFrom = (k) => {
620
+ let found = k;
621
+ while (nextLater[found] !== found)
622
+ found = nextLater[found];
623
+ while (nextLater[k] !== found) {
624
+ const step = nextLater[k];
625
+ nextLater[k] = found;
626
+ k = step;
627
+ }
628
+ return found;
629
+ };
630
+ for (let i = 0; slots > 1 && i < total; i++) {
631
+ const first = applied[slotOf(i)].spec;
632
+ const firstStart = startOf(i);
633
+ const firstEnd = firstStart + first.oldText.length;
634
+ for (let j = laterFrom(i + 1); j < total && startOf(j) < firstEnd; j = laterFrom(j + 1)) {
635
+ const later = applied[slotOf(j)].spec;
636
+ if (named.has(later.index)) {
637
+ nextLater[j] = j + 1;
638
+ continue;
639
+ }
640
+ const laterStart = startOf(j);
641
+ const a = Math.min(first.index, later.index);
642
+ const b = Math.max(first.index, later.index);
643
+ if (a === b || pairs.has(`${a} ${b}`))
644
+ continue;
645
+ named.add(later.index);
646
+ pairs.add(`${a} ${b}`);
647
+ clashes.push({ edits: [a, b], at: laterStart, same: laterStart === firstStart && laterStart + later.oldText.length === firstEnd });
648
+ }
649
+ }
650
+ // Every clash's line in one pass over the text, rather than a pass each.
651
+ const clashAts = [...new Set(clashes.map((clash) => clash.at))].sort((x, y) => x - y);
652
+ const lineAt = new Map(lineNumbersAt(value, clashAts).map((line, k) => [clashAts[k], line]));
653
+ for (const clash of clashes)
654
+ problems.push({ kind: 'clash', edits: clash.edits, line: lineAt.get(clash.at), same: clash.same });
655
+ if (problems.length)
656
+ return { problems };
657
+ // The new bytes are at least the file's, and for each replacement its new
658
+ // text's, less the most its replaced text can take: its own bytes, in a
659
+ // file with CRLF line ends one more for each line feed, which it may hold
660
+ // as two, and matched with quotes read as equal, two more for each straight
661
+ // quote, which a curly one takes as three. That's exact for a file with no
662
+ // CRLF line end matched as given. So a file
663
+ // the edits take past maxBytes is refused before its new text, which could
664
+ // run to gigabytes, is built. One that this floor leaves within it but the
665
+ // CRLF ends its new text takes, or its quotes, take past it is built, and
666
+ // its caller refuses it by its bytes.
667
+ const quotesRead = new Set(quoteNormalized);
668
+ const straightQuotes = (text) => text.length - text.replace(/['"]/g, '').length;
669
+ const leastBytes = applied.reduce((size, { spec, count }) => size + count * (Buffer.byteLength(spec.newText)
670
+ - Buffer.byteLength(spec.oldText) - (crlf ? lineFeeds(spec.oldText) : 0) - (quotesRead.has(spec.index) ? 2 * straightQuotes(spec.oldText) : 0)), bytes.length);
671
+ if (leastBytes > maxBytes)
672
+ return { tooLarge: leastBytes };
673
+ const endingAt = (lf) => (crAt(lf) ? '\r\n' : '\n');
674
+ // The line end after a replaced text that holds none: its line's own, found
675
+ // once per line as the matches ascend, or for an unterminated last line the
676
+ // file's last one. Only a file with CRLF line ends asks, so it has one.
677
+ let nextLf = -1;
678
+ let lastLf;
679
+ const lineEnding = (start) => {
680
+ if (nextLf !== Infinity && nextLf < start) {
681
+ const at = value.indexOf('\n', start);
682
+ nextLf = at === -1 ? Infinity : at;
683
+ }
684
+ return endingAt(nextLf !== Infinity ? nextLf : (lastLf ??= value.lastIndexOf('\n')));
685
+ };
686
+ // The new text's line ends take, by position, the line ends of the text it
687
+ // replaces: the k-th the k-th, any beyond them the last, so a mixed span
688
+ // keeps its mixture. A match's line ends sit where oldText's do,
689
+ // quote-normalized or not.
690
+ const shapes = new Map();
691
+ const withEndings = (spec, start) => {
692
+ if (!crlf)
693
+ return spec.newText;
694
+ let shape = shapes.get(spec);
695
+ if (!shape) {
696
+ const oldBreaks = [];
697
+ for (let at = spec.oldText.indexOf('\n'); at !== -1; at = spec.oldText.indexOf('\n', at + 1))
698
+ oldBreaks.push(at);
699
+ shapes.set(spec, (shape = { oldBreaks, newPieces: spec.newText.split('\n') }));
700
+ }
701
+ const { oldBreaks, newPieces } = shape;
702
+ if (newPieces.length === 1)
703
+ return spec.newText;
704
+ const replaced = oldBreaks.length ? oldBreaks.map((k) => endingAt(start + k)) : [lineEnding(start)];
705
+ let joined = newPieces[0];
706
+ for (let k = 1; k < newPieces.length; k++)
707
+ joined += replaced[Math.min(k - 1, replaced.length - 1)] + newPieces[k];
708
+ return joined;
709
+ };
710
+ // The CRs below an index of the text, counted as the replacements ascend:
711
+ // each index asked is at or past the one before.
712
+ let crsBelow = 0;
713
+ const rawIndex = (i) => {
714
+ while (crsBelow < crs.length && crs[crsBelow] < i)
715
+ crsBelow++;
716
+ return i + crsBelow;
717
+ };
718
+ // What each edit's new text adds: its line feeds, and the lines a receipt
719
+ // shows it on.
720
+ const added = applied.map(({ spec }) => {
721
+ const feeds = lineFeeds(spec.newText);
722
+ return { feeds, lines: feeds + 1 - (spec.newText.endsWith('\n') ? 1 : 0) };
723
+ });
724
+ const out = textBuilder();
725
+ const updated = textBuilder();
726
+ const kept = applied.map(() => undefined);
727
+ // The line of each edit's last replacement after its first; lines start at 1.
728
+ const lastOther = applied.map(() => 0);
729
+ let rawFrom = 0;
730
+ let from = 0;
731
+ let length = 0;
732
+ // The updated text's line at `from`: the line feeds of the text kept before
733
+ // it, and of each new text put in, counted as the replacements ascend. The
734
+ // text's next line feed is kept as it's found, so text with none left is
735
+ // read to its end once, not once per replacement.
736
+ let line = 1;
737
+ let nextFeed = value.indexOf('\n');
738
+ const feedsBefore = (to) => {
739
+ let feeds = 0;
740
+ while (nextFeed !== -1 && nextFeed < to) {
741
+ feeds++;
742
+ nextFeed = value.indexOf('\n', nextFeed + 1);
743
+ }
744
+ return feeds;
745
+ };
746
+ for (let k = 0; k < total; k++) {
747
+ const slot = slotOf(k);
748
+ const { spec } = applied[slot];
749
+ const start = startOf(k);
750
+ out.add(raw.slice(rawFrom, rawIndex(start)));
751
+ out.add(withEndings(spec, start));
752
+ rawFrom = rawIndex(start + spec.oldText.length);
753
+ updated.add(value.slice(from, start));
754
+ updated.add(spec.newText);
755
+ line += feedsBefore(start);
756
+ length += start - from;
757
+ const edit = kept[slot];
758
+ if (!edit) {
759
+ kept[slot] = { first: { edit: spec.index, at: length, line, lines: added[slot].lines }, count: 1, otherLines: [], otherLineCount: 0 };
760
+ }
761
+ else {
762
+ edit.count++;
763
+ // An edit's replacements ascend, so their lines never fall: a line
764
+ // already counted is the last one.
765
+ if (lastOther[slot] !== line) {
766
+ lastOther[slot] = line;
767
+ edit.otherLineCount++;
768
+ if (edit.otherLines.length < SHOWN_MATCHES)
769
+ edit.otherLines.push(line);
770
+ }
771
+ }
772
+ length += spec.newText.length;
773
+ line += added[slot].feeds;
774
+ from = start + spec.oldText.length;
775
+ // The old text's line feeds are replaced, not kept.
776
+ feedsBefore(from);
777
+ }
778
+ out.add(raw.slice(rawFrom));
779
+ updated.add(value.slice(from));
780
+ return {
781
+ plan: {
782
+ bytes: Buffer.concat([file.bom ? bytes.subarray(0, 3) : Buffer.alloc(0), Buffer.from(out.text(), 'utf8')]),
783
+ updated: updated.text(),
784
+ edits: kept.filter((edit) => edit !== undefined).sort((a, b) => a.first.at - b.first.at),
785
+ replacements: total,
786
+ quoteNormalized,
787
+ readForm: readForms,
788
+ crlf,
789
+ mixed,
790
+ bom: file.bom,
791
+ },
792
+ };
793
+ }
794
+ //# sourceMappingURL=edit-plan.js.map