{"record":{"id":"6efa04ffabbb2fce","repo":"JuliusBrussee/caveman","slug":"where-adjacency-edge-from-edge-to-appear","errorCode":null,"errorMessage":"${where} adjacency ${edge.from}->${edge.to} appears in both the hard chain and the unproven list","messagePattern":"(.+?) adjacency (.+?)->(.+?) appears in both the hard chain and the unproven list","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":203,"sourceCode":"    if (slice.manifestation_node_id !== \"\" && !slice.node_chain.includes(slice.manifestation_node_id)) {\n      throw new Error(`${where} causal slice omits its own manifestation node`);\n    }\n    const provenDependency = (edge) => edge.strength === \"hard\" && (edge.type === \"data\" || edge.type === \"control\") && edge.evidence_refs.length > 0;\n    const variantEdges = new Map(sliceVariant.edges.map((edge) => [`${edge.from}\u0000${edge.to}\u0000${edge.type}`, edge]));\n    for (const edge of slice.hard_edges) {\n      if (!provenDependency(edge)) throw new Error(`${where} hard chain carries an unproven edge ${edge.from}->${edge.to}`);\n      if (!sliceNodes.has(edge.from) || !sliceNodes.has(edge.to)) throw new Error(`${where} hard chain edge ${edge.from}->${edge.to} names a node outside variant ${sliceVariant.id}`);\n      if (!slice.node_chain.includes(edge.from) || !slice.node_chain.includes(edge.to)) {\n        throw new Error(`${where} hard chain edge ${edge.from}->${edge.to} is outside the slice's node chain`);\n      }\n      const source = variantEdges.get(`${edge.from}\u0000${edge.to}\u0000${edge.type}`);\n      if (!source || source.strength !== \"hard\") throw new Error(`${where} hard chain edge ${edge.from}->${edge.to} is not a hard edge of variant ${sliceVariant.id}`);\n    }\n    for (const edge of slice.unproven_adjacencies) {\n      // The whole point of the separate list: nothing in it may read as proven.\n      if (provenDependency(edge)) throw new Error(`${where} unproven adjacency ${edge.from}->${edge.to} is in fact a proven hard dependency`);\n      if (slice.hard_edges.some((hard) => hard.from === edge.from && hard.to === edge.to && hard.type === edge.type)) {\n        throw new Error(`${where} adjacency ${edge.from}->${edge.to} appears in both the hard chain and the unproven list`);\n      }\n    }\n    const upstream = new Set(item.diagnosis.root_cause_hypothesis.upstream_node_ids);\n    for (const nodeID of upstream) {\n      if (!slice.node_chain.includes(nodeID) || nodeID === slice.manifestation_node_id) {\n        throw new Error(`${where} diagnosis upstream node ${nodeID} is not an upstream node of its causal slice`);\n      }\n    }\n\n    // A case that proposes nothing must carry nothing: no operator, no eval\n    // pack, no replay. Anything else would present an abstention as a proposal.\n    if (item.status === \"diagnosis_only\") {\n      if (item.change_set.operator_id !== \"\" || item.eval_pack.id !== \"\" || item.replay.status !== \"not_run\") {\n        throw new Error(`${where} is diagnosis-only but carries a change set, eval pack or replay`);\n      }\n      continue;\n    }\n    if (item.change_set.case_id !== item.id || item.eval_pack.case_id !== item.id) {","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L185-L221","documentation":"Thrown when the same (from, to, type) edge appears in both causal_slice.hard_edges and causal_slice.unproven_adjacencies. The two lists must be disjoint: an edge cannot simultaneously be a proven hard dependency and an unproven adjacency, because readers could not tell which claim the slice makes.","triggerScenarios":"Adding an edge to unproven_adjacencies while its proven twin remains in hard_edges (or vice versa) — the dedup check compares from, to and type exactly, so only fully identical triples trigger it.","commonSituations":"Hand-editing a slice during diagnosis iteration; merging slices from two drafts; promoting/demoting an edge without deleting the original entry.","solutions":["Decide the edge's status once and keep it in exactly one list: delete it from unproven_adjacencies if it belongs to hard_edges, or from hard_edges if it is a hypothesis.","Audit the slice for other duplicate triples between the two lists (same from/to but different type will NOT be caught here but usually signals a modeling mistake).","Regenerate the slice from the analysis tooling if it was hand-merged."],"exampleFix":"// before\nhard_edges: [{from:\"n1\",to:\"n2\",type:\"data\",...}]\nunproven_adjacencies: [{from:\"n1\",to:\"n2\",type:\"data\",...}]\n\n// after\nhard_edges: [{from:\"n1\",to:\"n2\",type:\"data\",...}]\nunproven_adjacencies: []","handlingStrategy":"validation","validationCode":"const hardKey = (e) => `${e.from}|${e.to}|${e.type}`;\nconst hard = new Set(slice.hard_edges.map(hardKey));\nconst disjoint = slice.unproven_adjacencies.every((e) => !hard.has(hardKey(e)));","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Maintain one source-of-truth edge list per slice with a status field, and derive hard_edges/unproven_adjacencies from it.","Add a unit test that asserts the two lists are key-disjoint before publishing the report."],"tags":["validation","causal-graph","deduplication"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}