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>
153 lines
5.1 KiB
TypeScript
153 lines
5.1 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 { createTunedTtlTimelineAdapter } from "../../src/evals/timeline-adapters/tuned-ttl.js";
|
|
import {
|
|
REACTOR_ORACLE_SPEC_KIND,
|
|
REACTOR_TIMELINE_CASE_KIND,
|
|
runReactorTimelineCase,
|
|
type JsonValue,
|
|
type ReactorTimelineCase,
|
|
type ReactorTimelineEvent,
|
|
} from "../../src/evals/index.js";
|
|
|
|
describe("createTunedTtlTimelineAdapter", () => {
|
|
test("computes the first event, reuses inside the TTL, and rechecks after expiry", async () => {
|
|
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-tuned-ttl-cache-"));
|
|
const judgeCalls: JudgeCall[] = [];
|
|
const adapter = createTunedTtlTimelineAdapter({
|
|
ttlMs: 60 * 60 * 1000,
|
|
judge: async (input) => {
|
|
const verdict = { eventId: input.event.id, sequence: judgeCalls.length + 1 };
|
|
judgeCalls.push({
|
|
eventId: input.event.id,
|
|
priorVerdict: input.priorVerdict,
|
|
});
|
|
return verdict;
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await runReactorTimelineCase(
|
|
timelineCase([
|
|
event("first", "2026-05-17T12:00:00.000Z", "noop"),
|
|
event("within-ttl", "2026-05-17T12:30:00.000Z", "silent-drift"),
|
|
event("after-ttl", "2026-05-17T13:00:00.000Z", "silent-drift"),
|
|
]),
|
|
adapter,
|
|
{
|
|
runId: "tuned-ttl-run",
|
|
scenarioCacheRoot: cacheRoot,
|
|
now: () => new Date("2026-05-17T12:00:00.000Z"),
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe("passed");
|
|
expect(result.steps.map((step) => step.metadata?.decision)).toEqual(["compute", "reuse", "recheck"]);
|
|
expect(result.steps.map((step) => step.metrics?.modelCalls)).toEqual([1, 0, 1]);
|
|
expect(result.steps.map((step) => step.metrics?.cacheHit)).toEqual([0, 1, 0]);
|
|
expect(judgeCalls.map((call) => call.eventId)).toEqual(["first", "after-ttl"]);
|
|
expect(judgeCalls[1]?.priorVerdict).toEqual({ eventId: "first", sequence: 1 });
|
|
|
|
const cache = JSON.parse(await readFile(join(result.scenarioCacheDirectory, "tuned-ttl.json"), "utf8"));
|
|
expect(cache.state.lastComputedAt).toBe("2026-05-17T13:00:00.000Z");
|
|
expect(cache.state.lastEventId).toBe("after-ttl");
|
|
} finally {
|
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("computes relevant changes immediately and resets the TTL window", async () => {
|
|
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-tuned-ttl-cache-"));
|
|
const judgeCalls: JudgeCall[] = [];
|
|
const adapter = createTunedTtlTimelineAdapter({
|
|
ttlMs: 60 * 60 * 1000,
|
|
judge: (input) => {
|
|
const verdict = { eventId: input.event.id, sequence: judgeCalls.length + 1 };
|
|
judgeCalls.push({
|
|
eventId: input.event.id,
|
|
priorVerdict: input.priorVerdict,
|
|
});
|
|
return verdict;
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await runReactorTimelineCase(
|
|
timelineCase([
|
|
event("seed", "2026-05-17T12:00:00.000Z", "noop"),
|
|
event("relevant", "2026-05-17T12:05:00.000Z", "relevant-change"),
|
|
event("after-original-ttl", "2026-05-17T13:01:00.000Z", "silent-drift"),
|
|
]),
|
|
adapter,
|
|
{
|
|
runId: "tuned-ttl-reset-run",
|
|
scenarioCacheRoot: cacheRoot,
|
|
now: () => new Date("2026-05-17T12:00:00.000Z"),
|
|
},
|
|
);
|
|
|
|
expect(result.status).toBe("passed");
|
|
expect(result.steps.map((step) => step.metadata?.decision)).toEqual(["compute", "compute", "reuse"]);
|
|
expect(result.steps.map((step) => step.metrics?.modelCalls)).toEqual([1, 1, 0]);
|
|
expect(judgeCalls.map((call) => call.eventId)).toEqual(["seed", "relevant"]);
|
|
expect(judgeCalls[1]?.priorVerdict).toEqual({ eventId: "seed", sequence: 1 });
|
|
|
|
const cache = JSON.parse(await readFile(join(result.scenarioCacheDirectory, "tuned-ttl.json"), "utf8"));
|
|
expect(cache.state.lastComputedAt).toBe("2026-05-17T12:05:00.000Z");
|
|
expect(cache.state.lastEventId).toBe("relevant");
|
|
} finally {
|
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
interface JudgeCall {
|
|
eventId: string;
|
|
priorVerdict: JsonValue | undefined;
|
|
}
|
|
|
|
function timelineCase(events: readonly ReactorTimelineEvent[]): ReactorTimelineCase {
|
|
return {
|
|
kind: REACTOR_TIMELINE_CASE_KIND,
|
|
version: 1,
|
|
id: "tuned-ttl-timeline",
|
|
title: "Tuned TTL timeline",
|
|
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,
|
|
};
|
|
}
|
|
|
|
function event(id: string, at: string, label: ReactorTimelineEvent["label"]): ReactorTimelineEvent {
|
|
return {
|
|
id,
|
|
at,
|
|
label,
|
|
trigger: label === "relevant-change" ? "input" : "scheduled",
|
|
type: label === "relevant-change" ? "evidence.receipt" : "forecast.recheck",
|
|
payload: { id },
|
|
payloadCid: "d".repeat(64),
|
|
};
|
|
}
|