{"record":{"id":"c8bd76f6381dd02c","repo":"paperclipai/paperclip","slug":"missing-public-viewer-asset-file","errorCode":null,"errorMessage":"Missing public viewer asset: ${file}","messagePattern":"Missing public viewer asset: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs","lineNumber":226,"sourceCode":"        root,\n        ...file.split(\"/\").slice(0, -1),\n        ...clean.split(\"/\"),\n      );\n      const rel = relative(root, target).split(sep).join(\"/\");\n      if (\n        !isPublicProtocolEvalPath(rel) ||\n        !(await lstat(target).catch(() => null))?.isFile()\n      ) {\n        throw new Error(\n          `Public report contains a broken or unsafe link in ${file}: ${href}`,\n        );\n      }\n    }\n  }\n  if (viewer) {\n    for (const file of viewer.files.keys())\n      if (!files.includes(file))\n        throw new Error(`Missing public viewer asset: ${file}`);\n    if (files.some((file) => /^attempts\\/[^/]+\\.html$/.test(file)))\n      throw new Error(\n        \"Chat Evalbook must not mix in legacy plain attempt pages\",\n      );\n  }\n  const campaign = await loadObject(join(root, \"campaign.json\"));\n  if (\n    campaign.schema !== \"paperclip.runner-protocol-eval.campaign/v1\" ||\n    !SAFE_CAMPAIGN.test(String(campaign.campaignId ?? \"\"))\n  ) {\n    throw new Error(\"Public report campaign metadata is invalid\");\n  }\n  return { files, campaign };\n}\n\nexport async function createProtocolEvalBundleManifest(\n  reportRoot,\n  campaignId,","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/publish-runner-protocol-eval-history.mjs#L208-L244","documentation":"Thrown by validatePublicProtocolEvalReport when the report uses the chat rich-attempt viewer (detected via attempts/<id>/index.html pages) but one or more files required by the trusted viewer build (viewer.files from trustedViewerFiles(viewerRoot)) are absent from the report directory. The published bundle must ship the complete viewer asset set so the rich pages render; a partial viewer copy is rejected. The related check also rejects mixing legacy attempts/<id>.html pages into a chat evalbook.","triggerScenarios":"Publishing a report containing attempts/<id>/index.html pages where the viewer/ directory is missing some asset listed by the trusted viewer build (e.g. viewer/assets/app-abc123.js), typically because an old viewerRoot was used or the copy step skipped files.","commonSituations":"PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR points at a stale viewer build whose hashed asset names no longer match the copied viewer/ directory; the report was generated by a different run than the viewer build; an rsync/copy step excluded viewer assets; a manually assembled report omits some assets.","solutions":["Regenerate the report and copy the full viewer/ directory from the same trustedViewerFiles(viewerRoot) build used at publish time.","Ensure PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR points to the viewer build matching the report's generation run.","Remove any stale viewer/ directory before copying the fresh one so hashed asset names stay consistent.","If legacy attempts/<id>.html files exist alongside rich pages, delete or migrate them to attempts/<id>/index.html form."],"exampleFix":"// before: partial copy\naws s3 cp report/ s3://bucket/prefix/ --recursive --exclude \"viewer/assets/*.woff2\"\n// after: copy the full report including all viewer assets\naws s3 cp report/ s3://bucket/prefix/ --recursive","handlingStrategy":"validation","validationCode":"import { trustedViewerFiles } from \"./public-eval-viewer.mjs\";\nimport { readdir } from \"node:fs/promises\";\nexport async function assertViewerAssetsPresent(reportRoot, viewerRoot) {\n  const viewer = await trustedViewerFiles(viewerRoot);\n  const present = new Set(await readdir(`${reportRoot}/viewer/assets`));\n  for (const file of viewer.files.keys()) {\n    if (file.startsWith(\"viewer/\") && !present.has(file.slice(\"viewer/assets/\".length)))\n      throw new Error(`Missing viewer asset: ${file}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await validatePublicProtocolEvalReport(reportRoot, { viewerRoot });\n} catch (err) {\n  if (String(err.message).startsWith(\"Missing public viewer asset\")) {\n    console.error(\"Copy the complete viewer/ directory from the same build referenced by PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR.\");\n  }\n  throw err;\n}","preventionTips":["Always generate the report and copy viewer assets in the same CI step so hashed asset names match.","Delete stale viewer/ directories before copying a fresh viewer build.","Pin PAPERCLIP_RUNNER_PROTOCOL_EVAL_VIEWER_DIR to the run's viewer build, not a shared cached path.","Avoid legacy attempts/<id>.html pages in chat evalbooks; use attempts/<id>/index.html only."],"tags":["publish-validation","missing-asset","viewer"],"backgroundTag":"file-not-found","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"}