mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01: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
75 lines
3.4 KiB
TypeScript
75 lines
3.4 KiB
TypeScript
import { RUNNER_WORKFLOW_IDS, type RunnerWorkflowId } from "./workflow-contracts.js";
|
|
|
|
export const STRESS_TRACEABILITY_SCHEMA = "paperclip.runner.stress-eval-traceability.v1" as const;
|
|
|
|
export interface StressTraceabilityFinding {
|
|
id: string;
|
|
classification: "workflow_eval" | "regression_test" | "explicit_exclusion";
|
|
workflowIds: RunnerWorkflowId[];
|
|
regressionTests: string[];
|
|
reason?: string;
|
|
}
|
|
|
|
export interface StressTraceabilityManifest {
|
|
schema: typeof STRESS_TRACEABILITY_SCHEMA;
|
|
campaign: string;
|
|
expectedFindings: 44;
|
|
workflows: RunnerWorkflowId[];
|
|
findings: StressTraceabilityFinding[];
|
|
}
|
|
|
|
export interface StressTraceabilitySummary {
|
|
findings: number;
|
|
workflowEvalFindings: number;
|
|
regressionTestFindings: number;
|
|
exclusions: number;
|
|
coveredWorkflows: number;
|
|
}
|
|
|
|
export function validateStressTraceabilityManifest(manifest: StressTraceabilityManifest): StressTraceabilitySummary {
|
|
if (manifest.schema !== STRESS_TRACEABILITY_SCHEMA || manifest.expectedFindings !== 44) {
|
|
throw new Error("unsupported stress traceability manifest");
|
|
}
|
|
const expectedIds = Array.from({ length: 44 }, (_, index) => `STRESS-${String(index + 1).padStart(3, "0")}`);
|
|
const actualIds = manifest.findings.map((finding) => finding.id);
|
|
if (actualIds.length !== expectedIds.length || new Set(actualIds).size !== expectedIds.length) {
|
|
throw new Error("stress traceability must contain 44 unique findings");
|
|
}
|
|
for (const id of expectedIds) {
|
|
if (!actualIds.includes(id)) throw new Error(`stress traceability is missing ${id}`);
|
|
}
|
|
if (manifest.workflows.length !== RUNNER_WORKFLOW_IDS.length
|
|
|| !RUNNER_WORKFLOW_IDS.every((id) => manifest.workflows.includes(id))) {
|
|
throw new Error("stress traceability workflow inventory does not match the Runner workflow catalog");
|
|
}
|
|
for (const finding of manifest.findings) {
|
|
for (const workflowId of finding.workflowIds) {
|
|
if (!(RUNNER_WORKFLOW_IDS as readonly string[]).includes(workflowId)) {
|
|
throw new Error(`${finding.id} references unknown workflow ${workflowId}`);
|
|
}
|
|
}
|
|
if (finding.classification === "workflow_eval" && finding.workflowIds.length === 0) {
|
|
throw new Error(`${finding.id} is a workflow eval without a workflow`);
|
|
}
|
|
if (finding.classification !== "explicit_exclusion" && finding.regressionTests.length === 0) {
|
|
throw new Error(`${finding.id} is missing a regression test anchor`);
|
|
}
|
|
if (finding.regressionTests.some((path) => !/\.test\.[cm]?[jt]sx?$/.test(path))) {
|
|
throw new Error(`${finding.id} references a non-test regression anchor`);
|
|
}
|
|
if (finding.classification === "regression_test" && finding.regressionTests.length === 0) {
|
|
throw new Error(`${finding.id} is a regression finding without a test`);
|
|
}
|
|
if (finding.classification === "explicit_exclusion" && !finding.reason) {
|
|
throw new Error(`${finding.id} exclusion is missing a reason`);
|
|
}
|
|
}
|
|
return {
|
|
findings: manifest.findings.length,
|
|
workflowEvalFindings: manifest.findings.filter((finding) => finding.classification === "workflow_eval").length,
|
|
regressionTestFindings: manifest.findings.filter((finding) => finding.classification === "regression_test").length,
|
|
exclusions: manifest.findings.filter((finding) => finding.classification === "explicit_exclusion").length,
|
|
coveredWorkflows: new Set(manifest.findings.flatMap((finding) => finding.workflowIds)).size,
|
|
};
|
|
}
|