Skip to content

The counterfactual question — a notation, not a type (yet) #3

Description

@borisdev

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:

  • enough answers to see a shape. Three or four real ones, from different kinds of component (a threshold, a step, a filter, a guard). One example is not a schema.
  • a case where the prose form failed — someone wrote a counterfactual, it was wrong or unrunnable, and a type would have caught it. Without this, the type is speculation with tests.
  • a decision on evidence being empty. It must mean NOT MEASURED and never render as "supported", same discipline as CausalEdge.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions