Skip to content

Rename Outcome -> EffectCandidate; document its purpose - #380

Closed
iskandr wants to merge 4 commits into
generic-phasing-apifrom
rename-outcome-effect-candidate
Closed

Rename Outcome -> EffectCandidate; document its purpose#380
iskandr wants to merge 4 commits into
generic-phasing-apifrom
rename-outcome-effect-candidate

Conversation

@iskandr

@iskandr iskandr commented May 14, 2026

Copy link
Copy Markdown
Contributor

Stacked on #379 (generic-phasing-api). Merge that first; this PR will rebase to main.

Summary

Three cleanups on the multi-outcome machinery, addressing the "is this AI slop?" diagnosis:

  1. Rename OutcomeEffectCandidate. Same with outcomes_from_candidatescandidates_from_effects. The class isn't an outcome; it's a candidate effect with per-context provenance.

  2. Drop EffectCandidate.description. The field was a pure passthrough to effect.short_description. Producers wanting a richer narrative attach it to the underlying Effect, not the wrapper.

  3. Document why the wrapper exists. New module + class docstring in varcode/outcomes.py (and corrected docstring on MultiOutcomeEffect) explain the actual rationale: the same MutationEffect instance gets surfaced from multiple multi-outcome contexts with different per-context metadata. Concrete example: a splice candidate built by varcode's splice classifier is re-used by the SV annotator with a different source tag and sv_type evidence. Putting metadata on the wrapper instead of the Effect lets the Effect stay shared while the labels diverge.

    Previously the docstring framed candidates as "kept for back-compat with 2.x" — misleading, since it's still the canonical internal data store. Now both candidates (raw Effects) and outcomes (wrapped) are documented as complementary first-class accessors with distinct, legitimate use cases.

  4. Scrub aspirational "isovar" / "exacto" example tags from varcode docstrings. Those integrations don't exist yet; treating them as canonical examples in varcode's docs created a fake dependency.

What changed under the hood

  • Outcome class → EffectCandidate class. Module path unchanged (varcode.outcomes).
  • Outcome.description field removed; make_rna_outcome(..., description=) parameter removed.
  • MultiOutcomeEffect docstring rewritten — no more "back-compat" framing.
  • New design rationale at the top of varcode/outcomes.py.

What didn't change

  • The MultiOutcomeEffect.candidates vs MultiOutcomeEffect.outcomes dual access. Looking closer, the two serve distinct purposes (candidates = raw Effects for plain iteration; outcomes = wrapped for per-context metadata) — collapsing them would force either copies everywhere or verbose access at every consumer. Documented the distinction sharply instead.

Test plan

  • pytest tests/ — 1187 pass, 2 pre-existing skips.
  • Test rename: test_outcomes_from_candidates_tags_sourcetest_candidates_from_effects_tags_source; two tests that read EffectCandidate.description deleted/trimmed.
  • All test files using Outcome import or description= kwarg updated.

Files touched

19 files, +228 / -243 LOC.

References

iskandr added 4 commits May 14, 2026 16:29
…nt fields

The class was clearly named to its users but the docs framed
``MultiOutcomeEffect.candidates`` as "back-compat" while it's
actively the canonical internal data store. Three cleanups here:

1. **Rename ``Outcome`` -> ``EffectCandidate``**, ``outcomes_from_candidates``
   -> ``candidates_from_effects``. The wrapper isn't an "outcome";
   it's a candidate effect with per-context provenance.

2. **Drop ``EffectCandidate.description``** — pure passthrough to
   ``effect.short_description``. Producers wanting a richer narrative
   set it on the underlying Effect, not the wrapper.

3. **Rewrite docstrings** to explain *why* the wrapper exists (the
   same Effect appears in multiple contexts with different metadata —
   e.g. a SpliceOutcomeSet candidate re-surfaced by the SV annotator
   with a different source tag). The wrapper is the cheapest way to
   keep Effects shared while letting labels diverge. ``candidates``
   and ``outcomes`` are documented as two complementary first-class
   accessors, not legacy vs new.

4. **Scrub aspirational "isovar" / "exacto" example tags** from
   varcode docstrings. Those integrations don't exist yet — pointing
   at them as canonical examples implied an integration that hasn't
   shipped.

Hard rename, no deprecation alias. The class was exported as
``varcode.Outcome``; update imports to ``varcode.EffectCandidate``.

1187 tests pass, 2 pre-existing skips. CHANGELOG entry under Breaking.
- Rename varcode/outcomes.py -> varcode/effect_candidates.py and
  tests/test_outcomes.py -> tests/test_effect_candidates.py to match
  the class rename.
- Rename make_rna_outcome -> make_rna_candidate (was the only public
  surface still using the old terminology).
- Rename test methods test_outcome_* -> test_effect_candidate_*.
- Add test pinning that EffectCandidate(description=...) raises
  TypeError so the removed kwarg can't sneak back via copy/paste.
- CHANGELOG entry now links #380 directly instead of using a
  placeholder reference.

The internal _with_extra_outcomes hook and _extra_outcomes storage
slot keep their names — they hold EffectCandidate (i.e. outcomes-
shaped) entries, so the existing names accurately describe the
data being stored.

1188 pass, 2 pre-existing skips.
The class was named EffectCandidate but lived in eff.outcomes — an
inversion that read as messy. Realign so the class name matches the
accessor it returns from. Naming is now consistent throughout:

* "candidates" everywhere -> raw MutationEffect objects (subclass
  internal storage, the .candidates accessor)
* "outcomes" everywhere -> the wrapped form (EffectOutcome class,
  the .outcomes accessor, _extra_outcomes slot, observed_outcomes
  protocol method)

Renames:
* EffectCandidate -> EffectOutcome
* candidates_from_effects -> outcomes_from_effects
* varcode.effect_candidates -> varcode.effect_outcomes (module)
* tests/test_effect_candidates.py -> tests/test_effect_outcomes.py
* tests/test_effect_candidate_* -> tests/test_effect_outcome_*

The make_rna_candidate -> make_rna_outcome change from the prior
commit is also reverted in this pass (it's an outcome producer; the
"outcome" terminology is correct after this realignment).

1188 tests pass, 2 pre-existing skips.
You disliked "outcome" naming on the wrapper. Reverting to
EffectCandidate. The collapse to a single accessor was tried but
broke ~30 splice/RNA tests because SpliceOutcomeSet.candidates has a
deep history of returning SpliceCandidate (a different type), and
serialization round-trips through that name; collapsing forces
either widespread test migration or back-compat shims that defeat
the cleanup.

Final state on this PR:
- Class: EffectCandidate (has .effect, .source, .probability, .evidence)
- MultiOutcomeEffect.candidates -> tuple[MutationEffect, ...] (raw)
- MultiOutcomeEffect.outcomes -> tuple[EffectCandidate, ...] (wrapped)
- The accessor/type mismatch is documented as two complementary
  first-class accessors. Not as clean as fully aligned, but real.

1188 pass.
@iskandr
iskandr deleted the branch generic-phasing-api May 14, 2026 21:43
@iskandr iskandr closed this May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant