repoburg 1.3.8 → 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 (230) 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/apply-diff.handler.js +6 -3
  31. package/backend/dist/src/llm-orchestration/action-handlers/apply-diff.handler.js.map +1 -1
  32. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.d.ts +4 -0
  33. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.js +29 -0
  34. package/backend/dist/src/llm-orchestration/action-handlers/dto/patch.args.dto.js.map +1 -0
  35. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.d.ts +12 -0
  36. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.js +142 -0
  37. package/backend/dist/src/llm-orchestration/action-handlers/patch.handler.js.map +1 -0
  38. package/backend/dist/src/llm-orchestration/llm-orchestration.module.js +2 -0
  39. package/backend/dist/src/llm-orchestration/llm-orchestration.module.js.map +1 -1
  40. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js +1 -0
  41. package/backend/dist/src/llm-orchestration/llm-turn-processor.service.js.map +1 -1
  42. package/backend/dist/src/seeding/data/system-prompts/experimental_master-agent.d.ts +1 -1
  43. package/backend/dist/src/seeding/data/system-prompts/experimental_master-agent.js +13 -13
  44. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.d.ts +2 -0
  45. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.js +463 -0
  46. package/backend/dist/src/seeding/data/system-prompts/experimental_patch_master-agent.js.map +1 -0
  47. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  48. package/backend/package.json +6 -3
  49. package/backend/packages/tokenpatch/grammar/tree-sitter-tsx.wasm +0 -0
  50. package/backend/packages/tokenpatch/grammar/tree-sitter-typescript.wasm +0 -0
  51. package/backend/packages/tokenpatch/index.spec.ts +579 -0
  52. package/backend/packages/tokenpatch/index.ts +80 -0
  53. package/backend/packages/tokenpatch/parser.ts +18 -0
  54. package/backend/packages/tokenpatch/patcher.spec.ts +194 -0
  55. package/backend/packages/tokenpatch/patcher.ts +300 -0
  56. package/backend/packages/tokenpatch/tokens.spec.ts +84 -0
  57. package/backend/packages/tokenpatch/tokens.ts +50 -0
  58. package/backend/packages/tokenpatch/types.ts +9 -0
  59. package/package.json +13 -12
  60. package/backend/coverage/clover.xml +0 -3434
  61. package/backend/coverage/coverage-final.json +0 -120
  62. package/backend/coverage/lcov-report/base.css +0 -224
  63. package/backend/coverage/lcov-report/block-navigation.js +0 -87
  64. package/backend/coverage/lcov-report/favicon.png +0 -0
  65. package/backend/coverage/lcov-report/index.html +0 -701
  66. package/backend/coverage/lcov-report/prettify.css +0 -1
  67. package/backend/coverage/lcov-report/prettify.js +0 -2
  68. package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  69. package/backend/coverage/lcov-report/sorter.js +0 -196
  70. package/backend/coverage/lcov-report/src/action-execution/action-execution.module.ts.html +0 -109
  71. package/backend/coverage/lcov-report/src/action-execution/action-execution.service.ts.html +0 -448
  72. package/backend/coverage/lcov-report/src/action-execution/index.html +0 -131
  73. package/backend/coverage/lcov-report/src/ai-actions/ai-action-batch.service.ts.html +0 -940
  74. package/backend/coverage/lcov-report/src/ai-actions/ai-action-creation.service.ts.html +0 -1243
  75. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.controller.ts.html +0 -664
  76. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.module.ts.html +0 -154
  77. package/backend/coverage/lcov-report/src/ai-actions/ai-actions.service.ts.html +0 -859
  78. package/backend/coverage/lcov-report/src/ai-actions/index.html +0 -176
  79. package/backend/coverage/lcov-report/src/app.controller.ts.html +0 -151
  80. package/backend/coverage/lcov-report/src/app.module.ts.html +0 -373
  81. package/backend/coverage/lcov-report/src/app.service.ts.html +0 -196
  82. package/backend/coverage/lcov-report/src/application-state/application-state.controller.ts.html +0 -319
  83. package/backend/coverage/lcov-report/src/application-state/application-state.module.ts.html +0 -124
  84. package/backend/coverage/lcov-report/src/application-state/application-state.service.ts.html +0 -439
  85. package/backend/coverage/lcov-report/src/application-state/dto/index.html +0 -161
  86. package/backend/coverage/lcov-report/src/application-state/dto/set-auto-context-fetch-enabled.dto.ts.html +0 -103
  87. package/backend/coverage/lcov-report/src/application-state/dto/set-orchestration-timeout.dto.ts.html +0 -103
  88. package/backend/coverage/lcov-report/src/application-state/dto/set-theme.dto.ts.html +0 -106
  89. package/backend/coverage/lcov-report/src/application-state/dto/set-websocket-enabled.dto.ts.html +0 -103
  90. package/backend/coverage/lcov-report/src/application-state/index.html +0 -146
  91. package/backend/coverage/lcov-report/src/context-generation/context-generation.module.ts.html +0 -118
  92. package/backend/coverage/lcov-report/src/context-generation/context-generation.service.ts.html +0 -1348
  93. package/backend/coverage/lcov-report/src/context-generation/index.html +0 -131
  94. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.controller.ts.html +0 -289
  95. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.module.ts.html +0 -136
  96. package/backend/coverage/lcov-report/src/context-snippets/context-snippets.service.ts.html +0 -400
  97. package/backend/coverage/lcov-report/src/context-snippets/dto/context-snippet.dto.ts.html +0 -211
  98. package/backend/coverage/lcov-report/src/context-snippets/dto/index.html +0 -116
  99. package/backend/coverage/lcov-report/src/context-snippets/index.html +0 -146
  100. package/backend/coverage/lcov-report/src/context-templates/context-templates.controller.ts.html +0 -397
  101. package/backend/coverage/lcov-report/src/context-templates/context-templates.module.ts.html +0 -136
  102. package/backend/coverage/lcov-report/src/context-templates/context-templates.service.ts.html +0 -835
  103. package/backend/coverage/lcov-report/src/context-templates/dto/context-template.dto.ts.html +0 -358
  104. package/backend/coverage/lcov-report/src/context-templates/dto/index.html +0 -116
  105. package/backend/coverage/lcov-report/src/context-templates/index.html +0 -146
  106. package/backend/coverage/lcov-report/src/core-entities/ai-action.entity.ts.html +0 -241
  107. package/backend/coverage/lcov-report/src/core-entities/application-state.entity.ts.html +0 -115
  108. package/backend/coverage/lcov-report/src/core-entities/base.entity.ts.html +0 -145
  109. package/backend/coverage/lcov-report/src/core-entities/context-snippet.entity.ts.html +0 -130
  110. package/backend/coverage/lcov-report/src/core-entities/context-template.entity.ts.html +0 -223
  111. package/backend/coverage/lcov-report/src/core-entities/custom-snippet.entity.ts.html +0 -136
  112. package/backend/coverage/lcov-report/src/core-entities/execution-log.entity.ts.html +0 -157
  113. package/backend/coverage/lcov-report/src/core-entities/index.html +0 -281
  114. package/backend/coverage/lcov-report/src/core-entities/index.ts.html +0 -118
  115. package/backend/coverage/lcov-report/src/core-entities/project.entity.ts.html +0 -130
  116. package/backend/coverage/lcov-report/src/core-entities/session-input.entity.ts.html +0 -289
  117. package/backend/coverage/lcov-report/src/core-entities/session.entity.ts.html +0 -280
  118. package/backend/coverage/lcov-report/src/core-entities/system-prompt.entity.ts.html +0 -148
  119. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.controller.ts.html +0 -277
  120. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.module.ts.html +0 -124
  121. package/backend/coverage/lcov-report/src/custom-snippets/custom-snippets.service.ts.html +0 -304
  122. package/backend/coverage/lcov-report/src/custom-snippets/dto/custom-snippet.dto.ts.html +0 -205
  123. package/backend/coverage/lcov-report/src/custom-snippets/dto/index.html +0 -116
  124. package/backend/coverage/lcov-report/src/custom-snippets/index.html +0 -146
  125. package/backend/coverage/lcov-report/src/events/events.gateway.ts.html +0 -292
  126. package/backend/coverage/lcov-report/src/events/events.module.ts.html +0 -109
  127. package/backend/coverage/lcov-report/src/events/index.html +0 -131
  128. package/backend/coverage/lcov-report/src/execution-logs/dto/execution-log.dto.ts.html +0 -130
  129. package/backend/coverage/lcov-report/src/execution-logs/dto/index.html +0 -116
  130. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.controller.ts.html +0 -130
  131. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.module.ts.html +0 -124
  132. package/backend/coverage/lcov-report/src/execution-logs/execution-logs.service.ts.html +0 -238
  133. package/backend/coverage/lcov-report/src/execution-logs/index.html +0 -146
  134. package/backend/coverage/lcov-report/src/http-exception.filter.ts.html +0 -340
  135. package/backend/coverage/lcov-report/src/index.html +0 -191
  136. package/backend/coverage/lcov-report/src/llm-response-parser/dto/ai-action.dto.ts.html +0 -400
  137. package/backend/coverage/lcov-report/src/llm-response-parser/dto/index.html +0 -116
  138. package/backend/coverage/lcov-report/src/llm-response-parser/errors/index.html +0 -116
  139. package/backend/coverage/lcov-report/src/llm-response-parser/errors/parsing.error.ts.html +0 -118
  140. package/backend/coverage/lcov-report/src/llm-response-parser/index.html +0 -146
  141. package/backend/coverage/lcov-report/src/llm-response-parser/llm-response-parser.module.ts.html +0 -109
  142. package/backend/coverage/lcov-report/src/llm-response-parser/llm-response-parser.service.ts.html +0 -808
  143. package/backend/coverage/lcov-report/src/llm-response-parser/parsing.constants.ts.html +0 -139
  144. package/backend/coverage/lcov-report/src/llm-responses/dto/index.html +0 -116
  145. package/backend/coverage/lcov-report/src/llm-responses/dto/submit-llm-response.dto.ts.html +0 -172
  146. package/backend/coverage/lcov-report/src/llm-responses/index.html +0 -146
  147. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.controller.ts.html +0 -154
  148. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.module.ts.html +0 -166
  149. package/backend/coverage/lcov-report/src/llm-responses/llm-responses.service.ts.html +0 -787
  150. package/backend/coverage/lcov-report/src/main.ts.html +0 -382
  151. package/backend/coverage/lcov-report/src/orchestration/dto/index.html +0 -116
  152. package/backend/coverage/lcov-report/src/orchestration/dto/orchestration.dto.ts.html +0 -169
  153. package/backend/coverage/lcov-report/src/orchestration/index.html +0 -191
  154. package/backend/coverage/lcov-report/src/orchestration/orchestration-fs.service.ts.html +0 -595
  155. package/backend/coverage/lcov-report/src/orchestration/orchestration-parser.service.ts.html +0 -142
  156. package/backend/coverage/lcov-report/src/orchestration/orchestration.controller.ts.html +0 -406
  157. package/backend/coverage/lcov-report/src/orchestration/orchestration.module.ts.html +0 -169
  158. package/backend/coverage/lcov-report/src/orchestration/orchestration.service.ts.html +0 -1093
  159. package/backend/coverage/lcov-report/src/orchestration/orchestration.types.ts.html +0 -175
  160. package/backend/coverage/lcov-report/src/projects/dto/index.html +0 -116
  161. package/backend/coverage/lcov-report/src/projects/dto/project.dto.ts.html +0 -154
  162. package/backend/coverage/lcov-report/src/projects/index.html +0 -146
  163. package/backend/coverage/lcov-report/src/projects/projects.controller.ts.html +0 -232
  164. package/backend/coverage/lcov-report/src/projects/projects.module.ts.html +0 -124
  165. package/backend/coverage/lcov-report/src/projects/projects.service.ts.html +0 -223
  166. package/backend/coverage/lcov-report/src/seeding/context-template-seeding.service.ts.html +0 -355
  167. package/backend/coverage/lcov-report/src/seeding/custom-snippet-seeding.service.ts.html +0 -271
  168. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-followup_ad-hoc-focused-context.ts.html +0 -136
  169. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-initial_condensed-project-context.ts.html +0 -148
  170. package/backend/coverage/lcov-report/src/seeding/data/context-templates/default-initial_full-project-context.ts.html +0 -247
  171. package/backend/coverage/lcov-report/src/seeding/data/context-templates/index.html +0 -190
  172. package/backend/coverage/lcov-report/src/seeding/data/context-templates/pm-context.ts.html +0 -250
  173. package/backend/coverage/lcov-report/src/seeding/data/context-templates/pr-description.ts.html +0 -186
  174. package/backend/coverage/lcov-report/src/seeding/data/context-templates/sample_focused-tree.ts.html +0 -124
  175. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/default_rglob.ts.html +0 -94
  176. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/git-diff.ts.html +0 -94
  177. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/index.html +0 -236
  178. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-exclude.ts.html +0 -94
  179. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-search-glob.ts.html +0 -94
  180. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/rg-search.ts.html +0 -94
  181. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/run-command.ts.html +0 -94
  182. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/tree.ts.html +0 -94
  183. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/usr-adhoc-incl.ts.html +0 -100
  184. package/backend/coverage/lcov-report/src/seeding/data/custom-snippets/usr-input-incl.ts.html +0 -94
  185. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/codebase-explorer.ts.html +0 -331
  186. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/default_multi-file-action-generator-with-requester.ts.html +0 -675
  187. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/index.html +0 -160
  188. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/multi-file-action-generator.ts.html +0 -550
  189. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/packup.ts.html +0 -184
  190. package/backend/coverage/lcov-report/src/seeding/data/system-prompts/refactor-split.ts.html +0 -244
  191. package/backend/coverage/lcov-report/src/seeding/index.html +0 -176
  192. package/backend/coverage/lcov-report/src/seeding/seeding.module.ts.html +0 -145
  193. package/backend/coverage/lcov-report/src/seeding/seeding.service.ts.html +0 -151
  194. package/backend/coverage/lcov-report/src/seeding/system-prompt-seeding.service.ts.html +0 -289
  195. package/backend/coverage/lcov-report/src/session-followup/index.html +0 -131
  196. package/backend/coverage/lcov-report/src/session-followup/session-followup.module.ts.html +0 -130
  197. package/backend/coverage/lcov-report/src/session-followup/session-followup.service.ts.html +0 -670
  198. package/backend/coverage/lcov-report/src/session-inputs/dto/index.html +0 -116
  199. package/backend/coverage/lcov-report/src/session-inputs/dto/session-input.dto.ts.html +0 -247
  200. package/backend/coverage/lcov-report/src/session-inputs/index.html +0 -161
  201. package/backend/coverage/lcov-report/src/session-inputs/session-input-context.service.ts.html +0 -763
  202. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.controller.ts.html +0 -337
  203. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.module.ts.html +0 -205
  204. package/backend/coverage/lcov-report/src/session-inputs/session-inputs.service.ts.html +0 -1621
  205. package/backend/coverage/lcov-report/src/session-transfer/index.html +0 -131
  206. package/backend/coverage/lcov-report/src/session-transfer/session-transfer.module.ts.html +0 -172
  207. package/backend/coverage/lcov-report/src/session-transfer/session-transfer.service.ts.html +0 -574
  208. package/backend/coverage/lcov-report/src/sessions/dto/index.html +0 -116
  209. package/backend/coverage/lcov-report/src/sessions/dto/session.dto.ts.html +0 -340
  210. package/backend/coverage/lcov-report/src/sessions/index.html +0 -146
  211. package/backend/coverage/lcov-report/src/sessions/sessions.controller.ts.html +0 -457
  212. package/backend/coverage/lcov-report/src/sessions/sessions.module.ts.html +0 -214
  213. package/backend/coverage/lcov-report/src/sessions/sessions.service.ts.html +0 -844
  214. package/backend/coverage/lcov-report/src/system-prompts/dto/index.html +0 -116
  215. package/backend/coverage/lcov-report/src/system-prompts/dto/system-prompt.dto.ts.html +0 -217
  216. package/backend/coverage/lcov-report/src/system-prompts/index.html +0 -146
  217. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.controller.ts.html +0 -298
  218. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.module.ts.html +0 -127
  219. package/backend/coverage/lcov-report/src/system-prompts/system-prompts.service.ts.html +0 -352
  220. package/backend/coverage/lcov-report/src/timeout.interceptor.ts.html +0 -178
  221. package/backend/coverage/lcov-report/src/utils/fuzzy-search.ts.html +0 -310
  222. package/backend/coverage/lcov-report/src/utils/index.html +0 -131
  223. package/backend/coverage/lcov-report/src/utils/index.ts.html +0 -88
  224. package/backend/coverage/lcov-report/src/workspace/dto/index.html +0 -116
  225. package/backend/coverage/lcov-report/src/workspace/dto/search-workspace.dto.ts.html +0 -193
  226. package/backend/coverage/lcov-report/src/workspace/index.html +0 -146
  227. package/backend/coverage/lcov-report/src/workspace/workspace.controller.ts.html +0 -247
  228. package/backend/coverage/lcov-report/src/workspace/workspace.module.ts.html +0 -121
  229. package/backend/coverage/lcov-report/src/workspace/workspace.service.ts.html +0 -745
  230. 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
+ }