Skip to content

Stop recording a verdict the translation validator never reached - #726

Open
mircealungu wants to merge 1 commit into
masterfrom
fix-validator-fails-open
Open

mircealungu wants to merge 1 commit into
masterfrom
fix-validator-fails-open

Conversation

@mircealungu

Copy link
Copy Markdown
Member

The bug

The translation validator failed open in four places — an API exception, a batch API exception, an unparseable reply, and a batch response with fewer lines than items — all returning is_valid=True. validation_service then wrote validated = VALID on the meaning, and since it short-circuits on VALID, that meaning is never re-examined.

So a transient Anthropic outage permanently blessed whatever translations happened to be in flight. That is the opposite of what a validator is for.

What changed

  • Three-state outcome. ValidationResult carries VALID / INVALID / UNAVAILABLE instead of a boolean. is_valid survives as a property, so the one call site reads unchanged. On UNAVAILABLE the service persists nothing and returns the user_word, leaving it NOT_VALIDATED and in scope for the re-scanning tools.
  • Layout-driven parsing. The prompts declare two different pipe-delimited layouts — the batch prompt asks for no CEFR level — but both were parsed by the same positional reader, so in batch replies every field after the second was read as the wrong field. Each layout now has its own field list.
  • Bounded split. A stray | inside free text is absorbed by the final field instead of shifting every field after it. Closed-vocabulary fields (frequency / cefr_level / phrase_type) are dropped when out of vocabulary rather than written through — an out-of-vocabulary value is what a shifted line looks like.
  • arbitrary_multi_word alias. The prompt's vocabulary says arbitrary_multi_word; PhraseType.ARBITRARY_MULTI_WORD.value is multi_word, so from_string() has always returned None for it, and exclude_because_multi_word never fired.
  • A FIX with no correction is UNAVAILABLE. It is not actionable, and _fix_bookmark would have read it as "no correction provided" and marked the meaning permanently INVALID.

Verification

Only validation_service consumes ValidationResult, and the is_valid property keeps that call site working — checked by grep across zeeguu/ and tools/.

The parser was exercised by hand over valid / fix / no-correction / garbage / empty / aliased / out-of-vocabulary / embedded-pipe inputs, and each produced the intended outcome. There are no automated tests yet — the parse layer is now pure and easy to test, and that is the obvious next commit on this branch.

Open question, not addressed here

This stops new bad writes. It does nothing about rows already stamped by the old behaviour:

meaning.validated rows
0 (not validated) 297,595
1 (valid) 13,482
2 1,481

An unknown share of those 13,482 were never actually checked. There is no marker distinguishing them, so the options are to re-validate the lot, leave them, or find a proxy (e.g. rows written during a known outage window). Worth deciding separately.

🤖 Generated with Claude Code

The translation validator failed open in four places: an API exception, a
batch API exception, an unparseable reply, and a batch response with
fewer lines than items all returned is_valid=True. validation_service
then wrote validated=VALID on the meaning — and because it short-circuits
on VALID, that meaning is never looked at again. A transient Anthropic
outage therefore permanently blessed whatever translations were in
flight, which is the opposite of what a validator is for.

ValidationResult now carries a three-state outcome (VALID / INVALID /
UNAVAILABLE) instead of a boolean, with is_valid kept as a property so
the existing call site reads the same. On UNAVAILABLE the service
persists nothing and returns the user_word, leaving it NOT_VALIDATED and
in scope for the re-scanning tools.

Parsing is now layout-driven rather than positional. The prompts declare
two different pipe-delimited layouts (the batch one asks for no CEFR
level) but both were parsed by the same positional reader, so batch
replies had every field after the second one read as the wrong field.
Each layout now has its own field list, split with a bounded maxsplit so
a stray "|" inside free text is absorbed by the last field instead of
shifting everything after it, and closed-vocabulary fields are dropped
when out of vocabulary rather than written through. That also fixes
phrase_type "arbitrary_multi_word", which PhraseType.from_string() has
always returned None for.

A FIX verdict carrying no correction is now UNAVAILABLE too: it is not
actionable, and _fix_bookmark would have read it as "no correction
provided" and marked the meaning permanently INVALID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

ArchLens detected architectural changes in the following views:
diff

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