@agentsfleet/orly 0.12.1 → 0.14.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 +31 -13
- package/docs/CHANGELOG.md +20 -0
- package/docs/JUDGMENTS.md +4 -3
- package/docs/REST_API_DESIGN_GUIDELINES.md +12 -3
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/src/judgment_comparison.test.ts +138 -0
- package/src/judgment_comparison_controls.test.ts +81 -0
- package/src/judgment_comparison_jev.integration.test.ts +134 -0
- package/src/judgment_comparison_jev_consumer.test.ts +144 -0
- package/src/judgment_comparison_jev_report.test.ts +167 -0
- package/src/judgment_labels.test.ts +119 -0
- package/src/judgment_score.test.ts +166 -0
- package/src/judgment_tasks.test.ts +164 -0
- package/src/judgment_tasks_findings.test.ts +76 -0
- package/src/judgment_tasks_recovery.test.ts +119 -0
- package/src/judgments/transport.ts +2 -2
- package/src/judgments/types.ts +1 -0
- package/src/judgments/wire.ts +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { CONSUMER_SCENARIOS, CONSUMER_SOURCE, CONSUMER_TEST, OWNER_FILES, consumerProbe, mutateConsumer } from "../evals/judgments/comparison/consumer-checks";
|
|
7
|
+
import { consumerJevExamples, consumerReceiptSchema, CONSUMER_JEV_ERROR } from "../evals/judgments/comparison/jev-consumer";
|
|
8
|
+
import { MAX_JEV_REPORT_BYTES } from "../evals/judgments/comparison/jev-report";
|
|
9
|
+
import { CANDIDATE, CLASSIFICATION } from "../evals/judgments/comparison/types";
|
|
10
|
+
import { validateCatalog } from "../evals/judgments/comparison/validate";
|
|
11
|
+
import { MAX_SOURCE_BYTES } from "./judgments/constants";
|
|
12
|
+
import { digest, readBounded } from "./judgments/files";
|
|
13
|
+
|
|
14
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
15
|
+
const FIXTURE_ROOT = join(ROOT, "evals/judgments/comparison/fixtures");
|
|
16
|
+
const RECEIPT = join(ROOT, "evals/judgments/comparison/receipts/consumer-final.json");
|
|
17
|
+
const CLI = join(ROOT, "evals/judgments/comparison/jev-run.ts");
|
|
18
|
+
const PRIVATE_OWNER = "Owner bytes preserved in this isolated test.";
|
|
19
|
+
const TIMEOUT = 30_000;
|
|
20
|
+
|
|
21
|
+
class ConsumerFixture {
|
|
22
|
+
readonly parent = realpathSync(mkdtempSync(join(tmpdir(), "orly-jev-consumer-")));
|
|
23
|
+
readonly root = join(this.parent, "consumer");
|
|
24
|
+
readonly receipt = join(this.parent, "consumer.json");
|
|
25
|
+
|
|
26
|
+
async setup() {
|
|
27
|
+
const source = await readBounded(join(FIXTURE_ROOT, "consumer-source.txt"), MAX_SOURCE_BYTES);
|
|
28
|
+
const tests = await readBounded(join(FIXTURE_ROOT, "consumer-tests.txt"), MAX_SOURCE_BYTES);
|
|
29
|
+
for (const file of [CONSUMER_SOURCE, ...OWNER_FILES]) {
|
|
30
|
+
const path = join(this.root, file);
|
|
31
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
32
|
+
await Bun.write(path, file === CONSUMER_SOURCE ? source : file === CONSUMER_TEST ? tests : PRIVATE_OWNER);
|
|
33
|
+
}
|
|
34
|
+
const report = consumerReceiptSchema.parse(JSON.parse(await readBounded(RECEIPT, MAX_SOURCE_BYTES)));
|
|
35
|
+
report.metadata.owner_files = Object.fromEntries(OWNER_FILES.map((file) => [file, digest(file === CONSUMER_TEST ? tests : PRIVATE_OWNER)]));
|
|
36
|
+
for (const scenario of CONSUMER_SCENARIOS) for (const suffix of ["submitted", "hidden", "repaired"]) {
|
|
37
|
+
const text = suffix === "repaired" ? source : mutateConsumer(source, scenario);
|
|
38
|
+
const identity = digest(JSON.stringify([{ path: "consumer-source.txt", digest: digest(text) },
|
|
39
|
+
{ path: "probe.ts", digest: digest(consumerProbe(this.root, suffix !== "submitted")) }]));
|
|
40
|
+
const entry = report.attempts.find((item) => item.label === `${scenario}:${suffix}`);
|
|
41
|
+
if (!entry) throw new Error("Missing recorded example.");
|
|
42
|
+
entry.receipt.source_before = identity;
|
|
43
|
+
entry.receipt.source_after = identity;
|
|
44
|
+
}
|
|
45
|
+
await Bun.write(this.receipt, JSON.stringify(report));
|
|
46
|
+
return report;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async examples() {
|
|
50
|
+
const catalog = validateCatalog(await Bun.file(join(ROOT, "evals/judgments/comparison/catalog.json")).json());
|
|
51
|
+
return consumerJevExamples(this.root, this.receipt, catalog);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[Symbol.dispose]() { rmSync(this.parent, { recursive: true, force: true }); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("jev_inputs_exclude_expected_labels_and_validate_consumer_receipts", async () => {
|
|
58
|
+
using fixture = new ConsumerFixture();
|
|
59
|
+
await fixture.setup();
|
|
60
|
+
const examples = await fixture.examples();
|
|
61
|
+
expect(examples).toHaveLength(12);
|
|
62
|
+
expect(examples.find((entry) => entry.id === "consumer-context-known-gap")).toMatchObject({ question: CANDIDATE.evidence, expected: CLASSIFICATION.defective });
|
|
63
|
+
expect(examples.find((entry) => entry.id === "consumer-context-unknown-dependencies")).toMatchObject({ question: CANDIDATE.evidence, expected: CLASSIFICATION.insufficient });
|
|
64
|
+
for (const entry of examples) {
|
|
65
|
+
const state = JSON.parse(entry.input.request).state;
|
|
66
|
+
expect(Object.keys(state).sort()).toEqual(["evidence", "requirement"]);
|
|
67
|
+
expect(entry.input.request).not.toContain(entry.id);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("jev_consumer_refuses_each_tampered_probe_missing_outcome_and_duplicate", async () => {
|
|
72
|
+
using fixture = new ConsumerFixture();
|
|
73
|
+
const original = await fixture.setup();
|
|
74
|
+
for (const scenario of CONSUMER_SCENARIOS) for (const suffix of ["submitted", "hidden", "repaired"]) {
|
|
75
|
+
const changed = structuredClone(original);
|
|
76
|
+
const entry = changed.attempts.find((item: { label: string }) => item.label === `${scenario}:${suffix}`);
|
|
77
|
+
if (!entry) throw new Error("Missing attempt.");
|
|
78
|
+
entry.receipt.source_after = digest("unrelated source");
|
|
79
|
+
await Bun.write(fixture.receipt, JSON.stringify(changed));
|
|
80
|
+
await expect(fixture.examples()).rejects.toThrow(CONSUMER_JEV_ERROR);
|
|
81
|
+
}
|
|
82
|
+
for (const mutate of [
|
|
83
|
+
(report: typeof original) => { const entry = report.attempts.find((item) => item.label.endsWith(":submitted")); if (!entry) throw new Error("Missing attempt."); entry.label = "unrelated-attempt"; },
|
|
84
|
+
(report: typeof original) => { const entry = report.attempts.find((item) => item.label.endsWith(":hidden")); if (!entry) throw new Error("Missing attempt."); entry.receipt.result.exit_code = 0; },
|
|
85
|
+
(report: typeof original) => { const entry = report.attempts.find((item) => item.label.endsWith(":repaired")); if (!entry) throw new Error("Missing attempt."); entry.receipt.result.failure = "injected failure"; },
|
|
86
|
+
(report: typeof original) => { const entry = report.attempts[1]; if (!entry) throw new Error("Missing attempt."); report.attempts[0] = entry; },
|
|
87
|
+
(report: typeof original) => { const entry = report.attempts.find((item) => item.label.endsWith(":hidden")); if (!entry) throw new Error("Missing attempt."); entry.receipt.stdout = "[]"; },
|
|
88
|
+
(report: typeof original) => { report.metadata.restored_source = digest("not restored"); },
|
|
89
|
+
(report: typeof original) => { const owner = OWNER_FILES[0]; if (!owner) throw new Error("Missing owner."); report.metadata.owner_files[owner] = digest("changed owner"); },
|
|
90
|
+
]) {
|
|
91
|
+
const changed = structuredClone(original);
|
|
92
|
+
mutate(changed);
|
|
93
|
+
await Bun.write(fixture.receipt, JSON.stringify(changed));
|
|
94
|
+
await expect(fixture.examples()).rejects.toThrow(CONSUMER_JEV_ERROR);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("jev_cli_without_credentials_retains_every_scheduled_attempt_and_preserves_output", async () => {
|
|
99
|
+
using fixture = new ConsumerFixture();
|
|
100
|
+
await fixture.setup();
|
|
101
|
+
const output = join(fixture.parent, "results");
|
|
102
|
+
const invoke = (destination: string) => Bun.spawnSync([process.execPath, CLI, "--live", fixture.root, fixture.receipt, destination], {
|
|
103
|
+
env: { PATH: Bun.env.PATH ?? "", TYPESAFE_API_KEY: "" }, stdout: "pipe", stderr: "pipe", timeout: TIMEOUT,
|
|
104
|
+
});
|
|
105
|
+
const first = invoke(output);
|
|
106
|
+
if (first.exitCode !== 0) throw new Error(first.stderr.toString());
|
|
107
|
+
expect(first.exitCode).toBe(0);
|
|
108
|
+
const file = Bun.file(join(output, "report.json"));
|
|
109
|
+
const bytes = await file.text();
|
|
110
|
+
const report = JSON.parse(bytes);
|
|
111
|
+
expect(report.completed).toBe(true);
|
|
112
|
+
expect(report.requests).toBe(0);
|
|
113
|
+
expect(report.attempts).toHaveLength(188);
|
|
114
|
+
expect(report.results.every((entry: { status: string }) => entry.status === "unavailable")).toBe(true);
|
|
115
|
+
expect(invoke(output).exitCode).not.toBe(0);
|
|
116
|
+
expect(await file.text()).toBe(bytes);
|
|
117
|
+
const inside = invoke(join(fixture.root, "results"));
|
|
118
|
+
expect(inside.exitCode).toBe(2);
|
|
119
|
+
expect(inside.stderr.toString()).toContain("outside the authoring and consumer worktrees");
|
|
120
|
+
expect(await Bun.file(join(fixture.root, "results/report.json")).exists()).toBe(false);
|
|
121
|
+
}, TIMEOUT);
|
|
122
|
+
|
|
123
|
+
test("jev_cli_refuses_symlink_outputs_authoring_outputs_and_oversized_replay", async () => {
|
|
124
|
+
using fixture = new ConsumerFixture();
|
|
125
|
+
await fixture.setup();
|
|
126
|
+
const alias = join(fixture.parent, "consumer-alias");
|
|
127
|
+
symlinkSync(fixture.root, alias);
|
|
128
|
+
const oversized = join(fixture.parent, "oversized.json");
|
|
129
|
+
await Bun.write(oversized, " ".repeat(MAX_JEV_REPORT_BYTES + 1));
|
|
130
|
+
for (const [destination, mode, saved] of [
|
|
131
|
+
[join(alias, "results"), "--live", []],
|
|
132
|
+
[join(ROOT, "forbidden-jev-test-output"), "--live", []],
|
|
133
|
+
[alias, "--live", []],
|
|
134
|
+
[join(fixture.parent, "replay"), "--replay", [oversized]],
|
|
135
|
+
] as const) {
|
|
136
|
+
const result = Bun.spawnSync([process.execPath, CLI, mode, fixture.root, fixture.receipt, destination, ...saved], {
|
|
137
|
+
env: { PATH: Bun.env.PATH ?? "", TYPESAFE_API_KEY: "" }, stdout: "pipe", stderr: "pipe", timeout: TIMEOUT,
|
|
138
|
+
});
|
|
139
|
+
expect(result.exitCode).toBe(2);
|
|
140
|
+
expect(result.stdout.toString()).toBe("");
|
|
141
|
+
}
|
|
142
|
+
expect(await Bun.file(join(fixture.root, "results/report.json")).exists()).toBe(false);
|
|
143
|
+
expect(await Bun.file(join(ROOT, "forbidden-jev-test-output/report.json")).exists()).toBe(false);
|
|
144
|
+
}, TIMEOUT);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { loadCases } from "../evals/judgments/comparison/corpus";
|
|
5
|
+
import { corpusJevExamples, gradeJev, jevInput, JEV_PREFLIGHT_ERROR, type JevAttempt, type JevExample } from "../evals/judgments/comparison/jev";
|
|
6
|
+
import { JEV_MODE, jevReport, renderJevReport, replayJev } from "../evals/judgments/comparison/jev-report";
|
|
7
|
+
import { CANDIDATE, CASE_CLASS, CLASSIFICATION, labelsSchema, MEASUREMENT, NATIVE, OBSERVATION } from "../evals/judgments/comparison/types";
|
|
8
|
+
import { validateCatalog } from "../evals/judgments/comparison/validate";
|
|
9
|
+
import { ANSWER_KIND, MODEL, REPLY_KEY } from "./judgments/constants";
|
|
10
|
+
import { digest } from "./judgments/files";
|
|
11
|
+
import type { Reply } from "./judgments/wire";
|
|
12
|
+
|
|
13
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
14
|
+
const MISSING = "Request failed before a usable reply.";
|
|
15
|
+
|
|
16
|
+
async function inputs() {
|
|
17
|
+
const catalog = validateCatalog(await Bun.file(resolve(ROOT, "evals/judgments/comparison/catalog.json")).json());
|
|
18
|
+
const cases = await loadCases(ROOT);
|
|
19
|
+
const labels = labelsSchema.parse(await Bun.file(resolve(ROOT, "evals/judgments/comparison/labels.json")).json());
|
|
20
|
+
return { catalog, cases, labels, examples: corpusJevExamples(catalog, cases, labels) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function attempt(entry: JevExample, reply: Reply | null = null): JevAttempt {
|
|
24
|
+
return { id: entry.id, question: entry.question, identity: entry.identity, request_digest: digest(entry.input.request),
|
|
25
|
+
requests: reply === null ? 0 : 1, elapsed_ms: 0, reply, failure: reply === null ? MISSING : null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function reply(entry: JevExample, decision: string, strength = 1): Reply {
|
|
29
|
+
const question = entry.input.definition.question;
|
|
30
|
+
const keys = Object.keys(question.criteria);
|
|
31
|
+
return { model: MODEL, answers: { [REPLY_KEY]: question.type === ANSWER_KIND.noul ? { type: question.type, noul: strength } : {
|
|
32
|
+
type: question.type, choice: decision, confidence: strength,
|
|
33
|
+
probabilities: Object.fromEntries(keys.map((key) => [key, key === decision ? strength : (1 - strength) / (keys.length - 1)])),
|
|
34
|
+
} }, usage: { input_tokens: 100, output_tokens: 20 } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("jev_inputs_exclude_expected_labels_and_freeze_native_questions", async () => {
|
|
38
|
+
const { catalog, cases, labels, examples } = await inputs();
|
|
39
|
+
for (const entry of examples) {
|
|
40
|
+
const request = JSON.parse(entry.input.request);
|
|
41
|
+
expect(request.model).toBe(MODEL);
|
|
42
|
+
expect(Object.keys(request.questions)).toEqual([REPLY_KEY]);
|
|
43
|
+
expect(request.questions[REPLY_KEY]).toEqual(entry.input.definition.question);
|
|
44
|
+
expect(Object.keys(request.state).sort()).toEqual(["evidence", "requirement"]);
|
|
45
|
+
expect(request.state.evidence.every((item: Record<string, unknown>) => JSON.stringify(Object.keys(item).sort()) === JSON.stringify(["role", "text"]))).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
const first = cases[0];
|
|
48
|
+
const original = examples[0];
|
|
49
|
+
if (!first || !original) throw new Error("Missing cases.");
|
|
50
|
+
const changed = corpusJevExamples(catalog, [{ ...first, classification: CASE_CLASS.defective, author: "different-author", id: first.id }], labels)[0];
|
|
51
|
+
expect(changed?.input.request).toBe(original.input.request);
|
|
52
|
+
expect(changed?.identity).not.toBe(original.identity);
|
|
53
|
+
expect(() => jevInput(original.input.definition.question, { ...JSON.parse(original.input.request).state, expected: NATIVE.yes })).toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("jev_report_grades_actual_answers_without_model_accuracy_substitution", async () => {
|
|
57
|
+
const { cases, labels, examples } = await inputs();
|
|
58
|
+
const attempts = examples.map((entry) => attempt(entry));
|
|
59
|
+
const yesIndex = examples.findIndex((entry) => entry.expected === NATIVE.yes);
|
|
60
|
+
const noIndex = examples.findIndex((entry) => entry.expected === NATIVE.no);
|
|
61
|
+
const candidateIndex = examples.findIndex((entry) => entry.question === CANDIDATE.wiring && entry.expected === CLASSIFICATION.defective);
|
|
62
|
+
const uncertainIndex = examples.findIndex((entry) => entry.question === CANDIDATE.wiring && entry.expected === CLASSIFICATION.supported);
|
|
63
|
+
for (const [index, choice, strength] of [[yesIndex, NATIVE.yes, 1], [noIndex, NATIVE.yes, 1],
|
|
64
|
+
[candidateIndex, CLASSIFICATION.supported, 1], [uncertainIndex, CLASSIFICATION.supported, 0.4]] as const) {
|
|
65
|
+
const entry = examples[index];
|
|
66
|
+
if (!entry) throw new Error("Missing selected case.");
|
|
67
|
+
attempts[index] = attempt(entry, reply(entry, choice, strength));
|
|
68
|
+
}
|
|
69
|
+
const report = jevReport(examples, attempts, cases, labels, [], JEV_MODE.live, true);
|
|
70
|
+
expect(report.results[yesIndex]?.status).toBe("pass");
|
|
71
|
+
expect(report.results[noIndex]?.status).toBe("fail");
|
|
72
|
+
expect(report.results[candidateIndex]?.actual).toBe(CLASSIFICATION.supported);
|
|
73
|
+
expect(report.results[candidateIndex]?.status).toBe("fail");
|
|
74
|
+
expect(report.results[uncertainIndex]).toMatchObject({ actual: CLASSIFICATION.supported, graded: NATIVE.abstain, uncertain: true, status: "fail" });
|
|
75
|
+
expect(report.observations[uncertainIndex]).toMatchObject({ status: OBSERVATION.valid, answer: { decision: CLASSIFICATION.supported, strength: 0.4 } });
|
|
76
|
+
expect(report.observations.every((entry) => entry.measurement === MEASUREMENT.live)).toBe(true);
|
|
77
|
+
expect(report.adoption.every((entry) => !entry.eligible && entry.decision === "retain")).toBe(true);
|
|
78
|
+
expect(report.requests).toBe(4);
|
|
79
|
+
expect(report.usage).toEqual({ input_tokens: 400, output_tokens: 80 });
|
|
80
|
+
expect(report.scores).toHaveLength(22);
|
|
81
|
+
expect(report.results.filter((entry) => entry.status === "unavailable")).toHaveLength(examples.length - 4);
|
|
82
|
+
const rendered = renderJevReport(report);
|
|
83
|
+
for (const glyph of ["✅", "❌", "⚪"]) expect(rendered).toContain(glyph);
|
|
84
|
+
expect(rendered).toContain("(uncertain)");
|
|
85
|
+
expect(rendered).toContain("not measured autonomous agent improvement");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("jev_replay_preserves_original_answers_without_new_request_credit", async () => {
|
|
89
|
+
const { cases, labels, examples } = await inputs();
|
|
90
|
+
const attempts = examples.map((entry) => attempt(entry));
|
|
91
|
+
const first = examples[0];
|
|
92
|
+
if (!first) throw new Error("Missing case.");
|
|
93
|
+
attempts[0] = attempt(first, reply(first, NATIVE.yes));
|
|
94
|
+
const live = jevReport(examples, attempts, cases, labels, [], JEV_MODE.live, true);
|
|
95
|
+
const replay = jevReport(examples, replayJev(examples, live, []), cases, labels, [], JEV_MODE.replay, true);
|
|
96
|
+
expect(replay.results).toEqual(live.results);
|
|
97
|
+
expect(replay.attempts).toEqual(live.attempts);
|
|
98
|
+
expect(replay.requests).toBe(0);
|
|
99
|
+
expect(replay.original_requests).toBe(1);
|
|
100
|
+
expect(replay.observations.every((entry) => entry.measurement === MEASUREMENT.editable)).toBe(true);
|
|
101
|
+
expect(replay.adoption.every((entry) => !entry.eligible)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("jev_replay_rejects_changed_input_order_identity_reply_and_request_count", async () => {
|
|
105
|
+
const { cases, labels, examples } = await inputs();
|
|
106
|
+
const attempts = examples.map((entry) => attempt(entry));
|
|
107
|
+
const first = examples[0];
|
|
108
|
+
if (!first) throw new Error("Missing case.");
|
|
109
|
+
attempts[0] = attempt(first, reply(first, NATIVE.yes));
|
|
110
|
+
const report = jevReport(examples, attempts, cases, labels, [], JEV_MODE.live, true);
|
|
111
|
+
for (const field of ["id", "question", "identity", "request_digest"] as const) {
|
|
112
|
+
const changed = structuredClone(report);
|
|
113
|
+
const entry = changed.attempts[0];
|
|
114
|
+
if (!entry) throw new Error("Missing attempt.");
|
|
115
|
+
entry[field] = "altered";
|
|
116
|
+
expect(() => replayJev(examples, changed, [])).toThrow(JEV_PREFLIGHT_ERROR);
|
|
117
|
+
}
|
|
118
|
+
expect(() => replayJev([...examples].reverse(), report, [])).toThrow(JEV_PREFLIGHT_ERROR);
|
|
119
|
+
expect(() => replayJev(examples, { ...report, attempts: attempts.slice(1) }, [])).toThrow(JEV_PREFLIGHT_ERROR);
|
|
120
|
+
const firstAttempt = attempts[0];
|
|
121
|
+
if (!firstAttempt) throw new Error("Missing attempt.");
|
|
122
|
+
expect(() => gradeJev(first, { ...firstAttempt, requests: 0 })).toThrow(JEV_PREFLIGHT_ERROR);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("jev_replay_rejects_wrong_native_types_and_invalid_probability_distributions", async () => {
|
|
126
|
+
const { cases, labels, examples } = await inputs();
|
|
127
|
+
const index = examples.findIndex((entry) => entry.question === CANDIDATE.wiring);
|
|
128
|
+
const entry = examples[index];
|
|
129
|
+
if (!entry) throw new Error("Missing Choice example.");
|
|
130
|
+
const attempts = examples.map((value) => attempt(value));
|
|
131
|
+
for (const answer of [
|
|
132
|
+
{ type: ANSWER_KIND.noul, noul: 1 },
|
|
133
|
+
{ type: ANSWER_KIND.choice, choice: CLASSIFICATION.supported, confidence: 1, probabilities: {} },
|
|
134
|
+
{ type: ANSWER_KIND.choice, choice: CLASSIFICATION.supported, confidence: 2, probabilities: {} },
|
|
135
|
+
]) {
|
|
136
|
+
const report = jevReport(examples, attempts, cases, labels, [], JEV_MODE.live, true);
|
|
137
|
+
const changed = { ...attempt(entry), requests: 1, failure: null,
|
|
138
|
+
reply: { model: MODEL, answers: { [REPLY_KEY]: answer }, usage: { input_tokens: 1, output_tokens: 1 } } };
|
|
139
|
+
expect(() => replayJev(examples, { ...report, attempts: report.attempts.map((value, position) => position === index ? changed : value) }, [])).toThrow();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("jev_noul_uncertainty_preserves_native_probability_and_abstains", async () => {
|
|
144
|
+
const { examples } = await inputs();
|
|
145
|
+
const entry = examples.find((value) => value.input.definition.question.type === ANSWER_KIND.noul);
|
|
146
|
+
if (!entry) throw new Error("Missing Noul example.");
|
|
147
|
+
for (const probability of [0.49, 0.5, 0.51]) {
|
|
148
|
+
const native = reply(entry, NATIVE.yes, probability);
|
|
149
|
+
const result = gradeJev(entry, attempt(entry, native));
|
|
150
|
+
expect(result.actual).toBe(probability < 0.5 ? NATIVE.no : NATIVE.yes);
|
|
151
|
+
expect(result.graded).toBe(NATIVE.abstain);
|
|
152
|
+
expect(result.uncertain).toBe(true);
|
|
153
|
+
expect(native.answers[REPLY_KEY]).toEqual({ type: ANSWER_KIND.noul, noul: probability });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Saved observations must retain the evaluator that produced them.
|
|
158
|
+
test("jev_replay_rejects_missing_changed_or_reordered_source_inventory", async () => {
|
|
159
|
+
const { cases, labels, examples } = await inputs();
|
|
160
|
+
const sources = [{ path: "score.ts", digest: digest("scorer") }, { path: "jev.ts", digest: digest("transport") }];
|
|
161
|
+
const live = jevReport(examples, examples.map((entry) => attempt(entry)), cases, labels, sources, JEV_MODE.live, true);
|
|
162
|
+
expect(replayJev(examples, live, sources)).toEqual(live.attempts);
|
|
163
|
+
for (const changed of [undefined, [], sources.slice(1), [...sources].reverse(), [...sources, sources[0]],
|
|
164
|
+
sources.map((entry) => ({ ...entry, digest: "0".repeat(64) })), sources.map((entry) => ({ ...entry, path: "other.ts" }))]) {
|
|
165
|
+
expect(() => replayJev(examples, { ...live, sources: changed }, sources)).toThrow();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import catalogInput from "../evals/judgments/comparison/catalog.json";
|
|
5
|
+
import { validateLabels } from "../evals/judgments/comparison/labels";
|
|
6
|
+
import { validateCatalog } from "../evals/judgments/comparison/validate";
|
|
7
|
+
import { ADJUDICATION, CANDIDATE, CASE_CLASS, CHECK_ERROR, CLASSIFICATION, LABEL_AUTHORITY, MIN_RESOLVED_PER_GROUP, NATIVE, SPLIT,
|
|
8
|
+
type ComparisonCase, type IndependentLabel, type LabelOracle } from "../evals/judgments/comparison/types";
|
|
9
|
+
import { cleanupTemporaryDirectories, temporaryDirectory } from "./gates_test_support";
|
|
10
|
+
import { CHOICE, QUESTIONS } from "./judgments/constants";
|
|
11
|
+
import { digest } from "./judgments/files";
|
|
12
|
+
|
|
13
|
+
const FIXTURE_AUTHOR = "label-fixture-author";
|
|
14
|
+
const ORACLE_NAME = "independent-test-boundary";
|
|
15
|
+
const SOURCE_PATH = "reference.md";
|
|
16
|
+
const SOURCE_CONTENT = "An independently sourced expected value is compared with the selected result.\n";
|
|
17
|
+
const REVISION_LENGTH = 40;
|
|
18
|
+
const REVISION = "1".repeat(REVISION_LENGTH);
|
|
19
|
+
const catalog = validateCatalog(catalogInput);
|
|
20
|
+
|
|
21
|
+
afterEach(cleanupTemporaryDirectories);
|
|
22
|
+
|
|
23
|
+
function nativeAnswer(question: string, classification: ComparisonCase["classification"]): IndependentLabel["expected_native"] {
|
|
24
|
+
if (question === QUESTIONS[2]) return classification === CASE_CLASS.healthy ? CHOICE.exact : classification === CASE_CLASS.defective ? CHOICE.weak
|
|
25
|
+
: classification === CASE_CLASS.ambiguous ? NATIVE.abstain : CHOICE.insufficient;
|
|
26
|
+
if (QUESTIONS.some((item) => item === question)) return classification === CASE_CLASS.healthy ? NATIVE.yes
|
|
27
|
+
: classification === CASE_CLASS.defective ? NATIVE.no : NATIVE.abstain;
|
|
28
|
+
return classification === CASE_CLASS.healthy ? CLASSIFICATION.supported : classification === CASE_CLASS.defective ? CLASSIFICATION.defective
|
|
29
|
+
: classification === CASE_CLASS.ambiguous ? CLASSIFICATION.ambiguous : CLASSIFICATION.insufficient;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fixture() {
|
|
33
|
+
const root = temporaryDirectory();
|
|
34
|
+
await Bun.write(join(root, SOURCE_PATH), SOURCE_CONTENT);
|
|
35
|
+
const cases: ComparisonCase[] = [];
|
|
36
|
+
const labels: IndependentLabel[] = [];
|
|
37
|
+
for (const question of [...QUESTIONS, ...Object.values(CANDIDATE)]) for (const classification of Object.values(CASE_CLASS)) {
|
|
38
|
+
for (const split of Object.values(SPLIT)) for (let index = 0; index < MIN_RESOLVED_PER_GROUP; index++) {
|
|
39
|
+
const id = `${question}-${classification}-${split}-${index}`;
|
|
40
|
+
cases.push({ id, question, classification, split, author: FIXTURE_AUTHOR, origin: `${question}-${split}`,
|
|
41
|
+
requirement: `Independent unit boundary ${id}`, evidence: [] });
|
|
42
|
+
labels.push({ case_id: id, classification, expected_native: nativeAnswer(question, classification), rationale: SOURCE_CONTENT.trim(),
|
|
43
|
+
author: FIXTURE_AUTHOR, adjudication: ADJUDICATION.resolved, authority: { kind: LABEL_AUTHORITY.oracle, name: ORACLE_NAME },
|
|
44
|
+
source: { path: SOURCE_PATH, digest: digest(SOURCE_CONTENT), revision: REVISION, selected: SOURCE_CONTENT.trim() } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const oracle: LabelOracle = async (entry) => ({ classification: entry.classification, expected_native: nativeAnswer(entry.question, entry.classification),
|
|
48
|
+
source_digest: digest(SOURCE_CONTENT), revision: REVISION });
|
|
49
|
+
return { root, cases, labels, oracle };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("labels_require_independent_evidence", async () => {
|
|
53
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
54
|
+
expect(await validateLabels(root, catalog, cases, labels, oracle)).toEqual(labels);
|
|
55
|
+
const selfReviewed = labels.map((label) => ({ ...label, authority: { kind: LABEL_AUTHORITY.reviewer, name: FIXTURE_AUTHOR } }));
|
|
56
|
+
await expect(validateLabels(root, catalog, cases, selfReviewed, oracle)).rejects.toThrow(CHECK_ERROR.independence);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("all_pending_and_all_disputed_labels_cannot_satisfy_minimums", async () => {
|
|
60
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
61
|
+
for (const adjudication of [ADJUDICATION.pending, ADJUDICATION.disputed]) {
|
|
62
|
+
await expect(validateLabels(root, catalog, cases, labels.map((label) => ({ ...label, adjudication })), oracle)).rejects.toThrow(CHECK_ERROR.minimum);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a_missing_independent_source_and_invented_selected_text_refuse_labels", async () => {
|
|
67
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
68
|
+
const absent = labels.map((label) => ({ ...label, source: { ...label.source, path: "absent-reference.md" } }));
|
|
69
|
+
await expect(validateLabels(root, catalog, cases, absent, oracle)).rejects.toThrow();
|
|
70
|
+
const fabricated = labels.map((label) => ({ ...label, source: { ...label.source, selected: "Invented label approval." } }));
|
|
71
|
+
await expect(validateLabels(root, catalog, cases, fabricated, oracle)).rejects.toThrow(CHECK_ERROR.independence);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("an_independence_flag_cannot_replace_external_oracle_proof", async () => {
|
|
75
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
76
|
+
await expect(validateLabels(root, catalog, cases, labels.map((label) => ({ ...label, independent: true })), oracle)).rejects.toThrow(CHECK_ERROR.labels);
|
|
77
|
+
const wrongOracle: LabelOracle = async (entry, label) => ({ ...(await oracle(entry, label)), expected_native: CLASSIFICATION.defective });
|
|
78
|
+
await expect(validateLabels(root, catalog, cases, labels, wrongOracle)).rejects.toThrow(CHECK_ERROR.independence);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("stale_oracle_revision_and_changed_label_evidence_refuse_labels", async () => {
|
|
82
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
83
|
+
const stale: LabelOracle = async (entry, label) => ({ ...(await oracle(entry, label)), revision: "2".repeat(REVISION_LENGTH) });
|
|
84
|
+
await expect(validateLabels(root, catalog, cases, labels, stale)).rejects.toThrow(CHECK_ERROR.independence);
|
|
85
|
+
await Bun.write(join(root, SOURCE_PATH), "Changed independent evidence.\n");
|
|
86
|
+
await expect(validateLabels(root, catalog, cases, labels, oracle)).rejects.toThrow(CHECK_ERROR.independence);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("missing_duplicate_and_mismatched_labels_refuse_scoring", async () => {
|
|
90
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
91
|
+
await expect(validateLabels(root, catalog, cases, labels.slice(1), oracle)).rejects.toThrow(CHECK_ERROR.labelIdentity);
|
|
92
|
+
await expect(validateLabels(root, catalog, cases, [...labels, labels[0]], oracle)).rejects.toThrow(CHECK_ERROR.labelIdentity);
|
|
93
|
+
await expect(validateLabels(root, catalog, cases, labels.map((label) => ({ ...label, author: "invented-author" })), oracle)).rejects.toThrow(CHECK_ERROR.labelIdentity);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("labels_cannot_change_answer_types_or_relabel_case_health", async () => {
|
|
97
|
+
const { root, cases, labels, oracle } = await fixture();
|
|
98
|
+
await expect(validateLabels(root, catalog, cases, labels.map((label) => ({ ...label, expected_native: CHOICE.exact })), oracle)).rejects.toThrow(CHECK_ERROR.native);
|
|
99
|
+
await expect(validateLabels(root, catalog, cases, labels.map((label) => ({ ...label, classification: CASE_CLASS.defective })), oracle)).rejects.toThrow(CHECK_ERROR.labelIdentity);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("frozen_reference_bytes_validate_without_git_history_and_reject_modified_sources", async () => {
|
|
103
|
+
const { resolve } = await import("node:path");
|
|
104
|
+
const { loadCases } = await import("../evals/judgments/comparison/corpus");
|
|
105
|
+
const { verifyCaseLabel } = await import("../evals/judgments/comparison/hidden/label-oracle");
|
|
106
|
+
const { labelsSchema } = await import("../evals/judgments/comparison/types");
|
|
107
|
+
const { default: input } = await import("../evals/judgments/comparison/labels.json");
|
|
108
|
+
const sourceRoot = resolve(import.meta.dir, "..");
|
|
109
|
+
const label = labelsSchema.parse(input)[0];
|
|
110
|
+
if (!label) throw new Error("Missing independent label.");
|
|
111
|
+
const entry = (await loadCases(sourceRoot)).find((entry) => entry.id === label.case_id);
|
|
112
|
+
if (!entry) throw new Error("Missing sourced case.");
|
|
113
|
+
const root = temporaryDirectory();
|
|
114
|
+
const source = join(root, label.source.path);
|
|
115
|
+
await Bun.write(source, Bun.file(join(sourceRoot, label.source.path)));
|
|
116
|
+
expect((await verifyCaseLabel(root, entry, label)).source_digest).toBe(label.source.digest);
|
|
117
|
+
await Bun.write(source, "Changed reference bytes.");
|
|
118
|
+
await expect(verifyCaseLabel(root, entry, label)).rejects.toThrow(CHECK_ERROR.independence);
|
|
119
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { adoptionDecision, missingObservations, scoreQuestion, validateObservations } from "../evals/judgments/comparison/score";
|
|
4
|
+
import { ADJUDICATION, CANDIDATE, CASE_CLASS, CHECK_ERROR, CLASSIFICATION, LABEL_AUTHORITY, MEASUREMENT, NATIVE, OBSERVATION, SPLIT,
|
|
5
|
+
type ComparisonCase, type IndependentLabel, type Observation } from "../evals/judgments/comparison/types";
|
|
6
|
+
import { CHOICE, MODEL, QUESTIONS } from "./judgments/constants";
|
|
7
|
+
import { OrlyError } from "./model";
|
|
8
|
+
|
|
9
|
+
const CERTAIN = 1;
|
|
10
|
+
const UNCERTAIN = 0.5;
|
|
11
|
+
const TEST_AUTHOR = "score-fixture-author";
|
|
12
|
+
const TEST_ORACLE = "unit-boundary-oracle";
|
|
13
|
+
const DIGEST_LENGTH = 64;
|
|
14
|
+
const REVISION_LENGTH = 40;
|
|
15
|
+
const REFERENCE_SOURCE = "reference.md";
|
|
16
|
+
|
|
17
|
+
function fixture(question: ComparisonCase["question"], classifications: ComparisonCase["classification"][], expected: IndependentLabel["expected_native"][]) {
|
|
18
|
+
const cases: ComparisonCase[] = classifications.map((classification, index) => ({ id: `case-${index}`, question, classification,
|
|
19
|
+
origin: `family-${index}`, split: SPLIT.heldout, author: TEST_AUTHOR, requirement: "Assert the sourced result.", evidence: [] }));
|
|
20
|
+
const labels: IndependentLabel[] = cases.map((entry, index) => {
|
|
21
|
+
const answer = expected[index];
|
|
22
|
+
if (!answer) throw new Error("Missing unit expectation.");
|
|
23
|
+
return { case_id: entry.id, classification: entry.classification, expected_native: answer, rationale: "Exact unit boundary expectation.",
|
|
24
|
+
author: TEST_AUTHOR, adjudication: ADJUDICATION.resolved, authority: { kind: LABEL_AUTHORITY.oracle, name: TEST_ORACLE },
|
|
25
|
+
source: { path: REFERENCE_SOURCE, digest: "0".repeat(DIGEST_LENGTH), revision: "0".repeat(REVISION_LENGTH), selected: "Expected result." } };
|
|
26
|
+
});
|
|
27
|
+
const observations: Observation[] = missingObservations(cases).map((entry, index) => {
|
|
28
|
+
const decision = expected[index];
|
|
29
|
+
if (!decision) throw new Error("Missing unit answer.");
|
|
30
|
+
return { ...entry, status: OBSERVATION.valid, answer: { decision, strength: CERTAIN } };
|
|
31
|
+
});
|
|
32
|
+
return { cases, labels, observations };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("metrics_preserve_missing_outcomes", () => {
|
|
36
|
+
const { cases, labels, observations } = fixture(QUESTIONS[0], [CASE_CLASS.healthy, CASE_CLASS.defective, CASE_CLASS.ambiguous, CASE_CLASS.insufficient],
|
|
37
|
+
[NATIVE.yes, NATIVE.no, NATIVE.abstain, NATIVE.abstain]);
|
|
38
|
+
const mixed = observations.map((item, index) => index === 0 ? { ...item, status: OBSERVATION.unavailable, answer: null } : item);
|
|
39
|
+
const result = scoreQuestion(QUESTIONS[0], cases, labels, validateObservations(cases, mixed));
|
|
40
|
+
expect(result.native_correctness).toEqual({ numerator: 3, denominator: 4, value: 0.75 });
|
|
41
|
+
expect(result.defect_recall).toEqual({ numerator: 1, denominator: 1, value: 1 });
|
|
42
|
+
expect(result.false_concerns_bound).toEqual({ numerator: 1, denominator: 1, value: 1 });
|
|
43
|
+
expect(result.insufficient_detection).toBeNull();
|
|
44
|
+
expect(result.shared_abstention).toEqual({ numerator: 2, denominator: 2, value: 1 });
|
|
45
|
+
expect(() => validateObservations(cases, mixed.slice(1))).toThrow(new OrlyError(CHECK_ERROR.observations));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("healthy_applicable_yes_is_native_correctness_without_defect_metrics", () => {
|
|
49
|
+
const { cases, labels, observations } = fixture(QUESTIONS[4], [CASE_CLASS.healthy], [NATIVE.yes]);
|
|
50
|
+
const result = scoreQuestion(QUESTIONS[4], cases, labels, observations);
|
|
51
|
+
expect(result.native_correctness?.value).toBe(1);
|
|
52
|
+
expect(result.false_concerns).toBeNull();
|
|
53
|
+
expect(result.defect_recall).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("noul_half_probability_abstains_without_inventing_insufficiency", () => {
|
|
57
|
+
const { cases, labels, observations } = fixture(QUESTIONS[0], [CASE_CLASS.insufficient], [NATIVE.abstain]);
|
|
58
|
+
const answer = observations.map((item) => ({ ...item, answer: { decision: NATIVE.yes, strength: UNCERTAIN } }));
|
|
59
|
+
const result = scoreQuestion(QUESTIONS[0], cases, labels, answer);
|
|
60
|
+
expect(result.shared_abstention?.value).toBe(1);
|
|
61
|
+
expect(result.insufficient_detection).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("zero_responses_and_empty_denominators_are_unavailable", () => {
|
|
65
|
+
const { cases, labels } = fixture(CANDIDATE.obligation, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
|
|
66
|
+
const result = scoreQuestion(CANDIDATE.obligation, cases, labels, missingObservations(cases));
|
|
67
|
+
expect(result.attempted).toBe(1);
|
|
68
|
+
expect(result.unavailable).toBe(1);
|
|
69
|
+
expect(result.native_correctness).toBeNull();
|
|
70
|
+
expect(result.defect_recall).toBeNull();
|
|
71
|
+
expect(result.false_concerns_bound).toBeNull();
|
|
72
|
+
expect(scoreQuestion(CANDIDATE.wiring, [], [], []).native_correctness).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("invalid_defective_answers_are_misses_and_invalid_healthy_answers_raise_the_bound", () => {
|
|
76
|
+
const { cases, labels, observations } = fixture(CANDIDATE.failure, [CASE_CLASS.healthy, CASE_CLASS.defective], [CLASSIFICATION.supported, CLASSIFICATION.defective]);
|
|
77
|
+
const answer = observations.map((item, index) => index === 0 ? item : { ...item, status: OBSERVATION.invalid, answer: null });
|
|
78
|
+
const result = scoreQuestion(CANDIDATE.failure, cases, labels, answer);
|
|
79
|
+
expect(result.native_correctness?.value).toBe(0.5);
|
|
80
|
+
expect(result.defect_recall?.value).toBe(0);
|
|
81
|
+
expect(result.invalid).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("weak_low_confidence_assertion_is_an_abstention", () => {
|
|
85
|
+
const { cases, labels, observations } = fixture(QUESTIONS[2], [CASE_CLASS.defective], [CHOICE.weak]);
|
|
86
|
+
const answer = observations.map((item) => ({ ...item, answer: { decision: CHOICE.weak, strength: UNCERTAIN } }));
|
|
87
|
+
const result = scoreQuestion(QUESTIONS[2], cases, labels, answer);
|
|
88
|
+
expect(result.native_correctness?.value).toBe(0);
|
|
89
|
+
expect(result.defect_recall?.value).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("explicit_choice_insufficiency_is_scored_separately", () => {
|
|
93
|
+
const { cases, labels, observations } = fixture(QUESTIONS[2], [CASE_CLASS.insufficient], [CHOICE.insufficient]);
|
|
94
|
+
expect(scoreQuestion(QUESTIONS[2], cases, labels, observations).insufficient_detection?.value).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("observations_reject_stale_duplicate_unknown_and_executable_answers", () => {
|
|
98
|
+
const { cases, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy, CASE_CLASS.defective], [CLASSIFICATION.supported, CLASSIFICATION.defective]);
|
|
99
|
+
expect(() => validateObservations(cases, observations.map((item) => ({ ...item, identity: "0".repeat(DIGEST_LENGTH) })))).toThrow(CHECK_ERROR.observations);
|
|
100
|
+
expect(() => validateObservations(cases, [observations[0], observations[0]])).toThrow(CHECK_ERROR.observations);
|
|
101
|
+
expect(() => validateObservations(cases, observations.map((item) => ({ ...item, execute: "true" })))).toThrow(CHECK_ERROR.observations);
|
|
102
|
+
expect(() => validateObservations(cases.map((item) => ({ ...item, requirement: "Edited required behavior." })), observations)).toThrow(CHECK_ERROR.observations);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("adoption_requires_independent_measured_benefit", () => {
|
|
106
|
+
const { cases, labels, observations } = fixture(CANDIDATE.resolution, Object.values(CASE_CLASS), Object.values(CLASSIFICATION));
|
|
107
|
+
const decision = adoptionDecision(CANDIDATE.resolution, cases, labels, missingObservations(cases), null);
|
|
108
|
+
expect(decision.eligible).toBe(false);
|
|
109
|
+
expect(decision.reasons.length).toBe(4);
|
|
110
|
+
expect(decision.decision).toBe("retain");
|
|
111
|
+
expect(adoptionDecision(CANDIDATE.resolution, cases, labels, observations, null).eligible).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("unmeasured_candidates_remain_evaluation_only", () => {
|
|
115
|
+
for (const question of Object.values(CANDIDATE)) {
|
|
116
|
+
const { cases, labels } = fixture(question, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
|
|
117
|
+
const result = adoptionDecision(question, cases, labels, missingObservations(cases), null);
|
|
118
|
+
expect(result.eligible).toBe(false);
|
|
119
|
+
expect(result.metrics.native_correctness).toBeNull();
|
|
120
|
+
}
|
|
121
|
+
expect(QUESTIONS.length).toBe(6);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("development_cases_cannot_fill_the_heldout_adoption_minimum", () => {
|
|
125
|
+
const { cases, labels, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
|
|
126
|
+
const result = adoptionDecision(CANDIDATE.wiring, cases.map((entry) => ({ ...entry, split: SPLIT.development })), labels,
|
|
127
|
+
observations.map((entry) => ({ ...entry, measurement: MEASUREMENT.independent })), { baseline_resolved: 0, candidate_resolved: 1, unsupported_completions: 0, authority_violations: 0 });
|
|
128
|
+
expect(result.eligible).toBe(false);
|
|
129
|
+
expect(result.metrics.attempted).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("answers_from_another_question_format_count_as_invalid_attempts", () => {
|
|
133
|
+
const { cases, labels, observations } = fixture(CANDIDATE.wiring, [CASE_CLASS.healthy], [CLASSIFICATION.supported]);
|
|
134
|
+
const malformed = observations.map((entry) => ({ ...entry, answer: { decision: NATIVE.yes, strength: CERTAIN } }));
|
|
135
|
+
const validated = validateObservations(cases, malformed);
|
|
136
|
+
expect(validated.map((entry) => entry.status)).toEqual([OBSERVATION.invalid]);
|
|
137
|
+
const result = scoreQuestion(CANDIDATE.wiring, cases, labels, malformed);
|
|
138
|
+
expect(result.invalid).toBe(1);
|
|
139
|
+
expect(result.valid).toBe(0);
|
|
140
|
+
expect(result.false_concerns_bound?.value).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("pending_extra_cases_are_unscored_and_do_not_change_denominators", () => {
|
|
144
|
+
const { cases, labels, observations } = fixture(CANDIDATE.evidence, [CASE_CLASS.healthy, CASE_CLASS.healthy], [CLASSIFICATION.supported, CLASSIFICATION.supported]);
|
|
145
|
+
const pending = labels.map((entry, index) => index === 0 ? entry : { ...entry, adjudication: ADJUDICATION.pending });
|
|
146
|
+
const result = scoreQuestion(CANDIDATE.evidence, cases, pending, observations);
|
|
147
|
+
expect(result.attempted).toBe(1);
|
|
148
|
+
expect(result.unscored).toBe(1);
|
|
149
|
+
expect(result.native_correctness).toEqual({ numerator: 1, denominator: 1, value: 1 });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("fabricated_measurement_enums_and_paired_counts_never_qualify_for_adoption", () => {
|
|
153
|
+
const minimum = 20;
|
|
154
|
+
const classes = Object.values(CASE_CLASS).flatMap((classification) => Array.from({ length: minimum }, () => classification));
|
|
155
|
+
const answers = Object.values(CLASSIFICATION).flatMap((answer) => Array.from({ length: minimum }, () => answer));
|
|
156
|
+
const { cases, labels, observations } = fixture(CANDIDATE.resolution, classes, answers);
|
|
157
|
+
const fabricated = observations.map((entry) => ({ ...entry, measurement: MEASUREMENT.independent }));
|
|
158
|
+
const invalidPair = { baseline_resolved: -1, candidate_resolved: 0, unsupported_completions: 0, authority_violations: 0 };
|
|
159
|
+
const ordinaryPair = { ...invalidPair, baseline_resolved: 1, candidate_resolved: 2 };
|
|
160
|
+
for (const pair of [invalidPair, ordinaryPair]) {
|
|
161
|
+
const result = adoptionDecision(CANDIDATE.resolution, cases, labels, fabricated, pair);
|
|
162
|
+
expect(result.eligible).toBe(false);
|
|
163
|
+
expect(result.decision).toBe("retain");
|
|
164
|
+
expect(result.reasons.some((reason) => reason.includes("provenance"))).toBe(true);
|
|
165
|
+
}
|
|
166
|
+
});
|