{"record":{"id":"c57363654d919ec2","repo":"paperclipai/paperclip","slug":"immutable-evalbook-record-changed-path","errorCode":null,"errorMessage":"Immutable Evalbook record changed: ${path}","messagePattern":"Immutable Evalbook record changed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/render-runner-workflow-evalbook.mjs","lineNumber":217,"sourceCode":"  const config = {\n    schema: \"paperclip-runner/workflow-eval-config/v1\",\n    id: result.candidateId,\n    model,\n    provider: result.observation.provider,\n    driver,\n    runnerVersion: report.bundle.runnerVersion,\n    runnerBuild: report.bundle.runnerBuild,\n    promptPolicyId: report.bundle.promptPolicyId,\n  };\n  return { attemptId, artifact, score, case: evalCase, config };\n}\n\nasync function writeImmutable(path, value) {\n  const content = json(value);\n  try {\n    const existing = await readFile(path, \"utf8\");\n    if (existing !== content) {\n      throw new Error(`Immutable Evalbook record changed: ${path}`);\n    }\n  } catch (error) {\n    if (error?.code !== \"ENOENT\") throw error;\n    await writeFile(path, content, { flag: \"wx\", mode: 0o600 });\n  }\n}\n\nexport async function writeRunnerWorkflowEvalbookAttempts({\n  report,\n  runsRoot,\n  caseForId,\n}) {\n  await mkdir(runsRoot, { recursive: true });\n  const attempts = [];\n  for (const result of report.results) {\n    const attempt = runnerWorkflowEvalbookAttempt({\n      report,\n      result,","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/render-runner-workflow-evalbook.mjs#L199-L235","documentation":"writeImmutable enforces that once an Evalbook record exists on disk it never changes: if the serialized new content differs from the file's existing content it throws, preserving the append-only/immutable guarantee of the Evalbook history. ENOENT is the only tolerated case (write a new file with mode 0600).","triggerScenarios":"Re-rendering an attempt whose recorded JSON differs from what's on disk — e.g. rerunning with different input data, a changed renderer version, or nondeterministic content (timestamps, ordering) embedded in the record; writing to the same attempt path with updated results.","commonSituations":"Editing a historical attempt's data then re-running the renderer; upgrading the eval program so record fields change for an existing attempt; two runs racing on the same output directory producing different content for the same path.","solutions":["Use a new output/attempt path (new revision or run id) instead of re-rendering an existing attempt","Revert the underlying data change so the record matches what was previously written","Diff the existing file against the new json(value) to find the source of drift (often timestamps or ordering)","If drift is from tooling upgrades, render into a fresh directory rather than over an old Evalbook"],"exampleFix":"// before\nawait writeImmutable('attempts/run-1.json', recordWithNewTimestamp);\n// after\nawait writeImmutable('attempts/run-2.json', recordWithNewTimestamp);","handlingStrategy":"try-catch","validationCode":"const next = json(value); const prev = await readFile(path,'utf8').catch(e => e.code === 'ENOENT' ? null : e); if (prev !== null && prev !== next) throw new Error('Record would change: ' + path);","typeGuard":"null","tryCatchPattern":"try { await writeImmutable(path, value); } catch (e) { if (String(e.message).startsWith('Immutable Evalbook record changed')) { console.error('Diff existing file vs new content for', path); } throw e; }","preventionTips":["Keep Evalbook records deterministic (no timestamps or unstable ordering)","Render new attempts to new paths, never rewrite existing ones","Diff before writing when data may have changed"],"tags":["immutability","filesystem","evalbook"],"backgroundTag":"internal-invariant-violation","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}