Documented at docs/counterfactual.md. This issue tracks whether it should ever become a type.
The rule, as shipped
Pearl writes Y_x(u) — the value Y would take for unit u, had X been x. For a component C:
Y_do(C removed)(u) != Y(u) for which u?
Name the u. Then run it.
Why it is NOT a dataclass
A Counterfactual(claim, unit, observed, evidence=(...)) type was drafted and set aside. Two reasons:
- it would force every component into a shape validated on exactly one example
plan_types already refuses a SemanticInvariant whose check is not callable, on the grounds that a rule that cannot run is a preference. A required prose field is the mirror risk — ceremony, filled with "because we need it".
Ask the question, write the answer in the docstring, and revisit the type once enough answers exist to see their shape. That is .claude/rules/project.md's own rule: add the second implementation first, then extract the interface.
The case that produced it
MIN_SUPPORT = 20 in a case-graph enricher. Docstring claimed "SemRep is noisy; 20 sentences won't repeat a parse bug" and "protects the sparse case".
u |
Y_do(MIN_SUPPORT=0)(u) |
at 20 |
blocked |
| PCOS, dense |
5 nodes, support 160–493 |
identical |
nothing |
| aHUS, rare |
5 nodes, support 45–118 |
identical |
nothing |
| Erdheim-Chester |
Cardiac Tamponade(5), Myocarditis(5), Secondary malignancy(5), Pancreatitis(3), Toxic nephropathy(3) |
0 nodes |
all five |
Cardiac tamponade and myocarditis are classic Erdheim-Chester manifestations. The threshold deleted correct, clinically urgent findings — because support 3–5 is the entire literature for a rare disease.
It did nothing on both units where it was claimed to help, harm on the one it was claimed to protect, and its docstring asserted the opposite of its measured effect for as long as it existed. Nobody had run a sparse case, because nothing asked.
Fixed downstream: MIN_SUPPORT 20 → 2 (a floor against a single misparse, which is all a floor legitimately does — relevance was already handled by sort + cap).
What would justify promoting it to a type
Not "it feels rigorous". Concretely:
Until then it stays a question in a doc, and the honest answer to it is frequently "I still have not tested the thing I said" — which is worth more written down than a confident paragraph, because it tells the next person what to run.
Documented at
docs/counterfactual.md. This issue tracks whether it should ever become a type.The rule, as shipped
Pearl writes
Y_x(u)— the valueYwould take for unitu, hadXbeenx. For a componentC:Why it is NOT a dataclass
A
Counterfactual(claim, unit, observed, evidence=(...))type was drafted and set aside. Two reasons:plan_typesalready refuses aSemanticInvariantwhosecheckis not callable, on the grounds that a rule that cannot run is a preference. A required prose field is the mirror risk — ceremony, filled with "because we need it".Ask the question, write the answer in the docstring, and revisit the type once enough answers exist to see their shape. That is
.claude/rules/project.md's own rule: add the second implementation first, then extract the interface.The case that produced it
MIN_SUPPORT = 20in a case-graph enricher. Docstring claimed "SemRep is noisy; 20 sentences won't repeat a parse bug" and "protects the sparse case".uY_do(MIN_SUPPORT=0)(u)Cardiac tamponade and myocarditis are classic Erdheim-Chester manifestations. The threshold deleted correct, clinically urgent findings — because support 3–5 is the entire literature for a rare disease.
It did nothing on both units where it was claimed to help, harm on the one it was claimed to protect, and its docstring asserted the opposite of its measured effect for as long as it existed. Nobody had run a sparse case, because nothing asked.
Fixed downstream:
MIN_SUPPORT20 → 2 (a floor against a single misparse, which is all a floor legitimately does — relevance was already handled by sort + cap).What would justify promoting it to a type
Not "it feels rigorous". Concretely:
evidencebeing empty. It must mean NOT MEASURED and never render as "supported", same discipline asCausalEdge.queries. If that cannot be enforced, the field is worse than the docstring.verdict="refutes"and"no_effect"demonstrably recordable. A field that only ever holds confirmations is the failure this package exists to report.Until then it stays a question in a doc, and the honest answer to it is frequently "I still have not tested the thing I said" — which is worth more written down than a confident paragraph, because it tells the next person what to run.