@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.
@@ -0,0 +1,46 @@
1
+ import type { Check, Severity } from "./types.js";
2
+ import { type ToolCallDefinition } from "./checks.js";
3
+ /**
4
+ * JSON-serialisable description of a check. Used by the CLI and fixture files.
5
+ * `schemaCheck` is intentionally absent: Zod schemas cannot be serialised.
6
+ */
7
+ export type CheckSpec = {
8
+ type: "requiredFields";
9
+ fields: string[];
10
+ } | {
11
+ type: "noExtraFields";
12
+ allowed: string[];
13
+ severity?: Severity;
14
+ } | {
15
+ type: "enum";
16
+ field: string;
17
+ values: Array<string | number>;
18
+ } | {
19
+ type: "fieldType";
20
+ field: string;
21
+ expected: "string" | "number" | "boolean" | "object" | "array";
22
+ } | ({
23
+ type: "toolCall";
24
+ } & ToolCallDefinition) | {
25
+ type: "toolCallOneOf";
26
+ tools: ToolCallDefinition[];
27
+ } | {
28
+ type: "json";
29
+ } | {
30
+ type: "nonEmpty";
31
+ field: string;
32
+ } | {
33
+ type: "regex";
34
+ field: string;
35
+ pattern: string;
36
+ flags?: string;
37
+ } | {
38
+ type: "range";
39
+ field: string;
40
+ min?: number;
41
+ max?: number;
42
+ };
43
+ type AnyCheck = Check<any>;
44
+ export declare const buildCheck: (spec: unknown, index?: number) => AnyCheck;
45
+ export declare const buildChecks: (specs: unknown) => AnyCheck[];
46
+ export {};
@@ -0,0 +1,82 @@
1
+ import { enumCheck, fieldTypeCheck, jsonCheck, noExtraFields, nonEmpty, rangeCheck, regexCheck, requiredFields, toolCallCheck, } from "./checks.js";
2
+ const fail = (index, message) => {
3
+ throw new Error(`Invalid check spec at index ${index}: ${message}`);
4
+ };
5
+ const requireString = (spec, key, index) => {
6
+ const value = spec[key];
7
+ if (typeof value !== "string" || value === "")
8
+ fail(index, `"${key}" must be a non-empty string.`);
9
+ return value;
10
+ };
11
+ const requireStringArray = (spec, key, index) => {
12
+ const value = spec[key];
13
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
14
+ fail(index, `"${key}" must be an array of strings.`);
15
+ }
16
+ return value;
17
+ };
18
+ export const buildCheck = (spec, index = 0) => {
19
+ if (spec == null || typeof spec !== "object" || Array.isArray(spec)) {
20
+ return fail(index, "spec must be an object.");
21
+ }
22
+ const s = spec;
23
+ switch (s.type) {
24
+ case "requiredFields":
25
+ return requiredFields(requireStringArray(s, "fields", index));
26
+ case "noExtraFields":
27
+ return noExtraFields(requireStringArray(s, "allowed", index), {
28
+ severity: s.severity,
29
+ });
30
+ case "enum": {
31
+ if (!Array.isArray(s.values))
32
+ fail(index, `"values" must be an array.`);
33
+ return enumCheck(requireString(s, "field", index), s.values);
34
+ }
35
+ case "fieldType":
36
+ return fieldTypeCheck(requireString(s, "field", index), requireString(s, "expected", index));
37
+ case "toolCall": {
38
+ const { type: _type, ...definition } = s;
39
+ requireString(s, "name", index);
40
+ return toolCallCheck(definition);
41
+ }
42
+ case "toolCallOneOf": {
43
+ if (!Array.isArray(s.tools) || s.tools.length === 0)
44
+ fail(index, `"tools" must be a non-empty array.`);
45
+ return toolCallCheck.oneOf(s.tools);
46
+ }
47
+ case "json":
48
+ return jsonCheck();
49
+ case "nonEmpty":
50
+ return nonEmpty(requireString(s, "field", index));
51
+ case "regex": {
52
+ const field = requireString(s, "field", index);
53
+ const pattern = requireString(s, "pattern", index);
54
+ try {
55
+ return regexCheck(field, new RegExp(pattern, typeof s.flags === "string" ? s.flags : undefined));
56
+ }
57
+ catch (error) {
58
+ return fail(index, `invalid regex: ${error instanceof Error ? error.message : String(error)}`);
59
+ }
60
+ }
61
+ case "range": {
62
+ const min = s.min;
63
+ const max = s.max;
64
+ if ((min !== undefined && typeof min !== "number") ||
65
+ (max !== undefined && typeof max !== "number")) {
66
+ fail(index, `"min" and "max" must be numbers.`);
67
+ }
68
+ return rangeCheck(requireString(s, "field", index), {
69
+ min: min,
70
+ max: max,
71
+ });
72
+ }
73
+ default:
74
+ return fail(index, `unknown check type "${String(s.type)}".`);
75
+ }
76
+ };
77
+ export const buildChecks = (specs) => {
78
+ if (!Array.isArray(specs)) {
79
+ throw new Error("Checks must be an array of check specs.");
80
+ }
81
+ return specs.map((spec, index) => buildCheck(spec, index));
82
+ };
@@ -0,0 +1,47 @@
1
+ import type { EvaluationResult, Severity } from "./types.js";
2
+ /** Thrown for problems with how the runner was invoked or with fixture contents. Maps to CLI exit code 2. */
3
+ export declare class UsageError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface FixtureExpectation {
7
+ passed: boolean;
8
+ codes?: string[];
9
+ warningCodes?: string[];
10
+ }
11
+ export interface Fixture {
12
+ name?: string;
13
+ output: unknown;
14
+ checks: unknown[];
15
+ failOn?: Severity;
16
+ /** When present the case is a regression test: the evaluation must match this expectation. */
17
+ expected?: FixtureExpectation;
18
+ }
19
+ export interface LoadedFixture {
20
+ file: string;
21
+ index: number;
22
+ fixture: Fixture;
23
+ }
24
+ export interface CaseResult {
25
+ file: string;
26
+ name: string;
27
+ /** Whether the case itself succeeded (matched `expected`, or the evaluation passed when no `expected`). */
28
+ passed: boolean;
29
+ evaluation: EvaluationResult;
30
+ mismatch?: string;
31
+ }
32
+ export interface RunSummary {
33
+ passed: boolean;
34
+ total: number;
35
+ failed: number;
36
+ cases: CaseResult[];
37
+ }
38
+ export declare const parseSeverity: (value: string) => Severity;
39
+ /** Loads fixtures from files and/or directories. Directories are scanned non-recursively for `*.json`. */
40
+ export declare const loadFixtures: (paths: string[]) => LoadedFixture[];
41
+ /** Runs a single fixture. `failOn` (if given) overrides the fixture's own value. */
42
+ export declare const runFixture: (loaded: LoadedFixture, options?: {
43
+ failOn?: Severity;
44
+ }) => Promise<CaseResult>;
45
+ export declare const runAll: (loaded: LoadedFixture[], options?: {
46
+ failOn?: Severity;
47
+ }) => Promise<RunSummary>;
package/dist/runner.js ADDED
@@ -0,0 +1,143 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { evaluate } from "./index.js";
4
+ import { buildChecks } from "./registry.js";
5
+ /** Thrown for problems with how the runner was invoked or with fixture contents. Maps to CLI exit code 2. */
6
+ export class UsageError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "UsageError";
10
+ }
11
+ }
12
+ const SEVERITIES = ["info", "warning", "error", "critical"];
13
+ export const parseSeverity = (value) => {
14
+ if (!SEVERITIES.includes(value)) {
15
+ throw new UsageError(`Invalid severity "${value}". Expected one of: ${SEVERITIES.join(", ")}.`);
16
+ }
17
+ return value;
18
+ };
19
+ const readJson = (file) => {
20
+ let raw;
21
+ try {
22
+ raw = readFileSync(file, "utf8");
23
+ }
24
+ catch {
25
+ throw new UsageError(`Cannot read file: ${file}`);
26
+ }
27
+ try {
28
+ return JSON.parse(raw);
29
+ }
30
+ catch (error) {
31
+ throw new UsageError(`Invalid JSON in ${file}: ${error instanceof Error ? error.message : "parse error"}`);
32
+ }
33
+ };
34
+ const validateFixture = (value, file, index) => {
35
+ const where = `${file}${index > 0 ? ` (case ${index + 1})` : ""}`;
36
+ if (value == null || typeof value !== "object" || Array.isArray(value)) {
37
+ throw new UsageError(`Fixture in ${where} must be an object.`);
38
+ }
39
+ const candidate = value;
40
+ if (!("output" in candidate)) {
41
+ throw new UsageError(`Fixture in ${where} is missing "output".`);
42
+ }
43
+ if (!Array.isArray(candidate.checks)) {
44
+ throw new UsageError(`Fixture in ${where} must have a "checks" array.`);
45
+ }
46
+ if (candidate.failOn !== undefined) {
47
+ parseSeverity(String(candidate.failOn));
48
+ }
49
+ if (candidate.expected !== undefined) {
50
+ const expected = candidate.expected;
51
+ if (expected == null ||
52
+ typeof expected !== "object" ||
53
+ typeof expected.passed !== "boolean") {
54
+ throw new UsageError(`Fixture in ${where} has an invalid "expected" (needs a boolean "passed").`);
55
+ }
56
+ }
57
+ return candidate;
58
+ };
59
+ /** Loads fixtures from files and/or directories. Directories are scanned non-recursively for `*.json`. */
60
+ export const loadFixtures = (paths) => {
61
+ const files = [];
62
+ for (const path of paths) {
63
+ const absolute = resolve(path);
64
+ let stats;
65
+ try {
66
+ stats = statSync(absolute);
67
+ }
68
+ catch {
69
+ throw new UsageError(`Path not found: ${path}`);
70
+ }
71
+ if (stats.isDirectory()) {
72
+ const entries = readdirSync(absolute)
73
+ .filter((entry) => entry.endsWith(".json"))
74
+ .sort()
75
+ .map((entry) => join(absolute, entry));
76
+ files.push(...entries);
77
+ }
78
+ else {
79
+ files.push(absolute);
80
+ }
81
+ }
82
+ const loaded = [];
83
+ for (const file of files) {
84
+ const data = readJson(file);
85
+ const items = Array.isArray(data) ? data : [data];
86
+ items.forEach((item, index) => {
87
+ loaded.push({ file, index, fixture: validateFixture(item, file, index) });
88
+ });
89
+ }
90
+ return loaded;
91
+ };
92
+ const sameCodes = (actual, expected) => JSON.stringify([...actual].sort()) === JSON.stringify([...expected].sort());
93
+ /** Runs a single fixture. `failOn` (if given) overrides the fixture's own value. */
94
+ export const runFixture = async (loaded, options = {}) => {
95
+ const { fixture, file, index } = loaded;
96
+ const name = fixture.name ?? `${file}${index > 0 ? `#${index + 1}` : ""}`;
97
+ let checks;
98
+ try {
99
+ checks = buildChecks(fixture.checks);
100
+ }
101
+ catch (error) {
102
+ throw new UsageError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ const evaluation = await evaluate({
105
+ output: fixture.output,
106
+ checks,
107
+ failOn: options.failOn ?? fixture.failOn,
108
+ });
109
+ if (!fixture.expected) {
110
+ return { file, name, passed: evaluation.passed, evaluation };
111
+ }
112
+ const { expected } = fixture;
113
+ const problems = [];
114
+ if (evaluation.passed !== expected.passed) {
115
+ problems.push(`expected passed=${expected.passed} but got passed=${evaluation.passed}`);
116
+ }
117
+ const actualCodes = evaluation.failures.map((failure) => failure.code);
118
+ const expectedCodes = expected.codes ?? [];
119
+ if (!sameCodes(actualCodes, expectedCodes)) {
120
+ problems.push(`expected failure codes [${expectedCodes.join(", ")}] but got [${actualCodes.join(", ")}]`);
121
+ }
122
+ if (expected.warningCodes) {
123
+ const actualWarnings = evaluation.warnings.map((warning) => warning.code);
124
+ if (!sameCodes(actualWarnings, expected.warningCodes)) {
125
+ problems.push(`expected warning codes [${expected.warningCodes.join(", ")}] but got [${actualWarnings.join(", ")}]`);
126
+ }
127
+ }
128
+ return {
129
+ file,
130
+ name,
131
+ passed: problems.length === 0,
132
+ evaluation,
133
+ mismatch: problems.length > 0 ? problems.join("; ") : undefined,
134
+ };
135
+ };
136
+ export const runAll = async (loaded, options = {}) => {
137
+ const cases = [];
138
+ for (const item of loaded) {
139
+ cases.push(await runFixture(item, options));
140
+ }
141
+ const failed = cases.filter((result) => !result.passed).length;
142
+ return { passed: failed === 0, total: cases.length, failed, cases };
143
+ };
@@ -0,0 +1,36 @@
1
+ import type { Check, Severity } from "./types.js";
2
+ export interface JudgeRequest {
3
+ input: unknown;
4
+ output: unknown;
5
+ criteria: string;
6
+ }
7
+ export interface JudgeVerdict {
8
+ /** Score between 0 and 1 inclusive. */
9
+ score: number;
10
+ reasoning?: string;
11
+ metadata?: Record<string, unknown>;
12
+ }
13
+ /** A caller-supplied function that scores an output against criteria, typically by calling an LLM. */
14
+ export type SemanticJudge = (request: JudgeRequest) => Promise<JudgeVerdict> | JudgeVerdict;
15
+ export interface SemanticCheckOptions {
16
+ criteria: string;
17
+ /** Minimum passing score. Defaults to 0.5. */
18
+ threshold?: number;
19
+ /** Severity of the failure when the score is below the threshold. Defaults to "warning" (advisory). */
20
+ severity?: Severity;
21
+ /** Check name used in failures and result metadata. Defaults to "semanticCheck". */
22
+ name?: string;
23
+ }
24
+ /**
25
+ * Optional LLM-judged check. Scores are advisory by default: a low score is reported as a warning
26
+ * unless `severity` is raised or `failOn` is lowered. Results are tagged `metadata.kind = "semantic"`.
27
+ */
28
+ export declare const semanticCheck: <T = unknown>(judge: SemanticJudge, options: SemanticCheckOptions) => Check<T>;
29
+ export interface MockJudge extends SemanticJudge {
30
+ calls: JudgeRequest[];
31
+ }
32
+ /**
33
+ * Deterministic judge for tests and examples. Accepts a fixed score, a sequence of scores
34
+ * (the last one repeats), or a function mapping a request to a score or verdict.
35
+ */
36
+ export declare const mockJudge: (scores: number | number[] | ((request: JudgeRequest) => number | JudgeVerdict)) => MockJudge;
@@ -0,0 +1,74 @@
1
+ import { makeFailure } from "./checks.js";
2
+ /**
3
+ * Optional LLM-judged check. Scores are advisory by default: a low score is reported as a warning
4
+ * unless `severity` is raised or `failOn` is lowered. Results are tagged `metadata.kind = "semantic"`.
5
+ */
6
+ export const semanticCheck = (judge, options) => {
7
+ const threshold = options.threshold ?? 0.5;
8
+ const severity = options.severity ?? "warning";
9
+ return {
10
+ name: options.name ?? "semanticCheck",
11
+ description: `Semantic judge: ${options.criteria}`,
12
+ run: async (value, context) => {
13
+ const verdict = await judge({
14
+ input: context.input,
15
+ output: value,
16
+ criteria: options.criteria,
17
+ });
18
+ if (verdict == null ||
19
+ typeof verdict.score !== "number" ||
20
+ !Number.isFinite(verdict.score) ||
21
+ verdict.score < 0 ||
22
+ verdict.score > 1) {
23
+ return {
24
+ passed: false,
25
+ failures: [
26
+ makeFailure("custom_check", `Semantic judge returned an invalid score: ${String(verdict?.score)}. Expected a number between 0 and 1.`, "critical", {
27
+ metadata: { kind: "semantic", criteria: options.criteria },
28
+ }),
29
+ ],
30
+ warnings: [],
31
+ metadata: { kind: "semantic", criteria: options.criteria },
32
+ };
33
+ }
34
+ const metadata = {
35
+ kind: "semantic",
36
+ score: verdict.score,
37
+ threshold,
38
+ criteria: options.criteria,
39
+ reasoning: verdict.reasoning,
40
+ ...verdict.metadata,
41
+ };
42
+ if (verdict.score >= threshold) {
43
+ return { passed: true, failures: [], warnings: [], metadata };
44
+ }
45
+ return {
46
+ passed: false,
47
+ failures: [
48
+ makeFailure("unsupported_claim", `Semantic score ${verdict.score} is below threshold ${threshold} for: ${options.criteria}`, severity, { metadata }),
49
+ ],
50
+ warnings: [],
51
+ metadata,
52
+ };
53
+ },
54
+ };
55
+ };
56
+ /**
57
+ * Deterministic judge for tests and examples. Accepts a fixed score, a sequence of scores
58
+ * (the last one repeats), or a function mapping a request to a score or verdict.
59
+ */
60
+ export const mockJudge = (scores) => {
61
+ const calls = [];
62
+ const judge = ((request) => {
63
+ const index = calls.length;
64
+ calls.push(request);
65
+ const raw = typeof scores === "function"
66
+ ? scores(request)
67
+ : Array.isArray(scores)
68
+ ? scores[Math.min(index, scores.length - 1)]
69
+ : scores;
70
+ return typeof raw === "number" ? { score: raw } : raw;
71
+ });
72
+ judge.calls = calls;
73
+ return judge;
74
+ };
@@ -0,0 +1,42 @@
1
+ export type Severity = "info" | "warning" | "error" | "critical";
2
+ export type FailureCode = "missing_field" | "invalid_type" | "invalid_enum" | "schema_violation" | "invalid_json" | "tool_call_mismatch" | "unsupported_claim" | "policy_violation" | "custom_check" | "unknown";
3
+ export interface Failure {
4
+ code: FailureCode;
5
+ message: string;
6
+ severity: Severity;
7
+ path?: string;
8
+ metadata?: Record<string, unknown>;
9
+ suggestedRepair?: string;
10
+ }
11
+ export interface CheckResult {
12
+ passed: boolean;
13
+ failures: Failure[];
14
+ warnings: Failure[];
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export type CheckContext = {
18
+ input?: unknown;
19
+ output?: unknown;
20
+ metadata?: Record<string, unknown>;
21
+ };
22
+ export type CheckFn<T = unknown> = (value: T, context: CheckContext) => CheckResult | Promise<CheckResult>;
23
+ export interface Check<T = unknown> {
24
+ name: string;
25
+ description?: string;
26
+ run: CheckFn<T>;
27
+ }
28
+ export interface EvaluationOptions<T = unknown> {
29
+ input?: unknown;
30
+ output?: T;
31
+ checks: Check<T>[];
32
+ metadata?: Record<string, unknown>;
33
+ /** Minimum severity that counts as a failure. Lower severities become warnings. Defaults to "error". */
34
+ failOn?: Severity;
35
+ }
36
+ export interface EvaluationResult<T = unknown> {
37
+ passed: boolean;
38
+ failures: Failure[];
39
+ warnings: Failure[];
40
+ output?: T;
41
+ metadata?: Record<string, unknown>;
42
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,79 @@
1
1
  {
2
2
  "name": "@hamza1331/aieval",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "TypeScript-first validation toolkit for LLM outputs and agent actions.",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/hamza1331/aieval.git"
11
+ },
12
+ "homepage": "https://github.com/hamza1331/aieval#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/hamza1331/aieval/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "bin": {
20
+ "aieval": "dist/cli.js"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ },
27
+ "./cli": {
28
+ "import": "./dist/cli.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE",
36
+ "CHANGELOG.md"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "typecheck": "tsc -p tsconfig.test.json",
41
+ "lint": "eslint .",
42
+ "format": "prettier --write .",
43
+ "format:check": "prettier --check .",
44
+ "test": "vitest run",
45
+ "test:coverage": "vitest run --coverage",
46
+ "dev": "vitest",
47
+ "ci": "npm run lint && npm run format:check && npm run typecheck && npm run build && npm run test:coverage",
48
+ "prepublishOnly": "npm run build && npm test"
49
+ },
50
+ "keywords": [
51
+ "ai",
52
+ "llm",
53
+ "evaluation",
54
+ "validation",
55
+ "agents",
56
+ "typescript"
57
+ ],
58
+ "author": "Hamza Ali <hamxa1331@gmail.com>",
59
+ "license": "MIT",
60
+ "sideEffects": false,
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "peerDependencies": {
65
+ "zod": "^4.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@eslint/js": "^10.0.1",
69
+ "@types/node": "^24.0.0",
70
+ "@vitest/coverage-v8": "^3.2.7",
71
+ "eslint": "^10.12.0",
72
+ "prettier": "^3.9.9",
73
+ "tsx": "^4.23.15",
74
+ "typescript": "^5.8.3",
75
+ "typescript-eslint": "^8.71.1",
76
+ "vitest": "^3.2.4",
77
+ "zod": "^4.6.5"
78
+ }
79
+ }