@cef-ai/eval 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/README.md +202 -2
- package/dist/capture.d.ts +37 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +70 -0
- package/dist/capture.js.map +1 -0
- package/dist/conformance.d.ts +6 -0
- package/dist/conformance.d.ts.map +1 -0
- package/dist/conformance.js +257 -0
- package/dist/conformance.js.map +1 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +176 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +112 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +222 -0
- package/dist/layout.js.map +1 -0
- package/dist/migrate.d.ts +30 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +113 -0
- package/dist/migrate.js.map +1 -0
- package/dist/pool.d.ts +13 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +34 -0
- package/dist/pool.js.map +1 -0
- package/dist/result.d.ts +11 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +72 -0
- package/dist/result.js.map +1 -0
- package/dist/run.d.ts +86 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +240 -0
- package/dist/run.js.map +1 -0
- package/dist/score.d.ts +102 -0
- package/dist/score.d.ts.map +1 -0
- package/dist/score.js +433 -0
- package/dist/score.js.map +1 -0
- package/dist/storage.d.ts +218 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +577 -0
- package/dist/storage.js.map +1 -0
- package/dist/summary.d.ts +105 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +219 -0
- package/dist/summary.js.map +1 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cef-ai/eval
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Evaluations for CEF workflows: build datasets of cases from scratch or from real
|
|
4
|
+
runs, run them as experiments against a workflow version, and compare results and
|
|
5
|
+
run metrics against a baseline. Warn-only — nothing here blocks a deploy.
|
|
6
|
+
|
|
7
|
+
Browser-safe (no Node built-ins). ROC and the `cef eval` CLI use the same
|
|
8
|
+
library over the same storage layout, so either reads what the other wrote.
|
|
9
|
+
|
|
10
|
+
## Vocabulary
|
|
11
|
+
|
|
12
|
+
| Term | What it is |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **Dataset** | The set of cases for one workflow; versioned. Names are unique per workflow. |
|
|
15
|
+
| **Case** | One `workflow.start` payload (`input`) and what the run's Result should be (`expected`). |
|
|
16
|
+
| **Version** | A frozen cut of a dataset: every live case pinned at one revision by sha256. |
|
|
17
|
+
| **Experiment** | One run of a dataset version on a workflow version. |
|
|
18
|
+
| **Repeats** | How many times each case runs in an experiment (default 1). |
|
|
19
|
+
| **Run** | One case × one repeat inside an experiment — a `RunRecord` with output, scores and metrics. |
|
|
20
|
+
| **Baseline** | The experiment others are compared to (`Header.baselineId`, `setBaseline`). |
|
|
21
|
+
| **Result** | What a workflow declares as its output: an `output` node's `params.result`. A run's `workflow.completed` carries exactly those fields; that is what cases are scored against. |
|
|
22
|
+
|
|
23
|
+
## Declaring a Result
|
|
24
|
+
|
|
25
|
+
```jsonc
|
|
26
|
+
{ "id": "result", "kind": "output", "params": {
|
|
27
|
+
"result": { "category": "={{ $json.category }}", "confidence": "={{ $json.score }}" },
|
|
28
|
+
"resultTypes": { "category": "string", "confidence": "number" },
|
|
29
|
+
"outcome": "={{ $json.category }}"
|
|
30
|
+
} }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Cases and scoring
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"id": "refund-angry",
|
|
38
|
+
"input": { "text": "I want my money back NOW" },
|
|
39
|
+
"expected": {
|
|
40
|
+
"category": "billing",
|
|
41
|
+
"confidence": ">= 0.8",
|
|
42
|
+
"tags": { "$contains": "refund" },
|
|
43
|
+
"reply.language": { "$oneOf": ["en", "en-GB"] }
|
|
44
|
+
},
|
|
45
|
+
"limits": { "maxDurationMs": 20000 }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
One score per expected field (`eval/field@1`), addressed by dot path. A plain
|
|
50
|
+
value is deep equality (an object is a subset match), a string like `">= 0.8"`
|
|
51
|
+
is a numeric band, and matchers are `$eq $oneOf $contains $regex $between $gte
|
|
52
|
+
$lte $exists $approx/$tol`. A matcher naming several operators needs all of them
|
|
53
|
+
to hold (`{ "$exists": true, "$gte": 1 }`); `$between` is `[min, max]` with
|
|
54
|
+
min ≤ max. A field the output does not have fails. A case passes
|
|
55
|
+
when every judged field score passes; with no expectations it is unscored. A run that
|
|
56
|
+
failed or timed out fails. The summary's `passRate` (shown as **Accuracy**) and
|
|
57
|
+
`fields` (per expected field: passed / failed / notApplicable / rate) roll the
|
|
58
|
+
scores up. Limits (`maxDurationMs maxTokens maxSteps maxCost`,
|
|
59
|
+
header-wide or per case) are scored as `eval/limit@1`, counted as breaches, and
|
|
60
|
+
never fail a case.
|
|
61
|
+
|
|
62
|
+
**`$regex` and trust.** Patterns run in JavaScript's backtracking engine,
|
|
63
|
+
synchronously, inside scoring — nothing can time one out, so a catastrophic
|
|
64
|
+
pattern freezes the CLI or the ROC tab. Cases come from the shared bucket, so
|
|
65
|
+
scoring refuses (fails the field with `unsafe $regex refused: …`) a pattern
|
|
66
|
+
longer than 256 characters or one that quantifies a group which itself contains
|
|
67
|
+
a quantifier (`(a+)+`, `(\w+\s?)*`). That is a heuristic, not a proof — an
|
|
68
|
+
overlapping alternation such as `(a|a)*` passes it. Treat write access to a
|
|
69
|
+
dataset like write access to the workflow it tests: anyone who can write cases
|
|
70
|
+
can make scoring slow.
|
|
71
|
+
|
|
72
|
+
## Scoring against the version's Result
|
|
73
|
+
|
|
74
|
+
An experiment records the Result the workflow version declares
|
|
75
|
+
(`Experiment.resultFields`, from `resultFieldsFromGraph(graph)` — the graph
|
|
76
|
+
object, its JSON text, or a manifest's `params.graph`; several `output` nodes
|
|
77
|
+
are unioned). An expected field that Result does not declare is scored n/a
|
|
78
|
+
(`pass: null`, "not in this version's Result"), and one whose expectation cannot
|
|
79
|
+
match the declared type is n/a too ("type changed: expected number, Result
|
|
80
|
+
declares string") — the dataset is older than the version, which is not a
|
|
81
|
+
regression. `checkResultCompatibility(resultFields, cases)` reports the same up
|
|
82
|
+
front: `missing`, `typeChanged`, and `unexpectedNew` (declared, never expected).
|
|
83
|
+
|
|
84
|
+
## Storage
|
|
85
|
+
|
|
86
|
+
On an injected `EvalStore` (`list`, `get`, `put`, `putIfAbsent`; run
|
|
87
|
+
`storeConformance` against an implementation). In the agent service's bucket:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
workflows/<wf>/datasets/<dataset>/header.json Header (+ baselineId)
|
|
91
|
+
workflows/<wf>/datasets/<dataset>/cases/<caseId>/r<n>.json write-once revisions
|
|
92
|
+
workflows/<wf>/datasets/<dataset>/cases/<caseId>/deleted.json tombstone
|
|
93
|
+
workflows/<wf>/datasets/<dataset>/archived/<caseId>.json archive flag
|
|
94
|
+
workflows/<wf>/datasets/<dataset>/versions/v<n>.json {version, header, cases:[{id,rev,sha256}], createdAt, note?}
|
|
95
|
+
workflows/<wf>/experiments/<expId>/meta.json Experiment (dataset@version, workflowVersion, resultFields, status, summary)
|
|
96
|
+
workflows/<wf>/experiments/<expId>/runs/<caseId>.<repeat>.json RunRecord
|
|
97
|
+
workflows/<wf>/experiments/<expId>/attempts/a<n>.json attempt claim (write-once)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Experiments are siblings of the datasets: an experiment names the
|
|
101
|
+
`dataset@version` it ran. Every dataset function takes `(store, workflowId,
|
|
102
|
+
dataset, …)`, every experiment function `(store, workflowId, expId, …)`.
|
|
103
|
+
|
|
104
|
+
Storage written by the first release (`evals/datasets/<dataset>/…`, experiments
|
|
105
|
+
inside the dataset) is copied into this layout by `migrateLegacyLayout(store)`
|
|
106
|
+
(`cef eval migrate`): `putIfAbsent` per key, legacy keys left in place, a
|
|
107
|
+
`evals/MIGRATED.json` marker written once every dataset was copied.
|
|
108
|
+
`needsLegacyMigration(store)` says whether to run it.
|
|
109
|
+
|
|
110
|
+
## Dataset versions
|
|
111
|
+
|
|
112
|
+
Versions are cut automatically: `ensureDatasetVersion(store, wf, dataset)`
|
|
113
|
+
returns the latest version when the working set (live cases at their latest
|
|
114
|
+
revisions) is what it pins, and otherwise cuts the next one with the note
|
|
115
|
+
`auto: <n> added, <m> edited, <k> removed`. `runExperiment` calls it when no
|
|
116
|
+
`datasetVersion` is given. `cutVersion` remains for explicit cuts (`cef eval
|
|
117
|
+
push --cut`); `versionHistory` lists each version with what changed and how
|
|
118
|
+
many experiments ran it.
|
|
119
|
+
|
|
120
|
+
`compareExperiments` compares only the cases present on both sides at the same
|
|
121
|
+
revision (`comparedOn`), and reports the rest as `datasetDiff: {added, removed,
|
|
122
|
+
edited}`.
|
|
123
|
+
|
|
124
|
+
## Running experiments
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { runExperiment, resultFieldsFromGraph } from "@cef-ai/eval";
|
|
128
|
+
|
|
129
|
+
const exp = await runExperiment({
|
|
130
|
+
store, workflowId: "ticket-triage", dataset: "triage", // datasetVersion: 3 — default: ensureDatasetVersion
|
|
131
|
+
workflowVersion: "1.4.0", live: false,
|
|
132
|
+
resultFields: resultFieldsFromGraph(graph),
|
|
133
|
+
repeats: 3, concurrency: 4, timeoutMs: 120_000,
|
|
134
|
+
target, // WorkflowTarget: start / waitForEnd / metrics / pinVersion?
|
|
135
|
+
onProgress: ({ done, total }) => console.log(done, total),
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Each run publishes into its own context, `eval-<expId>-<caseId>-<repeat>-a<attempt>`.
|
|
140
|
+
A non-live version is run through `target.pinVersion`, which routes only this
|
|
141
|
+
experiment's contexts (prefix `eval-<expId>-`) to it. The meta is written first,
|
|
142
|
+
each run record as it finishes, the summary last; pass `experimentId` to resume.
|
|
143
|
+
A resume is a new attempt (`attempt` on the meta and on each run it writes), so
|
|
144
|
+
a run the dead attempt had already started never shares a context with its
|
|
145
|
+
replacement. The attempt is claimed write-once (`putIfAbsent` on
|
|
146
|
+
`experiments/<expId>/attempts/a<n>.json`, n upward until the claim sticks)
|
|
147
|
+
before anything is published, so two concurrent resumes of one experiment get
|
|
148
|
+
different attempts and never publish into the same contexts.
|
|
149
|
+
|
|
150
|
+
## CLI walkthrough
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
export CEF_AS_PUBKEY=0x… # the agent service; names the bucket
|
|
154
|
+
export CEF_KEYSTORE=~/owner.json # owner wallet (or CEF_SECRET_PHRASE)
|
|
155
|
+
export CEF_KEYSTORE_PASSWORD=…
|
|
156
|
+
export CEF_ACCESS_TOKEN=… # CLI access token from ROC, for the orchestrator
|
|
157
|
+
|
|
158
|
+
# In the workflow's folder: --workflow defaults to the workflow cef.config.ts
|
|
159
|
+
# (or dist/) declares, --dir to ./datasets.
|
|
160
|
+
cef eval create triage --max-duration-ms 30000
|
|
161
|
+
cef eval pull triage # → datasets/triage/{header.json,cases/*.json,.versions.json}
|
|
162
|
+
$EDITOR datasets/triage/cases/refund-angry.json
|
|
163
|
+
cef eval push triage # new revisions; --cut "note" cuts a version explicitly
|
|
164
|
+
cef eval run triage # latest dataset version (cut if the cases changed) × the live workflow version
|
|
165
|
+
cef eval run triage --workflow-version 1.5.0 --repeats 3 --baseline exp-mgfz8c1k-a1b2
|
|
166
|
+
cef eval run triage --dataset-version 2 # an older dataset version
|
|
167
|
+
cef eval experiments triage
|
|
168
|
+
cef eval compare exp-mgfz8c1k-a1b2 exp-mgg0p2xq-9zt4
|
|
169
|
+
cef eval migrate # once, for a bucket written by the first release
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The wallet's S3 credential is minted once, kept for its hour in
|
|
173
|
+
`~/.cef/eval-credentials.json` (owner-only; `CEF_EVAL_CREDENTIAL_CACHE=<file>`
|
|
174
|
+
moves it, `=off` disables it) so each command does not mint another against the
|
|
175
|
+
gateway's per-wallet cap, re-minted before it expires during a long run, and
|
|
176
|
+
re-minted once if the gateway refuses it.
|
|
177
|
+
|
|
178
|
+
Store credentials are taken in this order: explicit `--access-key`/`--secret`
|
|
179
|
+
(`$CEF_EVAL_KEY`/`$CEF_EVAL_SECRET`, an S3 key issued for the owner's wallet);
|
|
180
|
+
then the owner's wallet, minting as above. `$CEF_DDC_ACCESS_TOKEN`, the token
|
|
181
|
+
`cef push` uses, cannot open dataset storage: it is limited to the agent
|
|
182
|
+
service's registry bucket, while datasets live in the gateway bucket
|
|
183
|
+
`as-<pubkey>`, a separate DDC bucket, and it cannot be delegated to the gateway.
|
|
184
|
+
With only that token set, `cef eval` says so and names the two options above.
|
|
185
|
+
|
|
186
|
+
`run` records the version's Result — the live deployment's graph when the
|
|
187
|
+
version is live, else the project's own graph of exactly that version — and
|
|
188
|
+
warns before starting about cases expecting fields it no longer returns. The
|
|
189
|
+
baseline stays on the platform: `pull` leaves it out of `header.json`, `push`
|
|
190
|
+
keeps the bucket's, and `run` compares against it when `--baseline` is not given.
|
|
191
|
+
|
|
192
|
+
Every command takes `--json`. `push` writes a new revision only for a case that
|
|
193
|
+
changed and refuses one edited in the bucket since the last pull (`--force` to
|
|
194
|
+
write over it). A repo case that is deleted or archived in the bucket is refused
|
|
195
|
+
before anything is written; `--restore` undeletes/unarchives it and writes the
|
|
196
|
+
repo's version (`--force` does not). `pull` and `push --prune` only remove what
|
|
197
|
+
the last pull wrote (`.versions.json`): a case file the pull never wrote is
|
|
198
|
+
reported as local-only and kept, and a case added in the bucket after the pull
|
|
199
|
+
is never pruned. `pull` records each case's content hash in `.versions.json` and
|
|
200
|
+
refuses, before writing anything, to overwrite or remove a case file edited since
|
|
201
|
+
the last pull/push — one matching neither that hash nor the bucket's current
|
|
202
|
+
content — unless `--force`. Both `pull` and `push` refuse a dataset dir whose
|
|
203
|
+
`header.json` names another workflow.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Case, Json, Matcher } from "./types.js";
|
|
2
|
+
export interface CapturedRun {
|
|
3
|
+
/** The `workflow.start` payload. An inline `graph` is dropped. */
|
|
4
|
+
input: Record<string, Json>;
|
|
5
|
+
/** `workflow.completed`'s `output`; undefined or null when the run did not complete. */
|
|
6
|
+
output?: Json | null;
|
|
7
|
+
/** The run's context. */
|
|
8
|
+
context: string;
|
|
9
|
+
workflowVersion?: string;
|
|
10
|
+
/** Case id; derived from the context when absent. */
|
|
11
|
+
id?: string;
|
|
12
|
+
capturedAt?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
notes?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A case id from free text: `[A-Za-z0-9._-]`, at most 80 characters, never empty. */
|
|
17
|
+
export declare function slugCaseId(text: string): string;
|
|
18
|
+
/** `base`, or `base-2`, `base-3`… — the first one not in `taken`. */
|
|
19
|
+
export declare function uniqueCaseId(base: string, taken: Iterable<string>): string;
|
|
20
|
+
/**
|
|
21
|
+
* A case from a captured run. `expected` is the run's Result, field by field,
|
|
22
|
+
* when it is an object; a non-object output cannot be keyed by field, so it is
|
|
23
|
+
* left out and the person states the expectation themselves.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Whether a captured value is free text a model wrote — a sentence, not a label.
|
|
27
|
+
*
|
|
28
|
+
* Pinning prose with an exact match makes every later experiment fail on a
|
|
29
|
+
* harmless rewording, which buries the regressions the dataset exists to show.
|
|
30
|
+
* So a captured run expects such a field to be PRESENT; a person tightens it
|
|
31
|
+
* (contains, regex, or a judge) when the wording itself matters.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isFreeText(v: unknown): v is string;
|
|
34
|
+
/** What a captured output should be expected to be: labels exactly, prose present. */
|
|
35
|
+
export declare function expectationFrom(output: Record<string, unknown>): Record<string, Json | Matcher>;
|
|
36
|
+
export declare function caseFromRun(run: CapturedRun): Case;
|
|
37
|
+
//# sourceMappingURL=capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC5B,wFAAwF;IACxF,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qDAAqD;IACrD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAS1E;AAED;;;;GAIG;AACH;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAIlD;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC,CAI/F;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAiBlD"}
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building cases from real runs: a run's trigger payload becomes the input,
|
|
3
|
+
* its declared Result becomes the expectation (to be edited — what the
|
|
4
|
+
* workflow produced is not necessarily what it should have).
|
|
5
|
+
*/
|
|
6
|
+
import { isCaseId } from "./layout.js";
|
|
7
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8
|
+
/** A case id from free text: `[A-Za-z0-9._-]`, at most 80 characters, never empty. */
|
|
9
|
+
export function slugCaseId(text) {
|
|
10
|
+
const slug = text.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[-.]+|[-.]+$/g, "").slice(0, 80);
|
|
11
|
+
return slug && isCaseId(slug) ? slug : "case";
|
|
12
|
+
}
|
|
13
|
+
/** `base`, or `base-2`, `base-3`… — the first one not in `taken`. */
|
|
14
|
+
export function uniqueCaseId(base, taken) {
|
|
15
|
+
const used = new Set(taken);
|
|
16
|
+
const root = slugCaseId(base);
|
|
17
|
+
if (!used.has(root))
|
|
18
|
+
return root;
|
|
19
|
+
for (let i = 2;; i++) {
|
|
20
|
+
const suffix = `-${i}`;
|
|
21
|
+
const id = `${root.slice(0, 80 - suffix.length)}${suffix}`;
|
|
22
|
+
if (!used.has(id))
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A case from a captured run. `expected` is the run's Result, field by field,
|
|
28
|
+
* when it is an object; a non-object output cannot be keyed by field, so it is
|
|
29
|
+
* left out and the person states the expectation themselves.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Whether a captured value is free text a model wrote — a sentence, not a label.
|
|
33
|
+
*
|
|
34
|
+
* Pinning prose with an exact match makes every later experiment fail on a
|
|
35
|
+
* harmless rewording, which buries the regressions the dataset exists to show.
|
|
36
|
+
* So a captured run expects such a field to be PRESENT; a person tightens it
|
|
37
|
+
* (contains, regex, or a judge) when the wording itself matters.
|
|
38
|
+
*/
|
|
39
|
+
export function isFreeText(v) {
|
|
40
|
+
if (typeof v !== "string")
|
|
41
|
+
return false;
|
|
42
|
+
const t = v.trim();
|
|
43
|
+
return t.length > 60 || t.split(/\s+/).length > 8;
|
|
44
|
+
}
|
|
45
|
+
/** What a captured output should be expected to be: labels exactly, prose present. */
|
|
46
|
+
export function expectationFrom(output) {
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const [k, v] of Object.entries(output))
|
|
49
|
+
out[k] = isFreeText(v) ? { $exists: true } : v;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export function caseFromRun(run) {
|
|
53
|
+
const { graph: _inline, ...input } = run.input;
|
|
54
|
+
const expected = isPlainObject(run.output) && Object.keys(run.output).length > 0
|
|
55
|
+
? expectationFrom(run.output)
|
|
56
|
+
: undefined;
|
|
57
|
+
return {
|
|
58
|
+
id: run.id ?? slugCaseId(run.context),
|
|
59
|
+
input,
|
|
60
|
+
...(expected ? { expected } : {}),
|
|
61
|
+
...(run.tags?.length ? { tags: run.tags } : {}),
|
|
62
|
+
...(run.notes ? { notes: run.notes } : {}),
|
|
63
|
+
source: {
|
|
64
|
+
context: run.context,
|
|
65
|
+
...(run.workflowVersion ? { workflowVersion: run.workflowVersion } : {}),
|
|
66
|
+
capturedAt: run.capturedAt ?? new Date().toISOString(),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAkB,MAAM,cAAc,CAAC;AAwC9D,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,GAClC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAyPnD"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `EvalStore` contract as runnable checks.
|
|
3
|
+
*
|
|
4
|
+
* Exported rather than kept in this package's own tests because a second
|
|
5
|
+
* consumer (ROC) implements the same interface over a different client, and prose is
|
|
6
|
+
* not a contract. Each check throws on violation; the caller wraps them in
|
|
7
|
+
* whatever test runner it has.
|
|
8
|
+
*/
|
|
9
|
+
import { pool } from "./pool.js";
|
|
10
|
+
const assert = (ok, what) => {
|
|
11
|
+
if (!ok)
|
|
12
|
+
throw new Error(`store conformance: ${what}`);
|
|
13
|
+
};
|
|
14
|
+
/** Every mechanism this suite knows how to be told about. */
|
|
15
|
+
const MECHANISMS = ["conditional-write", "single-threaded"];
|
|
16
|
+
/**
|
|
17
|
+
* The declaration gate.
|
|
18
|
+
*
|
|
19
|
+
* A store that says nothing is REFUSED rather than quietly run, because the
|
|
20
|
+
* concurrent check below cannot tell a real compare-and-set from a
|
|
21
|
+
* write-then-verify and silently accepting the second is the bug this exists
|
|
22
|
+
* to close (see `StoreAtomicity`). An unrecognised word counts as saying
|
|
23
|
+
* nothing: ROC reaches this through the built package, where TypeScript's
|
|
24
|
+
* union is gone by the time the object is constructed.
|
|
25
|
+
*/
|
|
26
|
+
function declaredMechanism(store) {
|
|
27
|
+
const declared = store.atomicity;
|
|
28
|
+
if (declared === undefined) {
|
|
29
|
+
throw new Error("store conformance: this store declares no `atomicity`, so the suite refuses it. " +
|
|
30
|
+
"No in-process check can tell a real compare-and-set from a write-then-verify " +
|
|
31
|
+
"(both leave exactly one winner under Promise.all), so an implementation must " +
|
|
32
|
+
`state which it is — one of ${MECHANISMS.join(", ")} — and pin that claim in its ` +
|
|
33
|
+
"own test against what the transport actually sends.");
|
|
34
|
+
}
|
|
35
|
+
if (!MECHANISMS.includes(declared)) {
|
|
36
|
+
throw new Error(`store conformance: unknown \`atomicity\` ${JSON.stringify(declared)}; the suite ` +
|
|
37
|
+
`only knows ${MECHANISMS.join(", ")}. An unrecognised word is treated as no ` +
|
|
38
|
+
"declaration at all, not as a pass.");
|
|
39
|
+
}
|
|
40
|
+
return declared;
|
|
41
|
+
}
|
|
42
|
+
export function storeConformance(makeStore) {
|
|
43
|
+
const k = () => `conformance/${crypto.randomUUID()}`;
|
|
44
|
+
// Bodies receive the store and the mechanism it declared, so the gate below
|
|
45
|
+
// can be applied to all of them in one place. A body that does not care
|
|
46
|
+
// about the mechanism just omits the second parameter.
|
|
47
|
+
const checks = [
|
|
48
|
+
{
|
|
49
|
+
name: "get answers null for a key that was never written",
|
|
50
|
+
body: async (s) => {
|
|
51
|
+
assert((await s.get(k())) === null, "absent key must read null, not throw");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "put then get round-trips the exact bytes",
|
|
56
|
+
body: async (s) => {
|
|
57
|
+
const key = k();
|
|
58
|
+
await s.put(key, '{"a":1}\n');
|
|
59
|
+
assert((await s.get(key)) === '{"a":1}\n', "bytes must round-trip unchanged");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// Headers (baselineId), experiment metas (status) and flags are
|
|
64
|
+
// rewritten in place. A store that treats every key as write-once would
|
|
65
|
+
// keep the first baseline forever and every experiment `running`.
|
|
66
|
+
name: "put replaces the bytes of a key that exists",
|
|
67
|
+
body: async (s) => {
|
|
68
|
+
const key = k();
|
|
69
|
+
await s.put(key, "first");
|
|
70
|
+
await s.put(key, "second");
|
|
71
|
+
assert((await s.get(key)) === "second", "a second put must replace the first");
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
// The v2 layout nests datasets and experiments two levels under
|
|
76
|
+
// `workflows/<id>/`; a delimiter listing must answer at that depth too.
|
|
77
|
+
name: "listPrefixes, when present, answers below the first level",
|
|
78
|
+
body: async (s) => {
|
|
79
|
+
if (!s.listPrefixes)
|
|
80
|
+
return;
|
|
81
|
+
const root = `${k()}/workflows/wf`;
|
|
82
|
+
await s.put(`${root}/datasets/a/header.json`, "a");
|
|
83
|
+
await s.put(`${root}/datasets/b/cases/x/r1.json`, "b");
|
|
84
|
+
await s.put(`${root}/experiments/e/meta.json`, "e");
|
|
85
|
+
const got = await s.listPrefixes(`${root}/datasets/`);
|
|
86
|
+
assert(JSON.stringify([...got].sort()) === JSON.stringify([`${root}/datasets/a/`, `${root}/datasets/b/`]), `listPrefixes must return a nested prefix's children, got ${JSON.stringify(got)}`);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "putIfAbsent writes once and refuses the second writer",
|
|
91
|
+
body: async (s) => {
|
|
92
|
+
const key = k();
|
|
93
|
+
assert(await s.putIfAbsent(key, "first"), "first putIfAbsent must write");
|
|
94
|
+
assert(!(await s.putIfAbsent(key, "second")), "second must answer false");
|
|
95
|
+
assert((await s.get(key)) === "first", "a refused putIfAbsent must leave the stored bytes untouched");
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
// Two halves, because neither alone is enough.
|
|
100
|
+
//
|
|
101
|
+
// The DECLARATION is the half that matters most, and it is checked
|
|
102
|
+
// first. The check above is strictly sequential — write, write again,
|
|
103
|
+
// read — so a read-then-write `putIfAbsent` with no conditional header
|
|
104
|
+
// passes it. Eight at once catches the crudest shape of that (a store
|
|
105
|
+
// that reads, then writes, then answers `true` lets all eight see an
|
|
106
|
+
// absent key and all eight win) but it CANNOT catch a write-then-verify:
|
|
107
|
+
// that one clobbers seven writes and still leaves exactly one caller
|
|
108
|
+
// answering `true` over its own bytes, so it is green here and wrong in
|
|
109
|
+
// production, and wrong across two processes where both writers win.
|
|
110
|
+
// Nothing in this process can separate the two, so the store is required
|
|
111
|
+
// to say which it is and its own test pins the word to the wire (see
|
|
112
|
+
// `StoreAtomicity`).
|
|
113
|
+
//
|
|
114
|
+
// What the write-once layout rests on, and why this is not fussiness:
|
|
115
|
+
// the version number and the revision number are both DERIVED, so two
|
|
116
|
+
// writers compute the same one and only the conditional write decides
|
|
117
|
+
// who holds it. `memoryStore` declares `single-threaded` and passes by
|
|
118
|
+
// construction (no `await` between its check and its write); `s3Store`
|
|
119
|
+
// and ROC's store declare `conditional-write` and the dev gateway backs
|
|
120
|
+
// it with verified 412 behaviour.
|
|
121
|
+
name: "putIfAbsent declares its mechanism, and 8 concurrent calls leave one winner",
|
|
122
|
+
body: async (s, mechanism) => {
|
|
123
|
+
// Half of this union IS falsifiable in this process, and that half
|
|
124
|
+
// gets tested rather than trusted.
|
|
125
|
+
//
|
|
126
|
+
// `conditional-write` cannot be: a server adjudicating and a store
|
|
127
|
+
// writing-then-verifying are indistinguishable from in here, which is
|
|
128
|
+
// the whole reason the word is declared at all. `single-threaded` can
|
|
129
|
+
// be, because it is a much narrower claim than it sounds — it says the
|
|
130
|
+
// check and the write sit in ONE synchronous run with nothing
|
|
131
|
+
// suspending between them. So issue two `putIfAbsent`s on one key in
|
|
132
|
+
// the same turn, without awaiting in between: an honest
|
|
133
|
+
// single-threaded store finishes the first outright and the second
|
|
134
|
+
// finds the key taken, `[true, false]`. A write-then-verify wearing
|
|
135
|
+
// the label suspends on its first `get`, both calls read absent, both
|
|
136
|
+
// write, and it answers `[false, true]` — the first writer's bytes are
|
|
137
|
+
// already gone. Either way the label was false, and it fails here
|
|
138
|
+
// rather than in production.
|
|
139
|
+
if (mechanism === "single-threaded") {
|
|
140
|
+
const key = k();
|
|
141
|
+
const sameTurn = await Promise.all([
|
|
142
|
+
s.putIfAbsent(key, "same-turn-a"),
|
|
143
|
+
s.putIfAbsent(key, "same-turn-b"),
|
|
144
|
+
]);
|
|
145
|
+
assert(sameTurn[0] === true && sameTurn[1] === false, "a store declaring single-threaded must settle putIfAbsent without suspending: " +
|
|
146
|
+
"two calls issued in ONE synchronous turn must answer [true, false], got " +
|
|
147
|
+
`${JSON.stringify(sameTurn)} — this store suspends between its check and its ` +
|
|
148
|
+
"write, so the declaration is wrong");
|
|
149
|
+
assert((await s.get(key)) === "same-turn-a", "the same-turn winner's bytes must be the ones stored");
|
|
150
|
+
}
|
|
151
|
+
const key = k();
|
|
152
|
+
const bodies = Array.from({ length: 8 }, (_, i) => `writer-${i}`);
|
|
153
|
+
const won = await Promise.all(bodies.map((body) => s.putIfAbsent(key, body)));
|
|
154
|
+
const winners = won.filter(Boolean).length;
|
|
155
|
+
// Zero winners is not the same failure as two: if the gateway refused
|
|
156
|
+
// every writer (e.g. answered 409/412 to all eight, including the one
|
|
157
|
+
// that would otherwise have won), the write-once contract was never
|
|
158
|
+
// actually exercised — that's contention exhausting the race, not a
|
|
159
|
+
// broken guarantee. Reporting it as a contract violation would send
|
|
160
|
+
// someone hunting a storage bug that isn't there.
|
|
161
|
+
if (winners === 0) {
|
|
162
|
+
throw new Error("store conformance: contention, not a broken guarantee — all 8 concurrent " +
|
|
163
|
+
"putIfAbsent calls on one key answered false (the gateway refused every " +
|
|
164
|
+
"writer, e.g. 409/412 to all eight, including whichever would have won); " +
|
|
165
|
+
"nobody actually exercised write-once here, so this is not a storage bug");
|
|
166
|
+
}
|
|
167
|
+
assert(winners === 1, `exactly one of 8 concurrent putIfAbsent calls may answer true, ${winners} did`);
|
|
168
|
+
const winner = bodies[won.indexOf(true)];
|
|
169
|
+
const stored = await s.get(key);
|
|
170
|
+
assert(stored === winner, `the stored bytes must be the winner's (${winner}), got ${JSON.stringify(stored)}`);
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "list returns every key under a prefix and nothing above it",
|
|
175
|
+
body: async (s) => {
|
|
176
|
+
const root = k();
|
|
177
|
+
await s.put(`${root}/a.json`, "a");
|
|
178
|
+
await s.put(`${root}/nested/b.json`, "b");
|
|
179
|
+
await s.put(`${root}-sibling/c.json`, "c");
|
|
180
|
+
const keys = (await s.list(`${root}/`)).sort();
|
|
181
|
+
assert(keys.length === 2 && keys[0] === `${root}/a.json` && keys[1] === `${root}/nested/b.json`, `list must return exactly the prefix's keys, got ${JSON.stringify(keys)}`);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
// Optional, so a store without it passes trivially — the library then
|
|
186
|
+
// derives prefixes from `list`. A store that HAS it is held to it: a
|
|
187
|
+
// wrong answer here hides datasets and experiments, quietly.
|
|
188
|
+
name: "listPrefixes, when present, returns each immediate child prefix once",
|
|
189
|
+
body: async (s) => {
|
|
190
|
+
if (!s.listPrefixes)
|
|
191
|
+
return;
|
|
192
|
+
const root = k();
|
|
193
|
+
await s.put(`${root}/top.json`, "t");
|
|
194
|
+
await s.put(`${root}/x/1.json`, "1");
|
|
195
|
+
await s.put(`${root}/x/2.json`, "2");
|
|
196
|
+
await s.put(`${root}/y/deep/3.json`, "3");
|
|
197
|
+
await s.put(`${root}-sibling/z/4.json`, "4");
|
|
198
|
+
const got = await s.listPrefixes(`${root}/`);
|
|
199
|
+
assert(JSON.stringify([...got].sort()) === JSON.stringify([`${root}/x/`, `${root}/y/`]), `listPrefixes must return exactly the child prefixes, each once, got ${JSON.stringify(got)}`);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "list pages past any server limit",
|
|
204
|
+
body: async (s) => {
|
|
205
|
+
const root = k();
|
|
206
|
+
// The gateway caps a page at 1000 and does NOT signal truncation to a
|
|
207
|
+
// naive caller. A set that silently stops at 1000 cuts a version
|
|
208
|
+
// missing the rest, and the experiment built from it looks complete.
|
|
209
|
+
// Fixed at 1050 on purpose — a consumer whose backend makes this too
|
|
210
|
+
// slow to run every commit should filter storeConformance's returned
|
|
211
|
+
// array by name and skip this one check in their own test file, not
|
|
212
|
+
// shrink the count here: a smaller count can pass against a `list`
|
|
213
|
+
// that duplicates or drops keys just as easily as against a correct
|
|
214
|
+
// one, which would make the check stop proving anything and no test
|
|
215
|
+
// output would say so.
|
|
216
|
+
const n = 1050;
|
|
217
|
+
const expected = new Set();
|
|
218
|
+
// Bounded concurrency: 1050 sequential round trips over HTTP is the
|
|
219
|
+
// slow part of this check, not `list` itself — 32 in flight is fast
|
|
220
|
+
// against a real gateway without opening 1050 sockets at once.
|
|
221
|
+
await pool(Array.from({ length: n }, (_, i) => i), 32, async (i) => {
|
|
222
|
+
const key = `${root}/${i}.json`;
|
|
223
|
+
await s.put(key, String(i));
|
|
224
|
+
expected.add(key);
|
|
225
|
+
});
|
|
226
|
+
const keys = await s.list(`${root}/`);
|
|
227
|
+
assert(keys.length === n, `expected ${n} keys, got ${keys.length}`);
|
|
228
|
+
assert(new Set(keys).size === n, "list must not return duplicate keys across pages");
|
|
229
|
+
assert(keys.every((key) => expected.has(key)), "list must return exactly the keys written, not just the right count");
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
/**
|
|
234
|
+
* EVERY check is gated, not one of them.
|
|
235
|
+
*
|
|
236
|
+
* The gate first lived inside the concurrent check, reasoning that a
|
|
237
|
+
* standalone entry could be filtered away. But filtering by NAME is exactly
|
|
238
|
+
* what the paging check above documents as the supported way to skip a check
|
|
239
|
+
* a consumer's backend makes too slow — and that same filter drops a gate
|
|
240
|
+
* hidden in any one entry. Wrapping instead costs one string comparison per
|
|
241
|
+
* check and cannot be skipped by any subset a consumer chooses to run:
|
|
242
|
+
* whichever checks you keep, the store had to say how `putIfAbsent` is
|
|
243
|
+
* atomic before any of them ran.
|
|
244
|
+
*
|
|
245
|
+
* `makeStore()` is still called exactly once per check. Consumers depend on
|
|
246
|
+
* that — `eval-store.test.ts` mints one gateway credential in `beforeAll`
|
|
247
|
+
* precisely because this is per-entry and not per-operation.
|
|
248
|
+
*/
|
|
249
|
+
return checks.map(({ name, body }) => ({
|
|
250
|
+
name,
|
|
251
|
+
run: async () => {
|
|
252
|
+
const s = await makeStore();
|
|
253
|
+
await body(s, declaredMechanism(s));
|
|
254
|
+
},
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Case, Experiment, Header, Limits, RunRecord, VersionManifest } from "./types.js";
|
|
2
|
+
export declare class EvalContractError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** A plain JSON object: not null, not an array. */
|
|
6
|
+
export declare const isObject: (v: unknown) => v is Record<string, unknown>;
|
|
7
|
+
export declare function parseLimits(raw: unknown, where: string): Limits | undefined;
|
|
8
|
+
export declare function parseHeader(raw: unknown): Header;
|
|
9
|
+
export declare function parseCase(raw: unknown): Case;
|
|
10
|
+
export declare function parseVersionManifest(raw: unknown, dataset: string, version: number): VersionManifest;
|
|
11
|
+
/**
|
|
12
|
+
* A run record read back. Lenient: the library wrote it. A record written
|
|
13
|
+
* before v2 numbers its repeat `trial`; it reads as `repeat`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseRunRecord(raw: unknown, where: string): RunRecord;
|
|
16
|
+
/** An experiment's meta read back. Lenient: the library wrote it. */
|
|
17
|
+
export declare function parseExperiment(raw: unknown, where: string): Experiment;
|
|
18
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAQ,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAErG,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI5B;AAED,mDAAmD;AACnD,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,KAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACP,CAAC;AAiB3D,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU3E;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAwBhD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CA0C5C;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAoBpG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAQrE;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAQvE"}
|