{"record":{"id":"3e9d388190fcf203","repo":"JuliusBrussee/caveman","slug":"where-shares-more-units-than-either-theme-has","errorCode":null,"errorMessage":"${where} shares more units than either theme has","messagePattern":"(.+?) shares more units than either theme has","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":88,"sourceCode":"      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    }\n  }\n  relationshipCount += report.relationships.length;\n\n  // A motif is a structural count over variants this report carries, so every\n  // part of it must be re-derivable from those variants.\n  const variantsByID = new Map(report.workflow_variants.map((variant) => [variant.id, variant]));","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L70-L106","documentation":"A relationship's shared_unit_count cannot exceed the number of analysis units either endpoint theme has: since shared units are by definition a subset of each theme's units, the validator throws when shared_unit_count > theme_a_unit_count or > theme_b_unit_count. This count-consistency check keeps relationship arithmetic (and the probabilities derived from it) re-derivable.","triggerScenarios":"Running the validator on a report where relationship.shared_unit_count is greater than relationship.theme_a_unit_count or relationship.theme_b_unit_count — e.g. unit counts recomputed after a theme's unit set shrank but the shared count did not.","commonSituations":"Theme unit counts recomputed after pruning analysis units while relationship counts stayed stale; off-by-one or wrong-denominator bugs in the overlap detector; hand-written fixtures with invented counts.","solutions":["Recompute shared_unit_count as the actual size of the intersection of the two themes' analysis-unit sets.","Update theme_a_unit_count / theme_b_unit_count to current theme unit-set sizes if those are the stale values.","Regenerate the relationship from the canonical span fixture rather than hand-maintaining counts.","Re-run the validator to confirm all count invariants (and the derived probabilities) hold."],"exampleFix":"// before\n{ \"shared_unit_count\": 9, \"theme_a_unit_count\": 5, \"theme_b_unit_count\": 12 }\n// after\n{ \"shared_unit_count\": 4, \"theme_a_unit_count\": 5, \"theme_b_unit_count\": 12 }","handlingStrategy":"validation","validationCode":"if (report.relationships.some((r) => r.shared_unit_count > Math.min(r.theme_a_unit_count, r.theme_b_unit_count))) {\n  throw new Error(\"shared_unit_count exceeds a theme's unit count\");\n}","typeGuard":"const countsAreFeasible = (r) => r.shared_unit_count <= r.theme_a_unit_count && r.shared_unit_count <= r.theme_b_unit_count;","tryCatchPattern":"try {\n  await runValidator([reportPath, spansPath]);\n} catch (err) {\n  if (String(err.message).includes(\"shares more units than either theme has\")) {\n    console.error(\"Recompute shared_unit_count from the intersection of the two themes' unit sets.\");\n  }\n  throw err;\n}","preventionTips":["Compute counts from unit-set intersections, never store them by hand.","Recompute relationship counts whenever theme unit sets change.","Clamp or assert shared <= min(aCount, bCount) in the overlap detector itself."],"tags":["validation","fixtures","count-consistency","relationships"],"backgroundTag":"value-out-of-range","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"}