repoburg 1.3.9 → 1.3.10

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 (226) hide show
  1. package/backend/dist/bin/grammar/tree-sitter-bash.wasm +0 -0
  2. package/backend/dist/bin/grammar/tree-sitter-css.wasm +0 -0
  3. package/backend/dist/bin/grammar/tree-sitter-html.wasm +0 -0
  4. package/backend/dist/bin/grammar/tree-sitter-javascript.wasm +0 -0
  5. package/backend/dist/bin/grammar/tree-sitter-json.wasm +0 -0
  6. package/backend/dist/bin/grammar/tree-sitter-python.wasm +0 -0
  7. package/backend/dist/bin/grammar/tree-sitter-rust.wasm +0 -0
  8. package/backend/dist/bin/grammar/tree-sitter-toml.wasm +0 -0
  9. package/backend/dist/bin/grammar/tree-sitter-tsx.wasm +0 -0
  10. package/backend/dist/bin/grammar/tree-sitter-typescript.wasm +0 -0
  11. package/backend/dist/bin/grammar/tree-sitter-yaml.wasm +0 -0
  12. package/backend/dist/packages/tokenpatch/index.d.ts +1 -0
  13. package/backend/dist/packages/tokenpatch/index.js +52 -0
  14. package/backend/dist/packages/tokenpatch/index.js.map +1 -0
  15. package/backend/dist/packages/tokenpatch/parser.d.ts +2 -0
  16. package/backend/dist/packages/tokenpatch/parser.js +17 -0
  17. package/backend/dist/packages/tokenpatch/parser.js.map +1 -0
  18. package/backend/dist/packages/tokenpatch/patcher.d.ts +11 -0
  19. package/backend/dist/packages/tokenpatch/patcher.js +189 -0
  20. package/backend/dist/packages/tokenpatch/patcher.js.map +1 -0
  21. package/backend/dist/packages/tokenpatch/tokens.d.ts +6 -0
  22. package/backend/dist/packages/tokenpatch/tokens.js +49 -0
  23. package/backend/dist/packages/tokenpatch/tokens.js.map +1 -0
  24. package/backend/dist/packages/tokenpatch/types.d.ts +8 -0
  25. package/backend/dist/packages/tokenpatch/types.js +3 -0
  26. package/backend/dist/packages/tokenpatch/types.js.map +1 -0
  27. package/backend/dist/src/ai-actions/ai-actions.service.d.ts +1 -0
  28. package/backend/dist/src/ai-actions/ai-actions.service.js +2 -0
  29. package/backend/dist/src/ai-actions/ai-actions.service.js.map +1 -1
  30. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.d.ts +4 -0
  31. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.js +29 -0
  32. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.js.map +1 -0
  33. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.d.ts +12 -0
  34. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.js +142 -0
  35. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.js.map +1 -0
  36. package/backend/dist/src/llm-orchestration/llm-orchestration.module.js +2 -0
  37. package/backend/dist/src/llm-orchestration/llm-orchestration.module.js.map +1 -1
  38. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +1 -0
  39. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
  40. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.d.ts +2 -0
  41. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.js +463 -0
  42. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.js.map +1 -0
  43. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  44. package/backend/package.json +6 -3
  45. package/backend/packages/tokenpatch/grammar/tree-sitter-tsx.wasm +0 -0
  46. package/backend/packages/tokenpatch/grammar/tree-sitter-typescript.wasm +0 -0
  47. package/backend/packages/tokenpatch/index.spec.ts +579 -0
  48. package/backend/packages/tokenpatch/index.ts +80 -0
  49. package/backend/packages/tokenpatch/parser.ts +18 -0
  50. package/backend/packages/tokenpatch/patcher.spec.ts +194 -0
  51. package/backend/packages/tokenpatch/patcher.ts +300 -0
  52. package/backend/packages/tokenpatch/tokens.spec.ts +84 -0
  53. package/backend/packages/tokenpatch/tokens.ts +50 -0
  54. package/backend/packages/tokenpatch/types.ts +9 -0
  55. package/package.json +13 -12
  56. package/backend/coverage/clover.xml +0 -3434
  57. package/backend/coverage/coverage-final.json +0 -120
  58. package/backend/coverage/lcov-report/base.css +0 -224
  59. package/backend/coverage/lcov-report/block-navigation.js +0 -87
  60. package/backend/coverage/lcov-report/favicon.png +0 -0
  61. package/backend/coverage/lcov-report/index.html +0 -701
  62. package/backend/coverage/lcov-report/prettify.css +0 -1
  63. package/backend/coverage/lcov-report/prettify.js +0 -2
  64. package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  65. package/backend/coverage/lcov-report/sorter.js +0 -196
  66. package/backend/coverage/lcov-report/src/action-execution/action-execution.module.ts.html +0 -109
  67. package/backend/coverage/lcov-report/src/action-execution/action-execution.service.ts.html +0 -448
  68. package/backend/coverage/lcov-report/src/action-execution/index.html +0 -131
  69. package/backend/coverage/lcov-report/src/ai-actions/ai-action-batch.service.ts.html +0 -940
  70. package/backend/coverage/lcov-report/src/ai-actions/ai-action-creation.service.ts.html +0 -1243
  71. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.controller.ts.html +0 -664
  72. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.module.ts.html +0 -154
  73. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.service.ts.html +0 -859
  74. package/backend/coverage/lcov-report/src/ai-actions/index.html +0 -176
  75. package/backend/coverage/lcov-report/src/app.controller.ts.html +0 -151
  76. package/backend/coverage/lcov-report/src/app.module.ts.html +0 -373
  77. package/backend/coverage/lcov-report/src/app.service.ts.html +0 -196
  78. package/backend/coverage/lcov-report/src/application-state/application-state.controller.ts.html +0 -319
  79. package/backend/coverage/lcov-report/src/application-state/application-state.module.ts.html +0 -124
  80. package/backend/coverage/lcov-report/src/application-state/application-state.service.ts.html +0 -439
  81. package/backend/coverage/lcov-report/src/application-state/dto/index.html +0 -161
  82. package/backend/coverage/lcov-report/src/application-state/dto/set-auto-context-fetch-enabled.dto.ts.html +0 -103
  83. package/backend/coverage/lcov-report/src/application-state/dto/set-orchestration-timeout.dto.ts.html +0 -103
  84. package/backend/coverage/lcov-report/src/application-state/dto/set-theme.dto.ts.html +0 -106
  85. package/backend/coverage/lcov-report/src/application-state/dto/set-websocket-enabled.dto.ts.html +0 -103
  86. package/backend/coverage/lcov-report/src/application-state/index.html +0 -146
  87. package/backend/coverage/lcov-report/src/context-generation/context-generation.module.ts.html +0 -118
  88. package/backend/coverage/lcov-report/src/context-generation/context-generation.service.ts.html +0 -1348
  89. package/backend/coverage/lcov-report/src/context-generation/index.html +0 -131
  90. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.controller.ts.html +0 -289
  91. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.module.ts.html +0 -136
  92. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.service.ts.html +0 -400
  93. package/backend/coverage/lcov-report/src/context-snippets/dto/context-snippet.dto.ts.html +0 -211
  94. package/backend/coverage/lcov-report/src/context-snippets/dto/index.html +0 -116
  95. package/backend/coverage/lcov-report/src/context-snippets/index.html +0 -146
  96. package/backend/coverage/lcov-report/src/context-templates/context-templates.controller.ts.html +0 -397
  97. package/backend/coverage/lcov-report/src/context-templates/context-templates.module.ts.html +0 -136
  98. package/backend/coverage/lcov-report/src/context-templates/context-templates.service.ts.html +0 -835
  99. package/backend/coverage/lcov-report/src/context-templates/dto/context-template.dto.ts.html +0 -358
  100. package/backend/coverage/lcov-report/src/context-templates/dto/index.html +0 -116
  101. package/backend/coverage/lcov-report/src/context-templates/index.html +0 -146
  102. package/backend/coverage/lcov-report/src/core-entities/ai-action.entity.ts.html +0 -241
  103. package/backend/coverage/lcov-report/src/core-entities/application-state.entity.ts.html +0 -115
  104. package/backend/coverage/lcov-report/src/core-entities/base.entity.ts.html +0 -145
  105. package/backend/coverage/lcov-report/src/core-entities/context-snippet.entity.ts.html +0 -130
  106. package/backend/coverage/lcov-report/src/core-entities/context-template.entity.ts.html +0 -223
  107. package/backend/coverage/lcov-report/src/core-entities/custom-snippet.entity.ts.html +0 -136
  108. package/backend/coverage/lcov-report/src/core-entities/execution-log.entity.ts.html +0 -157
  109. package/backend/coverage/lcov-report/src/core-entities/index.html +0 -281
  110. package/backend/coverage/lcov-report/src/core-entities/index.ts.html +0 -118
  111. package/backend/coverage/lcov-report/src/core-entities/project.entity.ts.html +0 -130
  112. package/backend/coverage/lcov-report/src/core-entities/session-input.entity.ts.html +0 -289
  113. package/backend/coverage/lcov-report/src/core-entities/session.entity.ts.html +0 -280
  114. package/backend/coverage/lcov-report/src/core-entities/system-prompt.entity.ts.html +0 -148
  115. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.controller.ts.html +0 -277
  116. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.module.ts.html +0 -124
  117. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.service.ts.html +0 -304
  118. package/backend/coverage/lcov-report/src/custom-snippets/dto/custom-snippet.dto.ts.html +0 -205
  119. package/backend/coverage/lcov-report/src/custom-snippets/dto/index.html +0 -116
  120. package/backend/coverage/lcov-report/src/custom-snippets/index.html +0 -146
  121. package/backend/coverage/lcov-report/src/events/events.gateway.ts.html +0 -292
  122. package/backend/coverage/lcov-report/src/events/events.module.ts.html +0 -109
  123. package/backend/coverage/lcov-report/src/events/index.html +0 -131
  124. package/backend/coverage/lcov-report/src/execution-logs/dto/execution-log.dto.ts.html +0 -130
  125. package/backend/coverage/lcov-report/src/execution-logs/dto/index.html +0 -116
  126. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.controller.ts.html +0 -130
  127. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.module.ts.html +0 -124
  128. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.service.ts.html +0 -238
  129. package/backend/coverage/lcov-report/src/execution-logs/index.html +0 -146
  130. package/backend/coverage/lcov-report/src/http-exception.filter.ts.html +0 -340
  131. package/backend/coverage/lcov-report/src/index.html +0 -191
  132. package/backend/coverage/lcov-report/src/llm-response-parser/dto/ai-action.dto.ts.html +0 -400
  133. package/backend/coverage/lcov-report/src/llm-response-parser/dto/index.html +0 -116
  134. package/backend/coverage/lcov-report/src/llm-response-parser/errors/index.html +0 -116
  135. package/backend/coverage/lcov-report/src/llm-response-parser/errors/parsing.error.ts.html +0 -118
  136. package/backend/coverage/lcov-report/src/llm-response-parser/index.html +0 -146
  137. package/backend/coverage/lcov-report/src/llm-response-parser/llm-response-parser.module.ts.html +0 -109
  138. package/backend/coverage/lcov-report/src/llm-response-parser/llm-response-parser.service.ts.html +0 -808
  139. package/backend/coverage/lcov-report/src/llm-response-parser/parsing.constants.ts.html +0 -139
  140. package/backend/coverage/lcov-report/src/llm-responses/dto/index.html +0 -116
  141. package/backend/coverage/lcov-report/src/llm-responses/dto/submit-llm-response.dto.ts.html +0 -172
  142. package/backend/coverage/lcov-report/src/llm-responses/index.html +0 -146
  143. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.controller.ts.html +0 -154
  144. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.module.ts.html +0 -166
  145. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.service.ts.html +0 -787
  146. package/backend/coverage/lcov-report/src/main.ts.html +0 -382
  147. package/backend/coverage/lcov-report/src/orchestration/dto/index.html +0 -116
  148. package/backend/coverage/lcov-report/src/orchestration/dto/orchestration.dto.ts.html +0 -169
  149. package/backend/coverage/lcov-report/src/orchestration/index.html +0 -191
  150. package/backend/coverage/lcov-report/src/orchestration/orchestration-fs.service.ts.html +0 -595
  151. package/backend/coverage/lcov-report/src/orchestration/orchestration-parser.service.ts.html +0 -142
  152. package/backend/coverage/lcov-report/src/orchestration/orchestration.controller.ts.html +0 -406
  153. package/backend/coverage/lcov-report/src/orchestration/orchestration.module.ts.html +0 -169
  154. package/backend/coverage/lcov-report/src/orchestration/orchestration.service.ts.html +0 -1093
  155. package/backend/coverage/lcov-report/src/orchestration/orchestration.types.ts.html +0 -175
  156. package/backend/coverage/lcov-report/src/projects/dto/index.html +0 -116
  157. package/backend/coverage/lcov-report/src/projects/dto/project.dto.ts.html +0 -154
  158. package/backend/coverage/lcov-report/src/projects/index.html +0 -146
  159. package/backend/coverage/lcov-report/src/projects/projects.controller.ts.html +0 -232
  160. package/backend/coverage/lcov-report/src/projects/projects.module.ts.html +0 -124
  161. package/backend/coverage/lcov-report/src/projects/projects.service.ts.html +0 -223
  162. package/backend/coverage/lcov-report/src/seeding/context-template-seeding.service.ts.html +0 -355
  163. package/backend/coverage/lcov-report/src/seeding/custom-snippet-seeding.service.ts.html +0 -271
  164. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-followup_ad-hoc-focused-context.ts.html +0 -136
  165. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-initial_condensed-project-context.ts.html +0 -148
  166. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-initial_full-project-context.ts.html +0 -247
  167. package/backend/coverage/lcov-report/src/seeding/data/context-templates/index.html +0 -190
  168. package/backend/coverage/lcov-report/src/seeding/data/context-templates/pm-context.ts.html +0 -250
  169. package/backend/coverage/lcov-report/src/seeding/data/context-templates/pr-description.ts.html +0 -186
  170. package/backend/coverage/lcov-report/src/seeding/data/context-templates/sample_focused-tree.ts.html +0 -124
  171. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/default_rglob.ts.html +0 -94
  172. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/git-diff.ts.html +0 -94
  173. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/index.html +0 -236
  174. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-exclude.ts.html +0 -94
  175. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-search-glob.ts.html +0 -94
  176. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-search.ts.html +0 -94
  177. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/run-command.ts.html +0 -94
  178. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/tree.ts.html +0 -94
  179. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/usr-adhoc-incl.ts.html +0 -100
  180. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/usr-input-incl.ts.html +0 -94
  181. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/codebase-explorer.ts.html +0 -331
  182. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/default_multi-file-action-generator-with-requester.ts.html +0 -675
  183. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/index.html +0 -160
  184. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/multi-file-action-generator.ts.html +0 -550
  185. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/packup.ts.html +0 -184
  186. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/refactor-split.ts.html +0 -244
  187. package/backend/coverage/lcov-report/src/seeding/index.html +0 -176
  188. package/backend/coverage/lcov-report/src/seeding/seeding.module.ts.html +0 -145
  189. package/backend/coverage/lcov-report/src/seeding/seeding.service.ts.html +0 -151
  190. package/backend/coverage/lcov-report/src/seeding/system-prompt-seeding.service.ts.html +0 -289
  191. package/backend/coverage/lcov-report/src/session-followup/index.html +0 -131
  192. package/backend/coverage/lcov-report/src/session-followup/session-followup.module.ts.html +0 -130
  193. package/backend/coverage/lcov-report/src/session-followup/session-followup.service.ts.html +0 -670
  194. package/backend/coverage/lcov-report/src/session-inputs/dto/index.html +0 -116
  195. package/backend/coverage/lcov-report/src/session-inputs/dto/session-input.dto.ts.html +0 -247
  196. package/backend/coverage/lcov-report/src/session-inputs/index.html +0 -161
  197. package/backend/coverage/lcov-report/src/session-inputs/session-input-context.service.ts.html +0 -763
  198. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.controller.ts.html +0 -337
  199. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.module.ts.html +0 -205
  200. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.service.ts.html +0 -1621
  201. package/backend/coverage/lcov-report/src/session-transfer/index.html +0 -131
  202. package/backend/coverage/lcov-report/src/session-transfer/session-transfer.module.ts.html +0 -172
  203. package/backend/coverage/lcov-report/src/session-transfer/session-transfer.service.ts.html +0 -574
  204. package/backend/coverage/lcov-report/src/sessions/dto/index.html +0 -116
  205. package/backend/coverage/lcov-report/src/sessions/dto/session.dto.ts.html +0 -340
  206. package/backend/coverage/lcov-report/src/sessions/index.html +0 -146
  207. package/backend/coverage/lcov-report/src/sessions/sessions.controller.ts.html +0 -457
  208. package/backend/coverage/lcov-report/src/sessions/sessions.module.ts.html +0 -214
  209. package/backend/coverage/lcov-report/src/sessions/sessions.service.ts.html +0 -844
  210. package/backend/coverage/lcov-report/src/system-prompts/dto/index.html +0 -116
  211. package/backend/coverage/lcov-report/src/system-prompts/dto/system-prompt.dto.ts.html +0 -217
  212. package/backend/coverage/lcov-report/src/system-prompts/index.html +0 -146
  213. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.controller.ts.html +0 -298
  214. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.module.ts.html +0 -127
  215. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.service.ts.html +0 -352
  216. package/backend/coverage/lcov-report/src/timeout.interceptor.ts.html +0 -178
  217. package/backend/coverage/lcov-report/src/utils/fuzzy-search.ts.html +0 -310
  218. package/backend/coverage/lcov-report/src/utils/index.html +0 -131
  219. package/backend/coverage/lcov-report/src/utils/index.ts.html +0 -88
  220. package/backend/coverage/lcov-report/src/workspace/dto/index.html +0 -116
  221. package/backend/coverage/lcov-report/src/workspace/dto/search-workspace.dto.ts.html +0 -193
  222. package/backend/coverage/lcov-report/src/workspace/index.html +0 -146
  223. package/backend/coverage/lcov-report/src/workspace/workspace.controller.ts.html +0 -247
  224. package/backend/coverage/lcov-report/src/workspace/workspace.module.ts.html +0 -121
  225. package/backend/coverage/lcov-report/src/workspace/workspace.service.ts.html +0 -745
  226. package/backend/coverage/lcov.info +0 -5590
