{"record":{"id":"37a01ddbc2376bf5","repo":"paperclipai/paperclip","slug":"live-roster-rosterid-repeats-a-case","errorCode":null,"errorMessage":"Live roster ${rosterId} repeats a case","messagePattern":"Live roster (.+?) repeats a case","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs","lineNumber":164,"sourceCode":"    if (selected && !selected.has(rosterId) && !selected.has(rosterFile)) {\n      continue;\n    }\n    if (roster.schema !== \"paperclip-runner/live-roster/v1\") {\n      throw new Error(`Unsupported live roster schema in ${rosterFile}`);\n    }\n    if (!Array.isArray(roster.cases) || roster.cases.length === 0) {\n      throw new Error(`Live roster ${rosterId} has no cases`);\n    }\n    const configPath = inside(\n      programRoot,\n      resolve(rosterRoot, String(roster.config ?? \"\")),\n      `Config for ${rosterId}`,\n    );\n    const config = await loadObject(configPath);\n    const credentialName = credentialForConfig(config);\n    const cases = roster.cases.map((caseId) => safeId(caseId, \"case ID\"));\n    if (new Set(cases).size !== cases.length) {\n      throw new Error(`Live roster ${rosterId} repeats a case`);\n    }\n    rosters.push({\n      rosterId,\n      rosterFile,\n      configFile: relative(programRoot, configPath).split(sep).join(\"/\"),\n      model: String(roster.model ?? config.model ?? \"unknown\"),\n      provider: String(config.provider ?? \"codex\"),\n      driver: String(config.driver ?? \"codex_app_server\"),\n      credentialName,\n      cases,\n    });\n  }\n  if (selected) {\n    const found = new Set(\n      rosters.flatMap((roster) => [roster.rosterId, roster.rosterFile]),\n    );\n    const missing = [...selected].filter((entry) => !found.has(entry));\n    if (missing.length > 0) {","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs#L146-L182","documentation":"buildProtocolEvalCatalog maps each roster.cases entry through safeId and then checks whether the resulting list contains duplicates by comparing Set size to array length. Case IDs must be unique within a roster because each becomes a distinct eval cell keyed by '<rosterId>--<caseId>'; a duplicate would silently overwrite or double-run a cell.","triggerScenarios":"A live roster file's 'cases' array contains the same case ID string twice (e.g. [\"case-001\", \"case-001\"]).","commonSituations":"Merging roster files or case lists from two branches without deduplicating; copy-paste adding the same case twice; programmatic generation appending a case that is already listed.","solutions":["Open the roster file and delete the duplicated case ID from 'cases'.","Deduplicate programmatically before running: [...new Set(roster.cases)].","If two distinct scenarios share a name, rename one case ID so each is unique."],"exampleFix":"// before\n\"cases\": [\"case-001\", \"case-002\", \"case-001\"]\n// after\n\"cases\": [\"case-001\", \"case-002\"]","handlingStrategy":"validation","validationCode":"const ids = roster.cases.map(String);\nif (new Set(ids).size !== ids.length) {\n  throw new Error(`${rosterFile}: duplicate case IDs`);\n}","typeGuard":"function casesAreUnique(r) {\n  return Array.isArray(r.cases) && new Set(r.cases).size === r.cases.length;\n}","tryCatchPattern":null,"preventionTips":["Generate case lists programmatically from a single source of truth instead of copy-pasting.","Deduplicate with a Set when merging rosters from branches."],"tags":["validation","duplicate","config"],"backgroundTag":"schema-validation-failed","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"}