mirror of
https://github.com/openprose/prose.git
synced 2026-09-19 05:55:05 +08:00
8564d8a7d2
* feat(cli): add evaluation runner core
* feat(cli): add eval suite loading and cost mapping
* feat(cli): add public harness eval adapters
* feat(cli): add eval suite registry and runner helper
* fix(cli): harden eval runner safety gates
* feat(cli): harden eval evidence and isolation
* feat(cli): add eval claim eligibility gates
* Harden reactor eval claim eligibility
* Harden eval claim eligibility gates
* feat(eval): add reactor timeline case contract
* feat(eval): add reactor proof verdict library
* feat(eval): add timeline baseline adapters
* feat(eval): add docker isolation substrate
* feat(eval): add phase 1b reactor-independent tier 1
* feat(eval): add phase 1b cost and canary lanes
* feat(eval): add phase 1b statistical pilot
* feat(eval): reconcile openrouter costs and containerize competitors
* feat(evals): run isolated competitor harnesses locally
* feat(evals): add phase 1b report pilot runner
* feat(evals): add phase 1b confidence intervals
* fix(evals): harden provider host matching against URL prefix spoofing
CodeQL flagged "Incomplete URL substring sanitization" in
providerFromUrl: host.endsWith("anthropic.com") also matches an
attacker-controlled host like "evilanthropic.com". Match the exact
host or a proper subdomain (".anthropic.com") instead, applied to all
three provider domains via a hostMatches helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
223 lines
6.6 KiB
TypeScript
223 lines
6.6 KiB
TypeScript
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { readFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
REACTOR_ORACLE_SPEC_KIND,
|
|
REACTOR_TIMELINE_CASE_KIND,
|
|
runReactorTimelineCase,
|
|
type JsonValue,
|
|
type ReactorTimelineCase,
|
|
type ReactorTimelineEvent,
|
|
} from "../../src/evals/index.js";
|
|
import { createDiffcachePlusTimelineAdapter } from "../../src/evals/timeline-adapters/diffcache-plus.js";
|
|
|
|
describe("diffcache-plus timeline adapter", () => {
|
|
test("does not require a local model and falls back to exact canonical cache reuse", async () => {
|
|
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-diffcache-plus-cache-"));
|
|
const judgeCalls: string[] = [];
|
|
const adapter = createDiffcachePlusTimelineAdapter({
|
|
baselineJudge: async ({ event, eventKey }) => {
|
|
judgeCalls.push(event.id);
|
|
return { eventId: event.id, eventKey, status: "computed" };
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await runReactorTimelineCase(
|
|
timelineCase([
|
|
event("exact-1", { status: "same" }),
|
|
event("exact-2", { status: "same" }),
|
|
]),
|
|
adapter,
|
|
{
|
|
runId: "diffcache-plus-no-local",
|
|
scenarioCacheRoot: cacheRoot,
|
|
now: fixedNow,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe("passed");
|
|
expect(judgeCalls).toEqual(["exact-1"]);
|
|
expect(result.steps.map((step) => step.metadata?.decision)).toEqual(["compute", "reuse"]);
|
|
expect(result.steps[1]?.metadata).toEqual(
|
|
expect.objectContaining({
|
|
cacheMode: "exact",
|
|
embeddingProvider: "fixture-or-injected",
|
|
noLocalModel: true,
|
|
threshold: 0.985,
|
|
}),
|
|
);
|
|
|
|
const cache = JSON.parse(
|
|
await readFile(join(result.scenarioCacheDirectory, "diffcache-plus.json"), "utf8"),
|
|
) as DiffcachePlusCacheFile;
|
|
expect(cache.kind).toBe("prose.eval.timeline-baseline-cache.v1");
|
|
expect(Object.keys(cache.state.verdicts)).toHaveLength(1);
|
|
} finally {
|
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("reuses semantically close fixture embeddings without computing again", async () => {
|
|
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-diffcache-plus-cache-"));
|
|
const judgeCalls: string[] = [];
|
|
const adapter = createDiffcachePlusTimelineAdapter({
|
|
baselineJudge: async ({ event, eventKey }) => {
|
|
judgeCalls.push(event.id);
|
|
return { eventId: event.id, eventKey, status: "computed" };
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await runReactorTimelineCase(
|
|
timelineCase([
|
|
event("close-1", { text: "The account balance increased by five dollars." }, [1, 0, 0]),
|
|
event("close-2", { text: "Balance went up $5." }, [0.9999, 0.01, 0]),
|
|
]),
|
|
adapter,
|
|
{
|
|
runId: "diffcache-plus-close-fixtures",
|
|
scenarioCacheRoot: cacheRoot,
|
|
now: fixedNow,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe("passed");
|
|
expect(judgeCalls).toEqual(["close-1"]);
|
|
expect(result.steps.map((step) => step.metadata?.decision)).toEqual(["compute", "reuse"]);
|
|
expect(result.steps[1]?.metrics).toEqual(expect.objectContaining({ cacheHit: 1, modelCalls: 0 }));
|
|
expect(result.steps[1]?.metadata).toEqual(
|
|
expect.objectContaining({
|
|
cacheMode: "semantic",
|
|
embeddingProvider: "fixture-or-injected",
|
|
noLocalModel: true,
|
|
threshold: 0.985,
|
|
}),
|
|
);
|
|
const similarity = result.steps[1]?.metadata?.cosineSimilarity;
|
|
expect(typeof similarity).toBe("number");
|
|
if (typeof similarity === "number") {
|
|
expect(similarity).toBeGreaterThan(0.985);
|
|
}
|
|
} finally {
|
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computes distant injected embeddings instead of semantic reuse", async () => {
|
|
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-diffcache-plus-cache-"));
|
|
const judgeCalls: string[] = [];
|
|
const providerCalls: string[] = [];
|
|
const vectors: Record<string, readonly number[]> = {
|
|
"distant-1": [1, 0],
|
|
"distant-2": [0, 1],
|
|
};
|
|
const adapter = createDiffcachePlusTimelineAdapter({
|
|
embeddingProvider: (event) => {
|
|
providerCalls.push(event.id);
|
|
return vectors[event.id];
|
|
},
|
|
baselineJudge: async ({ event, eventKey }) => {
|
|
judgeCalls.push(event.id);
|
|
return { eventId: event.id, eventKey, status: "computed" };
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await runReactorTimelineCase(
|
|
timelineCase([
|
|
event("distant-1", { text: "Open the account." }),
|
|
event("distant-2", { text: "Archive the account." }),
|
|
]),
|
|
adapter,
|
|
{
|
|
runId: "diffcache-plus-distant-injected",
|
|
scenarioCacheRoot: cacheRoot,
|
|
now: fixedNow,
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe("passed");
|
|
expect(providerCalls).toEqual(["distant-1", "distant-2"]);
|
|
expect(judgeCalls).toEqual(["distant-1", "distant-2"]);
|
|
expect(result.steps.map((step) => step.metadata?.decision)).toEqual(["compute", "compute"]);
|
|
expect(result.steps.map((step) => step.metrics?.modelCalls)).toEqual([1, 1]);
|
|
expect(result.steps[1]?.metadata).toEqual(
|
|
expect.objectContaining({
|
|
cacheMode: "miss",
|
|
cosineSimilarity: 0,
|
|
embeddingProvider: "fixture-or-injected",
|
|
noLocalModel: true,
|
|
}),
|
|
);
|
|
} finally {
|
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
interface DiffcachePlusCacheFile {
|
|
kind: string;
|
|
state: {
|
|
verdicts: Record<string, JsonValue>;
|
|
};
|
|
}
|
|
|
|
function fixedNow(): Date {
|
|
return new Date("2026-05-17T12:00:00.000Z");
|
|
}
|
|
|
|
function timelineCase(events: readonly ReactorTimelineEvent[]): ReactorTimelineCase {
|
|
return {
|
|
kind: REACTOR_TIMELINE_CASE_KIND,
|
|
version: 1,
|
|
id: "diffcache-plus-fixture",
|
|
title: "Diffcache plus fixture",
|
|
contract: {
|
|
source: {
|
|
path: "tests/evals/fixtures/quiet-drift-canary.prose.md",
|
|
responsibilityId: "quiet-drift-canary",
|
|
revision: "fixture-v1",
|
|
sha256: "939de0bacfc591264a67abf985f13ffa08d822ed96c3e47d0f279766553c7fe8",
|
|
signerTrustContext: "fixture-null-signer",
|
|
},
|
|
},
|
|
oracle: {
|
|
kind: REACTOR_ORACLE_SPEC_KIND,
|
|
cid: "a".repeat(64),
|
|
policyCid: "b".repeat(64),
|
|
forecastModelId: "fixture-forecast-v1",
|
|
recheckSchedule: ["2026-05-17T13:00:00.000Z"],
|
|
recheckTolerance: 60_000,
|
|
preconditionSet: ["c".repeat(64)],
|
|
},
|
|
events,
|
|
claims: ["C2", "C5"],
|
|
limits: {
|
|
maxCostUsd: 0.05,
|
|
maxModelCalls: 2,
|
|
maxWallTimeMs: 120_000,
|
|
},
|
|
metadata: {
|
|
reportUse: "debug",
|
|
},
|
|
};
|
|
}
|
|
|
|
function event(id: string, payload: JsonValue, embedding?: readonly number[]): ReactorTimelineEvent {
|
|
return {
|
|
id,
|
|
at: "2026-05-17T12:00:00.000Z",
|
|
label: "relevant-change",
|
|
trigger: "input",
|
|
type: "evidence.receipt",
|
|
payload,
|
|
payloadCid: "d".repeat(64),
|
|
...(embedding === undefined ? {} : { metadata: { embedding: [...embedding] } }),
|
|
};
|
|
}
|