Repository navigation
validate should surface parse errors as a third outcome rather than propagating exceptions #1358
Description
Parent epic: #929
Phase: deferred (start after Phase 2 / IOContract consolidation is further along)
Depends on: #1516
Blocks: —
Background
PR #1138 changed requirement_check_to_bool (the output_to_bool function used by
ALoraRequirement) to raise AdapterSchemaMismatchError when the adapter output doesn't
match the expected {"requirement_check": {"score": <float>}} schema. Previously it
silently returned False, which was wrong — schema drift should be loud, not silently
treated as "requirement not met."
As a short-term fix, Requirement.validate() now documents that output_to_bool may
raise and that callers must handle it separately. But this leaves a sharp edge: the public
contract of validate() is "returns ValidationResult", and throwing is a third outcome
that isn't reflected in the type.
Problem
ValidationResult is a strict boolean: result: bool. There is no way for validate()
to return "the adapter produced unrecognizable output" without either:
- raising an exception (current state — undocumented surprise for callers)
- returning
False(old state — silently wrong)
Both are bad. Any caller that does if await req.validate(backend, ctx): will silently
swallow a parse error in the old design, or crash unexpectedly in the new one.
Proposed solution
Add an error/unknown state to ValidationResult so validate() can express "parse
failed" without raising. Concretely:
- Add
error: Exception | Nonefield (or promotePartialValidationResult's "unknown"
tri-state) toValidationResult - When
output_to_boolraises, catch it insidevalidate(), store the exception on the
result, and return rather than propagating - Callers that check
result.errorget the full exception; callers that only check
bool(result)getFalse— which is at least honest - Consider making
bool(result)raise whenerroris set, to force callers to handle
it explicitly
Why deferred
The fix is correct but touches the broader public API surface of ValidationResult.
Deferred until the IOContract consolidation in Phase 2 (#1516) is further along, since
that work may naturally provide the right abstraction for a tri-state result.
Affected code
mellea/core/requirement.py—ValidationResult,Requirement.validate()mellea/stdlib/requirements/requirement.py—requirement_check_to_bool,
ALoraRequirement
Acceptance criteria
-
ValidationResultcan express a third "parse error" state without raising -
validate()catchesAdapterSchemaMismatchErrorinternally and stores it on the
result rather than propagating -
bool(result)returnsFalsewhenerroris set (callers that ignore errors
fail closed, not silently) - Callers can inspect
result.errorto distinguish "requirement not met" from
"parse failed" -
ALoraRequirement.output_to_bool/requirement_check_to_boolpath exercises
the new state end-to-end - Existing callers that only check
bool(result)continue to work without changes - Type annotations updated;
mypyclean -
ruff format,ruff checkclean
Test plan
test_validate_returns_error_state_on_schema_mismatch— inject a schema-mismatched
output; assertresult.erroris set andbool(result)isFalsetest_validate_happy_path_unaffected— existing passing/failing requirement tests
still pass unchangedtest_bool_result_false_when_error_set—bool(result)isFalsewhenerroris
set, regardless ofresult.resulttest_callers_can_distinguish_failure_from_error— a caller checkingresult.error
sees the originalAdapterSchemaMismatchErrorinstance
References
- PR refactor(intrinsics): requirement_check migration — loud schema-mismatch, no more silent False (Epic #929 Phase 1) #1138 — where
AdapterSchemaMismatchErrorwas introduced and the deferral noted - refactor(intrinsics): resolve the adapter's output contract from the adapter, not a parallel argument (Epic #929 Phase 2) #1516 — IOContract consolidation; the natural predecessor
mellea/core/requirement.py—ValidationResult,Requirement.validate()mellea/stdlib/requirements/requirement.py—requirement_check_to_bool
Activity