@theokit/sdk-tools 0.19.0 → 0.20.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.
- package/CHANGELOG.md +15 -0
- package/dist/index.cjs +283 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.js +284 -109
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 68d0e7f: `createApplyPatchTool` now parses Codex's **V4A patch grammar** (`*** Begin Patch` … `*** End Patch`;
|
|
8
|
+
`*** Add/Update/Delete File:`, optional `*** Move to:`, `@@`-anchored `+`/`-`/context hunks) instead of a
|
|
9
|
+
unified diff — the format the model actually emits in a Codex-style agent. **BREAKING:** the `{ patch }`
|
|
10
|
+
input is now a V4A patch, not a unified diff. Matching uses a context-tolerant ladder (exact → rstrip →
|
|
11
|
+
trim → unicode) mirroring Codex `seek_sequence`; `@@ <ctx>` anchors the search; `*** End of File` anchors
|
|
12
|
+
to the tail. Applied **strictly atomically** — the whole patch is planned (every file read + new content
|
|
13
|
+
computed + path security-checked) before any write, so a parse error / context mismatch / path violation
|
|
14
|
+
anywhere yields a typed error and ZERO writes (stronger than Codex, which can leave partial writes).
|
|
15
|
+
Add File strips one `+` and always ends with a trailing newline; Update+Move transforms the old content
|
|
16
|
+
and renames. New `internal/v4a-patch.ts` parser/matcher.
|
|
17
|
+
|
|
3
18
|
## 0.19.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -104,129 +104,304 @@ function isForbiddenPath(input) {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// src/internal/v4a-patch.ts
|
|
108
|
+
var BEGIN = "*** Begin Patch";
|
|
109
|
+
var END = "*** End Patch";
|
|
110
|
+
var ADD = "*** Add File: ";
|
|
111
|
+
var DELETE = "*** Delete File: ";
|
|
112
|
+
var UPDATE = "*** Update File: ";
|
|
113
|
+
var MOVE = "*** Move to: ";
|
|
114
|
+
var EOF_MARK = "*** End of File";
|
|
115
|
+
var CTX = "@@ ";
|
|
116
|
+
var CTX_EMPTY = "@@";
|
|
117
|
+
var V4APatchError = class extends Error {
|
|
118
|
+
constructor(message) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "V4APatchError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function parseV4A(patch) {
|
|
124
|
+
const lines = patch.split("\n").map((l) => l.endsWith("\r") ? l.slice(0, -1) : l);
|
|
125
|
+
let i = 0;
|
|
126
|
+
while (i < lines.length && lines[i].trim() !== BEGIN) i++;
|
|
127
|
+
if (i >= lines.length) throw new V4APatchError("Invalid patch: missing '*** Begin Patch'");
|
|
128
|
+
i++;
|
|
129
|
+
const hunks = [];
|
|
130
|
+
while (i < lines.length) {
|
|
131
|
+
if (lines[i].trim() === END) return hunks;
|
|
132
|
+
const step = parseNextHunk(lines, i);
|
|
133
|
+
if (step.hunk) hunks.push(step.hunk);
|
|
134
|
+
i = step.next;
|
|
135
|
+
}
|
|
136
|
+
throw new V4APatchError("Invalid patch: missing '*** End Patch'");
|
|
137
|
+
}
|
|
138
|
+
function parseNextHunk(lines, i) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
if (line.startsWith(ADD)) {
|
|
141
|
+
const [hunk, next] = parseAdd(lines, i);
|
|
142
|
+
return { hunk, next };
|
|
143
|
+
}
|
|
144
|
+
if (line.startsWith(DELETE)) {
|
|
145
|
+
return { hunk: { kind: "delete", path: line.slice(DELETE.length).trim() }, next: i + 1 };
|
|
146
|
+
}
|
|
147
|
+
if (line.startsWith(UPDATE)) {
|
|
148
|
+
const [hunk, next] = parseUpdate(lines, i);
|
|
149
|
+
return { hunk, next };
|
|
150
|
+
}
|
|
151
|
+
if (line.trim() === "") return { next: i + 1 };
|
|
152
|
+
throw new V4APatchError(`Invalid patch hunk on line ${i + 1}: unexpected '${line}'`);
|
|
153
|
+
}
|
|
154
|
+
function parseAdd(lines, start) {
|
|
155
|
+
const path = lines[start].slice(ADD.length).trim();
|
|
156
|
+
let i = start + 1;
|
|
157
|
+
const body = [];
|
|
158
|
+
while (i < lines.length && lines[i].startsWith("+")) {
|
|
159
|
+
body.push(lines[i].slice(1));
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
const content = body.length > 0 ? `${body.join("\n")}
|
|
163
|
+
` : "";
|
|
164
|
+
return [{ kind: "add", path, content }, i];
|
|
165
|
+
}
|
|
166
|
+
function parseUpdate(lines, start) {
|
|
167
|
+
const path = lines[start].slice(UPDATE.length).trim();
|
|
168
|
+
let i = start + 1;
|
|
169
|
+
let movePath = null;
|
|
170
|
+
if (i < lines.length && lines[i].startsWith(MOVE)) {
|
|
171
|
+
movePath = lines[i].slice(MOVE.length).trim();
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
const chunks = [];
|
|
175
|
+
let cur = null;
|
|
176
|
+
for (; i < lines.length; i++) {
|
|
177
|
+
const line = lines[i];
|
|
178
|
+
if (isHunkBoundary(line)) break;
|
|
179
|
+
cur = feedLine(chunks, cur, line, path, i + 1);
|
|
180
|
+
}
|
|
181
|
+
pushChunk(chunks, cur, path);
|
|
182
|
+
if (chunks.length === 0) throw new V4APatchError(`Update file hunk for '${path}' is empty`);
|
|
183
|
+
return [{ kind: "update", path, movePath, chunks }, i];
|
|
184
|
+
}
|
|
185
|
+
function isHunkBoundary(line) {
|
|
186
|
+
return line.trim() === END || line.startsWith(ADD) || line.startsWith(DELETE) || line.startsWith(UPDATE);
|
|
187
|
+
}
|
|
188
|
+
function emptyChunk() {
|
|
189
|
+
return { context: null, oldLines: [], newLines: [], eof: false };
|
|
190
|
+
}
|
|
191
|
+
function pushChunk(chunks, cur, path) {
|
|
192
|
+
if (!cur) return;
|
|
193
|
+
if (cur.oldLines.length === 0 && cur.newLines.length === 0) {
|
|
194
|
+
throw new V4APatchError(`Update hunk for '${path}' has an empty change block`);
|
|
195
|
+
}
|
|
196
|
+
chunks.push(cur);
|
|
197
|
+
}
|
|
198
|
+
function feedLine(chunks, cur, line, path, lineNum) {
|
|
199
|
+
if (line === CTX_EMPTY || line.startsWith(CTX)) {
|
|
200
|
+
pushChunk(chunks, cur, path);
|
|
201
|
+
return {
|
|
202
|
+
context: line === CTX_EMPTY ? null : line.slice(CTX.length),
|
|
203
|
+
oldLines: [],
|
|
204
|
+
newLines: [],
|
|
205
|
+
eof: false
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (line.trim() === EOF_MARK) {
|
|
209
|
+
if (!cur) throw new V4APatchError(`'*** End of File' before any change in '${path}'`);
|
|
210
|
+
cur.eof = true;
|
|
211
|
+
return cur;
|
|
212
|
+
}
|
|
213
|
+
return appendChangeLine(cur, line, path, lineNum);
|
|
214
|
+
}
|
|
215
|
+
function appendChangeLine(cur, line, path, lineNum) {
|
|
216
|
+
const c = cur ?? emptyChunk();
|
|
217
|
+
if (line.startsWith("+")) {
|
|
218
|
+
c.newLines.push(line.slice(1));
|
|
219
|
+
return c;
|
|
220
|
+
}
|
|
221
|
+
if (line.startsWith("-")) {
|
|
222
|
+
c.oldLines.push(line.slice(1));
|
|
223
|
+
return c;
|
|
224
|
+
}
|
|
225
|
+
if (line === "" || line.startsWith(" ")) {
|
|
226
|
+
const content = line === "" ? "" : line.slice(1);
|
|
227
|
+
c.oldLines.push(content);
|
|
228
|
+
c.newLines.push(content);
|
|
229
|
+
return c;
|
|
230
|
+
}
|
|
231
|
+
throw new V4APatchError(
|
|
232
|
+
`Unexpected line in update hunk for '${path}' (line ${lineNum}): every line must start with ' ', '+', or '-': '${line}'`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
var UNICODE_MAP = {
|
|
236
|
+
"\u2010": "-",
|
|
237
|
+
"\u2011": "-",
|
|
238
|
+
"\u2012": "-",
|
|
239
|
+
"\u2013": "-",
|
|
240
|
+
"\u2014": "-",
|
|
241
|
+
"\u2015": "-",
|
|
242
|
+
"\u2018": "'",
|
|
243
|
+
"\u2019": "'",
|
|
244
|
+
"\u201C": '"',
|
|
245
|
+
"\u201D": '"',
|
|
246
|
+
"\xA0": " ",
|
|
247
|
+
"\u2007": " ",
|
|
248
|
+
"\u202F": " "
|
|
249
|
+
};
|
|
250
|
+
function normalise(s) {
|
|
251
|
+
let out = "";
|
|
252
|
+
for (const ch of s) out += UNICODE_MAP[ch] ?? ch;
|
|
253
|
+
return out.trim();
|
|
254
|
+
}
|
|
255
|
+
var LADDER = [
|
|
256
|
+
(a, b) => a === b,
|
|
257
|
+
(a, b) => a.replace(/\s+$/, "") === b.replace(/\s+$/, ""),
|
|
258
|
+
(a, b) => a.trim() === b.trim(),
|
|
259
|
+
(a, b) => normalise(a) === normalise(b)
|
|
260
|
+
];
|
|
261
|
+
function matchesAt(lines, pattern, at, eq) {
|
|
262
|
+
for (let k = 0; k < pattern.length; k++) {
|
|
263
|
+
if (!eq(lines[at + k], pattern[k])) return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function seekSequence(lines, pattern, searchStart, eof) {
|
|
268
|
+
if (pattern.length === 0) return searchStart;
|
|
269
|
+
if (pattern.length > lines.length) return null;
|
|
270
|
+
const start = eof ? Math.max(searchStart, lines.length - pattern.length) : searchStart;
|
|
271
|
+
for (const eq of LADDER) {
|
|
272
|
+
for (let i = start; i <= lines.length - pattern.length; i++) {
|
|
273
|
+
if (matchesAt(lines, pattern, i, eq)) return i;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
function applyUpdateChunks(content, path, chunks) {
|
|
279
|
+
const lines = content.split("\n");
|
|
280
|
+
const edits = [];
|
|
281
|
+
let cursor = 0;
|
|
282
|
+
for (const chunk of chunks) {
|
|
283
|
+
if (chunk.context !== null) {
|
|
284
|
+
const ctxAt = seekSequence(lines, [chunk.context], cursor, false);
|
|
285
|
+
if (ctxAt === null) {
|
|
286
|
+
throw new V4APatchError(`Failed to find context '${chunk.context}' in ${path}`);
|
|
287
|
+
}
|
|
288
|
+
cursor = ctxAt + 1;
|
|
289
|
+
}
|
|
290
|
+
const at = seekSequence(lines, chunk.oldLines, cursor, chunk.eof);
|
|
291
|
+
if (at === null) {
|
|
292
|
+
throw new V4APatchError(
|
|
293
|
+
`Failed to find expected lines in ${path}:
|
|
294
|
+
${chunk.oldLines.join("\n")}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
edits.push({ start: at, oldLen: chunk.oldLines.length, newLines: chunk.newLines });
|
|
298
|
+
cursor = at + chunk.oldLines.length;
|
|
299
|
+
}
|
|
300
|
+
edits.sort((a, b) => b.start - a.start);
|
|
301
|
+
for (const e of edits) {
|
|
302
|
+
lines.splice(e.start, e.oldLen, ...e.newLines);
|
|
303
|
+
}
|
|
304
|
+
return lines.join("\n");
|
|
305
|
+
}
|
|
306
|
+
|
|
107
307
|
// src/apply-patch.ts
|
|
108
308
|
function createApplyPatchTool(opts) {
|
|
109
309
|
const { projectRoot } = opts;
|
|
110
310
|
return sdk.Tool.create({
|
|
111
311
|
name: "apply_patch",
|
|
112
|
-
description: "Apply a
|
|
312
|
+
description: "Apply a Codex-style V4A patch. The patch is `*** Begin Patch` \u2026 `*** End Patch` wrapping one or more hunks: `*** Add File: <path>` (then `+`lines), `*** Delete File: <path>`, or `*** Update File: <path>` (optional `*** Move to: <path>`) with `@@`-anchored `+` (add) / `-` (remove) / ` ` (context) lines. Read a file first so your context/removed lines match. Applied atomically \u2014 a mismatch anywhere aborts the whole patch with zero writes; each path is security-checked. Returns { ok, files_patched } or { ok: false, error }.",
|
|
113
313
|
inputSchema: zod.z.object({
|
|
114
|
-
patch: zod.z.string().min(1).describe("
|
|
314
|
+
patch: zod.z.string().min(1).describe("V4A patch: *** Begin Patch \u2026 *** End Patch.")
|
|
115
315
|
}),
|
|
116
|
-
|
|
117
|
-
handler: async ({ patch }) => {
|
|
118
|
-
const hunks = parsePatch(patch);
|
|
119
|
-
if (hunks.length === 0) {
|
|
120
|
-
return JSON.stringify({ ok: false, error: "parse_error", detail: "no file hunks found" });
|
|
121
|
-
}
|
|
122
|
-
for (const hunk of hunks) {
|
|
123
|
-
if (isForbiddenPath(hunk.file)) {
|
|
124
|
-
return JSON.stringify({ ok: false, error: "forbidden_path", path: hunk.file });
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const abs = safePathJoin(projectRoot, hunk.file);
|
|
128
|
-
assertNoSymlinkEscape(abs, projectRoot);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
131
|
-
return JSON.stringify({ ok: false, error: "path_traversal", path: hunk.file });
|
|
132
|
-
}
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const patched = [];
|
|
137
|
-
for (const hunk of hunks) {
|
|
138
|
-
const absolutePath = safePathJoin(projectRoot, hunk.file);
|
|
139
|
-
let content;
|
|
140
|
-
try {
|
|
141
|
-
content = await promises.readFile(absolutePath, "utf-8");
|
|
142
|
-
} catch (err) {
|
|
143
|
-
const e = err;
|
|
144
|
-
if (e.code === "ENOENT") {
|
|
145
|
-
content = "";
|
|
146
|
-
} else {
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const result = applyHunks(content, hunk.changes);
|
|
151
|
-
if (result === null) {
|
|
152
|
-
return JSON.stringify({
|
|
153
|
-
ok: false,
|
|
154
|
-
error: "patch_failed",
|
|
155
|
-
path: hunk.file,
|
|
156
|
-
detail: "hunk context mismatch"
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
if (content !== "") {
|
|
160
|
-
await promises.copyFile(absolutePath, `${absolutePath}.bak`);
|
|
161
|
-
}
|
|
162
|
-
await promises.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
163
|
-
await promises.writeFile(absolutePath, result, "utf-8");
|
|
164
|
-
patched.push(hunk.file);
|
|
165
|
-
}
|
|
166
|
-
return JSON.stringify({ ok: true, files_patched: patched });
|
|
167
|
-
}
|
|
316
|
+
handler: async ({ patch }) => applyV4APatch(projectRoot, patch)
|
|
168
317
|
});
|
|
169
318
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
319
|
+
async function applyV4APatch(projectRoot, patch) {
|
|
320
|
+
let hunks;
|
|
321
|
+
try {
|
|
322
|
+
hunks = parseV4A(patch);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
const detail = err instanceof V4APatchError ? err.message : String(err);
|
|
325
|
+
return JSON.stringify({ ok: false, error: "parse_error", detail });
|
|
326
|
+
}
|
|
327
|
+
const plan = await buildPlan(projectRoot, hunks);
|
|
328
|
+
if ("error" in plan) return plan.error;
|
|
329
|
+
await executePlan(plan.ops);
|
|
330
|
+
return JSON.stringify({ ok: true, files_patched: plan.patched });
|
|
331
|
+
}
|
|
332
|
+
async function buildPlan(projectRoot, hunks) {
|
|
333
|
+
const ops = [];
|
|
334
|
+
const patched = [];
|
|
335
|
+
for (const hunk of hunks) {
|
|
336
|
+
const planned = await planHunk(projectRoot, hunk);
|
|
337
|
+
if ("error" in planned) return planned;
|
|
338
|
+
ops.push(...planned.ops);
|
|
339
|
+
patched.push(hunk.kind === "update" ? hunk.movePath ?? hunk.path : hunk.path);
|
|
340
|
+
}
|
|
341
|
+
return { ops, patched };
|
|
342
|
+
}
|
|
343
|
+
async function executePlan(ops) {
|
|
344
|
+
for (const op of ops) {
|
|
345
|
+
if ("rm" in op) {
|
|
346
|
+
await promises.rm(op.rm, { force: true });
|
|
347
|
+
} else {
|
|
348
|
+
await promises.mkdir(path.dirname(op.write.abs), { recursive: true });
|
|
349
|
+
await promises.writeFile(op.write.abs, op.write.content, "utf-8");
|
|
192
350
|
}
|
|
193
351
|
}
|
|
194
|
-
return hunks;
|
|
195
352
|
}
|
|
196
|
-
function
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
353
|
+
async function planHunk(projectRoot, hunk) {
|
|
354
|
+
const scope = v4aScope(projectRoot, hunk.path);
|
|
355
|
+
if ("error" in scope) return scope;
|
|
356
|
+
if (hunk.kind === "add") return { ops: [{ write: { abs: scope.abs, content: hunk.content } }] };
|
|
357
|
+
if (hunk.kind === "delete") return { ops: [{ rm: scope.abs }] };
|
|
358
|
+
return planUpdate(projectRoot, hunk, scope.abs);
|
|
359
|
+
}
|
|
360
|
+
async function planUpdate(projectRoot, hunk, abs) {
|
|
361
|
+
let content;
|
|
362
|
+
try {
|
|
363
|
+
content = await promises.readFile(abs, "utf-8");
|
|
364
|
+
} catch (err) {
|
|
365
|
+
if (err.code === "ENOENT") {
|
|
366
|
+
return { error: JSON.stringify({ ok: false, error: "not_found", path: hunk.path }) };
|
|
206
367
|
}
|
|
207
|
-
|
|
368
|
+
throw err;
|
|
208
369
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
result.push(change.content);
|
|
370
|
+
let updated;
|
|
371
|
+
try {
|
|
372
|
+
updated = applyUpdateChunks(content, hunk.path, hunk.chunks);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (err instanceof V4APatchError) {
|
|
375
|
+
return {
|
|
376
|
+
error: JSON.stringify({
|
|
377
|
+
ok: false,
|
|
378
|
+
error: "patch_failed",
|
|
379
|
+
path: hunk.path,
|
|
380
|
+
detail: err.message
|
|
381
|
+
})
|
|
382
|
+
};
|
|
223
383
|
}
|
|
384
|
+
throw err;
|
|
224
385
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
386
|
+
if (!hunk.movePath) return { ops: [{ write: { abs, content: updated } }] };
|
|
387
|
+
const dest = v4aScope(projectRoot, hunk.movePath);
|
|
388
|
+
if ("error" in dest) return dest;
|
|
389
|
+
return { ops: [{ write: { abs: dest.abs, content: updated } }, { rm: abs }] };
|
|
390
|
+
}
|
|
391
|
+
function v4aScope(projectRoot, file) {
|
|
392
|
+
if (isForbiddenPath(file)) {
|
|
393
|
+
return { error: JSON.stringify({ ok: false, error: "forbidden_path", path: file }) };
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const abs = safePathJoin(projectRoot, file);
|
|
397
|
+
assertNoSymlinkEscape(abs, projectRoot);
|
|
398
|
+
return { abs };
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err instanceof PathTraversalError || err instanceof ForbiddenPathError) {
|
|
401
|
+
return { error: JSON.stringify({ ok: false, error: "path_traversal", path: file }) };
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
228
404
|
}
|
|
229
|
-
return result.join("\n");
|
|
230
405
|
}
|
|
231
406
|
function createSessionArtifactStore(options) {
|
|
232
407
|
const { dir } = options;
|
|
@@ -330,7 +505,7 @@ var LINE_MATCH_LADDER = [
|
|
|
330
505
|
(s) => normalizeUnicode(s).trim()
|
|
331
506
|
// unicode + trim (loosest)
|
|
332
507
|
];
|
|
333
|
-
function
|
|
508
|
+
function matchesAt2(lines, pattern, i, norm) {
|
|
334
509
|
for (let p = 0; p < pattern.length; p++) {
|
|
335
510
|
const lineAt = lines[i + p];
|
|
336
511
|
const patAt = pattern[p];
|
|
@@ -341,7 +516,7 @@ function matchesAt(lines, pattern, i, norm) {
|
|
|
341
516
|
function findHits(lines, pattern, norm) {
|
|
342
517
|
const hits = [];
|
|
343
518
|
for (let i = 0; i + pattern.length <= lines.length; i++) {
|
|
344
|
-
if (
|
|
519
|
+
if (matchesAt2(lines, pattern, i, norm)) hits.push(i);
|
|
345
520
|
}
|
|
346
521
|
return hits;
|
|
347
522
|
}
|