{"record":{"id":"51bc509842b80054","repo":"JuliusBrussee/caveman","slug":"report-fixture-reportpaths-index-theme-theme-id-inherited-a","errorCode":null,"errorMessage":"report fixture ${reportPaths[index]}: theme ${theme.id} inherited a registry id that is not one of its predecessors","messagePattern":"report fixture (.+?): theme (.+?) inherited a registry id that is not one of its predecessors","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":46,"sourceCode":"  }\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])) {\n        throw new Error(`${where} ${key} ${opportunity[key]} is not a workflow variant of this report`);\n      }\n    }\n    if (opportunity.detector_id === \"dominated-workflow\") {\n      if (!opportunity.current_variant_id || !opportunity.alternative_variant_id) {\n        throw new Error(`${where} compares two workflows without naming both variants`);","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L28-L64","documentation":"A continuing theme must inherit its registry id from one of the themes it continues: the validator requires that for any theme whose transition is not \"new\", theme.lineage.predecessor_ids includes theme.registry_id. It throws when a theme claims a durable registry id that appears nowhere in its predecessor list, meaning the inherited identity cannot be traced to any ancestor. This prevents a fixture from silently rebinding a registry id to an unrelated lineage.","triggerScenarios":"Running the validator on a report fixture where theme.lineage.transition !== \"new\" (registry_id non-empty) but theme.lineage.predecessor_ids does not contain theme.registry_id — e.g. predecessors reference different theme ids than the inherited registry id.","commonSituations":"Renaming theme ids when regenerating fixtures without updating predecessor lists; copying a theme entry and changing registry_id but not its predecessors; merging reports where lineage chains were rewritten.","solutions":["Open the fixture and add the theme's registry_id to its lineage.predecessor_ids array.","Alternatively, correct registry_id to the actual id of one of the listed predecessors.","If the id and predecessors are both stale, regenerate the report fixture from the canonical span fixture instead of hand-editing.","Re-run the validator to confirm all three lineage invariants pass."],"exampleFix":"// before\n\"registry_id\": \"theme-retry-v2\", \"lineage\": { \"transition\": \"continues\", \"predecessor_ids\": [\"theme-elsewhere-v1\"] }\n// after\n\"registry_id\": \"theme-retry-v2\", \"lineage\": { \"transition\": \"continues\", \"predecessor_ids\": [\"theme-retry-v1\"] }","handlingStrategy":"validation","validationCode":"function registryIdTracedToPredecessor(theme) {\n  return theme.lineage.transition === \"new\" ||\n    theme.lineage.predecessor_ids.includes(theme.registry_id);\n}\nif (!report.themes.every(registryIdTracedToPredecessor)) {\n  throw new Error(\"inherited registry_id missing from predecessor_ids\");\n}","typeGuard":"const hasTracedLineage = (theme) => theme.lineage.transition === \"new\" || theme.lineage.predecessor_ids.includes(theme.registry_id);","tryCatchPattern":"try {\n  await runValidator([reportPath, spansPath]);\n} catch (err) {\n  if (String(err.message).includes(\"inherited a registry id that is not one of its predecessors\")) {\n    console.error(\"Add the theme's registry_id to lineage.predecessor_ids or fix the id.\");\n  }\n  throw err;\n}","preventionTips":["When assigning a registry_id to a continuing theme, always copy it from the chosen predecessor in the same code path.","Keep a lineage chain test that walks predecessor_ids and asserts registry_id membership.","Regenerate reports rather than editing registry_id/predecessors by hand."],"tags":["validation","fixtures","lineage","referential-integrity"],"backgroundTag":"schema-validation-failed","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"}