@hamza1331/aieval 0.0.0-stage → 0.1.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/checks.js ADDED
@@ -0,0 +1,396 @@
1
+ export const makeFailure = (code, message, severity = "error", options = {}) => ({
2
+ code,
3
+ message,
4
+ severity,
5
+ ...options,
6
+ });
7
+ export const requiredFields = (fields) => ({
8
+ name: "requiredFields",
9
+ description: "Ensures required keys are present in an object output.",
10
+ run: (value, _context) => {
11
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
12
+ return {
13
+ passed: false,
14
+ failures: [
15
+ makeFailure("schema_violation", "Output must be an object for required-field validation.", "error"),
16
+ ],
17
+ warnings: [],
18
+ };
19
+ }
20
+ const missing = fields.filter((field) => !(field in value));
21
+ if (missing.length === 0) {
22
+ return { passed: true, failures: [], warnings: [] };
23
+ }
24
+ return {
25
+ passed: false,
26
+ failures: missing.map((field) => makeFailure("missing_field", `Missing required field: ${field}`, "error", {
27
+ path: field,
28
+ })),
29
+ warnings: [],
30
+ };
31
+ },
32
+ });
33
+ export const noExtraFields = (allowedFields, options = {}) => ({
34
+ name: "noExtraFields",
35
+ description: "Ensures the object does not contain additional keys beyond the allowed set.",
36
+ run: (value, _context) => {
37
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
38
+ return {
39
+ passed: false,
40
+ failures: [
41
+ makeFailure("schema_violation", "Output must be an object for extra-field validation.", "error"),
42
+ ],
43
+ warnings: [],
44
+ };
45
+ }
46
+ const keys = Object.keys(value);
47
+ const unexpected = keys.filter((key) => !allowedFields.includes(key));
48
+ if (unexpected.length === 0) {
49
+ return { passed: true, failures: [], warnings: [] };
50
+ }
51
+ return {
52
+ passed: false,
53
+ failures: unexpected.map((key) => makeFailure("schema_violation", `Unexpected field: ${key}`, options.severity ?? "error", {
54
+ path: key,
55
+ metadata: { allowedFields },
56
+ })),
57
+ warnings: [],
58
+ };
59
+ },
60
+ });
61
+ export const enumCheck = (field, allowedValues) => ({
62
+ name: "enumCheck",
63
+ description: "Ensures a field matches one of a set of allowed values.",
64
+ run: (value, _context) => {
65
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
66
+ return {
67
+ passed: false,
68
+ failures: [
69
+ makeFailure("schema_violation", `Field ${field} cannot be checked because the value is not an object.`, "error"),
70
+ ],
71
+ warnings: [],
72
+ };
73
+ }
74
+ const actual = value[field];
75
+ if (actual === undefined) {
76
+ return {
77
+ passed: false,
78
+ failures: [
79
+ makeFailure("missing_field", `Missing field: ${field}`, "error", {
80
+ path: field,
81
+ }),
82
+ ],
83
+ warnings: [],
84
+ };
85
+ }
86
+ if (allowedValues.includes(actual)) {
87
+ return { passed: true, failures: [], warnings: [] };
88
+ }
89
+ return {
90
+ passed: false,
91
+ failures: [
92
+ makeFailure("invalid_enum", `Field ${field} must be one of: ${allowedValues.join(", ")}`, "error", {
93
+ path: field,
94
+ metadata: { allowedValues, actualValue: actual },
95
+ }),
96
+ ],
97
+ warnings: [],
98
+ };
99
+ },
100
+ });
101
+ export const fieldTypeCheck = (field, expectedType) => ({
102
+ name: "fieldTypeCheck",
103
+ description: "Ensures a field has the expected runtime type.",
104
+ run: (value, _context) => {
105
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
106
+ return {
107
+ passed: false,
108
+ failures: [
109
+ makeFailure("schema_violation", `Field ${field} cannot be checked because the value is not an object.`, "error"),
110
+ ],
111
+ warnings: [],
112
+ };
113
+ }
114
+ const actual = value[field];
115
+ const actualType = actual === null
116
+ ? "object"
117
+ : Array.isArray(actual)
118
+ ? "array"
119
+ : typeof actual;
120
+ if (actualType === expectedType) {
121
+ return { passed: true, failures: [], warnings: [] };
122
+ }
123
+ return {
124
+ passed: false,
125
+ failures: [
126
+ makeFailure("invalid_type", `Field ${field} expected type ${expectedType} but received ${actualType}.`, "error", {
127
+ path: field,
128
+ metadata: { expectedType, actualType },
129
+ }),
130
+ ],
131
+ warnings: [],
132
+ };
133
+ },
134
+ });
135
+ export const schemaCheck = (schema) => ({
136
+ name: "schemaCheck",
137
+ description: "Runs a Zod schema against the output.",
138
+ run: (value, _context) => {
139
+ const result = schema.safeParse(value);
140
+ if (result.success) {
141
+ return { passed: true, failures: [], warnings: [] };
142
+ }
143
+ return {
144
+ passed: false,
145
+ failures: result.error.issues.map((issue) => makeFailure("schema_violation", issue.message, "error", {
146
+ path: issue.path.join("."),
147
+ metadata: { issueCode: issue.code },
148
+ })),
149
+ warnings: [],
150
+ };
151
+ },
152
+ });
153
+ const typeOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
154
+ const resolveToolArgs = (candidate) => {
155
+ const raw = candidate.args ?? candidate.arguments;
156
+ if (raw === undefined) {
157
+ return { args: {} };
158
+ }
159
+ if (typeof raw === "string") {
160
+ try {
161
+ const parsed = JSON.parse(raw);
162
+ if (parsed == null ||
163
+ typeof parsed !== "object" ||
164
+ Array.isArray(parsed)) {
165
+ return {
166
+ failure: makeFailure("tool_call_mismatch", "Tool call arguments must be a JSON object.", "error", {
167
+ path: "arguments",
168
+ }),
169
+ };
170
+ }
171
+ return { args: parsed };
172
+ }
173
+ catch {
174
+ return {
175
+ failure: makeFailure("invalid_json", "Tool call arguments are not valid JSON.", "error", { path: "arguments" }),
176
+ };
177
+ }
178
+ }
179
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
180
+ return {
181
+ failure: makeFailure("tool_call_mismatch", "Tool call args must be an object.", "error", { path: "args" }),
182
+ };
183
+ }
184
+ return { args: raw };
185
+ };
186
+ const validateToolCall = (value, definition) => {
187
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
188
+ return [
189
+ makeFailure("tool_call_mismatch", "Tool call must be an object.", "error"),
190
+ ];
191
+ }
192
+ const candidate = value;
193
+ const failures = [];
194
+ if (candidate.name !== definition.name) {
195
+ failures.push(makeFailure("tool_call_mismatch", `Tool call name must be ${definition.name}.`, "error", {
196
+ path: "name",
197
+ metadata: {
198
+ expectedName: definition.name,
199
+ actualName: candidate.name,
200
+ },
201
+ }));
202
+ }
203
+ const resolved = resolveToolArgs(candidate);
204
+ if ("failure" in resolved) {
205
+ failures.push(resolved.failure);
206
+ return failures;
207
+ }
208
+ const { args } = resolved;
209
+ for (const arg of definition.requiredArgs ?? []) {
210
+ if (!(arg in args)) {
211
+ failures.push(makeFailure("missing_field", `Tool call is missing required argument: ${arg}`, "error", {
212
+ path: `args.${arg}`,
213
+ }));
214
+ }
215
+ }
216
+ const argTypes = definition.argTypes ?? {};
217
+ const known = new Set([
218
+ ...(definition.allowedArgs ?? []),
219
+ ...Object.keys(argTypes),
220
+ ...(definition.requiredArgs ?? []),
221
+ ]);
222
+ if (!definition.allowExtraArgs && known.size > 0) {
223
+ for (const key of Object.keys(args)) {
224
+ if (!known.has(key)) {
225
+ failures.push(makeFailure("schema_violation", `Unexpected tool argument: ${key}`, "error", { path: `args.${key}` }));
226
+ }
227
+ }
228
+ }
229
+ for (const [key, expectedType] of Object.entries(argTypes)) {
230
+ const actual = args[key];
231
+ if (actual === undefined)
232
+ continue;
233
+ const actualType = typeOf(actual);
234
+ if (actualType !== expectedType) {
235
+ failures.push(makeFailure("invalid_type", `Tool argument ${key} expected type ${expectedType} but received ${actualType}.`, "error", {
236
+ path: `args.${key}`,
237
+ metadata: { expectedType, actualType },
238
+ }));
239
+ }
240
+ }
241
+ return failures;
242
+ };
243
+ const resultFrom = (failures) => ({
244
+ passed: failures.length === 0,
245
+ failures,
246
+ warnings: [],
247
+ });
248
+ /**
249
+ * Validates a tool call shaped `{ name, args }` (or `{ name, arguments: "<json>" }`).
250
+ */
251
+ export const toolCallCheck = (definition) => ({
252
+ name: "toolCallCheck",
253
+ description: "Validates tool-call structure, required args, and argument types.",
254
+ run: (value, _context) => resultFrom(validateToolCall(value, definition)),
255
+ });
256
+ /**
257
+ * Validates a tool call against a set of allowed tools. Unknown tool names fail.
258
+ */
259
+ toolCallCheck.oneOf = (definitions) => ({
260
+ name: "toolCallCheck.oneOf",
261
+ description: "Validates a tool call against one of several allowed tool definitions.",
262
+ run: (value, _context) => {
263
+ const name = value != null && typeof value === "object"
264
+ ? value.name
265
+ : undefined;
266
+ const definition = definitions.find((candidate) => candidate.name === name);
267
+ if (!definition) {
268
+ return resultFrom([
269
+ makeFailure("tool_call_mismatch", `Unsupported tool: ${String(name)}`, "error", {
270
+ path: "name",
271
+ metadata: {
272
+ allowedTools: definitions.map((candidate) => candidate.name),
273
+ actualName: name,
274
+ },
275
+ }),
276
+ ]);
277
+ }
278
+ return resultFrom(validateToolCall(value, definition));
279
+ },
280
+ });
281
+ /**
282
+ * Parses a JSON string output and emits `invalid_json` on failure.
283
+ * Use to guard string outputs before running object-level checks.
284
+ */
285
+ export const jsonCheck = () => ({
286
+ name: "jsonCheck",
287
+ description: "Ensures a string output is valid JSON.",
288
+ run: (value, _context) => {
289
+ if (typeof value !== "string") {
290
+ return resultFrom([
291
+ makeFailure("invalid_json", "Output must be a JSON string.", "error"),
292
+ ]);
293
+ }
294
+ try {
295
+ JSON.parse(value);
296
+ return resultFrom([]);
297
+ }
298
+ catch (error) {
299
+ return resultFrom([
300
+ makeFailure("invalid_json", `Output is not valid JSON: ${error instanceof Error ? error.message : "parse error"}`, "error"),
301
+ ]);
302
+ }
303
+ },
304
+ });
305
+ const getField = (value, field) => {
306
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
307
+ return {
308
+ ok: false,
309
+ failure: makeFailure("schema_violation", `Field ${field} cannot be checked because the value is not an object.`, "error"),
310
+ };
311
+ }
312
+ const actual = value[field];
313
+ if (actual === undefined) {
314
+ return {
315
+ ok: false,
316
+ failure: makeFailure("missing_field", `Missing field: ${field}`, "error", { path: field }),
317
+ };
318
+ }
319
+ return { ok: true, actual };
320
+ };
321
+ export const nonEmpty = (field) => ({
322
+ name: "nonEmpty",
323
+ description: "Ensures a field is not an empty string, array, or object.",
324
+ run: (value, _context) => {
325
+ const found = getField(value, field);
326
+ if (!found.ok)
327
+ return resultFrom([found.failure]);
328
+ const { actual } = found;
329
+ const empty = actual === null ||
330
+ (typeof actual === "string" && actual.trim() === "") ||
331
+ (Array.isArray(actual) && actual.length === 0) ||
332
+ (typeof actual === "object" &&
333
+ !Array.isArray(actual) &&
334
+ Object.keys(actual).length === 0);
335
+ return resultFrom(empty
336
+ ? [
337
+ makeFailure("policy_violation", `Field ${field} must not be empty.`, "error", { path: field }),
338
+ ]
339
+ : []);
340
+ },
341
+ });
342
+ export const regexCheck = (field, pattern) => ({
343
+ name: "regexCheck",
344
+ description: "Ensures a string field matches a regular expression.",
345
+ run: (value, _context) => {
346
+ const found = getField(value, field);
347
+ if (!found.ok)
348
+ return resultFrom([found.failure]);
349
+ const { actual } = found;
350
+ if (typeof actual !== "string") {
351
+ return resultFrom([
352
+ makeFailure("invalid_type", `Field ${field} expected type string but received ${typeOf(actual)}.`, "error", {
353
+ path: field,
354
+ }),
355
+ ]);
356
+ }
357
+ // Reset lastIndex so global/sticky patterns behave deterministically.
358
+ pattern.lastIndex = 0;
359
+ return resultFrom(pattern.test(actual)
360
+ ? []
361
+ : [
362
+ makeFailure("policy_violation", `Field ${field} does not match ${pattern}.`, "error", {
363
+ path: field,
364
+ metadata: { pattern: String(pattern) },
365
+ }),
366
+ ]);
367
+ },
368
+ });
369
+ export const rangeCheck = (field, bounds) => ({
370
+ name: "rangeCheck",
371
+ description: "Ensures a numeric field falls within optional min/max bounds.",
372
+ run: (value, _context) => {
373
+ const found = getField(value, field);
374
+ if (!found.ok)
375
+ return resultFrom([found.failure]);
376
+ const { actual } = found;
377
+ if (typeof actual !== "number" || Number.isNaN(actual)) {
378
+ return resultFrom([
379
+ makeFailure("invalid_type", `Field ${field} expected type number but received ${typeOf(actual)}.`, "error", {
380
+ path: field,
381
+ }),
382
+ ]);
383
+ }
384
+ const tooLow = bounds.min !== undefined && actual < bounds.min;
385
+ const tooHigh = bounds.max !== undefined && actual > bounds.max;
386
+ return resultFrom(tooLow || tooHigh
387
+ ? [
388
+ makeFailure("policy_violation", `Field ${field} must be between ${bounds.min ?? "-∞"} and ${bounds.max ?? "∞"}.`, "error", { path: field, metadata: { ...bounds, actualValue: actual } }),
389
+ ]
390
+ : []);
391
+ },
392
+ });
393
+ export const customCheck = (name, fn) => ({
394
+ name,
395
+ run: fn,
396
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cliMain.js";
3
+ const readStdin = () => new Promise((resolvePromise, reject) => {
4
+ let chunk = "";
5
+ process.stdin.setEncoding("utf8");
6
+ process.stdin.on("data", (data) => {
7
+ chunk += data;
8
+ });
9
+ process.stdin.on("end", () => resolvePromise(chunk));
10
+ process.stdin.on("error", reject);
11
+ });
12
+ process.exitCode = await runCli(process.argv.slice(2), {
13
+ stdout: (text) => {
14
+ process.stdout.write(text);
15
+ },
16
+ stderr: (text) => {
17
+ process.stderr.write(text);
18
+ },
19
+ readStdin,
20
+ });
@@ -0,0 +1,7 @@
1
+ export interface CliIO {
2
+ stdout: (text: string) => void;
3
+ stderr: (text: string) => void;
4
+ readStdin: () => Promise<string>;
5
+ }
6
+ /** Runs the CLI in-process and returns the exit code. */
7
+ export declare const runCli: (argv: string[], io: CliIO) => Promise<number>;
@@ -0,0 +1,170 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseArgs } from "node:util";
3
+ import { buildChecks } from "./registry.js";
4
+ import { UsageError, loadFixtures, parseSeverity, runAll, runFixture, } from "./runner.js";
5
+ const USAGE = `Usage:
6
+ aieval run <file|dir>... [--fail-on <severity>] [--format text|json]
7
+ aieval check [output.json] --checks <checks.json> [--fail-on <severity>] [--format text|json]
8
+ aieval --help | --version
9
+
10
+ Commands:
11
+ run Run fixture files ({ name, output, checks, expected? }) or directories of them.
12
+ With "expected" a case is a regression test; without it, the case passes if validation passes.
13
+ check Validate one output (file or stdin) against a JSON array of check specs.
14
+
15
+ Options:
16
+ --fail-on <severity> info | warning | error | critical (default: error)
17
+ --format <format> text (default) or json
18
+ -h, --help Show this help
19
+ -v, --version Show version
20
+
21
+ Exit codes: 0 all passed, 1 at least one case failed, 2 usage or input error.
22
+ `;
23
+ const readVersion = () => {
24
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
25
+ return JSON.parse(raw).version;
26
+ };
27
+ const formatCase = (result) => {
28
+ const lines = [`${result.passed ? "PASS" : "FAIL"} ${result.name}`];
29
+ if (result.mismatch) {
30
+ lines.push(` ! ${result.mismatch}`);
31
+ }
32
+ if (!result.passed || result.mismatch) {
33
+ for (const failure of result.evaluation.failures) {
34
+ lines.push(` - ${failure.code}${failure.path ? ` (${failure.path})` : ""}: ${failure.message}`);
35
+ }
36
+ }
37
+ for (const warning of result.evaluation.warnings) {
38
+ lines.push(` ~ ${warning.code}${warning.path ? ` (${warning.path})` : ""}: ${warning.message}`);
39
+ }
40
+ return lines;
41
+ };
42
+ const formatText = (summary) => {
43
+ const lines = summary.cases.flatMap(formatCase);
44
+ lines.push("", `${summary.total - summary.failed} passed, ${summary.failed} failed, ${summary.total} total`);
45
+ return `${lines.join("\n")}\n`;
46
+ };
47
+ const formatJson = (summary) => `${JSON.stringify({
48
+ passed: summary.passed,
49
+ total: summary.total,
50
+ failed: summary.failed,
51
+ cases: summary.cases.map((result) => ({
52
+ file: result.file,
53
+ name: result.name,
54
+ passed: result.passed,
55
+ mismatch: result.mismatch,
56
+ failures: result.evaluation.failures,
57
+ warnings: result.evaluation.warnings,
58
+ })),
59
+ }, null, 2)}\n`;
60
+ const parseJsonText = (text, label) => {
61
+ if (!text.trim()) {
62
+ throw new UsageError(`No input provided for ${label}.`);
63
+ }
64
+ try {
65
+ return JSON.parse(text);
66
+ }
67
+ catch (error) {
68
+ throw new UsageError(`Invalid JSON in ${label}: ${error instanceof Error ? error.message : "parse error"}`);
69
+ }
70
+ };
71
+ const readFileText = (path) => {
72
+ try {
73
+ return readFileSync(path, "utf8");
74
+ }
75
+ catch {
76
+ throw new UsageError(`Cannot read file: ${path}`);
77
+ }
78
+ };
79
+ /** Runs the CLI in-process and returns the exit code. */
80
+ export const runCli = async (argv, io) => {
81
+ try {
82
+ let parsed;
83
+ try {
84
+ parsed = parseArgs({
85
+ args: argv,
86
+ allowPositionals: true,
87
+ options: {
88
+ "fail-on": { type: "string" },
89
+ format: { type: "string", default: "text" },
90
+ checks: { type: "string" },
91
+ help: { type: "boolean", short: "h" },
92
+ version: { type: "boolean", short: "v" },
93
+ },
94
+ });
95
+ }
96
+ catch (error) {
97
+ throw new UsageError(error instanceof Error ? error.message : String(error));
98
+ }
99
+ const { values, positionals } = parsed;
100
+ if (values.help || (positionals.length === 0 && !values.version)) {
101
+ (values.help ? io.stdout : io.stderr)(USAGE);
102
+ return values.help ? 0 : 2;
103
+ }
104
+ if (values.version) {
105
+ io.stdout(`${readVersion()}\n`);
106
+ return 0;
107
+ }
108
+ const format = values.format;
109
+ if (format !== "text" && format !== "json") {
110
+ throw new UsageError(`Invalid format "${String(format)}". Expected text or json.`);
111
+ }
112
+ const failOn = values["fail-on"] !== undefined
113
+ ? parseSeverity(values["fail-on"])
114
+ : undefined;
115
+ const [command, ...rest] = positionals;
116
+ let summary;
117
+ if (command === "run") {
118
+ if (rest.length === 0) {
119
+ throw new UsageError("`run` needs at least one file or directory.");
120
+ }
121
+ const loaded = loadFixtures(rest);
122
+ if (loaded.length === 0) {
123
+ throw new UsageError("No fixtures found.");
124
+ }
125
+ summary = await runAll(loaded, { failOn });
126
+ }
127
+ else if (command === "check") {
128
+ if (!values.checks) {
129
+ throw new UsageError("`check` requires --checks <checks.json>.");
130
+ }
131
+ if (rest.length > 1) {
132
+ throw new UsageError("`check` accepts at most one output file.");
133
+ }
134
+ const specs = parseJsonText(readFileText(values.checks), values.checks);
135
+ try {
136
+ buildChecks(specs);
137
+ }
138
+ catch (error) {
139
+ throw new UsageError(`${values.checks}: ${error instanceof Error ? error.message : String(error)}`);
140
+ }
141
+ const label = rest[0] ?? "stdin";
142
+ const output = parseJsonText(rest[0] ? readFileText(rest[0]) : await io.readStdin(), label);
143
+ const loaded = {
144
+ file: label,
145
+ index: 0,
146
+ fixture: { name: label, output, checks: specs },
147
+ };
148
+ const result = await runFixture(loaded, { failOn });
149
+ summary = {
150
+ passed: result.passed,
151
+ total: 1,
152
+ failed: result.passed ? 0 : 1,
153
+ cases: [result],
154
+ };
155
+ }
156
+ else {
157
+ throw new UsageError(`Unknown command "${command}".`);
158
+ }
159
+ io.stdout(format === "json" ? formatJson(summary) : formatText(summary));
160
+ return summary.passed ? 0 : 1;
161
+ }
162
+ catch (error) {
163
+ if (error instanceof UsageError) {
164
+ io.stderr(`${error.message}\n\n${USAGE}`);
165
+ return 2;
166
+ }
167
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
168
+ return 2;
169
+ }
170
+ };
@@ -0,0 +1,13 @@
1
+ export * from "./types.js";
2
+ export * from "./checks.js";
3
+ export * from "./registry.js";
4
+ export * from "./runner.js";
5
+ export * from "./semantic.js";
6
+ import type { Check, EvaluationOptions, EvaluationResult } from "./types.js";
7
+ export declare class EvaluationError<T = unknown> extends Error {
8
+ readonly result: EvaluationResult<T>;
9
+ constructor(result: EvaluationResult<T>);
10
+ }
11
+ export declare const evaluate: <T = unknown>({ input, output, checks, metadata, failOn, }: EvaluationOptions<T>) => Promise<EvaluationResult<T>>;
12
+ export declare const validate: <T = unknown>(output: T, checks: Check<T>[], metadata?: Record<string, unknown>) => Promise<EvaluationResult<T>>;
13
+ export declare const assert: <T = unknown>(output: T, checks: Check<T>[], metadata?: Record<string, unknown>) => Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ export * from "./types.js";
2
+ export * from "./checks.js";
3
+ export * from "./registry.js";
4
+ export * from "./runner.js";
5
+ export * from "./semantic.js";
6
+ import { makeFailure } from "./checks.js";
7
+ const SEVERITY_RANK = {
8
+ info: 0,
9
+ warning: 1,
10
+ error: 2,
11
+ critical: 3,
12
+ };
13
+ export class EvaluationError extends Error {
14
+ result;
15
+ constructor(result) {
16
+ super(result.failures.map((failure) => failure.message).join("; ") ||
17
+ "Validation failed");
18
+ this.name = "EvaluationError";
19
+ this.result = result;
20
+ }
21
+ }
22
+ export const evaluate = async ({ input, output, checks, metadata, failOn = "error", }) => {
23
+ const threshold = SEVERITY_RANK[failOn];
24
+ const allFailures = [];
25
+ const allWarnings = [];
26
+ const checkMetadata = {};
27
+ const startedAt = Date.now();
28
+ const withCheckName = (failure, checkName) => ({
29
+ ...failure,
30
+ metadata: { checkName, ...failure.metadata },
31
+ });
32
+ for (const check of checks) {
33
+ let result;
34
+ try {
35
+ result = await check.run(output, { input, output, metadata });
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ allFailures.push(makeFailure("custom_check", `Check "${check.name}" threw: ${message}`, "critical", {
40
+ metadata: { checkName: check.name },
41
+ }));
42
+ continue;
43
+ }
44
+ for (const failure of result.failures) {
45
+ const tagged = withCheckName(failure, check.name);
46
+ (SEVERITY_RANK[failure.severity] >= threshold
47
+ ? allFailures
48
+ : allWarnings).push(tagged);
49
+ }
50
+ for (const warning of result.warnings) {
51
+ allWarnings.push(withCheckName(warning, check.name));
52
+ }
53
+ if (result.metadata) {
54
+ let key = check.name;
55
+ for (let suffix = 2; key in checkMetadata; suffix++) {
56
+ key = `${check.name}#${suffix}`;
57
+ }
58
+ checkMetadata[key] = result.metadata;
59
+ }
60
+ }
61
+ return {
62
+ passed: allFailures.length === 0,
63
+ failures: allFailures,
64
+ warnings: allWarnings,
65
+ output,
66
+ metadata: {
67
+ ...metadata,
68
+ durationMs: Date.now() - startedAt,
69
+ ...(Object.keys(checkMetadata).length > 0
70
+ ? { checks: checkMetadata }
71
+ : {}),
72
+ },
73
+ };
74
+ };
75
+ export const validate = async (output, checks, metadata) => {
76
+ return evaluate({ output, checks, metadata });
77
+ };
78
+ export const assert = async (output, checks, metadata) => {
79
+ const result = await validate(output, checks, metadata);
80
+ if (!result.passed) {
81
+ throw new EvaluationError(result);
82
+ }
83
+ };