JuliusBrussee/caveman · error · Error
report fixture : theme transition disagrees with its…
Error message
report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with its predecessor list What it means
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.
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>.
Example fix
// before
"lineage": { "transition": "continues", "predecessor_ids": [] }
// after
"lineage": { "transition": "continues", "predecessor_ids": ["theme-auth-retry-v1"] } Defensive patterns
Strategy: validation
Validate before calling
function lineagePredecessorsConsistent(theme) {
const isNew = theme.lineage.transition === "new";
return isNew === (theme.lineage.predecessor_ids.length === 0);
}
if (!report.themes.every(lineagePredecessorsConsistent)) {
throw new Error("theme transition disagrees with predecessor list");
} Type guard
const isNewTheme = (theme) => theme.lineage.transition === "new" && Array.isArray(theme.lineage.predecessor_ids) && theme.lineage.predecessor_ids.length === 0;
Try / catch
try {
await runValidator([reportPath, spansPath]);
} catch (err) {
if (String(err.message).includes("disagrees with its predecessor list")) {
console.error(`Fix lineage.predecessor_ids in ${reportPath}: new themes need none, continuing themes need at least one.`);
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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: [].
Common situations: 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.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- canonical span
- ${fixtureFiles[index]}…
- report fixture : theme inherited a registry id that is not…
- cave_agent_definition_invalid
- cave_context_ir_invalid
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/899daac68d987e32.
Report an issue: GitHub.
Appendix: source
Thrown at packages/shared/contracts/scripts/validate-continuous-improvement.mjs:43
const contiguouslyContains = (haystack, needle) => {
for (let start = 0; start + needle.length <= haystack.length; start += 1) {
if (needle.every((operation, offset) => haystack[start + offset] === operation)) return true;
}
return false;
};
let relationshipCount = 0;
let motifCount = 0;
for (const [index, report] of reports.entries()) {
if (!validateReport(report)) throw new Error(`report fixture ${reportPaths[index]}: ${ajv.errorsText(validateReport.errors)}`);
for (const theme of report.themes) {
// A durable registry id and a predecessor are the same claim: a theme that
// continues an earlier one must carry both, and a new theme neither.
const isNew = theme.lineage.transition === "new";
if (isNew !== (theme.registry_id === "")) {
throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with registry_id "${theme.registry_id}"`);
}
if (isNew !== (theme.lineage.predecessor_ids.length === 0)) {
throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} transition ${theme.lineage.transition} disagrees with its predecessor list`);
}
if (!isNew && !theme.lineage.predecessor_ids.includes(theme.registry_id)) {
throw new Error(`report fixture ${reportPaths[index]}: theme ${theme.id} inherited a registry id that is not one of its predecessors`);
}
}
// An opportunity names the exact pair of workflow variants it was emitted
// from, and a safety finding carries no borrowed dollar figure: copying the
// efficiency finding's alternative metrics and expected value would let the
// same money be counted twice under two detectors.
const variantIds = new Set(report.workflow_variants.map((variant) => variant.id));
for (const opportunity of report.opportunities) {
const where = `report fixture ${reportPaths[index]}: opportunity ${opportunity.id}`;
for (const key of ["current_variant_id", "alternative_variant_id"]) {
if (opportunity[key] && !variantIds.has(opportunity[key])) {
throw new Error(`${where} ${key} ${opportunity[key]} is not a workflow variant of this report`);
}
}View on GitHub (pinned to 3ee70a1026)