{"record":{"id":"a534ff4da5871c31","repo":"JuliusBrussee/caveman","slug":"where-references-a-theme-that-is-not-in-this-report","errorCode":null,"errorMessage":"${where} references a theme that is not in this report","messagePattern":"(.+?) references a theme that is not in this report","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":82,"sourceCode":"        throw new Error(`${where} compares two workflows without naming both variants`);\n      }\n    }\n    if (opportunity.type === \"safety\") {\n      if (opportunity.alternative_variant_id) throw new Error(`${where} is a safety finding with an alternative variant`);\n      if (opportunity.expected_value !== 0) throw new Error(`${where} is a safety finding carrying expected value ${opportunity.expected_value}`);\n      if (opportunity.alternative_metrics.cost_per_outcome_usd !== null || opportunity.alternative_metrics.eligible_runs !== 0) {\n        throw new Error(`${where} is a safety finding carrying another workflow's metrics`);\n      }\n    }\n  }\n\n  // A relationship is a count, so it must be recomputable from the counts it\n  // carries. Anything a reader cannot re-derive is a claim, not evidence.\n  const themeIds = new Set(report.themes.map((theme) => theme.id));\n  for (const relationship of report.relationships) {\n    const where = `report fixture ${reportPaths[index]}: relationship ${relationship.id}`;\n    if (!themeIds.has(relationship.theme_a_id) || !themeIds.has(relationship.theme_b_id)) {\n      throw new Error(`${where} references a theme that is not in this report`);\n    }\n    if (!(relationship.theme_a_id < relationship.theme_b_id)) {\n      throw new Error(`${where} theme ids are not in canonical order`);\n    }\n    if (relationship.shared_unit_count > relationship.theme_a_unit_count || relationship.shared_unit_count > relationship.theme_b_unit_count) {\n      throw new Error(`${where} shares more units than either theme has`);\n    }\n    if (relationship.shared_unit_ids.length > relationship.shared_unit_count) {\n      throw new Error(`${where} carries more shared unit ids than its shared unit count`);\n    }\n    const expectedBGivenA = relationship.shared_unit_count / relationship.theme_a_unit_count;\n    const expectedAGivenB = relationship.shared_unit_count / relationship.theme_b_unit_count;\n    if (Math.abs(relationship.probability_b_given_a - expectedBGivenA) > 1e-9) {\n      throw new Error(`${where} probability_b_given_a ${relationship.probability_b_given_a} != ${expectedBGivenA}`);\n    }\n    if (Math.abs(relationship.probability_a_given_b - expectedAGivenB) > 1e-9) {\n      throw new Error(`${where} probability_a_given_b ${relationship.probability_a_given_b} != ${expectedAGivenB}`);\n    }","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L64-L100","documentation":"Each relationship is an edge between exactly two themes of the same report, so the validator builds a Set of report.themes ids and throws if either relationship.theme_a_id or relationship.theme_b_id is missing from it. This referential-integrity check ensures every relationship can be recomputed from evidence the report itself carries.","triggerScenarios":"Running the validator on a report where a relationship references a theme id not present in report.themes — e.g. after a theme was dropped from the report or its id renamed.","commonSituations":"Pruning low-support themes without pruning their relationships; theme-id renames during fixture regeneration; merging relationships from a different report's theme set.","solutions":["Fix relationship.theme_a_id / theme_b_id to ids present in report.themes.","Delete relationships whose themes were removed from the report (and re-check the relationship count stays non-zero across fixtures).","If the theme belongs in the report, re-add it to report.themes rather than leaving a dangling edge.","Re-run the validator to confirm all relationship endpoints resolve."],"exampleFix":"// before\n{ \"id\": \"rel-1\", \"theme_a_id\": \"theme-retry-old\", \"theme_b_id\": \"theme-guard\" }\n// after\n{ \"id\": \"rel-1\", \"theme_a_id\": \"theme-retry\", \"theme_b_id\": \"theme-guard\" }","handlingStrategy":"validation","validationCode":"const themeIds = new Set(report.themes.map((t) => t.id));\nconst dangling = report.relationships.filter((r) => !themeIds.has(r.theme_a_id) || !themeIds.has(r.theme_b_id));\nif (dangling.length) throw new Error(`relationships reference unknown themes: ${dangling.map((r) => r.id)}`);","typeGuard":"const endpointsResolved = (r, report) => { const ids = new Set(report.themes.map((t) => t.id)); return ids.has(r.theme_a_id) && ids.has(r.theme_b_id); };","tryCatchPattern":"try {\n  await runValidator([reportPath, spansPath]);\n} catch (err) {\n  if (String(err.message).includes(\"references a theme that is not in this report\")) {\n    console.error(\"Drop or re-point relationships whose themes are absent from report.themes.\");\n  }\n  throw err;\n}","preventionTips":["Prune relationships in the same commit that prunes themes.","Derive relationship endpoints from the theme set at serialization time.","Run this validator in CI before committing any regenerated fixture."],"tags":["validation","fixtures","referential-integrity","relationships"],"backgroundTag":"entity-not-found","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"}