Classification
- Change type: correctness fix + provenance improvement
- Priority: P0
- Expected value: high — prevents silent candidate loss and protects S8/S9 lineage semantics
- Estimated effort: low-medium
- Implementation risk: low
- Recommendation: YES, immediate improvement; small scope with disproportionate correctness value
Problem
rerank_candidates() currently scores candidates through filter_map. When candidate_axes() fails, the candidate is silently removed from the returned ranking.
That creates several bad states:
- input and output cardinality no longer match;
- the caller cannot distinguish measurement failure from policy rejection;
- S8 cannot explain why a candidate disappeared;
- S9 may treat absence as if the user rejected the candidate;
- future Evolution Lab lineage loses children without a recorded cause;
- aggregate metrics can be biased because unmeasurable candidates vanish from the denominator.
A candidate should not disappear like a witness after an unusually persuasive legal consultation.
Goal
Preserve one explicit evaluation outcome for every input candidate.
Replace silent filtering with a typed result model such as:
pub struct CandidateEvaluation {
pub candidate: SetCandidate,
pub outcome: CandidateOutcome,
}
pub enum CandidateOutcome {
Ranked {
axes: Axes,
aggregate: f64,
rank: usize,
},
Rejected {
reason: CandidateRejection,
},
}
pub enum CandidateRejection {
ClosureMeasurement(ClosureIssue),
NoveltyMeasurement(NoveltyIssue),
InvalidScore(ScoreInvariant),
}
Exact type names are reviewable. The invariant is not.
Required semantics
- Every input candidate yields exactly one
CandidateEvaluation.
- Ranked candidates receive a deterministic rank.
- Rejected candidates retain strategy, seed, gesture and score provenance.
- Rejection reason is typed and inspectable; no string-only error bucket.
- Ranking order is computed only among ranked candidates, while the full evaluated population remains available.
- Rejection is not confused with human dislike/reject feedback.
- Metrics report both ranked and rejected counts.
- Existing deterministic tie-breaking remains unchanged for successfully ranked candidates.
API compatibility
A staged approach is acceptable:
new canonical API: evaluate_and_rerank_candidates(...)
compat helper: rerank_candidates(...) -> ranked-only projection
The compatibility helper must be documented as a projection that discards explicit rejection records, not remain the only product surface.
S8/S9-facing code should consume the canonical full evaluation result.
Required controls
- N valid candidates → N evaluations, all ranked.
- One candidate fails closure measurement → N evaluations, one typed rejection.
- One candidate fails novelty measurement → distinct typed rejection.
- Ranked subset order matches the current implementation for all-success fixtures.
- Rejected candidate retains exact strategy/seed/gesture provenance.
- Re-running fixed inputs is byte-stable after normalization.
- UI/provenance projection shows rejection reason without recomputing scoring.
- Population metrics include rejected candidates and rejection-rate breakdown.
Acceptance
Non-goals
- No new scoring axes or tuned weights.
- No automatic retry/regeneration of rejected candidates.
- No S9 preference-learning implementation.
- No requirement to display rejected candidates in every UI immediately.
- No broad refactor of
Score validation.
Classification
Problem
rerank_candidates()currently scores candidates throughfilter_map. Whencandidate_axes()fails, the candidate is silently removed from the returned ranking.That creates several bad states:
A candidate should not disappear like a witness after an unusually persuasive legal consultation.
Goal
Preserve one explicit evaluation outcome for every input candidate.
Replace silent filtering with a typed result model such as:
Exact type names are reviewable. The invariant is not.
Required semantics
CandidateEvaluation.API compatibility
A staged approach is acceptable:
The compatibility helper must be documented as a projection that discards explicit rejection records, not remain the only product surface.
S8/S9-facing code should consume the canonical full evaluation result.
Required controls
Acceptance
filter_mapdisappearance is removed from the canonical path.Non-goals
Scorevalidation.