{"record":{"id":"60c0fcc144647de8","repo":"JuliusBrussee/caveman","slug":"report-fixture-reportpaths-index-theme-theme-id-transition","errorCode":null,"errorMessage":"report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with registry_id \"${theme.registry_id}\"","messagePattern":"report fixture (.+?): theme (.+?) transition (.+?) disagrees with registry_id \"(.+?)\"","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":40,"sourceCode":"const ajv = new Ajv2020({ allErrors: true, strict: true });\nconst validateReport = ajv.compile(reportSchema);\nconst validateSpan = ajv.compile(spanSchema);\nconst contiguouslyContains = (haystack, needle) => {\n  for (let start = 0; start + needle.length <= haystack.length; start += 1) {\n    if (needle.every((operation, offset) => haystack[start + offset] === operation)) return true;\n  }\n  return false;\n};\nlet relationshipCount = 0;\nlet motifCount = 0;\nfor (const [index, report] of reports.entries()) {\n  if (!validateReport(report)) throw new Error(`report fixture ${reportPaths[index]}: ${ajv.errorsText(validateReport.errors)}`);\n  for (const theme of report.themes) {\n    // A durable registry id and a predecessor are the same claim: a theme that\n    // continues an earlier one must carry both, and a new theme neither.\n    const isNew = theme.lineage.transition === \"new\";\n    if (isNew !== (theme.registry_id === \"\")) {\n      throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with registry_id \"${theme.registry_id}\"`);\n    }\n    if (isNew !== (theme.lineage.predecessor_ids.length === 0)) {\n      throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with its predecessor list`);\n    }\n    if (!isNew && !theme.lineage.predecessor_ids.includes(theme.registry_id)) {\n      throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} inherited a registry id that is not one of its predecessors`);\n    }\n  }\n\n  // An opportunity names the exact pair of workflow variants it was emitted\n  // from, and a safety finding carries no borrowed dollar figure: copying the\n  // efficiency finding's alternative metrics and expected value would let the\n  // same money be counted twice under two detectors.\n  const variantIds = new Set(report.workflow_variants.map((variant) => variant.id));\n  for (const opportunity of report.opportunities) {\n    const where = `report fixture ${reportPaths[index]}: opportunity ${opportunity.id}`;\n    for (const key of [\"current_variant_id\", \"alternative_variant_id\"]) {\n      if (opportunity[key] && !variantIds.has(opportunity[key])) {","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L22-L58","documentation":"Beyond the JSON Schema, the validator enforces a semantic invariant: `lineage.transition` must agree with `registry_id` and `predecessor_ids`. A theme with transition \"new\" must have an empty registry_id and no predecessors; a continuing theme must carry a non-empty registry_id and at least one predecessor. When the transition value and the registry_id contradict each other, this error is thrown naming the theme and both values.","triggerScenarios":"Running the validator on a report where a theme has e.g. `transition: \"new\"` but a non-empty `registry_id`, or `transition: \"extends\"`/`\"continues\"` with `registry_id: \"\"`. Related sibling errors fire for mismatched predecessor lists and for inherited registry ids not present among predecessors.","commonSituations":"Copy-pasting a theme entry from an earlier report and forgetting to reset lineage fields for a genuinely new theme; manually bumping a transition to \"new\" while leaving the old registry_id; a generator bug emitting transition without clearing predecessors.","solutions":["For a genuinely new theme: set `registry_id: \"\"`, `predecessor_ids: []`, and `transition: \"new\"`.","For a continuing theme: set `registry_id` to its durable id, list that id in `predecessor_ids`, and use a non-new transition.","Check the sibling invariants too: transition vs predecessor list, and registry_id membership in predecessor_ids, to fix all violations in one pass.","Regenerate the report with the current pipeline rather than hand-editing lineage fields."],"exampleFix":"// before\n{ \"id\": \"t3\", \"registry_id\": \"t2\", \"lineage\": { \"transition\": \"new\", \"predecessor_ids\": [] } }\n// after\n{ \"id\": \"t3\", \"registry_id\": \"\", \"lineage\": { \"transition\": \"new\", \"predecessor_ids\": [] } }","handlingStrategy":"validation","validationCode":"function lineageConsistent(theme) {\n  const isNew = theme.lineage.transition === \"new\";\n  return isNew\n    ? theme.registry_id === \"\" && theme.lineage.predecessor_ids.length === 0\n    : theme.registry_id !== \"\" && theme.lineage.predecessor_ids.includes(theme.registry_id);\n}\n// run on every theme before invoking the validator","typeGuard":null,"tryCatchPattern":"try {\n  await validateReports(reportPaths, spansPath);\n} catch (e) {\n  if (String(e.message).includes(\"disagrees with registry_id\")) {\n    const m = String(e.message).match(/theme (\\S+) transition (\\S+) disagrees with registry_id \"(\\S*)\"/);\n    console.error(`Fix theme ${m?.[1]}: transition=${m?.[2]} registry_id=\"${m?.[3]}\"`);\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["When adding a new theme to an updated report, always reset registry_id and predecessor_ids.","When continuing a theme, copy the predecessor's registry_id into both registry_id and predecessor_ids.","Add the lineageConsistent pre-check to fixture generators.","Keep the semantic rules documented next to the schema."],"tags":["validation","semantic-invariant","lineage"],"backgroundTag":"invalid-enum-value","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}