@wrongstack/tools 0.309.1 → 0.310.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/dist/_regex.d.ts +6 -34
- package/dist/bash.js +3 -3
- package/dist/builtin.js +3011 -1677
- package/dist/codebase-index/binary-frame.d.ts +57 -8
- package/dist/codebase-index/codebase-incoming-calls-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-outgoing-calls-tool.d.ts +6 -0
- package/dist/codebase-index/codebase-search-tool.d.ts +15 -5
- package/dist/codebase-index/index-service.d.ts +3 -19
- package/dist/codebase-index/index.js +2735 -1415
- package/dist/codebase-index/indexer.d.ts +3 -0
- package/dist/codebase-index/parser-batch.d.ts +53 -0
- package/dist/codebase-index/parser-dispatch.d.ts +32 -0
- package/dist/codebase-index/parser-output.d.ts +14 -0
- package/dist/codebase-index/parser-worker-pool.d.ts +57 -4
- package/dist/codebase-index/parser-worker-script.d.ts +5 -2
- package/dist/codebase-index/parser-worker-script.js +4042 -0
- package/dist/codebase-index/project-server-cache.d.ts +16 -0
- package/dist/codebase-index/project-server-client.d.ts +2 -2
- package/dist/codebase-index/project-server-query-cache.d.ts +88 -0
- package/dist/codebase-index/project-server.js +2846 -1308
- package/dist/codebase-index/py-parser.d.ts +5 -0
- package/dist/codebase-index/schema.d.ts +14 -1
- package/dist/codebase-index/sqlite-runtime.d.ts +2 -2
- package/dist/codebase-index/tree-sitter/queries.d.ts +30 -3
- package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
- package/dist/codebase-index/vector-search.d.ts +12 -0
- package/dist/codebase-index/wal-maintenance.d.ts +58 -0
- package/dist/codebase-index/worker-protocol/contracts.d.ts +44 -0
- package/dist/codebase-index/worker-protocol.d.ts +17 -1
- package/dist/codebase-index/worker.js +2300 -1020
- package/dist/codebase-index/writer-admin.d.ts +11 -0
- package/dist/codebase-index/writer-helpers.d.ts +31 -1
- package/dist/codebase-index/writer-mutations.d.ts +0 -6
- package/dist/codebase-index/writer-schema.d.ts +2 -2
- package/dist/codebase-index/writer.d.ts +15 -0
- package/dist/edit.js +2511 -1203
- package/dist/exec.js +5 -3
- package/dist/grep.js +5 -124
- package/dist/index.js +3007 -1736
- package/dist/json.js +5 -124
- package/dist/kanban.js +130 -0
- package/dist/logs.js +5 -121
- package/dist/pack.js +3011 -1677
- package/dist/patch.js +2524 -1216
- package/dist/plan.js +106 -0
- package/dist/read.js +2506 -1198
- package/dist/replace.js +2487 -1295
- package/dist/search.js +6 -2
- package/dist/session-kanban.js +24 -16
- package/dist/task.js +106 -0
- package/dist/todo.js +106 -0
- package/dist/tool-tier.d.ts +11 -0
- package/dist/tool-tier.js +3019 -1677
- package/dist/tree.js +14 -3
- package/dist/win32.js +3 -3
- package/dist/write.js +2513 -1205
- package/package.json +5 -4
package/dist/json.js
CHANGED
|
@@ -3,130 +3,11 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
|
|
4
4
|
|
|
5
5
|
// src/_regex.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Adjacent quantifiers: a++ a*+
|
|
12
|
-
/[+*]{2,}/,
|
|
13
|
-
// Quantifier on alternation with length 2+
|
|
14
|
-
/\([^|)]+\|[^)]+\)[+*][+*]/,
|
|
15
|
-
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
16
|
-
/[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
|
|
17
|
-
];
|
|
18
|
-
function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
19
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
20
|
-
if (pattern[i] !== "(") continue;
|
|
21
|
-
if (i > 0 && pattern[i - 1] === "\\") continue;
|
|
22
|
-
let depth = 0;
|
|
23
|
-
let inClass = false;
|
|
24
|
-
let j = i;
|
|
25
|
-
for (; j < pattern.length; j++) {
|
|
26
|
-
const ch = pattern[j];
|
|
27
|
-
if (ch === "\\") {
|
|
28
|
-
j++;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (inClass) {
|
|
32
|
-
if (ch === "]") inClass = false;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (ch === "[") {
|
|
36
|
-
inClass = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (ch === "(") depth++;
|
|
40
|
-
else if (ch === ")") {
|
|
41
|
-
depth--;
|
|
42
|
-
if (depth === 0) break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (j >= pattern.length) return false;
|
|
46
|
-
const next = pattern[j + 1];
|
|
47
|
-
if (next !== "+" && next !== "*" && next !== "{") continue;
|
|
48
|
-
let inner = pattern.slice(i + 1, j);
|
|
49
|
-
inner = inner.replace(/^\?(?::|<?[=!])/u, "");
|
|
50
|
-
const branches = [];
|
|
51
|
-
let current = "";
|
|
52
|
-
let d = 0;
|
|
53
|
-
let cls = false;
|
|
54
|
-
for (let k = 0; k < inner.length; k++) {
|
|
55
|
-
const ch = inner[k];
|
|
56
|
-
if (ch === "\\") {
|
|
57
|
-
current += ch + (inner[k + 1] ?? "");
|
|
58
|
-
k++;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (cls) {
|
|
62
|
-
if (ch === "]") cls = false;
|
|
63
|
-
current += ch;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (ch === "[") {
|
|
67
|
-
cls = true;
|
|
68
|
-
current += ch;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (ch === "(") d++;
|
|
72
|
-
if (ch === ")") d--;
|
|
73
|
-
if (ch === "|" && d === 0) {
|
|
74
|
-
branches.push(current);
|
|
75
|
-
current = "";
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
current += ch;
|
|
79
|
-
}
|
|
80
|
-
branches.push(current);
|
|
81
|
-
if (branches.length < 2) continue;
|
|
82
|
-
for (let a = 0; a < branches.length; a++) {
|
|
83
|
-
for (let b = a + 1; b < branches.length; b++) {
|
|
84
|
-
const x = branches[a];
|
|
85
|
-
const y = branches[b];
|
|
86
|
-
if (x === "" || y === "") return true;
|
|
87
|
-
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
function compileUserRegex(pattern, flags) {
|
|
94
|
-
if (typeof pattern !== "string") {
|
|
95
|
-
return { ok: false, reason: "pattern must be a string" };
|
|
96
|
-
}
|
|
97
|
-
if (pattern.length === 0) {
|
|
98
|
-
return { ok: false, reason: "pattern is empty" };
|
|
99
|
-
}
|
|
100
|
-
if (pattern.length > MAX_PATTERN_LEN) {
|
|
101
|
-
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
102
|
-
}
|
|
103
|
-
for (const rx of DANGEROUS_PATTERNS) {
|
|
104
|
-
if (rx.test(pattern)) {
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (hasAmbiguousQuantifiedAlternation(pattern)) {
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
119
|
-
} catch (err) {
|
|
120
|
-
return {
|
|
121
|
-
ok: false,
|
|
122
|
-
reason: err instanceof Error ? err.message : "invalid regex"
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
var MAX_SUBJECT_LEN = 64 * 1024;
|
|
127
|
-
function capSubject(line) {
|
|
128
|
-
return line.length > MAX_SUBJECT_LEN ? line.slice(0, MAX_SUBJECT_LEN) : line;
|
|
129
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
capSubject,
|
|
8
|
+
compileUserRegex,
|
|
9
|
+
MAX_SUBJECT_LEN
|
|
10
|
+
} from "@wrongstack/primitives";
|
|
130
11
|
|
|
131
12
|
// src/_util.ts
|
|
132
13
|
import * as fsp from "node:fs/promises";
|
package/dist/kanban.js
CHANGED
|
@@ -201,10 +201,112 @@ function taskFileToSerializedGraph(tasks, sessionId) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
// src/session-kanban-sync.ts
|
|
205
|
+
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
206
|
+
import { mutatePlan, mutateTasks } from "@wrongstack/core/storage";
|
|
207
|
+
import { formatTodosForModel, resolveWstackPaths } from "@wrongstack/core/utils";
|
|
208
|
+
import {
|
|
209
|
+
getDependencyReadinessIssues
|
|
210
|
+
} from "@wrongstack/kanban";
|
|
211
|
+
function sourceStatus(task) {
|
|
212
|
+
if (task.status === "completed") return "completed";
|
|
213
|
+
if (task.status === "in_progress") return "in_progress";
|
|
214
|
+
if (task.status === "review") return "review";
|
|
215
|
+
if (task.status === "blocked") return "blocked";
|
|
216
|
+
if (task.status === "failed") return "failed";
|
|
217
|
+
return "pending";
|
|
218
|
+
}
|
|
219
|
+
function todoStatus(task) {
|
|
220
|
+
const status = sourceStatus(task);
|
|
221
|
+
if (status === "completed") return "completed";
|
|
222
|
+
if (status === "review" && task.assignment?.status === "completed") return "completed";
|
|
223
|
+
if (status === "in_progress" || status === "review") return "in_progress";
|
|
224
|
+
return "pending";
|
|
225
|
+
}
|
|
226
|
+
async function applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors2, options = {}) {
|
|
227
|
+
const originId = task.origin?.taskId;
|
|
228
|
+
const graphId = task.origin?.graphId ?? "";
|
|
229
|
+
const isTodoOrigin = task.origin?.system === "session-todo" || graphId.startsWith("todo:");
|
|
230
|
+
const targetTodo = context.todos?.find(
|
|
231
|
+
(todo) => originId !== void 0 && todo.id === originId || todo.kanbanTaskId === task.id || todo.id === task.id && !todo.promotedFromPlan && !todo.promotedFromTask
|
|
232
|
+
);
|
|
233
|
+
if (isTodoOrigin || targetTodo) {
|
|
234
|
+
const matchedId = targetTodo?.id ?? originId;
|
|
235
|
+
if (matchedId) {
|
|
236
|
+
const mappedStatus = todoStatus(task);
|
|
237
|
+
const next = options.remove ? context.todos.filter((todo) => todo.id !== matchedId) : context.todos.map(
|
|
238
|
+
(todo) => todo.id === matchedId ? {
|
|
239
|
+
...todo,
|
|
240
|
+
content: task.title,
|
|
241
|
+
status: mappedStatus,
|
|
242
|
+
kanbanTaskId: task.id
|
|
243
|
+
} : todo
|
|
244
|
+
);
|
|
245
|
+
suppressedTodoMirrors2.add(context);
|
|
246
|
+
try {
|
|
247
|
+
context.state.replaceTodos(next);
|
|
248
|
+
} finally {
|
|
249
|
+
suppressedTodoMirrors2.delete(context);
|
|
250
|
+
}
|
|
251
|
+
return { source: "todo", todos: [...context.todos] };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!originId) return { source: null };
|
|
255
|
+
const id = context.session?.id ?? "";
|
|
256
|
+
if (task.origin?.system === "session-plan" || graphId.startsWith("plan:")) {
|
|
257
|
+
const planPath = context.meta["plan.path"];
|
|
258
|
+
if (typeof planPath !== "string" || !planPath) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"Cannot reflect this Kanban edit back to its plan source: the session has no plan.path configured. The board mutation already succeeded; the plan file is now out of sync."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const plan = await mutatePlan(planPath, id, (file) => ({
|
|
264
|
+
...file,
|
|
265
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
266
|
+
items: options.remove ? file.items.filter((item) => item.id !== originId) : file.items.map(
|
|
267
|
+
(item) => item.id === originId ? {
|
|
268
|
+
...item,
|
|
269
|
+
title: task.title,
|
|
270
|
+
details: task.description,
|
|
271
|
+
status: task.status === "completed" ? "done" : task.status === "in_progress" || task.status === "review" ? "in_progress" : "open",
|
|
272
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
|
+
} : item
|
|
274
|
+
)
|
|
275
|
+
}));
|
|
276
|
+
return { source: "plan", plan };
|
|
277
|
+
}
|
|
278
|
+
if (task.origin?.system === "session-task" || task.origin?.system === "session" || graphId.startsWith("session:")) {
|
|
279
|
+
const taskPath = context.meta["task.path"];
|
|
280
|
+
if (typeof taskPath !== "string" || !taskPath) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"Cannot reflect this Kanban edit back to its task source: the session has no task.path configured. The board mutation already succeeded; the task file is now out of sync."
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const tasks = await mutateTasks(taskPath, id, (file) => ({
|
|
286
|
+
...file,
|
|
287
|
+
tasks: options.remove ? file.tasks.filter((item) => item.id !== originId) : file.tasks.map(
|
|
288
|
+
(item) => item.id === originId ? {
|
|
289
|
+
...item,
|
|
290
|
+
title: task.title,
|
|
291
|
+
description: task.description,
|
|
292
|
+
status: sourceStatus(task),
|
|
293
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
294
|
+
} : item
|
|
295
|
+
)
|
|
296
|
+
}));
|
|
297
|
+
return { source: "task", tasks };
|
|
298
|
+
}
|
|
299
|
+
return { source: null };
|
|
300
|
+
}
|
|
301
|
+
|
|
204
302
|
// src/session-kanban.ts
|
|
205
303
|
var SESSION_KANBAN_COLUMNS = DEFAULT_COLUMNS.map((column) => ({
|
|
206
304
|
...column
|
|
207
305
|
}));
|
|
306
|
+
var suppressedTodoMirrors = /* @__PURE__ */ new WeakSet();
|
|
307
|
+
function applySessionKanbanTaskToSource2(context, task, options = {}) {
|
|
308
|
+
return applySessionKanbanTaskToSource(context, task, suppressedTodoMirrors, options);
|
|
309
|
+
}
|
|
208
310
|
|
|
209
311
|
// src/kanban-board-actions.ts
|
|
210
312
|
async function handleKanbanBoardAction(projectRoot, input, ctx) {
|
|
@@ -1027,12 +1129,20 @@ function assignmentForTaskCreate(input) {
|
|
|
1027
1129
|
}
|
|
1028
1130
|
|
|
1029
1131
|
// src/kanban-lifecycle-actions.ts
|
|
1132
|
+
async function syncContextTask(ctx, task, options = {}) {
|
|
1133
|
+
if (!ctx?.state || !task) return;
|
|
1134
|
+
try {
|
|
1135
|
+
await applySessionKanbanTaskToSource2(ctx, task, options);
|
|
1136
|
+
} catch {
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1030
1139
|
async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
1031
1140
|
switch (input.action) {
|
|
1032
1141
|
case "add_task": {
|
|
1033
1142
|
if (!input.boardId || !input.title) return fail("add_task requires boardId and title.");
|
|
1034
1143
|
const result = await addTask2(projectRoot, input.boardId, taskInput(input));
|
|
1035
1144
|
if (!result) return fail("Board not found.");
|
|
1145
|
+
await syncContextTask(ctx, result.task);
|
|
1036
1146
|
return okTask(result.board, result.task, `Task added.${atomicityNudge(result.task)}`);
|
|
1037
1147
|
}
|
|
1038
1148
|
case "split_task": {
|
|
@@ -1177,6 +1287,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1177
1287
|
);
|
|
1178
1288
|
}
|
|
1179
1289
|
ctx.setCurrentKanbanTask(task.id, board.id);
|
|
1290
|
+
await syncContextTask(ctx, task);
|
|
1180
1291
|
return okTask(
|
|
1181
1292
|
board,
|
|
1182
1293
|
task,
|
|
@@ -1186,6 +1297,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1186
1297
|
case "update_task": {
|
|
1187
1298
|
if (!input.boardId || !input.taskId) return fail("update_task requires boardId and taskId.");
|
|
1188
1299
|
const board = await updateTask2(projectRoot, input.boardId, input.taskId, taskPatch(input));
|
|
1300
|
+
if (board) {
|
|
1301
|
+
await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
|
|
1302
|
+
}
|
|
1189
1303
|
return board ? okBoard(board, "Task updated.") : fail("Task not found.");
|
|
1190
1304
|
}
|
|
1191
1305
|
case "transition_task": {
|
|
@@ -1225,6 +1339,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1225
1339
|
if (result && input.lifecycleStage === "done" && result.task.verificationReport) {
|
|
1226
1340
|
recordKanbanVerificationEvidence(ctx, result.task.verificationReport);
|
|
1227
1341
|
}
|
|
1342
|
+
if (result?.task) {
|
|
1343
|
+
await syncContextTask(ctx, result.task);
|
|
1344
|
+
}
|
|
1228
1345
|
return result ? okTask(result.board, result.task, `Task advanced to ${result.transition.to}.`) : fail("Board or task not found.");
|
|
1229
1346
|
}
|
|
1230
1347
|
case "repair_managed_projection": {
|
|
@@ -1237,6 +1354,9 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1237
1354
|
actor: input.author,
|
|
1238
1355
|
comment: input.transitionComment
|
|
1239
1356
|
});
|
|
1357
|
+
if (result?.task) {
|
|
1358
|
+
await syncContextTask(ctx, result.task);
|
|
1359
|
+
}
|
|
1240
1360
|
return result ? okTask(
|
|
1241
1361
|
result.board,
|
|
1242
1362
|
result.task,
|
|
@@ -1254,14 +1374,22 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1254
1374
|
input.targetColumnId,
|
|
1255
1375
|
input.order
|
|
1256
1376
|
);
|
|
1377
|
+
if (board) {
|
|
1378
|
+
await syncContextTask(ctx, board.tasks.find((t) => t.id === input.taskId));
|
|
1379
|
+
}
|
|
1257
1380
|
return board ? okBoard(board, "Task moved.") : fail("Move failed.");
|
|
1258
1381
|
}
|
|
1259
1382
|
case "delete_task": {
|
|
1260
1383
|
if (!input.boardId || !input.taskId) return fail("delete_task requires boardId and taskId.");
|
|
1384
|
+
const boardBefore = await getBoard4(projectRoot, input.boardId);
|
|
1385
|
+
const taskToDelete = boardBefore?.tasks.find((t) => t.id === input.taskId);
|
|
1261
1386
|
const board = await removeTask(projectRoot, input.boardId, input.taskId);
|
|
1262
1387
|
if (board && ctx.currentKanbanTaskId === input.taskId) {
|
|
1263
1388
|
ctx.setCurrentKanbanTask?.(void 0, ctx.currentKanbanBoardId);
|
|
1264
1389
|
}
|
|
1390
|
+
if (board && taskToDelete) {
|
|
1391
|
+
await syncContextTask(ctx, taskToDelete, { remove: true });
|
|
1392
|
+
}
|
|
1265
1393
|
return board ? okBoard(board, "Task deleted.") : fail("Task not found.");
|
|
1266
1394
|
}
|
|
1267
1395
|
case "set_chain": {
|
|
@@ -1368,6 +1496,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1368
1496
|
issues: finalized.gate.issues.map((issue) => issue.message)
|
|
1369
1497
|
};
|
|
1370
1498
|
const gateMessage = finalized.gate.allowed ? `Completion gate ${finalized.gate.verdict === "skipped" ? "passed" : "passed"}; task completed.` : finalized.gate.enforcement === "strict" ? `Completion gate BLOCKED (verdict: ${finalized.gate.verdict}); task parked in review. Issues: ${gateSummary.issues.join(" | ")}` : `Completion gate failed softly (verdict: ${finalized.gate.verdict}); task completed with warnings. Issues: ${gateSummary.issues.join(" | ")}`;
|
|
1499
|
+
await syncContextTask(ctx, finalized.task);
|
|
1371
1500
|
return {
|
|
1372
1501
|
...okTask(finalized.board, finalized.task, `Assignment updated. ${gateMessage}`),
|
|
1373
1502
|
gate: gateSummary
|
|
@@ -1473,6 +1602,7 @@ async function handleKanbanLifecycleAction(projectRoot, input, ctx) {
|
|
|
1473
1602
|
msgParts.push(`Card advanced to ${transitionResult.transition.to}.`);
|
|
1474
1603
|
}
|
|
1475
1604
|
for (const w of lifecycleWarnings) msgParts.push(`Warning: ${w}`);
|
|
1605
|
+
await syncContextTask(ctx, responseTask);
|
|
1476
1606
|
return okTask(responseBoard, responseTask, msgParts.join(" "));
|
|
1477
1607
|
}
|
|
1478
1608
|
return okBoard(board, "Assignment updated.");
|
package/dist/logs.js
CHANGED
|
@@ -3,127 +3,11 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
4
4
|
|
|
5
5
|
// src/_regex.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// Adjacent quantifiers: a++ a*+
|
|
12
|
-
/[+*]{2,}/,
|
|
13
|
-
// Quantifier on alternation with length 2+
|
|
14
|
-
/\([^|)]+\|[^)]+\)[+*][+*]/,
|
|
15
|
-
// Greedy quantifier inside lookahead/lookbehind — (?!.*a+)
|
|
16
|
-
/[([][^)\]]*[+*][^)\]]*[)\]][^)]*\?\??/
|
|
17
|
-
];
|
|
18
|
-
function hasAmbiguousQuantifiedAlternation(pattern) {
|
|
19
|
-
for (let i = 0; i < pattern.length; i++) {
|
|
20
|
-
if (pattern[i] !== "(") continue;
|
|
21
|
-
if (i > 0 && pattern[i - 1] === "\\") continue;
|
|
22
|
-
let depth = 0;
|
|
23
|
-
let inClass = false;
|
|
24
|
-
let j = i;
|
|
25
|
-
for (; j < pattern.length; j++) {
|
|
26
|
-
const ch = pattern[j];
|
|
27
|
-
if (ch === "\\") {
|
|
28
|
-
j++;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (inClass) {
|
|
32
|
-
if (ch === "]") inClass = false;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (ch === "[") {
|
|
36
|
-
inClass = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (ch === "(") depth++;
|
|
40
|
-
else if (ch === ")") {
|
|
41
|
-
depth--;
|
|
42
|
-
if (depth === 0) break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (j >= pattern.length) return false;
|
|
46
|
-
const next = pattern[j + 1];
|
|
47
|
-
if (next !== "+" && next !== "*" && next !== "{") continue;
|
|
48
|
-
let inner = pattern.slice(i + 1, j);
|
|
49
|
-
inner = inner.replace(/^\?(?::|<?[=!])/u, "");
|
|
50
|
-
const branches = [];
|
|
51
|
-
let current = "";
|
|
52
|
-
let d = 0;
|
|
53
|
-
let cls = false;
|
|
54
|
-
for (let k = 0; k < inner.length; k++) {
|
|
55
|
-
const ch = inner[k];
|
|
56
|
-
if (ch === "\\") {
|
|
57
|
-
current += ch + (inner[k + 1] ?? "");
|
|
58
|
-
k++;
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (cls) {
|
|
62
|
-
if (ch === "]") cls = false;
|
|
63
|
-
current += ch;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (ch === "[") {
|
|
67
|
-
cls = true;
|
|
68
|
-
current += ch;
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (ch === "(") d++;
|
|
72
|
-
if (ch === ")") d--;
|
|
73
|
-
if (ch === "|" && d === 0) {
|
|
74
|
-
branches.push(current);
|
|
75
|
-
current = "";
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
current += ch;
|
|
79
|
-
}
|
|
80
|
-
branches.push(current);
|
|
81
|
-
if (branches.length < 2) continue;
|
|
82
|
-
for (let a = 0; a < branches.length; a++) {
|
|
83
|
-
for (let b = a + 1; b < branches.length; b++) {
|
|
84
|
-
const x = branches[a];
|
|
85
|
-
const y = branches[b];
|
|
86
|
-
if (x === "" || y === "") return true;
|
|
87
|
-
if (x === y || x.startsWith(y) || y.startsWith(x)) return true;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
function compileUserRegex(pattern, flags) {
|
|
94
|
-
if (typeof pattern !== "string") {
|
|
95
|
-
return { ok: false, reason: "pattern must be a string" };
|
|
96
|
-
}
|
|
97
|
-
if (pattern.length === 0) {
|
|
98
|
-
return { ok: false, reason: "pattern is empty" };
|
|
99
|
-
}
|
|
100
|
-
if (pattern.length > MAX_PATTERN_LEN) {
|
|
101
|
-
return { ok: false, reason: `pattern exceeds ${MAX_PATTERN_LEN} characters` };
|
|
102
|
-
}
|
|
103
|
-
for (const rx of DANGEROUS_PATTERNS) {
|
|
104
|
-
if (rx.test(pattern)) {
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
reason: "pattern looks vulnerable to catastrophic backtracking \u2014 rewrite without nested quantifiers"
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
if (hasAmbiguousQuantifiedAlternation(pattern)) {
|
|
112
|
-
return {
|
|
113
|
-
ok: false,
|
|
114
|
-
reason: "pattern quantifies an alternation with overlapping branches \u2014 rewrite so no two branches can match the same text"
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
try {
|
|
118
|
-
return { ok: true, regex: new RegExp(pattern, flags) };
|
|
119
|
-
} catch (err) {
|
|
120
|
-
return {
|
|
121
|
-
ok: false,
|
|
122
|
-
reason: err instanceof Error ? err.message : "invalid regex"
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
var MAX_SUBJECT_LEN = 64 * 1024;
|
|
6
|
+
import {
|
|
7
|
+
capSubject,
|
|
8
|
+
compileUserRegex,
|
|
9
|
+
MAX_SUBJECT_LEN
|
|
10
|
+
} from "@wrongstack/primitives";
|
|
127
11
|
|
|
128
12
|
// src/_util.ts
|
|
129
13
|
import * as fsp from "node:fs/promises";
|