@alisio/core 0.5.2 → 0.7.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/analysis/janitor.d.ts +48 -0
- package/dist/analysis/janitor.js +170 -18
- package/dist/analysis/jobs.d.ts +11 -2
- package/dist/analysis/jobs.js +13 -0
- package/dist/analysis/oci.d.ts +11 -2
- package/dist/analysis/oci.js +20 -4
- package/dist/analysis/python/sources.js +2 -1
- package/dist/analysis/rerun.d.ts +1 -1
- package/dist/analysis/status.d.ts +3 -1
- package/dist/analysis/status.js +3 -0
- package/dist/application.d.ts +28 -2
- package/dist/application.js +189 -4
- package/dist/changelog/data.js +42 -0
- package/dist/config.d.ts +110 -2
- package/dist/config.js +188 -6
- package/dist/core/call-internals.d.ts +22 -0
- package/dist/core/call-internals.js +9 -0
- package/dist/core/contracts.d.ts +2 -2
- package/dist/core/runner.js +111 -38
- package/dist/eval/canonical.d.ts +14 -0
- package/dist/eval/canonical.js +40 -0
- package/dist/eval/cassette.d.ts +180 -0
- package/dist/eval/cassette.js +424 -0
- package/dist/eval/compare.d.ts +147 -0
- package/dist/eval/compare.js +436 -0
- package/dist/eval/engine.d.ts +144 -0
- package/dist/eval/engine.js +728 -0
- package/dist/eval/export.d.ts +40 -0
- package/dist/eval/export.js +90 -0
- package/dist/eval/fingerprint.d.ts +88 -0
- package/dist/eval/fingerprint.js +186 -0
- package/dist/eval/normalize.d.ts +68 -0
- package/dist/eval/normalize.js +138 -0
- package/dist/eval/paths.d.ts +11 -0
- package/dist/eval/paths.js +50 -0
- package/dist/eval/plan.d.ts +26 -0
- package/dist/eval/plan.js +44 -0
- package/dist/eval/report.d.ts +53 -0
- package/dist/eval/report.js +258 -0
- package/dist/eval/run-suite.d.ts +68 -0
- package/dist/eval/run-suite.js +94 -0
- package/dist/eval/secrets.d.ts +12 -0
- package/dist/eval/secrets.js +45 -0
- package/dist/eval/stats.d.ts +17 -0
- package/dist/eval/stats.js +50 -0
- package/dist/eval/suite.d.ts +20 -0
- package/dist/eval/suite.js +165 -0
- package/dist/eval/variants.d.ts +35 -0
- package/dist/eval/variants.js +137 -0
- package/dist/eval/verifiers.d.ts +49 -0
- package/dist/eval/verifiers.js +264 -0
- package/dist/forge/broker.d.ts +51 -0
- package/dist/forge/broker.js +244 -0
- package/dist/forge/budget.d.ts +27 -0
- package/dist/forge/budget.js +60 -0
- package/dist/forge/catalog-view.d.ts +24 -0
- package/dist/forge/catalog-view.js +43 -0
- package/dist/forge/errors.d.ts +26 -0
- package/dist/forge/errors.js +49 -0
- package/dist/forge/guides/javascript.d.ts +25 -0
- package/dist/forge/guides/javascript.js +90 -0
- package/dist/forge/guides/provider.d.ts +42 -0
- package/dist/forge/guides/provider.js +96 -0
- package/dist/forge/guides/python-environment.d.ts +15 -0
- package/dist/forge/guides/python-environment.js +81 -0
- package/dist/forge/guides/python.d.ts +38 -0
- package/dist/forge/guides/python.js +170 -0
- package/dist/forge/javascript/engine.d.ts +44 -0
- package/dist/forge/javascript/engine.js +229 -0
- package/dist/forge/javascript/hosts/factory.d.ts +25 -0
- package/dist/forge/javascript/hosts/factory.js +21 -0
- package/dist/forge/javascript/hosts/framing.d.ts +17 -0
- package/dist/forge/javascript/hosts/framing.js +49 -0
- package/dist/forge/javascript/hosts/isolate-main.d.ts +9 -0
- package/dist/forge/javascript/hosts/isolate-main.js +63 -0
- package/dist/forge/javascript/hosts/process-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/process-entry.js +3 -0
- package/dist/forge/javascript/hosts/process-host.d.ts +33 -0
- package/dist/forge/javascript/hosts/process-host.js +160 -0
- package/dist/forge/javascript/hosts/worker-entry.d.ts +1 -0
- package/dist/forge/javascript/hosts/worker-entry.js +3 -0
- package/dist/forge/javascript/hosts/worker-host.d.ts +27 -0
- package/dist/forge/javascript/hosts/worker-host.js +116 -0
- package/dist/forge/javascript/prelude.d.ts +2 -0
- package/dist/forge/javascript/prelude.js +333 -0
- package/dist/forge/javascript/probe.d.ts +2 -0
- package/dist/forge/javascript/probe.js +53 -0
- package/dist/forge/javascript/runtime-manifest.d.ts +14 -0
- package/dist/forge/javascript/runtime-manifest.js +14 -0
- package/dist/forge/javascript/vm-runner.d.ts +21 -0
- package/dist/forge/javascript/vm-runner.js +301 -0
- package/dist/forge/javascript/wasm-asset.d.ts +8 -0
- package/dist/forge/javascript/wasm-asset.js +8 -0
- package/dist/forge/javascript/wasm.d.ts +15 -0
- package/dist/forge/javascript/wasm.js +54 -0
- package/dist/forge/journal.d.ts +105 -0
- package/dist/forge/journal.js +174 -0
- package/dist/forge/kernel.d.ts +76 -0
- package/dist/forge/kernel.js +339 -0
- package/dist/forge/limits.d.ts +38 -0
- package/dist/forge/limits.js +73 -0
- package/dist/forge/nested.d.ts +96 -0
- package/dist/forge/nested.js +214 -0
- package/dist/forge/ports.d.ts +120 -0
- package/dist/forge/ports.js +1 -0
- package/dist/forge/progress.d.ts +24 -0
- package/dist/forge/progress.js +76 -0
- package/dist/forge/protocol.d.ts +110 -0
- package/dist/forge/protocol.js +171 -0
- package/dist/forge/python/backend-selector.d.ts +38 -0
- package/dist/forge/python/backend-selector.js +111 -0
- package/dist/forge/python/backend.d.ts +89 -0
- package/dist/forge/python/backend.js +1 -0
- package/dist/forge/python/engine.d.ts +84 -0
- package/dist/forge/python/engine.js +279 -0
- package/dist/forge/python/frame-parser.d.ts +24 -0
- package/dist/forge/python/frame-parser.js +107 -0
- package/dist/forge/python/frame-transport.d.ts +40 -0
- package/dist/forge/python/frame-transport.js +101 -0
- package/dist/forge/python/import-scan.d.ts +22 -0
- package/dist/forge/python/import-scan.js +133 -0
- package/dist/forge/python/managed-backend.d.ts +30 -0
- package/dist/forge/python/managed-backend.js +111 -0
- package/dist/forge/python/oci-backend.d.ts +27 -0
- package/dist/forge/python/oci-backend.js +84 -0
- package/dist/forge/python/pyodide-backend.d.ts +40 -0
- package/dist/forge/python/pyodide-backend.js +289 -0
- package/dist/forge/python/pyodide-install.d.ts +41 -0
- package/dist/forge/python/pyodide-install.js +167 -0
- package/dist/forge/python/pyodide-manifest.d.ts +42 -0
- package/dist/forge/python/pyodide-manifest.js +376 -0
- package/dist/forge/python/pyodide-runner-source.d.ts +2 -0
- package/dist/forge/python/pyodide-runner-source.js +3 -0
- package/dist/forge/python/rss.d.ts +2 -0
- package/dist/forge/python/rss.js +33 -0
- package/dist/forge/python/scratch-copy.d.ts +6 -0
- package/dist/forge/python/scratch-copy.js +61 -0
- package/dist/forge/python/settle.d.ts +6 -0
- package/dist/forge/python/settle.js +21 -0
- package/dist/forge/python/status.d.ts +8 -0
- package/dist/forge/python/status.js +15 -0
- package/dist/forge/python/wasm-node.d.ts +59 -0
- package/dist/forge/python/wasm-node.js +220 -0
- package/dist/forge/python/wiring.d.ts +39 -0
- package/dist/forge/python/wiring.js +47 -0
- package/dist/forge/registry.d.ts +26 -0
- package/dist/forge/registry.js +83 -0
- package/dist/forge/result.d.ts +58 -0
- package/dist/forge/result.js +86 -0
- package/dist/forge/routing-rule.d.ts +19 -0
- package/dist/forge/routing-rule.js +31 -0
- package/dist/forge/runtime.d.ts +64 -0
- package/dist/forge/runtime.js +88 -0
- package/dist/forge/scratch-files.d.ts +36 -0
- package/dist/forge/scratch-files.js +94 -0
- package/dist/forge/scratch-provider.d.ts +41 -0
- package/dist/forge/scratch-provider.js +79 -0
- package/dist/forge/scratch-quota.d.ts +8 -0
- package/dist/forge/scratch-quota.js +25 -0
- package/dist/forge/selector.d.ts +35 -0
- package/dist/forge/selector.js +51 -0
- package/dist/forge/session-dir.d.ts +59 -0
- package/dist/forge/session-dir.js +239 -0
- package/dist/forge/session-path.d.ts +31 -0
- package/dist/forge/session-path.js +61 -0
- package/dist/forge/slots.d.ts +16 -0
- package/dist/forge/slots.js +47 -0
- package/dist/forge/state-delta.d.ts +54 -0
- package/dist/forge/state-delta.js +99 -0
- package/dist/forge/state-machine.d.ts +20 -0
- package/dist/forge/state-machine.js +40 -0
- package/dist/forge/state-store.d.ts +25 -0
- package/dist/forge/state-store.js +50 -0
- package/dist/forge/task-log.d.ts +9 -0
- package/dist/forge/task-log.js +42 -0
- package/dist/forge/verdict.d.ts +20 -0
- package/dist/forge/verdict.js +28 -0
- package/dist/forge-isolate.d.ts +6 -0
- package/dist/forge-isolate.js +6 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +23 -1
- package/dist/permissions/decide.d.ts +42 -0
- package/dist/permissions/decide.js +24 -0
- package/dist/plan/exit-plan.js +2 -1
- package/dist/resources/context.d.ts +2 -1
- package/dist/resources/context.js +2 -1
- package/dist/runtime/access.d.ts +34 -0
- package/dist/runtime/access.js +80 -0
- package/dist/runtime/ids.d.ts +21 -0
- package/dist/runtime/ids.js +41 -1
- package/dist/runtime/process.d.ts +2 -1
- package/dist/runtime/process.js +5 -5
- package/dist/runtime/store.d.ts +11 -0
- package/dist/runtime/store.js +106 -2
- package/dist/tools/analysis.d.ts +25 -2
- package/dist/tools/analysis.js +315 -267
- package/dist/tools/execute.js +6 -7
- package/dist/tools/forge-guide.d.ts +6 -0
- package/dist/tools/forge-guide.js +60 -0
- package/dist/tools/js-run.d.ts +19 -0
- package/dist/tools/js-run.js +126 -0
- package/dist/tools/standard.d.ts +7 -1
- package/dist/tools/standard.js +44 -43
- package/package.json +7 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Pure statistics of the reliability lab (specs/010 D6, D17). No I/O, no dependencies. */
|
|
2
|
+
/** z of a two-sided 95 % interval. */
|
|
3
|
+
const Z95 = 1.96;
|
|
4
|
+
/**
|
|
5
|
+
* Wilson score interval of a proportion (default 95 %). Unlike the normal approximation it stays
|
|
6
|
+
* inside [0, 1] and is informative at 0/n and n/n. With no trials it carries no information.
|
|
7
|
+
*/
|
|
8
|
+
export function wilsonInterval(successes, trials, z = Z95) {
|
|
9
|
+
if (!Number.isInteger(trials) || trials < 0)
|
|
10
|
+
throw new RangeError(`trials must be a non-negative integer, got ${trials}.`);
|
|
11
|
+
if (!Number.isInteger(successes) || successes < 0 || successes > trials)
|
|
12
|
+
throw new RangeError(`successes must be an integer in [0, ${trials}], got ${successes}.`);
|
|
13
|
+
if (trials === 0)
|
|
14
|
+
return { low: 0, high: 1 };
|
|
15
|
+
const p = successes / trials;
|
|
16
|
+
const z2 = z * z;
|
|
17
|
+
const denominator = 1 + z2 / trials;
|
|
18
|
+
const centre = (p + z2 / (2 * trials)) / denominator;
|
|
19
|
+
const half = (z * Math.sqrt((p * (1 - p)) / trials + z2 / (4 * trials * trials))) / denominator;
|
|
20
|
+
// Floating point can leave 1e-17 residue at the extremes; the bounds there are exact.
|
|
21
|
+
return {
|
|
22
|
+
low: successes === 0 ? 0 : Math.max(0, centre - half),
|
|
23
|
+
high: successes === trials ? 1 : Math.min(1, centre + half),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Median; null for an empty set. Does not mutate the input. */
|
|
27
|
+
export function median(values) {
|
|
28
|
+
if (values.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
31
|
+
const mid = Math.floor(sorted.length / 2);
|
|
32
|
+
return sorted.length % 2
|
|
33
|
+
? sorted[mid]
|
|
34
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
35
|
+
}
|
|
36
|
+
/** Sample variance (n - 1), the same convention as `scripts/eval-baseline.ts`; null below two samples. */
|
|
37
|
+
export function sampleVariance(values) {
|
|
38
|
+
if (values.length < 2)
|
|
39
|
+
return null;
|
|
40
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
41
|
+
return values.reduce((a, b) => a + (b - mean) ** 2, 0) / (values.length - 1);
|
|
42
|
+
}
|
|
43
|
+
/** pass@k: at least one of the k repetitions succeeded. No repetitions is not a pass. */
|
|
44
|
+
export function passAtK(results) {
|
|
45
|
+
return results.some(Boolean);
|
|
46
|
+
}
|
|
47
|
+
/** pass^k: every one of the k repetitions succeeded (consistency). No repetitions is not a pass. */
|
|
48
|
+
export function passPowK(results) {
|
|
49
|
+
return results.length > 0 && results.every(Boolean);
|
|
50
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Loader and validator of reliability-lab suites (specs/010 D1). */
|
|
2
|
+
import type { EvalSuite } from "@alisio/sdk";
|
|
3
|
+
export declare class EvalSuiteError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface LoadedSuite {
|
|
7
|
+
suite: EvalSuite;
|
|
8
|
+
/** Absolute suite directory. */
|
|
9
|
+
dir: string;
|
|
10
|
+
/** Absolute fixture directory per task id. */
|
|
11
|
+
fixtureDirs: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
/** Upper bound of a `command` verifier timeout: a suite cannot park a run for hours. */
|
|
14
|
+
export declare const MAX_VERIFIER_TIMEOUT_MS = 600000;
|
|
15
|
+
/**
|
|
16
|
+
* Reads `<dir>/suite.json` and proves the suite is internally consistent: schema, unique ids,
|
|
17
|
+
* existing fixture directories and every path confined to the suite (or, for verifiers, to the
|
|
18
|
+
* task working directory). Every failure names the task, the field and how to fix it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadSuite(dir: string): LoadedSuite;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** Loader and validator of reliability-lab suites (specs/010 D1). */
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { EvalPathError, resolveInside, unsafeRelativeReason } from "./paths.js";
|
|
6
|
+
export class EvalSuiteError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "EvalSuiteError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
13
|
+
/** Upper bound of a `command` verifier timeout: a suite cannot park a run for hours. */
|
|
14
|
+
export const MAX_VERIFIER_TIMEOUT_MS = 600_000;
|
|
15
|
+
/** Effects a task policy may add to its mode; MCP and agents are never grantable by a suite. */
|
|
16
|
+
const TASK_ALLOW = ["write", "process", "external", "analysis"];
|
|
17
|
+
const id = z.string().min(1);
|
|
18
|
+
const verifier = z.discriminatedUnion("kind", [
|
|
19
|
+
z.strictObject({
|
|
20
|
+
kind: z.literal("command"),
|
|
21
|
+
id,
|
|
22
|
+
command: z.array(z.string()).min(1),
|
|
23
|
+
timeoutMs: z
|
|
24
|
+
.number()
|
|
25
|
+
.int()
|
|
26
|
+
.positive()
|
|
27
|
+
.max(MAX_VERIFIER_TIMEOUT_MS, `must be at most ${MAX_VERIFIER_TIMEOUT_MS} ms; split the check or lower timeoutMs`),
|
|
28
|
+
expectExit: z.number().int().optional(),
|
|
29
|
+
expectStdoutIncludes: z.string().optional(),
|
|
30
|
+
}),
|
|
31
|
+
z.strictObject({
|
|
32
|
+
kind: z.literal("file-equals"),
|
|
33
|
+
id,
|
|
34
|
+
path: id,
|
|
35
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase hex sha256"),
|
|
36
|
+
}),
|
|
37
|
+
z.strictObject({ kind: z.literal("file-contains"), id, path: id, text: z.string() }),
|
|
38
|
+
z.strictObject({ kind: z.literal("file-absent"), id, path: id }),
|
|
39
|
+
z.strictObject({ kind: z.literal("tree-diff-within"), id, allowed: z.array(id) }),
|
|
40
|
+
z.strictObject({
|
|
41
|
+
kind: z.literal("tool-sequence"),
|
|
42
|
+
id,
|
|
43
|
+
rule: z.discriminatedUnion("type", [
|
|
44
|
+
z.strictObject({ type: z.literal("before"), first: id, then: id }),
|
|
45
|
+
z.strictObject({ type: z.literal("never"), tool: id }),
|
|
46
|
+
z.strictObject({
|
|
47
|
+
type: z.literal("max-calls"),
|
|
48
|
+
tool: id,
|
|
49
|
+
max: z.number().int().nonnegative(),
|
|
50
|
+
}),
|
|
51
|
+
z.strictObject({ type: z.literal("no-repeat-failure"), tool: id }),
|
|
52
|
+
]),
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
const task = z.strictObject({
|
|
56
|
+
id,
|
|
57
|
+
prompt: id,
|
|
58
|
+
fixture: id,
|
|
59
|
+
agent: z.string().optional(),
|
|
60
|
+
budget: z.strictObject({
|
|
61
|
+
maxTurns: z.number().int().positive(),
|
|
62
|
+
maxTokens: z.number().int().positive().optional(),
|
|
63
|
+
maxWallMs: z.number().int().positive(),
|
|
64
|
+
maxToolCalls: z.number().int().positive().optional(),
|
|
65
|
+
}),
|
|
66
|
+
policy: z.strictObject({
|
|
67
|
+
mode: z.enum(["ask", "auto", "full"]),
|
|
68
|
+
readOnly: z.boolean().optional(),
|
|
69
|
+
allow: z
|
|
70
|
+
.array(z.enum(TASK_ALLOW, {
|
|
71
|
+
error: `must be one of ${TASK_ALLOW.join(", ")} (MCP and agents cannot be granted by a suite)`,
|
|
72
|
+
}))
|
|
73
|
+
.optional(),
|
|
74
|
+
}),
|
|
75
|
+
verifiers: z.array(verifier),
|
|
76
|
+
tags: z.array(z.string()).optional(),
|
|
77
|
+
fixtureGit: z.boolean().optional(),
|
|
78
|
+
replayNormalize: z
|
|
79
|
+
.array(z.strictObject({
|
|
80
|
+
pattern: z.string().superRefine((value, ctx) => {
|
|
81
|
+
try {
|
|
82
|
+
new RegExp(value, "g");
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
ctx.addIssue({ code: "custom", message: "must be a valid regular expression" });
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
replace: z.string(),
|
|
89
|
+
}))
|
|
90
|
+
.optional(),
|
|
91
|
+
});
|
|
92
|
+
const suiteSchema = z.strictObject({
|
|
93
|
+
schemaVersion: z.literal(1),
|
|
94
|
+
suiteId: id,
|
|
95
|
+
suiteVersion: z.string().regex(SEMVER, "must be a semver version such as 1.0.0"),
|
|
96
|
+
description: z.string(),
|
|
97
|
+
tasks: z.array(task).min(1, "must declare at least one task"),
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* Reads `<dir>/suite.json` and proves the suite is internally consistent: schema, unique ids,
|
|
101
|
+
* existing fixture directories and every path confined to the suite (or, for verifiers, to the
|
|
102
|
+
* task working directory). Every failure names the task, the field and how to fix it.
|
|
103
|
+
*/
|
|
104
|
+
export function loadSuite(dir) {
|
|
105
|
+
const suiteDir = resolve(dir);
|
|
106
|
+
const file = join(suiteDir, "suite.json");
|
|
107
|
+
if (!existsSync(file))
|
|
108
|
+
throw new EvalSuiteError(`suite.json not found in ${suiteDir}. Create it or pass the suite directory.`);
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new EvalSuiteError(`${file} is not valid JSON. Fix the syntax error and retry.`);
|
|
115
|
+
}
|
|
116
|
+
const version = raw?.schemaVersion;
|
|
117
|
+
if (version !== 1)
|
|
118
|
+
throw new EvalSuiteError(`Unsupported schemaVersion in ${file}: expected 1, found ${JSON.stringify(version)}.`);
|
|
119
|
+
const parsed = suiteSchema.safeParse(raw);
|
|
120
|
+
if (!parsed.success) {
|
|
121
|
+
const issues = parsed.error.issues
|
|
122
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
123
|
+
.join("; ");
|
|
124
|
+
throw new EvalSuiteError(`Invalid suite ${file}: ${issues}`);
|
|
125
|
+
}
|
|
126
|
+
const suite = parsed.data;
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const fixtureDirs = {};
|
|
129
|
+
for (const t of suite.tasks) {
|
|
130
|
+
if (seen.has(t.id))
|
|
131
|
+
throw new EvalSuiteError(`Duplicate task id "${t.id}" in ${file}. Task ids must be unique.`);
|
|
132
|
+
seen.add(t.id);
|
|
133
|
+
let fixtureDir;
|
|
134
|
+
try {
|
|
135
|
+
fixtureDir = resolveInside(suiteDir, t.fixture, "fixture");
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!(error instanceof EvalPathError))
|
|
139
|
+
throw error;
|
|
140
|
+
throw new EvalSuiteError(`Task "${t.id}": fixture "${t.fixture}" must be a relative path inside the suite directory (${error.message})`);
|
|
141
|
+
}
|
|
142
|
+
if (!existsSync(fixtureDir))
|
|
143
|
+
throw new EvalSuiteError(`Task "${t.id}": fixture "${t.fixture}" does not exist (looked for ${fixtureDir}). Create the directory or fix the path.`);
|
|
144
|
+
if (!statSync(fixtureDir).isDirectory())
|
|
145
|
+
throw new EvalSuiteError(`Task "${t.id}": fixture "${t.fixture}" is not a directory.`);
|
|
146
|
+
fixtureDirs[t.id] = fixtureDir;
|
|
147
|
+
const verifierIds = new Set();
|
|
148
|
+
for (const v of t.verifiers) {
|
|
149
|
+
if (verifierIds.has(v.id))
|
|
150
|
+
throw new EvalSuiteError(`Task "${t.id}": duplicate verifier id "${v.id}".`);
|
|
151
|
+
verifierIds.add(v.id);
|
|
152
|
+
const paths = v.kind === "tree-diff-within"
|
|
153
|
+
? { field: "allowed", values: v.allowed }
|
|
154
|
+
: "path" in v
|
|
155
|
+
? { field: "path", values: [v.path] }
|
|
156
|
+
: undefined;
|
|
157
|
+
for (const value of paths?.values ?? []) {
|
|
158
|
+
const reason = unsafeRelativeReason(value);
|
|
159
|
+
if (reason)
|
|
160
|
+
throw new EvalSuiteError(`Task "${t.id}": verifier "${v.id}" ${paths?.field} "${value}" ${reason}. Verifier paths are relative to the task working directory.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { suite, dir: suiteDir, fixtureDirs };
|
|
165
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration variants of the reliability lab (specs/010 D15, section 7). A variant is a set of
|
|
3
|
+
* dotted-key overrides over the effective configuration. Keys are validated against the REAL
|
|
4
|
+
* config schema (strict objects: an unknown key is rejected and named), never applied as code.
|
|
5
|
+
* The overrides are applied BEFORE `runTask`, which hashes what it writes, so a variant gets its
|
|
6
|
+
* own `configHash`.
|
|
7
|
+
*/
|
|
8
|
+
import { type Config } from "../config.ts";
|
|
9
|
+
export declare class EvalVariantError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `base` with `overrides` applied (dotted keys, e.g. `{"compaction.threshold": 0.6}`); `base` is
|
|
14
|
+
* not modified. Throws `EvalVariantError` naming the key for an unknown key, a value the schema
|
|
15
|
+
* refuses, or a key the lab never measures (provider, plugins, MCP, paths: they are outside the
|
|
16
|
+
* configuration hash, so overriding them would change behavior without changing `configHash`).
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyConfigOverrides(base: Config, overrides: Record<string, unknown>): Config;
|
|
19
|
+
export interface VariantsFile {
|
|
20
|
+
suite: string;
|
|
21
|
+
variants: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
overrides: Record<string, unknown>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
/** Reads and validates a `compare.json` (`{ suite, variants: [{ id, overrides }] }`). */
|
|
27
|
+
export declare function loadVariantsFile(path: string): VariantsFile;
|
|
28
|
+
/**
|
|
29
|
+
* Applies every variant's overrides to `base`, all-or-nothing: one bad key rejects the file before
|
|
30
|
+
* anything runs, and the error names the variant and the key.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveVariants(file: VariantsFile, base: Config): Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
config: Config;
|
|
35
|
+
}>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration variants of the reliability lab (specs/010 D15, section 7). A variant is a set of
|
|
3
|
+
* dotted-key overrides over the effective configuration. Keys are validated against the REAL
|
|
4
|
+
* config schema (strict objects: an unknown key is rejected and named), never applied as code.
|
|
5
|
+
* The overrides are applied BEFORE `runTask`, which hashes what it writes, so a variant gets its
|
|
6
|
+
* own `configHash`.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { configSchema } from "../config.js";
|
|
11
|
+
import { CONFIG_HASH_COVERAGE } from "./engine.js";
|
|
12
|
+
export class EvalVariantError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "EvalVariantError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const FORBIDDEN_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
|
|
19
|
+
const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
function knownKeysAt(root, path) {
|
|
21
|
+
let cursor = root;
|
|
22
|
+
for (const segment of path) {
|
|
23
|
+
if (!isRecord(cursor) || !Object.hasOwn(cursor, segment))
|
|
24
|
+
return [];
|
|
25
|
+
cursor = cursor[segment];
|
|
26
|
+
}
|
|
27
|
+
return isRecord(cursor) ? Object.keys(cursor).sort() : [];
|
|
28
|
+
}
|
|
29
|
+
function unknownKey(key, base, parent) {
|
|
30
|
+
const siblings = knownKeysAt(base, parent);
|
|
31
|
+
const where = parent.length > 0 ? `section "${parent.join(".")}"` : "the configuration";
|
|
32
|
+
return new EvalVariantError(`Unknown config key "${key}". ${siblings.length > 0
|
|
33
|
+
? `Known keys in ${where}: ${siblings.join(", ")}.`
|
|
34
|
+
: `Use a dotted key of the Alisio configuration, for example "compaction.threshold".`}`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* `base` with `overrides` applied (dotted keys, e.g. `{"compaction.threshold": 0.6}`); `base` is
|
|
38
|
+
* not modified. Throws `EvalVariantError` naming the key for an unknown key, a value the schema
|
|
39
|
+
* refuses, or a key the lab never measures (provider, plugins, MCP, paths: they are outside the
|
|
40
|
+
* configuration hash, so overriding them would change behavior without changing `configHash`).
|
|
41
|
+
*/
|
|
42
|
+
export function applyConfigOverrides(base, overrides) {
|
|
43
|
+
const { mcpSources: _sources, ...plain } = base;
|
|
44
|
+
const draft = JSON.parse(JSON.stringify(plain));
|
|
45
|
+
const reference = JSON.parse(JSON.stringify(plain));
|
|
46
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
47
|
+
const segments = key.split(".");
|
|
48
|
+
if (segments.some((s) => s === "" || FORBIDDEN_SEGMENTS.has(s)))
|
|
49
|
+
throw unknownKey(key, reference, []);
|
|
50
|
+
const top = segments[0];
|
|
51
|
+
if (!Object.hasOwn(CONFIG_HASH_COVERAGE, top))
|
|
52
|
+
throw unknownKey(key, reference, []);
|
|
53
|
+
if (CONFIG_HASH_COVERAGE[top] === "excluded")
|
|
54
|
+
throw new EvalVariantError(`Config key "${key}" cannot be overridden by a variant: "${top}" is not part of the measured configuration (it holds endpoints, secrets, plugins or paths), so the change would not show up in configHash. Compare models with --model instead.`);
|
|
55
|
+
let cursor = draft;
|
|
56
|
+
for (const [index, segment] of segments.slice(0, -1).entries()) {
|
|
57
|
+
const next = Object.hasOwn(cursor, segment) ? cursor[segment] : undefined;
|
|
58
|
+
if (next === undefined)
|
|
59
|
+
cursor[segment] = {};
|
|
60
|
+
else if (!isRecord(next))
|
|
61
|
+
throw unknownKey(key, reference, segments.slice(0, index + 1));
|
|
62
|
+
cursor = cursor[segment];
|
|
63
|
+
}
|
|
64
|
+
cursor[segments[segments.length - 1]] = JSON.parse(JSON.stringify(value ?? null));
|
|
65
|
+
}
|
|
66
|
+
const parsed = configSchema.safeParse(draft);
|
|
67
|
+
if (!parsed.success) {
|
|
68
|
+
const issue = parsed.error.issues[0];
|
|
69
|
+
if (issue && issue.code === "unrecognized_keys") {
|
|
70
|
+
const parent = issue.path.map(String);
|
|
71
|
+
const bad = issue.keys[0];
|
|
72
|
+
throw unknownKey([...parent, bad].join("."), reference, parent);
|
|
73
|
+
}
|
|
74
|
+
const path = issue?.path.map(String).join(".") ?? "";
|
|
75
|
+
throw new EvalVariantError(`Invalid value for config key "${path}": ${issue?.message ?? "rejected by the configuration schema"}.`);
|
|
76
|
+
}
|
|
77
|
+
return parsed.data;
|
|
78
|
+
}
|
|
79
|
+
const variantsSchema = z
|
|
80
|
+
.object({
|
|
81
|
+
suite: z.string().min(1),
|
|
82
|
+
variants: z
|
|
83
|
+
.array(z
|
|
84
|
+
.object({
|
|
85
|
+
id: z
|
|
86
|
+
.string()
|
|
87
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/, "variant ids use letters, digits, dot, underscore or dash"),
|
|
88
|
+
overrides: z.record(z.string(), z.unknown()).default({}),
|
|
89
|
+
})
|
|
90
|
+
.strict())
|
|
91
|
+
.min(2, "a variants file needs at least two variants (a baseline and a candidate)"),
|
|
92
|
+
})
|
|
93
|
+
.strict();
|
|
94
|
+
/** Reads and validates a `compare.json` (`{ suite, variants: [{ id, overrides }] }`). */
|
|
95
|
+
export function loadVariantsFile(path) {
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = readFileSync(path, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new EvalVariantError(`Cannot read the variants file "${path}": ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
let json;
|
|
104
|
+
try {
|
|
105
|
+
json = JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new EvalVariantError(`The variants file "${path}" is not valid JSON.`);
|
|
109
|
+
}
|
|
110
|
+
const parsed = variantsSchema.safeParse(json);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
const issue = parsed.error.issues[0];
|
|
113
|
+
const where = issue?.path.length ? ` at "${issue.path.join(".")}"` : "";
|
|
114
|
+
throw new EvalVariantError(`Invalid variants file "${path}"${where}: ${issue?.message ?? "invalid"}.`);
|
|
115
|
+
}
|
|
116
|
+
const ids = parsed.data.variants.map((v) => v.id);
|
|
117
|
+
const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
|
|
118
|
+
if (duplicate)
|
|
119
|
+
throw new EvalVariantError(`Invalid variants file "${path}": duplicate variant id "${duplicate}".`);
|
|
120
|
+
return parsed.data;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Applies every variant's overrides to `base`, all-or-nothing: one bad key rejects the file before
|
|
124
|
+
* anything runs, and the error names the variant and the key.
|
|
125
|
+
*/
|
|
126
|
+
export function resolveVariants(file, base) {
|
|
127
|
+
return file.variants.map((variant) => {
|
|
128
|
+
try {
|
|
129
|
+
return { id: variant.id, config: applyConfigOverrides(base, variant.overrides) };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof EvalVariantError)
|
|
133
|
+
throw new EvalVariantError(`Variant "${variant.id}": ${error.message}`);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, local verifiers of the reliability lab (specs/010 D4). A verifier answers
|
|
3
|
+
* `pass`, `fail` or `error`; `error` means it could not run (bad path, missing input, command that
|
|
4
|
+
* cannot start) and is never folded into `fail`. Details never echo file contents.
|
|
5
|
+
*
|
|
6
|
+
* `command` runs a subprocess with a clean environment and a timeout. A subprocess is NOT a
|
|
7
|
+
* sandbox: suites are trusted repository code and the command runs with the user's privileges.
|
|
8
|
+
*/
|
|
9
|
+
import type { EvalVerifier } from "@alisio/sdk";
|
|
10
|
+
export type VerifierStatus = "pass" | "fail" | "error";
|
|
11
|
+
export interface VerifierResult {
|
|
12
|
+
id: string;
|
|
13
|
+
status: VerifierStatus;
|
|
14
|
+
detail?: string;
|
|
15
|
+
}
|
|
16
|
+
/** One tool call observed in a run, as the `tool-sequence` verifier needs it. */
|
|
17
|
+
export interface ToolEvent {
|
|
18
|
+
tool: string;
|
|
19
|
+
ok: boolean;
|
|
20
|
+
/** Canonical key of the call arguments; two calls with the same key are "identical". */
|
|
21
|
+
argsKey?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Content hashes of a tree (relative POSIX path -> sha256) plus one hash of the whole. */
|
|
24
|
+
export interface TreeSnapshot {
|
|
25
|
+
files: Record<string, string>;
|
|
26
|
+
hash: string;
|
|
27
|
+
}
|
|
28
|
+
export interface VerifierContext {
|
|
29
|
+
/** Working directory of the run; every verifier path is confined to it. */
|
|
30
|
+
cwd: string;
|
|
31
|
+
/** Snapshot of the fixture before the run; required by `tree-diff-within`. */
|
|
32
|
+
baseline?: TreeSnapshot;
|
|
33
|
+
/** Tool calls of the run in order; required by `tool-sequence`. */
|
|
34
|
+
toolEvents?: ToolEvent[];
|
|
35
|
+
/**
|
|
36
|
+
* Run-private temporary directory. When set, `command` verifiers get HOME and TMPDIR pointing at
|
|
37
|
+
* it so tooling caches and temp files stay inside the run; unset keeps the minimal environment.
|
|
38
|
+
*/
|
|
39
|
+
tmpDir?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Hashes every file under `dir` (symlinks by their target text, never followed). `.git` and
|
|
43
|
+
* `node_modules` are skipped at any depth: they are tooling state, not task output.
|
|
44
|
+
*/
|
|
45
|
+
export declare function snapshotTree(dir: string): TreeSnapshot;
|
|
46
|
+
/** Runs one verifier; anything that prevents a verdict (including exceptions) is `error`. */
|
|
47
|
+
export declare function runVerifier(v: EvalVerifier, ctx: VerifierContext): Promise<VerifierResult>;
|
|
48
|
+
/** Runs the verifiers in order (sequentially: `command` verifiers may share the working directory). */
|
|
49
|
+
export declare function runVerifiers(verifiers: EvalVerifier[], ctx: VerifierContext): Promise<VerifierResult[]>;
|