@sweny-ai/core 0.1.84 → 0.1.85

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/verify.js DELETED
@@ -1,357 +0,0 @@
1
- // ─── Verify: path resolver + check evaluators ───────────────────────
2
- //
3
- // Evaluates `node.verify` post-conditions after the LLM finishes.
4
- // All checks are AND-ed; failures are aggregated into one error string.
5
- const SEGMENT_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)(\[\*\])?$/;
6
- const PREFIX_RE = /^([a-z]+):/;
7
- function parsePath(path) {
8
- if (path.length === 0)
9
- return "empty path";
10
- let mode = "all";
11
- let body = path;
12
- const prefixMatch = PREFIX_RE.exec(path);
13
- if (prefixMatch) {
14
- const prefix = prefixMatch[1];
15
- if (prefix !== "all" && prefix !== "any") {
16
- return `unknown prefix '${prefix}' (expected 'all' or 'any')`;
17
- }
18
- mode = prefix;
19
- body = path.slice(prefixMatch[0].length);
20
- if (body.length === 0)
21
- return "empty path after prefix";
22
- }
23
- const parts = body.split(".");
24
- const segments = [];
25
- for (const part of parts) {
26
- const m = SEGMENT_RE.exec(part);
27
- if (!m)
28
- return `malformed segment '${part}'`;
29
- segments.push({ name: m[1], wildcard: m[2] === "[*]" });
30
- }
31
- return { mode, segments };
32
- }
33
- export function resolvePath(data, path) {
34
- const parsed = parsePath(path);
35
- if (typeof parsed === "string")
36
- return { ok: false, reason: parsed };
37
- let current = [data];
38
- for (const seg of parsed.segments) {
39
- const next = [];
40
- for (const v of current) {
41
- if (v === null)
42
- return { ok: false, reason: `null encountered before segment '${seg.name}'` };
43
- if (typeof v !== "object") {
44
- return { ok: false, reason: `expected object before segment '${seg.name}', got ${typeof v}` };
45
- }
46
- const obj = v;
47
- if (!(seg.name in obj)) {
48
- return { ok: false, reason: `missing segment '${seg.name}'` };
49
- }
50
- const child = obj[seg.name];
51
- if (seg.wildcard) {
52
- if (!Array.isArray(child)) {
53
- return {
54
- ok: false,
55
- reason: `expected array at '${seg.name}', got ${child === null ? "null" : typeof child}`,
56
- };
57
- }
58
- for (const el of child)
59
- next.push(el);
60
- }
61
- else {
62
- next.push(child);
63
- }
64
- }
65
- current = next;
66
- if (current.length === 0) {
67
- return { ok: true, mode: parsed.mode, values: [] };
68
- }
69
- }
70
- return { ok: true, mode: parsed.mode, values: current };
71
- }
72
- // Legacy output-shape heuristic: a tool call "succeeded" when its output
73
- // does not carry a non-null `error` field. Kept as a fallback for ToolCall
74
- // records that predate the explicit `status` field (hand-constructed in
75
- // tests, older runtimes). The Claude runtime now sets `status` directly.
76
- export function isErrorOutput(output) {
77
- if (!output || typeof output !== "object")
78
- return false;
79
- const err = output.error;
80
- return err !== undefined && err !== null && err !== false;
81
- }
82
- /**
83
- * A tool call is considered successful when:
84
- * - explicit `status === "success"`, OR
85
- * - no explicit status AND output does not look like an error.
86
- *
87
- * `status` is authoritative. It's the only signal we have for external MCP
88
- * tools whose structured output isn't exposed to the workflow runtime.
89
- */
90
- function didSucceed(c) {
91
- if (c.status === "error")
92
- return false;
93
- if (c.status === "success")
94
- return true;
95
- return !isErrorOutput(c.output);
96
- }
97
- function succeededTools(toolCalls) {
98
- const names = new Set();
99
- for (const c of toolCalls) {
100
- if (didSucceed(c))
101
- names.add(c.tool);
102
- }
103
- return names;
104
- }
105
- function calledList(toolCalls) {
106
- return toolCalls.map((c) => c.tool).join(", ") || "none";
107
- }
108
- function expandAliases(name, aliases) {
109
- return aliases?.get(name) ?? new Set([name]);
110
- }
111
- function anyAliasIn(name, set, aliases) {
112
- for (const alias of expandAliases(name, aliases)) {
113
- if (set.has(alias))
114
- return true;
115
- }
116
- return false;
117
- }
118
- export function checkAnyToolCalled(required, toolCalls, aliases) {
119
- const succeeded = succeededTools(toolCalls);
120
- if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
121
- return null;
122
- return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
123
- }
124
- export function checkAllToolsCalled(required, toolCalls, aliases) {
125
- const succeeded = succeededTools(toolCalls);
126
- const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
127
- if (missing.length === 0)
128
- return null;
129
- return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
130
- }
131
- export function checkNoToolCalled(forbidden, toolCalls, aliases) {
132
- const calledNames = new Set(toolCalls.map((c) => c.tool));
133
- const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
134
- if (violated.length === 0)
135
- return null;
136
- return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
137
- }
138
- // Structural deep equality for JSON-shaped values. Handles object key-order
139
- // (compares by key set, not insertion order), NaN (NaN === NaN here), arrays
140
- // (element-wise), and treats `undefined` distinctly from missing keys. Does
141
- // not handle Date/Map/Set/RegExp — verify only ever sees agent-produced JSON.
142
- function deepEqual(a, b) {
143
- if (Object.is(a, b))
144
- return true;
145
- if (typeof a !== typeof b)
146
- return false;
147
- if (a === null || b === null)
148
- return false;
149
- if (typeof a !== "object")
150
- return false;
151
- if (Array.isArray(a)) {
152
- if (!Array.isArray(b) || a.length !== b.length)
153
- return false;
154
- for (let i = 0; i < a.length; i++) {
155
- if (!deepEqual(a[i], b[i]))
156
- return false;
157
- }
158
- return true;
159
- }
160
- if (Array.isArray(b))
161
- return false;
162
- const ao = a;
163
- const bo = b;
164
- const aKeys = Object.keys(ao);
165
- const bKeys = Object.keys(bo);
166
- if (aKeys.length !== bKeys.length)
167
- return false;
168
- for (const k of aKeys) {
169
- if (!Object.prototype.hasOwnProperty.call(bo, k))
170
- return false;
171
- if (!deepEqual(ao[k], bo[k]))
172
- return false;
173
- }
174
- return true;
175
- }
176
- function formatValue(v) {
177
- if (v === null)
178
- return "null";
179
- if (v === undefined)
180
- return "undefined";
181
- return JSON.stringify(v);
182
- }
183
- export function checkOutputRequired(paths, data) {
184
- const failures = [];
185
- for (const path of paths) {
186
- const r = resolvePath(data, path);
187
- if (!r.ok) {
188
- failures.push(`'${path}' ${r.reason}`);
189
- continue;
190
- }
191
- if (r.mode === "all") {
192
- if (r.values.length > 0 && !r.values.every((v) => v !== null && v !== undefined)) {
193
- failures.push(`'${path}' resolved to null/undefined value(s)`);
194
- }
195
- }
196
- else {
197
- if (r.values.length === 0) {
198
- failures.push(`'${path}' (any:) resolved to no elements`);
199
- }
200
- else if (!r.values.some((v) => v !== null && v !== undefined)) {
201
- failures.push(`'${path}' (any:) all resolved values are null/undefined`);
202
- }
203
- }
204
- }
205
- if (failures.length === 0)
206
- return null;
207
- return `output_required: ${failures.join("; ")}`;
208
- }
209
- /**
210
- * Cap on `verify.output_matches[].matches` source length.
211
- *
212
- * `matches` is user-supplied through workflow YAML, including marketplace
213
- * workflows. A pathological pattern like `(a+)+$` causes exponential
214
- * backtracking in V8's regex engine. Capping the source length is a cheap,
215
- * useful defense — well-formed verify regexes are tens of characters at
216
- * most. Patterns longer than this are almost always either generated junk
217
- * or a deliberate ReDoS attempt; either way, refusing to compile is safer
218
- * than executing them.
219
- *
220
- * Caveat: length alone does NOT prevent all ReDoS — a 12-char pattern like
221
- * `(a+)+$` is still catastrophic against the right input. Future work:
222
- * gate compilation through `safe-regex` or use the `re2` engine for
223
- * marketplace inputs. The cap closes the obvious abuse surface without
224
- * adding a runtime dep right now.
225
- */
226
- const MAX_REGEX_SOURCE_LENGTH = 1000;
227
- function compileMatch(m) {
228
- if (m.matches !== undefined) {
229
- if (m.matches.length > MAX_REGEX_SOURCE_LENGTH) {
230
- throw new Error(`regex source exceeds ${MAX_REGEX_SOURCE_LENGTH} characters (got ${m.matches.length}); ` +
231
- `tighten the pattern to avoid catastrophic-backtracking risk`);
232
- }
233
- // Throws synchronously if the regex source is invalid; surfaced as a
234
- // verify failure rather than a crash because the executor catches.
235
- return { match: m, regex: new RegExp(m.matches) };
236
- }
237
- return { match: m };
238
- }
239
- function applyOperator(c, value) {
240
- const m = c.match;
241
- if (m.equals !== undefined) {
242
- if (deepEqual(value, m.equals))
243
- return { ok: true };
244
- return { ok: false, reason: `equals ${formatValue(m.equals)}, got ${formatValue(value)}` };
245
- }
246
- if (m.in !== undefined) {
247
- if (m.in.some((x) => deepEqual(value, x)))
248
- return { ok: true };
249
- return {
250
- ok: false,
251
- reason: `in [${m.in.map(formatValue).join(", ")}], got ${formatValue(value)}`,
252
- };
253
- }
254
- if (c.regex) {
255
- const s = typeof value === "string" ? value : JSON.stringify(value);
256
- if (c.regex.test(s))
257
- return { ok: true };
258
- return { ok: false, reason: `matches /${m.matches}/, got ${formatValue(value)}` };
259
- }
260
- return { ok: false, reason: "no operator declared (zod should have caught this)" };
261
- }
262
- export function checkOutputMatches(matches, data) {
263
- const failures = [];
264
- for (const m of matches) {
265
- let compiled;
266
- try {
267
- compiled = compileMatch(m);
268
- }
269
- catch (err) {
270
- failures.push(`'${m.path}' invalid regex /${m.matches}/: ${err.message}`);
271
- continue;
272
- }
273
- const r = resolvePath(data, m.path);
274
- if (!r.ok) {
275
- failures.push(`'${m.path}' ${r.reason}`);
276
- continue;
277
- }
278
- if (r.mode === "all") {
279
- if (r.values.length === 0)
280
- continue;
281
- const offenders = [];
282
- for (const v of r.values) {
283
- const res = applyOperator(compiled, v);
284
- if (!res.ok)
285
- offenders.push(formatValue(v));
286
- }
287
- if (offenders.length > 0) {
288
- const opDescription = describeOperator(compiled);
289
- failures.push(`'${m.path}' ${opDescription}, got [${offenders.join(", ")}]`);
290
- }
291
- }
292
- else {
293
- if (r.values.length === 0) {
294
- failures.push(`'${m.path}' (any:) resolved to no elements`);
295
- continue;
296
- }
297
- const anyOk = r.values.some((v) => applyOperator(compiled, v).ok);
298
- if (!anyOk) {
299
- const opDescription = describeOperator(compiled);
300
- failures.push(`'${m.path}' (any:) no element satisfies ${opDescription}`);
301
- }
302
- }
303
- }
304
- if (failures.length === 0)
305
- return null;
306
- return `output_matches: ${failures.join("; ")}`;
307
- }
308
- function describeOperator(c) {
309
- const m = c.match;
310
- if (m.equals !== undefined)
311
- return `equals ${formatValue(m.equals)}`;
312
- if (m.in !== undefined)
313
- return `in [${m.in.map(formatValue).join(", ")}]`;
314
- if (m.matches !== undefined)
315
- return `matches /${m.matches}/`;
316
- return "no operator";
317
- }
318
- /**
319
- * Evaluate every declared check and return a concatenated error string,
320
- * or null when the node passes verification (or has no verify block).
321
- *
322
- * Deterministic and side-effect free — the executor calls this synchronously
323
- * after the LLM finishes.
324
- */
325
- export function evaluateVerify(verify, result, aliases) {
326
- if (!verify)
327
- return null;
328
- const failures = [];
329
- if (verify.any_tool_called && verify.any_tool_called.length > 0) {
330
- const e = checkAnyToolCalled(verify.any_tool_called, result.toolCalls, aliases);
331
- if (e)
332
- failures.push(e);
333
- }
334
- if (verify.all_tools_called && verify.all_tools_called.length > 0) {
335
- const e = checkAllToolsCalled(verify.all_tools_called, result.toolCalls, aliases);
336
- if (e)
337
- failures.push(e);
338
- }
339
- if (verify.no_tool_called && verify.no_tool_called.length > 0) {
340
- const e = checkNoToolCalled(verify.no_tool_called, result.toolCalls, aliases);
341
- if (e)
342
- failures.push(e);
343
- }
344
- if (verify.output_required && verify.output_required.length > 0) {
345
- const e = checkOutputRequired(verify.output_required, result.data);
346
- if (e)
347
- failures.push(e);
348
- }
349
- if (verify.output_matches && verify.output_matches.length > 0) {
350
- const e = checkOutputMatches(verify.output_matches, result.data);
351
- if (e)
352
- failures.push(e);
353
- }
354
- if (failures.length === 0)
355
- return null;
356
- return `verify failed:\n - ${failures.join("\n - ")}`;
357
- }