dw-kit 1.9.0 → 1.9.2

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 (53) hide show
  1. package/.claude/agents/planner.md +100 -100
  2. package/.claude/agents/quality-checker.md +86 -86
  3. package/.claude/agents/researcher.md +93 -93
  4. package/.claude/agents/reviewer.md +126 -126
  5. package/.claude/hooks/supply-chain-scan.sh +0 -0
  6. package/.claude/rules/code-style.md +37 -37
  7. package/.claude/settings.json +2 -28
  8. package/.claude/skills/dw-kit-report/SKILL.md +38 -7
  9. package/.claude/skills/dw-plan/template-plan.md +47 -47
  10. package/.claude/skills/dw-research/template-research.md +51 -51
  11. package/.claude/skills/dw-review/checklist.md +88 -88
  12. package/.claude/skills/dw-thinking/THINKING.md +91 -91
  13. package/.claude/templates/agent-report.md +35 -35
  14. package/.claude/templates/en/task-context.md +77 -73
  15. package/.claude/templates/en/task-plan.md +83 -79
  16. package/.claude/templates/en/task-progress.md +69 -65
  17. package/.claude/templates/pr-template.md +56 -56
  18. package/.claude/templates/task-context.md +77 -73
  19. package/.claude/templates/task-plan.md +83 -79
  20. package/.claude/templates/task-progress.md +69 -65
  21. package/.dw/adapters/claude-cli/extensions/README.md +36 -36
  22. package/.dw/adapters/claude-cli/generated/README.md +23 -23
  23. package/.dw/adapters/claude-cli/overrides/README.md +37 -37
  24. package/.dw/adapters/generic/README.md +21 -21
  25. package/.dw/config/presets/enterprise.yml +52 -52
  26. package/.dw/config/presets/small-team.yml +39 -39
  27. package/.dw/config/presets/solo-quick.yml +37 -37
  28. package/.dw/core/AGENTS.md +53 -53
  29. package/.dw/core/QUALITY.md +220 -220
  30. package/.dw/core/THINKING.md +126 -126
  31. package/.dw/core/WORKFLOW.md +17 -12
  32. package/.dw/core/templates/v2/spec.md +2 -0
  33. package/.dw/core/templates/v2/tracking.md +2 -0
  34. package/.dw/core/templates/v3/task.md +15 -22
  35. package/.dw/core/templates/vi/task-context.md +96 -92
  36. package/.dw/core/templates/vi/task-plan.md +97 -93
  37. package/.dw/core/templates/vi/task-progress.md +60 -56
  38. package/LICENSE +201 -201
  39. package/NOTICE +26 -26
  40. package/README.md +1 -1
  41. package/bin/dw.mjs +28 -28
  42. package/package.json +1 -1
  43. package/src/commands/claude-vn-fix.mjs +267 -267
  44. package/src/commands/prompt.mjs +112 -112
  45. package/src/commands/validate.mjs +102 -102
  46. package/src/lib/clipboard.mjs +24 -24
  47. package/src/lib/goal-store.mjs +2 -14
  48. package/src/lib/platform.mjs +39 -39
  49. package/src/lib/prompt-suggest.mjs +84 -84
  50. package/src/lib/timeline-parser.mjs +54 -15
  51. package/src/lib/ui.mjs +66 -66
  52. package/src/lib/update-checker.mjs +73 -73
  53. package/.dw/security/advisory-snapshot.json +0 -157
