@adeu/core 1.6.2 → 1.6.4

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.
package/src/diff.test.ts CHANGED
@@ -1,62 +1,62 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { trim_common_context, generate_edits_from_text } from './diff.js';
3
-
4
- describe('Diff Logic & Context Trimming', () => {
5
- it('handles basic prefix and suffix', () => {
6
- const t = 'Context A Context';
7
- const n = 'Context B Context';
8
- const [p, s] = trim_common_context(t, n);
9
- expect(p).toBe(8); // "Context "
10
- expect(s).toBe(8); // " Context"
11
- });
12
-
13
- it('handles prefix only', () => {
14
- const t = 'Hello World';
15
- const n = 'Hello User';
16
- const [p, s] = trim_common_context(t, n);
17
- expect(p).toBe(6); // "Hello "
18
- expect(s).toBe(0);
19
- });
20
-
21
- it('handles suffix only', () => {
22
- const t = 'Old Item';
23
- const n = 'New Item';
24
- const [p, s] = trim_common_context(t, n);
25
- expect(p).toBe(0);
26
- expect(s).toBe(5); // " Item"
27
- });
28
-
29
- it('handles morph to insert (no suffix overlap)', () => {
30
- const t = 'Prefix';
31
- const n = 'Prefix Added';
32
- const [p, s] = trim_common_context(t, n);
33
- expect(p).toBe(6);
34
- expect(s).toBe(0);
35
- });
36
-
37
- it('prevents full suffix overlap crash (IndexError repro)', () => {
38
- const target = 'Agreement';
39
- const new_val = 'New Agreement';
40
- const [p, s] = trim_common_context(target, new_val);
41
- expect(p).toBe(0);
42
- expect(s).toBe(9); // "Agreement"
43
- });
44
-
45
- it('fixes start-of-doc insertion duplication bug', () => {
46
- const original = 'Contract Agreement';
47
- const modified = 'Big Contract Agreement';
48
-
49
- const edits = generate_edits_from_text(original, modified);
50
-
51
- // We want exactly 1 semantic edit to represent this change.
52
- expect(edits.length).toBe(1);
53
-
54
- const edit = edits[0];
55
- if (edit.target_text === '') {
56
- expect(edit.new_text.trim()).toBe('Big');
57
- } else {
58
- expect(edit.target_text).toContain('Contract');
59
- expect(edit.new_text).toContain('Big');
60
- }
61
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import { trim_common_context, generate_edits_from_text } from './diff.js';
3
+
4
+ describe('Diff Logic & Context Trimming', () => {
5
+ it('handles basic prefix and suffix', () => {
6
+ const t = 'Context A Context';
7
+ const n = 'Context B Context';
8
+ const [p, s] = trim_common_context(t, n);
9
+ expect(p).toBe(8); // "Context "
10
+ expect(s).toBe(8); // " Context"
11
+ });
12
+
13
+ it('handles prefix only', () => {
14
+ const t = 'Hello World';
15
+ const n = 'Hello User';
16
+ const [p, s] = trim_common_context(t, n);
17
+ expect(p).toBe(6); // "Hello "
18
+ expect(s).toBe(0);
19
+ });
20
+
21
+ it('handles suffix only', () => {
22
+ const t = 'Old Item';
23
+ const n = 'New Item';
24
+ const [p, s] = trim_common_context(t, n);
25
+ expect(p).toBe(0);
26
+ expect(s).toBe(5); // " Item"
27
+ });
28
+
29
+ it('handles morph to insert (no suffix overlap)', () => {
30
+ const t = 'Prefix';
31
+ const n = 'Prefix Added';
32
+ const [p, s] = trim_common_context(t, n);
33
+ expect(p).toBe(6);
34
+ expect(s).toBe(0);
35
+ });
36
+
37
+ it('prevents full suffix overlap crash (IndexError repro)', () => {
38
+ const target = 'Agreement';
39
+ const new_val = 'New Agreement';
40
+ const [p, s] = trim_common_context(target, new_val);
41
+ expect(p).toBe(0);
42
+ expect(s).toBe(9); // "Agreement"
43
+ });
44
+
45
+ it('fixes start-of-doc insertion duplication bug', () => {
46
+ const original = 'Contract Agreement';
47
+ const modified = 'Big Contract Agreement';
48
+
49
+ const edits = generate_edits_from_text(original, modified);
50
+
51
+ // We want exactly 1 semantic edit to represent this change.
52
+ expect(edits.length).toBe(1);
53
+
54
+ const edit = edits[0];
55
+ if (edit.target_text === '') {
56
+ expect(edit.new_text.trim()).toBe('Big');
57
+ } else {
58
+ expect(edit.target_text).toContain('Contract');
59
+ expect(edit.new_text).toContain('Big');
60
+ }
61
+ });
62
62
  });
package/src/diff.ts CHANGED
@@ -1,251 +1,251 @@
1
- import diff_match_patch from 'diff-match-patch';
2
- import { ModifyText } from './models.js';
3
-
4
- export function trim_common_context(target: string, new_val: string): [number, number] {
5
- if (!target || !new_val) return [0, 0];
6
-
7
- const isSpace = (char: string) => /\s/.test(char);
8
-
9
- // 1. Prefix with Word Boundary Check
10
- let prefix_len = 0;
11
- let limit = Math.min(target.length, new_val.length);
12
- while (prefix_len < limit && target[prefix_len] === new_val[prefix_len]) {
13
- prefix_len++;
14
- }
15
-
16
- // Backtrack to nearest whitespace if we split a word
17
- if (prefix_len < target.length && prefix_len < new_val.length) {
18
- while (prefix_len > 0) {
19
- const target_split = !isSpace(target[prefix_len - 1]) && !isSpace(target[prefix_len]);
20
- const new_split = !isSpace(new_val[prefix_len - 1]) && !isSpace(new_val[prefix_len]);
21
- if (target_split || new_split) {
22
- prefix_len--;
23
- } else {
24
- break;
25
- }
26
- }
27
- }
28
-
29
- // Backtrack prefix to avoid splitting markdown markers
30
- while (prefix_len > 0) {
31
- if (prefix_len < target.length) {
32
- const charSeq = target.substring(prefix_len - 1, prefix_len + 1);
33
- if (charSeq === '**' || charSeq === '__') {
34
- prefix_len--;
35
- continue;
36
- }
37
- }
38
-
39
- const left = target.substring(0, prefix_len);
40
- const b_count = (left.match(/\*\*/g) || []).length;
41
- const u2_count = (left.match(/__/g) || []).length;
42
- const u1_count = (left.replace(/__/g, '').match(/_/g) || []).length;
43
-
44
- if (b_count % 2 !== 0) {
45
- prefix_len = left.lastIndexOf('**');
46
- continue;
47
- }
48
- if (u2_count % 2 !== 0) {
49
- prefix_len = left.lastIndexOf('__');
50
- continue;
51
- }
52
- if (u1_count % 2 !== 0) {
53
- let idx = left.length - 1;
54
- while (idx >= 0) {
55
- if (left[idx] === '_' &&
56
- (idx === 0 || left[idx - 1] !== '_') &&
57
- (idx === left.length - 1 || left[idx + 1] !== '_')) {
58
- prefix_len = idx;
59
- break;
60
- }
61
- idx--;
62
- }
63
- continue;
64
- }
65
-
66
- // Safety: Backtrack if we consumed a Markdown Header marker (#)
67
- let temp_len = prefix_len;
68
- let hit_header = false;
69
- while (temp_len > 0) {
70
- const char = target[temp_len - 1];
71
- if (char === '#') {
72
- prefix_len = temp_len - 1;
73
- while (prefix_len > 0 && target[prefix_len - 1] !== '\n') {
74
- prefix_len--;
75
- }
76
- hit_header = true;
77
- break;
78
- }
79
- if (char === '\n') break;
80
- temp_len--;
81
- }
82
- if (hit_header) continue;
83
-
84
- break;
85
- }
86
-
87
- // 2. Suffix with Word Boundary Check
88
- let suffix_len = 0;
89
- const target_rem_len = target.length - prefix_len;
90
- const new_rem_len = new_val.length - prefix_len;
91
- const limit_suffix = Math.min(target_rem_len, new_rem_len);
92
-
93
- while (suffix_len < limit_suffix && target[target.length - 1 - suffix_len] === new_val[new_val.length - 1 - suffix_len]) {
94
- suffix_len++;
95
- }
96
-
97
- if (suffix_len > 0) {
98
- while (suffix_len > 0) {
99
- let target_split = false;
100
- if (suffix_len < target.length) {
101
- target_split = !isSpace(target[target.length - 1 - suffix_len]) && !isSpace(target[target.length - suffix_len]);
102
- }
103
- let new_split = false;
104
- if (suffix_len < new_val.length) {
105
- new_split = !isSpace(new_val[new_val.length - 1 - suffix_len]) && !isSpace(new_val[new_val.length - suffix_len]);
106
- }
107
- if (target_split || new_split) {
108
- suffix_len--;
109
- } else {
110
- break;
111
- }
112
- }
113
- }
114
-
115
- while (suffix_len > 0) {
116
- const idx = target.length - suffix_len;
117
- if (idx > 0) {
118
- const charSeq = target.substring(idx - 1, idx + 1);
119
- if (charSeq === '**' || charSeq === '__') {
120
- suffix_len--;
121
- continue;
122
- }
123
- }
124
-
125
- const right = target.substring(target.length - suffix_len);
126
- const b_count = (right.match(/\*\*/g) || []).length;
127
- const u2_count = (right.match(/__/g) || []).length;
128
- const u1_count = (right.replace(/__/g, '').match(/_/g) || []).length;
129
-
130
- if (b_count % 2 !== 0) {
131
- suffix_len -= right.indexOf('**') + 2;
132
- continue;
133
- }
134
- if (u2_count % 2 !== 0) {
135
- suffix_len -= right.indexOf('__') + 2;
136
- continue;
137
- }
138
- if (u1_count % 2 !== 0) {
139
- let idx_in_right = 0;
140
- while (idx_in_right < right.length) {
141
- if (right[idx_in_right] === '_' &&
142
- (idx_in_right === 0 || right[idx_in_right - 1] !== '_') &&
143
- (idx_in_right === right.length - 1 || right[idx_in_right + 1] !== '_')) {
144
- suffix_len -= idx_in_right + 1;
145
- break;
146
- }
147
- idx_in_right++;
148
- }
149
- continue;
150
- }
151
- break;
152
- }
153
-
154
- if (suffix_len > 0 && /^\s+$/.test(target.substring(target.length - suffix_len))) {
155
- suffix_len = 0;
156
- }
157
-
158
- // Absorb balanced wrappers
159
- for (const marker of ['**', '__', '_']) {
160
- const mlen = marker.length;
161
- const tgt_rem = target.substring(prefix_len, target.length - suffix_len);
162
- const new_rem = new_val.substring(prefix_len, new_val.length - suffix_len);
163
-
164
- if (
165
- tgt_rem.startsWith(marker) && new_rem.startsWith(marker) &&
166
- tgt_rem.endsWith(marker) && new_rem.endsWith(marker) &&
167
- tgt_rem.length >= 2 * mlen && new_rem.length >= 2 * mlen
168
- ) {
169
- prefix_len += mlen;
170
- suffix_len += mlen;
171
- }
172
- }
173
-
174
- return [prefix_len, suffix_len];
175
- }
176
-
177
- function _words_to_chars(text1: string, text2: string): [string, string, string[]] {
178
- const token_array: string[] = [];
179
- const token_hash: Record<string, number> = {};
180
-
181
- // RegExp equivalent to Python's r"(\s+|\w+|[^\w\s])" with unicode support
182
- const split_pattern = /(\s+|[\p{L}\p{N}_]+|[^\p{L}\p{N}_\s])/gu;
183
-
184
- const encode_text = (text: string) => {
185
- // Keep delimiters via capture group in split
186
- const tokens = text.split(split_pattern).filter(Boolean);
187
- let encoded_chars = '';
188
- for (const token of tokens) {
189
- if (token in token_hash) {
190
- encoded_chars += String.fromCharCode(token_hash[token]);
191
- } else {
192
- const code = token_array.length;
193
- token_hash[token] = code;
194
- token_array.push(token);
195
- encoded_chars += String.fromCharCode(code);
196
- }
197
- }
198
- return encoded_chars;
199
- };
200
-
201
- return [encode_text(text1), encode_text(text2), token_array];
202
- }
203
-
204
- export function generate_edits_from_text(original_text: string, modified_text: string): ModifyText[] {
205
- const dmp = new diff_match_patch.diff_match_patch();
206
-
207
- const [chars1, chars2, token_array] = _words_to_chars(original_text, modified_text);
208
- const diffs = dmp.diff_main(chars1, chars2, false);
209
- dmp.diff_cleanupSemantic(diffs);
210
-
211
- // Manually map characters back to words to bypass prototype volatility (diff_charsToLines_)
212
- for (let i = 0; i < diffs.length; i++) {
213
- const chars = diffs[i][1];
214
- let text = '';
215
- for (let j = 0; j < chars.length; j++) text += token_array[chars.charCodeAt(j)];
216
- diffs[i][1] = text;
217
- }
218
-
219
- const edits: ModifyText[] = [];
220
- let current_original_index = 0;
221
- let pending_delete: [number, string] | null = null;
222
-
223
- for (const [op, text] of diffs) {
224
- if (op === 0) { // Equal
225
- if (pending_delete) {
226
- const [idx, del_txt] = pending_delete;
227
- edits.push({ type: 'modify', target_text: del_txt, new_text: '', comment: 'Diff: Text deleted', _match_start_index: idx });
228
- pending_delete = null;
229
- }
230
- current_original_index += text.length;
231
- } else if (op === -1) { // Delete
232
- pending_delete = [current_original_index, text];
233
- current_original_index += text.length;
234
- } else if (op === 1) { // Insert
235
- if (pending_delete) {
236
- const [idx, del_txt] = pending_delete;
237
- edits.push({ type: 'modify', target_text: del_txt, new_text: text, comment: 'Diff: Replacement', _match_start_index: idx });
238
- pending_delete = null;
239
- } else {
240
- edits.push({ type: 'modify', target_text: '', new_text: text, comment: 'Diff: Text inserted', _match_start_index: current_original_index });
241
- }
242
- }
243
- }
244
-
245
- if (pending_delete) {
246
- const [idx, del_txt] = pending_delete;
247
- edits.push({ type: 'modify', target_text: del_txt, new_text: '', comment: 'Diff: Text deleted', _match_start_index: idx });
248
- }
249
-
250
- return edits;
1
+ import diff_match_patch from 'diff-match-patch';
2
+ import { ModifyText } from './models.js';
3
+
4
+ export function trim_common_context(target: string, new_val: string): [number, number] {
5
+ if (!target || !new_val) return [0, 0];
6
+
7
+ const isSpace = (char: string) => /\s/.test(char);
8
+
9
+ // 1. Prefix with Word Boundary Check
10
+ let prefix_len = 0;
11
+ let limit = Math.min(target.length, new_val.length);
12
+ while (prefix_len < limit && target[prefix_len] === new_val[prefix_len]) {
13
+ prefix_len++;
14
+ }
15
+
16
+ // Backtrack to nearest whitespace if we split a word
17
+ if (prefix_len < target.length && prefix_len < new_val.length) {
18
+ while (prefix_len > 0) {
19
+ const target_split = !isSpace(target[prefix_len - 1]) && !isSpace(target[prefix_len]);
20
+ const new_split = !isSpace(new_val[prefix_len - 1]) && !isSpace(new_val[prefix_len]);
21
+ if (target_split || new_split) {
22
+ prefix_len--;
23
+ } else {
24
+ break;
25
+ }
26
+ }
27
+ }
28
+
29
+ // Backtrack prefix to avoid splitting markdown markers
30
+ while (prefix_len > 0) {
31
+ if (prefix_len < target.length) {
32
+ const charSeq = target.substring(prefix_len - 1, prefix_len + 1);
33
+ if (charSeq === '**' || charSeq === '__') {
34
+ prefix_len--;
35
+ continue;
36
+ }
37
+ }
38
+
39
+ const left = target.substring(0, prefix_len);
40
+ const b_count = (left.match(/\*\*/g) || []).length;
41
+ const u2_count = (left.match(/__/g) || []).length;
42
+ const u1_count = (left.replace(/__/g, '').match(/_/g) || []).length;
43
+
44
+ if (b_count % 2 !== 0) {
45
+ prefix_len = left.lastIndexOf('**');
46
+ continue;
47
+ }
48
+ if (u2_count % 2 !== 0) {
49
+ prefix_len = left.lastIndexOf('__');
50
+ continue;
51
+ }
52
+ if (u1_count % 2 !== 0) {
53
+ let idx = left.length - 1;
54
+ while (idx >= 0) {
55
+ if (left[idx] === '_' &&
56
+ (idx === 0 || left[idx - 1] !== '_') &&
57
+ (idx === left.length - 1 || left[idx + 1] !== '_')) {
58
+ prefix_len = idx;
59
+ break;
60
+ }
61
+ idx--;
62
+ }
63
+ continue;
64
+ }
65
+
66
+ // Safety: Backtrack if we consumed a Markdown Header marker (#)
67
+ let temp_len = prefix_len;
68
+ let hit_header = false;
69
+ while (temp_len > 0) {
70
+ const char = target[temp_len - 1];
71
+ if (char === '#') {
72
+ prefix_len = temp_len - 1;
73
+ while (prefix_len > 0 && target[prefix_len - 1] !== '\n') {
74
+ prefix_len--;
75
+ }
76
+ hit_header = true;
77
+ break;
78
+ }
79
+ if (char === '\n') break;
80
+ temp_len--;
81
+ }
82
+ if (hit_header) continue;
83
+
84
+ break;
85
+ }
86
+
87
+ // 2. Suffix with Word Boundary Check
88
+ let suffix_len = 0;
89
+ const target_rem_len = target.length - prefix_len;
90
+ const new_rem_len = new_val.length - prefix_len;
91
+ const limit_suffix = Math.min(target_rem_len, new_rem_len);
92
+
93
+ while (suffix_len < limit_suffix && target[target.length - 1 - suffix_len] === new_val[new_val.length - 1 - suffix_len]) {
94
+ suffix_len++;
95
+ }
96
+
97
+ if (suffix_len > 0) {
98
+ while (suffix_len > 0) {
99
+ let target_split = false;
100
+ if (suffix_len < target.length) {
101
+ target_split = !isSpace(target[target.length - 1 - suffix_len]) && !isSpace(target[target.length - suffix_len]);
102
+ }
103
+ let new_split = false;
104
+ if (suffix_len < new_val.length) {
105
+ new_split = !isSpace(new_val[new_val.length - 1 - suffix_len]) && !isSpace(new_val[new_val.length - suffix_len]);
106
+ }
107
+ if (target_split || new_split) {
108
+ suffix_len--;
109
+ } else {
110
+ break;
111
+ }
112
+ }
113
+ }
114
+
115
+ while (suffix_len > 0) {
116
+ const idx = target.length - suffix_len;
117
+ if (idx > 0) {
118
+ const charSeq = target.substring(idx - 1, idx + 1);
119
+ if (charSeq === '**' || charSeq === '__') {
120
+ suffix_len--;
121
+ continue;
122
+ }
123
+ }
124
+
125
+ const right = target.substring(target.length - suffix_len);
126
+ const b_count = (right.match(/\*\*/g) || []).length;
127
+ const u2_count = (right.match(/__/g) || []).length;
128
+ const u1_count = (right.replace(/__/g, '').match(/_/g) || []).length;
129
+
130
+ if (b_count % 2 !== 0) {
131
+ suffix_len -= right.indexOf('**') + 2;
132
+ continue;
133
+ }
134
+ if (u2_count % 2 !== 0) {
135
+ suffix_len -= right.indexOf('__') + 2;
136
+ continue;
137
+ }
138
+ if (u1_count % 2 !== 0) {
139
+ let idx_in_right = 0;
140
+ while (idx_in_right < right.length) {
141
+ if (right[idx_in_right] === '_' &&
142
+ (idx_in_right === 0 || right[idx_in_right - 1] !== '_') &&
143
+ (idx_in_right === right.length - 1 || right[idx_in_right + 1] !== '_')) {
144
+ suffix_len -= idx_in_right + 1;
145
+ break;
146
+ }
147
+ idx_in_right++;
148
+ }
149
+ continue;
150
+ }
151
+ break;
152
+ }
153
+
154
+ if (suffix_len > 0 && /^\s+$/.test(target.substring(target.length - suffix_len))) {
155
+ suffix_len = 0;
156
+ }
157
+
158
+ // Absorb balanced wrappers
159
+ for (const marker of ['**', '__', '_']) {
160
+ const mlen = marker.length;
161
+ const tgt_rem = target.substring(prefix_len, target.length - suffix_len);
162
+ const new_rem = new_val.substring(prefix_len, new_val.length - suffix_len);
163
+
164
+ if (
165
+ tgt_rem.startsWith(marker) && new_rem.startsWith(marker) &&
166
+ tgt_rem.endsWith(marker) && new_rem.endsWith(marker) &&
167
+ tgt_rem.length >= 2 * mlen && new_rem.length >= 2 * mlen
168
+ ) {
169
+ prefix_len += mlen;
170
+ suffix_len += mlen;
171
+ }
172
+ }
173
+
174
+ return [prefix_len, suffix_len];
175
+ }
176
+
177
+ function _words_to_chars(text1: string, text2: string): [string, string, string[]] {
178
+ const token_array: string[] = [];
179
+ const token_hash: Record<string, number> = {};
180
+
181
+ // RegExp equivalent to Python's r"(\s+|\w+|[^\w\s])" with unicode support
182
+ const split_pattern = /(\s+|[\p{L}\p{N}_]+|[^\p{L}\p{N}_\s])/gu;
183
+
184
+ const encode_text = (text: string) => {
185
+ // Keep delimiters via capture group in split
186
+ const tokens = text.split(split_pattern).filter(Boolean);
187
+ let encoded_chars = '';
188
+ for (const token of tokens) {
189
+ if (token in token_hash) {
190
+ encoded_chars += String.fromCharCode(token_hash[token]);
191
+ } else {
192
+ const code = token_array.length;
193
+ token_hash[token] = code;
194
+ token_array.push(token);
195
+ encoded_chars += String.fromCharCode(code);
196
+ }
197
+ }
198
+ return encoded_chars;
199
+ };
200
+
201
+ return [encode_text(text1), encode_text(text2), token_array];
202
+ }
203
+
204
+ export function generate_edits_from_text(original_text: string, modified_text: string): ModifyText[] {
205
+ const dmp = new diff_match_patch.diff_match_patch();
206
+
207
+ const [chars1, chars2, token_array] = _words_to_chars(original_text, modified_text);
208
+ const diffs = dmp.diff_main(chars1, chars2, false);
209
+ dmp.diff_cleanupSemantic(diffs);
210
+
211
+ // Manually map characters back to words to bypass prototype volatility (diff_charsToLines_)
212
+ for (let i = 0; i < diffs.length; i++) {
213
+ const chars = diffs[i][1];
214
+ let text = '';
215
+ for (let j = 0; j < chars.length; j++) text += token_array[chars.charCodeAt(j)];
216
+ diffs[i][1] = text;
217
+ }
218
+
219
+ const edits: ModifyText[] = [];
220
+ let current_original_index = 0;
221
+ let pending_delete: [number, string] | null = null;
222
+
223
+ for (const [op, text] of diffs) {
224
+ if (op === 0) { // Equal
225
+ if (pending_delete) {
226
+ const [idx, del_txt] = pending_delete;
227
+ edits.push({ type: 'modify', target_text: del_txt, new_text: '', comment: 'Diff: Text deleted', _match_start_index: idx });
228
+ pending_delete = null;
229
+ }
230
+ current_original_index += text.length;
231
+ } else if (op === -1) { // Delete
232
+ pending_delete = [current_original_index, text];
233
+ current_original_index += text.length;
234
+ } else if (op === 1) { // Insert
235
+ if (pending_delete) {
236
+ const [idx, del_txt] = pending_delete;
237
+ edits.push({ type: 'modify', target_text: del_txt, new_text: text, comment: 'Diff: Replacement', _match_start_index: idx });
238
+ pending_delete = null;
239
+ } else {
240
+ edits.push({ type: 'modify', target_text: '', new_text: text, comment: 'Diff: Text inserted', _match_start_index: current_original_index });
241
+ }
242
+ }
243
+ }
244
+
245
+ if (pending_delete) {
246
+ const [idx, del_txt] = pending_delete;
247
+ edits.push({ type: 'modify', target_text: del_txt, new_text: '', comment: 'Diff: Text deleted', _match_start_index: idx });
248
+ }
249
+
250
+ return edits;
251
251
  }