@agentsfleet/orly 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,22 +42,30 @@ Works with Claude Code, Codex, OpenCode, and Amp.
42
42
 
43
43
  ---
44
44
 
45
- ## 0.12.0: migration verification
46
-
47
- 0.12.0 is a local release candidate. Published 0.11.0 already provides the
48
- `.orly/` layout and the six existing `orly judge` questions.
49
- This release focuses on package verification, migration evidence and clearer setup guidance; it adds no new runtime judgment question.
50
-
51
- The `agentsfleet` trial upgraded a dedicated worktree from 0.10.14 using the packed candidate.
52
- Owner files, hooks and verification commands survived; a second update wrote nothing.
53
- See the [release report](evals/release/release-report.md) for exact proof and remaining release checks.
54
-
55
- Jev, TypeSafe's bounded judgment model, supplies advice only when requested.
56
- Existing deterministic checks retain control of gate results.
57
- The comparison evaluator is parked for proposed 0.13 work; model accuracy and candidate benefits remain unmeasured.
58
- See the [judgment reference](docs/JUDGMENTS.md) for evidence selection, offline replay and explicit source upload.
59
-
60
- The install commands below fetch the published package. They do not select this unpublished candidate.
45
+ ## 0.13: measured judgment comparison
46
+
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
+
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.
55
+
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.
66
+
67
+ See [releases](https://github.com/agentsfleet/orly/releases) for published versions.
68
+ The install commands below fetch the published package; 0.13.0 publication is a separate owner action.
61
69
 
62
70
  ---
63
71
 
@@ -48,7 +48,7 @@ set -euo pipefail
48
48
 
49
49
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
50
50
  audit_scope_init --all "$@"
51
- audit_index_snapshot "$@"
51
+ audit_index_snapshot '*.zig'
52
52
 
53
53
  MODE="${1:-${SCOPE:-all}}"
54
54
  ROOT="$(git rev-parse --show-toplevel)"
@@ -39,7 +39,7 @@ set -euo pipefail
39
39
 
40
40
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
41
41
  audit_scope_init --all "$@"
42
- audit_index_snapshot "$@"
42
+ audit_index_snapshot '*.tsx' '*.jsx' 'ui/packages/app/*' 'ui/packages/website/*' 'ui/packages/design-system/*'
43
43
 
44
44
  MODE="$AUDIT_MODE"
45
45
  ROOT="$(git rev-parse --show-toplevel)"
@@ -51,7 +51,7 @@ set -euo pipefail
51
51
 
52
52
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
53
53
  audit_scope_init --all "$@"
54
- audit_index_snapshot "$@"
54
+ audit_index_snapshot
55
55
 
56
56
  MODE="$AUDIT_MODE"
57
57
  ROOT="$(git rev-parse --show-toplevel)"
package/audits/logging.sh CHANGED
@@ -43,7 +43,7 @@ set -euo pipefail
43
43
 
44
44
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
45
45
  audit_scope_init --all "$@"
46
- audit_index_snapshot "$@"
46
+ audit_index_snapshot '*.zig' '*.rs' 'agentsfleet/src/*.js' 'agentsfleet/src/*.jsx' 'agentsfleet/src/*.ts' 'agentsfleet/src/*.tsx'
47
47
 
48
48
  MODE="--all"
49
49
  STRICT=0
@@ -44,7 +44,7 @@ set -euo pipefail
44
44
 
45
45
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
46
46
  audit_scope_init --all "$@"
47
- audit_index_snapshot "$@"
47
+ audit_index_snapshot '*.rs'
48
48
 
49
49
  MODE_STAGED="--staged"
50
50
  MODE_ALL="--all"
package/audits/scope.sh CHANGED
@@ -37,7 +37,7 @@ audit_scope_init() {
37
37
  # the index copy and checked-out files belong to this invocation alone.
38
38
  audit_index_snapshot() {
39
39
  [ "$AUDIT_MODE" = --staged ] || return 0
40
- local gitdir index
40
+ local gitdir index changed=0
41
41
  AUDIT_SNAPSHOT_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/orly-index.XXXXXX")" || return 1
42
42
  trap 'rm -rf -- "$AUDIT_SNAPSHOT_ROOT"' EXIT
43
43
  trap 'exit 130' INT
@@ -46,6 +46,14 @@ audit_index_snapshot() {
46
46
  index="$(git rev-parse --git-path index)" || exit 1
47
47
  cp "$index" "$AUDIT_SNAPSHOT_ROOT/index" || exit 1
48
48
  export GIT_DIR="$gitdir" GIT_INDEX_FILE="$AUDIT_SNAPSHOT_ROOT/index" GIT_OPTIONAL_LOCKS=0
49
+ if [ "$#" -gt 0 ]; then
50
+ git diff --cached --quiet --diff-filter=ACMRT -- "$@" || changed=$?
51
+ case "$changed" in
52
+ 0) return 0 ;;
53
+ 1) ;;
54
+ *) printf 'cannot select staged audit files (git exit %s)\n' "$changed" >&2; exit 2 ;;
55
+ esac
56
+ fi
49
57
  export GIT_WORK_TREE="$AUDIT_SNAPSHOT_ROOT/tree"
50
58
  mkdir "$GIT_WORK_TREE" || exit 1
51
59
  git checkout-index --all --prefix="$GIT_WORK_TREE/" || exit 1
package/audits/sql-mod.sh CHANGED
@@ -25,7 +25,7 @@ set -euo pipefail
25
25
 
26
26
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
27
27
  audit_scope_init --all "$@"
28
- audit_index_snapshot "$@"
28
+ audit_index_snapshot '*.zig'
29
29
 
30
30
  MODE="${1:-${SCOPE:---all}}"
31
31
  ROOT="$(git rev-parse --show-toplevel)"
package/audits/ufs.sh CHANGED
@@ -55,7 +55,7 @@ set -euo pipefail
55
55
 
56
56
  source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/scope.sh"
57
57
  audit_scope_init --all "$@"
58
- audit_index_snapshot "$@"
58
+ audit_index_snapshot '*.zig' '*.ts' '*.tsx' '*.js' '*.jsx' '*.rs' '*.go'
59
59
 
60
60
  # Default: full-codebase scan (`--all` is an explicit alias). `--staged` is the
61
61
  # pre-commit lens — it narrows the per-file checks to `git diff --cached`. The
package/dispatch/lib.sh CHANGED
@@ -125,7 +125,7 @@ dispatch_managed_paths() {
125
125
  dispatch_resolve_files() {
126
126
  DISPATCH_FILES=()
127
127
  audit_scope_init --all "$@"
128
- audit_index_snapshot "$@"
128
+ audit_index_snapshot
129
129
  DISPATCH_STAGED=0
130
130
  if [ "${1:-}" = "--staged" ]; then
131
131
  DISPATCH_STAGED=1
package/docs/CHANGELOG.md CHANGED
@@ -1,6 +1,26 @@
1
1
  # Changelog
2
2
 
3
- ## 0.12.0 — unreleased
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
+
15
+ ## 0.12.1
16
+
17
+ Staged audits avoid copying repository files when their filename scope is empty.
18
+
19
+ - **Audit preflight** — selects filenames from a copied index before materialising repository files; relevant audits retain coherent staged reads.
20
+ - **Check diagnostics** — prints hosted job or pipeline states and links before reporting pending or failed checks.
21
+ - **Release status** — corrects documentation that still described published 0.12.0 as an unpublished candidate.
22
+
23
+ ## 0.12.0 — 2026-10-07
4
24
 
5
25
  Setup guidance now distinguishes the published package from the local release candidate.
6
26
 
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.12.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.12.0 candidate retains the six questions shipped in 0.11.0.
14
- The comparison evaluator is parked for proposed 0.13 work; this release makes no new model-quality claim.
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.
@@ -84,6 +84,13 @@ Every criterion is mechanical — it reads an exit code or a file. Claims that
84
84
  cannot be proven that way stay prose and are graded by the spec's rubric; they
85
85
  never become fake criteria.
86
86
 
87
+ Filename-scoped staged audits copy Git's index before selecting relevant paths.
88
+ An empty selection skips the repository checkout; matching paths still trigger
89
+ a private checkout, dependency reads and indexed-link checks. Each audit owns
90
+ its temporary files and cleanup. Audits with unconditional registry checks
91
+ retain a complete checkout. Separate relevant audits retain separate snapshots;
92
+ this optimization does not introduce a shared snapshot cache.
93
+
87
94
  Four behaviours worth knowing:
88
95
 
89
96
  - **No spec, no problem — but closing is not escaping.** Spec criteria skip
@@ -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 and fails a test before it gets that far.
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.** Three tests grade it, and the
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
@@ -12,11 +12,19 @@ Package: `@agentsfleet/orly` (npm)
12
12
  Source: https://github.com/agentsfleet/orly
13
13
  License: Massachusetts Institute of Technology (MIT)
14
14
 
15
- This checkout prepares the unpublished 0.12.0 candidate. Published 0.11.0 already
16
- contains the same runtime commands and `.orly/` layout. The `bunx` commands below
17
- fetch the published package. Use the installed local tarball for candidate checks.
18
- The comparison evaluator is parked for proposed 0.13 work; do not claim measured
19
- judgment improvement or enable new questions from its unfinished results.
15
+ 0.12.0 shipped migration verification; the runtime commands and `.orly/` layout
16
+ originally shipped in 0.11.0. The 0.12.1 patch avoids irrelevant staged audit
17
+ snapshots and preserves hosted-check diagnostics. The `bunx` commands below
18
+ fetch the published package; available versions are listed at
19
+ https://github.com/agentsfleet/orly/releases. Use an installed local tarball for candidate checks.
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.
20
28
 
21
29
  ---
22
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.12.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",
@@ -93,8 +93,17 @@ Each cycle, after the review-thread walk, poll CI and act by cause:
93
93
 
94
94
  ```bash
95
95
  set -euo pipefail
96
- CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state,bucket,link) \
97
- || { echo "BABYSIT: check fetch incomplete; reset quiet polls" >&2; exit 1; }
96
+ CHECK_STATUS=0
97
+ CHECKS=$(gh pr checks "$PR_NUMBER" --json name,state,bucket,link) || CHECK_STATUS=$?
98
+ printf '%s' "$CHECKS" | jq -e 'type == "array" and length > 0 and all(.[];
99
+ (.name | type == "string") and (.state | type == "string") and
100
+ (.link | type == "string") and (.bucket | IN("pass", "fail", "pending", "skipping", "cancel")))' >/dev/null \
101
+ || { printf 'BABYSIT: invalid check response (exit %s); reset quiet polls\n%s\n' "$CHECK_STATUS" "$CHECKS" >&2; exit 1; }
102
+ printf '%s' "$CHECKS" | jq -r '.[] | "\(.name) | \(.state) | \(.bucket) | \(.link)"'
103
+ case "$CHECK_STATUS" in
104
+ 0|1|8) ;; # GitHub reports failed and pending checks with nonzero statuses.
105
+ *) printf 'BABYSIT: check fetch incomplete (exit %s); reset quiet polls\n' "$CHECK_STATUS" >&2; exit 1 ;;
106
+ esac
98
107
  printf '%s' "$CHECKS" | jq -e 'type == "array" and length > 0 and all(.[]; .bucket == "pass" or .bucket == "skipping")' >/dev/null \
99
108
  || { echo "BABYSIT: checks empty or not green; reset quiet polls" >&2; exit 1; }
100
109
  CHECK_HEAD=$(gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid')
@@ -107,8 +116,14 @@ echo "BABYSIT: ci=green revision=$CHECK_HEAD"
107
116
 
108
117
  ```bash
109
118
  set -euo pipefail
110
- PIPELINES=$(glab api "projects/$PROJ_ENC/merge_requests/$MR_IID/pipelines" --paginate | jq -sc 'add') \
111
- || { echo "BABYSIT: pipeline fetch incomplete; reset quiet polls" >&2; exit 1; }
119
+ PIPELINE_STATUS=0
120
+ PIPELINES=$(glab api "projects/$PROJ_ENC/merge_requests/$MR_IID/pipelines" --paginate | jq -sc 'add') || PIPELINE_STATUS=$?
121
+ printf '%s' "$PIPELINES" | jq -e 'type == "array" and length > 0 and all(.[];
122
+ (.id | type == "number") and (.sha | type == "string") and (.status | type == "string"))' >/dev/null \
123
+ || { printf 'BABYSIT: invalid pipeline response (exit %s); reset quiet polls\n%s\n' "$PIPELINE_STATUS" "$PIPELINES" >&2; exit 1; }
124
+ printf '%s' "$PIPELINES" | jq -r '.[] | "pipeline \(.id) | \(.status) | \(.web_url // "URL not reported")"'
125
+ [ "$PIPELINE_STATUS" -eq 0 ] \
126
+ || { printf 'BABYSIT: pipeline fetch incomplete (exit %s); reset quiet polls\n' "$PIPELINE_STATUS" >&2; exit 1; }
112
127
  printf '%s' "$PIPELINES" | jq -e --arg revision "$POLL_HEAD" \
113
128
  '[.[] | select(.sha == $revision)] | sort_by(.id) | last | .status == "success"' >/dev/null \
114
129
  || { echo "BABYSIT: current revision has no successful pipeline; reset quiet polls" >&2; exit 1; }
@@ -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
+ });