@skill-harness/core 0.11.0 → 0.13.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 +2 -1
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adjudication.d.ts +9 -1
- package/dist/adjudication.js +41 -33
- package/dist/adoption.d.ts +128 -0
- package/dist/adoption.js +102 -0
- package/dist/arms.d.ts +1 -1
- package/dist/capture-trace-types.d.ts +2 -0
- package/dist/command-cost.d.ts +9 -0
- package/dist/command-cost.js +23 -0
- package/dist/comparison.d.ts +1 -0
- package/dist/comparison.js +9 -3
- package/dist/execution-trace.js +9 -1
- package/dist/factory-calibration.d.ts +63 -0
- package/dist/factory-calibration.js +115 -0
- package/dist/grade.d.ts +2 -0
- package/dist/grade.js +2 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/intervention-results.d.ts +6 -0
- package/dist/intervention-results.js +52 -0
- package/dist/intervention.d.ts +123 -0
- package/dist/intervention.js +218 -0
- package/dist/investigation.d.ts +94 -0
- package/dist/investigation.js +148 -0
- package/dist/journal.d.ts +1 -1
- package/dist/judge-agreement.d.ts +19 -0
- package/dist/judge-agreement.js +31 -0
- package/dist/judge-policy.js +5 -3
- package/dist/lift.js +1 -1
- package/dist/lint.js +1 -1
- package/dist/prompt-normalization.d.ts +6 -0
- package/dist/prompt-normalization.js +14 -0
- package/dist/qualification-capture.d.ts +44 -0
- package/dist/qualification-capture.js +163 -0
- package/dist/qualification-config.d.ts +233 -0
- package/dist/qualification-config.js +694 -0
- package/dist/qualification-lock.d.ts +15 -0
- package/dist/qualification-lock.js +87 -0
- package/dist/qualification-oauth-directory.d.ts +79 -0
- package/dist/qualification-oauth-directory.js +444 -0
- package/dist/qualification-panel-store.d.ts +14 -0
- package/dist/qualification-panel-store.js +188 -0
- package/dist/qualification-panels.d.ts +99 -0
- package/dist/qualification-panels.js +91 -0
- package/dist/qualification-process.d.ts +13 -0
- package/dist/qualification-process.js +190 -0
- package/dist/qualification-runner.d.ts +203 -0
- package/dist/qualification-runner.js +2037 -0
- package/dist/qualification-store.d.ts +187 -0
- package/dist/qualification-store.js +833 -0
- package/dist/qualification-validation.d.ts +12 -0
- package/dist/qualification-validation.js +8 -0
- package/dist/regate.js +42 -10
- package/dist/regrade.d.ts +3 -1
- package/dist/regrade.js +37 -5
- package/dist/reps.d.ts +4 -2
- package/dist/reps.js +13 -4
- package/dist/rescore.js +2 -1
- package/dist/results.d.ts +53 -7
- package/dist/results.js +210 -7
- package/dist/run.js +66 -17
- package/dist/score.d.ts +4 -3
- package/dist/score.js +13 -4
- package/dist/screen.d.ts +31 -0
- package/dist/screen.js +105 -0
- package/dist/seeded.d.ts +2 -1
- package/dist/seeded.js +1 -0
- package/dist/sources.d.ts +7 -1
- package/dist/sources.js +13 -5
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +1 -1
- package/dist/stability.js +1 -1
- package/dist/trajectory-gates.d.ts +14 -14
- package/dist/trajectory-gates.js +18 -34
- package/dist/trusted-host-supervision.d.ts +25 -0
- package/dist/trusted-host-supervision.js +94 -0
- package/dist/vote-panel.d.ts +33 -0
- package/dist/vote-panel.js +41 -0
- package/dist/work-capture-schema.d.ts +38 -0
- package/dist/work-capture-schema.js +33 -0
- package/dist/work-capture.d.ts +43 -0
- package/dist/work-capture.js +81 -0
- package/dist/work-signals.d.ts +55 -0
- package/dist/work-signals.js +71 -0
- package/dist/workspace.js +4 -1
- package/package.json +3 -3
- package/schemas/qualification-cell-result-v1.schema.json +32 -0
- package/schemas/qualification-config-v1.schema.json +244 -0
- package/schemas/qualification-invocation-request-v1.schema.json +59 -0
- package/schemas/qualification-judge-panel-result-v1.schema.json +42 -0
- package/schemas/results-v2.schema.json +19 -21
- package/schemas/results-v3.schema.json +654 -0
- package/schemas/specification-v1.schema.json +1 -1
- package/schemas/trajectory-event-v1.schema.json +20 -1
package/dist/results.js
CHANGED
|
@@ -4,6 +4,7 @@ import yaml from "js-yaml";
|
|
|
4
4
|
import { modelSlug } from "./adapters/types.js";
|
|
5
5
|
import { score } from "./score.js";
|
|
6
6
|
import { HARNESS_VERSION } from "./version.js";
|
|
7
|
+
import { collapseVotePanel } from "./vote-panel.js";
|
|
7
8
|
/** Merge bookkeeping from a later judge/gate pass without duplicating subject usage. */
|
|
8
9
|
export function mergeScenarioMetrics(prior, fresh) {
|
|
9
10
|
if (!prior)
|
|
@@ -22,11 +23,20 @@ export function mergeScenarioMetrics(prior, fresh) {
|
|
|
22
23
|
...(subject.cache_read_tokens === undefined ? {} : { cache_read_tokens: subject.cache_read_tokens }),
|
|
23
24
|
...(subject.cache_write_tokens === undefined ? {} : { cache_write_tokens: subject.cache_write_tokens }),
|
|
24
25
|
...(subject.subject_cost_usd === undefined ? {} : { subject_cost_usd: subject.subject_cost_usd }),
|
|
26
|
+
...(subject.cost_source === undefined ? {} : { cost_source: subject.cost_source }),
|
|
25
27
|
...(subject.tool_calls === undefined ? {} : { tool_calls: subject.tool_calls }),
|
|
26
28
|
...(subject.delegated_children === undefined ? {} : { delegated_children: subject.delegated_children }),
|
|
27
29
|
...(subject.max_concurrency === undefined ? {} : { max_concurrency: subject.max_concurrency }),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
export function carryRepObjectives(fresh, prior) {
|
|
33
|
+
if (!fresh)
|
|
34
|
+
return prior;
|
|
35
|
+
return fresh.map(panel => {
|
|
36
|
+
const objective = prior?.find(candidate => candidate.repetition === panel.repetition)?.objective;
|
|
37
|
+
return objective ? { ...panel, objective } : panel;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
30
40
|
/**
|
|
31
41
|
* The run modes whose results carry a real grade: the ones where the skill under
|
|
32
42
|
* test was actually delivered to the model.
|
|
@@ -159,6 +169,8 @@ function objectiveVerdict(s) {
|
|
|
159
169
|
return undefined;
|
|
160
170
|
if (s.objective.status === "ERROR")
|
|
161
171
|
return "ERROR";
|
|
172
|
+
if (s.objective.status === "NOT-MEASURED")
|
|
173
|
+
return "NOT-MEASURED";
|
|
162
174
|
if (s.objective.status === "FAIL")
|
|
163
175
|
return "FAIL";
|
|
164
176
|
return undefined;
|
|
@@ -179,8 +191,9 @@ export function finalizeResults(draft, ctx) {
|
|
|
179
191
|
const why = draft.partial ? "partial run (--only) — not scored" : `mode=${draft.mode} (not scored)`;
|
|
180
192
|
effective_grade = { passed: 0, total: 0, pct: 0, letter: "-", ship: false, note: why };
|
|
181
193
|
}
|
|
194
|
+
const schema = draft.schema ?? (draft.subject_invocations ? 3 : 2);
|
|
182
195
|
return {
|
|
183
|
-
schema
|
|
196
|
+
schema,
|
|
184
197
|
// Stamped here, the single place every writer passes through, so `run`,
|
|
185
198
|
// `grade`, `rescore` and the review UI's override save all record which tool
|
|
186
199
|
// produced the record they leave behind.
|
|
@@ -201,11 +214,13 @@ export function finalizeResults(draft, ctx) {
|
|
|
201
214
|
...(draft.source_hashes ? { source_hashes: draft.source_hashes } : {}),
|
|
202
215
|
effective_grade,
|
|
203
216
|
scenarios: draft.scenarios,
|
|
217
|
+
...(draft.subject_invocations ? { subject_invocations: draft.subject_invocations } : {}),
|
|
204
218
|
};
|
|
205
219
|
}
|
|
206
220
|
/** Finalize + persist results.yaml (creating the run dir). Returns what was written. */
|
|
207
221
|
export function writeResults(runDir, draft, ctx) {
|
|
208
222
|
const results = finalizeResults(draft, ctx);
|
|
223
|
+
validateResults(results);
|
|
209
224
|
mkdirSync(runDir, { recursive: true });
|
|
210
225
|
writeFileSync(resultsPath(runDir), yaml.dump(results, { lineWidth: 100 }), "utf8");
|
|
211
226
|
return results;
|
|
@@ -217,8 +232,8 @@ export function migrateResults(raw) {
|
|
|
217
232
|
throw new Error("empty or invalid results.yaml");
|
|
218
233
|
}
|
|
219
234
|
const o = raw;
|
|
220
|
-
if (o.schema === 2)
|
|
221
|
-
return raw;
|
|
235
|
+
if (o.schema === 2 || o.schema === 3)
|
|
236
|
+
return validateResults(raw);
|
|
222
237
|
const v1 = raw;
|
|
223
238
|
const modeMatch = /^mode=(\w+)/.exec(v1.grade?.note ?? "");
|
|
224
239
|
return {
|
|
@@ -250,6 +265,191 @@ export function readResults(runDir) {
|
|
|
250
265
|
const text = readFileSync(resultsPath(runDir), "utf8");
|
|
251
266
|
return migrateResults(yaml.load(text));
|
|
252
267
|
}
|
|
268
|
+
const CRITERION_RE = /^\s*(\d+)[.)]\s*\**\s*(PASS|FAIL)\b\**\s*(.*)$/gim;
|
|
269
|
+
/** Parse rubric-item votes for retention only; verdict semantics remain in grade.ts. */
|
|
270
|
+
export function parseCriterionVotes(raw) {
|
|
271
|
+
return [...raw.matchAll(CRITERION_RE)].map((match) => ({
|
|
272
|
+
index: Number(match[1]), verdict: match[2].toUpperCase(),
|
|
273
|
+
reason: match[3].trim().replace(/^[-—:]\s*/, ""),
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
export function completeCriterionVotes(votes, expected) {
|
|
277
|
+
return Array.from({ length: expected }, (_, offset) => votes.find(vote => vote.index === offset + 1) ?? {
|
|
278
|
+
index: offset + 1, verdict: "ERROR", reason: "judge emitted no parseable vote for this criterion",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
export function deliveryStatusForObservations(observations) {
|
|
282
|
+
if (observations.length === 0)
|
|
283
|
+
return "ERROR";
|
|
284
|
+
const terminalAttempt = Math.max(...observations.map(observation => observation.attempt ?? 0));
|
|
285
|
+
const terminal = observations.filter(observation => (observation.attempt ?? 0) === terminalAttempt);
|
|
286
|
+
if (terminal.some(observation => observation.prompt.status === "ERROR"))
|
|
287
|
+
return "ERROR";
|
|
288
|
+
const mechanism = terminal[0].prompt.mechanism;
|
|
289
|
+
if (terminal.some(observation => observation.prompt.mechanism !== mechanism))
|
|
290
|
+
return "ERROR";
|
|
291
|
+
const counts = terminal.map(observation => observation.prompt.contract_occurrences);
|
|
292
|
+
if (mechanism === "none")
|
|
293
|
+
return counts.every(count => count === 0) ? "PASS" : "NOT-MEASURED";
|
|
294
|
+
if (mechanism === "pi-skill") {
|
|
295
|
+
const firstDelivered = counts.indexOf(1);
|
|
296
|
+
return firstDelivered >= 0 && counts.slice(0, firstDelivered).every(count => count === 0) && counts.slice(firstDelivered).every(count => count === 1) ? "PASS" : "NOT-MEASURED";
|
|
297
|
+
}
|
|
298
|
+
return counts.every(count => count === 1) ? "PASS" : "NOT-MEASURED";
|
|
299
|
+
}
|
|
300
|
+
export function recomputeRecordedPanels(results) {
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const scenario of results.scenarios)
|
|
303
|
+
for (const panel of scenario.rep_judgments ?? []) {
|
|
304
|
+
const clean = panel.judgments.filter(j => !j.suspect && (j.verdict === "PASS" || j.verdict === "FAIL"));
|
|
305
|
+
let verdict = panel.recorded_verdict;
|
|
306
|
+
if (clean.length === 1)
|
|
307
|
+
verdict = clean[0].verdict;
|
|
308
|
+
else if (clean.length >= 2)
|
|
309
|
+
verdict = collapseVotePanel(panel.judgments).verdict ?? "JUDGE-AMBIGUOUS";
|
|
310
|
+
out.push({ scenario_id: scenario.id, repetition: panel.repetition, verdict });
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
/** Runtime v2/v3 validator. v2 remains permissive; v3 fails closed on observation gaps. */
|
|
315
|
+
export function validateResults(raw) {
|
|
316
|
+
if (!raw || typeof raw !== "object")
|
|
317
|
+
throw new Error("invalid results");
|
|
318
|
+
const result = raw;
|
|
319
|
+
if (result.schema !== 2 && result.schema !== 3)
|
|
320
|
+
throw new Error(`unsupported results schema ${String(raw.schema)}`);
|
|
321
|
+
if (!Array.isArray(result.scenarios))
|
|
322
|
+
throw new Error("results scenarios missing");
|
|
323
|
+
if (result.schema === 2)
|
|
324
|
+
return result;
|
|
325
|
+
if (!Array.isArray(result.subject_invocations))
|
|
326
|
+
throw new Error("schema v3 delivery observations missing");
|
|
327
|
+
for (const observation of result.subject_invocations) {
|
|
328
|
+
const p = observation?.prompt;
|
|
329
|
+
if (!p || !["PASS", "NOT-MEASURED", "ERROR"].includes(p.status))
|
|
330
|
+
throw new Error("delivery status missing");
|
|
331
|
+
if (p.normalization_rule !== "cwd-line-v1")
|
|
332
|
+
throw new Error("unknown prompt normalization rule");
|
|
333
|
+
if (!/^[a-f0-9]{64}$/i.test(p.raw_sha256) || !/^[a-f0-9]{64}$/i.test(p.normalized_sha256) || !/^[a-f0-9]{64}$/i.test(p.contract_sha256))
|
|
334
|
+
throw new Error("invalid prompt provenance digest");
|
|
335
|
+
if (!Number.isInteger(p.bytes) || p.bytes < 0 || !Number.isInteger(p.contract_bytes) || p.contract_bytes < 0)
|
|
336
|
+
throw new Error("invalid prompt provenance byte length");
|
|
337
|
+
if (!Number.isInteger(p.contract_occurrences) || p.contract_occurrences < 0)
|
|
338
|
+
throw new Error("invalid delivery occurrence count");
|
|
339
|
+
const expected = p.mechanism === "none" ? 0 : 1;
|
|
340
|
+
const computed = p.contract_occurrences === expected ? "PASS" : "NOT-MEASURED";
|
|
341
|
+
if (p.status !== "ERROR" && p.status !== computed)
|
|
342
|
+
throw new Error("delivery status contradicts occurrence count");
|
|
343
|
+
}
|
|
344
|
+
const scenarioIds = new Set(result.scenarios.map(scenario => scenario.id));
|
|
345
|
+
const repsByScenario = new Map(result.scenarios.map(scenario => [scenario.id, scenario.reps ?? 1]));
|
|
346
|
+
if (result.subject_invocations.some(observation => !scenarioIds.has(observation.scenario_id) || !Number.isInteger(observation.repetition) || observation.repetition < 0 || observation.repetition >= (repsByScenario.get(observation.scenario_id) ?? 0)))
|
|
347
|
+
throw new Error("schema v3 contains orphan or out-of-range subject invocation observation");
|
|
348
|
+
const recomputed = recomputeRecordedPanels(result);
|
|
349
|
+
for (const scenario of result.scenarios) {
|
|
350
|
+
const reps = scenario.reps ?? 1;
|
|
351
|
+
if (!Array.isArray(scenario.rep_judgments) || scenario.rep_judgments.length !== reps)
|
|
352
|
+
throw new Error(`schema v3 repetition judgments missing for ${scenario.id}`);
|
|
353
|
+
const panelRepetitions = scenario.rep_judgments.map(panel => panel.repetition).sort((a, b) => a - b);
|
|
354
|
+
if (panelRepetitions.some((repetition, index) => repetition !== index))
|
|
355
|
+
throw new Error(`schema v3 repetition judgments duplicate, missing, or out of range for ${scenario.id}`);
|
|
356
|
+
for (let repetition = 0; repetition < reps; repetition++) {
|
|
357
|
+
const observations = result.subject_invocations.filter(o => o.scenario_id === scenario.id && o.repetition === repetition);
|
|
358
|
+
// A pre-provider infrastructure failure legitimately has zero requests;
|
|
359
|
+
// its per-repetition skill_delivered objective must remain explicit ERROR.
|
|
360
|
+
const attempts = [...new Set(observations.map(observation => observation.attempt ?? 0))].sort((a, b) => a - b);
|
|
361
|
+
if (attempts.some((attempt, index) => !Number.isInteger(attempt) || attempt !== index))
|
|
362
|
+
throw new Error(`delivery attempts are duplicate, missing, or out of range for ${scenario.id}#${repetition}`);
|
|
363
|
+
for (const attempt of attempts) {
|
|
364
|
+
const indexes = observations.filter(observation => (observation.attempt ?? 0) === attempt).map(observation => observation.prompt.request_index).sort((a, b) => a - b);
|
|
365
|
+
if (indexes.some((requestIndex, index) => !Number.isInteger(requestIndex) || requestIndex !== index))
|
|
366
|
+
throw new Error(`prompt request indexes are duplicate, missing, or out of range for ${scenario.id}#${repetition} attempt ${attempt}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const repStatuses = Array.from({ length: reps }, (_, repetition) => deliveryStatusForObservations(result.subject_invocations.filter(o => o.scenario_id === scenario.id && o.repetition === repetition)));
|
|
370
|
+
const expectedStatus = repStatuses.includes("ERROR") ? "ERROR" : repStatuses.includes("NOT-MEASURED") ? "NOT-MEASURED" : "PASS";
|
|
371
|
+
const gate = scenario.objective?.assertions.find(assertion => assertion.kind === "skill_delivered");
|
|
372
|
+
if (!gate || gate.status !== expectedStatus)
|
|
373
|
+
throw new Error(`skill_delivered gate missing or inconsistent for ${scenario.id}`);
|
|
374
|
+
if (!Number.isInteger(scenario.criterion_count) || scenario.criterion_count < 1)
|
|
375
|
+
throw new Error(`schema v3 criterion count missing for ${scenario.id}`);
|
|
376
|
+
const assertCriteria = (judgment) => {
|
|
377
|
+
if (!Array.isArray(judgment.criteria))
|
|
378
|
+
throw new Error("schema v3 criterion votes missing");
|
|
379
|
+
const indexes = judgment.criteria.map(vote => vote.index).sort((a, b) => a - b);
|
|
380
|
+
if (indexes.length !== scenario.criterion_count || indexes.some((index, offset) => index !== offset + 1))
|
|
381
|
+
throw new Error(`schema v3 criterion votes are not complete contiguous indexes for ${scenario.id}`);
|
|
382
|
+
};
|
|
383
|
+
for (const panel of scenario.rep_judgments) {
|
|
384
|
+
for (const judgment of panel.judgments)
|
|
385
|
+
assertCriteria(judgment);
|
|
386
|
+
const cleanJudgments = panel.judgments.filter(judgment => !judgment.suspect && (judgment.verdict === "PASS" || judgment.verdict === "FAIL"));
|
|
387
|
+
const objectiveBehavioralFail = panel.objective?.status === "FAIL" && panel.recorded_verdict === "FAIL";
|
|
388
|
+
if ((panel.recorded_verdict === "PASS" || panel.recorded_verdict === "FAIL") && cleanJudgments.length === 0 && !objectiveBehavioralFail)
|
|
389
|
+
throw new Error(`recorded behavioral verdict is unsupported by a clean judgment for ${scenario.id}#${panel.repetition}`);
|
|
390
|
+
const actual = recomputed.find(x => x.scenario_id === scenario.id && x.repetition === panel.repetition);
|
|
391
|
+
if (panel.judgments.length > 0 && actual.verdict !== panel.recorded_verdict)
|
|
392
|
+
throw new Error(`recorded panel verdict diverges from recomputed votes for ${scenario.id}#${panel.repetition}`);
|
|
393
|
+
if (!panel.objective)
|
|
394
|
+
throw new Error(`schema v3 per-repetition objective missing for ${scenario.id}#${panel.repetition}`);
|
|
395
|
+
const observations = result.subject_invocations.filter(o => o.scenario_id === scenario.id && o.repetition === panel.repetition);
|
|
396
|
+
const deliveryStatus = deliveryStatusForObservations(observations);
|
|
397
|
+
const delivery = panel.objective.assertions.find(assertion => assertion.kind === "skill_delivered");
|
|
398
|
+
if (!delivery || delivery.status !== deliveryStatus)
|
|
399
|
+
throw new Error(`per-repetition skill_delivered gate missing or inconsistent for ${scenario.id}#${panel.repetition}`);
|
|
400
|
+
if (deliveryStatus === "ERROR" && panel.objective.status !== "ERROR")
|
|
401
|
+
throw new Error(`per-repetition objective weakens delivery ERROR for ${scenario.id}#${panel.repetition}`);
|
|
402
|
+
if (deliveryStatus === "NOT-MEASURED" && !["ERROR", "NOT-MEASURED"].includes(panel.objective.status))
|
|
403
|
+
throw new Error(`per-repetition objective weakens delivery NOT-MEASURED for ${scenario.id}#${panel.repetition}`);
|
|
404
|
+
const objectiveVerdict = panel.objective.status === "ERROR" ? "ERROR" : panel.objective.status === "NOT-MEASURED" ? "NOT-MEASURED" : panel.objective.status === "FAIL" ? "FAIL" : undefined;
|
|
405
|
+
if (objectiveVerdict && panel.recorded_verdict !== objectiveVerdict)
|
|
406
|
+
throw new Error(`recorded panel verdict weakens objective evidence for ${scenario.id}#${panel.repetition}`);
|
|
407
|
+
}
|
|
408
|
+
const objectiveStatuses = scenario.rep_judgments.map(panel => panel.objective.status);
|
|
409
|
+
const aggregateObjectiveStatus = objectiveStatuses.includes("ERROR") ? "ERROR" : objectiveStatuses.includes("NOT-MEASURED") ? "NOT-MEASURED" : objectiveStatuses.includes("FAIL") ? "FAIL" : "PASS";
|
|
410
|
+
if (!scenario.objective || scenario.objective.status !== aggregateObjectiveStatus)
|
|
411
|
+
throw new Error(`scenario objective diverges from per-repetition objectives for ${scenario.id}`);
|
|
412
|
+
for (const judgment of [...(scenario.judge_history ?? []), ...(scenario.adjudication?.judgments ?? [])])
|
|
413
|
+
assertCriteria(judgment);
|
|
414
|
+
let adjudicatedVerdict;
|
|
415
|
+
if (scenario.adjudication) {
|
|
416
|
+
if (!Number.isInteger(scenario.adjudication.repetition) || scenario.adjudication.repetition < 0 || scenario.adjudication.repetition >= reps)
|
|
417
|
+
throw new Error(`schema v3 adjudication repetition missing or out of range for ${scenario.id}`);
|
|
418
|
+
const collapsed = collapseVotePanel(scenario.adjudication.judgments);
|
|
419
|
+
const boundedCriticalAggregate = (scenario.reps ?? 1) > 1 && scenario.pass_threshold === 1 && scenario.judge_verdict !== "PASS" && collapsed.verdict === "PASS" && scenario.adjudication.state === "unresolved" && scenario.adjudication.verdict === undefined;
|
|
420
|
+
if (!boundedCriticalAggregate && (scenario.adjudication.state !== collapsed.state || scenario.adjudication.verdict !== collapsed.verdict))
|
|
421
|
+
throw new Error(`recorded adjudication state/verdict diverges from recomputed votes for ${scenario.id}`);
|
|
422
|
+
if (scenario.adjudication.state !== "unresolved")
|
|
423
|
+
adjudicatedVerdict = scenario.adjudication.verdict;
|
|
424
|
+
}
|
|
425
|
+
const panels = scenario.rep_judgments;
|
|
426
|
+
const errorCount = panels.filter(panel => panel.recorded_verdict === "ERROR").length;
|
|
427
|
+
const notMeasuredCount = panels.filter(panel => panel.recorded_verdict === "NOT-MEASURED").length;
|
|
428
|
+
const cleanPanels = panels.filter(panel => !(panel.judgments[0]?.suspect ?? false));
|
|
429
|
+
const passes = cleanPanels.filter(panel => panel.recorded_verdict === "PASS").length;
|
|
430
|
+
let aggregateVerdict;
|
|
431
|
+
let aggregateSuspect = false;
|
|
432
|
+
if (panels.length === 1) {
|
|
433
|
+
aggregateVerdict = panels[0].recorded_verdict;
|
|
434
|
+
aggregateSuspect = panels[0].judgments[0]?.suspect ?? false;
|
|
435
|
+
}
|
|
436
|
+
else if (errorCount > 0)
|
|
437
|
+
aggregateVerdict = "ERROR";
|
|
438
|
+
else if (notMeasuredCount > 0)
|
|
439
|
+
aggregateVerdict = "NOT-MEASURED";
|
|
440
|
+
else if (cleanPanels.length * 2 < panels.length) {
|
|
441
|
+
aggregateVerdict = "FAIL";
|
|
442
|
+
aggregateSuspect = true;
|
|
443
|
+
}
|
|
444
|
+
else
|
|
445
|
+
aggregateVerdict = passes / cleanPanels.length >= (scenario.pass_threshold ?? 0.5) ? "PASS" : "FAIL";
|
|
446
|
+
const expectedVerdict = adjudicatedVerdict ?? aggregateVerdict;
|
|
447
|
+
const expectedSuspect = scenario.adjudication ? scenario.adjudication.state === "unresolved" : aggregateSuspect;
|
|
448
|
+
if (scenario.judge_verdict !== expectedVerdict || scenario.suspect !== expectedSuspect)
|
|
449
|
+
throw new Error(`schema v3 scenario verdict/suspect diverges from repetition aggregate for ${scenario.id}`);
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
253
453
|
/** Pure: return a copy with override + note applied to one scenario. */
|
|
254
454
|
export function applyOverride(results, scenarioId, override, note) {
|
|
255
455
|
if (override !== null && note.trim() === "") {
|
|
@@ -399,7 +599,7 @@ export function diffPath(runDir, scenarioId, mode, rep) {
|
|
|
399
599
|
export function rebuildScenarioResult(fresh, prior, policy) {
|
|
400
600
|
// Exhaustive destructure. Do not replace with a spread: the spread is what
|
|
401
601
|
// allowed a new field to pass through unconsidered in the first place.
|
|
402
|
-
const { id, judge_verdict, judge_reason, suspect, override: _freshOverride, note: _freshNote, reps, passes, clean, flakiness, pass_threshold, metrics: freshMetrics, objective: freshObjective, adjudication: freshAdjudication, ...rest } = fresh;
|
|
602
|
+
const { id, criterion_count: freshCriterionCount, judge_verdict, judge_reason, suspect, override: _freshOverride, note: _freshNote, reps, passes, clean, flakiness, pass_threshold, metrics: freshMetrics, objective: freshObjective, adjudication: freshAdjudication, judge_history: freshJudgeHistory, rep_judgments: freshRepJudgments, ...rest } = fresh;
|
|
403
603
|
const _exhaustive = rest;
|
|
404
604
|
void _exhaustive;
|
|
405
605
|
void _freshOverride;
|
|
@@ -411,7 +611,7 @@ export function rebuildScenarioResult(fresh, prior, policy) {
|
|
|
411
611
|
};
|
|
412
612
|
const objective = pick(policy.objective, freshObjective, prior?.objective);
|
|
413
613
|
const pickedAdjudication = pick(policy.adjudication, freshAdjudication, prior?.adjudication);
|
|
414
|
-
const conflictsWithFreshEvidence = Boolean(pickedAdjudication?.verdict && (objective?.status === "FAIL" || objective?.status === "ERROR" ||
|
|
614
|
+
const conflictsWithFreshEvidence = Boolean(pickedAdjudication?.verdict && (objective?.status === "FAIL" || objective?.status === "ERROR" || objective?.status === "NOT-MEASURED" ||
|
|
415
615
|
(pass_threshold === 1 && (reps ?? 1) > 1 && judge_verdict !== "PASS")));
|
|
416
616
|
// A settled cell-level panel cannot overrule newly evaluated objective gates,
|
|
417
617
|
// nor can one transcript erase a failed repetition under an all-clean policy.
|
|
@@ -423,8 +623,8 @@ export function rebuildScenarioResult(fresh, prior, policy) {
|
|
|
423
623
|
// Taking `suspect` from `fresh` while carrying the block therefore published a
|
|
424
624
|
// record that said `state: "unresolved"` and scored as a clean SHIP. `regate`
|
|
425
625
|
// did exactly that: it rebuilds verdict and suspect from the saved first-wave
|
|
426
|
-
// judge file, so a
|
|
427
|
-
//
|
|
626
|
+
// judge file, so a gate-only rewrite silently resolved a disagreement in favour
|
|
627
|
+
// of shipping — inverting this module's stated invariant that an
|
|
428
628
|
// unresolved disagreement must not resolve itself.
|
|
429
629
|
const unresolved = adjudication?.state === "unresolved";
|
|
430
630
|
// A settled adjudication outranks a re-read of the first-wave judge file for
|
|
@@ -440,6 +640,7 @@ export function rebuildScenarioResult(fresh, prior, policy) {
|
|
|
440
640
|
// corpus with a pure-noise diff.
|
|
441
641
|
return {
|
|
442
642
|
id,
|
|
643
|
+
...((freshCriterionCount ?? prior?.criterion_count) === undefined ? {} : { criterion_count: freshCriterionCount ?? prior.criterion_count }),
|
|
443
644
|
judge_verdict: settled ?? judge_verdict,
|
|
444
645
|
judge_reason,
|
|
445
646
|
suspect: suspect || unresolved,
|
|
@@ -459,6 +660,8 @@ export function rebuildScenarioResult(fresh, prior, policy) {
|
|
|
459
660
|
// existed.
|
|
460
661
|
...(objective ? { objective } : {}),
|
|
461
662
|
...(adjudication ? { adjudication } : {}),
|
|
663
|
+
...((freshJudgeHistory ?? prior?.judge_history) ? { judge_history: freshJudgeHistory ?? prior.judge_history } : {}),
|
|
664
|
+
...((freshRepJudgments ?? prior?.rep_judgments) ? { rep_judgments: freshRepJudgments ?? prior.rep_judgments } : {}),
|
|
462
665
|
};
|
|
463
666
|
}
|
|
464
667
|
/**
|
package/dist/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { sourceHashes } from "./sources.js";
|
|
3
|
+
import { sourceHashes, PROMPT_NORMALIZATION_SOURCE_KEY, PROMPT_NORMALIZATION_SOURCE_DIGEST } from "./sources.js";
|
|
4
4
|
import { judgeResemblesSubject } from "./grade.js";
|
|
5
5
|
import { NONE_ARM, seedArmDefinitions } from "./arms.js";
|
|
6
|
-
import { runDirFor, transcriptPath, diffPath, tracePath, trajectoryPath, writeResults, ensureResultsGitignore, scoreContextFor, effectiveThreshold, isScoredMode, } from "./results.js";
|
|
6
|
+
import { runDirFor, transcriptPath, diffPath, tracePath, trajectoryPath, writeResults, ensureResultsGitignore, scoreContextFor, effectiveThreshold, isScoredMode, deliveryStatusForObservations, } from "./results.js";
|
|
7
7
|
import { appendJournal } from "./journal.js";
|
|
8
8
|
import { liftHeadline } from "./lift.js";
|
|
9
9
|
import { runSeeded } from "./seeded.js";
|
|
@@ -139,13 +139,20 @@ export async function runSkillModel(opts) {
|
|
|
139
139
|
}
|
|
140
140
|
});
|
|
141
141
|
const flat = await runPool(tasks, opts.concurrency ?? 1);
|
|
142
|
+
const subjectInvocations = flat.flatMap(outcome => outcome.subject_invocations ?? []);
|
|
142
143
|
const grouped = scenarios.map(() => []);
|
|
143
144
|
flat.forEach((outcome, i) => grouped[owners[i]].push(outcome));
|
|
144
145
|
const scenarioResults = scenarios.map((scenario, si) => {
|
|
145
146
|
const threshold = scenario.critical
|
|
146
147
|
? effectiveThreshold(undefined, scenario)
|
|
147
148
|
: scenario.passThreshold ?? opts.passThreshold ?? 0.5;
|
|
148
|
-
|
|
149
|
+
const result = outcomesToResult(scenario.id, grouped[si], repCounts[si], threshold);
|
|
150
|
+
if (adapter.observesPrompts)
|
|
151
|
+
result.criterion_count = scenario.checklist.length;
|
|
152
|
+
if (adapter.observesPrompts && !result.rep_judgments) {
|
|
153
|
+
result.rep_judgments = grouped[si].map((outcome, repetition) => ({ repetition, judgments: [], recorded_verdict: outcome.verdict, ...(outcome.objective ? { objective: outcome.objective } : {}) }));
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
149
156
|
});
|
|
150
157
|
const ctx = scoreContextFor({ mode, partial }, spec);
|
|
151
158
|
const results = writeResults(runDir, {
|
|
@@ -161,8 +168,12 @@ export async function runSkillModel(opts) {
|
|
|
161
168
|
...(partial ? { partial: true } : {}),
|
|
162
169
|
// Only the scenarios this run actually measured: a --only run must not claim
|
|
163
170
|
// coverage of scenarios it skipped.
|
|
164
|
-
source_hashes:
|
|
171
|
+
source_hashes: {
|
|
172
|
+
...sourceHashes({ skillDir, specDir: dirname(opts.specPath), scenarios, judgePersona: spec.judge_persona }),
|
|
173
|
+
...(adapter.observesPrompts ? { [PROMPT_NORMALIZATION_SOURCE_KEY]: PROMPT_NORMALIZATION_SOURCE_DIGEST } : {}),
|
|
174
|
+
},
|
|
165
175
|
scenarios: scenarioResults,
|
|
176
|
+
...(adapter.observesPrompts ? { schema: 3, subject_invocations: subjectInvocations } : {}),
|
|
166
177
|
...(arm.name === NONE_ARM.name ? {} : {
|
|
167
178
|
arm: {
|
|
168
179
|
name: arm.name,
|
|
@@ -210,11 +221,12 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
210
221
|
const repField = repCount > 1 ? { rep } : {};
|
|
211
222
|
const arm = ctx.arm ?? NONE_ARM;
|
|
212
223
|
const skillsRoot = ctx.skillsRoot ?? dirname(ctx.skillDir);
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
// Arm paths are per-run/per-workspace values. Plain split/join avoids treating
|
|
225
|
+
// temp-path characters as regex syntax.
|
|
226
|
+
const armEnvFor = (workspace) => Object.keys(arm.env).length
|
|
227
|
+
? Object.fromEntries(Object.entries(arm.env).map(([k, v]) => [k, v
|
|
228
|
+
.split("<run-dir>").join(runDir)
|
|
229
|
+
.split("<workspace>").join(workspace)]))
|
|
218
230
|
: undefined;
|
|
219
231
|
if (rep === 0) {
|
|
220
232
|
log(` ${scenario.id} (${scenario.title})${repCount > 1 ? ` ×${repCount}` : ""} …`);
|
|
@@ -228,6 +240,7 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
228
240
|
// produces no diff, and writing an empty artifact there would misreport "the
|
|
229
241
|
// model changed nothing" for a rep that never ran.
|
|
230
242
|
let stagedDiff = null;
|
|
243
|
+
const subjectInvocations = [];
|
|
231
244
|
try {
|
|
232
245
|
try {
|
|
233
246
|
ws = createWorkspace(scenario.workspace, { specDir: dirname(ctx.specPath), remote: scenario.remote });
|
|
@@ -284,6 +297,7 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
284
297
|
// it happens again the verdict is ERROR — never a judged FAIL on an empty reply.
|
|
285
298
|
// An adapter that THREW is the same class of event and takes the same retry.
|
|
286
299
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
300
|
+
const observe = (prompt) => subjectInvocations.push({ scenario_id: scenario.id, repetition: rep, attempt, prompt });
|
|
287
301
|
if (attempt > 0) {
|
|
288
302
|
const why = adapterFailure ? `adapter failed (${adapterFailure})` : "empty response";
|
|
289
303
|
appendJournal(runDir, { event: "empty-response-retry", ts: now(), id: scenario.id, attempt, reason: why, ...repField });
|
|
@@ -322,7 +336,8 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
322
336
|
// builds the RunReq — the arm's extensions and env (with `<run-dir>`
|
|
323
337
|
// already substituted) both must reach pi.
|
|
324
338
|
armExtensions: arm.extensions,
|
|
325
|
-
...(
|
|
339
|
+
...(armEnvFor(ws.cwd) ? { armEnv: armEnvFor(ws.cwd) } : {}),
|
|
340
|
+
...(ctx.adapter.observesPrompts ? { onPromptObservation: observe } : {}),
|
|
326
341
|
});
|
|
327
342
|
transcript = r.transcript;
|
|
328
343
|
gatePrefix = r.gateFailure;
|
|
@@ -347,7 +362,8 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
347
362
|
...arm.extensions,
|
|
348
363
|
],
|
|
349
364
|
eventSources: scenario.eventSources,
|
|
350
|
-
...(
|
|
365
|
+
...(armEnvFor(ws.cwd) ? { armEnv: armEnvFor(ws.cwd) } : {}),
|
|
366
|
+
...(ctx.adapter.observesPrompts ? { onPromptObservation: observe } : {}),
|
|
351
367
|
};
|
|
352
368
|
if (useStructured) {
|
|
353
369
|
const structured = await ctx.adapter.runStructured({ ...req, scenarioId: scenario.id, rep });
|
|
@@ -441,6 +457,16 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
441
457
|
});
|
|
442
458
|
}
|
|
443
459
|
}
|
|
460
|
+
// Delivery is an objective gate on every Pi subject invocation. It is derived
|
|
461
|
+
// from captured provider payload bytes, never from mode/argv supplied by a caller.
|
|
462
|
+
let deliveryObjective;
|
|
463
|
+
if (ctx.adapter.observesPrompts) {
|
|
464
|
+
const statuses = subjectInvocations.map(observation => observation.prompt.status);
|
|
465
|
+
const status = deliveryStatusForObservations(subjectInvocations);
|
|
466
|
+
deliveryObjective = { status, assertions: [{ kind: "skill_delivered", status, detail: statuses.length === 0 ? "no model-visible prompt observation was retained" : `${statuses.length} provider request(s): ${statuses.join(", ")}` }] };
|
|
467
|
+
if (status !== "PASS")
|
|
468
|
+
gatePrefix = `objective: skill_delivered ${status.toLowerCase()} — ${deliveryObjective.assertions[0].detail}`;
|
|
469
|
+
}
|
|
444
470
|
// Objective evidence is persisted for every rep, pass or fail — a failing gate
|
|
445
471
|
// is exactly when someone wants to read what the model actually did.
|
|
446
472
|
// `!adapterFailure`: a rep whose adapter threw never ran, so there is nothing
|
|
@@ -448,7 +474,7 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
448
474
|
// which is the symptom — it names the missing artifact instead of the crash or
|
|
449
475
|
// timeout that caused it, and it is that text, not the real reason, that would
|
|
450
476
|
// reach the results record. The verdict is ERROR either way.
|
|
451
|
-
let objective;
|
|
477
|
+
let objective = deliveryObjective;
|
|
452
478
|
if ((scenario.traceAssert || scenario.trajectoryAssert) && !adapterFailure) {
|
|
453
479
|
const assertionResults = [];
|
|
454
480
|
let status = "PASS";
|
|
@@ -494,6 +520,11 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
494
520
|
trajectoryMeta = { trajectory_version: gate.event_version, events_sha256: gate.events_sha256 };
|
|
495
521
|
}
|
|
496
522
|
}
|
|
523
|
+
if (deliveryObjective) {
|
|
524
|
+
if (deliveryObjective.status === "ERROR" || (deliveryObjective.status === "NOT-MEASURED" && status !== "ERROR") || (deliveryObjective.status === "FAIL" && status === "PASS"))
|
|
525
|
+
status = deliveryObjective.status;
|
|
526
|
+
assertionResults.unshift(...deliveryObjective.assertions);
|
|
527
|
+
}
|
|
497
528
|
objective = { status, ...traceMeta, ...trajectoryMeta, assertions: assertionResults };
|
|
498
529
|
if (status !== "PASS") {
|
|
499
530
|
const details = assertionResults.filter((result) => result.status === status).map((result) => result.detail);
|
|
@@ -507,12 +538,18 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
507
538
|
let verdict;
|
|
508
539
|
let reason;
|
|
509
540
|
let suspect = false;
|
|
541
|
+
let judgment;
|
|
510
542
|
let judgeCalls = 0;
|
|
511
543
|
if (objective?.status === "ERROR") {
|
|
512
544
|
verdict = "ERROR";
|
|
513
545
|
reason = gatePrefix ?? "objective evidence missing";
|
|
514
546
|
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict, reason, suspect, ...repField });
|
|
515
547
|
}
|
|
548
|
+
else if (objective?.status === "NOT-MEASURED") {
|
|
549
|
+
verdict = "NOT-MEASURED";
|
|
550
|
+
reason = gatePrefix ?? "skill delivery was not established";
|
|
551
|
+
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict, reason, suspect, ...repField });
|
|
552
|
+
}
|
|
516
553
|
else if (infrastructureFailure) {
|
|
517
554
|
verdict = "ERROR";
|
|
518
555
|
reason = infrastructureFailure;
|
|
@@ -536,13 +573,14 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
536
573
|
});
|
|
537
574
|
verdict = o.verdict;
|
|
538
575
|
reason = o.reason;
|
|
539
|
-
suspect = o.suspect;
|
|
576
|
+
suspect = o.suspect;
|
|
577
|
+
judgment = o.judgment; // judgeOneRep already journaled (verdict + misfire)
|
|
540
578
|
judgeCalls = 1;
|
|
541
579
|
}
|
|
542
580
|
log(` → ${scenario.id}${repCount > 1 ? `#${rep}` : ""} ${verdict}${reason ? `: ${reason}` : ""}${suspect ? " ⚠ suspect" : ""}`);
|
|
543
581
|
const subject = mergeTraces(traces)?.metrics;
|
|
544
582
|
return {
|
|
545
|
-
verdict, reason, suspect, objective,
|
|
583
|
+
verdict, reason, suspect, objective, judgment, subject_invocations: subjectInvocations,
|
|
546
584
|
metrics: {
|
|
547
585
|
wall_time_ms: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
548
586
|
judge_calls: judgeCalls,
|
|
@@ -577,8 +615,8 @@ export function formatScorecard(summary, lift, stability) {
|
|
|
577
615
|
const lines = [];
|
|
578
616
|
lines.push(`── ${results.skill} · ${results.harness} · ${results.model} ──`);
|
|
579
617
|
for (const s of results.scenarios) {
|
|
580
|
-
const v = s.override ?? (s.objective?.status === "ERROR" ? "ERROR" : s.objective?.status === "FAIL" ? "FAIL" : s.judge_verdict);
|
|
581
|
-
const mark = v === "PASS" ? "✓" : v === "FAIL" ? "✗" : "?";
|
|
618
|
+
const v = s.override ?? (s.objective?.status === "ERROR" ? "ERROR" : s.objective?.status === "NOT-MEASURED" ? "NOT-MEASURED" : s.objective?.status === "FAIL" ? "FAIL" : s.judge_verdict);
|
|
619
|
+
const mark = v === "PASS" ? "✓" : v === "FAIL" ? "✗" : v === "NOT-MEASURED" ? "∅" : "?";
|
|
582
620
|
const ov = s.override ? " (override)" : "";
|
|
583
621
|
const susp = s.suspect ? " ⚠suspect" : "";
|
|
584
622
|
const misfired = s.clean !== undefined && s.reps !== undefined && s.clean < s.reps ? ` · ${s.reps - s.clean} misfired` : "";
|
|
@@ -594,7 +632,18 @@ export function formatScorecard(summary, lift, stability) {
|
|
|
594
632
|
? "subject tokens unavailable"
|
|
595
633
|
: `subject tokens ${metrics.input_tokens} in / ${metrics.output_tokens ?? 0} out / ${metrics.cache_read_tokens ?? 0} cache-read`;
|
|
596
634
|
const tools = metrics.tool_calls === null ? "tool calls unavailable" : `${metrics.tool_calls} tool call(s), ${metrics.delegated_children ?? 0} delegated, max concurrency ${metrics.max_concurrency ?? 0}`;
|
|
597
|
-
|
|
635
|
+
const cost = metrics.cost_source === "subscription"
|
|
636
|
+
? " · subscription ($0 marginal cost recorded)"
|
|
637
|
+
: metrics.cost_source === "unreported"
|
|
638
|
+
? metrics.input_tokens !== null && metrics.input_tokens > 0
|
|
639
|
+
? " · WARNING: subject tokens were used but subject cost was not reported"
|
|
640
|
+
: " · subject cost unavailable"
|
|
641
|
+
: metrics.input_tokens !== null && metrics.input_tokens > 0 && metrics.subject_cost_usd === 0
|
|
642
|
+
? " · WARNING: subject tokens were used but the provider reported $0 cost"
|
|
643
|
+
: metrics.subject_cost_usd === null
|
|
644
|
+
? " · subject cost unavailable"
|
|
645
|
+
: ` · $${metrics.subject_cost_usd.toFixed(6)} provider-reported`;
|
|
646
|
+
lines.push(` COST: ${tokens} (${metrics.subject_metrics_reps}/${metrics.total_reps} reps reported)${cost} · ${metrics.judge_calls} judge + ${metrics.judge_rejudge_calls} re-judge call(s) · ${metrics.wall_time_ms}ms · ${tools}`);
|
|
598
647
|
}
|
|
599
648
|
// Lift is a statement about a skill-delivered run (green or force). On a red run
|
|
600
649
|
// the caller may still have a lift in hand (a scored run exists in the same tag),
|
package/dist/score.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ShipBar } from "./spec.js";
|
|
|
3
3
|
* "JUDGE-AMBIGUOUS": the judge emitted conflicting verdicts for one transcript. It is
|
|
4
4
|
* never a pass and never silently resolved — it marks the run for a rejudge.
|
|
5
5
|
*/
|
|
6
|
-
export type Verdict = "PASS" | "FAIL" | "ERROR" | "JUDGE-AMBIGUOUS";
|
|
6
|
+
export type Verdict = "PASS" | "FAIL" | "ERROR" | "NOT-MEASURED" | "JUDGE-AMBIGUOUS";
|
|
7
7
|
export interface ScenarioVerdict {
|
|
8
8
|
id: string;
|
|
9
9
|
verdict: Verdict;
|
|
@@ -23,13 +23,14 @@ export interface ScoreResult {
|
|
|
23
23
|
bSeriesFails: number;
|
|
24
24
|
suspectCount: number;
|
|
25
25
|
errorCount: number;
|
|
26
|
+
notMeasuredCount: number;
|
|
26
27
|
note: string;
|
|
27
28
|
}
|
|
28
29
|
export declare function letterFor(pct: number): string;
|
|
29
30
|
/**
|
|
30
31
|
* Score a set of green-mode verdicts against the ship bar. A scenario PASSes only
|
|
31
|
-
* on verdict PASS; FAIL is behavioral, while ERROR/JUDGE-AMBIGUOUS
|
|
32
|
-
* infrastructure/indeterminate and independently block SHIP. A `suspect` verdict (an
|
|
32
|
+
* on verdict PASS; FAIL is behavioral, while ERROR/JUDGE-AMBIGUOUS and
|
|
33
|
+
* NOT-MEASURED are excluded as infrastructure/indeterminate and independently block SHIP. A `suspect` verdict (an
|
|
33
34
|
* unresolved judge misfire) is excluded from both `passed` and `total` — it is
|
|
34
35
|
* untrustworthy, neither a pass nor a fail — and any suspect count blocks SHIP
|
|
35
36
|
* until an author override resolves it. SHIP otherwise requires: enough total
|
package/dist/score.js
CHANGED
|
@@ -11,8 +11,8 @@ export function letterFor(pct) {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Score a set of green-mode verdicts against the ship bar. A scenario PASSes only
|
|
14
|
-
* on verdict PASS; FAIL is behavioral, while ERROR/JUDGE-AMBIGUOUS
|
|
15
|
-
* infrastructure/indeterminate and independently block SHIP. A `suspect` verdict (an
|
|
14
|
+
* on verdict PASS; FAIL is behavioral, while ERROR/JUDGE-AMBIGUOUS and
|
|
15
|
+
* NOT-MEASURED are excluded as infrastructure/indeterminate and independently block SHIP. A `suspect` verdict (an
|
|
16
16
|
* unresolved judge misfire) is excluded from both `passed` and `total` — it is
|
|
17
17
|
* untrustworthy, neither a pass nor a fail — and any suspect count blocks SHIP
|
|
18
18
|
* until an author override resolves it. SHIP otherwise requires: enough total
|
|
@@ -27,11 +27,16 @@ export function score(verdicts, input) {
|
|
|
27
27
|
let bSeriesFails = 0;
|
|
28
28
|
let suspectCount = 0;
|
|
29
29
|
let errorCount = 0;
|
|
30
|
+
let notMeasuredCount = 0;
|
|
30
31
|
for (const v of verdicts) {
|
|
31
32
|
if (v.suspect) {
|
|
32
33
|
suspectCount++;
|
|
33
34
|
continue; // untrustworthy: neither pass nor fail
|
|
34
35
|
}
|
|
36
|
+
if (v.verdict === "NOT-MEASURED") {
|
|
37
|
+
notMeasuredCount++;
|
|
38
|
+
continue; // known-undelivered experiment: neither behavioral pass nor fail
|
|
39
|
+
}
|
|
35
40
|
if (v.verdict === "ERROR" || v.verdict === "JUDGE-AMBIGUOUS") {
|
|
36
41
|
errorCount++;
|
|
37
42
|
continue; // infrastructure/indeterminate: neither behavioral pass nor fail
|
|
@@ -55,7 +60,8 @@ export function score(verdicts, input) {
|
|
|
55
60
|
(!shipBar.no_critical_fail || criticalFails === 0) &&
|
|
56
61
|
bSeriesFails === 0 &&
|
|
57
62
|
suspectCount === 0 &&
|
|
58
|
-
errorCount === 0
|
|
63
|
+
errorCount === 0 &&
|
|
64
|
+
notMeasuredCount === 0;
|
|
59
65
|
let note = "";
|
|
60
66
|
if (!validBar) {
|
|
61
67
|
note = "invalid ship bar: total/min_pass must be positive integers with min_pass <= total";
|
|
@@ -66,12 +72,15 @@ export function score(verdicts, input) {
|
|
|
66
72
|
else if (errorCount > 0) {
|
|
67
73
|
note = `${errorCount} infrastructure error${errorCount === 1 ? "" : "s"}: retry/repair evidence`;
|
|
68
74
|
}
|
|
75
|
+
else if (notMeasuredCount > 0) {
|
|
76
|
+
note = `${notMeasuredCount} not measured: skill delivery was not established`;
|
|
77
|
+
}
|
|
69
78
|
else if (criticalFails > 0) {
|
|
70
79
|
note = `gated: ${criticalFails} critical fail${criticalFails === 1 ? "" : "s"}`;
|
|
71
80
|
}
|
|
72
81
|
else if (bSeriesFails > 0) {
|
|
73
82
|
note = `gated: ${bSeriesFails} B-series fail${bSeriesFails === 1 ? "" : "s"}`;
|
|
74
83
|
}
|
|
75
|
-
return { passed, total, pct, letter, ship, criticalFails, bSeriesFails, suspectCount, errorCount, note };
|
|
84
|
+
return { passed, total, pct, letter, ship, criticalFails, bSeriesFails, suspectCount, errorCount, notMeasuredCount, note };
|
|
76
85
|
}
|
|
77
86
|
//# sourceMappingURL=score.js.map
|
package/dist/screen.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ResultsFile } from "./results.js";
|
|
2
|
+
export type ScreenClassification = "CEILING" | "FLOOR" | "INFORMATIVE" | "UNKNOWN";
|
|
3
|
+
export interface ScreenRate {
|
|
4
|
+
passes: number;
|
|
5
|
+
n: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ScreenScenario {
|
|
8
|
+
skill: string;
|
|
9
|
+
model: string;
|
|
10
|
+
id: string;
|
|
11
|
+
control: ScreenRate;
|
|
12
|
+
treatment: ScreenRate;
|
|
13
|
+
not_measured: number;
|
|
14
|
+
classification: ScreenClassification;
|
|
15
|
+
}
|
|
16
|
+
export interface ScreenCriterion {
|
|
17
|
+
skill: string;
|
|
18
|
+
model: string;
|
|
19
|
+
scenario_id: string;
|
|
20
|
+
criterion: number;
|
|
21
|
+
failures: number;
|
|
22
|
+
n: number;
|
|
23
|
+
fail_rate: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ScreenReport {
|
|
26
|
+
scenarios: ScreenScenario[];
|
|
27
|
+
criteria: ScreenCriterion[];
|
|
28
|
+
}
|
|
29
|
+
/** Pure/offline: derive only from v3 retained invocation and vote evidence. */
|
|
30
|
+
export declare function screenResults(results: ResultsFile[]): ScreenReport;
|
|
31
|
+
export declare function formatScreen(report: ScreenReport): string;
|