Skip to content

Harden input validation and error handling across CLI, adapters, and runner - #1

Merged
nuk3s merged 5 commits into
mainfrom
claude/zen-wright-16taik
Sep 25, 2026
Merged

nuk3s merged 5 commits into
mainfrom
claude/zen-wright-16taik

Conversation

@nuk3s

@nuk3s nuk3s commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

This PR significantly improves robustness by adding comprehensive input validation at load time and better error handling throughout the codebase.

Summary

The changes add early validation of corpus, gold standard, and adversarial probe files to catch configuration errors before they cause runtime failures. HTTP adapter code is consolidated into a shared module to ensure consistent error reporting. The readiness polling is improved to sample across the entire corpus rather than just the head. Query retry logic is refined to avoid retrying client errors that will fail identically.

Key Changes

Input Validation

  • Added load_corpus() and load_gold() functions that validate row structure at load time, reporting line numbers and specific missing fields
  • Added validate_probe() for adversarial probes, checking corpus/checks/corrections structure and regex validity before the run starts
  • Enhanced load_jsonl() to report line numbers on JSON decode errors
  • All validation failures now exit before any writes to the store, preventing orphaned run logs

HTTP Adapter Consolidation

  • Created new memeval/adapters/_http.py module with shared request_json() helper
  • Refactored Hindsight, Mem0, Letta, Zep, and ExampleRest adapters to use the shared module
  • Ensures HTTP error responses include the server's error body (up to 200 chars) in all adapters for better debugging

Runner Improvements

  • Added _spread() function to sample corpus docs evenly across the entire range (first and last always included) rather than just the head, catching silent write losses at the tail
  • Refined retry logic with _retryable() to distinguish between retryable errors (5xx, 408, 425, 429) and client errors (4xx) that fail identically
  • Updated readiness poll to use the spread sampling strategy

CLI and Event Handling

  • Updated cli.py to use new load_corpus() and load_gold() functions
  • Fixed cmd_adapters() to handle adapters with empty docstrings without IndexError
  • Added read_events() robustness to skip non-object lines in run logs (arrays, scalars, etc.)
  • Added python -m memeval entry point via __main__.py

Test Coverage

  • Added comprehensive tests for malformed input files (corpus, gold, probe)
  • Added tests for HTTP error handling across all adapters
  • Added tests for readiness polling with spread sampling
  • Added tests for retry logic distinguishing retryable vs non-retryable HTTP errors
  • Added tests for negation handling in adversarial checks
  • Added tests for CLI robustness (missing files, empty docstrings, etc.)

Dependencies

  • Updated Textual requirement from >=0.80 to >=2.0
  • Added dev optional dependency group with pytest and pytest-asyncio
  • Updated CI to test against Python 3.10-3.13

Notable Implementation Details

  • Hindsight batch size now has a minimum of 1 to prevent range() errors
  • Readiness polling now samples up to 10 docs spread evenly across the corpus, ensuring tail batches are validated
  • HTTP error bodies are truncated to 200 chars to keep error messages readable
  • Malformed answer regexes in gold files remain runtime errors (per README contract) rather than load-time rejections
  • All validation errors include row numbers for easy debugging

`memeval[tui]` declared textual>=0.80, but render/tui.py imports
textual.markup, which first shipped in textual 2.0.0. Installing the
extra at the declared floor gave ModuleNotFoundError (verified against
0.80.0, 1.0.0 and 2.0.0). matplotlib>=3.8 was verified good as is.

Add a `dev` extra (pytest, pytest-asyncio, ruff) so CI and contributors
install the same thing, and run the test job on every supported minor
(requires-python is >=3.10; CI only exercised 3.12).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc5DZiN4NhcQ9mBX2SL2yP
…rors

wait_ready checked only the first 10 docs. Stores retain in batches
(Hindsight: 20 per POST), so a batch dropped at the tail of the 125-doc
bundled corpus was invisible to the very check that exists to catch
silent write loss. Sample up to 10 docs spread evenly across the corpus,
first and last always included. The `#i` fallback label in `missing`
stays the corpus index.

Runner.query retried on OSError, and urllib's HTTPError is an OSError,
so a 401/404/422 was re-issued --retries times per query: no chance of
success, an inflated latency on the error row, and N extra requests to
a misconfigured store. Retry HTTPError only for 5xx/408/425/429.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc5DZiN4NhcQ9mBX2SL2yP
A foreign .jsonl in runs/ whose line parses to an array or scalar took
down `memeval runs`, `memeval chart` and the replay TUI with
AttributeError on .get(). Every event is an object, so drop anything
else the same way a truncated line is dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc5DZiN4NhcQ9mBX2SL2yP
…emeval

A gold row without `answer`, a corpus row without `content`, or a probe
without `checks` was a KeyError traceback after retain had already
written into the store. load_corpus/load_gold/validate_probe now reject
malformed input with a row number before anything is written, and
load_jsonl reports the line of a JSON syntax error. A malformed gold
regex is still an error row at query time, per the README contract;
probe regexes are compiled at load since the probe is one small file.

`memeval replay` and `memeval chart` on a missing path were tracebacks;
they exit with a message. `chart RUN --ab A B` silently ignored RUN; it
is now rejected. `memeval adapters` crashed on an adapter with an empty
docstring. `python -m memeval` works (there was no __main__).

Replace test_negation_correction_not_flagged_as_fabrication, which only
asserted the negation regex matched a string, with a run against a store
that recalls the negated correction (must stay clean) and one that
recalls the affirmative claim (must be flagged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc5DZiN4NhcQ9mBX2SL2yP
…failures

The five bundled adapters each carried a copy of the urllib request/
decode code, and only mem0, letta and zep folded the server's error body
into the exception; hindsight and example-rest surfaced a bare status
line. adapters/_http.request_json is the one copy, so a 4xx from any
store says why in the run log. Request shapes are unchanged (existing
FakeHTTP tests pass as-is); example-rest gains its first test.

HINDSIGHT_BATCH=0 raised `range() arg 3 must not be zero` on the first
retain; clamp to >= 1.

HindsightAdapter.consolidate swallowed every exception, so an
adversarial run against an unreachable server, or a bank that rejects
the call, logged consolidate ok=true and reported the fabrication check
as if consolidation had happened. Runner.consolidate already guards the
call, records ok=false with the error, and continues, and doctor reports
it as informational; let it propagate so the log tells the truth.
Verified with mocked HTTP only, not against a live instance.

README: quote the extras in the install lines so they work under zsh,
and point adapter authors at the shared helper.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nc5DZiN4NhcQ9mBX2SL2yP
@nuk3s
nuk3s merged commit b54e1a1 into main Sep 25, 2026
10 checks passed
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.

2 participants