@cef-ai/eval 0.0.0-stage → 0.2.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.
Files changed (54) hide show
  1. package/README.md +195 -2
  2. package/dist/capture.d.ts +37 -0
  3. package/dist/capture.d.ts.map +1 -0
  4. package/dist/capture.js +70 -0
  5. package/dist/capture.js.map +1 -0
  6. package/dist/conformance.d.ts +6 -0
  7. package/dist/conformance.d.ts.map +1 -0
  8. package/dist/conformance.js +256 -0
  9. package/dist/conformance.js.map +1 -0
  10. package/dist/contract.d.ts +18 -0
  11. package/dist/contract.d.ts.map +1 -0
  12. package/dist/contract.js +176 -0
  13. package/dist/contract.js.map +1 -0
  14. package/dist/index.d.ts +18 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +11 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/layout.d.ts +105 -0
  19. package/dist/layout.d.ts.map +1 -0
  20. package/dist/layout.js +215 -0
  21. package/dist/layout.js.map +1 -0
  22. package/dist/pool.d.ts +13 -0
  23. package/dist/pool.d.ts.map +1 -0
  24. package/dist/pool.js +34 -0
  25. package/dist/pool.js.map +1 -0
  26. package/dist/result.d.ts +11 -0
  27. package/dist/result.d.ts.map +1 -0
  28. package/dist/result.js +72 -0
  29. package/dist/result.js.map +1 -0
  30. package/dist/run.d.ts +86 -0
  31. package/dist/run.d.ts.map +1 -0
  32. package/dist/run.js +240 -0
  33. package/dist/run.js.map +1 -0
  34. package/dist/score.d.ts +102 -0
  35. package/dist/score.d.ts.map +1 -0
  36. package/dist/score.js +433 -0
  37. package/dist/score.js.map +1 -0
  38. package/dist/storage.d.ts +218 -0
  39. package/dist/storage.d.ts.map +1 -0
  40. package/dist/storage.js +577 -0
  41. package/dist/storage.js.map +1 -0
  42. package/dist/summary.d.ts +105 -0
  43. package/dist/summary.d.ts.map +1 -0
  44. package/dist/summary.js +219 -0
  45. package/dist/summary.js.map +1 -0
  46. package/dist/types.d.ts +265 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +14 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/vault-store.d.ts +46 -0
  51. package/dist/vault-store.d.ts.map +1 -0
  52. package/dist/vault-store.js +127 -0
  53. package/dist/vault-store.js.map +1 -0
  54. package/package.json +27 -3
