{"record":{"id":"7db5bf7c15a440f2","repo":"JuliusBrussee/caveman","slug":"report-fixture-reportpaths-index-ajv-errorstext","errorCode":null,"errorMessage":"report fixture ${reportPaths[index]}: ${ajv.errorsText(validateReport.errors)}","messagePattern":"report fixture (.+?): (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":34,"sourceCode":"  readFile(path.join(packageRoot, \"schemas\", \"continuous-improvement-report.schema.json\"), \"utf8\").then(JSON.parse),\n  readFile(path.join(packageRoot, \"schemas\", \"canonical-span.schema.json\"), \"utf8\").then(JSON.parse),\n  readFile(spansPath, \"utf8\").then(JSON.parse),\n]);\nconst reports = await Promise.all(reportPaths.map((reportPath) => readFile(reportPath, \"utf8\").then(JSON.parse)));\n\nconst ajv = new Ajv2020({ allErrors: true, strict: true });\nconst validateReport = ajv.compile(reportSchema);\nconst validateSpan = ajv.compile(spanSchema);\nconst 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","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L16-L52","documentation":"Each report fixture is validated against the continuous-improvement-report JSON Schema with Ajv. If `validateReport(report)` fails, the script throws with the fixture path and `ajv.errorsText(...)` describing every schema violation. This ensures reports consumed by the continuous-improvement pipeline conform to the published contract.","triggerScenarios":"Running the validator with a report JSON missing required properties (e.g. `themes` array items lacking `id`, `registry_id`, or `lineage`), wrong types (string instead of enum for `lineage.transition`), additional properties when additionalProperties is false, or a non-object root.","commonSituations":"Hand-authored test fixtures drifting from the schema after a schema version bump; a report generated by an older generator before new required fields (like lineage) were introduced; typos in field names; JSON exported with different casing.","solutions":["Read the `ajv.errorsText` portion of the message — it names the exact JSON path and violated keyword.","Open the report fixture and fix the fields the error path points at.","Validate locally against packages/shared/contracts/schemas/continuous-improvement-report.schema.json (or with an IDE JSON-schema binding) before rerunning.","If the fixture came from a generator, regenerate with the current tool version instead of hand-editing."],"exampleFix":"// before (fixture)\n{ \"themes\": [{ \"id\": \"t1\", \"lineage\": { \"transition\": \"new\" } }] }        // registry_id missing\n// after\n{ \"themes\": [{ \"id\": \"t1\", \"registry_id\": \"\", \"lineage\": { \"transition\": \"new\", \"predecessor_ids\": [] } }] }","handlingStrategy":"validation","validationCode":"import Ajv from \"ajv\";\nimport reportSchema from \"../schemas/continuous-improvement-report.schema.json\" with { type: \"json\" };\nconst ajv = new Ajv({ allErrors: true });\nconst validate = ajv.compile(reportSchema);\nconst report = JSON.parse(await readFile(reportPath, \"utf8\"));\nif (!validate(report)) console.error(`pre-check ${reportPath}:`, ajv.errorsText(validate.errors));","typeGuard":null,"tryCatchPattern":"try {\n  await validateReports([reportPath], spansPath);\n} catch (e) {\n  if (String(e.message).startsWith(\"report fixture\")) {\n    console.error(e.message); // includes ajv.errorsText with exact JSON paths\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Bind the schema in your editor so fixtures are validated as you edit them.","Regenerate fixtures with the current pipeline instead of hand-editing after schema changes.","Run the validator in CI on every fixture change.","Keep a schema-version field in reports to detect stale generators."],"tags":["schema","validation","json"],"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"}