mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip Runner needs repeatable evaluation contracts. > - Evaluation code must stay separate from provider launch and production orchestration. > - Offline fixtures need stable compatibility, scoring, traceability, and report rules. > - Published Runner consumers need only the supported evaluation contract surface. > - This pull request adds offline evaluation tooling and a workspace-private matrix kernel. > - The benefit is deterministic evaluation without credentials or paid provider calls. ## Linked Issues or Issue Description Refs #11297 This pull request extracts the offline evaluation unit from the earlier aggregate Runner work. ## What Changed - Add a workspace-private, provider-neutral evaluation matrix kernel. - Add the public `@paperclipai/paperclip-runner/evals` compatibility and native execution contracts. - Add fail-closed runnerd artifact and protocol compatibility checks. - Add deterministic workflow catalogs, scoring, traceability, and report generation. - Add sanitized Codex, OpenCode, and ACPX fixtures. - Add package-boundary and clean-consumer checks. - Add the eval package manifest to the Docker dependency stage. - Add the generated protocol fixture digest without changing the lockfile. ## Verification GitHub Actions must run: - Runner TypeScript and Rust type checks. - Runner unit and protocol tests. - Evaluation kernel tests. - Workflow traceability checks. - Clean-consumer and package-boundary checks. - Repository test, type-check, build, policy, and security gates. No local test command was run. The repository owner requested GitHub-only verification. ## Risks This is a large greenfield review surface with 51 files. The code does not launch a live provider or load credentials. Package and protocol drift fail closed. The workspace lockfile remains under the existing CI-owned process. ## Model Used OpenAI Codex with the GPT-5 agent model. The work used high reasoning, repository inspection, tool use, and parallel code review. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
export const PAPERCLIP_EVAL_KERNEL_COMPATIBILITY = Object.freeze({
|
|
schema: "paperclip.eval-kernel.compatibility.v1" as const,
|
|
packageName: "@paperclipai/paperclip-eval-kernel" as const,
|
|
packageVersion: "0.1.0" as const,
|
|
apiVersion: 1 as const,
|
|
});
|
|
|
|
export interface PaperclipEvalScenario<TInput = unknown> {
|
|
readonly id: string;
|
|
readonly input: TInput;
|
|
}
|
|
|
|
export interface PaperclipEvalCandidate<TCandidate = unknown> {
|
|
readonly id: string;
|
|
readonly config: TCandidate;
|
|
/** Fail-closed runner/catalog/provider compatibility check. */
|
|
readonly preflight?: () => void | Promise<void>;
|
|
}
|
|
|
|
export interface PaperclipEvalResult<TOutput = unknown, TScore = unknown> {
|
|
readonly scenarioId: string;
|
|
readonly candidateId: string;
|
|
readonly output: TOutput;
|
|
readonly score: TScore;
|
|
}
|
|
|
|
export class PaperclipEvalKernelConfigurationError extends Error {
|
|
readonly code = "paperclip_eval_kernel_configuration_invalid" as const;
|
|
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = "PaperclipEvalKernelConfigurationError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generic deterministic matrix orchestration. Scenario definitions, provider
|
|
* configuration, scorers, reports, and persistence remain caller-owned.
|
|
*/
|
|
export async function runPaperclipEvalMatrix<
|
|
TInput,
|
|
TCandidate,
|
|
TOutput,
|
|
TScore,
|
|
>(input: {
|
|
readonly scenarios: readonly PaperclipEvalScenario<TInput>[];
|
|
readonly candidates: readonly PaperclipEvalCandidate<TCandidate>[];
|
|
readonly execute: (context: {
|
|
readonly scenario: PaperclipEvalScenario<TInput>;
|
|
readonly candidate: PaperclipEvalCandidate<TCandidate>;
|
|
}) => Promise<TOutput>;
|
|
readonly score: (context: {
|
|
readonly scenario: PaperclipEvalScenario<TInput>;
|
|
readonly candidate: PaperclipEvalCandidate<TCandidate>;
|
|
readonly output: TOutput;
|
|
}) => Promise<TScore> | TScore;
|
|
}): Promise<readonly PaperclipEvalResult<TOutput, TScore>[]> {
|
|
assertUniqueNonEmptyIds("scenario", input.scenarios);
|
|
assertUniqueNonEmptyIds("candidate", input.candidates);
|
|
|
|
for (const candidate of input.candidates) {
|
|
await candidate.preflight?.();
|
|
}
|
|
|
|
const results: PaperclipEvalResult<TOutput, TScore>[] = [];
|
|
for (const scenario of input.scenarios) {
|
|
for (const candidate of input.candidates) {
|
|
const output = await input.execute({ scenario, candidate });
|
|
const score = await input.score({ scenario, candidate, output });
|
|
results.push(Object.freeze({
|
|
scenarioId: scenario.id,
|
|
candidateId: candidate.id,
|
|
output,
|
|
score,
|
|
}));
|
|
}
|
|
}
|
|
return Object.freeze(results);
|
|
}
|
|
|
|
function assertUniqueNonEmptyIds(
|
|
kind: "scenario" | "candidate",
|
|
values: readonly { readonly id: string }[],
|
|
): void {
|
|
if (values.length === 0) {
|
|
throw new PaperclipEvalKernelConfigurationError(`${kind} list must not be empty`);
|
|
}
|
|
const ids = new Set<string>();
|
|
for (const value of values) {
|
|
if (value.id.trim().length === 0) {
|
|
throw new PaperclipEvalKernelConfigurationError(`${kind} id must not be empty`);
|
|
}
|
|
if (ids.has(value.id)) {
|
|
throw new PaperclipEvalKernelConfigurationError(`duplicate ${kind} id: ${value.id}`);
|
|
}
|
|
ids.add(value.id);
|
|
}
|
|
}
|