SPB Git

spb/autoscholar

Public
8.5 KB · 277 lines typescript
1import { mkdir, writeFile } from "node:fs/promises";2import { join } from "node:path";3import { CONTRACT_PATHS as P } from "@autoscholar/schemas";4import { PDFDocument, StandardFonts } from "pdf-lib";5import { syncArtifacts, writeChecksums } from "./artifacts.js";6import type { RunnerHost, RunnerJob, RunOutcome } from "./runner.js";7import { STAGE_SPECS } from "./stages.js";89/**10 * TEST-ONLY simulated runner: produces a contract-shaped artifact set WITHOUT any model call. Every artifact is uploaded11 * with synthetic=true and the worker reports itself as simulated, so the quality gates can never publish its output.12 */13export async function simulateResearch(14  job: RunnerJob,15  host: RunnerHost,16  opts: { failAtStage?: string; delayMs?: number; hang?: boolean } = {},17): Promise<RunOutcome> {18  const ws = join(host.workspaceRoot, "runs", job.runId);19  for (const d of [20    "input",21    "plan",22    "sources",23    "data",24    "code",25    "results/tables",26    "results/figures",27    "manuscript",28    "review",29    "provenance",30  ])31    await mkdir(join(ws, d), { recursive: true });32  await writeFile(join(ws, "CLAUDE.md"), job.instructionText);33  await writeFile(join(ws, P.instruction), job.instructionText);34  await writeFile(join(ws, P.manifest), JSON.stringify(job.manifest, null, 2));35  const upload = (p: string, k: Parameters<RunnerHost["upload"]>[1], ct: string, b: Buffer) =>36    host.upload(p, k, ct, b);37  let cost = 0;38  const completed: string[] = [];39  const started = Date.now();40  const startIdx = job.resumeFromStage ? STAGE_SPECS.findIndex((s) => s.key === job.resumeFromStage) + 1 : 0;41  for (let i = startIdx; i < STAGE_SPECS.length; i++) {42    const s = STAGE_SPECS[i]!;43    if (host.isCancelled())44      return {45        outcome: "cancelled",46        error: "cancelled (simulated)",47        costUsd: cost,48        turns: i,49        stagesCompleted: completed as never,50        resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 0, peakRssBytes: 0 },51      };52    await host.stage({ stage: s.key, status: "running", costUsd: 0, turns: 0 });53    host.emit({54      kind: "sim.stage",55      message: `[SIMULATED] ${s.key}`,56      at: new Date().toISOString(),57      stage: s.key,58      level: "info",59      data: { synthetic: true },60    });61    if (opts.hang) await new Promise(() => {});62    if (opts.delayMs) await new Promise((r) => setTimeout(r, opts.delayMs));63    if (opts.failAtStage === s.key) {64      await host.stage({65        stage: s.key,66        status: "failed",67        costUsd: 0.01,68        turns: 1,69        error: "simulated failure",70      });71      return {72        outcome: "failed",73        error: `simulated failure at ${s.key}`,74        costUsd: cost,75        turns: i,76        stagesCompleted: completed as never,77        resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 0, peakRssBytes: 0 },78      };79    }80    await writeStageFiles(ws, s.key, job);81    await syncArtifacts(82      ws,83      s.key,84      upload,85      (k, m, d) =>86        host.emit({87          kind: k,88          message: m,89          data: d,90          at: new Date().toISOString(),91          level: "info",92          stage: s.key,93        }),94      { synthetic: true },95    );96    cost += 0.01;97    completed.push(s.key);98    await host.stage({99      stage: s.key,100      status: "completed",101      costUsd: 0.01,102      turns: 1,103      usage: { model: "simulated" },104      checkpoint: { stage: s.key, completedStages: completed, spentUsd: cost },105    });106  }107  await writeFile(108    join(ws, P.runSummary),109    JSON.stringify(110      {111        runId: job.runId,112        attemptNo: job.attemptNo,113        researchMode: job.manifest.researchMode,114        stagesCompleted: completed,115        costUsd: cost,116        turns: 9,117        model: "simulated",118        reviewerModel: "simulated",119        workerName: host.workerName,120        startedAt: new Date(started).toISOString(),121        endedAt: new Date().toISOString(),122        sdkVersion: "simulated",123        toolchain: {},124        synthetic: true,125      },126      null,127      2,128    ),129  );130  await writeChecksums(ws);131  await syncArtifacts(ws, "reproduction", upload, () => {}, { synthetic: true });132  return {133    outcome: "completed",134    costUsd: cost,135    turns: 9,136    stagesCompleted: completed as never,137    resourceUsage: { wallSeconds: (Date.now() - started) / 1000, diskBytes: 1000, peakRssBytes: 0 },138  };139}140141async function writeStageFiles(ws: string, stage: string, job: RunnerJob) {142  const w = (p: string, c: string) => writeFile(join(ws, p), c);143  switch (stage) {144    case "preflight":145      await w(146        P.feasibility,147        JSON.stringify({148          feasible: true,149          researchMode: job.manifest.researchMode,150          summary: "simulated",151          restrictedActivities: [],152          requiredApprovals: [],153          missingInputs: [],154          risks: [],155        }),156      );157      await w(158        P.plan,159        "# Research plan\n## Question\nsim\n## Evaluation plan\nsim\n## Failure modes\nsim\n## Pre-registration\nsim\n## Log\n",160      );161      break;162    case "literature":163      await w(164        P.sources,165        JSON.stringify({166          sources: [167            {168              key: "sim2020",169              kind: "doi",170              identifier: "10.0000/sim",171              title: "Simulated",172              verified: true,173              verifiedAt: new Date().toISOString(),174              verificationMethod: "simulated",175            },176          ],177        }),178      );179      await w(180        P.sourceMap,181        JSON.stringify({182          claims: [183            { claim: "a", sourceKeys: ["sim2020"] },184            { claim: "b", sourceKeys: ["sim2020"] },185            { claim: "c", sourceKeys: ["sim2020"] },186          ],187        }),188      );189      await w(P.referencesBib, "@article{sim2020, title={Simulated}, year={2020}, doi={10.0000/sim}}\n");190      break;191    case "design":192      await w(193        P.dataManifest,194        JSON.stringify({195          datasets: [196            { name: "sim", source: "simulated", license: "none", acquiredAt: "now", synthetic: true },197          ],198        }),199      );200      await w(P.dataReadme, "simulated");201      await w(P.codeReadme, "simulated");202      await w("code/analysis.py", "print('simulated')\n");203      break;204    case "execute":205      await w(206        P.metrics,207        JSON.stringify({208          metrics: { x: 1 },209          reportedValues: [210            { label: "x", value: 1 },211            { label: "y", value: 2 },212            { label: "z", value: 3 },213          ],214          synthetic: true,215        }),216      );217      await w(218        P.experimentLog,219        `${JSON.stringify({ name: "sim", command: "python analysis.py", startedAt: "a", endedAt: "b", exitCode: 0, synthetic: true })}\n`,220      );221      break;222    case "robustness":223      await w(P.robustness, "simulated robustness");224      break;225    case "manuscript": {226      const pdf = await PDFDocument.create();227      const page = pdf.addPage();228      const font = await pdf.embedFont(StandardFonts.Helvetica);229      page.drawText(230        "[SIMULATED] AutoScholar Working Paper — AI-generated, not formally peer reviewed, MacLustr",231        { x: 40, y: 700, size: 10, font },232      );233      await writeFile(join(ws, P.mainPdf), Buffer.from(await pdf.save()));234      await w(235        P.mainTex,236        "\\documentclass{article}\\title{Simulated}\\begin{document}\\maketitle x=1, y=2, z=3 \\cite{sim2020}\\bibliography{references}\\end{document}",237      );238      await w(P.manuscriptBib, "@article{sim2020, title={Simulated}, year={2020}, doi={10.0000/sim}}\n");239      await w(P.abstract, "Simulated abstract.");240      await w(241        P.metadata,242        JSON.stringify({243          title: "Simulated paper",244          abstract: "This is a simulated abstract used only by the orchestration tests. ".repeat(3),245          keywords: ["simulated", "test"],246          discipline: job.manifest.discipline,247          researchMode: job.manifest.researchMode,248          contributions: ["none"],249          methods: "none",250          mainResults: "none",251          limitations: "synthetic simulation only",252          synthetic: true,253        }),254      );255      break;256    }257    case "review":258      await w(P.review1, "# Review\nsimulated");259      await w(260        "review/review-1.json",261        JSON.stringify({262          round: 1,263          reviewerModel: "simulated",264          summary: "sim",265          rubric: [],266          findings: [],267          recommendation: "accept",268          limitationsOfThisReview: "simulated",269        }),270      );271      break;272    case "reproduction":273      await w(P.revisions, "simulated revisions");274      break;275  }276}277
Files · ad78891b6b969902dedc372df227e1adc3cd93ba
Menu

Theme