Skip to content

Validator: the meaning must fit the sentence it is shown in - #900

Merged
mircealungu merged 13 commits into
masterfrom
validator-card-fits-sentence
Oct 25, 2026
Merged

mircealungu merged 13 commits into
masterfrom
validator-card-fits-sentence

Conversation

@mircealungu

@mircealungu mircealungu commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

Builds on #899 (the regression set, merged).

Problem. An exercise showed "mean" above "vil sige" in Jeg lærte, hvad det vil sige at gå ("what it mean to walk"). The validator passes that pair in 3 of 3 runs ("a correct, tense-neutral gloss"), and on 13 Aug it fixed "will say" to that same "mean". It checks whether a translation holds in other sentences, never whether it reads right in this one.

The PR in three lines

  • Prompt: fit this sentence first, and you may give up.
  • Reply: the model writes the sentence with its meaning in place and says whether it reads right.
  • Code: if it says no, ask once more, then give up. It never returns a meaning the model itself called wrong.

Changes

  • Prompt (prompts/translation_validator.py). STEP 1 now runs: (1) is the sentence usable, (2) does the translation fit this sentence, form included (English may add "does"/"not"), (3) is it the word's own meaning, tested in another sentence (the noget = "one" example), expanding to the smallest expression worth learning, (4) the "to" rule, (5) give up. A corrected word must appear in the sentence as written. Correct but less common translations are not "improved".

  • Two ways to give up:

    • BROKEN_SENTENCE: the sentence is cut off, ungrammatical or nonsense.
    • WORD_NOT_FIT: the sentence is fine, but no meaning of the word fits it.

    Both take only this learner's word out of study, with a log line giving the kind and the sentence. The meaning is not touched, nothing is written to validation_log (whose action says what happened to a meaning), and user_preference (the learner's choice) is not written either. When the learner acts on the word again (a new look-up, starring), it's judged again.

  • Self-check in code (translation_validator.py). Four new reply fields hold the sentence with the meaning in place, and whether it reads right, for the meaning given and for any correction. _misfit acts on them:

    • certain (the model says it reads wrong, or a corrected word isn't in the sentence): one retry quoting the model's own sentence, then WORD_NOT_FIT;
    • uncertain (the translation isn't word for word in the model's sentence, which separable verbs do legitimately): kept.

    "/" alternatives and "(glosses)" are reduced to one option before the fit check, keeping the model's own judgement. A failed retry is no verdict and goes to Sentry, unless the first answer would have been kept anyway.

  • One validator, two callers. The validator takes ask (who answers) and retry. The audio-lesson check (second_opinion_on_meaning, formerly judge_pair) is now the same validator asked of DeepSeek without retries, so the decision logic lives in one place. A give-up there counts as not confirmed (script_generator._meaning_confirmed_wrong), so it never marks a meaning INVALID. "true"/"false" sent as strings by DeepSeek count as booleans.

Regression set (3 runs per case)

before after
runs passing 104/150 126/147

"vil sige" = "mean" is no longer accepted in any run; from "will say" it comes out as "means". Still missed: Provencesanger (passes as "Provence singer"; it's a bird), the broken Kan jeg at få…, and mere ved. Results vary by about ±4 runs between identical runs. The noget case left the set when it became the prompt's example.

Cost: about 60 more output tokens per call, plus one retry when a meaning doesn't fit (worst case 3 calls per word, normally 1).

Deploy: no migration. After deploy, old audio-lesson audit reports can't be acted on (checker_id hashes the prompt); re-run the audit before any --deprecate.

Not in this PR (see docs/future-work/llm-tasks-roadmap.md):

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

ArchLens detected architectural changes in the following views:
diff
diff

@mircealungu
mircealungu force-pushed the validator-card-fits-sentence branch from 94d56ee to 670ea2f Compare October 25, 2026 00:57
mircealungu and others added 4 commits October 25, 2026 02:11
The validator passed "vil sige" = "mean" in "Jeg lærte, hvad det vil sige
at gå" 3/3 ("a correct, tense-neutral gloss"), and fixed "will say" to that
same "mean": it judged whether a translation holds in other sentences, never
whether it reads right in this one.

- The prompt now asks first whether the card fits THIS sentence (form
  included, English free to add "does"/"not" around it), then whether it is
  the word's own meaning, expanding to the smallest expression worth
  learning; corrected words must appear in the sentence as written.
- A third verdict, UNRESOLVED, for broken sentences and cards nothing fits.
  validate_and_fix already treats it as "no correction": unfit for study.
- The reply writes the sentence with the card in place and judges it, for
  the card and for a correction. The model judges those sentences right but
  cannot revise a field it already wrote ("what it mean to walk" marked
  false, "mean" kept), so the code acts on it: one retry with its own
  sentence in front of it, then UNRESOLVED rather than a card it said is
  wrong.
- The audio lesson pair check treats UNRESOLVED as not confirmed: a broken
  sentence is no reason to mark the meaning INVALID for every learner.

Regression set, 3 runs x 50 cases, runs failing by kind:
                               before  after
  wrong card shown               34     18
  broken sentence let through    12      3
  gave up when it should not      0      5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- validate_and_fix sent UNRESOLVED into _fix_bookmark's "no correction"
  branch, which marks the meaning INVALID for every learner. A broken
  sentence says nothing about the pair: now only this learner's word leaves
  study, and the log reason starts with [unresolved].
- A failed misfit retry is no verdict (None), as for the first call, not a
  give-up.
- UNRESOLVED returns before the options retry, which could re-ask a give-up
  into an unchecked FIX.
- A correction counts as in its sentence when its words are there in order:
  "put up for sale" is in "put their homes up for sale", "don't" matches
  "don’t". The contiguous match gave sætter til salg up 3/3.
- The audio lesson pair check asks DeepSeek for the new fields, keeps the
  booleans booleans, applies the same misfit check, and reports UNRESOLVED as
  a verdict of its own rather than an error that every run retries; the
  script generator treats it as not confirmed.
- "corrected_translation is in {target_lang}" back on the corrected pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The reply wrote "what it means to walk", judged it right, and passed the
  card "mean" as VALID (1 of 3). The words-in-order check on corrections now
  applies to the VALID card too.
- "homeowners who put up for sale" is wrong English (it needs an object),
  so the reply rejected the right card "sætter til salg = put up for sale"
  and gave up 3/3. The sentence may now put words between the card's words
  ("put the house up for sale"), which words_in_order already allows.

Regression set, 3 runs x 50 cases: 123/150; wrong card 18, broken sentence
let through 3, gave up when it should not 6. vil sige = mean: never VALID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Options are reduced before the fit check: "by (doing something)" was
  never word for word in any sentence, so it was retried and given up on
  instead of becoming "by". The options retry can no longer replace a
  checked reply with an unchecked one, nor revive a give-up.
- A corrected word must be in the sentence, checked in code ("særlig" in
  "Det ganske særl", "umanned"); a word-only fix checks the old translation.
- A card missing word for word from its own sentence twice is kept, not
  given up on: separable and inflected verbs do that ("anrufen" in "Ich
  rufe dich an"). Give-ups are for what the reply calls wrong, or a word
  not in the sentence.
- A failed misfit retry goes to Sentry, like a failed first call.
- A give-up clears the word's schedule (set_unfit_for_study).
- The retry prompt no longer quotes "None" when the reply wrote no sentence.
- Only the two flags stay booleans in the DeepSeek pair check; a boolean in
  a text field would crash the parser.
- ValidationResult.verdict names the verdict in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mircealungu
mircealungu force-pushed the validator-card-fits-sentence branch from 670ea2f to 0aa8720 Compare October 25, 2026 01:11
@mircealungu mircealungu changed the title Validator: the card must fit the sentence it is shown in Validator: the meaning must fit the sentence it is shown in Oct 25, 2026
@mircealungu
mircealungu changed the base branch from validator-regression-set to master October 25, 2026 01:47
mircealungu and others added 9 commits October 25, 2026 02:59
UNRESOLVED covered two different failures. A broken sentence (cut off,
ungrammatical, nonsense) says nothing about the word, which may fit any
other sentence. A word no meaning fits, as given or after a fix, is the
word's problem. ValidationResult.gave_up now names which; the code's own
give-up (a meaning still misfitting after the retry) is WORD_NOT_FIT.

Both still take this learner's word out of study, logged as
[broken_sentence] or [word_not_fit]; the meaning is untouched either way.

Regression set, 3 runs: 122/150 (125 before, within run-to-run noise);
BROKEN_SENTENCE given for 7 of the broken-sentence runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rewrite of step 4 had lost "a translation that adds 'to' ... is a FIX,
even though its meaning is right" (from #853). The example FIX øjnene =
"the eyes", taken from "se virkeligheden i øjnene", now contradicts step
3, which expands to the idiom; the next example already shows that fix.

Regression set, 3 runs: 122/150; vil sige right 6/6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
They were logged as invalid with a [broken_sentence]/[word_not_fit] reason
prefix, so anything counting action='invalid' mixed them with meanings that
were marked INVALID. The prefix stays, to say which give-up.

Migration: tools/migrations/26-10-25-a--validation_log_gave_up.sql, before
deploy (appends an enum value; metadata only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"It should still hold when the learner meets the word in an ordinary
sentence" read as if it tested the form too, against step 2. It now says
only the meaning is tested ("in that sentence's form"), with the case
behind it: noget = one, saved from "noget af det mest ekstreme", is
"something" in "Vil du have noget at spise?". That case leaves the
regression set, since the prompt now shows it.

Regression set, 3 runs: 126/147 (122/150 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
judge_pair repeated validate_and_classify's steps (give-up, misfit,
options) for a DeepSeek reply. The validator now takes `ask` (who answers)
and `retry` (whether to ask again), so the pair check is the validator
asked of DeepSeek without retries, and the decisions live in one place.

Renamed for what they are: judge_pair -> second_opinion_on_meaning, and its
caller script_generator._pair_verdict -> _meaning_confirmed_wrong (it returns
the verdict only when the meaning is confirmed wrong).

Fixes a flag read as true: DeepSeek writing "false" as a string kept it as
the string "false". Only booleans count now; anything else is not given.

Side effect: a DeepSeek reply without a verdict now also goes to Sentry, as
the validator's own failed calls do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validation_log's action says what happened to a meaning: valid, fixed,
invalid. A give-up is the opposite (nothing happened to the meaning; the
learner's word left study because of this sentence), so it does not belong
in that column. Reverts the gave_up enum value and its migration; the
give-up is in the log line, with its kind and the sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reply's judgement

- A give-up only flipped fit_for_study, which update_fit_for_study
  recomputes on later events (starring, ...): the word came back, was
  validated again and given up again. It now also sets user_preference to
  DONT_USE_IN_EXERCISES, the only thing fit_for_study keeps (as #896 does
  with BAD_TRANSLATION).
- A failed retry after an uncertain misfit returned no verdict, throwing
  away a first answer that would have been kept; it keeps it now.
- Cutting a VALID "by (doing something)" to a FIX "by" lost the reply's
  "does not read right", so the cut card passed unchecked; the sentence and
  the judgement now go with it.
- A give-up from the options retry was ignored; it wins, as on the first
  call (it moves nobody).
- DeepSeek's "true"/"false" written as strings count as the booleans.

Regression set, 3 runs: 126/147; vil sige 9/9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
user_preference is the learner's choice; the validator setting it made a
system decision look like the learner's (the mix-up #895 fixes the other
way). A give-up takes the word out of study only. What brings it back is
the learner acting on the word again (a new look-up, starring), which
recomputes fit_for_study and has it judged again, in the new sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mircealungu
mircealungu merged commit 6895196 into master Oct 25, 2026
2 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.

1 participant