@sweny-ai/core 0.1.84 → 0.1.86

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/claude.d.ts CHANGED
@@ -56,6 +56,7 @@ export declare class ClaudeClient implements Claude {
56
56
  ask(opts: {
57
57
  instruction: string;
58
58
  context: Record<string, unknown>;
59
+ model?: string;
59
60
  }): Promise<string>;
60
61
  }
61
62
  /**
package/dist/claude.js CHANGED
@@ -158,8 +158,8 @@ export class ClaudeClient {
158
158
  const parsed = parseToolResultContent(block.content);
159
159
  call.output = isError ? { error: parsed } : parsed;
160
160
  // Surface tool errors in the CI log stream at warn level so
161
- // postmortems don't have to reconstruct them from verify-time
162
- // tool-call summaries. Without this, "verify failed: tool X
161
+ // postmortems don't have to reconstruct them from eval-time
162
+ // tool-call summaries. Without this, "eval failed: tool X
163
163
  // did not succeed" never answers the WHY question, because
164
164
  // the error body is buried in the per-call output captured
165
165
  // only for in-memory verify evaluation.
@@ -263,7 +263,7 @@ export class ClaudeClient {
263
263
  return validIds[0];
264
264
  }
265
265
  async ask(opts) {
266
- const { instruction, context } = opts;
266
+ const { instruction, context, model } = opts;
267
267
  const prompt = [
268
268
  instruction,
269
269
  Object.keys(context).length > 0 ? `\nContext:\n\`\`\`json\n${JSON.stringify(context, null, 2)}\n\`\`\`` : "",
@@ -272,6 +272,7 @@ export class ClaudeClient {
272
272
  .join("\n");
273
273
  const env = this.buildEnv();
274
274
  let response = "";
275
+ const effectiveModel = model ?? this.model;
275
276
  try {
276
277
  const stream = query({
277
278
  prompt,
@@ -282,7 +283,7 @@ export class ClaudeClient {
282
283
  permissionMode: "bypassPermissions",
283
284
  allowDangerouslySkipPermissions: true,
284
285
  stderr: (data) => this.logger.debug(`[claude-code] ${data}`),
285
- ...(this.model ? { model: this.model } : {}),
286
+ ...(effectiveModel ? { model: effectiveModel } : {}),
286
287
  },
287
288
  });
288
289
  for await (const message of stream) {
@@ -0,0 +1,25 @@
1
+ import type { EvaluatorRule, ToolCall } from "../types.js";
2
+ /**
3
+ * Legacy output-shape heuristic: a tool call "succeeded" when its output
4
+ * does not carry a non-null `error` field. Kept as a fallback for ToolCall
5
+ * records that predate the explicit `status` field (hand-constructed in
6
+ * tests, older runtimes). The Claude runtime now sets `status` directly.
7
+ */
8
+ export declare function isErrorOutput(output: unknown): boolean;
9
+ export type ToolAliases = ReadonlyMap<string, ReadonlySet<string>>;
10
+ export declare function checkAnyToolCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
11
+ export declare function checkAllToolsCalled(required: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
12
+ export declare function checkNoToolCalled(forbidden: string[], toolCalls: ToolCall[], aliases?: ToolAliases): string | null;
13
+ /**
14
+ * Evaluate a `function` rule against the node's tool-call trace.
15
+ *
16
+ * Returns `{ pass: true }` when every declared check passes, otherwise
17
+ * `{ pass: false, reasoning: <semicolon-joined failure list> }`.
18
+ *
19
+ * Value-rule fields (`output_required` / `output_matches`) are ignored
20
+ * even if present.
21
+ */
22
+ export declare function evaluateFunctionRule(rule: EvaluatorRule, toolCalls: ToolCall[], aliases?: ToolAliases): {
23
+ pass: boolean;
24
+ reasoning?: string;
25
+ };
@@ -0,0 +1,106 @@
1
+ // ─── Eval: function (trace-shape) evaluator ──────────────────────────
2
+ //
3
+ // Operates on the node's tool-call trace. Accepts an alias map so a workflow
4
+ // rule can reference a canonical tool name and match against an equivalent
5
+ // MCP tool name. Each skill owns its own alias map; core stays vendor-neutral.
6
+ // ─── Tool-call success heuristic ─────────────────────────────────────
7
+ /**
8
+ * Legacy output-shape heuristic: a tool call "succeeded" when its output
9
+ * does not carry a non-null `error` field. Kept as a fallback for ToolCall
10
+ * records that predate the explicit `status` field (hand-constructed in
11
+ * tests, older runtimes). The Claude runtime now sets `status` directly.
12
+ */
13
+ export function isErrorOutput(output) {
14
+ if (!output || typeof output !== "object")
15
+ return false;
16
+ const err = output.error;
17
+ return err !== undefined && err !== null && err !== false;
18
+ }
19
+ /**
20
+ * A tool call is considered successful when:
21
+ * - explicit `status === "success"`, OR
22
+ * - no explicit status AND output does not look like an error.
23
+ *
24
+ * `status` is authoritative. It's the only signal we have for external MCP
25
+ * tools whose structured output isn't exposed to the workflow runtime.
26
+ */
27
+ function didSucceed(c) {
28
+ if (c.status === "error")
29
+ return false;
30
+ if (c.status === "success")
31
+ return true;
32
+ return !isErrorOutput(c.output);
33
+ }
34
+ function succeededTools(toolCalls) {
35
+ const names = new Set();
36
+ for (const c of toolCalls) {
37
+ if (didSucceed(c))
38
+ names.add(c.tool);
39
+ }
40
+ return names;
41
+ }
42
+ function calledList(toolCalls) {
43
+ return toolCalls.map((c) => c.tool).join(", ") || "none";
44
+ }
45
+ function expandAliases(name, aliases) {
46
+ return aliases?.get(name) ?? new Set([name]);
47
+ }
48
+ function anyAliasIn(name, set, aliases) {
49
+ for (const alias of expandAliases(name, aliases)) {
50
+ if (set.has(alias))
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ // ─── Public check helpers ────────────────────────────────────────────
56
+ export function checkAnyToolCalled(required, toolCalls, aliases) {
57
+ const succeeded = succeededTools(toolCalls);
58
+ if (required.some((t) => anyAliasIn(t, succeeded, aliases)))
59
+ return null;
60
+ return `any_tool_called: required one of [${required.join(", ")}] to succeed, called: [${calledList(toolCalls)}]`;
61
+ }
62
+ export function checkAllToolsCalled(required, toolCalls, aliases) {
63
+ const succeeded = succeededTools(toolCalls);
64
+ const missing = required.filter((t) => !anyAliasIn(t, succeeded, aliases));
65
+ if (missing.length === 0)
66
+ return null;
67
+ return `all_tools_called: missing successful calls to [${missing.join(", ")}], called: [${calledList(toolCalls)}]`;
68
+ }
69
+ export function checkNoToolCalled(forbidden, toolCalls, aliases) {
70
+ const calledNames = new Set(toolCalls.map((c) => c.tool));
71
+ const violated = forbidden.filter((t) => anyAliasIn(t, calledNames, aliases));
72
+ if (violated.length === 0)
73
+ return null;
74
+ return `no_tool_called: forbidden tools were invoked: [${violated.join(", ")}], called: [${calledList(toolCalls)}]`;
75
+ }
76
+ // ─── Evaluator entry point ───────────────────────────────────────────
77
+ /**
78
+ * Evaluate a `function` rule against the node's tool-call trace.
79
+ *
80
+ * Returns `{ pass: true }` when every declared check passes, otherwise
81
+ * `{ pass: false, reasoning: <semicolon-joined failure list> }`.
82
+ *
83
+ * Value-rule fields (`output_required` / `output_matches`) are ignored
84
+ * even if present.
85
+ */
86
+ export function evaluateFunctionRule(rule, toolCalls, aliases) {
87
+ const failures = [];
88
+ if (rule.any_tool_called && rule.any_tool_called.length > 0) {
89
+ const e = checkAnyToolCalled(rule.any_tool_called, toolCalls, aliases);
90
+ if (e)
91
+ failures.push(e);
92
+ }
93
+ if (rule.all_tools_called && rule.all_tools_called.length > 0) {
94
+ const e = checkAllToolsCalled(rule.all_tools_called, toolCalls, aliases);
95
+ if (e)
96
+ failures.push(e);
97
+ }
98
+ if (rule.no_tool_called && rule.no_tool_called.length > 0) {
99
+ const e = checkNoToolCalled(rule.no_tool_called, toolCalls, aliases);
100
+ if (e)
101
+ failures.push(e);
102
+ }
103
+ if (failures.length === 0)
104
+ return { pass: true };
105
+ return { pass: false, reasoning: failures.join("; ") };
106
+ }
@@ -0,0 +1,41 @@
1
+ import type { Claude, EvalPolicy, EvalResult, Evaluator, NodeResult, Workflow, Node } from "../types.js";
2
+ import { type ToolAliases } from "./function.js";
3
+ export type { ToolAliases } from "./function.js";
4
+ export interface EvaluateAllOptions {
5
+ /** Tool-name aliases from the loaded skills. Used by `function` evaluators only. */
6
+ aliases?: ToolAliases;
7
+ /** Optional Claude client. Required when any evaluator has kind: "judge". */
8
+ claude?: Claude;
9
+ /** The node being evaluated. Used to resolve per-node `judge_model` overrides. */
10
+ node?: Node;
11
+ /** The workflow being executed. Used to resolve workflow-level `judge_model` overrides. */
12
+ workflow?: Workflow;
13
+ }
14
+ /**
15
+ * Run every declared evaluator against the node's result and return one
16
+ * {@link EvalResult} per evaluator, in declaration order.
17
+ *
18
+ * The function never throws on a failing rule (failures become
19
+ * `{ pass: false, reasoning }` results). It DOES throw if the underlying
20
+ * judge implementation throws (e.g. v1.0 judge stub) so the executor can
21
+ * surface the error clearly at the call site.
22
+ */
23
+ export declare function evaluateAll(evaluators: Evaluator[] | undefined, result: NodeResult, opts?: EvaluateAllOptions): Promise<EvalResult[]>;
24
+ export interface AggregateOutcome {
25
+ /** Whether the node passed under the configured policy. */
26
+ pass: boolean;
27
+ /**
28
+ * Structured failure message for the executor when `pass` is false.
29
+ * Format: header line + one bulleted line per failing evaluator.
30
+ * Suitable as input to {@link buildRetryPreamble}.
31
+ */
32
+ error?: string;
33
+ /** Direct access to the failed evaluator results, in declaration order. */
34
+ failures: EvalResult[];
35
+ }
36
+ /**
37
+ * Collapse a list of {@link EvalResult}s into a single pass/fail under the
38
+ * node's `eval_policy`. v1 implements `all_pass`. Other policies are
39
+ * accepted at parse time but rejected here so users get a clear error.
40
+ */
41
+ export declare function aggregateEval(results: EvalResult[], policy?: EvalPolicy): AggregateOutcome;
@@ -0,0 +1,91 @@
1
+ // ─── Eval: dispatcher + aggregation ──────────────────────────────────
2
+ //
3
+ // `evaluateAll(...)` runs every declared evaluator and returns a structured
4
+ // list of EvalResults. `aggregateEval(...)` collapses that list into a
5
+ // single pass/fail under the node's eval_policy. v1 implements `all_pass`.
6
+ import { evaluateValueRule } from "./value.js";
7
+ import { evaluateFunctionRule } from "./function.js";
8
+ import { evaluateJudge } from "./judge.js";
9
+ /** Reasoning cap, matching the spec's ~500-character ceiling. */
10
+ const MAX_REASONING_LENGTH = 500;
11
+ /** Default judge model when neither evaluator nor node nor workflow specify one. */
12
+ const DEFAULT_JUDGE_MODEL = "claude-haiku-4-5";
13
+ function capReasoning(reasoning) {
14
+ if (!reasoning)
15
+ return undefined;
16
+ if (reasoning.length <= MAX_REASONING_LENGTH)
17
+ return reasoning;
18
+ return reasoning.slice(0, MAX_REASONING_LENGTH - 1) + "…";
19
+ }
20
+ function resolveJudgeModel(evaluator, node, workflow) {
21
+ return evaluator.model ?? node?.judge_model ?? workflow?.judge_model ?? DEFAULT_JUDGE_MODEL;
22
+ }
23
+ /**
24
+ * Run every declared evaluator against the node's result and return one
25
+ * {@link EvalResult} per evaluator, in declaration order.
26
+ *
27
+ * The function never throws on a failing rule (failures become
28
+ * `{ pass: false, reasoning }` results). It DOES throw if the underlying
29
+ * judge implementation throws (e.g. v1.0 judge stub) so the executor can
30
+ * surface the error clearly at the call site.
31
+ */
32
+ export async function evaluateAll(evaluators, result, opts = {}) {
33
+ if (!evaluators || evaluators.length === 0)
34
+ return [];
35
+ const out = [];
36
+ for (const e of evaluators) {
37
+ if (e.kind === "value") {
38
+ const verdict = evaluateValueRule(e.rule ?? {}, result.data);
39
+ out.push({
40
+ name: e.name,
41
+ kind: "value",
42
+ pass: verdict.pass,
43
+ reasoning: capReasoning(verdict.reasoning),
44
+ });
45
+ }
46
+ else if (e.kind === "function") {
47
+ const verdict = evaluateFunctionRule(e.rule ?? {}, result.toolCalls, opts.aliases);
48
+ out.push({
49
+ name: e.name,
50
+ kind: "function",
51
+ pass: verdict.pass,
52
+ reasoning: capReasoning(verdict.reasoning),
53
+ });
54
+ }
55
+ else if (e.kind === "judge") {
56
+ if (!opts.claude) {
57
+ throw new Error(`evaluator '${e.name}' is kind: judge but no Claude client was provided to evaluateAll`);
58
+ }
59
+ const model = resolveJudgeModel(e, opts.node, opts.workflow);
60
+ const verdict = await evaluateJudge(e, result, opts.claude, { model });
61
+ out.push({
62
+ name: e.name,
63
+ kind: "judge",
64
+ pass: verdict.pass,
65
+ reasoning: capReasoning(verdict.reasoning),
66
+ });
67
+ }
68
+ }
69
+ return out;
70
+ }
71
+ /**
72
+ * Collapse a list of {@link EvalResult}s into a single pass/fail under the
73
+ * node's `eval_policy`. v1 implements `all_pass`. Other policies are
74
+ * accepted at parse time but rejected here so users get a clear error.
75
+ */
76
+ export function aggregateEval(results, policy = "all_pass") {
77
+ if (results.length === 0)
78
+ return { pass: true, failures: [] };
79
+ if (policy === "all_pass") {
80
+ const failures = results.filter((r) => !r.pass);
81
+ if (failures.length === 0)
82
+ return { pass: true, failures: [] };
83
+ const lines = failures.map((f) => ` - ${f.name} (${f.kind}): ${f.reasoning ?? "no reasoning"}`);
84
+ return {
85
+ pass: false,
86
+ error: `eval failed (policy: all_pass):\n${lines.join("\n")}`,
87
+ failures,
88
+ };
89
+ }
90
+ throw new Error(`eval_policy '${policy}' is reserved in v1.0 but not yet implemented; use 'all_pass' (default) for v1.0 workflows`);
91
+ }
@@ -0,0 +1,43 @@
1
+ import type { Claude, Evaluator, NodeResult } from "../types.js";
2
+ export interface JudgeOptions {
3
+ /** Resolved judge model (evaluator > node > workflow > package default). */
4
+ model: string;
5
+ }
6
+ interface ParsedResponse {
7
+ verdict: string | null;
8
+ reasoning: string | null;
9
+ }
10
+ /**
11
+ * Build the structured judge prompt.
12
+ *
13
+ * The prompt format is fixed so the parser can rely on it. Model authors
14
+ * MUST NOT change the section markers without updating {@link parseJudgeResponse}
15
+ * in lockstep.
16
+ */
17
+ export declare function buildJudgePrompt(evaluator: Evaluator, result: NodeResult): string;
18
+ /**
19
+ * Parse a judge response into `{ verdict, reasoning }`.
20
+ *
21
+ * Format expected:
22
+ * VERDICT: <token>
23
+ * REASONING: <text...>
24
+ *
25
+ * Lines may have any leading whitespace. Reasoning may span multiple lines
26
+ * (everything after the `REASONING:` marker is consumed). Returns null
27
+ * fields when the markers are missing.
28
+ */
29
+ export declare function parseJudgeResponse(text: string): ParsedResponse;
30
+ /**
31
+ * Run a single judge evaluator against a node result.
32
+ *
33
+ * Resolves a verdict by calling the judge model, parsing VERDICT/REASONING,
34
+ * and comparing the verdict to `pass_when` (default `yes`). On parse
35
+ * failure, retries the call once. If the retry also fails to parse,
36
+ * returns `pass: false` with a "judge parse failure" reasoning instead of
37
+ * throwing, so a single flaky model response doesn't halt the workflow.
38
+ */
39
+ export declare function evaluateJudge(evaluator: Evaluator, result: NodeResult, claude: Claude, opts: JudgeOptions): Promise<{
40
+ pass: boolean;
41
+ reasoning?: string;
42
+ }>;
43
+ export {};
@@ -0,0 +1,142 @@
1
+ // ─── Eval: judge (LLM-as-judge) evaluator ────────────────────────────
2
+ //
3
+ // Calls a small Claude model with the node's data, the node's tool-call
4
+ // trace, and the author's rubric. Parses a verdict token and a short
5
+ // reasoning paragraph from a structured response.
6
+ //
7
+ // Cost: one model call per judge evaluator per node attempt. Judges are
8
+ // expected to use a small fast model (default `claude-haiku-4-5`); the
9
+ // reasoning is capped at ~500 chars before it lands on EvalResult so a
10
+ // chatty judge doesn't bloat the trace.
11
+ //
12
+ // Latency: a judge adds one round-trip per node beyond the main LLM call.
13
+ // For a typical 4-node workflow with 2 judges per node, that's 8 extra
14
+ // calls per run. The workflow-level `judge_budget` (default 50) gives
15
+ // authors a load-time warning when the total stretches past expected.
16
+ /**
17
+ * Build the structured judge prompt.
18
+ *
19
+ * The prompt format is fixed so the parser can rely on it. Model authors
20
+ * MUST NOT change the section markers without updating {@link parseJudgeResponse}
21
+ * in lockstep.
22
+ */
23
+ export function buildJudgePrompt(evaluator, result) {
24
+ const passWhen = evaluator.pass_when ?? "yes";
25
+ const failWhen = passWhen === "yes" ? "no" : `not_${passWhen}`;
26
+ return [
27
+ `You are a judge evaluator. Read the rubric, the node's result data, and`,
28
+ `the tool-call trace, then decide whether the result satisfies the rubric.`,
29
+ ``,
30
+ `## Rubric`,
31
+ ``,
32
+ evaluator.rubric ?? "(no rubric provided)",
33
+ ``,
34
+ `## Node result data`,
35
+ ``,
36
+ "```json",
37
+ JSON.stringify(result.data ?? {}, null, 2),
38
+ "```",
39
+ ``,
40
+ `## Tool calls`,
41
+ ``,
42
+ summarizeToolCalls(result.toolCalls ?? []),
43
+ ``,
44
+ `## Output format`,
45
+ ``,
46
+ `Respond with EXACTLY two lines, in this order, and nothing else:`,
47
+ ``,
48
+ `VERDICT: <${passWhen}|${failWhen}>`,
49
+ `REASONING: <one paragraph, max 3 sentences, explaining the verdict>`,
50
+ ``,
51
+ `If the rubric passes vacuously (e.g. a conditional clause does not`,
52
+ `apply to this result), respond with VERDICT: ${passWhen} and a brief`,
53
+ `note in REASONING.`,
54
+ ].join("\n");
55
+ }
56
+ function summarizeToolCalls(calls) {
57
+ if (calls.length === 0)
58
+ return "(no tools were called)";
59
+ return calls
60
+ .map((c) => {
61
+ const status = c.status ?? (looksLikeError(c.output) ? "error" : "success");
62
+ const inputPreview = preview(c.input);
63
+ const outputPreview = preview(c.output);
64
+ return `- ${c.tool} [${status}] input=${inputPreview} output=${outputPreview}`;
65
+ })
66
+ .join("\n");
67
+ }
68
+ function looksLikeError(output) {
69
+ if (!output || typeof output !== "object")
70
+ return false;
71
+ const err = output.error;
72
+ return err !== undefined && err !== null && err !== false;
73
+ }
74
+ /** Compact JSON preview. Caps long values to keep the prompt short. */
75
+ function preview(v) {
76
+ let s;
77
+ try {
78
+ s = JSON.stringify(v);
79
+ }
80
+ catch {
81
+ s = String(v);
82
+ }
83
+ if (!s)
84
+ return "<empty>";
85
+ return s.length > 200 ? s.slice(0, 200) + "…" : s;
86
+ }
87
+ /**
88
+ * Parse a judge response into `{ verdict, reasoning }`.
89
+ *
90
+ * Format expected:
91
+ * VERDICT: <token>
92
+ * REASONING: <text...>
93
+ *
94
+ * Lines may have any leading whitespace. Reasoning may span multiple lines
95
+ * (everything after the `REASONING:` marker is consumed). Returns null
96
+ * fields when the markers are missing.
97
+ */
98
+ export function parseJudgeResponse(text) {
99
+ const verdictMatch = /VERDICT\s*:\s*(\S+)/i.exec(text);
100
+ const reasoningMatch = /REASONING\s*:\s*([\s\S]*)$/i.exec(text);
101
+ const verdict = verdictMatch ? verdictMatch[1].trim().toLowerCase() : null;
102
+ const reasoning = reasoningMatch ? reasoningMatch[1].trim() : null;
103
+ return { verdict, reasoning };
104
+ }
105
+ const PARSE_FAILURE_REASONING = "judge parse failure: model response did not contain a parseable VERDICT line";
106
+ /**
107
+ * Run a single judge evaluator against a node result.
108
+ *
109
+ * Resolves a verdict by calling the judge model, parsing VERDICT/REASONING,
110
+ * and comparing the verdict to `pass_when` (default `yes`). On parse
111
+ * failure, retries the call once. If the retry also fails to parse,
112
+ * returns `pass: false` with a "judge parse failure" reasoning instead of
113
+ * throwing, so a single flaky model response doesn't halt the workflow.
114
+ */
115
+ export async function evaluateJudge(evaluator, result, claude, opts) {
116
+ const passWhen = (evaluator.pass_when ?? "yes").toLowerCase();
117
+ const prompt = buildJudgePrompt(evaluator, result);
118
+ for (let attempt = 0; attempt < 2; attempt++) {
119
+ let response;
120
+ try {
121
+ response = await claude.ask({
122
+ instruction: prompt,
123
+ context: {},
124
+ model: opts.model,
125
+ });
126
+ }
127
+ catch {
128
+ // Treat throws as parse failure for retry purposes; the second attempt
129
+ // either succeeds or surfaces the same path through the same channel.
130
+ continue;
131
+ }
132
+ const parsed = parseJudgeResponse(response);
133
+ if (parsed.verdict === null)
134
+ continue;
135
+ const pass = parsed.verdict === passWhen;
136
+ return {
137
+ pass,
138
+ reasoning: parsed.reasoning ?? undefined,
139
+ };
140
+ }
141
+ return { pass: false, reasoning: PARSE_FAILURE_REASONING };
142
+ }
@@ -0,0 +1,9 @@
1
+ export type Resolution = {
2
+ ok: true;
3
+ mode: "all" | "any";
4
+ values: unknown[];
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ export declare function resolvePath(data: unknown, path: string): Resolution;
@@ -0,0 +1,72 @@
1
+ // ─── Eval: path parser + resolver ────────────────────────────────────
2
+ //
3
+ // Shared by `value` evaluators (operate on `result.data`) and the `requires`
4
+ // pre-condition gate (operates on the cross-node context map). The grammar
5
+ // is documented at https://spec.sweny.ai/nodes/#path-grammar.
6
+ const SEGMENT_RE = /^([a-zA-Z_][a-zA-Z0-9_]*)(\[\*\])?$/;
7
+ const PREFIX_RE = /^([a-z]+):/;
8
+ function parsePath(path) {
9
+ if (path.length === 0)
10
+ return "empty path";
11
+ let mode = "all";
12
+ let body = path;
13
+ const prefixMatch = PREFIX_RE.exec(path);
14
+ if (prefixMatch) {
15
+ const prefix = prefixMatch[1];
16
+ if (prefix !== "all" && prefix !== "any") {
17
+ return `unknown prefix '${prefix}' (expected 'all' or 'any')`;
18
+ }
19
+ mode = prefix;
20
+ body = path.slice(prefixMatch[0].length);
21
+ if (body.length === 0)
22
+ return "empty path after prefix";
23
+ }
24
+ const parts = body.split(".");
25
+ const segments = [];
26
+ for (const part of parts) {
27
+ const m = SEGMENT_RE.exec(part);
28
+ if (!m)
29
+ return `malformed segment '${part}'`;
30
+ segments.push({ name: m[1], wildcard: m[2] === "[*]" });
31
+ }
32
+ return { mode, segments };
33
+ }
34
+ export function resolvePath(data, path) {
35
+ const parsed = parsePath(path);
36
+ if (typeof parsed === "string")
37
+ return { ok: false, reason: parsed };
38
+ let current = [data];
39
+ for (const seg of parsed.segments) {
40
+ const next = [];
41
+ for (const v of current) {
42
+ if (v === null)
43
+ return { ok: false, reason: `null encountered before segment '${seg.name}'` };
44
+ if (typeof v !== "object") {
45
+ return { ok: false, reason: `expected object before segment '${seg.name}', got ${typeof v}` };
46
+ }
47
+ const obj = v;
48
+ if (!(seg.name in obj)) {
49
+ return { ok: false, reason: `missing segment '${seg.name}'` };
50
+ }
51
+ const child = obj[seg.name];
52
+ if (seg.wildcard) {
53
+ if (!Array.isArray(child)) {
54
+ return {
55
+ ok: false,
56
+ reason: `expected array at '${seg.name}', got ${child === null ? "null" : typeof child}`,
57
+ };
58
+ }
59
+ for (const el of child)
60
+ next.push(el);
61
+ }
62
+ else {
63
+ next.push(child);
64
+ }
65
+ }
66
+ current = next;
67
+ if (current.length === 0) {
68
+ return { ok: true, mode: parsed.mode, values: [] };
69
+ }
70
+ }
71
+ return { ok: true, mode: parsed.mode, values: current };
72
+ }
@@ -0,0 +1,15 @@
1
+ import type { EvaluatorRule, OutputMatch } from "../types.js";
2
+ export declare function checkOutputRequired(paths: string[], data: unknown): string | null;
3
+ export declare function checkOutputMatches(matches: OutputMatch[], data: unknown): string | null;
4
+ /**
5
+ * Evaluate a `value` rule against the node's `result.data`.
6
+ *
7
+ * Returns `{ pass: true }` when every declared check passes, otherwise
8
+ * `{ pass: false, reasoning: <semicolon-joined failure list> }`.
9
+ *
10
+ * Function-rule fields (`*_tool_called`) are ignored even if present.
11
+ */
12
+ export declare function evaluateValueRule(rule: EvaluatorRule, data: unknown): {
13
+ pass: boolean;
14
+ reasoning?: string;
15
+ };