Skip to content

validate should surface parse errors as a third outcome rather than propagating exceptions #1358

Closed

Description

@AngeloDanducci

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 | None field (or promote PartialValidationResult's "unknown"
    tri-state) to ValidationResult
  • When output_to_bool raises, catch it inside validate(), store the exception on the
    result, and return rather than propagating
  • Callers that check result.error get the full exception; callers that only check
    bool(result) get False — which is at least honest
  • Consider making bool(result) raise when error is 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

  • ValidationResult can express a third "parse error" state without raising
  • validate() catches AdapterSchemaMismatchError internally and stores it on the
    result rather than propagating
  • bool(result) returns False when error is set (callers that ignore errors
    fail closed, not silently)
  • Callers can inspect result.error to distinguish "requirement not met" from
    "parse failed"
  • ALoraRequirement.output_to_bool / requirement_check_to_bool path exercises
    the new state end-to-end
  • Existing callers that only check bool(result) continue to work without changes
  • Type annotations updated; mypy clean
  • ruff format, ruff check clean

Test plan

  • test_validate_returns_error_state_on_schema_mismatch — inject a schema-mismatched
    output; assert result.error is set and bool(result) is False
  • test_validate_happy_path_unaffected — existing passing/failing requirement tests
    still pass unchanged
  • test_bool_result_false_when_error_set — bool(result) is False when error is
    set, regardless of result.result
  • test_callers_can_distinguish_failure_from_error — a caller checking result.error
    sees the original AdapterSchemaMismatchError instance

References

Activity

  1. added
    enhancementNew feature or request
    p2Medium/low: minor bugs, niche features, polish, docs, tests, cleanup. Scoped, lower urgency.
    area/requirementRequirement base class, validation, repair loops
    area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Core
    p1High: important bugs (workaround exists) or high-value core features. Do soon, not on fire.
    area/stdlibCore abstractions: Context, MOT, SamplingStrategy, formatters, serialization
    and removed
    p2Medium/low: minor bugs, niche features, polish, docs, tests, cleanup. Scoped, lower urgency.
    p1High: important bugs (workaround exists) or high-value core features. Do soon, not on fire.
    on Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/adapter-functionsGranite adapter functions: framework and adaptiers including RAG, Guardian, Corearea/requirementRequirement base class, validation, repair loopsarea/stdlibCore abstractions: Context, MOT, SamplingStrategy, formatters, serializationenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions