mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(repairs re-review HIGH-1): preflight exempts behavior cases from production-ref resolution
The restack onto slice-05 Q3 added the `behavior` EvalCategory to the shared live-model harness. Behavior cases assert observable behavior and carry NO `rig context get <ref>` contract, but the Repair-2 preflight ran resolveCaseRefs on EVERY case and refused the whole run (run-evals.mjs exit 2) on any unresolved one — so a conforming behavior case could never travel through the shipped eval entry. Latent at 35f7d8dea (pre-slice-05, no behavior cases); activated by the restack, exactly the base interaction the re-review checked. Fix: resolveCaseRefs stamps requiresRef (true only for selection/loading — the categories whose contract pulls context); unresolvedCases enforces only on requiresRef cases. Behavior cases are never refused. The hermetic guard applies canonicality only where the contract requires it. RED evidence: mixed selection+behavior set failed (behavior-observable in unresolvedCases, requiresRef undefined) → 2 failing; GREEN after fix, 46/46 eval+router+ref+behavior-bridge. Pin at the actual entry/preflight seam (run-evals.mjs:55 composition), not a direct runEvals unit: a mixed selection+behavior invocation proves the entry can never refuse a valid behavior case while genuinely-broken selection cases still refuse. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
2a756f9ab1
commit
a8cd1cf249
3 files changed
+34
-7
No files matched your search
@@ -23,9 +23,11 @@ describe("REPAIR (re-review HIGH-1) — refs resolve in a freshly-built producti
|
||||
afterAll(built.cleanup);
|
||||
const resolutions = resolveCaseRefs(cases, built.dir);
|
||||
|
||||
it("yields a canonical ref for EVERY case (per-case, not a suite-wide count)", () => {
|
||||
it("yields a canonical ref for EVERY ref-bearing case (per-case, not a suite-wide count)", () => {
|
||||
expect(resolutions).toHaveLength(cases.length);
|
||||
const bad = resolutions.filter((r) => r.ref === null || !r.canonical).map((r) => r.caseId);
|
||||
// Only selection/loading cases contract to pull context; a behavior case (slice-05 Q3) carries
|
||||
// no ref and is exempt — enforce canonicality exactly where the contract requires it.
|
||||
const bad = resolutions.filter((r) => r.requiresRef && (r.ref === null || !r.canonical)).map((r) => r.caseId);
|
||||
expect(bad).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ const CASES: EvalCase[] = [
|
||||
{ id: "bad-absent", name: "x", category: "selection", prompt: "p", expectedPatterns: ["rig context get\\s+skills/core/absent"] },
|
||||
{ id: "bad-bare", name: "x", category: "selection", prompt: "p", expectedPatterns: ["rig context get\\s+core/known"] },
|
||||
{ id: "bad-noref", name: "x", category: "selection", prompt: "p", expectedPatterns: ["do the thing"] },
|
||||
// slice-05 Q3 behavior case — asserts observable behavior, NO context-pull contract, so it must
|
||||
// never be refused by the production-ref preflight (re-review restack HIGH-1).
|
||||
{ id: "behavior-observable", name: "x", category: "behavior", prompt: "p", expectedPatterns: ["rig context (get|profile)"] },
|
||||
];
|
||||
|
||||
describe("eval-ref-resolution — per-case, by name", () => {
|
||||
@@ -35,11 +38,23 @@ describe("eval-ref-resolution — per-case, by name", () => {
|
||||
expect(res.find((r) => r.caseId === "good")?.resolved).toBe(true);
|
||||
});
|
||||
|
||||
it("flags absent / bare (non-canonical) / no-ref cases — each by caseId", () => {
|
||||
it("flags only the SELECTION/LOADING cases (absent / bare / no-ref) — never the behavior case", () => {
|
||||
// The behavior case has no context-pull contract, so it is NOT among the refused cases.
|
||||
expect(unresolvedCases(res).map((r) => r.caseId).sort()).toEqual(["bad-absent", "bad-bare", "bad-noref"]);
|
||||
expect(res.find((r) => r.caseId === "bad-noref")?.ref).toBeNull();
|
||||
expect(res.find((r) => r.caseId === "bad-bare")?.canonical).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT refuse a behavior case — it has no context-pull contract (re-review restack HIGH-1)", () => {
|
||||
const behavior = res.find((r) => r.caseId === "behavior-observable");
|
||||
expect(behavior?.requiresRef).toBe(false);
|
||||
// run-evals.mjs refuses the whole run iff `unresolvedCases(resolveCaseRefs(...))` is non-empty
|
||||
// (the entry's exit-2 preflight). Pinning that composition on a MIXED selection+behavior set proves
|
||||
// the entry can never refuse a valid behavior case, while genuinely-broken selection cases still do.
|
||||
const refused = unresolvedCases(res);
|
||||
expect(refused.map((r) => r.caseId)).not.toContain("behavior-observable");
|
||||
expect(refused.every((r) => r.requiresRef)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildProductionPackage — cleans up its temp package (re-review MEDIUM-2)", () => {
|
||||
|
||||
@@ -10,10 +10,13 @@ import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { EvalCase } from "./eval-grader.js";
|
||||
import type { EvalCase, EvalCategory } from "./eval-grader.js";
|
||||
|
||||
export interface CaseRefResolution {
|
||||
caseId: string;
|
||||
category: EvalCategory;
|
||||
/** Only selection/loading cases have a context-pull contract; behavior cases do not. */
|
||||
requiresRef: boolean;
|
||||
/** The canonical ref extracted from the case, or null if none was found. */
|
||||
ref: string | null;
|
||||
/** ref is a canonical full path (skills/<ns>/<name>). */
|
||||
@@ -31,18 +34,25 @@ export function extractRef(pattern: string): string | null {
|
||||
/** One resolution per case (never fewer), against a built production package directory. */
|
||||
export function resolveCaseRefs(cases: EvalCase[], productionPackageDir: string): CaseRefResolution[] {
|
||||
return cases.map((c) => {
|
||||
// Only selection/loading cases contract to pull context; behavior cases (slice-05 Q3) assert
|
||||
// observable behavior and carry no context ref, so they are never subject to ref resolution.
|
||||
const requiresRef = c.category === "selection" || c.category === "loading";
|
||||
const patterns = [...(c.expectedPatterns ?? [])];
|
||||
if (c.order?.getPattern) patterns.push(c.order.getPattern);
|
||||
const ref = patterns.map(extractRef).find((r) => r !== null) ?? null;
|
||||
const canonical = ref !== null && ref.startsWith("skills/");
|
||||
const resolved = canonical && existsSync(join(productionPackageDir, ref, "manifest.yaml"));
|
||||
return { caseId: c.id, ref, canonical, resolved };
|
||||
return { caseId: c.id, category: c.category, requiresRef, ref, canonical, resolved };
|
||||
});
|
||||
}
|
||||
|
||||
/** Cases whose ref is missing, non-canonical, or absent from the production package. */
|
||||
/**
|
||||
* Cases that MUST resolve a canonical production ref but do not — i.e. selection/loading cases whose
|
||||
* ref is missing, non-canonical, or absent from the production package. Behavior cases carry no
|
||||
* context-pull contract and are never included, so a valid behavior case never refuses the run.
|
||||
*/
|
||||
export function unresolvedCases(resolutions: CaseRefResolution[]): CaseRefResolution[] {
|
||||
return resolutions.filter((r) => !r.resolved);
|
||||
return resolutions.filter((r) => r.requiresRef && !r.resolved);
|
||||
}
|
||||
|
||||
export interface BuiltProductionPackage {
|
||||
|
||||
Reference in new issue
Block a user