@mutagent/evaluator 0.1.0-alpha.5 → 0.2.0-alpha.1
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/.claude/skills/mutagent-evaluator/SKILL.md +28 -30
- package/.claude/skills/mutagent-evaluator/assets/agents/dataset-builder.md +6 -5
- package/.claude/skills/mutagent-evaluator/assets/agents/evaluator.md +93 -30
- package/.claude/skills/mutagent-evaluator/assets/code-quality-criteria.yaml +75 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/discover-report.template.html +397 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/eval-report.template.html +594 -0
- package/.claude/skills/mutagent-evaluator/assets/templates/review-report.template.html +560 -0
- package/.claude/skills/mutagent-evaluator/golden/judge-trajectory.prose.md +69 -0
- package/.claude/skills/mutagent-evaluator/lenses/methodology-critic-lens.md +1 -1
- package/.claude/skills/mutagent-evaluator/references/data-registry.md +43 -0
- package/.claude/skills/mutagent-evaluator/references/edd-loop.md +6 -6
- package/.claude/skills/mutagent-evaluator/references/eval-layers.md +52 -0
- package/.claude/skills/mutagent-evaluator/references/eval-stage.md +37 -4
- package/.claude/skills/mutagent-evaluator/references/memory-format.md +1 -1
- package/.claude/skills/mutagent-evaluator/schemas/methodology-review.schema.yaml +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/agent-dispatch.ts +0 -0
- package/.claude/skills/mutagent-evaluator/scripts/aggregate-discover.ts +254 -9
- package/.claude/skills/mutagent-evaluator/scripts/audience.ts +84 -0
- package/.claude/skills/mutagent-evaluator/scripts/build-review-ui.ts +320 -6
- package/.claude/skills/mutagent-evaluator/scripts/cli/prep.ts +32 -2
- package/.claude/skills/mutagent-evaluator/scripts/cli/profile-subject.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/code-eval-library.ts +201 -0
- package/.claude/skills/mutagent-evaluator/scripts/code-quality-verdict.ts +60 -68
- package/.claude/skills/mutagent-evaluator/scripts/config/schema.ts +3 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/agentspec-evals.ts +101 -39
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-matrix.ts +326 -3
- package/.claude/skills/mutagent-evaluator/scripts/contracts/eval-types.ts +30 -4
- package/.claude/skills/mutagent-evaluator/scripts/determine-outcome.ts +8 -0
- package/.claude/skills/mutagent-evaluator/scripts/edd/change-request.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/edd/edd-types.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/judge-prompt-template.ts +144 -15
- package/.claude/skills/mutagent-evaluator/scripts/materialize-dataset.ts +130 -68
- package/.claude/skills/mutagent-evaluator/scripts/matrix-judge.ts +219 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/append.ts +1 -1
- package/.claude/skills/mutagent-evaluator/scripts/memory/ratify.ts +165 -0
- package/.claude/skills/mutagent-evaluator/scripts/merge-criteria.ts +849 -0
- package/.claude/skills/mutagent-evaluator/scripts/prep-tasks.ts +15 -4
- package/.claude/skills/mutagent-evaluator/scripts/read-manifest.ts +120 -0
- package/.claude/skills/mutagent-evaluator/scripts/read-unitf-traces.ts +34 -2
- package/.claude/skills/mutagent-evaluator/scripts/render-build-cards.ts +37 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-discover-report-v3.ts +1155 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report-v3.ts +610 -0
- package/.claude/skills/mutagent-evaluator/scripts/render-eval-report.ts +11 -92
- package/.claude/skills/mutagent-evaluator/scripts/render-review-report-v3.ts +691 -0
- package/.claude/skills/mutagent-evaluator/scripts/report-fragments.ts +173 -0
- package/.claude/skills/mutagent-evaluator/scripts/route-failures.ts +12 -9
- package/.claude/skills/mutagent-evaluator/scripts/run-evaluate.ts +46 -1
- package/.claude/skills/mutagent-evaluator/scripts/run-pipeline.ts +39 -4
- package/.claude/skills/mutagent-evaluator/scripts/run-review.ts +266 -0
- package/.claude/skills/mutagent-evaluator/scripts/subject-profile.ts +82 -0
- package/.claude/skills/mutagent-evaluator/scripts/sync-eval-criteria.ts +2 -2
- package/.claude/skills/mutagent-evaluator/scripts/unitf-to-evaltrace.ts +64 -21
- package/.claude/skills/mutagent-evaluator/scripts/verify-render.ts +728 -0
- package/bin/mutagent-cli.mjs +9 -5
- package/package.json +2 -2
|
@@ -48,20 +48,94 @@ function promptOf(trace: EvalTrace): string {
|
|
|
48
48
|
|
|
49
49
|
// ── Determiner (EV-042) prompt — MIRRORS assets/agents/error-analyst.md ──────
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* EV-1 — the compact SUBJECT FRAME the determiner reads so it derives THIS subject's
|
|
53
|
+
* expected outcome (identity · purpose · tools + reversibility), instead of judging
|
|
54
|
+
* against a hardcoded email-agent signal block. Trajectory-LIGHT by design — it never
|
|
55
|
+
* carries the full step-by-step trajectory (that is the per-criterion judge's job).
|
|
56
|
+
*/
|
|
57
|
+
export interface SubjectFrame {
|
|
58
|
+
identity: string;
|
|
59
|
+
purpose: string;
|
|
60
|
+
tools: string[];
|
|
61
|
+
toolReversibility?: { name: string; reversibility: string }[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The distinct tool names actually invoked in a trace, in first-seen order (compact summary). */
|
|
65
|
+
function keyToolsUsed(trace: EvalTrace, limit = 8): string[] {
|
|
66
|
+
const seen: string[] = [];
|
|
67
|
+
for (const o of trace.observations) {
|
|
68
|
+
if (o.type === "TOOL" && typeof o.name === "string" && !seen.includes(o.name)) {
|
|
69
|
+
seen.push(o.name);
|
|
70
|
+
if (seen.length >= limit) break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return seen;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Render the subject frame block for the determiner (EV-1). Trajectory-light. */
|
|
77
|
+
function subjectFrameBlock(subject: SubjectFrame | undefined): string[] {
|
|
78
|
+
if (subject === undefined) {
|
|
79
|
+
return [
|
|
80
|
+
"SUBJECT: not supplied — RECONSTRUCT what the agent is from the input event + the",
|
|
81
|
+
"tools it called, and derive the expected outcome from that. Do NOT assume any",
|
|
82
|
+
"particular domain (e.g. email).",
|
|
83
|
+
"",
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
const rev = new Map((subject.toolReversibility ?? []).map((t) => [t.name, t.reversibility]));
|
|
87
|
+
const toolLine =
|
|
88
|
+
subject.tools.length > 0
|
|
89
|
+
? subject.tools
|
|
90
|
+
.map((n) => {
|
|
91
|
+
const r = rev.get(n);
|
|
92
|
+
return r !== undefined && r !== "unknown" ? `${n} [${r}]` : n;
|
|
93
|
+
})
|
|
94
|
+
.join(", ")
|
|
95
|
+
: "(none)";
|
|
96
|
+
const irreversible = (subject.toolReversibility ?? [])
|
|
97
|
+
.filter((t) => t.reversibility === "irreversible-external")
|
|
98
|
+
.map((t) => t.name);
|
|
99
|
+
return [
|
|
100
|
+
"SUBJECT (derive the EXPECTED outcome for THIS subject before you judge):",
|
|
101
|
+
` identity: ${subject.identity}`,
|
|
102
|
+
` purpose: ${subject.purpose}`,
|
|
103
|
+
` tools: ${toolLine}`,
|
|
104
|
+
...(irreversible.length > 0
|
|
105
|
+
? [` ⚠ irreversible-external tools: ${irreversible.join(", ")} — a failure that also fired one is materially worse.`]
|
|
106
|
+
: []),
|
|
107
|
+
"",
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
51
111
|
/**
|
|
52
112
|
* Render the determiner judge prompt for the in-house/export substrate. The
|
|
53
113
|
* AUTHORITATIVE "inaction can be success" rubric is `error-analyst.md`; this is
|
|
54
114
|
* its provider-callable mirror. No decision is made here.
|
|
115
|
+
*
|
|
116
|
+
* EV-1 — subject-aware: given a `subject` frame the determiner derives the expected
|
|
117
|
+
* outcome from the subject's identity/purpose/tools (not a fixed email-agent signal
|
|
118
|
+
* block), and reads a COMPACT outcome summary (event · did-it-act · key tools ·
|
|
119
|
+
* terminal state) — deliberately NOT the full trajectory (the per-criterion judge's
|
|
120
|
+
* job). The domain-specific signals (send / guard / recovery) render ONLY when the
|
|
121
|
+
* subject vocab actually defines them, so a non-email subject no longer sees dead lines.
|
|
55
122
|
*/
|
|
56
123
|
export function buildOutcomePrompt(
|
|
57
124
|
trace: EvalTrace,
|
|
58
125
|
signals: OutcomeSignals,
|
|
59
126
|
vocab: SubjectVocab,
|
|
127
|
+
subject?: SubjectFrame,
|
|
60
128
|
): { system: string; user: string } {
|
|
61
129
|
const system = [
|
|
62
130
|
"You are a success/failure determiner for an autonomous agent session.",
|
|
63
131
|
"Decide whether the session REACHED THE GOAL implied by its input event.",
|
|
64
132
|
"",
|
|
133
|
+
"Derive the EXPECTED outcome from the SUBJECT (its identity · purpose · tools) +",
|
|
134
|
+
"the input event — do NOT assume any particular domain (e.g. email). This is a",
|
|
135
|
+
"FAST input→output outcome check: read the goal (input), the result (output/self-",
|
|
136
|
+
"summary) and the compact summary below — NOT the full step-by-step trajectory",
|
|
137
|
+
"(that is the per-criterion judge's job).",
|
|
138
|
+
"",
|
|
65
139
|
"CRITICAL RULE — inaction can be success. Holding (sending nothing, calling",
|
|
66
140
|
"no tool) is the CORRECT outcome when the event is a restraint directive",
|
|
67
141
|
"(e.g. a guard/hold directive) or when acting would be wrong. You MUST NOT",
|
|
@@ -74,23 +148,39 @@ export function buildOutcomePrompt(
|
|
|
74
148
|
"Reason first in `critique`, then commit to `result`.",
|
|
75
149
|
].join("\n");
|
|
76
150
|
|
|
77
|
-
|
|
78
|
-
const
|
|
151
|
+
const responseText = typeof trace.output?.response === "string" ? trace.output.response : "";
|
|
152
|
+
const keyTools = keyToolsUsed(trace);
|
|
153
|
+
|
|
154
|
+
// EV-1 — domain signals render ONLY when the subject vocab defines them (else they
|
|
155
|
+
// were email-agent noise for every subject). honest-null preserved.
|
|
156
|
+
const conditionalSignals: string[] = [];
|
|
157
|
+
if (vocab.guardCounterAttr !== null) {
|
|
158
|
+
conditionalSignals.push(` Guard ${vocab.guardCounterAttr}: ${signals.guardConsecutive ?? "n/a"}`);
|
|
159
|
+
}
|
|
160
|
+
if (vocab.sendTool.length > 0) {
|
|
161
|
+
conditionalSignals.push(
|
|
162
|
+
` Sent a message: ${signals.sentMessage === null ? "unknown" : signals.sentMessage}`,
|
|
163
|
+
` Send succeeded: ${signals.sendSucceeded ?? "n/a (no send)"}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (vocab.recoveryTools.length > 0) {
|
|
167
|
+
conditionalSignals.push(` Recovery tool present: ${signals.recoveryPresent}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
79
170
|
const user = [
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
`Recovery tool present: ${signals.recoveryPresent}`,
|
|
171
|
+
...subjectFrameBlock(subject),
|
|
172
|
+
"OUTCOME SUMMARY (compact — NOT the full trajectory):",
|
|
173
|
+
` Event kind: ${signals.eventKind}`,
|
|
174
|
+
` Tools called: ${signals.toolCount}${keyTools.length > 0 ? ` · key tools: ${keyTools.join(", ")}` : ""}`,
|
|
175
|
+
` Did the agent act: ${signals.toolCount > 0 ? "yes (called tools)" : "no (no tool calls)"}`,
|
|
176
|
+
` Terminal state: ${responseText.length > 0 ? "produced a self-summary/response" : "no self-summary"}`,
|
|
177
|
+
...conditionalSignals,
|
|
88
178
|
"",
|
|
89
179
|
"Input event prompt:",
|
|
90
180
|
promptOf(trace),
|
|
91
181
|
"",
|
|
92
182
|
"Agent self-summary (output.response):",
|
|
93
|
-
|
|
183
|
+
responseText.length > 0 ? responseText : "(none)",
|
|
94
184
|
].join("\n");
|
|
95
185
|
|
|
96
186
|
return { system, user };
|
|
@@ -107,10 +197,13 @@ export async function determineOutcome(
|
|
|
107
197
|
trace: EvalTrace,
|
|
108
198
|
judge: JudgeInvoke,
|
|
109
199
|
vocab: SubjectVocab,
|
|
200
|
+
subject?: SubjectFrame,
|
|
110
201
|
): Promise<OutcomeResult> {
|
|
111
202
|
const signals = extractOutcomeSignals(trace, vocab);
|
|
112
|
-
const { system, user } = buildOutcomePrompt(trace, signals, vocab);
|
|
113
|
-
|
|
203
|
+
const { system, user } = buildOutcomePrompt(trace, signals, vocab, subject);
|
|
204
|
+
// INF-3 — pass the trace id so the determiner verdict is keyed PER-TRACE (two
|
|
205
|
+
// sessions with an identical determiner prompt never share one verdict).
|
|
206
|
+
const raw = await judge(system, user, trace.id);
|
|
114
207
|
const verdict = parseCritiqueVerdict(raw);
|
|
115
208
|
return {
|
|
116
209
|
traceId: trace.id,
|
|
@@ -153,7 +246,8 @@ function profilePreamble(profile?: SubjectProfile): string[] {
|
|
|
153
246
|
` identity: ${profile.identity}`,
|
|
154
247
|
` purpose: ${profile.purpose}`,
|
|
155
248
|
` scope: ${profile.scope}`,
|
|
156
|
-
` tools: ${(profile
|
|
249
|
+
` tools: ${renderToolLine(profile)}`,
|
|
250
|
+
...reversibilityCallout(profile),
|
|
157
251
|
profile.skill !== undefined ? ` skill: ${profile.skill}` : "",
|
|
158
252
|
` harness: ${profile.harness}`,
|
|
159
253
|
` provenance: ${profile.provenance}${profile.version !== undefined ? ` · version ${profile.version}` : ""}`,
|
|
@@ -161,6 +255,36 @@ function profilePreamble(profile?: SubjectProfile): string[] {
|
|
|
161
255
|
].filter((l) => l !== "");
|
|
162
256
|
}
|
|
163
257
|
|
|
258
|
+
/** Render the tools line, annotating each tool with its EV-5 reversibility when known. */
|
|
259
|
+
function renderToolLine(profile: SubjectProfile): string {
|
|
260
|
+
const tools = profile.tools ?? [];
|
|
261
|
+
if (tools.length === 0) return "(none observed)";
|
|
262
|
+
const rev = new Map((profile.toolReversibility ?? []).map((t) => [t.name, t.reversibility]));
|
|
263
|
+
return tools
|
|
264
|
+
.map((name) => {
|
|
265
|
+
const r = rev.get(name);
|
|
266
|
+
return r !== undefined && r !== "unknown" ? `${name} [${r}]` : name;
|
|
267
|
+
})
|
|
268
|
+
.join(", ");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* EV-5 — a one-line callout naming the tools that take IRREVERSIBLE EXTERNAL actions,
|
|
273
|
+
* so a failure that ALSO fired one weighs heavier than a read-only failure. Absent
|
|
274
|
+
* reversibility data ⇒ no callout (the judge still reads the bare tool list).
|
|
275
|
+
*/
|
|
276
|
+
function reversibilityCallout(profile: SubjectProfile): string[] {
|
|
277
|
+
const irreversible = (profile.toolReversibility ?? [])
|
|
278
|
+
.filter((t) => t.reversibility === "irreversible-external")
|
|
279
|
+
.map((t) => t.name);
|
|
280
|
+
if (irreversible.length === 0) return [];
|
|
281
|
+
return [
|
|
282
|
+
` ⚠ irreversible-external tools: ${irreversible.join(", ")} — a failure that ALSO`,
|
|
283
|
+
" fired one of these (an email sent, a live system mutated) is MATERIALLY WORSE",
|
|
284
|
+
" than a read-only failure; weigh that in your critique.",
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
164
288
|
/**
|
|
165
289
|
* Render the 4-component per-criterion judge prompt for the in-house/export
|
|
166
290
|
* substrate. The AUTHORITATIVE 4-component / binary / critique-before-verdict
|
|
@@ -227,6 +351,11 @@ export async function runJudge(
|
|
|
227
351
|
subjectTrace: EvalTrace,
|
|
228
352
|
judge: JudgeInvoke,
|
|
229
353
|
pin: JudgePin,
|
|
354
|
+
// EV-5 CUT WIRE 1 — the M1 subject profile the judge reads in its preamble (tools +
|
|
355
|
+
// reversibility). Pre-fix runJudge called buildJudgePrompt WITHOUT it, so the export
|
|
356
|
+
// path always hit the "profile not supplied — reconstruct" branch and never saw the
|
|
357
|
+
// reversibility flags. The pipeline now builds it once (buildSubjectProfile) and passes it.
|
|
358
|
+
subjectProfile?: SubjectProfile,
|
|
230
359
|
): Promise<CriterionVerdict> {
|
|
231
360
|
if (typeof pin.modelId !== "string" || pin.modelId.length === 0) {
|
|
232
361
|
throw new Error(
|
|
@@ -240,7 +369,7 @@ export async function runJudge(
|
|
|
240
369
|
"MODEL INTENT IS SACRED: reruns must be byte-identical (C-PIN); refusing.",
|
|
241
370
|
);
|
|
242
371
|
}
|
|
243
|
-
const { system, user } = buildJudgePrompt(spec, subjectTrace);
|
|
372
|
+
const { system, user } = buildJudgePrompt(spec, subjectTrace, subjectProfile);
|
|
244
373
|
const raw = await judge(system, user);
|
|
245
374
|
const verdict = parseCritiqueVerdict(raw);
|
|
246
375
|
return {
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* scripts/materialize-dataset.ts — F8:
|
|
3
|
-
*
|
|
2
|
+
* scripts/materialize-dataset.ts — F8: PROJECT the agentspec's real dataset items
|
|
3
|
+
* into runnable evaluator DatasetCases (Type A — PURE).
|
|
4
4
|
* ---------------------------------------------------------------------------
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* AgentSpec 0.3.0 (D18/D19) moved the dataset from a DEFINITION (0.2's
|
|
6
|
+
* `dataset_categories[] × edge_cases[]`, which the evaluator had to synthesize
|
|
7
|
+
* base+edge queries from) to `spec.evaluation.datasets[].items[]` — REAL,
|
|
8
|
+
* already-materialized rows, each carrying `category` / `scenarioRef` / a `case`
|
|
9
|
+
* assignment over the dataset's `caseDimensions`, plus kind-specific `input` /
|
|
10
|
+
* `expected` payloads.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* - one
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* So materialization is now a faithful PROJECTION, not a synthesis:
|
|
13
|
+
* - one DatasetCase per spec item (the seed layer, source: "seed").
|
|
14
|
+
* - the case TUPLE = the item's `category` + its `case` assignment (the axes the
|
|
15
|
+
* dataset varies over — see `dimensionsFromAgentspec`).
|
|
16
|
+
* - the case QUERY = the item's opaque `input` payload rendered deterministically
|
|
17
|
+
* (raw string when `input` is a string; a stable key-sorted serialization
|
|
18
|
+
* otherwise), so a re-projection is byte-identical.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* The dataset-builder agent then EXPANDS these seeds synthetically; build-dataset.ts's
|
|
21
|
+
* deterministic id/dedup/merge dedups any overlap. Subject-agnostic (EV-002): axes
|
|
22
|
+
* and values come from the agentspec, never hard-coded. DETERMINISTIC — items and
|
|
23
|
+
* axes consumed in given order; no clock / random / network → re-projecting is
|
|
24
|
+
* byte-identical (C-PIN).
|
|
22
25
|
*/
|
|
23
26
|
import {
|
|
24
27
|
type Dataset,
|
|
@@ -26,88 +29,147 @@ import {
|
|
|
26
29
|
type DatasetTuple,
|
|
27
30
|
type Dimension,
|
|
28
31
|
} from "./contracts/dataset.ts";
|
|
29
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
SpecEvaluation,
|
|
34
|
+
SpecEvalDatasetItem,
|
|
35
|
+
} from "./contracts/agentspec-evals.ts";
|
|
30
36
|
import { buildCase, appendToDataset } from "./build-dataset.ts";
|
|
31
37
|
import { CaseSource } from "./contracts/dataset.ts";
|
|
32
38
|
|
|
33
|
-
/** The
|
|
34
|
-
const
|
|
35
|
-
const EDGE_FALSE = "false";
|
|
39
|
+
/** The tuple key under which an item's dataset-local category is recorded. */
|
|
40
|
+
const CATEGORY_DIM = "category";
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
43
|
+
* Deterministic, key-sorted serialization — a stable rendering of an opaque item
|
|
44
|
+
* payload so two projections of the same payload are byte-identical regardless of
|
|
45
|
+
* key insertion order. PURE.
|
|
41
46
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
function stableStringify(value: unknown): string {
|
|
48
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
49
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
50
|
+
const record = value as Record<string, unknown>;
|
|
51
|
+
const body = Object.keys(record)
|
|
52
|
+
.sort()
|
|
53
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(record[k])}`)
|
|
54
|
+
.join(",");
|
|
55
|
+
return `{${body}}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The dimensions the projected dataset varies over: `category` (values = the
|
|
60
|
+
* dataset-local category ids, first-seen order) + one dimension per distinct
|
|
61
|
+
* `caseDimensions` axis (values merged across datasets, first-seen order). Both are
|
|
62
|
+
* derived from the agentspec, never hard-coded. Subject-agnostic. PURE.
|
|
63
|
+
*/
|
|
64
|
+
export function dimensionsFromAgentspec(evaluation: SpecEvaluation): Dimension[] {
|
|
44
65
|
const dims: Dimension[] = [];
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
|
|
67
|
+
// category dimension — the union of dataset-local category ids (first-seen, deduped).
|
|
68
|
+
const categoryIds: string[] = [];
|
|
69
|
+
const seenCategory = new Set<string>();
|
|
70
|
+
for (const dataset of evaluation.datasets) {
|
|
71
|
+
for (const category of dataset.categories ?? []) {
|
|
72
|
+
if (seenCategory.has(category.id)) continue;
|
|
73
|
+
seenCategory.add(category.id);
|
|
74
|
+
categoryIds.push(category.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (categoryIds.length > 0) {
|
|
78
|
+
dims.push({ name: CATEGORY_DIM, description: "the dataset-local category slice", values: categoryIds });
|
|
47
79
|
}
|
|
48
|
-
dims.push({ name: "edge_case", description: "is this an edge/adversarial case", values: [EDGE_FALSE, EDGE_TRUE] });
|
|
49
|
-
return dims;
|
|
50
|
-
}
|
|
51
80
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
// caseDimensions — each named axis, values merged across datasets (first-seen, deduped).
|
|
82
|
+
const axisOrder: string[] = [];
|
|
83
|
+
const axes = new Map<string, { description?: string; values: string[]; seen: Set<string> }>();
|
|
84
|
+
for (const dataset of evaluation.datasets) {
|
|
85
|
+
for (const [name, axis] of Object.entries(dataset.caseDimensions ?? {})) {
|
|
86
|
+
let entry = axes.get(name);
|
|
87
|
+
if (entry === undefined) {
|
|
88
|
+
entry = { values: [], seen: new Set<string>() };
|
|
89
|
+
if (axis.description !== undefined) entry.description = axis.description;
|
|
90
|
+
axes.set(name, entry);
|
|
91
|
+
axisOrder.push(name);
|
|
92
|
+
}
|
|
93
|
+
for (const value of axis.values) {
|
|
94
|
+
if (entry.seen.has(value)) continue;
|
|
95
|
+
entry.seen.add(value);
|
|
96
|
+
entry.values.push(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const name of axisOrder) {
|
|
101
|
+
const entry = axes.get(name);
|
|
102
|
+
if (entry === undefined) continue;
|
|
103
|
+
const dim: Dimension = { name, values: entry.values };
|
|
104
|
+
if (entry.description !== undefined) dim.description = entry.description;
|
|
105
|
+
dims.push(dim);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return dims;
|
|
55
109
|
}
|
|
56
110
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The variation tuple an item realizes — its dataset-local `category` (when present)
|
|
113
|
+
* plus its `case` axis assignments. Falls back to the item id when an item declares
|
|
114
|
+
* neither (a DatasetTuple must have ≥1 assignment). PURE.
|
|
115
|
+
*/
|
|
116
|
+
function tupleFromItem(item: SpecEvalDatasetItem): DatasetTuple {
|
|
117
|
+
const tuple: DatasetTuple = {};
|
|
118
|
+
if (item.category !== undefined && item.category.length > 0) tuple[CATEGORY_DIM] = item.category;
|
|
119
|
+
for (const [axis, value] of Object.entries(item.case ?? {})) tuple[axis] = value;
|
|
120
|
+
if (Object.keys(tuple).length === 0) tuple.item = item.id;
|
|
121
|
+
return tuple;
|
|
61
122
|
}
|
|
62
123
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The natural-language query an item realizes — its opaque `input` payload rendered
|
|
126
|
+
* deterministically. A string input is used verbatim; any other payload is
|
|
127
|
+
* stable-serialized; a missing payload falls back to a deterministic descriptor so
|
|
128
|
+
* the query is always non-empty (DatasetCase requires it). PURE.
|
|
129
|
+
*/
|
|
130
|
+
function queryFromItem(item: SpecEvalDatasetItem): string {
|
|
131
|
+
const input = item.input;
|
|
132
|
+
if (typeof input === "string" && input.length > 0) return input;
|
|
133
|
+
if (input !== undefined) {
|
|
134
|
+
const rendered = stableStringify(input);
|
|
135
|
+
if (rendered.length > 0) return rendered;
|
|
136
|
+
}
|
|
137
|
+
const context = item.category ?? item.scenarioRef ?? item.id;
|
|
138
|
+
return `[${context}] ${item.id}`;
|
|
67
139
|
}
|
|
68
140
|
|
|
69
141
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
142
|
+
* PROJECT the agentspec's real dataset items (0.3.0 `spec.evaluation.datasets[].items[]`)
|
|
143
|
+
* into runnable DatasetCases (F8), one seed case per item. DETERMINISTIC, deduped by
|
|
144
|
+
* content id (a re-projection collides → drop). Returns [] when no dataset carries
|
|
145
|
+
* items. PURE.
|
|
74
146
|
*/
|
|
75
|
-
export function materializeFromAgentspec(
|
|
147
|
+
export function materializeFromAgentspec(evaluation: SpecEvaluation): DatasetCase[] {
|
|
76
148
|
const out: DatasetCase[] = [];
|
|
77
149
|
const seen = new Set<string>();
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const cat of evals.dataset_categories) {
|
|
85
|
-
// base item — one real query per category.
|
|
86
|
-
push(
|
|
87
|
-
{ category: cat.id, edge_case: EDGE_FALSE },
|
|
88
|
-
baseQueryFor(evals, cat.id, cat.description),
|
|
89
|
-
);
|
|
90
|
-
// edge items — one real query per declared edge_case.
|
|
91
|
-
for (const edge of cat.edge_cases) {
|
|
92
|
-
if (edge.length === 0) continue;
|
|
93
|
-
push({ category: cat.id, edge_case: EDGE_TRUE }, edgeQueryFor(cat.id, edge));
|
|
150
|
+
for (const dataset of evaluation.datasets) {
|
|
151
|
+
for (const item of dataset.items ?? []) {
|
|
152
|
+
const c = buildCase(tupleFromItem(item), queryFromItem(item), CaseSource.Seed);
|
|
153
|
+
if (seen.has(c.id)) continue; // content-id dedup (re-project collides → drop)
|
|
154
|
+
seen.add(c.id);
|
|
155
|
+
out.push(c);
|
|
94
156
|
}
|
|
95
157
|
}
|
|
96
158
|
return out;
|
|
97
159
|
}
|
|
98
160
|
|
|
99
161
|
/**
|
|
100
|
-
* Assemble (or extend) a Dataset seeded with the
|
|
101
|
-
* `existing` is supplied, the
|
|
162
|
+
* Assemble (or extend) a Dataset seeded with the projected real items. When
|
|
163
|
+
* `existing` is supplied, the projected cases are merged MONOTONICALLY (no
|
|
102
164
|
* duplicates, version bumps). Subject-agnostic. DETERMINISTIC. PURE.
|
|
103
165
|
*/
|
|
104
166
|
export function materializeToDataset(
|
|
105
167
|
subject: string,
|
|
106
|
-
|
|
168
|
+
evaluation: SpecEvaluation,
|
|
107
169
|
existing?: Dataset,
|
|
108
170
|
): Dataset {
|
|
109
|
-
const dimensions = dimensionsFromAgentspec(
|
|
110
|
-
const cases = materializeFromAgentspec(
|
|
171
|
+
const dimensions = dimensionsFromAgentspec(evaluation);
|
|
172
|
+
const cases = materializeFromAgentspec(evaluation);
|
|
111
173
|
const base: Dataset = existing ?? { subject, dimensions, cases: [], version: 0 };
|
|
112
174
|
return appendToDataset(base, cases);
|
|
113
175
|
}
|