@@ -1,267 +1,267 @@
1
- import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, statSync } from 'node:fs';
2
- import { join, dirname } from 'node:path';
3
- import os from 'node:os';
4
- import { execSync } from 'node:child_process';
5
- import { header, info, ok, warn, err, log } from '../lib/ui.mjs';
6
-
7
- export const PATCH_MARKER = '/* dw-kit Vietnamese IME fix */';
8
- export const DEL_CHAR = '\x7f';
9
-
10
- export async function claudeVnFixCommand(opts) {
11
- header('dw-kit Claude Vietnamese IME Fix');
12
-
13
- const filePath = opts.path ? opts.path : findCliJs();
14
- log(`Target file: ${filePath}`);
15
-
16
- if (!existsSync(filePath)) {
17
- err(`File not found: ${filePath}`);
18
- process.exit(1);
19
- }
20
-
21
- if (opts.restore) {
22
- info('Restoring from latest backup');
23
- const restored = restoreLatestBackup(filePath, { dryRun: !!opts.dryRun });
24
- if (!restored) process.exit(1);
25
- ok('Restore complete. Restart Claude CLI.');
26
- return;
27
- }
28
-
29
- info('Patching');
30
- const result = patchCliJs(filePath, { dryRun: !!opts.dryRun });
31
- if (!result.ok) {
32
- err(result.message);
33
- process.exit(1);
34
- }
35
- ok(result.message);
36
- log('Restart Claude CLI for changes to take effect.');
37
- warn('Note: This modifies a third-party installed file; you may need to re-run after Claude updates.');
38
- }
39
-
40
- export function findCliJs() {
41
- // Strategy: check global npm root first, then common cache locations.
42
- // This is intentionally conservative (no deep recursive scan of entire home).
43
- const candidates = [];
44
-
45
- // npm root -g
46
- const npmRoot = tryNpmRootGlobal();
47
- if (npmRoot) {
48
- candidates.push(join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'));
49
- }
50
-
51
- const home = os.homedir();
52
- if (process.platform === 'win32') {
53
- const appData = process.env.APPDATA || '';
54
- const localAppData = process.env.LOCALAPPDATA || '';
55
- if (appData) candidates.push(join(appData, 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'));
56
- if (localAppData) {
57
- const npxBase = join(localAppData, 'npm-cache', '_npx');
58
- const latest = findLatestNpxClaudeCli(npxBase);
59
- if (latest) candidates.push(latest);
60
- }
61
- } else {
62
- const npxLatest = findLatestNpxClaudeCli(join(home, '.npm', '_npx'));
63
- if (npxLatest) candidates.push(npxLatest);
64
- candidates.push('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js');
65
- candidates.push('/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js');
66
- }
67
-
68
- for (const p of candidates) {
69
- if (p && existsSync(p) && statSync(p).isFile()) return p;
70
- }
71
-
72
- throw new Error(
73
- 'Could not auto-detect @anthropic-ai/claude-code/cli.js.\n' +
74
- 'Provide it explicitly via: dw claude-vn-fix --path "<path-to-cli.js>"',
75
- );
76
- }
77
-
78
- function tryNpmRootGlobal() {
79
- try {
80
- const out = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8', timeout: 5000 });
81
- return out.trim();
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- function safeMtime(p) {
88
- try { return statSync(p).mtimeMs || 0; } catch { return 0; }
89
- }
90
-
91
- function findLatestNpxClaudeCli(npxBase) {
92
- try {
93
- if (!npxBase || !existsSync(npxBase)) return null;
94
- const entries = readdirSync(npxBase, { withFileTypes: true })
95
- .filter((e) => e.isDirectory())
96
- .map((e) => join(npxBase, e.name));
97
- const sorted = entries
98
- .map((d) => ({ d, t: safeMtime(d) }))
99
- .sort((a, b) => b.t - a.t)
100
- .map((x) => x.d);
101
-
102
- for (const dir of sorted.slice(0, 50)) {
103
- const p = join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
104
- if (existsSync(p)) return p;
105
- }
106
- return null;
107
- } catch {
108
- return null;
109
- }
110
- }
111
-
112
- export function patchCliJs(filePath, { dryRun }) {
113
- const content = readFileSync(filePath, 'utf-8');
114
-
115
- // Guard: ensure this is actually a Claude CLI bundle before patching.
116
- if (!content.includes('@anthropic-ai') || !content.includes('claude-code')) {
117
- return { ok: false, message: 'File does not appear to be a Claude CLI bundle. Use --path to specify the correct file.' };
118
- }
119
-
120
- if (content.includes(PATCH_MARKER)) {
121
- return { ok: true, message: 'Already patched (marker found).' };
122
- }
123
-
124
- const idx = findBugPatternIndex(content);
125
- if (idx === -1) {
126
- return { ok: false, message: 'Bug pattern not found (.includes("\\x7f")). Claude may already be fixed upstream.' };
127
- }
128
-
129
- const { start, end, block } = findIfBlock(content, idx);
130
- const vars = extractVariables(block);
131
- const fixCode = generateFix(vars);
132
- const patched = content.slice(0, start) + fixCode + content.slice(end);
133
-
134
- if (dryRun) {
135
- log('DRY RUN: would create backup and patch file.');
136
- log(`Detected vars: input=${vars.input}, state=${vars.state}, cur=${vars.curState}`);
137
- return { ok: true, message: 'Dry run OK.' };
138
- }
139
-
140
- const backupPath = createBackup(filePath);
141
- ok(`Backup created: ${backupPath}`);
142
- try {
143
- writeFileSync(filePath, patched, 'utf-8');
144
- const verify = readFileSync(filePath, 'utf-8');
145
- if (!verify.includes(PATCH_MARKER)) throw new Error('verify failed (marker missing after write)');
146
- return { ok: true, message: `Patch applied. Backup: ${backupPath}` };
147
- } catch (e) {
148
- warn(`Patch failed: ${e.message}`);
149
- warn('Rolling back from backup.');
150
- try {
151
- copyFileSync(backupPath, filePath);
152
- } catch (rollbackErr) {
153
- return { ok: false, message: `Patch failed AND rollback failed: ${rollbackErr.message}. Manual restore from: ${backupPath}` };
154
- }
155
- return { ok: false, message: `Patch failed and rolled back: ${e.message}` };
156
- }
157
- }
158
-
159
- function findBugPatternIndex(content) {
160
- // Claude builds may contain either:
161
- // - literal escape sequence: ".includes(\"\\x7f\")"
162
- // - actual DEL char 0x7f inside the string: ".includes(\"\")"
163
- const literal = content.indexOf('.includes("\\x7f")');
164
- if (literal !== -1) return literal;
165
- return content.indexOf(`.includes("${DEL_CHAR}")`);
166
- }
167
-
168
- function createBackup(filePath) {
169
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
170
- const backupPath = `${filePath}.backup-${ts}`;
171
- copyFileSync(filePath, backupPath);
172
- return backupPath;
173
- }
174
-
175
- export function restoreLatestBackup(filePath, { dryRun }) {
176
- const dir = dirname(filePath);
177
- const base = filePath.split(/[\\/]/).pop();
178
- const backups = readdirSync(dir)
179
- .filter((f) => f.startsWith(`${base}.backup-`))
180
- .map((f) => join(dir, f))
181
- .map((p) => ({ p, t: safeMtime(p) }))
182
- .sort((a, b) => b.t - a.t);
183
-
184
- const latest = backups[0]?.p;
185
- if (!latest) {
186
- err('No backups found to restore.');
187
- return false;
188
- }
189
-
190
- if (dryRun) {
191
- log(`DRY RUN: would restore from ${latest}`);
192
- return true;
193
- }
194
-
195
- copyFileSync(latest, filePath);
196
- ok(`Restored from: ${latest}`);
197
- return true;
198
- }
199
-
200
- function findIfBlock(content, idx) {
201
- // Search backward from idx within a 500-char window to find the nearest if(
202
- const windowStart = Math.max(0, idx - 500);
203
- const searchSlice = content.slice(windowStart, idx);
204
- const localOffset = searchSlice.lastIndexOf('if(');
205
- if (localOffset === -1) throw new Error(`Could not find containing if(...) block near index ${idx}`);
206
- const start = windowStart + localOffset;
207
-
208
- let depth = 0;
209
- let end = -1;
210
- for (let i = start; i < content.length; i++) {
211
- const c = content[i];
212
- if (c === '{') depth++;
213
- else if (c === '}') {
214
- depth--;
215
- if (depth === 0) { end = i + 1; break; }
216
- }
217
- }
218
- if (end === -1) throw new Error('Could not find end of if block (brace mismatch)');
219
- if (idx < start || idx > end) throw new Error('Bug pattern found outside expected if block');
220
- return { start, end, block: content.slice(start, end) };
221
- }
222
-
223
- function extractVariables(block) {
224
- // Normalize DEL char for regex matching
225
- const normalized = block.replaceAll(DEL_CHAR, '\\x7f');
226
-
227
- // Match: let COUNT=(INPUT.match(/\x7f/g)||[]).length,STATE=CURSTATE;
228
- const m = normalized.match(/let ([\w$]+)=\(\w+\.match\(\/\\x7f\/g\)\|\|\[\]\)\.length[,;]([\w$]+)=([\w$]+)[;,]/);
229
- if (!m) throw new Error('Could not extract variables (count/state/curState)');
230
- const state = m[2];
231
- const curState = m[3];
232
-
233
- const m2 = block.match(new RegExp(`([\\w$]+)\\(${escapeRegex(state)}\\.text\\);([\\w$]+)\\(${escapeRegex(state)}\\.offset\\)`));
234
- if (!m2) throw new Error('Could not extract update functions');
235
-
236
- const m3 = block.match(/([\w$]+)\.includes\("/);
237
- if (!m3) throw new Error('Could not extract input variable');
238
-
239
- return {
240
- input: m3[1],
241
- state,
242
- curState,
243
- updateText: m2[1],
244
- updateOffset: m2[2],
245
- };
246
- }
247
-
248
- function generateFix(v) {
249
- // This mirrors the known fix: backspace N times, then insert replacement text.
250
- return (
251
- `${PATCH_MARKER}` +
252
- `if(${v.input}.includes("\\x7f")){` +
253
- `let _n=(${v.input}.match(/\\x7f/g)||[]).length,` +
254
- `_vn=${v.input}.replace(/\\x7f/g,""),` +
255
- `${v.state}=${v.curState};` +
256
- `for(let _i=0;_i<_n;_i++)${v.state}=${v.state}.backspace();` +
257
- `for(const _c of _vn)${v.state}=${v.state}.insert(_c);` +
258
- `if(!${v.curState}.equals(${v.state})){` +
259
- `if(${v.curState}.text!==${v.state}.text)${v.updateText}(${v.state}.text);` +
260
- `${v.updateOffset}(${v.state}.offset)` +
261
- `}return;}`
262
- );
263
- }
264
-
265
- function escapeRegex(s) {
266
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
267
- }
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import { header, info, ok, warn, err, log } from '../lib/ui.mjs';
6
+
7
+ export const PATCH_MARKER = '/* dw-kit Vietnamese IME fix */';
8
+ export const DEL_CHAR = '\x7f';
9
+
10
+ export async function claudeVnFixCommand(opts) {
11
+ header('dw-kit Claude Vietnamese IME Fix');
12
+
13
+ const filePath = opts.path ? opts.path : findCliJs();
14
+ log(`Target file: ${filePath}`);
15
+
16
+ if (!existsSync(filePath)) {
17
+ err(`File not found: ${filePath}`);
18
+ process.exit(1);
19
+ }
20
+
21
+ if (opts.restore) {
22
+ info('Restoring from latest backup');
23
+ const restored = restoreLatestBackup(filePath, { dryRun: !!opts.dryRun });
24
+ if (!restored) process.exit(1);
25
+ ok('Restore complete. Restart Claude CLI.');
26
+ return;
27
+ }
28
+
29
+ info('Patching');
30
+ const result = patchCliJs(filePath, { dryRun: !!opts.dryRun });
31
+ if (!result.ok) {
32
+ err(result.message);
33
+ process.exit(1);
34
+ }
35
+ ok(result.message);
36
+ log('Restart Claude CLI for changes to take effect.');
37
+ warn('Note: This modifies a third-party installed file; you may need to re-run after Claude updates.');
38
+ }
39
+
40
+ export function findCliJs() {
41
+ // Strategy: check global npm root first, then common cache locations.
42
+ // This is intentionally conservative (no deep recursive scan of entire home).
43
+ const candidates = [];
44
+
45
+ // npm root -g
46
+ const npmRoot = tryNpmRootGlobal();
47
+ if (npmRoot) {
48
+ candidates.push(join(npmRoot, '@anthropic-ai', 'claude-code', 'cli.js'));
49
+ }
50
+
51
+ const home = os.homedir();
52
+ if (process.platform === 'win32') {
53
+ const appData = process.env.APPDATA || '';
54
+ const localAppData = process.env.LOCALAPPDATA || '';
55
+ if (appData) candidates.push(join(appData, 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'));
56
+ if (localAppData) {
57
+ const npxBase = join(localAppData, 'npm-cache', '_npx');
58
+ const latest = findLatestNpxClaudeCli(npxBase);
59
+ if (latest) candidates.push(latest);
60
+ }
61
+ } else {
62
+ const npxLatest = findLatestNpxClaudeCli(join(home, '.npm', '_npx'));
63
+ if (npxLatest) candidates.push(npxLatest);
64
+ candidates.push('/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js');
65
+ candidates.push('/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js');
66
+ }
67
+
68
+ for (const p of candidates) {
69
+ if (p && existsSync(p) && statSync(p).isFile()) return p;
70
+ }
71
+
72
+ throw new Error(
73
+ 'Could not auto-detect @anthropic-ai/claude-code/cli.js.\n' +
74
+ 'Provide it explicitly via: dw claude-vn-fix --path "<path-to-cli.js>"',
75
+ );
76
+ }
77
+
78
+ function tryNpmRootGlobal() {
79
+ try {
80
+ const out = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8', timeout: 5000 });
81
+ return out.trim();
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function safeMtime(p) {
88
+ try { return statSync(p).mtimeMs || 0; } catch { return 0; }
89
+ }
90
+
91
+ function findLatestNpxClaudeCli(npxBase) {
92
+ try {
93
+ if (!npxBase || !existsSync(npxBase)) return null;
94
+ const entries = readdirSync(npxBase, { withFileTypes: true })
95
+ .filter((e) => e.isDirectory())
96
+ .map((e) => join(npxBase, e.name));
97
+ const sorted = entries
98
+ .map((d) => ({ d, t: safeMtime(d) }))
99
+ .sort((a, b) => b.t - a.t)
100
+ .map((x) => x.d);
101
+
102
+ for (const dir of sorted.slice(0, 50)) {
103
+ const p = join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js');
104
+ if (existsSync(p)) return p;
105
+ }
106
+ return null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ export function patchCliJs(filePath, { dryRun }) {
113
+ const content = readFileSync(filePath, 'utf-8');
114
+
115
+ // Guard: ensure this is actually a Claude CLI bundle before patching.
116
+ if (!content.includes('@anthropic-ai') || !content.includes('claude-code')) {
117
+ return { ok: false, message: 'File does not appear to be a Claude CLI bundle. Use --path to specify the correct file.' };
118
+ }
119
+
120
+ if (content.includes(PATCH_MARKER)) {
121
+ return { ok: true, message: 'Already patched (marker found).' };
122
+ }
123
+
124
+ const idx = findBugPatternIndex(content);
125
+ if (idx === -1) {
126
+ return { ok: false, message: 'Bug pattern not found (.includes("\\x7f")). Claude may already be fixed upstream.' };
127
+ }
128
+
129
+ const { start, end, block } = findIfBlock(content, idx);
130
+ const vars = extractVariables(block);
131
+ const fixCode = generateFix(vars);
132
+ const patched = content.slice(0, start) + fixCode + content.slice(end);
133
+
134
+ if (dryRun) {
135
+ log('DRY RUN: would create backup and patch file.');
136
+ log(`Detected vars: input=${vars.input}, state=${vars.state}, cur=${vars.curState}`);
137
+ return { ok: true, message: 'Dry run OK.' };
138
+ }
139
+
140
+ const backupPath = createBackup(filePath);
141
+ ok(`Backup created: ${backupPath}`);
142
+ try {
143
+ writeFileSync(filePath, patched, 'utf-8');
144
+ const verify = readFileSync(filePath, 'utf-8');
145
+ if (!verify.includes(PATCH_MARKER)) throw new Error('verify failed (marker missing after write)');
146
+ return { ok: true, message: `Patch applied. Backup: ${backupPath}` };
147
+ } catch (e) {
148
+ warn(`Patch failed: ${e.message}`);
149
+ warn('Rolling back from backup.');
150
+ try {
151
+ copyFileSync(backupPath, filePath);
152
+ } catch (rollbackErr) {
153
+ return { ok: false, message: `Patch failed AND rollback failed: ${rollbackErr.message}. Manual restore from: ${backupPath}` };
154
+ }
155
+ return { ok: false, message: `Patch failed and rolled back: ${e.message}` };
156
+ }
157
+ }
158
+
159
+ function findBugPatternIndex(content) {
160
+ // Claude builds may contain either:
161
+ // - literal escape sequence: ".includes(\"\\x7f\")"
162
+ // - actual DEL char 0x7f inside the string: ".includes(\"\")"
163
+ const literal = content.indexOf('.includes("\\x7f")');
164
+ if (literal !== -1) return literal;
165
+ return content.indexOf(`.includes("${DEL_CHAR}")`);
166
+ }
167
+
168
+ function createBackup(filePath) {
169
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
170
+ const backupPath = `${filePath}.backup-${ts}`;
171
+ copyFileSync(filePath, backupPath);
172
+ return backupPath;
173
+ }
174
+
175
+ export function restoreLatestBackup(filePath, { dryRun }) {
176
+ const dir = dirname(filePath);
177
+ const base = filePath.split(/[\\/]/).pop();
178
+ const backups = readdirSync(dir)
179
+ .filter((f) => f.startsWith(`${base}.backup-`))
180
+ .map((f) => join(dir, f))
181
+ .map((p) => ({ p, t: safeMtime(p) }))
182
+ .sort((a, b) => b.t - a.t);
183
+
184
+ const latest = backups[0]?.p;
185
+ if (!latest) {
186
+ err('No backups found to restore.');
187
+ return false;
188
+ }
189
+
190
+ if (dryRun) {
191
+ log(`DRY RUN: would restore from ${latest}`);
192
+ return true;
193
+ }
194
+
195
+ copyFileSync(latest, filePath);
196
+ ok(`Restored from: ${latest}`);
197
+ return true;
198
+ }
199
+
200
+ function findIfBlock(content, idx) {
201
+ // Search backward from idx within a 500-char window to find the nearest if(
202
+ const windowStart = Math.max(0, idx - 500);
203
+ const searchSlice = content.slice(windowStart, idx);
204
+ const localOffset = searchSlice.lastIndexOf('if(');
205
+ if (localOffset === -1) throw new Error(`Could not find containing if(...) block near index ${idx}`);
206
+ const start = windowStart + localOffset;
207
+
208
+ let depth = 0;
209
+ let end = -1;
210
+ for (let i = start; i < content.length; i++) {
211
+ const c = content[i];
212
+ if (c === '{') depth++;
213
+ else if (c === '}') {
214
+ depth--;
215
+ if (depth === 0) { end = i + 1; break; }
216
+ }
217
+ }
218
+ if (end === -1) throw new Error('Could not find end of if block (brace mismatch)');
219
+ if (idx < start || idx > end) throw new Error('Bug pattern found outside expected if block');
220
+ return { start, end, block: content.slice(start, end) };
221
+ }
222
+
223
+ function extractVariables(block) {
224
+ // Normalize DEL char for regex matching
225
+ const normalized = block.replaceAll(DEL_CHAR, '\\x7f');
226
+
227
+ // Match: let COUNT=(INPUT.match(/\x7f/g)||[]).length,STATE=CURSTATE;
228
+ const m = normalized.match(/let ([\w$]+)=\(\w+\.match\(\/\\x7f\/g\)\|\|\[\]\)\.length[,;]([\w$]+)=([\w$]+)[;,]/);
229
+ if (!m) throw new Error('Could not extract variables (count/state/curState)');
230
+ const state = m[2];
231
+ const curState = m[3];
232
+
233
+ const m2 = block.match(new RegExp(`([\\w$]+)\\(${escapeRegex(state)}\\.text\\);([\\w$]+)\\(${escapeRegex(state)}\\.offset\\)`));
234
+ if (!m2) throw new Error('Could not extract update functions');
235
+
236
+ const m3 = block.match(/([\w$]+)\.includes\("/);
237
+ if (!m3) throw new Error('Could not extract input variable');
238
+
239
+ return {
240
+ input: m3[1],
241
+ state,
242
+ curState,
243
+ updateText: m2[1],
244
+ updateOffset: m2[2],
245
+ };
246
+ }
247
+
248
+ function generateFix(v) {
249
+ // This mirrors the known fix: backspace N times, then insert replacement text.
250
+ return (
251
+ `${PATCH_MARKER}` +
252
+ `if(${v.input}.includes("\\x7f")){` +
253
+ `let _n=(${v.input}.match(/\\x7f/g)||[]).length,` +
254
+ `_vn=${v.input}.replace(/\\x7f/g,""),` +
255
+ `${v.state}=${v.curState};` +
256
+ `for(let _i=0;_i<_n;_i++)${v.state}=${v.state}.backspace();` +
257
+ `for(const _c of _vn)${v.state}=${v.state}.insert(_c);` +
258
+ `if(!${v.curState}.equals(${v.state})){` +
259
+ `if(${v.curState}.text!==${v.state}.text)${v.updateText}(${v.state}.text);` +
260
+ `${v.updateOffset}(${v.state}.offset)` +
261
+ `}return;}`
262
+ );
263
+ }
264
+
265
+ function escapeRegex(s) {
266
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
267
+ }