@@ -0,0 +1,194 @@
1
+ import {
2
+ handleBeginOfFilePatch,
3
+ handleEndOfFilePatch,
4
+ handleStandardPatch,
5
+ } from './patcher';
6
+ import type { Token } from './types';
7
+
8
+ // Helper to create mock tokens for testing.
9
+ // Simplified: we only care about text, startIndex, and endIndex for these tests.
10
+ const makeTokens = (texts: string[]): Token[] => {
11
+ let offset = 0;
12
+ return texts.map((text) => {
13
+ const start = offset;
14
+ const end = start + text.length;
15
+ offset = end + 1; // Simulate space
16
+ return {
17
+ text,
18
+ type: 'test',
19
+ startIndex: start,
20
+ endIndex: end,
21
+ startPosition: { row: 0, column: start },
22
+ };
23
+ });
24
+ };
25
+
26
+ describe('patcher', () => {
27
+ describe('handleStandardPatch', () => {
28
+ it('should find a unique prefix and suffix', () => {
29
+ const sourceTokens = makeTokens(['prefix', '(', 'old', ')', 'suffix']);
30
+ const patchTokens = makeTokens(['prefix', '(', 'new', ')', 'suffix']);
31
+ const result = handleStandardPatch(sourceTokens, patchTokens);
32
+ expect(result).toEqual({
33
+ patchInsertEnd: 21,
34
+ patchInsertStart: 0,
35
+ replaceStart: 0, // 'prefix' startIndex
36
+ replaceEnd: 21, // 'suffix' endIndex
37
+ });
38
+ });
39
+
40
+ it('should trim non-matching tokens from both ends to find a match', () => {
41
+ const sourceTokens = makeTokens(['prefix', 'content', 'suffix']);
42
+ // The patch has extra tokens at the start and end that aren't in the source.
43
+ const patchTokens = makeTokens([
44
+ 'bad', // 0-3
45
+ 'prefix', // 4-10
46
+ 'new-content', // 11-22
47
+ 'suffix', // 23-29
48
+ 'good', // 30-34
49
+ ]);
50
+ const result = handleStandardPatch(sourceTokens, patchTokens);
51
+ // It should fail with 'bad'/'good', then trim them, then succeed on ['prefix', 'new-content', 'suffix']
52
+ expect(result).toEqual({
53
+ replaceStart: 0, // start of 'prefix' in source
54
+ replaceEnd: 21, // end of 'suffix' in source
55
+ patchInsertStart: 4, // start of 'prefix' in patch
56
+ patchInsertEnd: 29, // end of 'suffix' in patch
57
+ });
58
+ });
59
+
60
+ it('should throw for persistently ambiguous prefix', () => {
61
+ const sourceTokens = makeTokens(['a', 'b', 'c', 'a', 'b', 'd']);
62
+ const patchTokens = makeTokens(['a', 'b', 'e']); // a b is ambiguous
63
+ expect(() => handleStandardPatch(sourceTokens, patchTokens)).toThrow(
64
+ /Ambiguous prefix anchor/,
65
+ );
66
+ });
67
+
68
+ it('should throw for persistently ambiguous suffix', () => {
69
+ const sourceTokens = makeTokens(['prefix', 'x', 'suffix', 'y', 'suffix']);
70
+ const patchTokens = makeTokens(['prefix', 'z', 'suffix']);
71
+ expect(() => handleStandardPatch(sourceTokens, patchTokens)).toThrow(
72
+ /Ambiguous suffix anchor/,
73
+ );
74
+ });
75
+
76
+ it('should throw if patch has fewer than two tokens', () => {
77
+ const sourceTokens = makeTokens(['a', 'b', 'c']);
78
+ const patchTokens = makeTokens(['a']);
79
+ expect(() => handleStandardPatch(sourceTokens, patchTokens)).toThrow(
80
+ 'Patch must contain at least two tokens to form a prefix and a suffix.',
81
+ );
82
+ });
83
+ });
84
+
85
+ describe('handleBeginOfFilePatch', () => {
86
+ it('should find a unique suffix anchor', () => {
87
+ const sourceTokens = makeTokens(['a', 'b', 'c', 'd']);
88
+ const patchTokens = makeTokens(['x', 'y', 'c', 'd']); // Suffix is 'c', 'd'
89
+ const result = handleBeginOfFilePatch(sourceTokens, patchTokens);
90
+ expect(result).toEqual({
91
+ patchInsertEnd: 7,
92
+ patchInsertStart: 0,
93
+ replaceStart: 0,
94
+ replaceEnd: 7, // endIndex of 'd'
95
+ });
96
+ });
97
+
98
+ it('should trim from the start to find a unique suffix', () => {
99
+ // The "good" part of the patch, ['a', 'b'], is unique in the source.
100
+ const sourceTokens = makeTokens(['x', 'y', 'a', 'b']);
101
+ // The initial patch will fail because the suffix ['y', 'a', 'b'] is not in the source.
102
+ const patchTokens = makeTokens([
103
+ 'bad', // 0-3
104
+ 'token', // 4-9
105
+ 'a', // 10-11
106
+ 'b', // 12-13
107
+ ]);
108
+ const result = handleBeginOfFilePatch(sourceTokens, patchTokens);
109
+ // Should fail, trim 'bad', fail, trim 'token', then succeed with suffix ['a', 'b'].
110
+ expect(result).toEqual({
111
+ replaceStart: 0,
112
+ replaceEnd: 7, // end of 'b' in source
113
+ patchInsertStart: 0, // start of 'a' in patch
114
+ patchInsertEnd: 13, // end of 'b' in patch
115
+ });
116
+ });
117
+
118
+ it('should throw for a persistently ambiguous suffix', () => {
119
+ const sourceTokens = makeTokens(['a', 'b', 'a', 'b']);
120
+ const patchTokens = makeTokens(['x', 'y', 'a', 'b']);
121
+ expect(() => handleBeginOfFilePatch(sourceTokens, patchTokens)).toThrow(
122
+ /Ambiguous suffix anchor/,
123
+ );
124
+ });
125
+
126
+ it('should throw if anchor not found even after trimming', () => {
127
+ const sourceTokens = makeTokens(['a', 'b', 'c']);
128
+ const patchTokens = makeTokens(['x', 'y', 'z']);
129
+ expect(() => handleBeginOfFilePatch(sourceTokens, patchTokens)).toThrow(
130
+ /Suffix anchor not found/,
131
+ );
132
+ });
133
+ });
134
+
135
+ describe('handleEndOfFilePatch', () => {
136
+ const sourceCode = 'a b c d'; // length 7
137
+ const sourceTokens = makeTokens(['a', 'b', 'c', 'd']);
138
+
139
+ it('should find a unique prefix anchor', () => {
140
+ const patchTokens = makeTokens(['a', 'b', 'x', 'y']); // Prefix is 'a', 'b'
141
+ const result = handleEndOfFilePatch(
142
+ sourceTokens,
143
+ patchTokens,
144
+ sourceCode,
145
+ );
146
+ expect(result).toEqual({
147
+ patchInsertEnd: 7,
148
+ patchInsertStart: 0,
149
+ replaceStart: 0, // startIndex of 'a'
150
+ replaceEnd: 7, // sourceCode.length
151
+ });
152
+ });
153
+
154
+ it('should trim from the end to find a unique prefix', () => {
155
+ // The "good" part of the patch, ['c', 'd'], is unique in the source.
156
+ const sourceTokens = makeTokens(['c', 'd', 'x', 'y']);
157
+ const sourceCode = 'c d x y'; // length 7
158
+ // The initial patch will fail because the prefix ['c', 'd', 'bad'] is not in the source.
159
+ const patchTokens = makeTokens([
160
+ 'c', // 0-1
161
+ 'd', // 2-3
162
+ 'bad', // 4-7
163
+ 'token', // 8-13
164
+ ]);
165
+ const result = handleEndOfFilePatch(
166
+ sourceTokens,
167
+ patchTokens,
168
+ sourceCode,
169
+ );
170
+ // Should fail, trim 'token', fail, trim 'bad', then succeed with prefix ['c', 'd'].
171
+ expect(result).toEqual({
172
+ replaceStart: 0, // start of 'c' in source
173
+ replaceEnd: 7, // sourceCode.length
174
+ patchInsertStart: 0, // start of 'c' in patch
175
+ patchInsertEnd: 13, // end of 'd' in patch
176
+ });
177
+ });
178
+
179
+ it('should throw for a persistently ambiguous prefix', () => {
180
+ const ambiguousSourceTokens = makeTokens(['a', 'b', 'a', 'b']);
181
+ const patchTokens = makeTokens(['a', 'b', 'x', 'y']);
182
+ expect(() =>
183
+ handleEndOfFilePatch(ambiguousSourceTokens, patchTokens, 'a b a b'),
184
+ ).toThrow(/Ambiguous prefix anchor/);
185
+ });
186
+
187
+ it('should throw if anchor not found even after trimming', () => {
188
+ const patchTokens = makeTokens(['x', 'y', 'z']);
189
+ expect(() =>
190
+ handleEndOfFilePatch(sourceTokens, patchTokens, sourceCode),
191
+ ).toThrow(/Prefix anchor not found/);
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,300 @@
1
+ import type { Token } from './types';
2
+ import { findAllSequences, formatAnchor } from './tokens';
3
+
4
+ interface PatchResult {
5
+ replaceStart: number;
6
+ replaceEnd: number;
7
+ patchInsertStart: number;
8
+ patchInsertEnd: number;
9
+ }
10
+
11
+ interface SimplePatchResult {
12
+ replaceStart: number;
13
+ replaceEnd: number;
14
+ }
15
+
16
+ // Internal helper for the original matching logic
17
+ function _findBeginOfFilePatchLocation(
18
+ sourceTokens: Token[],
19
+ patchTokens: Token[],
20
+ ): SimplePatchResult {
21
+ let replaceStart: number | null = null;
22
+ let replaceEnd: number | null = null;
23
+ let lastError = '';
24
+
25
+ for (let anchorSize = 1; anchorSize <= patchTokens.length; anchorSize++) {
26
+ const suffixAnchor = patchTokens.slice(patchTokens.length - anchorSize);
27
+ const indices = findAllSequences(sourceTokens, suffixAnchor);
28
+
29
+ if (indices.length === 1) {
30
+ replaceStart = 0;
31
+ const suffixIndex = indices[0];
32
+ replaceEnd = sourceTokens[suffixIndex + anchorSize - 1].endIndex;
33
+ break; // Found unique anchor
34
+ }
35
+
36
+ if (indices.length > 1) {
37
+ const formattedAnchor = formatAnchor(suffixAnchor);
38
+ const locations = indices
39
+ .map((i) => `line ${sourceTokens[i].startPosition.row + 1}`)
40
+ .join(', ');
41
+ lastError = `Ambiguous suffix anchor. The sequence "${formattedAnchor}" was found at ${indices.length} locations: ${locations}.`;
42
+ }
43
+ }
44
+
45
+ if (replaceStart === null || replaceEnd === null) {
46
+ const smallestAnchor = patchTokens.slice(patchTokens.length - 1);
47
+ throw new Error(
48
+ lastError ||
49
+ `Suffix anchor not found. The sequence "${formatAnchor(
50
+ smallestAnchor,
51
+ )}" could not be located.`,
52
+ );
53
+ }
54
+
55
+ return { replaceStart, replaceEnd };
56
+ }
57
+
58
+ export function handleBeginOfFilePatch(
59
+ sourceTokens: Token[],
60
+ originalPatchTokens: Token[],
61
+ ): PatchResult {
62
+ if (originalPatchTokens.length === 0) {
63
+ throw new Error('Patch is empty after removing @begin-of-file marker.');
64
+ }
65
+
66
+ let patchAttempt = [...originalPatchTokens];
67
+ let lastError: Error | null = null;
68
+
69
+ // Outer loop for trimming tokens from the start
70
+ while (patchAttempt.length >= 1) {
71
+ try {
72
+ const { replaceStart, replaceEnd } = _findBeginOfFilePatchLocation(
73
+ sourceTokens,
74
+ patchAttempt,
75
+ );
76
+ const patchInsertStart = patchAttempt[0].startIndex;
77
+ const patchInsertEnd = patchAttempt[patchAttempt.length - 1].endIndex;
78
+ return { replaceStart, replaceEnd, patchInsertStart, patchInsertEnd };
79
+ } catch (e) {
80
+ lastError = e as Error;
81
+ patchAttempt = patchAttempt.slice(1); // Trim one token from the beginning
82
+ }
83
+ }
84
+
85
+ throw new Error(
86
+ `Failed to apply @begin-of-file patch. Could not find a unique anchor, even after trimming tokens. Last known error: ${lastError?.message}`,
87
+ );
88
+ }
89
+
90
+ // Internal helper for the original matching logic
91
+ function _findEndOfFilePatchLocation(
92
+ sourceTokens: Token[],
93
+ patchTokens: Token[],
94
+ sourceCode: string,
95
+ ): SimplePatchResult {
96
+ let replaceStart: number | null = null;
97
+ let replaceEnd: number | null = null;
98
+ let lastError = '';
99
+
100
+ for (let anchorSize = 1; anchorSize <= patchTokens.length; anchorSize++) {
101
+ const prefixAnchor = patchTokens.slice(0, anchorSize);
102
+ const indices = findAllSequences(sourceTokens, prefixAnchor);
103
+
104
+ if (indices.length === 1) {
105
+ replaceStart = sourceTokens[indices[0]].startIndex;
106
+ replaceEnd = sourceCode.length;
107
+ break; // Found unique anchor
108
+ }
109
+
110
+ if (indices.length > 1) {
111
+ const formattedAnchor = formatAnchor(prefixAnchor);
112
+ const locations = indices
113
+ .map((i) => `line ${sourceTokens[i].startPosition.row + 1}`)
114
+ .join(', ');
115
+ lastError = `Ambiguous prefix anchor. The sequence "${formattedAnchor}" was found at ${indices.length} locations: ${locations}.`;
116
+ }
117
+ }
118
+
119
+ if (replaceStart === null || replaceEnd === null) {
120
+ const smallestAnchor = patchTokens.slice(0, 1);
121
+ throw new Error(
122
+ lastError ||
123
+ `Prefix anchor not found. The sequence "${formatAnchor(
124
+ smallestAnchor,
125
+ )}" could not be located.`,
126
+ );
127
+ }
128
+ return { replaceStart, replaceEnd };
129
+ }
130
+
131
+ export function handleEndOfFilePatch(
132
+ sourceTokens: Token[],
133
+ originalPatchTokens: Token[],
134
+ sourceCode: string,
135
+ ): PatchResult {
136
+ if (originalPatchTokens.length === 0) {
137
+ throw new Error('Patch is empty after removing @end-of-file marker.');
138
+ }
139
+
140
+ let patchAttempt = [...originalPatchTokens];
141
+ let lastError: Error | null = null;
142
+
143
+ // Outer loop for trimming tokens from the end
144
+ while (patchAttempt.length >= 1) {
145
+ try {
146
+ const { replaceStart, replaceEnd } = _findEndOfFilePatchLocation(
147
+ sourceTokens,
148
+ patchAttempt,
149
+ sourceCode,
150
+ );
151
+ const patchInsertStart = patchAttempt[0].startIndex;
152
+ const patchInsertEnd = patchAttempt[patchAttempt.length - 1].endIndex;
153
+ return { replaceStart, replaceEnd, patchInsertStart, patchInsertEnd };
154
+ } catch (e) {
155
+ lastError = e as Error;
156
+ patchAttempt = patchAttempt.slice(0, -1); // Trim one token from the end
157
+ }
158
+ }
159
+
160
+ throw new Error(
161
+ `Failed to apply @end-of-file patch. Could not find a unique anchor, even after trimming tokens. Last known error: ${lastError?.message}`,
162
+ );
163
+ }
164
+
165
+ // Internal helper for the original matching logic
166
+ function _findStandardPatchLocation(
167
+ sourceTokens: Token[],
168
+ patchTokens: Token[],
169
+ ): SimplePatchResult {
170
+ // 1. Find smallest unique prefix
171
+ let prefixAnchor: Token[] | null = null;
172
+ let prefixIndex: number | null = null;
173
+ let bestPrefixError: string | null = null;
174
+
175
+ for (let prefixSize = 1; prefixSize < patchTokens.length; prefixSize++) {
176
+ const currentPrefix = patchTokens.slice(0, prefixSize);
177
+ const prefixIndices = findAllSequences(sourceTokens, currentPrefix);
178
+
179
+ if (prefixIndices.length === 1) {
180
+ prefixAnchor = currentPrefix;
181
+ prefixIndex = prefixIndices[0];
182
+ bestPrefixError = null; // Found it
183
+ break;
184
+ }
185
+ if (prefixIndices.length > 1) {
186
+ const formatted = formatAnchor(currentPrefix);
187
+ const locations = prefixIndices
188
+ .map((i) => `line ${sourceTokens[i].startPosition.row + 1}`)
189
+ .join(', ');
190
+ bestPrefixError = `Ambiguous prefix anchor. The sequence "${formatted}" was found at ${prefixIndices.length} locations: ${locations}.`;
191
+ }
192
+ if (prefixIndices.length === 0) {
193
+ if (!bestPrefixError) {
194
+ const formatted = formatAnchor(currentPrefix);
195
+ bestPrefixError = `Prefix anchor not found: "${formatted}"`;
196
+ }
197
+ break; // A larger prefix will also not be found
198
+ }
199
+ }
200
+
201
+ if (!prefixAnchor || prefixIndex === null) {
202
+ throw new Error(
203
+ bestPrefixError || 'Could not find a unique prefix anchor.',
204
+ );
205
+ }
206
+
207
+ // 2. Find smallest unique suffix after prefix
208
+ let suffixAnchor: Token[] | null = null;
209
+ let suffixIndex: number | null = null;
210
+ let bestSuffixError: string | null = null;
211
+ const searchStartIndex = prefixIndex + prefixAnchor.length;
212
+ const sourceAfterPrefix = sourceTokens.slice(searchStartIndex);
213
+
214
+ for (
215
+ let suffixSize = 1;
216
+ suffixSize <= patchTokens.length - prefixAnchor.length;
217
+ suffixSize++
218
+ ) {
219
+ const currentSuffix = patchTokens.slice(patchTokens.length - suffixSize);
220
+ const suffixIndices = findAllSequences(sourceAfterPrefix, currentSuffix);
221
+
222
+ if (suffixIndices.length === 1) {
223
+ suffixAnchor = currentSuffix;
224
+ suffixIndex = searchStartIndex + suffixIndices[0];
225
+ bestSuffixError = null; // Found it
226
+ break;
227
+ }
228
+ if (suffixIndices.length > 1) {
229
+ const formatted = formatAnchor(currentSuffix);
230
+ const locations = suffixIndices
231
+ .map(
232
+ (i) =>
233
+ `line ${
234
+ sourceTokens[searchStartIndex + i].startPosition.row + 1
235
+ }`,
236
+ )
237
+ .join(', ');
238
+ bestSuffixError = `Ambiguous suffix anchor. The sequence "${formatted}" was found ${suffixIndices.length} times after the prefix: ${locations}.`;
239
+ }
240
+ }
241
+ if (!suffixAnchor || suffixIndex === null) {
242
+ if (bestSuffixError) {
243
+ throw new Error(bestSuffixError);
244
+ }
245
+ const prefixLocation = `line ${
246
+ sourceTokens[prefixIndex].startPosition.row + 1
247
+ }`;
248
+ const formattedPrefix = formatAnchor(prefixAnchor);
249
+ const smallestSuffix = formatAnchor(
250
+ patchTokens.slice(patchTokens.length - 1),
251
+ );
252
+
253
+ throw new Error(
254
+ `Could not find a unique suffix anchor after the prefix anchor "${formattedPrefix}" (found at ${prefixLocation}). ` +
255
+ `The smallest suffix searched for ("${smallestSuffix}") was not found after it.`,
256
+ );
257
+ }
258
+
259
+ // 3. Apply patch
260
+ const replaceStart = sourceTokens[prefixIndex].startIndex;
261
+ const replaceEnd =
262
+ sourceTokens[suffixIndex + suffixAnchor.length - 1].endIndex;
263
+
264
+ return { replaceStart, replaceEnd };
265
+ }
266
+
267
+ export function handleStandardPatch(
268
+ sourceTokens: Token[],
269
+ originalPatchTokens: Token[],
270
+ ): PatchResult {
271
+ if (originalPatchTokens.length < 2) {
272
+ throw new Error(
273
+ 'Patch must contain at least two tokens to form a prefix and a suffix.',
274
+ );
275
+ }
276
+
277
+ let patchAttempt = [...originalPatchTokens];
278
+ let lastError: Error | null = null;
279
+
280
+ // Outer loop for trimming tokens from both ends
281
+ while (patchAttempt.length >= 2) {
282
+ try {
283
+ const { replaceStart, replaceEnd } = _findStandardPatchLocation(
284
+ sourceTokens,
285
+ patchAttempt,
286
+ );
287
+ const patchInsertStart = patchAttempt[0].startIndex;
288
+ const patchInsertEnd = patchAttempt[patchAttempt.length - 1].endIndex;
289
+ return { replaceStart, replaceEnd, patchInsertStart, patchInsertEnd };
290
+ } catch (e) {
291
+ lastError = e as Error;
292
+ // Trim one token from the start and one from the end for the next attempt
293
+ patchAttempt = patchAttempt.slice(1, -1);
294
+ }
295
+ }
296
+
297
+ throw new Error(
298
+ `Failed to apply patch. Could not find a unique anchor in the source file, even after trimming ambiguous tokens. Last known error: ${lastError?.message}`,
299
+ );
300
+ }
@@ -0,0 +1,84 @@
1
+ import { findAllSequences, tokensEqual, formatAnchor } from './tokens';
2
+ import type { Token } from './types';
3
+
4
+ // A helper to create mock tokens for testing
5
+ const makeTokens = (texts: string[]): Token[] => {
6
+ let startIndex = 0;
7
+ return texts.map((text) => {
8
+ const token: Token = {
9
+ text,
10
+ type: 'identifier', // type and positions are not important for these tests
11
+ startIndex: startIndex,
12
+ endIndex: startIndex + text.length,
13
+ startPosition: { row: 0, column: startIndex },
14
+ };
15
+ startIndex += text.length + 1; // +1 for space
16
+ return token;
17
+ });
18
+ };
19
+
20
+ describe('token-utils', () => {
21
+ describe('findAllSequences', () => {
22
+ it('should find a single unique sequence', () => {
23
+ const haystack = makeTokens(['a', 'b', 'c', 'd', 'e']);
24
+ const needle = makeTokens(['b', 'c']);
25
+ const indices = findAllSequences(haystack, needle);
26
+ expect(indices).toEqual([1]);
27
+ });
28
+
29
+ it('should find multiple occurrences of a sequence', () => {
30
+ const haystack = makeTokens(['a', 'b', 'c', 'a', 'b', 'c']);
31
+ const needle = makeTokens(['a', 'b', 'c']);
32
+ const indices = findAllSequences(haystack, needle);
33
+ expect(indices).toEqual([0, 3]);
34
+ });
35
+
36
+ it('should return an empty array if sequence is not found', () => {
37
+ const haystack = makeTokens(['a', 'b', 'c']);
38
+ const needle = makeTokens(['d', 'e']);
39
+ const indices = findAllSequences(haystack, needle);
40
+ expect(indices).toEqual([]);
41
+ });
42
+
43
+ it('should return an empty array for an empty needle', () => {
44
+ const haystack = makeTokens(['a', 'b', 'c']);
45
+ const needle = makeTokens([]);
46
+ const indices = findAllSequences(haystack, needle);
47
+ expect(indices).toEqual([]);
48
+ });
49
+
50
+ it('should handle sequences at the beginning and end of haystack', () => {
51
+ const haystack = makeTokens(['a', 'b', 'c', 'd', 'a', 'b']);
52
+ const needleAtStart = makeTokens(['a', 'b']);
53
+ const needleAtEnd = makeTokens(['a', 'b']);
54
+ expect(findAllSequences(haystack, needleAtStart)).toEqual([0, 4]);
55
+ expect(findAllSequences(haystack, needleAtEnd)).toEqual([0, 4]);
56
+ });
57
+ });
58
+
59
+ describe('tokensEqual', () => {
60
+ it('should return true for tokens with same text', () => {
61
+ const tokenA = makeTokens(['hello'])[0];
62
+ const tokenB = makeTokens(['hello'])[0];
63
+ expect(tokensEqual(tokenA, tokenB)).toBe(true);
64
+ });
65
+
66
+ it('should return false for tokens with different text', () => {
67
+ const tokenA = makeTokens(['hello'])[0];
68
+ const tokenB = makeTokens(['world'])[0];
69
+ expect(tokensEqual(tokenA, tokenB)).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe('formatAnchor', () => {
74
+ it('should format an anchor to a space-separated string', () => {
75
+ const anchor = makeTokens(['const', 'x', '=', '1', ';']);
76
+ expect(formatAnchor(anchor)).toBe('const x = 1 ;');
77
+ });
78
+
79
+ it('should return an empty string for an empty anchor', () => {
80
+ const anchor = makeTokens([]);
81
+ expect(formatAnchor(anchor)).toBe('');
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,50 @@
1
+ import type { Tree, Node } from 'web-tree-sitter';
2
+ import type { Token } from './types';
3
+
4
+ export function collectTokens(tree: Tree, code: string): Token[] {
5
+ const tokens: Token[] = [];
6
+
7
+ function visit(node: Node) {
8
+ if (node.childCount === 0) {
9
+ tokens.push({
10
+ text: code.slice(node.startIndex, node.endIndex),
11
+ type: node.type,
12
+ startIndex: node.startIndex,
13
+ endIndex: node.endIndex,
14
+ startPosition: node.startPosition,
15
+ });
16
+ return;
17
+ }
18
+ // By iterating over all children (not just named), we include punctuation
19
+ // which is critical for anchor-based matching.
20
+ for (let i = 0; i < node.childCount; i++) {
21
+ const child = node.child(i);
22
+ if (child) visit(child);
23
+ }
24
+ }
25
+
26
+ visit(tree.rootNode);
27
+ return tokens;
28
+ }
29
+
30
+ export function tokensEqual(a: Token, b: Token): boolean {
31
+ return a.text === b.text;
32
+ }
33
+
34
+ export function formatAnchor(anchor: Token[]): string {
35
+ return anchor.map((t) => t.text).join(' ');
36
+ }
37
+
38
+ export function findAllSequences(haystack: Token[], needle: Token[]): number[] {
39
+ if (needle.length === 0) return [];
40
+ const indices: number[] = [];
41
+ outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
42
+ for (let j = 0; j < needle.length; j++) {
43
+ if (!tokensEqual(haystack[i + j], needle[j])) {
44
+ continue outer;
45
+ }
46
+ }
47
+ indices.push(i);
48
+ }
49
+ return indices;
50
+ }
@@ -0,0 +1,9 @@
1
+ import type { Point } from 'web-tree-sitter';
2
+
3
+ export interface Token {
4
+ text: string;
5
+ type: string;
6
+ startIndex: number; // byte offsets (tree-sitter)
7
+ endIndex: number;
8
+ startPosition: Point;
9
+ }