{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/evolve/run.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AA+EvF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAGzE;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IACjD,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CACjB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkBxB;AAkFD,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAEzF;AAED,wBAAsB,kBAAkB,CACvC,KAAK,EAAE,QAAQ,EACf,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CACb,IAAI,CACH,YAAY,EACV,QAAQ,GACR,SAAS,GACT,gBAAgB,GAChB,UAAU,GACV,gBAAgB,GAChB,YAAY,GACZ,OAAO,GACP,WAAW,GACX,iBAAiB,GACjB,SAAS,GACT,UAAU,GACV,YAAY,GACZ,kBAAkB,GAClB,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,oBAAoB,GACpB,iBAAiB,GACjB,uBAAuB,GACvB,sBAAsB,GACtB,2BAA2B,GAC3B,kBAAkB,GAClB,cAAc,GACd,kBAAkB,GAClB,YAAY,CACd,CACD,EACD,GAAG,GAAE,MAAM,IAAuB,GAChC,OAAO,CAAC,YAAY,CAAC,CAYvB;AAED,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;AAmBD,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAQhF;AAED;;;;GAIG;AACH,wBAAsB,+BAA+B,CACpD,KAAK,EAAE,QAAQ,EACf,UAAU,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAiBxC","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { mkdir, readdir, rm } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { EvoPaths } from \"../paths.ts\";\nimport { ensureEvoLayout } from \"../paths.ts\";\nimport { loadProposal } from \"../proposal.ts\";\nimport { atomicWriteJson, readJson, readJsonIfExists } from \"../storage.ts\";\nimport type { EvoExperimentSpec, EvolutionRun, EvolutionRunStatus } from \"../types.ts\";\n\nconst RUN_ID_PATTERN = /^r-[A-Za-z0-9._-]+$/;\nconst PROPOSAL_ID_PATTERN = /^p-[A-Za-z0-9._-]+$/;\nconst DIGEST_PATTERN = /^[a-f0-9]{64}$/;\nconst TERMINAL_STATUSES = new Set<EvolutionRunStatus>([\"completed\", \"failed\", \"cancelled\"]);\n\n/**\n * Legal run-status transitions. Same-status patches always pass; anything absent\n * here throws, which makes accidental resets (most importantly any transition out\n * of \"completed\") unrepresentable instead of merely unlikely.\n */\nconst RUN_TRANSITIONS: Record<EvolutionRunStatus, ReadonlySet<EvolutionRunStatus>> = {\n\tqueued: new Set([\n\t\t\"researching\",\n\t\t\"building\",\n\t\t\"completed\",\n\t\t\"validating\",\n\t\t\"awaiting-canary-approval\",\n\t\t\"paused\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]),\n\tresearching: new Set([\"planned\", \"paused\", \"failed\", \"cancelled\"]),\n\tplanned: new Set([\"building\", \"completed\", \"paused\", \"failed\", \"cancelled\"]),\n\tbuilding: new Set([\"validating\", \"paused\", \"failed\", \"cancelled\"]),\n\tvalidating: new Set([\n\t\t\"replaying\",\n\t\t\"evaluating\",\n\t\t\"awaiting-canary-approval\",\n\t\t\"awaiting-evidence\",\n\t\t\"awaiting-decision\",\n\t\t\"paused\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]),\n\treplaying: new Set([\"evaluating\", \"paused\", \"failed\", \"cancelled\"]),\n\tevaluating: new Set([\n\t\t\"completed\",\n\t\t\"trialing\",\n\t\t\"awaiting-evidence\",\n\t\t\"awaiting-canary-approval\",\n\t\t\"awaiting-decision\",\n\t\t\"paused\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]),\n\t\"awaiting-evidence\": new Set([\n\t\t\"replaying\",\n\t\t\"evaluating\",\n\t\t\"trialing\",\n\t\t\"awaiting-decision\",\n\t\t\"completed\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]),\n\t\"awaiting-canary-approval\": new Set([\"trialing\", \"awaiting-decision\", \"completed\", \"failed\", \"cancelled\"]),\n\t\"awaiting-decision\": new Set([\"trialing\", \"completed\", \"failed\", \"cancelled\"]),\n\ttrialing: new Set([\"awaiting-decision\", \"completed\", \"failed\", \"cancelled\"]),\n\tpaused: new Set([\n\t\t\"queued\",\n\t\t\"researching\",\n\t\t\"planned\",\n\t\t\"building\",\n\t\t\"validating\",\n\t\t\"replaying\",\n\t\t\"evaluating\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]),\n\tfailed: new Set([\"researching\"]),\n\tcancelled: new Set([\"researching\"]),\n\tcompleted: new Set(),\n};\n\nfunction runId(): string {\n\treturn `r-${new Date().toISOString().replace(/[:.]/g, \"-\")}-${randomUUID().slice(0, 8)}`;\n}\n\nexport function evolutionRunDirectory(paths: EvoPaths, id: string): string {\n\tif (!RUN_ID_PATTERN.test(id)) throw new Error(`Invalid evolution run id: ${id}`);\n\treturn join(paths.runs, id);\n}\n\nexport async function createEvolutionRun(options: {\n\tpaths: EvoPaths;\n\ttrigger: EvolutionRun[\"trigger\"];\n\trequest?: string;\n\tevidenceDigest: string;\n\tstatus?: EvolutionRunStatus;\n\tnow?: () => Date;\n}): Promise<EvolutionRun> {\n\tawait ensureEvoLayout(options.paths);\n\tconst id = runId();\n\tconst timestamp = (options.now ?? (() => new Date()))().toISOString();\n\tconst run: EvolutionRun = {\n\t\tschemaVersion: 1,\n\t\tid,\n\t\ttrigger: options.trigger,\n\t\t...(options.request ? { request: options.request } : {}),\n\t\tstatus: options.status ?? \"researching\",\n\t\tstartedAt: timestamp,\n\t\tupdatedAt: timestamp,\n\t\tevidenceDigest: options.evidenceDigest,\n\t};\n\tconst directory = evolutionRunDirectory(options.paths, id);\n\tawait mkdir(directory, { recursive: false, mode: 0o700 });\n\tawait atomicWriteJson(join(directory, \"run.json\"), run);\n\treturn run;\n}\n\nfunction parseEvolutionRun(value: unknown): EvolutionRun {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new Error(\"Evolution run is invalid\");\n\tconst run = value as Record<string, unknown>;\n\tconst statuses = new Set<EvolutionRunStatus>([\n\t\t\"queued\",\n\t\t\"researching\",\n\t\t\"planned\",\n\t\t\"building\",\n\t\t\"validating\",\n\t\t\"replaying\",\n\t\t\"evaluating\",\n\t\t\"awaiting-evidence\",\n\t\t\"awaiting-canary-approval\",\n\t\t\"trialing\",\n\t\t\"awaiting-decision\",\n\t\t\"paused\",\n\t\t\"completed\",\n\t\t\"failed\",\n\t\t\"cancelled\",\n\t]);\n\tif (\n\t\trun.schemaVersion !== 1 ||\n\t\ttypeof run.id !== \"string\" ||\n\t\t!RUN_ID_PATTERN.test(run.id) ||\n\t\t(run.trigger !== \"scheduled\" && run.trigger !== \"request\") ||\n\t\ttypeof run.status !== \"string\" ||\n\t\t!statuses.has(run.status as EvolutionRunStatus) ||\n\t\ttypeof run.startedAt !== \"string\" ||\n\t\ttypeof run.updatedAt !== \"string\" ||\n\t\ttypeof run.evidenceDigest !== \"string\" ||\n\t\t(run.request !== undefined && typeof run.request !== \"string\") ||\n\t\t(run.planFile !== undefined && typeof run.planFile !== \"string\") ||\n\t\t(run.experimentFile !== undefined && typeof run.experimentFile !== \"string\") ||\n\t\t(run.proposalId !== undefined &&\n\t\t\t(typeof run.proposalId !== \"string\" || !PROPOSAL_ID_PATTERN.test(run.proposalId))) ||\n\t\t(run.error !== undefined && typeof run.error !== \"string\") ||\n\t\t(run.workerPid !== undefined && (!Number.isInteger(run.workerPid) || (run.workerPid as number) <= 0)) ||\n\t\t(run.workerStartedAt !== undefined && typeof run.workerStartedAt !== \"string\") ||\n\t\t(run.logFile !== undefined && typeof run.logFile !== \"string\") ||\n\t\t(run.pausedAt !== undefined && typeof run.pausedAt !== \"string\") ||\n\t\t(run.pausedFrom !== undefined &&\n\t\t\trun.pausedFrom !== \"queued\" &&\n\t\t\trun.pausedFrom !== \"researching\" &&\n\t\t\trun.pausedFrom !== \"planned\" &&\n\t\t\trun.pausedFrom !== \"building\" &&\n\t\t\trun.pausedFrom !== \"validating\" &&\n\t\t\trun.pausedFrom !== \"replaying\" &&\n\t\t\trun.pausedFrom !== \"evaluating\") ||\n\t\t(run.canaryApprovedAt !== undefined && typeof run.canaryApprovedAt !== \"string\") ||\n\t\t(run.canaryApprovalDigest !== undefined &&\n\t\t\t(typeof run.canaryApprovalDigest !== \"string\" || !DIGEST_PATTERN.test(run.canaryApprovalDigest))) ||\n\t\t(run.canaryStableDigest !== undefined &&\n\t\t\t(typeof run.canaryStableDigest !== \"string\" || !DIGEST_PATTERN.test(run.canaryStableDigest))) ||\n\t\t(run.canaryCandidateDigest !== undefined &&\n\t\t\t(typeof run.canaryCandidateDigest !== \"string\" || !DIGEST_PATTERN.test(run.canaryCandidateDigest))) ||\n\t\t(run.canaryParentDigest !== undefined &&\n\t\t\t(typeof run.canaryParentDigest !== \"string\" || !DIGEST_PATTERN.test(run.canaryParentDigest))) ||\n\t\t(run.canaryTargetAbi !== undefined &&\n\t\t\t(typeof run.canaryTargetAbi !== \"string\" || !/^[a-z][a-z0-9-]*\\/v[1-9][0-9]*$/.test(run.canaryTargetAbi))) ||\n\t\t(run.componentApprovalMode !== undefined &&\n\t\t\trun.componentApprovalMode !== \"direct\" &&\n\t\t\trun.componentApprovalMode !== \"default-canary\" &&\n\t\t\trun.componentApprovalMode !== \"custom-canary\") ||\n\t\t(run.canaryMinimumSamples !== undefined &&\n\t\t\t(!Number.isSafeInteger(run.canaryMinimumSamples) || (run.canaryMinimumSamples as number) <= 0)) ||\n\t\t(run.canaryMaximumDurationDays !== undefined &&\n\t\t\t(!Number.isSafeInteger(run.canaryMaximumDurationDays) || (run.canaryMaximumDurationDays as number) <= 0)) ||\n\t\t(run.experimentDigest !== undefined &&\n\t\t\t(typeof run.experimentDigest !== \"string\" || !DIGEST_PATTERN.test(run.experimentDigest))) ||\n\t\t(run.retryOfRunId !== undefined &&\n\t\t\t(typeof run.retryOfRunId !== \"string\" || !RUN_ID_PATTERN.test(run.retryOfRunId))) ||\n\t\t(run.sourceProposalId !== undefined &&\n\t\t\t(typeof run.sourceProposalId !== \"string\" || !PROPOSAL_ID_PATTERN.test(run.sourceProposalId))) ||\n\t\t(run.resumeFrom !== undefined && run.resumeFrom !== \"building\")\n\t) {\n\t\tthrow new Error(\"Evolution run is invalid\");\n\t}\n\treturn value as EvolutionRun;\n}\n\nexport async function readEvolutionRun(paths: EvoPaths, id: string): Promise<EvolutionRun> {\n\treturn parseEvolutionRun(await readJson(join(evolutionRunDirectory(paths, id), \"run.json\")));\n}\n\nexport async function updateEvolutionRun(\n\tpaths: EvoPaths,\n\tid: string,\n\tpatch: Partial<\n\t\tPick<\n\t\t\tEvolutionRun,\n\t\t\t| \"status\"\n\t\t\t| \"request\"\n\t\t\t| \"evidenceDigest\"\n\t\t\t| \"planFile\"\n\t\t\t| \"experimentFile\"\n\t\t\t| \"proposalId\"\n\t\t\t| \"error\"\n\t\t\t| \"workerPid\"\n\t\t\t| \"workerStartedAt\"\n\t\t\t| \"logFile\"\n\t\t\t| \"pausedAt\"\n\t\t\t| \"pausedFrom\"\n\t\t\t| \"canaryApprovedAt\"\n\t\t\t| \"canaryApprovalDigest\"\n\t\t\t| \"canaryStableDigest\"\n\t\t\t| \"canaryCandidateDigest\"\n\t\t\t| \"canaryParentDigest\"\n\t\t\t| \"canaryTargetAbi\"\n\t\t\t| \"componentApprovalMode\"\n\t\t\t| \"canaryMinimumSamples\"\n\t\t\t| \"canaryMaximumDurationDays\"\n\t\t\t| \"experimentDigest\"\n\t\t\t| \"retryOfRunId\"\n\t\t\t| \"sourceProposalId\"\n\t\t\t| \"resumeFrom\"\n\t\t>\n\t>,\n\tnow: () => Date = () => new Date(),\n): Promise<EvolutionRun> {\n\tconst current = await readEvolutionRun(paths, id);\n\tif (\n\t\tpatch.status !== undefined &&\n\t\tpatch.status !== current.status &&\n\t\t!RUN_TRANSITIONS[current.status].has(patch.status)\n\t) {\n\t\tthrow new Error(`Illegal evolution run transition ${current.status} → ${patch.status} for ${id}`);\n\t}\n\tconst next: EvolutionRun = { ...current, ...patch, updatedAt: now().toISOString() };\n\tawait atomicWriteJson(join(evolutionRunDirectory(paths, id), \"run.json\"), next);\n\treturn next;\n}\n\nexport async function deleteEvolutionRun(paths: EvoPaths, id: string): Promise<void> {\n\tawait rm(evolutionRunDirectory(paths, id), { recursive: true, force: true });\n}\n\nasync function reconcileEvolutionRun(paths: EvoPaths, run: EvolutionRun): Promise<EvolutionRun> {\n\tif (TERMINAL_STATUSES.has(run.status) || !run.proposalId) return run;\n\tconst proposal = await loadProposal(paths, run.proposalId);\n\tconst status =\n\t\tproposal.status === \"trialing\"\n\t\t\t? \"trialing\"\n\t\t\t: proposal.status === \"deferred\"\n\t\t\t\t? \"awaiting-decision\"\n\t\t\t\t: proposal.status === \"approved\" ||\n\t\t\t\t\t\tproposal.status === \"rejected\" ||\n\t\t\t\t\t\tproposal.status === \"kept\" ||\n\t\t\t\t\t\tproposal.status === \"rolled-back\"\n\t\t\t\t\t? \"completed\"\n\t\t\t\t\t: run.status;\n\treturn status === run.status ? run : updateEvolutionRun(paths, run.id, { status });\n}\n\nexport async function listEvolutionRuns(paths: EvoPaths): Promise<EvolutionRun[]> {\n\tawait ensureEvoLayout(paths);\n\tconst runs: EvolutionRun[] = [];\n\tfor (const entry of await readdir(paths.runs, { withFileTypes: true })) {\n\t\tif (!entry.isDirectory() || !RUN_ID_PATTERN.test(entry.name)) continue;\n\t\truns.push(await reconcileEvolutionRun(paths, await readEvolutionRun(paths, entry.name)));\n\t}\n\treturn runs.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n}\n\n/**\n * Locate the frozen experiment contract that produced a proposal by scanning run\n * records for the matching proposalId. Reads are deliberately tolerant: legacy\n * runs without a contract simply yield undefined.\n */\nexport async function readFrozenExperimentForProposal(\n\tpaths: EvoPaths,\n\tproposalId: string,\n): Promise<EvoExperimentSpec | undefined> {\n\tlet entries: string[];\n\ttry {\n\t\tentries = await readdir(paths.runs);\n\t} catch (error) {\n\t\tif ((error as { code?: string }).code === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n\tfor (const id of entries) {\n\t\tconst run = await readJsonIfExists<{ proposalId?: string; experimentFile?: string }>(\n\t\t\tjoin(paths.runs, id, \"run.json\"),\n\t\t);\n\t\tif (!run || run.proposalId !== proposalId || !run.experimentFile) continue;\n\t\tconst experiment = await readJsonIfExists<EvoExperimentSpec>(join(paths.runs, id, run.experimentFile));\n\t\tif (experiment) return experiment;\n\t}\n\treturn undefined;\n}\n"]}