@skill-harness/core 0.11.0 → 0.12.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/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/grade.d.ts +2 -0
- package/dist/grade.js +2 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -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 +188 -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 -2
- package/dist/trajectory-gates.js +26 -6
- package/dist/vote-panel.d.ts +33 -0
- package/dist/vote-panel.js +41 -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/screen.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { deliveryStatusForObservations } from "./results.js";
|
|
2
|
+
import { collapseVotePanel } from "./vote-panel.js";
|
|
3
|
+
function classify(rate) {
|
|
4
|
+
if (rate.n === 0)
|
|
5
|
+
return "UNKNOWN";
|
|
6
|
+
const p = rate.passes / rate.n;
|
|
7
|
+
if (p >= 0.8)
|
|
8
|
+
return "CEILING";
|
|
9
|
+
if (p <= 0.1)
|
|
10
|
+
return "FLOOR";
|
|
11
|
+
if (p >= 0.2 && p <= 0.7)
|
|
12
|
+
return "INFORMATIVE";
|
|
13
|
+
return "UNKNOWN";
|
|
14
|
+
}
|
|
15
|
+
/** Pure/offline: derive only from v3 retained invocation and vote evidence. */
|
|
16
|
+
export function screenResults(results) {
|
|
17
|
+
const rows = new Map();
|
|
18
|
+
const criteria = new Map();
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
const observations = result.schema === 3 ? result.subject_invocations : [];
|
|
21
|
+
for (const scenario of result.scenarios) {
|
|
22
|
+
const rowKey = `${result.skill}\u0000${result.model}\u0000${scenario.id}`;
|
|
23
|
+
const row = rows.get(rowKey) ?? { control: { passes: 0, n: 0 }, treatment: { passes: 0, n: 0 }, not_measured: 0 };
|
|
24
|
+
for (const rep of scenario.rep_judgments ?? []) {
|
|
25
|
+
const repObservations = observations.filter(observation => observation.scenario_id === scenario.id && observation.repetition === rep.repetition);
|
|
26
|
+
const deliveryStatus = deliveryStatusForObservations(repObservations);
|
|
27
|
+
if (deliveryStatus === "NOT-MEASURED")
|
|
28
|
+
row.not_measured++;
|
|
29
|
+
if (deliveryStatus !== "PASS")
|
|
30
|
+
continue;
|
|
31
|
+
const terminalAttempt = Math.max(...repObservations.map(observation => observation.attempt ?? 0));
|
|
32
|
+
const mechanisms = new Set(repObservations.filter(observation => (observation.attempt ?? 0) === terminalAttempt).map(observation => observation.prompt.mechanism));
|
|
33
|
+
if (mechanisms.size !== 1)
|
|
34
|
+
continue;
|
|
35
|
+
const rate = mechanisms.has("none") ? row.control : row.treatment;
|
|
36
|
+
let panelVerdict = rep.recorded_verdict;
|
|
37
|
+
if (rep.judgments.length > 0) {
|
|
38
|
+
const clean = rep.judgments.filter(judgment => !judgment.suspect && (judgment.verdict === "PASS" || judgment.verdict === "FAIL"));
|
|
39
|
+
if (clean.length === 0)
|
|
40
|
+
continue;
|
|
41
|
+
panelVerdict = clean.length === 1 ? clean[0].verdict : collapseVotePanel(rep.judgments).verdict ?? "JUDGE-AMBIGUOUS";
|
|
42
|
+
}
|
|
43
|
+
else if (rep.objective?.status === "FAIL" && rep.recorded_verdict === "FAIL") {
|
|
44
|
+
panelVerdict = "FAIL"; // objective behavioral evidence needs no prose judge
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
continue;
|
|
48
|
+
if (scenario.adjudication?.repetition === rep.repetition) {
|
|
49
|
+
if (scenario.adjudication.state === "unresolved" || !scenario.adjudication.verdict)
|
|
50
|
+
continue;
|
|
51
|
+
const adjudicated = collapseVotePanel(scenario.adjudication.judgments);
|
|
52
|
+
if (adjudicated.state !== scenario.adjudication.state || adjudicated.verdict !== scenario.adjudication.verdict)
|
|
53
|
+
continue;
|
|
54
|
+
panelVerdict = adjudicated.verdict;
|
|
55
|
+
}
|
|
56
|
+
const effective = rep.objective?.status === "ERROR" ? "ERROR" : rep.objective?.status === "NOT-MEASURED" ? "NOT-MEASURED" : rep.objective?.status === "FAIL" ? "FAIL" : panelVerdict;
|
|
57
|
+
if (effective !== "PASS" && effective !== "FAIL")
|
|
58
|
+
continue;
|
|
59
|
+
rate.n++;
|
|
60
|
+
if (effective === "PASS")
|
|
61
|
+
rate.passes++;
|
|
62
|
+
for (const judgment of rep.judgments) {
|
|
63
|
+
if (judgment.suspect || (judgment.verdict !== "PASS" && judgment.verdict !== "FAIL"))
|
|
64
|
+
continue;
|
|
65
|
+
for (const vote of judgment.criteria ?? [])
|
|
66
|
+
addCriterion(criteria, result.skill, result.model, scenario.id, vote);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const adjudicatedRep = scenario.adjudication?.repetition;
|
|
70
|
+
const adjudicationDelivery = result.schema === 3 && adjudicatedRep !== undefined
|
|
71
|
+
? deliveryStatusForObservations(observations.filter(observation => observation.scenario_id === scenario.id && observation.repetition === adjudicatedRep))
|
|
72
|
+
: "ERROR";
|
|
73
|
+
for (const judgment of adjudicationDelivery === "PASS" ? scenario.adjudication?.judgments.filter(judgment => judgment.ordinal > 1) ?? [] : []) {
|
|
74
|
+
if (judgment.suspect || (judgment.verdict !== "PASS" && judgment.verdict !== "FAIL"))
|
|
75
|
+
continue;
|
|
76
|
+
for (const vote of judgment.criteria ?? [])
|
|
77
|
+
addCriterion(criteria, result.skill, result.model, scenario.id, vote);
|
|
78
|
+
}
|
|
79
|
+
rows.set(rowKey, row);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
scenarios: [...rows.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, row]) => { const [skill, model, id] = key.split("\u0000"); return { skill, model, id, ...row, classification: classify(row.control) }; }),
|
|
84
|
+
criteria: [...criteria.values()].sort((a, b) => a.scenario_id.localeCompare(b.scenario_id) || a.criterion - b.criterion).map(x => ({ ...x, fail_rate: x.failures / x.n })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function addCriterion(map, skill, model, id, vote) {
|
|
88
|
+
if (vote.verdict === "ERROR")
|
|
89
|
+
return;
|
|
90
|
+
const key = `${skill}\u0000${model}\u0000${id}:${vote.index}`, row = map.get(key) ?? { skill, model, scenario_id: id, criterion: vote.index, failures: 0, n: 0 };
|
|
91
|
+
row.n++;
|
|
92
|
+
if (vote.verdict === "FAIL")
|
|
93
|
+
row.failures++;
|
|
94
|
+
map.set(key, row);
|
|
95
|
+
}
|
|
96
|
+
export function formatScreen(report) {
|
|
97
|
+
const lines = ["skill/model/scenario control treatment not-measured class"];
|
|
98
|
+
for (const s of report.scenarios)
|
|
99
|
+
lines.push(`${s.skill}/${s.model}/${s.id} ${s.control.passes}/${s.control.n} ${s.treatment.passes}/${s.treatment.n} ${s.not_measured} ${s.classification}`);
|
|
100
|
+
lines.push("", "skill/model/criterion failures/n fail-rate");
|
|
101
|
+
for (const c of report.criteria)
|
|
102
|
+
lines.push(`${c.skill}/${c.model}/${c.scenario_id}.${c.criterion} ${c.failures}/${c.n} ${(c.fail_rate * 100).toFixed(1)}%`);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=screen.js.map
|
package/dist/seeded.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Scenario } from "./spec.js";
|
|
2
2
|
import type { ExecutionTraceV1 } from "./capture-trace-types.js";
|
|
3
3
|
import type { TrajectoryEventV1 } from "./trajectory-gates.js";
|
|
4
|
-
import type { HarnessAdapter, ModelRef, RunMode } from "./adapters/types.js";
|
|
4
|
+
import type { HarnessAdapter, ModelRef, RunMode, PromptProvenance } from "./adapters/types.js";
|
|
5
5
|
import { type ExecResult } from "./util/exec.js";
|
|
6
6
|
interface SeededOpts {
|
|
7
7
|
skillDir: string;
|
|
@@ -37,6 +37,7 @@ interface SeededOpts {
|
|
|
37
37
|
armExtensions?: string[];
|
|
38
38
|
/** The arm's env for the subject process; `<run-dir>` already substituted by the caller. */
|
|
39
39
|
armEnv?: Record<string, string>;
|
|
40
|
+
onPromptObservation?: (observation: PromptProvenance) => void;
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Result of one vitest invocation — deliberately `ExecResult`, not a narrower
|
package/dist/seeded.js
CHANGED
|
@@ -175,6 +175,7 @@ export async function runSeeded(scenario, opts) {
|
|
|
175
175
|
],
|
|
176
176
|
eventSources: scenario.eventSources,
|
|
177
177
|
...(opts.armEnv ? { armEnv: opts.armEnv } : {}),
|
|
178
|
+
...(opts.onPromptObservation ? { onPromptObservation: opts.onPromptObservation } : {}),
|
|
178
179
|
};
|
|
179
180
|
// A trace-gated seeded scenario runs through the structured path so the tool
|
|
180
181
|
// calls are recorded; everything downstream (gates, diff, transcript) is
|
package/dist/sources.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Scenario } from "./spec.js";
|
|
2
|
+
export { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
|
|
2
3
|
/**
|
|
3
4
|
* What a run measured, as a map of `key -> sha256`, recorded in results.yaml so
|
|
4
5
|
* lint can prove a published result still describes the current inputs.
|
|
@@ -53,7 +54,7 @@ export declare const FIXTURE_PREFIX = "fixture:";
|
|
|
53
54
|
* | `stimulus:<id>` | mode, turns, workspace, remote, agent-file path, fixture path, `assert.vitest`, `post_test` path | the transcripts answer a different question | `run` (model + judge) |
|
|
54
55
|
* | `rubric:<id>` | title, checklist | transcripts fine, verdicts wrong | `grade` (judge only) |
|
|
55
56
|
* | `policy:<id>` | critical, reps, pass_threshold | only the scoring moved | `rescore` (free) |
|
|
56
|
-
* | `gates:<id>` | `diff_contains`, `diff_excludes` | needle wrong, behavior fine | `regate` (
|
|
57
|
+
* | `gates:<id>` | `diff_contains`, `diff_excludes` | needle wrong, behavior fine | `regate` (no subject call; judges fail→pass reps) |
|
|
57
58
|
* | `rubric:__persona` | spec-level `judge_persona` | every verdict in the skill | `grade` per model |
|
|
58
59
|
*
|
|
59
60
|
* Why this matters more than it looks: with one key, lint had exactly one remedy for
|
|
@@ -190,6 +191,11 @@ export declare const SKILL_KEY = "SKILL.md";
|
|
|
190
191
|
*/
|
|
191
192
|
export declare const SKILL_PROMPT_KEY = "skill:prompt";
|
|
192
193
|
export declare const PROMPT_PREFIX = "prompt:";
|
|
194
|
+
/** A prompt document split into its frontmatter text (null when it has none) and its body. */
|
|
195
|
+
export declare function splitPromptDoc(text: string): {
|
|
196
|
+
frontmatter: string | null;
|
|
197
|
+
body: string;
|
|
198
|
+
};
|
|
193
199
|
/**
|
|
194
200
|
* The digest of a prompt document: its body verbatim, plus every frontmatter field the
|
|
195
201
|
* model can actually receive.
|
package/dist/sources.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import yaml from "js-yaml";
|
|
5
|
+
import { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
|
|
6
|
+
export { PROMPT_NORMALIZATION_SOURCE_DIGEST, PROMPT_NORMALIZATION_SOURCE_KEY } from "./prompt-normalization.js";
|
|
5
7
|
/**
|
|
6
8
|
* What a run measured, as a map of `key -> sha256`, recorded in results.yaml so
|
|
7
9
|
* lint can prove a published result still describes the current inputs.
|
|
@@ -56,7 +58,7 @@ export const FIXTURE_PREFIX = "fixture:";
|
|
|
56
58
|
* | `stimulus:<id>` | mode, turns, workspace, remote, agent-file path, fixture path, `assert.vitest`, `post_test` path | the transcripts answer a different question | `run` (model + judge) |
|
|
57
59
|
* | `rubric:<id>` | title, checklist | transcripts fine, verdicts wrong | `grade` (judge only) |
|
|
58
60
|
* | `policy:<id>` | critical, reps, pass_threshold | only the scoring moved | `rescore` (free) |
|
|
59
|
-
* | `gates:<id>` | `diff_contains`, `diff_excludes` | needle wrong, behavior fine | `regate` (
|
|
61
|
+
* | `gates:<id>` | `diff_contains`, `diff_excludes` | needle wrong, behavior fine | `regate` (no subject call; judges fail→pass reps) |
|
|
60
62
|
* | `rubric:__persona` | spec-level `judge_persona` | every verdict in the skill | `grade` per model |
|
|
61
63
|
*
|
|
62
64
|
* Why this matters more than it looks: with one key, lint had exactly one remedy for
|
|
@@ -182,7 +184,7 @@ function facets(s) {
|
|
|
182
184
|
const _assertExhaustive = restAssert;
|
|
183
185
|
void _assertExhaustive;
|
|
184
186
|
// `traceAssert` is a GATE, not stimulus: it is evaluated against a trace the run
|
|
185
|
-
// already saved, so `regate`
|
|
187
|
+
// already saved, so `regate` can re-answer it without re-running the subject model.
|
|
186
188
|
// Note the asymmetry with `env.extensions` in Phase 3, which IS stimulus — one
|
|
187
189
|
// changes what gets executed, the other only what we conclude from it.
|
|
188
190
|
const hasGates = diff_contains !== undefined || diff_excludes !== undefined || traceAssert !== undefined || trajectoryAssert !== undefined;
|
|
@@ -194,7 +196,7 @@ function facets(s) {
|
|
|
194
196
|
// below. Changing which extensions load changes what the model can DO, so the
|
|
195
197
|
// old transcripts describe a different agent and only a re-run can answer.
|
|
196
198
|
// Changing an assertion only changes what we conclude from evidence already on
|
|
197
|
-
// disk, which `regate` can redo
|
|
199
|
+
// disk, which `regate` can redo without a subject re-run.
|
|
198
200
|
// APPENDED CONDITIONALLY, never as a fixed slot. This tuple is positional and
|
|
199
201
|
// its hash is stored in every published results.yaml, so adding an
|
|
200
202
|
// unconditional element re-hashes every scenario that never used the field —
|
|
@@ -363,6 +365,8 @@ export function sourceHashes(ctx) {
|
|
|
363
365
|
* from drifting: a new key kind is defined once, for both sides.
|
|
364
366
|
*/
|
|
365
367
|
export function currentHashFor(key, ctx) {
|
|
368
|
+
if (key === PROMPT_NORMALIZATION_SOURCE_KEY)
|
|
369
|
+
return PROMPT_NORMALIZATION_SOURCE_DIGEST;
|
|
366
370
|
if (key === SKILL_KEY)
|
|
367
371
|
return fileSha256(resolve(ctx.skillDir, "SKILL.md"));
|
|
368
372
|
if (key === SKILL_PROMPT_KEY)
|
|
@@ -408,6 +412,8 @@ export function currentHashFor(key, ctx) {
|
|
|
408
412
|
}
|
|
409
413
|
/** Human label for a recorded key, used in lint messages. */
|
|
410
414
|
export function describeSourceKey(key) {
|
|
415
|
+
if (key === PROMPT_NORMALIZATION_SOURCE_KEY)
|
|
416
|
+
return "the prompt normalization rule registry";
|
|
411
417
|
if (key === PERSONA_KEY)
|
|
412
418
|
return "the judge persona";
|
|
413
419
|
// Deliberately the same label as the raw-bytes key: only one of the pair is ever
|
|
@@ -439,6 +445,8 @@ export function describeSourceKey(key) {
|
|
|
439
445
|
* in place. Naming the actual remedy is what converts that into a free command.
|
|
440
446
|
*/
|
|
441
447
|
export function remedyForKey(key) {
|
|
448
|
+
if (key === PROMPT_NORMALIZATION_SOURCE_KEY)
|
|
449
|
+
return "re-run — prompt provenance cannot be recomputed without the original model-visible payload";
|
|
442
450
|
if (key === PERSONA_KEY) {
|
|
443
451
|
return "re-grade each model's saved transcripts (`grade <run-dir>`) — judge-only, no model spend";
|
|
444
452
|
}
|
|
@@ -449,7 +457,7 @@ export function remedyForKey(key) {
|
|
|
449
457
|
return "re-score the saved reps (`rescore <run-dir>`) — free, offline";
|
|
450
458
|
}
|
|
451
459
|
if (key.startsWith(GATES_PREFIX)) {
|
|
452
|
-
return "re-evaluate
|
|
460
|
+
return "re-evaluate gates against saved artifacts (`regate <run-dir>`) — no subject call; fail→pass reps require judge calls";
|
|
453
461
|
}
|
|
454
462
|
// A pre-split run recorded one hash over stimulus + rubric + policy + gates, so
|
|
455
463
|
// which of them moved is genuinely unknowable from the record. Naming a cheap
|
|
@@ -498,7 +506,7 @@ const CAPABILITY_KEYS = new Set(["allowed-tools", "tools"]);
|
|
|
498
506
|
/** Opening `---` on line 1 only — a `---` further down is a horizontal rule. */
|
|
499
507
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
500
508
|
/** A prompt document split into its frontmatter text (null when it has none) and its body. */
|
|
501
|
-
function splitPromptDoc(text) {
|
|
509
|
+
export function splitPromptDoc(text) {
|
|
502
510
|
const m = FRONTMATTER_RE.exec(text);
|
|
503
511
|
// Unterminated frontmatter falls through to "all body", the same call
|
|
504
512
|
// instruction-coverage.ts makes: treat the whole file as content rather than lose it.
|
package/dist/spec.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface SeededAssert {
|
|
|
23
23
|
}
|
|
24
24
|
export interface TrajectoryEventSource {
|
|
25
25
|
/** Native format adapter registered by the harness adapter. */
|
|
26
|
-
adapter: "normalized-v1" | "principal-assurance-v1" | "pi-daddy-v1";
|
|
26
|
+
adapter: "normalized-v1" | "principal-assurance-v1" | "pi-daddy-v1" | "pi-daddy-ledger-v3";
|
|
27
27
|
/** Workspace-relative file/glob. Never an absolute path or traversal. */
|
|
28
28
|
path: string;
|
|
29
29
|
/** Missing evidence is an error by default; false is for optional supplemental ledgers. */
|
package/dist/spec.js
CHANGED
|
@@ -97,7 +97,7 @@ function resolveEventSources(env, id, file) {
|
|
|
97
97
|
throw new SpecError(`scenario \`${id}\` env.event_sources must be a non-empty list`, file);
|
|
98
98
|
}
|
|
99
99
|
const allowedAdapters = new Set([
|
|
100
|
-
"normalized-v1", "principal-assurance-v1", "pi-daddy-v1",
|
|
100
|
+
"normalized-v1", "principal-assurance-v1", "pi-daddy-v1", "pi-daddy-ledger-v3",
|
|
101
101
|
]);
|
|
102
102
|
return raw.map((entry, index) => {
|
|
103
103
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
package/dist/stability.js
CHANGED
|
@@ -9,7 +9,7 @@ function conclusive(v) {
|
|
|
9
9
|
// Same rule lift.ts applies, for the same reason: ERROR is a harness failure and an
|
|
10
10
|
// unresolved misfire is a judge failure. Counting either as a side of a flip would
|
|
11
11
|
// report infrastructure noise as behavioural instability.
|
|
12
|
-
return !v.suspect && v.verdict !== "ERROR" && v.verdict !== "JUDGE-AMBIGUOUS";
|
|
12
|
+
return !v.suspect && v.verdict !== "ERROR" && v.verdict !== "NOT-MEASURED" && v.verdict !== "JUDGE-AMBIGUOUS";
|
|
13
13
|
}
|
|
14
14
|
function pointFor(r, s, verdict) {
|
|
15
15
|
const reps = s.reps ?? 1;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ArgPredicate, type CountConstraint } from "./trace-gates.js";
|
|
2
2
|
/** Adapter-neutral event contract used by objective trajectory assertions. */
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const LEGACY_TRAJECTORY_EVENT_VERSION: "1.0";
|
|
4
|
+
export declare const TRAJECTORY_EVENT_VERSION: "1.1";
|
|
5
|
+
export type TrajectoryEventVersion = typeof LEGACY_TRAJECTORY_EVENT_VERSION | typeof TRAJECTORY_EVENT_VERSION;
|
|
4
6
|
export declare const TRAJECTORY_ASSERT_VERSION: "1.0";
|
|
5
7
|
export interface TrajectoryApproval {
|
|
6
8
|
id?: string;
|
|
@@ -26,7 +28,7 @@ export interface TrajectoryDigests {
|
|
|
26
28
|
* needs to know whether the source was pi, pi-daddy, or principal assurance.
|
|
27
29
|
*/
|
|
28
30
|
export interface TrajectoryEventV1 {
|
|
29
|
-
event_version:
|
|
31
|
+
event_version: TrajectoryEventVersion;
|
|
30
32
|
seq: number;
|
|
31
33
|
type: string;
|
|
32
34
|
source: string;
|
|
@@ -38,6 +40,16 @@ export interface TrajectoryEventV1 {
|
|
|
38
40
|
finding_id?: string;
|
|
39
41
|
parent_id?: string;
|
|
40
42
|
child_id?: string;
|
|
43
|
+
/** Unique pi-daddy v3 execution occurrence; never synthesized for v2/0.17. */
|
|
44
|
+
execution_id?: string;
|
|
45
|
+
/** Explicit v3 execution parent. Null is a meaningful root identity. */
|
|
46
|
+
parent_execution_id?: string | null;
|
|
47
|
+
/** Unique v3 occurrence whose output composed this task. */
|
|
48
|
+
task_from_execution_id?: string;
|
|
49
|
+
/** Identity of a pi-daddy v3 workflow_fact occurrence. */
|
|
50
|
+
workflow_fact_id?: string;
|
|
51
|
+
/** Immutable v3 nonterminal lifecycle deadline. */
|
|
52
|
+
deadline_at?: string;
|
|
41
53
|
phase?: string;
|
|
42
54
|
tool?: string;
|
|
43
55
|
capability?: string;
|
package/dist/trajectory-gates.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { parsePredicate, testPredicate } from "./trace-gates.js";
|
|
3
3
|
/** Adapter-neutral event contract used by objective trajectory assertions. */
|
|
4
|
-
export const
|
|
4
|
+
export const LEGACY_TRAJECTORY_EVENT_VERSION = "1.0";
|
|
5
|
+
export const TRAJECTORY_EVENT_VERSION = "1.1";
|
|
5
6
|
export const TRAJECTORY_ASSERT_VERSION = "1.0";
|
|
6
7
|
export function trajectoryEventsSha256(events) {
|
|
7
8
|
return createHash("sha256").update(stableStringify(events)).digest("hex");
|
|
@@ -434,7 +435,7 @@ export function runTrajectoryMutationSelfTest() {
|
|
|
434
435
|
const h = "a".repeat(40), t = "b".repeat(40), p = "1".repeat(64);
|
|
435
436
|
const cases = [
|
|
436
437
|
{ id: "remove-required-event", assertion: { version: "1.0", require: [{ event: "risk_classified" }] }, good: [e(1, "risk_classified")], mutate: () => [e(1, "unrelated")] },
|
|
437
|
-
{ id: "add-forbidden-tool-side-effect", assertion: { version: "1.0", forbid: [{ event: "tool_called", where: { tool: { equals: "rm" } } }, { event: "
|
|
438
|
+
{ id: "add-forbidden-tool-side-effect-approval", assertion: { version: "1.0", forbid: [{ event: "tool_called", where: { tool: { equals: "rm" } } }, { event: "side_effect_approved" }] }, good: [e(1, "tool_called", { tool: "read" })], mutate: (events) => [...events, e(2, "side_effect_approved", { attributes: { action: "migration" } })] },
|
|
438
439
|
{ id: "reorder-transition", assertion: { version: "1.0", ordered: [[{ event: "phase_started" }, { event: "phase_completed" }]] }, good: [e(1, "phase_started"), e(2, "phase_completed")], mutate: () => [e(2, "phase_started"), e(1, "phase_completed")] },
|
|
439
440
|
{ id: "substitute-workspace-id", assertion: { version: "1.0", correlate: [{ left: { event: "code_changed" }, right: { event: "evidence_recorded" }, same: ["workspace_id"] }] }, good: [e(1, "code_changed", { workspace_id: "ws-1" }), e(2, "evidence_recorded", { workspace_id: "ws-1" })], mutate: (events) => events.map((x) => x.type === "evidence_recorded" ? { ...x, workspace_id: "ws-2" } : x) },
|
|
440
441
|
{ id: "concurrent-writer", assertion: { version: "1.0", forbid: [{ event: "writer_lease_conflict" }] }, good: [e(1, "writer_lease_acquired", { workspace_id: "ws-1" })], mutate: (events) => [...events, e(2, "writer_lease_conflict", { workspace_id: "ws-1" })] },
|
|
@@ -447,7 +448,13 @@ export function runTrajectoryMutationSelfTest() {
|
|
|
447
448
|
{ id: "remove-requirement-coverage", assertion: { version: "1.0", coverage: [{ requirements: ["AUTH-7"], events: { event: "evidence_recorded" } }] }, good: [e(1, "evidence_recorded", { requirements: ["AUTH-7"] })], mutate: (events) => events.map((x) => ({ ...x, requirements: [] })) },
|
|
448
449
|
{ id: "mutate-superseded-task", assertion: { version: "1.0", forbid_after: [{ anchor: { event: "task_packet_superseded" }, forbidden: [{ event: "code_changed" }, { event: "repair_started" }], same: ["task_id"] }] }, good: [e(1, "task_packet_superseded", { task_id: "old" }), e(2, "code_changed", { task_id: "new" })], mutate: (events) => events.map((x) => x.type === "code_changed" ? { ...x, task_id: "old" } : x) },
|
|
449
450
|
{ id: "reuse-context-id", assertion: { version: "1.0", unique: [{ events: { event: "review_recorded" }, fields: ["context_id"] }] }, good: [e(1, "review_recorded", { context_id: "c1" }), e(2, "review_recorded", { context_id: "c2" })], mutate: (events) => events.map((x) => x.seq === 2 ? { ...x, context_id: "c1" } : x) },
|
|
450
|
-
{ id: "mismatch-finalization-identity", assertion: { version: "1.0", correlate: [{ left: { event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" } } }, right: { event: "finalization_completed" }, same: ["digests.head", "digests.tree"], order: "before" }] }, good: [e(1, "gate_evaluated", { digests: { head: h, tree: t }, attributes: { gate: "finalize",
|
|
451
|
+
{ id: "mismatch-finalization-identity", assertion: { version: "1.0", correlate: [{ left: { event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" } } }, right: { event: "finalization_completed" }, same: ["digests.head", "digests.tree"], order: "before" }] }, good: [e(1, "gate_evaluated", { digests: { head: h, tree: t }, attributes: { gate: "finalize", code: "OK" } }), e(2, "finalization_completed", { digests: { head: h, tree: t } })], mutate: (events) => events.map((x) => x.type === "finalization_completed" ? { ...x, digests: { head: h, tree: "d".repeat(40) } } : x) },
|
|
452
|
+
{ id: "v3-blocked-critical-code", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.code": { equals: "BLOCKED_CRITICAL_ASSURANCE" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "BLOCKED_CRITICAL_ASSURANCE", missing_count: 2 } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "OK" } })) },
|
|
453
|
+
{ id: "v3-stale-gate-must-block", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.code": { matches: "^BLOCKED_" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "BLOCKED_ASSURANCE" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "OK" } })) },
|
|
454
|
+
{ id: "v3-finalize-gate-must-be-ok", assertion: { version: "1.0", require: [{ event: "gate_evaluated", where: { "attributes.gate": { equals: "finalize" }, "attributes.code": { equals: "OK" } } }] }, good: [e(1, "gate_evaluated", { attributes: { gate: "finalize", code: "OK" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, code: "BLOCKED_ASSURANCE" } })) },
|
|
455
|
+
{ id: "v3-discard-requires-explicit-request", assertion: { version: "1.0", forbid: [{ event: "finish_selected", where: { "attributes.choice": { equals: "discard" }, "attributes.explicit_request": { equals: false } } }] }, good: [e(1, "finish_selected", { attributes: { choice: "discard", explicit_request: true } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, explicit_request: false } })) },
|
|
456
|
+
{ id: "v3-side-effect-approval-and-gate", assertion: { version: "1.0", ordered: [[{ event: "side_effect_approved", where: { "attributes.action": { equals: "migration" } } }, { event: "gate_evaluated", where: { "attributes.gate": { equals: "side-effect" }, "attributes.code": { equals: "OK" } } }]] }, good: [e(1, "side_effect_approved", { attributes: { action: "migration" } }), e(2, "gate_evaluated", { attributes: { gate: "side-effect", code: "OK" } })], mutate: (events) => events.map((x) => x.type === "gate_evaluated" ? { ...x, attributes: { ...x.attributes, code: "BLOCKED_ASSURANCE" } } : x) },
|
|
457
|
+
{ id: "v3-governed-spawn-started", assertion: { version: "1.0", require: [{ event: "child_started", where: { source: { equals: "pi-daddy-v3" }, "attributes.state": { equals: "starting" } } }] }, good: [e(1, "child_started", { source: "pi-daddy-v3", attributes: { state: "starting" } })], mutate: (events) => events.map((x) => ({ ...x, attributes: { ...x.attributes, state: "failed" } })) },
|
|
451
458
|
];
|
|
452
459
|
const results = cases.map((testCase) => {
|
|
453
460
|
const baseline = evaluateTrajectoryGates(testCase.assertion, testCase.good);
|
|
@@ -461,9 +468,11 @@ export function runTrajectoryMutationSelfTest() {
|
|
|
461
468
|
// ---------------------------------------------------------------------------
|
|
462
469
|
// Internals
|
|
463
470
|
// ---------------------------------------------------------------------------
|
|
471
|
+
const V11_EVENT_KEYS = new Set(["execution_id", "parent_execution_id", "task_from_execution_id", "workflow_fact_id", "deadline_at"]);
|
|
464
472
|
const EVENT_KEYS = new Set([
|
|
465
473
|
"event_version", "seq", "type", "source", "at", "run_id", "task_id", "workspace_id", "context_id", "finding_id",
|
|
466
|
-
"parent_id", "child_id",
|
|
474
|
+
"parent_id", "child_id", ...V11_EVENT_KEYS,
|
|
475
|
+
"phase", "tool", "capability", "requested_capabilities", "effective_capabilities", "refusal_code",
|
|
467
476
|
"exit_code", "digests", "approval", "requirements", "attributes",
|
|
468
477
|
]);
|
|
469
478
|
const APPROVAL_KEYS = new Set(["id", "capability", "subject", "source", "scope", "approved_at", "expires_at", "used_at"]);
|
|
@@ -479,8 +488,14 @@ function validateEvent(event) {
|
|
|
479
488
|
const unknown = Object.keys(object).find((key) => !EVENT_KEYS.has(key));
|
|
480
489
|
if (unknown)
|
|
481
490
|
return `unknown field ${unknown}`;
|
|
482
|
-
if (event.event_version !== TRAJECTORY_EVENT_VERSION)
|
|
491
|
+
if (event.event_version !== LEGACY_TRAJECTORY_EVENT_VERSION && event.event_version !== TRAJECTORY_EVENT_VERSION) {
|
|
483
492
|
return `unsupported event_version ${String(event.event_version)}`;
|
|
493
|
+
}
|
|
494
|
+
if (event.event_version === LEGACY_TRAJECTORY_EVENT_VERSION) {
|
|
495
|
+
const versionedField = Object.keys(object).find((key) => V11_EVENT_KEYS.has(key));
|
|
496
|
+
if (versionedField)
|
|
497
|
+
return `${versionedField} requires event_version ${TRAJECTORY_EVENT_VERSION}`;
|
|
498
|
+
}
|
|
484
499
|
if (!Number.isInteger(event.seq) || event.seq < 1)
|
|
485
500
|
return "seq must be a positive integer";
|
|
486
501
|
if (typeof event.type !== "string" || !event.type)
|
|
@@ -489,10 +504,15 @@ function validateEvent(event) {
|
|
|
489
504
|
return "source must be a non-empty string";
|
|
490
505
|
if (event.at !== undefined && !validDate(event.at))
|
|
491
506
|
return "at must be an RFC 3339 date-time";
|
|
492
|
-
for (const field of ["run_id", "task_id", "workspace_id", "context_id", "finding_id", "parent_id", "child_id"]) {
|
|
507
|
+
for (const field of ["run_id", "task_id", "workspace_id", "context_id", "finding_id", "parent_id", "child_id", "execution_id", "task_from_execution_id", "workflow_fact_id"]) {
|
|
493
508
|
if (event[field] !== undefined && (typeof event[field] !== "string" || !ID_RE.test(event[field])))
|
|
494
509
|
return `${field} is not a valid bounded identifier`;
|
|
495
510
|
}
|
|
511
|
+
if (event.parent_execution_id !== undefined && event.parent_execution_id !== null &&
|
|
512
|
+
(typeof event.parent_execution_id !== "string" || !ID_RE.test(event.parent_execution_id)))
|
|
513
|
+
return "parent_execution_id is not a valid bounded identifier or null";
|
|
514
|
+
if (event.deadline_at !== undefined && !validDate(event.deadline_at))
|
|
515
|
+
return "deadline_at must be an RFC 3339 date-time";
|
|
496
516
|
for (const field of ["phase", "tool", "capability"]) {
|
|
497
517
|
if (event[field] !== undefined && (typeof event[field] !== "string" || !event[field]))
|
|
498
518
|
return `${field} must be a non-empty string`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Verdict } from "./score.js";
|
|
2
|
+
/** Domain-neutral vote shape shared by ordinary adjudication and qualification panels. */
|
|
3
|
+
export interface PanelVote {
|
|
4
|
+
ordinal: number;
|
|
5
|
+
verdict: Verdict;
|
|
6
|
+
suspect: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface VotePanelCollapse {
|
|
9
|
+
state: "confirmed" | "tie_broken" | "unresolved";
|
|
10
|
+
verdict?: "PASS" | "FAIL";
|
|
11
|
+
clean_votes: number;
|
|
12
|
+
pass_votes: number;
|
|
13
|
+
fail_votes: number;
|
|
14
|
+
/** True only when the first two answers are both clean and oppose one another. */
|
|
15
|
+
split: boolean;
|
|
16
|
+
/** Minority clean votes / all clean votes; zero when there is no minority. */
|
|
17
|
+
minority_rate: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function isCleanPanelVote(vote: PanelVote): vote is PanelVote & {
|
|
20
|
+
verdict: "PASS" | "FAIL";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One collapse rule for both skill-harness adjudication and qualification.
|
|
24
|
+
* Invalid, ambiguous, and suspect answers remain evidence but are never votes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function collapseVotePanel(votes: readonly PanelVote[]): VotePanelCollapse;
|
|
27
|
+
export declare function runVotePanel<T extends PanelVote>(options: {
|
|
28
|
+
cast: (ordinal: 1 | 2 | 3) => Promise<T>;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
votes: T[];
|
|
31
|
+
collapse: VotePanelCollapse;
|
|
32
|
+
calls_made: number;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function isCleanPanelVote(vote) {
|
|
2
|
+
return !vote.suspect && (vote.verdict === "PASS" || vote.verdict === "FAIL");
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* One collapse rule for both skill-harness adjudication and qualification.
|
|
6
|
+
* Invalid, ambiguous, and suspect answers remain evidence but are never votes.
|
|
7
|
+
*/
|
|
8
|
+
export function collapseVotePanel(votes) {
|
|
9
|
+
const clean = votes.filter(isCleanPanelVote);
|
|
10
|
+
const passVotes = clean.filter((vote) => vote.verdict === "PASS").length;
|
|
11
|
+
const failVotes = clean.length - passVotes;
|
|
12
|
+
const firstTwo = votes.filter((vote) => vote.ordinal === 1 || vote.ordinal === 2);
|
|
13
|
+
const split = firstTwo.length === 2 && firstTwo.every(isCleanPanelVote) && firstTwo[0].verdict !== firstTwo[1].verdict;
|
|
14
|
+
const minority = Math.min(passVotes, failVotes);
|
|
15
|
+
const base = {
|
|
16
|
+
clean_votes: clean.length,
|
|
17
|
+
pass_votes: passVotes,
|
|
18
|
+
fail_votes: failVotes,
|
|
19
|
+
split,
|
|
20
|
+
minority_rate: clean.length === 0 ? 0 : minority / clean.length,
|
|
21
|
+
};
|
|
22
|
+
if (clean.length < 2)
|
|
23
|
+
return { ...base, state: "unresolved" };
|
|
24
|
+
if (passVotes === 0 || failVotes === 0)
|
|
25
|
+
return { ...base, state: "confirmed", verdict: clean[0].verdict };
|
|
26
|
+
if (passVotes > failVotes)
|
|
27
|
+
return { ...base, state: "tie_broken", verdict: "PASS" };
|
|
28
|
+
if (failVotes > passVotes)
|
|
29
|
+
return { ...base, state: "tie_broken", verdict: "FAIL" };
|
|
30
|
+
return { ...base, state: "unresolved" };
|
|
31
|
+
}
|
|
32
|
+
export async function runVotePanel(options) {
|
|
33
|
+
const votes = [await options.cast(1), await options.cast(2)];
|
|
34
|
+
let collapse = collapseVotePanel(votes);
|
|
35
|
+
if (collapse.split) {
|
|
36
|
+
votes.push(await options.cast(3));
|
|
37
|
+
collapse = collapseVotePanel(votes);
|
|
38
|
+
}
|
|
39
|
+
return { votes, collapse, calls_made: votes.length };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=vote-panel.js.map
|
package/dist/workspace.js
CHANGED
|
@@ -143,7 +143,10 @@ function gitBaseline(cwd) {
|
|
|
143
143
|
* claim in the prompt, and needs no network.
|
|
144
144
|
*/
|
|
145
145
|
function addLocalRemote(cwd) {
|
|
146
|
-
|
|
146
|
+
// Use the directory mkdtemp created. Appending `.git` orphaned the original
|
|
147
|
+
// empty directory on every remote-backed scenario because cleanup removed only
|
|
148
|
+
// the suffixed repository.
|
|
149
|
+
const bare = mkdtempSync(join(tmpdir(), "sc-remote-"));
|
|
147
150
|
execFileSync("git", ["init", "-q", "--bare", "-b", "main", bare], { timeout: GIT_TIMEOUT_MS });
|
|
148
151
|
execFileSync("git", ["remote", "add", "origin", bare], { cwd, timeout: GIT_TIMEOUT_MS });
|
|
149
152
|
execFileSync("git", ["push", "-q", "-u", "origin", "main"], { cwd, timeout: GIT_TIMEOUT_MS });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skill-harness/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "skill-harness engine — spec, discover, run, LLM-judge grade, score, results (internal API)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"node": ">=20"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"prepack": "
|
|
42
|
+
"prepack": "node ../../scripts/release-pack.mjs --guard"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"js-yaml": "^4.1
|
|
45
|
+
"js-yaml": "^4.3.1"
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://skill-harness.dev/schemas/qualification-cell-result-v1.schema.json",
|
|
4
|
+
"title": "skill-harness qualification cell result v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "cell_id", "scenario_id", "subject_arm", "critical", "pass_threshold", "panels", "critical_failure", "acceptance", "collection", "disagreement"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": "qualification-cell-result-v1" },
|
|
10
|
+
"cell_id": { "$ref": "#/$defs/id" }, "scenario_id": { "$ref": "#/$defs/id" }, "subject_arm": { "$ref": "#/$defs/id" },
|
|
11
|
+
"critical": { "type": "boolean" }, "pass_threshold": { "type": "integer", "minimum": 1 },
|
|
12
|
+
"panels": { "type": "array", "minItems": 1, "items": { "$ref": "qualification-judge-panel-result-v1.schema.json" } },
|
|
13
|
+
"verdict": { "enum": ["PASS", "FAIL"] }, "critical_failure": { "type": "boolean" },
|
|
14
|
+
"acceptance": { "enum": ["pass", "fail", "inconclusive"] }, "collection": { "const": "continue" },
|
|
15
|
+
"disagreement": {
|
|
16
|
+
"type": "object", "additionalProperties": false,
|
|
17
|
+
"required": ["judge_calls", "clean_votes", "split_artifacts", "artifacts_with_two_clean_initial_votes", "unresolved_artifacts", "judge_split_rate"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"judge_calls": { "type": "integer", "minimum": 0 }, "clean_votes": { "type": "integer", "minimum": 0 },
|
|
20
|
+
"split_artifacts": { "type": "integer", "minimum": 0 }, "artifacts_with_two_clean_initial_votes": { "type": "integer", "minimum": 0 },
|
|
21
|
+
"unresolved_artifacts": { "type": "integer", "minimum": 0 }, "judge_split_rate": { "type": "number", "minimum": 0, "maximum": 1 }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"allOf": [
|
|
26
|
+
{ "if": { "properties": { "acceptance": { "const": "inconclusive" } }, "required": ["acceptance"] }, "then": { "not": { "properties": { "verdict": {} }, "required": ["verdict"] }, "properties": { "critical_failure": { "const": false } } }, "else": { "required": ["verdict"] } },
|
|
27
|
+
{ "if": { "properties": { "acceptance": { "const": "pass" } }, "required": ["acceptance"] }, "then": { "properties": { "verdict": { "const": "PASS" }, "critical_failure": { "const": false } } } },
|
|
28
|
+
{ "if": { "properties": { "acceptance": { "const": "fail" } }, "required": ["acceptance"] }, "then": { "properties": { "verdict": { "const": "FAIL" } }, "required": ["verdict"] } },
|
|
29
|
+
{ "if": { "properties": { "critical_failure": { "const": true } }, "required": ["critical_failure"] }, "then": { "properties": { "critical": { "const": true }, "acceptance": { "const": "fail" }, "verdict": { "const": "FAIL" } }, "required": ["verdict"] } }
|
|
30
|
+
],
|
|
31
|
+
"$defs": { "id": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" } }
|
|
32
|
+
}
|