@agentsfleet/orly 0.12.1 → 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 +19 -14
- package/docs/CHANGELOG.md +12 -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
package/README.md
CHANGED
|
@@ -42,25 +42,30 @@ Works with Claude Code, Codex, OpenCode, and Amp.
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
-
## 0.
|
|
45
|
+
## 0.13: measured judgment comparison
|
|
46
46
|
|
|
47
|
-
0.
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
The 0.13.0 source candidate adds a deterministic examiner for unsupported completion claims.
|
|
48
|
+
It freezes the six runtime questions and evaluates five proposed checks separately.
|
|
49
|
+
Those checks cover obligations, evidence, production wiring, failure tests and finding resolution.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
From a source checkout, run `bun evals/judgments/comparison/run.ts --check` to validate the frozen inputs.
|
|
52
|
+
Expected output is JSON with six baseline questions, five candidates and `passed: true`.
|
|
53
|
+
Run `bun evals/judgments/comparison/run.ts --offline` for whole-task exercises with hidden behavioral checks.
|
|
54
|
+
A successful run reports `passed: true`; seeded false completions remain failed task outcomes.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
A separate explicit `jev-run.ts --live` command measures TypeSafe Jev answers against the frozen expectations.
|
|
57
|
+
Its retained report lists every measured answer, grading match, disagreement and unavailable response.
|
|
58
|
+
Replay reproduces every result with zero new requests. Autonomous agent improvement remains unmeasured.
|
|
59
|
+
All five candidates remain evaluation-only; uncertain and failed replies stay in the report.
|
|
60
|
+
An additional rehearsal uses the packed candidate and actual `agentsfleet` source in an isolated worktree.
|
|
61
|
+
See the [evaluation design](docs/architecture/judgment-evaluation.md) for commands, evidence and limits.
|
|
62
|
+
|
|
63
|
+
The `.orly/` layout, six runtime questions and 0.12.1 audit and check-diagnostic fixes remain unchanged.
|
|
64
|
+
The [0.12 report](evals/release/release-report.md) retains the earlier package and consumer migration evidence.
|
|
65
|
+
TypeSafe's Jev model supplies runtime advice through explicit `orly judge` requests; contributor measurement uses the separate live evaluator.
|
|
59
66
|
|
|
60
|
-
0.12.1 avoids copying repository files for staged audits with no matching files
|
|
61
|
-
and makes hosted-check monitoring show job states and links before reporting a failure.
|
|
62
67
|
See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
|
|
63
|
-
The install commands below fetch the published package.
|
|
68
|
+
The install commands below fetch the published package; 0.13.0 publication is a separate owner action.
|
|
64
69
|
|
|
65
70
|
---
|
|
66
71
|
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 — 2026-10-07
|
|
4
|
+
|
|
5
|
+
A source-checkout examiner checks unsupported completion claims and grades actual TypeSafe Jev answers against frozen expectations.
|
|
6
|
+
|
|
7
|
+
- **Comparison** — freezes six runtime questions and evaluates five proposed checks with separately validated expected answers.
|
|
8
|
+
- **Task evidence** — hidden behavioral checks reject seeded false completions; reports retain failed and missing attempts.
|
|
9
|
+
- **Measurement** — retains typed Jev replies, uncertainty, failures and per-example grades; replay sends no new requests.
|
|
10
|
+
- **OpenAPI guidance** — names the two surviving parity tests and makes build regeneration plus a clean diff an explicit verify step.
|
|
11
|
+
- **Adoption** — keeps candidates evaluation-only because independent provenance and paired-task improvement remain unproved.
|
|
12
|
+
|
|
13
|
+
The installed `.orly/` layout and 0.12.1 fixes remain unchanged.
|
|
14
|
+
|
|
3
15
|
## 0.12.1
|
|
4
16
|
|
|
5
17
|
Staged audits avoid copying repository files when their filename scope is empty.
|
package/docs/JUDGMENTS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: reference
|
|
3
3
|
audience: contributor
|
|
4
4
|
verified: 2026-10-07
|
|
5
|
-
product_version: 0.
|
|
5
|
+
product_version: 0.13.0
|
|
6
6
|
executable: false
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -10,8 +10,9 @@ executable: false
|
|
|
10
10
|
|
|
11
11
|
## Synopsis
|
|
12
12
|
|
|
13
|
-
The 0.
|
|
14
|
-
The comparison
|
|
13
|
+
The 0.13.0 candidate retains the six questions shipped in 0.11.0.
|
|
14
|
+
The contributor [comparison examiner](architecture/judgment-evaluation.md) tests five evaluation-only additions with offline controls and explicitly requested TypeSafe Jev measurements.
|
|
15
|
+
Its results establish examiner behavior; model quality and autonomous agent improvement remain unmeasured.
|
|
15
16
|
|
|
16
17
|
`orly judge` asks one bounded question about evidence you select.
|
|
17
18
|
It accompanies planning, verification, review and documentation.
|
|
@@ -452,7 +452,7 @@ table decides the paths and methods, `#[utoipa::path]` beside each handler
|
|
|
452
452
|
carries the prose and the status codes, and the `afd_wire` types carry the
|
|
453
453
|
schemas. The `public/openapi/` YAML tree this section used to describe was
|
|
454
454
|
retired with the Zig daemon; a hand-edit to the JSON is reverted by the next
|
|
455
|
-
regeneration
|
|
455
|
+
regeneration.
|
|
456
456
|
|
|
457
457
|
### Adding, renaming, or removing an endpoint
|
|
458
458
|
|
|
@@ -468,15 +468,24 @@ regeneration and fails a test before it gets that far.
|
|
|
468
468
|
--bin agentsfleetd -- --no-banner openapi > ../public/openapi.json
|
|
469
469
|
```
|
|
470
470
|
|
|
471
|
-
**Parity is mechanical, not reviewer-enforced.**
|
|
471
|
+
**Parity is mechanical, not reviewer-enforced.** Two tests grade it, and the
|
|
472
472
|
first is the one that used to be a review obligation:
|
|
473
473
|
|
|
474
474
|
| Test | What it refuses |
|
|
475
475
|
|---|---|
|
|
476
476
|
| `test_coverage_gate_rust_source` | a served route with no annotation, or an annotation for a route nobody mounts — named with its method and direction |
|
|
477
|
-
| `test_openapi_build_is_the_source` | a committed artifact that is not what the build emits |
|
|
478
477
|
| `test_documented_codes_match_refusals` | an operation that omits a refusal its guard or scope rung guarantees |
|
|
479
478
|
|
|
479
|
+
No test checks that `public/openapi.json` matches the build. Regeneration is a
|
|
480
|
+
verify step: from the repository root, run the generator, then check for drift.
|
|
481
|
+
The subshell keeps the diff command at the repository root.
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
(cd rustd && cargo run -q -p agentsfleetd --features openapi \
|
|
485
|
+
--bin agentsfleetd -- --no-banner openapi > ../public/openapi.json)
|
|
486
|
+
git diff --exit-code public/openapi.json
|
|
487
|
+
```
|
|
488
|
+
|
|
480
489
|
The prose is graded too: `scripts/check_documentation_rules.py` reads the
|
|
481
490
|
generated document, so a description that breaks the wording rules fails
|
|
482
491
|
`make lint-all`. Fix it at the annotation — or, for a schema description, at the
|
package/llms.txt
CHANGED
|
@@ -17,8 +17,14 @@ originally shipped in 0.11.0. The 0.12.1 patch avoids irrelevant staged audit
|
|
|
17
17
|
snapshots and preserves hosted-check diagnostics. The `bunx` commands below
|
|
18
18
|
fetch the published package; available versions are listed at
|
|
19
19
|
https://github.com/agentsfleet/orly/releases. Use an installed local tarball for candidate checks.
|
|
20
|
-
The
|
|
21
|
-
|
|
20
|
+
The 0.13.0 source candidate adds contributor-only offline controls and a separate explicit TypeSafe Jev measurement command.
|
|
21
|
+
Run `bun evals/judgments/comparison/run.ts --check` or `--offline` from source.
|
|
22
|
+
The separate `jev-run.ts --live` collects actual model replies; `--replay` checks saved answers with no new requests.
|
|
23
|
+
See `docs/architecture/judgment-evaluation.md` for required inputs and measured limitations.
|
|
24
|
+
Expected output is JSON with `passed: true`; deliberate false completions stay failed.
|
|
25
|
+
All five proposed checks remain evaluation-only. Do not infer model accuracy or
|
|
26
|
+
agent improvement from scripted controls. Live model runs, publication, merge
|
|
27
|
+
and live consumer upgrades need subsequent owner instruction.
|
|
22
28
|
|
|
23
29
|
---
|
|
24
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentsfleet/orly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary \u2014 for any coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import catalog from "../evals/judgments/comparison/catalog.json";
|
|
5
|
+
import { CASE_CLASS, CHECK_ERROR, SPLIT } from "../evals/judgments/comparison/types";
|
|
6
|
+
import { validateCases, validateCatalog } from "../evals/judgments/comparison/validate";
|
|
7
|
+
import { OrlyError } from "./model";
|
|
8
|
+
import { MODEL, QUESTIONS, ROLE } from "./judgments/constants";
|
|
9
|
+
import { digest } from "./judgments/files";
|
|
10
|
+
import { cleanupTemporaryDirectories, temporaryDirectory } from "./gates_test_support";
|
|
11
|
+
|
|
12
|
+
const OTHER_MODEL = "different-model";
|
|
13
|
+
const DIGEST_HEX_LENGTH = 64;
|
|
14
|
+
const WRONG_DIGEST = "0".repeat(DIGEST_HEX_LENGTH);
|
|
15
|
+
const SOURCE_PATH = "requirement.md";
|
|
16
|
+
const SOURCE_CONTENT = "Input must include a locale. The caller supplies the locale before formatting.\n";
|
|
17
|
+
|
|
18
|
+
afterEach(cleanupTemporaryDirectories);
|
|
19
|
+
|
|
20
|
+
async function caseFixture() {
|
|
21
|
+
const root = temporaryDirectory();
|
|
22
|
+
await Bun.write(join(root, SOURCE_PATH), SOURCE_CONTENT);
|
|
23
|
+
const entry = {
|
|
24
|
+
id: "locale-prerequisite", question: QUESTIONS[0], origin: "locale-source", split: SPLIT.development,
|
|
25
|
+
classification: CASE_CLASS.healthy, author: "fixture-author", requirement: "Name the required locale before formatting.",
|
|
26
|
+
evidence: [{ role: ROLE.spec, content: SOURCE_CONTENT.trim(), source: { path: SOURCE_PATH, digest: digest(SOURCE_CONTENT) } }],
|
|
27
|
+
};
|
|
28
|
+
return { root, entry };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("comparison_preserves_runtime_baseline", () => {
|
|
32
|
+
const parsed = validateCatalog(catalog);
|
|
33
|
+
expect(parsed.model).toBe(MODEL);
|
|
34
|
+
expect(parsed.baseline.map((entry) => entry.question)).toEqual([...QUESTIONS]);
|
|
35
|
+
expect(parsed.candidates.map((entry) => entry.question)).toEqual([
|
|
36
|
+
"plan.obligation_coverage", "evidence.sufficiency", "verify.production_wiring",
|
|
37
|
+
"review.failure_coverage", "review.finding_resolution",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("comparison_rejects_changed_model", () => {
|
|
42
|
+
expect(() => validateCatalog({ ...catalog, model: OTHER_MODEL })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("comparison_rejects_changed_baseline_definition", () => {
|
|
46
|
+
const changed = structuredClone(catalog);
|
|
47
|
+
for (const entry of changed.baseline) entry.definition_digest = WRONG_DIGEST;
|
|
48
|
+
expect(() => validateCatalog(changed)).toThrow(new OrlyError(CHECK_ERROR.baseline));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("comparison_rejects_missing_baseline_question", () => {
|
|
52
|
+
expect(() => validateCatalog({ ...catalog, baseline: catalog.baseline.slice(1) })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("comparison_rejects_duplicate_baseline_question", () => {
|
|
56
|
+
const first = catalog.baseline.at(0);
|
|
57
|
+
expect(first).toBeDefined();
|
|
58
|
+
expect(() => validateCatalog({ ...catalog, baseline: catalog.baseline.map(() => first) })).toThrow(new OrlyError(CHECK_ERROR.baseline));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("candidate_inputs_are_complete", () => {
|
|
62
|
+
const parsed = validateCatalog(catalog);
|
|
63
|
+
for (const candidate of parsed.candidates) {
|
|
64
|
+
expect(candidate.required_roles.length).toBeGreaterThan(0);
|
|
65
|
+
expect(Object.keys(candidate.criteria).sort()).toEqual(["ambiguous", "defective", "insufficient", "supported"]);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("comparison_rejects_missing_candidate", () => {
|
|
70
|
+
expect(() => validateCatalog({ ...catalog, candidates: catalog.candidates.slice(1) })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("comparison_rejects_duplicate_candidates", () => {
|
|
74
|
+
const first = catalog.candidates.at(0);
|
|
75
|
+
expect(first).toBeDefined();
|
|
76
|
+
expect(() => validateCatalog({ ...catalog, candidates: catalog.candidates.map(() => first) })).toThrow(new OrlyError(CHECK_ERROR.candidates));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("comparison_rejects_missing_evidence_roles", () => {
|
|
80
|
+
const candidates = catalog.candidates.map((entry) => ({ ...entry, required_roles: entry.required_roles.slice(1) }));
|
|
81
|
+
expect(() => validateCatalog({ ...catalog, candidates })).toThrow(new OrlyError(CHECK_ERROR.roles));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("comparison_rejects_empty_question_instructions", () => {
|
|
85
|
+
const candidates = catalog.candidates.map((entry) => ({ ...entry, instructions: "" }));
|
|
86
|
+
expect(() => validateCatalog({ ...catalog, candidates })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("comparison_rejects_unbounded_answer_fields", () => {
|
|
90
|
+
const candidates = catalog.candidates.map((entry) => ({ ...entry, criteria: { ...entry.criteria, run_command: "true" } }));
|
|
91
|
+
expect(() => validateCatalog({ ...catalog, candidates })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("comparison_rejects_unknown_catalog_fields", () => {
|
|
95
|
+
expect(() => validateCatalog({ ...catalog, enable_remote: true })).toThrow(new OrlyError(CHECK_ERROR.schema));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("comparison_rejects_invalid_provenance", async () => {
|
|
99
|
+
const { root, entry } = await caseFixture();
|
|
100
|
+
const checked = validateCatalog(catalog);
|
|
101
|
+
expect(await validateCases(root, checked, [entry])).toEqual([entry]);
|
|
102
|
+
const changed = { ...entry, evidence: entry.evidence.map((item) => ({ ...item, source: { ...item.source, digest: WRONG_DIGEST } })) };
|
|
103
|
+
await expect(validateCases(root, checked, [changed])).rejects.toThrow(new OrlyError(CHECK_ERROR.source));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("comparison_rejects_duplicate_cases", async () => {
|
|
107
|
+
const { root, entry } = await caseFixture();
|
|
108
|
+
await expect(validateCases(root, validateCatalog(catalog), [entry, entry])).rejects.toThrow(new OrlyError(CHECK_ERROR.duplicate));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("comparison_rejects_copied_evidence_with_changed_identity", async () => {
|
|
112
|
+
const { root, entry } = await caseFixture();
|
|
113
|
+
const copied = { ...entry, id: "copied-case", origin: "invented-origin", split: SPLIT.heldout };
|
|
114
|
+
await expect(validateCases(root, validateCatalog(catalog), [entry, copied])).rejects.toThrow(new OrlyError(CHECK_ERROR.duplicate));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("comparison_rejects_origin_crossing_splits", async () => {
|
|
118
|
+
const { root, entry } = await caseFixture();
|
|
119
|
+
const copied = { ...entry, id: "second-case", requirement: "State the locale needed by the formatter.", split: SPLIT.heldout };
|
|
120
|
+
await expect(validateCases(root, validateCatalog(catalog), [entry, copied])).rejects.toThrow(new OrlyError(CHECK_ERROR.split));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("comparison_rejects_invented_evidence_text", async () => {
|
|
124
|
+
const { root, entry } = await caseFixture();
|
|
125
|
+
const changed = { ...entry, evidence: entry.evidence.map((item) => ({ ...item, content: "Unrelated invented source." })) };
|
|
126
|
+
await expect(validateCases(root, validateCatalog(catalog), [changed])).rejects.toThrow(new OrlyError(CHECK_ERROR.source));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("comparison_rejects_incomplete_case_roles", async () => {
|
|
130
|
+
const { root, entry } = await caseFixture();
|
|
131
|
+
const changed = { ...entry, evidence: entry.evidence.map((item) => ({ ...item, role: ROLE.context })) };
|
|
132
|
+
await expect(validateCases(root, validateCatalog(catalog), [changed])).rejects.toThrow(new OrlyError(CHECK_ERROR.evidence));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("comparison_rejects_empty_case_corpus", async () => {
|
|
136
|
+
const { root } = await caseFixture();
|
|
137
|
+
await expect(validateCases(root, validateCatalog(catalog), [])).rejects.toThrow(new OrlyError(CHECK_ERROR.cases));
|
|
138
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { loadCases } from "../evals/judgments/comparison/corpus";
|
|
5
|
+
import { independentlyExpected } from "../evals/judgments/comparison/hidden/label-oracle";
|
|
6
|
+
import { comparisonCommand } from "../evals/judgments/comparison/run";
|
|
7
|
+
import { collectTasks } from "../evals/judgments/comparison/report";
|
|
8
|
+
import { loadTasks } from "../evals/judgments/comparison/tasks";
|
|
9
|
+
import { CASE_CLASS, CANDIDATE, CLASSIFICATION, TASK_CONTROL, TASK_STATE } from "../evals/judgments/comparison/types";
|
|
10
|
+
import { QUESTIONS } from "./judgments/constants";
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
13
|
+
const OFFLINE = "--offline";
|
|
14
|
+
const INCOMPLETE = "incomplete";
|
|
15
|
+
const FIXTURE_FAILURE = "injected unavailable execution";
|
|
16
|
+
const TEST_TIMEOUT = 120_000;
|
|
17
|
+
|
|
18
|
+
test("hidden_assertions_reject_false_completion_and_preserve_honest_blocks", async () => {
|
|
19
|
+
const result = await comparisonCommand([OFFLINE], ROOT);
|
|
20
|
+
expect(result.exit).toBe(0);
|
|
21
|
+
expect(result.report.controls).toHaveLength(12);
|
|
22
|
+
for (const control of result.report.controls) {
|
|
23
|
+
expect(control.state).not.toBe(INCOMPLETE);
|
|
24
|
+
if (control.state === INCOMPLETE) throw new Error(control.error);
|
|
25
|
+
expect(control.linked.result.exit_code).toBe(0);
|
|
26
|
+
expect(control.cleanup).toEqual({ workspace_removed: true, receipts_removed: true });
|
|
27
|
+
if (control.control === TASK_CONTROL.falseCompletion) {
|
|
28
|
+
expect(control.state).toBe(TASK_STATE.failed);
|
|
29
|
+
expect(control.independent.checks.some((check) => !check.passed)).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
expect(result.report.controls.filter((entry) => entry.state === TASK_STATE.completed)).toHaveLength(5);
|
|
33
|
+
expect(result.report.controls.filter((entry) => entry.state === TASK_STATE.blocked)).toHaveLength(1);
|
|
34
|
+
expect(result.report.observations.every((entry) => entry.answer === null)).toBe(true);
|
|
35
|
+
expect(result.report.adoption.every((entry) => !entry.eligible)).toBe(true);
|
|
36
|
+
expect(result.report.live_requests).toBe(0);
|
|
37
|
+
expect(result.report.autonomous_completion).toBeNull();
|
|
38
|
+
const repeated = await comparisonCommand([OFFLINE], ROOT);
|
|
39
|
+
expect(repeated.report.reproducible).toEqual(result.report.reproducible);
|
|
40
|
+
}, TEST_TIMEOUT);
|
|
41
|
+
|
|
42
|
+
test("missing_task_attempts_are_retained_without_preventing_later_attempts", async () => {
|
|
43
|
+
let called = 0;
|
|
44
|
+
const tasks = await loadTasks(ROOT);
|
|
45
|
+
const attempts = await collectTasks(ROOT, tasks, async () => { called++; throw new Error(FIXTURE_FAILURE); });
|
|
46
|
+
expect(called).toBe(12);
|
|
47
|
+
expect(attempts).toHaveLength(12);
|
|
48
|
+
expect(attempts.every((entry) => entry.state === INCOMPLETE && entry.completion_credit === 0)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("question_oracles_use_role_evidence_and_never_the_stated_classification", async () => {
|
|
52
|
+
const cases = await loadCases(ROOT);
|
|
53
|
+
for (const question of [...QUESTIONS, ...Object.values(CANDIDATE)]) {
|
|
54
|
+
const healthy = cases.find((entry) => entry.question === question && entry.classification === CASE_CLASS.healthy);
|
|
55
|
+
const defective = cases.find((entry) => entry.question === question && entry.classification === CASE_CLASS.defective);
|
|
56
|
+
if (!healthy || !defective) throw new Error("Missing paired question evidence.");
|
|
57
|
+
expect(independentlyExpected({ ...healthy, classification: CASE_CLASS.defective }).classification).toBe(CASE_CLASS.healthy);
|
|
58
|
+
expect(independentlyExpected({ ...healthy, evidence: defective.evidence }).classification).toBe(CASE_CLASS.defective);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("exact_assertion_can_correctly_reject_wrong_application_output", async () => {
|
|
63
|
+
const entry = (await loadCases(ROOT)).find((entry) => entry.question === QUESTIONS[2] && entry.classification === CASE_CLASS.healthy);
|
|
64
|
+
if (!entry) throw new Error("Missing assertion fixture.");
|
|
65
|
+
const changed = { ...entry, evidence: entry.evidence.map((item) => ({ ...item,
|
|
66
|
+
content: JSON.stringify({ ...JSON.parse(item.content), observed: "wrong output" }) })) };
|
|
67
|
+
expect(independentlyExpected(changed).expected_native).toBe("exact");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("missing_source_and_missing_dependency_inventory_have_different_answers", async () => {
|
|
71
|
+
const cases = await loadCases(ROOT);
|
|
72
|
+
const gap = cases.find((entry) => entry.question === CANDIDATE.evidence && entry.classification === CASE_CLASS.defective);
|
|
73
|
+
if (!gap) throw new Error("Missing dependency-gap fixture.");
|
|
74
|
+
expect(independentlyExpected(gap).expected_native).toBe(CLASSIFICATION.defective);
|
|
75
|
+
const noInventory = { ...gap, evidence: gap.evidence.map((item) => item.role === "dependencies" ? { ...item, content: "{}" } : item) };
|
|
76
|
+
expect(independentlyExpected(noInventory).expected_native).toBe(CLASSIFICATION.insufficient);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("contributor_command_refuses_live_or_unknown_arguments", async () => {
|
|
80
|
+
await expect(comparisonCommand(["--live"], ROOT)).rejects.toThrow("Use exactly --check or --offline");
|
|
81
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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, evaluateJev, JEV_PREFLIGHT_ERROR, MAX_JEV_REQUESTS, type JevAttempt, type JevExample } from "../evals/judgments/comparison/jev";
|
|
6
|
+
import { CANDIDATE, labelsSchema } from "../evals/judgments/comparison/types";
|
|
7
|
+
import { validateCatalog } from "../evals/judgments/comparison/validate";
|
|
8
|
+
import { ANSWER_KIND, MAX_REQUEST_BYTES, MAX_STATE_BYTES, MODEL, REPLY_KEY } from "./judgments/constants";
|
|
9
|
+
import type { Reply } from "./judgments/wire";
|
|
10
|
+
import { OrlyError } from "./model";
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(import.meta.dir, "..");
|
|
13
|
+
const CREDENTIAL = "test-credential";
|
|
14
|
+
const PRIVATE_DETAIL = "private-provider-detail";
|
|
15
|
+
const FAILURE = "retention write failed";
|
|
16
|
+
const SCAN = "scan:";
|
|
17
|
+
const REQUEST = "request:";
|
|
18
|
+
|
|
19
|
+
async function examples(): Promise<JevExample[]> {
|
|
20
|
+
const catalog = validateCatalog(await Bun.file(resolve(ROOT, "evals/judgments/comparison/catalog.json")).json());
|
|
21
|
+
const cases = await loadCases(ROOT);
|
|
22
|
+
const labels = labelsSchema.parse(await Bun.file(resolve(ROOT, "evals/judgments/comparison/labels.json")).json());
|
|
23
|
+
const all = corpusJevExamples(catalog, cases, labels);
|
|
24
|
+
return [all.find((entry) => entry.question === "document.claim"), all.find((entry) => entry.question === "verify.assertion"),
|
|
25
|
+
all.find((entry) => entry.question === CANDIDATE.wiring)].filter((entry) => entry !== undefined);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function response(entry: JevExample): Reply {
|
|
29
|
+
const question = entry.input.definition.question;
|
|
30
|
+
const answer = question.type === ANSWER_KIND.noul ? { type: question.type, noul: 1 } : {
|
|
31
|
+
type: question.type, choice: entry.expected, confidence: 1,
|
|
32
|
+
probabilities: Object.fromEntries(Object.keys(question.criteria).map((key) => [key, key === entry.expected ? 1 : 0])),
|
|
33
|
+
};
|
|
34
|
+
return { model: MODEL, answers: { [REPLY_KEY]: answer }, usage: { input_tokens: 100, output_tokens: 20 } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("jev_requests_preserve_actual_native_answers_and_failures", async () => {
|
|
38
|
+
const selected = await examples();
|
|
39
|
+
const order: string[] = [];
|
|
40
|
+
const retained: JevAttempt[] = [];
|
|
41
|
+
let ordinal = 0;
|
|
42
|
+
using server = Bun.serve({ port: 0, async fetch(request) {
|
|
43
|
+
const entry = selected[ordinal++];
|
|
44
|
+
if (!entry) throw new Error("Unexpected request.");
|
|
45
|
+
order.push(REQUEST + entry.id);
|
|
46
|
+
expect(await request.text()).toBe(entry.input.request);
|
|
47
|
+
expect(request.headers.get("authorization")).toBe(`Bearer ${CREDENTIAL}`);
|
|
48
|
+
expect(retained.at(-1)).toMatchObject({ id: entry.id, requests: 1, reply: null });
|
|
49
|
+
return ordinal === 2 ? new Response(PRIVATE_DETAIL, { status: 429 }) : Response.json(response(entry));
|
|
50
|
+
} });
|
|
51
|
+
const actual = await evaluateJev(selected, CREDENTIAL, {
|
|
52
|
+
scan: async (input) => { order.push(SCAN + selected.find((entry) => entry.input.request === input)?.id); },
|
|
53
|
+
fetch: (request) => fetch(new Request(server.url, request)),
|
|
54
|
+
}, async (attempt) => { retained.push(attempt); });
|
|
55
|
+
expect(ordinal).toBe(3);
|
|
56
|
+
expect(order.slice(0, 3)).toEqual(selected.map((entry) => SCAN + entry.id));
|
|
57
|
+
expect(actual.map((entry) => entry.requests)).toEqual([1, 1, 1]);
|
|
58
|
+
expect(actual.map((entry) => entry.reply !== null)).toEqual([true, false, true]);
|
|
59
|
+
expect(actual[1]?.failure).toContain("HTTP 429; no automatic retry");
|
|
60
|
+
expect(JSON.stringify(actual)).not.toContain(PRIVATE_DETAIL);
|
|
61
|
+
expect(JSON.stringify(actual)).not.toContain(CREDENTIAL);
|
|
62
|
+
expect(retained).toHaveLength(6);
|
|
63
|
+
const first = selected[0];
|
|
64
|
+
if (!first) throw new Error("Missing input.");
|
|
65
|
+
expect(actual[0]?.reply).toEqual(response(first));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("jev_missing_credentials_and_each_scanner_failure_prevent_every_upload", async () => {
|
|
69
|
+
const selected = await examples();
|
|
70
|
+
for (const failAt of [0, 1, 2, 3]) {
|
|
71
|
+
let scans = 0;
|
|
72
|
+
let uploads = 0;
|
|
73
|
+
const attempts = await evaluateJev(selected, failAt === 0 ? undefined : CREDENTIAL, {
|
|
74
|
+
scan: async () => { if (++scans === failAt) throw new OrlyError("Secret scanner failed; nothing was uploaded."); },
|
|
75
|
+
fetch: async () => { uploads++; throw new Error(PRIVATE_DETAIL); },
|
|
76
|
+
});
|
|
77
|
+
expect(scans).toBe(failAt);
|
|
78
|
+
expect(uploads).toBe(0);
|
|
79
|
+
expect(attempts).toHaveLength(3);
|
|
80
|
+
expect(attempts.every((entry) => entry.requests === 0 && entry.reply === null && entry.failure !== null)).toBe(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("jev_invalid_schedule_and_forged_upload_refuse_before_scanning", async () => {
|
|
85
|
+
const selected = await examples();
|
|
86
|
+
const entry = selected[0];
|
|
87
|
+
if (!entry) throw new Error("Missing input.");
|
|
88
|
+
let touched = 0;
|
|
89
|
+
const dependencies = { scan: async () => { touched++; }, fetch: async () => { touched++; return Response.json({}); } };
|
|
90
|
+
const badInputs = ["{", "x".repeat(MAX_REQUEST_BYTES + 1), JSON.stringify({ ...JSON.parse(entry.input.request), model: "unpinned" }),
|
|
91
|
+
JSON.stringify({ ...JSON.parse(entry.input.request), state: { requirement: "bounded", evidence: [{ role: "spec", text: "x".repeat(MAX_STATE_BYTES + 1) }] } })];
|
|
92
|
+
const invalid = [[], [entry, entry], Array.from({ length: MAX_JEV_REQUESTS + 1 }, (_, id) => ({ ...entry, id: String(id) })),
|
|
93
|
+
...badInputs.map((request) => [{ ...entry, input: { ...entry.input, request } }])];
|
|
94
|
+
for (const schedule of invalid) await expect(evaluateJev(schedule, CREDENTIAL, dependencies)).rejects.toThrow(JEV_PREFLIGHT_ERROR);
|
|
95
|
+
expect(touched).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("jev_malformed_native_reply_and_transport_failure_remain_missing_and_continue", async () => {
|
|
99
|
+
const selected = await examples();
|
|
100
|
+
let ordinal = 0;
|
|
101
|
+
const attempts = await evaluateJev(selected, CREDENTIAL, { scan: async () => {}, fetch: async () => {
|
|
102
|
+
ordinal++;
|
|
103
|
+
if (ordinal === 1) return Response.json({ model: "wrong-model", answers: {}, usage: {} });
|
|
104
|
+
if (ordinal === 2) throw new Error(CREDENTIAL + PRIVATE_DETAIL);
|
|
105
|
+
return new Response("{");
|
|
106
|
+
} });
|
|
107
|
+
expect(ordinal).toBe(3);
|
|
108
|
+
expect(attempts.every((entry) => entry.reply === null && entry.requests === 1 && entry.failure !== null)).toBe(true);
|
|
109
|
+
expect(attempts[0]?.failure).toContain("pinned model and answer schema");
|
|
110
|
+
expect(attempts[1]?.failure).toContain("Provider transport failed");
|
|
111
|
+
expect(attempts[2]?.failure).toContain("not valid JSON");
|
|
112
|
+
expect(JSON.stringify(attempts)).not.toContain(CREDENTIAL);
|
|
113
|
+
expect(JSON.stringify(attempts)).not.toContain(PRIVATE_DETAIL);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("jev_retention_failure_at_each_ordered_write_prevents_the_next_upload", async () => {
|
|
117
|
+
const selected = await examples();
|
|
118
|
+
for (const failAt of [1, 2, 3, 4, 5, 6]) {
|
|
119
|
+
let writes = 0;
|
|
120
|
+
let uploads = 0;
|
|
121
|
+
const durable: JevAttempt[] = [];
|
|
122
|
+
await expect(evaluateJev(selected, CREDENTIAL, { scan: async () => {}, fetch: async () => {
|
|
123
|
+
const entry = selected[uploads++];
|
|
124
|
+
if (!entry) throw new Error("Unexpected upload.");
|
|
125
|
+
return Response.json(response(entry));
|
|
126
|
+
} }, async (attempt) => {
|
|
127
|
+
if (++writes === failAt) throw new Error(FAILURE);
|
|
128
|
+
durable.push(attempt);
|
|
129
|
+
})).rejects.toThrow(FAILURE);
|
|
130
|
+
expect(uploads).toBe(Math.floor(failAt / 2));
|
|
131
|
+
expect(durable).toHaveLength(failAt - 1);
|
|
132
|
+
if (failAt % 2 === 0) expect(durable.at(-1)).toMatchObject({ requests: 1, reply: null });
|
|
133
|
+
}
|
|
134
|
+
});
|