package/dist/pool.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bounded-concurrency for-each, shared by `runExperiment` and the store
3
+ * conformance suite. Dependency-free and browser-safe.
4
+ *
5
+ * IT FAILS SAFELY. When one item throws, no further item is started, every
6
+ * item already in flight is awaited, and only then does the pool reject, with
7
+ * the first error. `Promise.all` over the workers would reject at the first
8
+ * throw while its siblings kept running — so a caller's cleanup (releasing a
9
+ * version pin, writing the final summary) would run under work that was still
10
+ * writing, and land before it.
11
+ */
12
+ export async function pool(items, limit, fn) {
13
+ let next = 0;
14
+ let failed = false;
15
+ let first;
16
+ const worker = async () => {
17
+ while (!failed && next < items.length) {
18
+ const item = items[next++];
19
+ try {
20
+ await fn(item);
21
+ }
22
+ catch (e) {
23
+ if (!failed) {
24
+ failed = true;
25
+ first = e;
26
+ }
27
+ }
28
+ }
29
+ };
30
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
31
+ if (failed)
32
+ throw first;
33
+ }
34
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,IAAI,CAAI,KAAmB,EAAE,KAAa,EAAE,EAA8B;IAC9F,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,KAAc,CAAC;IACnB,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,OAAO,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,MAAM,EAAE,CAAC;oBACZ,MAAM,GAAG,IAAI,CAAC;oBACd,KAAK,GAAG,CAAC,CAAC;gBACZ,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9F,IAAI,MAAM;QAAE,MAAM,KAAK,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { ResultField } from "./types.js";
2
+ /**
3
+ * The fields the graph's `output` node(s) declare. A graph with several
4
+ * outputs (branches ending differently) yields the union; a field two outputs
5
+ * type differently is reported untyped, since either type can come back.
6
+ *
7
+ * undefined when no output declares a result — the Result is unknown, and
8
+ * scoring then judges every expected field as before.
9
+ */
10
+ export declare function resultFieldsFromGraph(graph: unknown): ResultField[] | undefined;
11
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA0B9C;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,EAAE,GAAG,SAAS,CAmB/E"}
package/dist/result.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Reading a workflow version's declared Result off its graph — what ROC and
3
+ * the CLI record as `Experiment.resultFields`.
4
+ *
5
+ * Mirrors the runner's own reading (`@cef-ai/agent-sdk` `readResultSpec`)
6
+ * without depending on it: this package is zero-dependency and browser-safe.
7
+ * An `output` node declares `params.result` (`{ field: value }`, or that object
8
+ * as JSON text — what a builder text field saves) and optionally
9
+ * `params.resultTypes` (`{ field: type }`, object or JSON text).
10
+ */
11
+ import { isObject } from "./contract.js";
12
+ /** An object, or JSON text that parses to one; anything else is undefined. */
13
+ function objectOf(raw) {
14
+ if (isObject(raw))
15
+ return raw;
16
+ if (typeof raw !== "string" || !raw.trim().startsWith("{"))
17
+ return undefined;
18
+ try {
19
+ const parsed = JSON.parse(raw);
20
+ return isObject(parsed) ? parsed : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ /**
27
+ * The graph itself, from any of the forms it travels in: the object, its JSON
28
+ * text, or a manifest param (`params.graph` = `{ type: "string", default: "<json>" }`).
29
+ */
30
+ function graphOf(raw) {
31
+ const g = objectOf(raw);
32
+ if (!g)
33
+ return undefined;
34
+ if (Array.isArray(g.nodes))
35
+ return g;
36
+ if ("default" in g)
37
+ return graphOf(g.default);
38
+ return undefined;
39
+ }
40
+ /**
41
+ * The fields the graph's `output` node(s) declare. A graph with several
42
+ * outputs (branches ending differently) yields the union; a field two outputs
43
+ * type differently is reported untyped, since either type can come back.
44
+ *
45
+ * undefined when no output declares a result — the Result is unknown, and
46
+ * scoring then judges every expected field as before.
47
+ */
48
+ export function resultFieldsFromGraph(graph) {
49
+ const g = graphOf(graph);
50
+ if (!g)
51
+ return undefined;
52
+ const types = new Map();
53
+ let declared = false;
54
+ for (const node of g.nodes) {
55
+ if (!isObject(node) || (node.kind ?? node.type) !== "output" || !isObject(node.params))
56
+ continue;
57
+ const result = objectOf(node.params.result);
58
+ if (!result)
59
+ continue;
60
+ declared = true;
61
+ const typed = objectOf(node.params.resultTypes) ?? {};
62
+ for (const name of Object.keys(result)) {
63
+ const t = typeof typed[name] === "string" && typed[name] ? typed[name] : undefined;
64
+ // First sighting sets the type; any disagreement (typed vs untyped included) drops it.
65
+ types.set(name, types.has(name) && types.get(name) !== t ? undefined : t);
66
+ }
67
+ }
68
+ if (!declared)
69
+ return undefined;
70
+ return [...types].sort(([a], [b]) => a.localeCompare(b)).map(([name, type]) => (type ? { name, type } : { name }));
71
+ }
72
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"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"}
package/dist/run.d.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Running an experiment: one dataset version × one workflow version × N
3
+ * repeats, against an injected {@link WorkflowTarget}.
4
+ *
5
+ * WHAT IS WRITTEN, AND WHEN. The meta first, `running`, so an experiment that
6
+ * dies half way is still visible as one. Each run record as its run finishes,
7
+ * write-once, so a long experiment is resumable: rerun with the same
8
+ * `experimentId` and every run already on file is reused rather than repeated.
9
+ * The summary last, with the final status.
10
+ *
11
+ * A NON-LIVE VERSION is run through `target.pinVersion`, which routes only this
12
+ * experiment's contexts (`eval-<expId>-…`) to the candidate and is released
13
+ * however the experiment ends.
14
+ */
15
+ import { type EvalStore } from "./storage.js";
16
+ import type { Case, Experiment, Header, ResultField, RunRecord, WorkflowTarget } from "./types.js";
17
+ export interface ExperimentProgress {
18
+ /** Runs finished (or reused) so far, including this one. */
19
+ done: number;
20
+ total: number;
21
+ run: RunRecord;
22
+ /** True when the run was already on file from an earlier attempt. */
23
+ reused: boolean;
24
+ }
25
+ export interface RunExperimentOptions {
26
+ store: EvalStore;
27
+ dataset: string;
28
+ /**
29
+ * The dataset version to run. Absent: `ensureDatasetVersion` — the latest,
30
+ * or a new one cut automatically when the cases changed since.
31
+ */
32
+ datasetVersion?: number;
33
+ workflowId: string;
34
+ /** The version being evaluated. With `live`, the version the live deployment runs (recorded only). */
35
+ workflowVersion: string;
36
+ /** Run against the live deployment (no pin). False requires `target.pinVersion`. */
37
+ live: boolean;
38
+ /** How many times each case runs. Default 1. */
39
+ repeats?: number;
40
+ /**
41
+ * The Result the workflow version declares (`resultFieldsFromGraph`).
42
+ * Recorded on the experiment; expected fields it rules out score n/a.
43
+ */
44
+ resultFields?: ResultField[];
45
+ /** Runs in flight at once. Default 4. */
46
+ concurrency?: number;
47
+ /** How long one run may take to end. Default 300 000 ms. */
48
+ timeoutMs?: number;
49
+ target: WorkflowTarget;
50
+ onProgress?: (p: ExperimentProgress) => void;
51
+ baselineId?: string;
52
+ /** Resume this experiment instead of starting a new one. */
53
+ experimentId?: string;
54
+ origin?: "roc" | "cli";
55
+ createdBy?: string;
56
+ note?: string;
57
+ /** Stops dispatching new runs; the experiment ends `cancelled`. */
58
+ signal?: AbortSignal;
59
+ /** Clock, for tests. */
60
+ now?: () => number;
61
+ /** Waits between retries of a run record's write; for tests. */
62
+ sleep?: (ms: number) => Promise<void>;
63
+ }
64
+ export declare const DEFAULT_TIMEOUT_MS = 300000;
65
+ export declare const DEFAULT_CONCURRENCY = 4;
66
+ /** Backoff before each retry of a run record whose write-once lost to nothing. */
67
+ export declare const RECORD_WRITE_BACKOFF_MS: readonly [250, 1000, 4000];
68
+ /** Run one case × repeat against the target, and score it. Never throws. */
69
+ export declare function runOne(opts: {
70
+ target: WorkflowTarget;
71
+ header?: Pick<Header, "limits">;
72
+ case: Case;
73
+ caseRev: number;
74
+ /** 1-based. */
75
+ repeat: number;
76
+ /** Recorded on the run; the context already carries it. */
77
+ attempt?: number;
78
+ context: string;
79
+ timeoutMs: number;
80
+ now?: () => number;
81
+ /** The version's declared Result; see {@link scoreFields}. */
82
+ resultFields?: readonly ResultField[];
83
+ }): Promise<RunRecord>;
84
+ /** Run an experiment end to end. Resolves with the final meta (summary included). */
85
+ export declare function runExperiment(opts: RunExperimentOptions): Promise<Experiment>;
86
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EASL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAMtB,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAiB,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElH,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,SAAS,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,sGAAsG;IACtG,eAAe,EAAE,MAAM,CAAC;IACxB,oFAAoF;IACpF,IAAI,EAAE,OAAO,CAAC;IACd,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,gEAAgE;IAChE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,4BAA6B,CAAC;AAKlE,4EAA4E;AAC5E,wBAAsB,MAAM,CAAC,IAAI,EAAE;IACjC,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe;IACf,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACvC,GAAG,OAAO,CAAC,SAAS,CAAC,CAuErB;AAED,qFAAqF;AACrF,wBAAsB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CA0InF"}
package/dist/run.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Running an experiment: one dataset version × one workflow version × N
3
+ * repeats, against an injected {@link WorkflowTarget}.
4
+ *
5
+ * WHAT IS WRITTEN, AND WHEN. The meta first, `running`, so an experiment that
6
+ * dies half way is still visible as one. Each run record as its run finishes,
7
+ * write-once, so a long experiment is resumable: rerun with the same
8
+ * `experimentId` and every run already on file is reused rather than repeated.
9
+ * The summary last, with the final status.
10
+ *
11
+ * A NON-LIVE VERSION is run through `target.pinVersion`, which routes only this
12
+ * experiment's contexts (`eval-<expId>-…`) to the candidate and is released
13
+ * however the experiment ends.
14
+ */
15
+ import { claimAttempt, ensureDatasetVersion, getExperiment, getRunRecord, loadVersion, putExperiment, putRunRecord, requireHeader, } from "./storage.js";
16
+ import { experimentContextPrefix, newExperimentId, runContext, runIdOf } from "./layout.js";
17
+ import { casePass, effectiveLimits, scoreFields, scoreLimits } from "./score.js";
18
+ import { emptyMetrics, summarize } from "./summary.js";
19
+ import { EvalContractError } from "./contract.js";
20
+ import { pool } from "./pool.js";
21
+ export const DEFAULT_TIMEOUT_MS = 300_000;
22
+ export const DEFAULT_CONCURRENCY = 4;
23
+ /** Backoff before each retry of a run record whose write-once lost to nothing. */
24
+ export const RECORD_WRITE_BACKOFF_MS = [250, 1000, 4000];
25
+ const iso = (ms) => new Date(ms).toISOString();
26
+ const message = (e) => (e instanceof Error ? e.message : String(e));
27
+ /** Run one case × repeat against the target, and score it. Never throws. */
28
+ export async function runOne(opts) {
29
+ const now = opts.now ?? Date.now;
30
+ const c = opts.case;
31
+ const started = now();
32
+ const base = {
33
+ caseId: c.id,
34
+ caseRev: opts.caseRev,
35
+ repeat: opts.repeat,
36
+ ...(opts.attempt !== undefined ? { attempt: opts.attempt } : {}),
37
+ context: opts.context,
38
+ runId: runIdOf(opts.context),
39
+ startedAt: iso(started),
40
+ };
41
+ try {
42
+ await opts.target.start(c.input, opts.context);
43
+ }
44
+ catch (e) {
45
+ return {
46
+ ...base, status: "error", output: null, error: `could not start the run: ${message(e)}`,
47
+ endedAt: null, metrics: emptyMetrics(), scores: [], pass: null,
48
+ };
49
+ }
50
+ let end;
51
+ try {
52
+ end = await opts.target.waitForEnd(opts.context, opts.timeoutMs);
53
+ }
54
+ catch (e) {
55
+ return {
56
+ ...base, status: "error", output: null, error: `could not read how the run ended: ${message(e)}`,
57
+ endedAt: null, metrics: emptyMetrics(), scores: [], pass: null,
58
+ };
59
+ }
60
+ const endedMs = end.endedAt ? Date.parse(end.endedAt) : NaN;
61
+ const startedMs = end.startedAt ? Date.parse(end.startedAt) : NaN;
62
+ let metrics;
63
+ let metricsError;
64
+ try {
65
+ metrics = await opts.target.metrics(opts.context);
66
+ }
67
+ catch (e) {
68
+ metrics = emptyMetrics();
69
+ metricsError = `metrics unavailable: ${message(e)}`;
70
+ }
71
+ /*
72
+ * ONE CLOCK. Duration is the end event's timestamp minus the start event's,
73
+ * both stamped by the server. When the target could not read both, whatever
74
+ * it measured itself (also server-side — the CLI target uses the jobs'
75
+ * createdAt/completedAt) stands, and otherwise there is no duration. This
76
+ * machine's clock is never subtracted from the server's: their skew would
77
+ * land in every number and differ between the CLI and ROC.
78
+ */
79
+ if (Number.isFinite(endedMs) && Number.isFinite(startedMs)) {
80
+ metrics = { ...metrics, durationMs: Math.max(0, endedMs - startedMs) };
81
+ }
82
+ const output = end.status === "done" ? (end.output ?? null) : null;
83
+ const fieldScores = scoreFields(c.expected, end.status === "done" ? (end.output ?? null) : undefined, opts.resultFields);
84
+ const scores = [...fieldScores, ...scoreLimits(effectiveLimits(opts.header, c), metrics)];
85
+ // A run that did not complete has no Result to judge; it is a failure of the
86
+ // workflow, whether or not the case states expectations.
87
+ const pass = end.status === "done" ? casePass(scores) : false;
88
+ const error = [end.error, metricsError].filter(Boolean).join("; ");
89
+ return {
90
+ ...base,
91
+ ...(Number.isFinite(startedMs) ? { startedAt: iso(startedMs) } : {}),
92
+ status: end.status,
93
+ output,
94
+ ...(end.outcome ? { outcome: end.outcome } : {}),
95
+ ...(error ? { error } : {}),
96
+ endedAt: Number.isFinite(endedMs) ? iso(endedMs) : null,
97
+ metrics,
98
+ scores,
99
+ pass,
100
+ };
101
+ }
102
+ /** Run an experiment end to end. Resolves with the final meta (summary included). */
103
+ export async function runExperiment(opts) {
104
+ const now = opts.now ?? Date.now;
105
+ const repeats = opts.repeats ?? 1;
106
+ const concurrency = opts.concurrency ?? DEFAULT_CONCURRENCY;
107
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
108
+ if (!Number.isInteger(repeats) || repeats < 1)
109
+ throw new EvalContractError("repeats must be a whole number ≥ 1");
110
+ if (!Number.isInteger(concurrency) || concurrency < 1)
111
+ throw new EvalContractError("concurrency must be a whole number ≥ 1");
112
+ if (!opts.live && !opts.target.pinVersion) {
113
+ throw new EvalContractError(`running workflow version ${opts.workflowVersion} that is not live needs a target that can pin a version`);
114
+ }
115
+ await requireHeader(opts.store, opts.workflowId, opts.dataset);
116
+ const existing = opts.experimentId ? await getExperiment(opts.store, opts.workflowId, opts.experimentId) : null;
117
+ if (opts.experimentId && (!existing || existing.dataset !== opts.dataset)) {
118
+ throw new EvalContractError(`experiment "${opts.experimentId}" not found for dataset "${opts.dataset}"`);
119
+ }
120
+ // A resume runs the version it started with; a new experiment the chosen one, or the working set's.
121
+ const datasetVersion = opts.datasetVersion
122
+ ?? existing?.datasetVersion
123
+ ?? (await ensureDatasetVersion(opts.store, opts.workflowId, opts.dataset, { now: new Date(now()) })).version;
124
+ const { manifest, cases } = await loadVersion(opts.store, opts.workflowId, opts.dataset, datasetVersion);
125
+ let experiment;
126
+ if (existing) {
127
+ if (existing.datasetVersion !== datasetVersion || existing.workflowVersion !== opts.workflowVersion
128
+ || existing.repeats !== repeats) {
129
+ throw new EvalContractError(`experiment "${existing.id}" ran dataset v${existing.datasetVersion} × workflow ${existing.workflowVersion} × ${existing.repeats}; `
130
+ + "resume it with the same settings");
131
+ }
132
+ // A new attempt, claimed write-once before anything is published: runs the
133
+ // dead one may have started — or a concurrent resumer is starting — keep
134
+ // their contexts to themselves.
135
+ const attempt = await claimAttempt(opts.store, opts.workflowId, existing.id, {
136
+ atLeast: (existing.attempt ?? 1) + 1, claimedAt: iso(now()),
137
+ });
138
+ experiment = { ...existing, status: "running", attempt };
139
+ delete experiment.finishedAt;
140
+ delete experiment.error;
141
+ }
142
+ else {
143
+ const id = newExperimentId(now());
144
+ await claimAttempt(opts.store, opts.workflowId, id, { atLeast: 1, claimedAt: iso(now()) });
145
+ experiment = {
146
+ id,
147
+ dataset: opts.dataset,
148
+ datasetVersion: manifest.version,
149
+ workflowId: opts.workflowId,
150
+ workflowVersion: opts.workflowVersion,
151
+ live: opts.live,
152
+ repeats,
153
+ ...(opts.resultFields ? { resultFields: opts.resultFields } : {}),
154
+ attempt: 1,
155
+ startedAt: iso(now()),
156
+ status: "running",
157
+ origin: opts.origin ?? "cli",
158
+ ...(opts.createdBy ? { createdBy: opts.createdBy } : {}),
159
+ ...(opts.baselineId ? { baselineId: opts.baselineId } : {}),
160
+ ...(opts.note ? { note: opts.note } : {}),
161
+ };
162
+ }
163
+ await putExperiment(opts.store, experiment);
164
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
165
+ /*
166
+ * Write-once, and honest about what happened. `putIfAbsent` answering false
167
+ * means "not written", which is two different things: the key EXISTS (a
168
+ * concurrent resumer got there first — its record is the one kept, and this
169
+ * run is reported as reused) or it does NOT (the store refused for another
170
+ * reason, e.g. a 409 from a colliding write). The second is retried with
171
+ * backoff and, if it never sticks, fails the experiment loudly — a record
172
+ * that was never stored must not be reported as reused.
173
+ */
174
+ const storeRecord = async (record) => {
175
+ for (let attempt = 0;; attempt++) {
176
+ if (await putRunRecord(opts.store, opts.workflowId, experiment.id, record))
177
+ return { kept: record, reused: false };
178
+ const there = await getRunRecord(opts.store, opts.workflowId, experiment.id, record.caseId, record.repeat);
179
+ if (there)
180
+ return { kept: there, reused: true };
181
+ const wait = RECORD_WRITE_BACKOFF_MS[attempt];
182
+ if (wait === undefined) {
183
+ throw new EvalContractError(`could not store the run record for ${record.caseId}#${record.repeat}: the store refused the write `
184
+ + `${attempt + 1} times and the key is still empty`);
185
+ }
186
+ await sleep(wait);
187
+ }
188
+ };
189
+ const work = cases.flatMap(({ case: c, rev }) => Array.from({ length: repeats }, (_, i) => ({ c, rev, repeat: i + 1 })));
190
+ const records = [];
191
+ let done = 0;
192
+ let release;
193
+ let failure;
194
+ try {
195
+ if (!opts.live)
196
+ release = await opts.target.pinVersion(opts.workflowVersion, experimentContextPrefix(experiment.id));
197
+ await pool(work, concurrency, async ({ c, rev, repeat }) => {
198
+ const onFile = await getRunRecord(opts.store, opts.workflowId, experiment.id, c.id, repeat);
199
+ if (onFile) {
200
+ records.push(onFile);
201
+ opts.onProgress?.({ done: ++done, total: work.length, run: onFile, reused: true });
202
+ return;
203
+ }
204
+ if (opts.signal?.aborted)
205
+ return;
206
+ const record = await runOne({
207
+ target: opts.target, header: manifest.header, case: c, caseRev: rev, repeat,
208
+ attempt: experiment.attempt ?? 1,
209
+ context: runContext(experiment.id, c.id, repeat, experiment.attempt ?? 1), timeoutMs, now,
210
+ ...(experiment.resultFields ? { resultFields: experiment.resultFields } : {}),
211
+ });
212
+ const { kept, reused } = await storeRecord(record);
213
+ records.push(kept);
214
+ opts.onProgress?.({ done: ++done, total: work.length, run: kept, reused });
215
+ });
216
+ }
217
+ catch (e) {
218
+ failure = e;
219
+ }
220
+ finally {
221
+ if (release) {
222
+ try {
223
+ await release();
224
+ }
225
+ catch (e) {
226
+ failure ??= new Error(`could not release the version pin: ${message(e)}`);
227
+ }
228
+ }
229
+ }
230
+ const finished = {
231
+ ...experiment,
232
+ finishedAt: iso(now()),
233
+ status: failure ? "error" : opts.signal?.aborted ? "cancelled" : "done",
234
+ ...(failure ? { error: message(failure) } : {}),
235
+ summary: summarize(records),
236
+ };
237
+ await putExperiment(opts.store, finished);
238
+ return finished;
239
+ }
240
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Deterministic scoring of a run's Result against a case's expectations.
3
+ *
4
+ * One score per expected field (`eval/field@1`). Field access is by dot path
5
+ * (`"forecast.amount"`, `"items.0.id"`). An expected field the output does not
6
+ * have FAILS — silently skipping a missing field is how a broken run goes green.
7
+ *
8
+ * Per field, the expectation is:
9
+ * - a matcher object (`{ $oneOf: [...] }`, see {@link Matcher});
10
+ * - a numeric band string (`">= 0.8"`) when the actual value is a number;
11
+ * - a plain object: SUBSET match — every key it names must match (leaves
12
+ * recursively by these same rules), keys it does not name are ignored;
13
+ * - anything else: deep equality (arrays ordered and length-sensitive).
14
+ *
15
+ * Limits (`eval/limit@1`) are scored too, and only WARN: a breach is counted
16
+ * and shown, and never flips a case's pass.
17
+ *
18
+ * AGAINST A DECLARED RESULT. When the experiment knows the Result the workflow
19
+ * version declares (`Experiment.resultFields`), an expectation that version
20
+ * cannot meet by construction is not a failure of the run: a field it does
21
+ * not declare, or declares with a type the expectation cannot match, is scored
22
+ * `pass: null` (n/a) with the reason. The dataset is older than the version;
23
+ * that is a finding about the dataset, shown as one, not a regression.
24
+ */
25
+ import type { Case, Header, Json, Limits, Matcher, Metrics, ResultField, Score } from "./types.js";
26
+ export declare const FIELD_SCORER = "eval/field@1";
27
+ export declare const LIMIT_SCORER = "eval/limit@1";
28
+ declare const MISSING: unique symbol;
29
+ type Actual = Json | typeof MISSING;
30
+ /** Read a dot path out of a value. `MISSING` when any segment is absent. */
31
+ export declare function readField(value: Json | null | undefined, path: string): Json | undefined;
32
+ export declare function deepEqual(a: Json, b: Json): boolean;
33
+ export type BandOp = ">=" | "<=" | ">" | "<";
34
+ export declare function parseBand(expr: string): {
35
+ op: BandOp;
36
+ threshold: number;
37
+ } | null;
38
+ /** Whether an expectation is a matcher object (every key starts with `$`). */
39
+ export declare function isMatcher(v: unknown): v is Matcher;
40
+ /** Longest `$regex` pattern scoring will compile. */
41
+ export declare const MAX_REGEX_LENGTH = 256;
42
+ /**
43
+ * Why a `$regex` pattern is refused before it is compiled, or null.
44
+ *
45
+ * TRUST MODEL. Cases come from a shared bucket, and JavaScript's regex engine
46
+ * backtracks: a pattern like `(a+)+$` runs for exponential time on a short
47
+ * non-matching string, synchronously, inside scoring — no timeout can
48
+ * interrupt it, and the CLI or the ROC tab freezes. So a pattern is refused
49
+ * when it is longer than {@link MAX_REGEX_LENGTH} or quantifies a group that
50
+ * itself contains a quantifier (`(a+)+`, `(a*)*`, `((ab)+){2,}`; a fixed
51
+ * `{n}` does not count). This is a heuristic, not a proof: overlapping
52
+ * alternations (`(a|a)*`) still pass it.
53
+ * Anyone who can write cases can make scoring slow; datasets are trusted the
54
+ * way the workflow code they test is.
55
+ */
56
+ export declare function unsafeRegex(pattern: string): string | null;
57
+ /** null when `actual` satisfies `expected`, otherwise why not. */
58
+ export declare function matchValue(expected: Json | Matcher, actual: Actual | Json): string | null;
59
+ /**
60
+ * The value types an expectation can match: `["number"]` for `{ $gte: 1 }`,
61
+ * `["number", "string"]` for a band string such as `">= 0.8"` (a band against
62
+ * a number, plain equality against a string), null when it can match any
63
+ * (`null`, `{ $exists }`, an empty or unknown matcher).
64
+ */
65
+ export declare function expectedTypesOf(want: Json | Matcher): string[] | null;
66
+ /**
67
+ * One `eval/field@1` score per expected field. With `resultFields` (the
68
+ * version's declared Result), an expectation the declaration rules out is
69
+ * n/a (`pass: null`) rather than a failure — see the file comment.
70
+ */
71
+ export declare function scoreFields(expected: Case["expected"], output: Json | null | undefined, resultFields?: readonly ResultField[]): Score[];
72
+ /** The limits in force for a case: the header's, each overridden by the case's own. */
73
+ export declare function effectiveLimits(header: Pick<Header, "limits"> | undefined, c: Pick<Case, "limits">): Limits;
74
+ /** One `eval/limit@1` score per limit in force. A metric that was not measured is not judged. */
75
+ export declare function scoreLimits(limits: Limits, metrics: Metrics): Score[];
76
+ /** A case passes when every field score passes; null when there were none. Limits never count. */
77
+ export declare function casePass(scores: Score[]): boolean | null;
78
+ export interface ResultCompatibility {
79
+ /** Expected fields the Result does not declare, with the cases expecting each. */
80
+ missing: Array<{
81
+ field: string;
82
+ caseIds: string[];
83
+ }>;
84
+ /** Expected fields whose expectation cannot match the declared type. */
85
+ typeChanged: Array<{
86
+ field: string;
87
+ expectedType: string;
88
+ declaredType: string;
89
+ caseIds: string[];
90
+ }>;
91
+ /** Declared fields no case states an expectation for. */
92
+ unexpectedNew: string[];
93
+ }
94
+ /**
95
+ * Whether a dataset's expectations fit a workflow version's declared Result —
96
+ * the check behind "2 dataset cases expect `priority`, which this version no
97
+ * longer returns". Fields are reported by their top-level name; each list is
98
+ * sorted.
99
+ */
100
+ export declare function checkResultCompatibility(resultFields: readonly ResultField[], cases: ReadonlyArray<Pick<Case, "id" | "expected">>): ResultCompatibility;
101
+ export {};
102
+ //# sourceMappingURL=score.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"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"}