{"record":{"id":"899daac68d987e32","repo":"JuliusBrussee/caveman","slug":"report-fixture-reportpaths-index-theme-theme-id-transition-899daa","errorCode":null,"errorMessage":"report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with its predecessor list","messagePattern":"report fixture (.+?): theme (.+?) transition (.+?) disagrees with its predecessor list","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":43,"sourceCode":"const 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])) {\n        throw new Error(`${where} ${key} ${opportunity[key]} is not a workflow variant of this report`);\n      }\n    }","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L25-L61","documentation":"This validator enforces that a theme's lineage.transition value agrees with its lineage.predecessor_ids list: a theme marked \"new\" must have an empty predecessor list, and a theme carrying any other transition (e.g. \"continues\") must list at least one predecessor. It throws when the boolean isNew does not match (predecessor_ids.length === 0), i.e. a new theme lists predecessors or a continuing theme lists none. The report schema cannot express this cross-field rule, so the script checks it after AJV schema validation.","triggerScenarios":"Running validate-continuous-improvement.mjs on a report fixture JSON where a theme has lineage.transition === \"new\" but theme.lineage.predecessor_ids is non-empty, or transition !== \"new\" (e.g. \"continues\") with predecessor_ids: [].","commonSituations":"Hand-editing a report fixture and adding a transition without clearing predecessors; a report generator defaulting predecessor_ids to [] while setting transition to \"continues\"; schema drift after adding the lineage block to older fixtures that only had a registry id.","solutions":["Open the named fixture and set lineage.predecessor_ids to [] for themes whose lineage.transition is \"new\".","For continuing themes, populate lineage.predecessor_ids with the registry ids of the earlier themes this theme continues (at minimum its own registry_id).","If the theme really is new, change lineage.transition to \"new\" instead of leaving predecessors attached.","Re-run the validator: node packages/shared/contracts/scripts/validate-continuous-improvement.mjs <reports...> <spansPath>."],"exampleFix":"// before\n\"lineage\": { \"transition\": \"continues\", \"predecessor_ids\": [] }\n// after\n\"lineage\": { \"transition\": \"continues\", \"predecessor_ids\": [\"theme-auth-retry-v1\"] }","handlingStrategy":"validation","validationCode":"function lineagePredecessorsConsistent(theme) {\n  const isNew = theme.lineage.transition === \"new\";\n  return isNew === (theme.lineage.predecessor_ids.length === 0);\n}\nif (!report.themes.every(lineagePredecessorsConsistent)) {\n  throw new Error(\"theme transition disagrees with predecessor list\");\n}","typeGuard":"const isNewTheme = (theme) => theme.lineage.transition === \"new\" && Array.isArray(theme.lineage.predecessor_ids) && theme.lineage.predecessor_ids.length === 0;","tryCatchPattern":"try {\n  await runValidator([reportPath, spansPath]);\n} catch (err) {\n  if (String(err.message).includes(\"disagrees with its predecessor list\")) {\n    console.error(`Fix lineage.predecessor_ids in ${reportPath}: new themes need none, continuing themes need at least one.`);\n  }\n  throw err;\n}","preventionTips":["Derive transition from the data (predecessor_ids.length > 0 ? \"continues\" : \"new\") instead of storing both independently.","Add a fixture-level unit test asserting the transition/predecessor/registry_id triple agrees.","Never hand-edit lineage fields; regenerate fixtures from the canonical spans."],"tags":["validation","fixtures","schema","lineage"],"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"}