{"record":{"id":"3e6c0ac4fb9a736f","repo":"JuliusBrussee/caveman","slug":"where-diagnosis-upstream-node-nodeid-is-not","errorCode":null,"errorMessage":"${where} diagnosis upstream node ${nodeID} is not an upstream node of its causal slice","messagePattern":"(.+?) diagnosis upstream node (.+?) is not an upstream node of its causal slice","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/shared/contracts/scripts/validate-continuous-improvement.mjs","lineNumber":209,"sourceCode":"      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) {\n      throw new Error(`${where} change set or eval pack is bound to another case`);\n    }\n\n    // The case must investigate the pair its own opportunity named. A cohort\n    // built from a differently-derived pair would describe one comparison and\n    // measure another.","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/shared/contracts/scripts/validate-continuous-improvement.mjs#L191-L227","documentation":"Thrown when a node in diagnosis.root_cause_hypothesis.upstream_node_ids is either absent from causal_slice.node_chain or equals the slice's manifestation_node_id. Upstream nodes are the hypothesized causes; by definition they must sit earlier in the chain than the manifestation and must not be the manifestation itself.","triggerScenarios":"Listing a manifestation node as its own upstream cause; naming a node from another variant or a node dropped from the slice's chain; stale upstream ids after the node_chain was revised.","commonSituations":"Updating node_chain (e.g., trimming it to the baseline variant) without updating the diagnosis hypothesis; reusing node ids from a different workflow revision.","solutions":["Remove or fix upstream ids that are not in slice.node_chain — every entry must be a chain member other than manifestation_node_id.","If a genuinely causal node was dropped from the chain, add it back to node_chain (it must be a node of the slice's variant).","Delete manifestation_node_id from upstream_node_ids if it was listed by mistake."],"exampleFix":"// before\nnode_chain: [\"fetch\",\"parse\",\"emit\"]\nmanifestation_node_id: \"emit\"\nupstream_node_ids: [\"parse\",\"emit\"]\n\n// after\nupstream_node_ids: [\"parse\"]","handlingStrategy":"validation","validationCode":"const ok = item.diagnosis.root_cause_hypothesis.upstream_node_ids.every(\n  (id) => slice.node_chain.includes(id) && id !== slice.manifestation_node_id\n);","typeGuard":"function isUpstreamChainNode(id, slice) {\n  return slice.node_chain.includes(id) && id !== slice.manifestation_node_id;\n}","tryCatchPattern":null,"preventionTips":["Derive upstream_node_ids by filtering node_chain instead of authoring them independently.","When trimming a node_chain, re-derive the diagnosis hypothesis in the same edit."],"tags":["validation","diagnosis","causal-graph"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}