feat(ai-judge): add advisory model catalog and strict corpus replay

- add versioned advisory model catalog shipped with the package and a fail-closed loader
- annotate the enforce session notice for untested, deprecated, and revoked models
- add --strict to corpus-replay with 0/1/2 exit codes and reject strict subset runs
- extract replay qualification into a pure module that recomputes matches and validates latencies
- remove the documented-but-unimplemented --thinking flag and stamp reports with a corpus version
- qualify gpt-5.6-sol as the first recommended entry and archive three real replay reports
- revise the corpus to 2026-08-21.2 changing unclear-forward expected defer to deny
This commit is contained in:
2026-08-21 23:11:40 +08:00
parent 061c60c344
commit 7987fc7a3c
13 changed files with 1544 additions and 39 deletions
@@ -32,6 +32,25 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
getAgentDir: () => mockAgentDir.dir || "/nonexistent-ai-judge-test",
};
});
// Catalog seam (PIEXTENSIO-24): index.ts reads the advisory catalog once
// per session; tests inject entries through this hoisted holder while
// keeping the real classifyModel (pure lookup).
const { mockCatalog } = vi.hoisted(() => ({
mockCatalog: {
entries: [] as Array<Record<string, unknown>>,
diagnostics: [] as Array<Record<string, unknown>>,
},
}));
vi.mock("../src/catalog", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/catalog")>();
return {
...actual,
loadModelCatalog: () => ({
catalog: { version: 1, entries: mockCatalog.entries },
diagnostics: mockCatalog.diagnostics,
}),
};
});
import type { AssistantMessage, Context, Model } from "@earendil-works/pi-ai";
import type {
ExtensionAPI,
@@ -50,6 +69,7 @@ import {
unpublishPermissionsService,
} from "@gotgenes/pi-permission-system";
import extension from "../src/index";
import type { ModelCatalogEntry } from "../src/catalog";
import { PROMPT_VERSION, TOOL_SCHEMA_VERSION } from "../src/prompt";
function createFakePi(): {
@@ -191,6 +211,8 @@ afterEach(() => {
publishedService = undefined;
}
cleanupMockAgentDir();
mockCatalog.entries = [];
mockCatalog.diagnostics = [];
});
describe("AI judge lifecycle", () => {
@@ -875,4 +897,72 @@ describe("AI judge Enforce authority seam (PIEXTENSIO-23, ADR 0008)", () => {
notify.mock.calls.filter((call) => /Enforce/i.test(String(call[0]))),
).toHaveLength(0);
});
it("appends an untested-model note to the enforce notice for an out-of-catalog model", async () => {
const { notify } = await runAsk({
config: { version: 2, mode: "enforce" },
});
const enforceNotice = notify.mock.calls
.map((call) => String(call[0]))
.find((message) => /Enforce/i.test(message));
expect(enforceNotice).toBeDefined();
expect(enforceNotice).toMatch(/untested/i);
expect(enforceNotice).toMatch(/advisory catalog/i);
});
it("adds catalog-status notes for deprecated and revoked enforce models", async () => {
for (const status of ["deprecated", "revoked"] as const) {
mockCatalog.entries = [
{
provider: "session-provider",
model: "session-model",
api: "openai-codex-responses",
status,
promptVersion: "v",
corpusVersion: "v",
testedAt: "2026-01-01T00:00:00Z",
corpusCases: 21,
matched: 21,
infrastructureFailures: 0,
latencyMs: { p50: 1, p95: 2, max: 3 },
reportPath: "reports/x.json",
} satisfies ModelCatalogEntry,
];
const { notify } = await runAsk({
config: { version: 2, mode: "enforce" },
});
const enforceNotice = notify.mock.calls
.map((call) => String(call[0]))
.find((message) => /Enforce/i.test(message));
expect(enforceNotice).toMatch(new RegExp(`catalog status: ${status}`, "i"));
}
});
it("omits the untested note when the enforce model is catalog-recommended", async () => {
mockCatalog.entries = [
{
provider: "session-provider",
model: "session-model",
api: "openai-codex-responses",
status: "recommended",
promptVersion: "v",
corpusVersion: "v",
testedAt: "2026-01-01T00:00:00Z",
corpusCases: 21,
matched: 21,
infrastructureFailures: 0,
latencyMs: { p50: 1, p95: 2, max: 3 },
reportPath: "reports/x.json",
} satisfies ModelCatalogEntry,
];
const { notify } = await runAsk({
config: { version: 2, mode: "enforce" },
});
const enforceNotice = notify.mock.calls
.map((call) => String(call[0]))
.find((message) => /Enforce/i.test(message));
expect(enforceNotice).toBeDefined();
expect(enforceNotice).not.toMatch(/untested/i);
expect(enforceNotice).not.toMatch(/catalog status/i);
});
});