import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { CONTRACT_PATHS as P } from "@autoscholar/schemas"; import { PDFDocument, StandardFonts } from "pdf-lib"; import { syncArtifacts, writeChecksums } from "./artifacts.js"; import type { RunnerHost, RunnerJob, RunOutcome } from "./runner.js"; import { STAGE_SPECS } from "./stages.js"; /** * TEST-ONLY simulated runner: produces a contract-shaped artifact set WITHOUT any model call. Every artifact is uploaded * with synthetic=true and the worker reports itself as simulated, so the quality gates can never publish its output. */ export async function simulateResearch( job: RunnerJob, host: RunnerHost, opts: { failAtStage?: string; delayMs?: number; hang?: boolean } = {}, ): Promise { const ws = join(host.workspaceRoot, "runs", job.runId); for (const d of [ "input", "plan", "sources", "data", "code", "results/tables", "results/figures", "manuscript", "review", "provenance", ]) await mkdir(join(ws, d), { recursive: true }); await writeFile(join(ws, "CLAUDE.md"), job.instructionText); await writeFile(join(ws, P.instruction), job.instructionText); await writeFile(join(ws, P.manifest), JSON.stringify(job.manifest, null, 2)); const upload = (p: string, k: Parameters[1], ct: string, b: Buffer) => host.upload(p, k, ct, b); let cost = 0; const completed: string[] = []; const started = Date.now(); const startIdx = job.resumeFromStage ? STAGE_SPECS.findIndex((s) => s.key === job.resumeFromStage) + 1 : 0; for (let i = startIdx; i < STAGE_SPECS.length; i++) { const s = STAGE_SPECS[i]!; if (host.isCancelled()) return { outcome: "cancelled", error: "cancelled (simulated)", costUsd: cost, turns: i, stagesCompleted: completed as never, resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 0, peakRssBytes: 0 }, }; await host.stage({ stage: s.key, status: "running", costUsd: 0, turns: 0 }); host.emit({ kind: "sim.stage", message: `[SIMULATED] ${s.key}`, at: new Date().toISOString(), stage: s.key, level: "info", data: { synthetic: true }, }); if (opts.hang) await new Promise(() => {}); if (opts.delayMs) await new Promise((r) => setTimeout(r, opts.delayMs)); if (opts.failAtStage === s.key) { await host.stage({ stage: s.key, status: "failed", costUsd: 0.01, turns: 1, error: "simulated failure", }); return { outcome: "failed", error: `simulated failure at ${s.key}`, costUsd: cost, turns: i, stagesCompleted: completed as never, resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 0, peakRssBytes: 0 }, }; } await writeStageFiles(ws, s.key, job); await syncArtifacts( ws, s.key, upload, (k, m, d) => host.emit({ kind: k, message: m, data: d, at: new Date().toISOString(), level: "info", stage: s.key, }), { synthetic: true }, ); cost += 0.01; completed.push(s.key); await host.stage({ stage: s.key, status: "completed", costUsd: 0.01, turns: 1, usage: { model: "simulated" }, checkpoint: { stage: s.key, completedStages: completed, spentUsd: cost }, }); } await writeFile( join(ws, P.runSummary), JSON.stringify( { runId: job.runId, attemptNo: job.attemptNo, researchMode: job.manifest.researchMode, stagesCompleted: completed, costUsd: cost, turns: 9, model: "simulated", reviewerModel: "simulated", workerName: host.workerName, startedAt: new Date(started).toISOString(), endedAt: new Date().toISOString(), sdkVersion: "simulated", toolchain: {}, synthetic: true, }, null, 2, ), ); await writeChecksums(ws); await syncArtifacts(ws, "reproduction", upload, () => {}, { synthetic: true }); return { outcome: "completed", costUsd: cost, turns: 9, stagesCompleted: completed as never, resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 1000, peakRssBytes: 0 }, }; } async function writeStageFiles(ws: string, stage: string, job: RunnerJob) { const w = (p: string, c: string) => writeFile(join(ws, p), c); switch (stage) { case "preflight": await w( P.feasibility, JSON.stringify({ feasible: true, researchMode: job.manifest.researchMode, summary: "simulated", restrictedActivities: [], requiredApprovals: [], missingInputs: [], risks: [], }), ); await w( P.plan, "# Research plan\n## Question\nsim\n## Evaluation plan\nsim\n## Failure modes\nsim\n## Pre-registration\nsim\n## Log\n", ); break; case "literature": await w( P.sources, JSON.stringify({ sources: [ { key: "sim2020", kind: "doi", identifier: "10.0000/sim", title: "Simulated", verified: true, verifiedAt: new Date().toISOString(), verificationMethod: "simulated", }, ], }), ); await w( P.sourceMap, JSON.stringify({ claims: [ { claim: "a", sourceKeys: ["sim2020"] }, { claim: "b", sourceKeys: ["sim2020"] }, { claim: "c", sourceKeys: ["sim2020"] }, ], }), ); await w(P.referencesBib, "@article{sim2020, title={Simulated}, year={2020}, doi={10.0000/sim}}\n"); break; case "design": await w( P.dataManifest, JSON.stringify({ datasets: [ { name: "sim", source: "simulated", license: "none", acquiredAt: "now", synthetic: true }, ], }), ); await w(P.dataReadme, "simulated"); await w(P.codeReadme, "simulated"); await w("code/analysis.py", "print('simulated')\n"); break; case "execute": await w( P.metrics, JSON.stringify({ metrics: { x: 1 }, reportedValues: [ { label: "x", value: 1 }, { label: "y", value: 2 }, { label: "z", value: 3 }, ], synthetic: true, }), ); await w( P.experimentLog, `${JSON.stringify({ name: "sim", command: "python analysis.py", startedAt: "a", endedAt: "b", exitCode: 0, synthetic: true })}\n`, ); break; case "robustness": await w(P.robustness, "simulated robustness"); break; case "manuscript": { const pdf = await PDFDocument.create(); const page = pdf.addPage(); const font = await pdf.embedFont(StandardFonts.Helvetica); page.drawText( "[SIMULATED] AutoScholar Working Paper — AI-generated, not formally peer reviewed, MacLustr", { x: 40, y: 700, size: 10, font }, ); await writeFile(join(ws, P.mainPdf), Buffer.from(await pdf.save())); await w( P.mainTex, "\\documentclass{article}\\title{Simulated}\\begin{document}\\maketitle x=1, y=2, z=3 \\cite{sim2020}\\bibliography{references}\\end{document}", ); await w(P.manuscriptBib, "@article{sim2020, title={Simulated}, year={2020}, doi={10.0000/sim}}\n"); await w(P.abstract, "Simulated abstract."); await w( P.metadata, JSON.stringify({ title: "Simulated paper", abstract: "This is a simulated abstract used only by the orchestration tests. ".repeat(3), keywords: ["simulated", "test"], discipline: job.manifest.discipline, researchMode: job.manifest.researchMode, contributions: ["none"], methods: "none", mainResults: "none", limitations: "synthetic simulation only", synthetic: true, }), ); break; } case "review": await w(P.review1, "# Review\nsimulated"); await w( "review/review-1.json", JSON.stringify({ round: 1, reviewerModel: "simulated", summary: "sim", rubric: [], findings: [], recommendation: "accept", limitationsOfThisReview: "simulated", }), ); break; case "reproduction": await w(P.revisions, "simulated revisions"); break; } }