opencode-dux 1.0.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,586 @@
1
+ import { normalizeUnicode } from './codec';
2
+ import type {
3
+ LineComparator,
4
+ MatchComparatorName,
5
+ MatchHit,
6
+ RescueResult,
7
+ SeekHit,
8
+ } from './types';
9
+
10
+ type NamedComparator = {
11
+ name: MatchComparatorName;
12
+ exact: boolean;
13
+ same: LineComparator;
14
+ };
15
+
16
+ export type PreparedAutoRescueTarget = {
17
+ exact: string;
18
+ unicode: string;
19
+ trimEnd: string;
20
+ unicodeTrimEnd: string;
21
+ };
22
+
23
+ export function equalExact(a: string, b: string): boolean {
24
+ return a === b;
25
+ }
26
+
27
+ export function equalUnicodeExact(a: string, b: string): boolean {
28
+ return normalizeUnicode(a) === normalizeUnicode(b);
29
+ }
30
+
31
+ export function equalTrimEnd(a: string, b: string): boolean {
32
+ return a.trimEnd() === b.trimEnd();
33
+ }
34
+
35
+ export function equalUnicodeTrimEnd(a: string, b: string): boolean {
36
+ return normalizeUnicode(a.trimEnd()) === normalizeUnicode(b.trimEnd());
37
+ }
38
+
39
+ export function equalTrim(a: string, b: string): boolean {
40
+ return a.trim() === b.trim();
41
+ }
42
+
43
+ export function equalUnicodeTrim(a: string, b: string): boolean {
44
+ return normalizeUnicode(a.trim()) === normalizeUnicode(b.trim());
45
+ }
46
+
47
+ const autoRescueComparatorEntries: NamedComparator[] = [
48
+ { name: 'exact', exact: true, same: equalExact },
49
+ { name: 'unicode', exact: false, same: equalUnicodeExact },
50
+ { name: 'trim-end', exact: false, same: equalTrimEnd },
51
+ {
52
+ name: 'unicode-trim-end',
53
+ exact: false,
54
+ same: equalUnicodeTrimEnd,
55
+ },
56
+ ];
57
+
58
+ const comparatorEntries: NamedComparator[] = [
59
+ ...autoRescueComparatorEntries,
60
+ { name: 'trim', exact: false, same: equalTrim },
61
+ { name: 'unicode-trim', exact: false, same: equalUnicodeTrim },
62
+ ];
63
+
64
+ const MAX_LCS_CHUNK_LINES = 48;
65
+ const MAX_LCS_CANDIDATES = 64;
66
+
67
+ export const autoRescueComparators: LineComparator[] =
68
+ autoRescueComparatorEntries.map((entry) => entry.same);
69
+
70
+ export function prepareAutoRescueTarget(
71
+ target: string,
72
+ ): PreparedAutoRescueTarget {
73
+ const trimEnd = target.trimEnd();
74
+ const unicode = normalizeUnicode(target);
75
+
76
+ return {
77
+ exact: target,
78
+ unicode,
79
+ trimEnd,
80
+ unicodeTrimEnd: trimEnd === target ? unicode : normalizeUnicode(trimEnd),
81
+ };
82
+ }
83
+
84
+ export function matchPreparedAutoRescueComparator(
85
+ candidate: string,
86
+ target: PreparedAutoRescueTarget,
87
+ ): MatchComparatorName | undefined {
88
+ if (candidate === target.exact) {
89
+ return 'exact';
90
+ }
91
+
92
+ const unicode = normalizeUnicode(candidate);
93
+ if (unicode === target.unicode) {
94
+ return 'unicode';
95
+ }
96
+
97
+ const trimEnd = candidate.trimEnd();
98
+ if (trimEnd === target.trimEnd) {
99
+ return 'trim-end';
100
+ }
101
+
102
+ const unicodeTrimEnd =
103
+ trimEnd === candidate ? unicode : normalizeUnicode(trimEnd);
104
+ if (unicodeTrimEnd === target.unicodeTrimEnd) {
105
+ return 'unicode-trim-end';
106
+ }
107
+
108
+ return undefined;
109
+ }
110
+
111
+ // Full-trim comparators remain available as explicit utilities, but stay out
112
+ // of automatic canonicalization because they can cross indentation levels and
113
+ // rescue semantically unsafe patches.
114
+ export const permissiveComparators: LineComparator[] = comparatorEntries.map(
115
+ (entry) => entry.same,
116
+ );
117
+
118
+ function tryMatch(
119
+ lines: string[],
120
+ pattern: string[],
121
+ start: number,
122
+ comparator: NamedComparator,
123
+ eof: boolean,
124
+ ): SeekHit | undefined {
125
+ if (eof) {
126
+ const at = lines.length - pattern.length;
127
+ if (at >= start) {
128
+ let ok = true;
129
+ for (let index = 0; index < pattern.length; index += 1) {
130
+ if (!comparator.same(lines[at + index], pattern[index])) {
131
+ ok = false;
132
+ break;
133
+ }
134
+ }
135
+
136
+ if (ok) {
137
+ return {
138
+ index: at,
139
+ comparator: comparator.name,
140
+ exact: comparator.exact,
141
+ };
142
+ }
143
+ }
144
+ }
145
+
146
+ for (let index = start; index <= lines.length - pattern.length; index += 1) {
147
+ let ok = true;
148
+
149
+ for (let inner = 0; inner < pattern.length; inner += 1) {
150
+ if (!comparator.same(lines[index + inner], pattern[inner])) {
151
+ ok = false;
152
+ break;
153
+ }
154
+ }
155
+
156
+ if (ok) {
157
+ return {
158
+ index,
159
+ comparator: comparator.name,
160
+ exact: comparator.exact,
161
+ };
162
+ }
163
+ }
164
+
165
+ return undefined;
166
+ }
167
+
168
+ export function seekMatch(
169
+ lines: string[],
170
+ pattern: string[],
171
+ start: number,
172
+ eof = false,
173
+ ): SeekHit | undefined {
174
+ if (pattern.length === 0) {
175
+ return undefined;
176
+ }
177
+
178
+ for (const comparator of autoRescueComparatorEntries) {
179
+ const hit = tryMatch(lines, pattern, start, comparator, eof);
180
+ if (hit) {
181
+ return hit;
182
+ }
183
+ }
184
+
185
+ return undefined;
186
+ }
187
+
188
+ export function seek(
189
+ lines: string[],
190
+ pattern: string[],
191
+ start: number,
192
+ eof = false,
193
+ ): number {
194
+ return seekMatch(lines, pattern, start, eof)?.index ?? -1;
195
+ }
196
+
197
+ export function list(
198
+ lines: string[],
199
+ pattern: string[],
200
+ start: number,
201
+ same: LineComparator,
202
+ ): number[] {
203
+ if (pattern.length === 0) {
204
+ return [];
205
+ }
206
+
207
+ const out: number[] = [];
208
+
209
+ for (let index = start; index <= lines.length - pattern.length; index += 1) {
210
+ let ok = true;
211
+
212
+ for (let inner = 0; inner < pattern.length; inner += 1) {
213
+ if (!same(lines[index + inner], pattern[inner])) {
214
+ ok = false;
215
+ break;
216
+ }
217
+ }
218
+
219
+ if (ok) {
220
+ out.push(index);
221
+ }
222
+ }
223
+
224
+ return out;
225
+ }
226
+
227
+ function lowerBound(values: number[], target: number): number {
228
+ let low = 0;
229
+ let high = values.length;
230
+
231
+ while (low < high) {
232
+ const middle = Math.floor((low + high) / 2);
233
+ if (values[middle] < target) {
234
+ low = middle + 1;
235
+ continue;
236
+ }
237
+
238
+ high = middle;
239
+ }
240
+
241
+ return low;
242
+ }
243
+
244
+ export function sameRescueLine(a: string, b: string): boolean {
245
+ return equalExact(a, b) || equalUnicodeExact(a, b);
246
+ }
247
+
248
+ export function prefix(old_lines: string[], new_lines: string[]): number {
249
+ let index = 0;
250
+
251
+ while (
252
+ index < old_lines.length &&
253
+ index < new_lines.length &&
254
+ sameRescueLine(old_lines[index], new_lines[index])
255
+ ) {
256
+ index += 1;
257
+ }
258
+
259
+ return index;
260
+ }
261
+
262
+ export function suffix(
263
+ old_lines: string[],
264
+ new_lines: string[],
265
+ prefixLength: number,
266
+ ): number {
267
+ let index = 0;
268
+
269
+ while (
270
+ old_lines.length - index - 1 >= prefixLength &&
271
+ new_lines.length - index - 1 >= prefixLength &&
272
+ sameRescueLine(
273
+ old_lines[old_lines.length - index - 1],
274
+ new_lines[new_lines.length - index - 1],
275
+ )
276
+ ) {
277
+ index += 1;
278
+ }
279
+
280
+ return index;
281
+ }
282
+
283
+ export function rescueByPrefixSuffix(
284
+ lines: string[],
285
+ old_lines: string[],
286
+ new_lines: string[],
287
+ start: number,
288
+ ): RescueResult {
289
+ const prefixLength = prefix(old_lines, new_lines);
290
+ const suffixLength = suffix(old_lines, new_lines, prefixLength);
291
+
292
+ if (prefixLength === 0 || suffixLength === 0) {
293
+ return { kind: 'miss' };
294
+ }
295
+
296
+ const left = old_lines.slice(0, prefixLength);
297
+ const right = old_lines.slice(old_lines.length - suffixLength);
298
+ const middle = new_lines.slice(prefixLength, new_lines.length - suffixLength);
299
+
300
+ if (left.length === 1 && right.length === 1) {
301
+ const { leftHits, rightHits } = collectOneLinePrefixSuffixHits(
302
+ lines,
303
+ left[0],
304
+ right[0],
305
+ start,
306
+ );
307
+
308
+ return resolvePrefixSuffixHits(leftHits, rightHits, left.length, middle);
309
+ }
310
+
311
+ const hits = new Set<string>();
312
+ let hit: MatchHit | undefined;
313
+
314
+ for (const same of autoRescueComparators) {
315
+ const leftHits = list(lines, left, start, same);
316
+ if (leftHits.length === 0) {
317
+ continue;
318
+ }
319
+
320
+ const rightHits = list(lines, right, leftHits[0] + left.length, same);
321
+ if (rightHits.length === 0) {
322
+ continue;
323
+ }
324
+
325
+ for (const leftIndex of leftHits) {
326
+ const from = leftIndex + left.length;
327
+
328
+ for (
329
+ let index = lowerBound(rightHits, from);
330
+ index < rightHits.length;
331
+ index += 1
332
+ ) {
333
+ const rightIndex = rightHits[index];
334
+ const key = `${from}:${rightIndex}`;
335
+ if (!hits.has(key)) {
336
+ hits.add(key);
337
+ hit = {
338
+ start: from,
339
+ del: rightIndex - from,
340
+ add: [...middle],
341
+ };
342
+ }
343
+
344
+ if (hits.size > 1) {
345
+ return { kind: 'ambiguous', phase: 'prefix_suffix' };
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ if (!hit) {
352
+ return { kind: 'miss' };
353
+ }
354
+
355
+ return { kind: 'match', hit };
356
+ }
357
+
358
+ function collectOneLinePrefixSuffixHits(
359
+ lines: string[],
360
+ left: string,
361
+ right: string,
362
+ start: number,
363
+ ): { leftHits: number[]; rightHits: number[] } {
364
+ const leftTarget = prepareAutoRescueTarget(left);
365
+ const rightTarget = prepareAutoRescueTarget(right);
366
+ const leftHits: number[] = [];
367
+ const rightHits: number[] = [];
368
+
369
+ // The one-line prefix/suffix fast path intentionally compares at the
370
+ // broadest safe automatic level. This preserves exact/unicode/trim-end
371
+ // behavior while avoiding multiple full scans for the common one-line edge
372
+ // case. Full-trim remains excluded from automatic rescue.
373
+ for (let index = start; index < lines.length; index += 1) {
374
+ const line = prepareAutoRescueTarget(lines[index]);
375
+
376
+ if (line.unicodeTrimEnd === leftTarget.unicodeTrimEnd) {
377
+ leftHits.push(index);
378
+ }
379
+
380
+ if (index > start && line.unicodeTrimEnd === rightTarget.unicodeTrimEnd) {
381
+ rightHits.push(index);
382
+ }
383
+ }
384
+
385
+ return { leftHits, rightHits };
386
+ }
387
+
388
+ function resolvePrefixSuffixHits(
389
+ leftHits: number[],
390
+ rightHits: number[],
391
+ leftLength: number,
392
+ middle: string[],
393
+ ): RescueResult {
394
+ if (leftHits.length === 0 || rightHits.length === 0) {
395
+ return { kind: 'miss' };
396
+ }
397
+
398
+ const hits = new Set<string>();
399
+ let hit: MatchHit | undefined;
400
+
401
+ for (const leftIndex of leftHits) {
402
+ const from = leftIndex + leftLength;
403
+
404
+ for (
405
+ let index = lowerBound(rightHits, from);
406
+ index < rightHits.length;
407
+ index += 1
408
+ ) {
409
+ const rightIndex = rightHits[index];
410
+ const key = `${from}:${rightIndex}`;
411
+ if (!hits.has(key)) {
412
+ hits.add(key);
413
+ hit = {
414
+ start: from,
415
+ del: rightIndex - from,
416
+ add: [...middle],
417
+ };
418
+ }
419
+
420
+ if (hits.size > 1) {
421
+ return { kind: 'ambiguous', phase: 'prefix_suffix' };
422
+ }
423
+ }
424
+ }
425
+
426
+ if (!hit) {
427
+ return { kind: 'miss' };
428
+ }
429
+
430
+ return { kind: 'match', hit };
431
+ }
432
+
433
+ export function score(a: string[], b: string[]): number {
434
+ const normalizedA = a.map(normalizeLcsLine);
435
+ const normalizedB = b.map(normalizeLcsLine);
436
+ let previous = Array<number>(b.length + 1).fill(0);
437
+
438
+ for (let i = 1; i <= a.length; i += 1) {
439
+ const current = Array<number>(b.length + 1).fill(0);
440
+
441
+ for (let j = 1; j <= b.length; j += 1) {
442
+ current[j] =
443
+ normalizedA[i - 1] === normalizedB[j - 1]
444
+ ? previous[j - 1] + 1
445
+ : Math.max(previous[j], current[j - 1]);
446
+ }
447
+
448
+ previous = current;
449
+ }
450
+
451
+ return previous[b.length];
452
+ }
453
+
454
+ function normalizeLcsLine(line: string): string {
455
+ return normalizeUnicode(line).trim();
456
+ }
457
+
458
+ function countLcsUpperBound(a: string[], b: string[]): number {
459
+ const counts = new Map<string, number>();
460
+
461
+ for (const line of a) {
462
+ const key = normalizeLcsLine(line);
463
+ counts.set(key, (counts.get(key) ?? 0) + 1);
464
+ }
465
+
466
+ let shared = 0;
467
+ for (const line of b) {
468
+ const key = normalizeLcsLine(line);
469
+ const available = counts.get(key) ?? 0;
470
+ if (available === 0) {
471
+ continue;
472
+ }
473
+
474
+ shared += 1;
475
+ if (available === 1) {
476
+ counts.delete(key);
477
+ continue;
478
+ }
479
+
480
+ counts.set(key, available - 1);
481
+ }
482
+
483
+ return shared;
484
+ }
485
+
486
+ function collectBorderAnchoredStarts(
487
+ lines: string[],
488
+ oldLines: string[],
489
+ start: number,
490
+ ): number[] {
491
+ if (oldLines.length === 0) {
492
+ return [];
493
+ }
494
+
495
+ const candidates: number[] = [];
496
+ const firstLine = prepareAutoRescueTarget(oldLines[0]);
497
+ const lastLine = prepareAutoRescueTarget(oldLines[oldLines.length - 1]);
498
+
499
+ // LCS keeps its current scoring, but only competes across windows whose
500
+ // edges pass safe comparators. Ignoring full-trim here prevents automatic
501
+ // rescue from changing indentation depth in format-sensitive files.
502
+ const lastOffset = oldLines.length - 1;
503
+ const maxStart = lines.length - oldLines.length;
504
+
505
+ for (let index = start; index <= maxStart; index += 1) {
506
+ const end = index + lastOffset;
507
+
508
+ if (
509
+ matchPreparedAutoRescueComparator(lines[index], firstLine) === undefined
510
+ ) {
511
+ continue;
512
+ }
513
+
514
+ if (
515
+ oldLines.length === 1 ||
516
+ matchPreparedAutoRescueComparator(lines[end], lastLine) !== undefined
517
+ ) {
518
+ candidates.push(index);
519
+ }
520
+ }
521
+
522
+ return candidates;
523
+ }
524
+
525
+ export function rescueByLcs(
526
+ lines: string[],
527
+ old_lines: string[],
528
+ new_lines: string[],
529
+ start: number,
530
+ ): RescueResult {
531
+ if (old_lines.length === 0 || lines.length === 0) {
532
+ return { kind: 'miss' };
533
+ }
534
+
535
+ if (old_lines.length > MAX_LCS_CHUNK_LINES) {
536
+ return { kind: 'miss' };
537
+ }
538
+
539
+ const needed =
540
+ old_lines.length <= 2
541
+ ? old_lines.length
542
+ : Math.max(2, Math.ceil(old_lines.length * 0.7));
543
+ const candidates = collectBorderAnchoredStarts(lines, old_lines, start);
544
+
545
+ if (candidates.length === 0 || candidates.length > MAX_LCS_CANDIDATES) {
546
+ return { kind: 'miss' };
547
+ }
548
+
549
+ let best: MatchHit | undefined;
550
+ let bestScore = 0;
551
+ let ties = 0;
552
+
553
+ for (const index of candidates) {
554
+ const window = lines.slice(index, index + old_lines.length);
555
+ if (countLcsUpperBound(old_lines, window) < needed) {
556
+ continue;
557
+ }
558
+
559
+ const current = score(old_lines, window);
560
+
561
+ if (current > bestScore) {
562
+ bestScore = current;
563
+ ties = 1;
564
+ best = {
565
+ start: index,
566
+ del: old_lines.length,
567
+ add: [...new_lines],
568
+ };
569
+ continue;
570
+ }
571
+
572
+ if (current === bestScore && current > 0) {
573
+ ties += 1;
574
+ }
575
+ }
576
+
577
+ if (!best || bestScore < needed) {
578
+ return { kind: 'miss' };
579
+ }
580
+
581
+ if (ties > 1) {
582
+ return { kind: 'ambiguous', phase: 'lcs' };
583
+ }
584
+
585
+ return { kind: 'match', hit: best };
586
+ }