fix(ir): the analyst does not count a word as a change (ENG-743) - #326
Conversation
…ses it The analyst answered a `missing` finding with only a chunk number, and null meant "after everything" — a case the model chose wrong even when its prompt spelled it out: the night of Rute 1:15-18, v.18 came back as `chunk:5` instead of `null`, and the room sent the team to redo chunk 5 whole, losing the oath (v.17) that was already there. `"where"` replaces the guess: `"before"` / `"inside"` / `"after"`, always relative to the named chunk. The server resolves it — `"after"` on the last chunk is now the one case with no address (to the rehearsal, never `null`); `"after"` short of the last chunk moves the pointer to the next one; `"before"` and `"inside"` land on the chunk itself. A reply with no `"where"` (a still-served legacy prompt row) or an unrecognised one falls back to naming the chunk as before, the second case logged with what the analyst actually said. Verified live against session b0cceac5's own five original chunks (before the team's six retells): the old prompt answers `chunk:5` for v.18 with no way to tell "redo this" from "go on recording"; the new one answers `chunk:5, where:"after"`, which now resolves to no address at all. Analyst prompt text included for the product owner's review. Part of the R4 02/09 finding; first slice ahead of `henok/a-mend-is-checked-against-what-it-carried`. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
analyse_telling_back and verify_correction only logged when a reply from the model could not be trusted at all. A reply that parsed cleanly and was simply wrong left nothing behind once the call returned, so an addition invented by the corrector, say, was gone the moment the room answered. Both now take a session_id and log one INFO record per accepted reading, carrying the model's raw reply whole — never the team's own words. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
Four scenarios, calling analyse_telling_back and verify_correction directly and asserting on caplog records by field, never by matching the log sentence: an accepted analysis, an accepted correction, the team's own words never reaching the logger, and a refused reading leaving only its existing WARNING trace, not a second record. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
…what-it-carried' into henok/every-reading-leaves-a-trace
…re-it-goes' into henok/every-reading-leaves-a-trace # Conflicts: # app/services/internalization_room/back_translation.py
…hat-it-can-read' into tmp-316-reconcile
The merge of ENG-719's parser fix (_refused, a single named-condition refusal log) auto-merged textually clean against this branch's own _refused_where and _session_of, but left both defined twice: two _session_of bodies, and two refusal loggers doing the same job in slightly different words for the same log line. Kept _refused's one _session_of and its one refusal path; the unrecognised-'where' case now calls _refused with its own named condition instead of a sibling logger.warning call. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
…ery-reading-leaves-a-trace analyse_telling_back's exception path was a real conflict, not a positional one: ENG-719 (already merged into #316) now raises UpstreamServiceError on a failed model call instead of returning None, and this branch's own accepted-reading log has to run only on the success path after that. Kept both — raise on outage, log and return the parsed analysis on success. The merge's own auto-merge of the API layer's import line silently dropped UpstreamServiceError in favour of ENG-719's new UnreadableReply, leaving two other call sites in the same file (verify_correction's and the closing-reading's own outage handling, neither touched by ENG-719) referencing an undefined name — caught by ruff/mypy across the whole file, not by the six flow tests alone. Restored the import. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
Merging ENG-719 (_refused, UnreadableReply) auto-merged this file's import line to its side, silently dropping UpstreamServiceError — still raised at two call sites in the same file that ENG-719 never touched (verify_correction's own outage, and the closing reading's). CI on this branch was red on ruff and mypy for exactly that; a check scoped to the changed files alone would not have caught it, since neither of those two call sites is part of what this branch or ENG-719 individually changed. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
R11 (03/09, session P02): the map for Ruth 1:9a says only "rest ... each in
the house of her husband", and the team said "may they be happy, each in
the house of her new husband." The analyst read "novo" and "sejam felizes"
as `addition` across five unnecessary re-recordings of a telling-back that
already had the story right.
The correction prompt already carries this calibration ("Wording varies.
Content does not."); the analyst compared telling-back against the map
directly and never had it. Give it the same law, adapted to compare
against the map rather than an earlier telling, and repeat it inside the
`addition` definition itself.
🤖 Generated with [Nori](https://noriagentic.com)
Co-Authored-By: Nori <contact@tilework.tech>
# Conflicts: # app/services/internalization_room/back_translation.py
# Conflicts: # app/services/internalization_room/back_translation.py
20260901_room09 (this branch's new BT_CORRECTION prompt key) and 20260902_room09 (main's drop of ir_prompts, PR #320) both descend from 20260831_lang01 with nothing joining them. Same pattern as the existing join migrations in this file: parents only, no schema change. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
Same fork as in #326: 20260901_room09 (this line's BT_CORRECTION prompt key) and 20260902_room09 (main's drop of ir_prompts, PR #320) both descend from 20260831_lang01 with nothing joining them. 🤖 Generated with [Nori](https://noriagentic.com) Co-Authored-By: Nori <contact@tilework.tech>
# Conflicts: # alembic/versions/20260903_join_bt_correction_and_drop_ir_prompts.py
…enok/a-missing-element-says-where-it-goes The parser branch carries the main's ENG-609 with it. Two resolutions, both unions: the exception imports in the back-translation route (UnreadableReply and UpstreamServiceError both have callers here, HaltKind arrives from the main), and the service export list.
…enok/every-reading-leaves-a-trace One resolution, the exception import block of the back-translation route: UnreadableReply from this branch's side, UpstreamServiceError which both keep, and HaltKind arriving with the main.
|
|
||
| Judging by literal difference would turn every honest telling-back into a stream of false | ||
| findings, and send the team back to re-record a stretch that already told the story right. When | ||
| wording changed and content did not, report nothing. |
There was a problem hiding this comment.
"When wording changed and content did not, report nothing."
The bullets above scope this to "addition" or "missing", but this closing sentence does not, and the section title says it is "the whole of your calibration". Category 6 is exactly a case where the wording changed and the content did not: a map that withholds "redeemer" behind "our kinsman (moda)" and a telling-back that says the name is literally "a name in place of a description, for the same person" from the first bullet. Maybe scoping this last sentence the same way the bullet intro already does is enough — could you verify that against P08-Ruth-3-1-5.md? You spotted this one yourself in the description, I think it deserves the one-line fix and not only a note for the owner.
|
|
||
| - a different word for the same thing — a synonym, an adjective, an epithet, a name in place of | ||
| a description, for the same person, place, or thing; | ||
| - a different order of sentences, a different starting point, a different amount of detail; |
There was a problem hiding this comment.
"- a different order of sentences, a different starting point, a different amount of detail;"
"a different amount of detail" under a "never report as missing" list feels too wide for me. Item 1 defines missing as an element the map gives that appears in NO chunk — a telling-back that gives less detail is precisely that, so the analyst ends up with two rules pointing opposite ways. Your case 2 probe cut the whole proposition, which is absence, not a thinner telling. Just make sure here that this bullet is about the map's phrasing being richer than the team's, not about the telling-back carrying fewer elements.
| # O item 2 (Added/addition) vive dentro da mesma lista numerada que os outros sete | ||
| # achados, sem linha em branco entre eles — então isola-se pelo próprio marcador | ||
| # numérico, não por um split em blocos de parágrafo (que pegaria a seção de | ||
| # calibração inteira, e essa também cita "adjective" no próprio exemplo). |
There was a problem hiding this comment.
This why is good but CLAUDE.md asks for it in the docstring instead of a # comment, and the test already has a docstring where it would be read.
Linear: ENG-743
Summary
R11 from the open round of 03/09. No new issue.
Session P02 (
006189f9…), stretch 5 (Ruth 1:9a — Map Proposition 5: rest (menucha) … each inthe house of her husband). The team said "…sejam felizes, cada uma na casa do seu novo
marido". The analyst marked "felizes" and "novo" as
addition— neither an event, actor,cause, pairing, or outside detail, which is the very definition of
addition. Fivere-recordings of a stretch that already told the story right.
The correction verifier's prompt already has this calibration ("Wording varies. Content does
not. This is the whole of your calibration."): different words for the same referent, order,
amount of detail, paraphrase — none of that is a difference. The analyst's prompt never had
this section; it compares the telling-back directly against the map, not one retelling against
another, so the text was adapted (its own example, anchored on the map, not on an "earlier
telling").
What changed
Only the analyst's prompt — the new calibration section and one sentence in the definition of
addition— and one prose test that pins both anchors (house convention: seetest_the_analyst_is_told_where_a_missing_element_sits, its neighbor in the same file). Theparser, the verifier and the closing blocks were not touched (sibling slice
henok/a-missing-part-names-both-voicestouchesclosing_block/CLOSING_*;git merge-treeclean against it).
Prompt diff, for product-owner review
ir_promptsoverrides the file in seeded environmentsget_prompt_textreads their_promptsrow forbt_analystbefore the.mdfile. There is noseeded row for
bt_analystintripod_dbtoday (select key from ir_prompts where key='bt_analyst'→ 0 rows), so this merge changes behavior immediately, with no row updateneeded — as #316 already recorded for the same prompt.
Probe on a real pair (Testing Plan, cases 1 and 2)
analyse_telling_backrun for real insidetripod_backend(real credits), over the 10 activestretches of session P02
006189f9…, with stretch 5 at its 12:55:40 version (4b49b16f…,"sejam felizes... novo marido") and stretch 1 at the version active at that moment (
5f3093b4…,before the 12:59 re-recording) — the rest at the only version they ever had.
scope/pericope_num=P02, as the session itself stores it (back_translation.scope).Case 1 — BEFORE (base prompt), 3 of 3 runs reproduce
addition{"evidence_sufficient": true, "findings": [{"kind": "addition", "note": "O relato acrescenta que Noemi desejou que as noras fossem 'felizes'.", "segment_id": "4b49b16f-e0d0-4049-bfd1-3dc6b1026059"}]}{"evidence_sufficient": true, "findings": [{"kind": "addition", "note": "O relato acrescenta que Noemi desejou que as noras fossem 'felizes'.", "segment_id": "4b49b16f-e0d0-4049-bfd1-3dc6b1026059"}]}{"evidence_sufficient": true, "findings": [{"kind": "missing", "note": "O relato não menciona que Noemi se levantou (ou se preparou) para sair de Moabe.", "segment_id": "5f3093b4-b9cc-4e5b-84e4-51f8209e2615"}, {"kind": "addition", "note": "O relato acrescenta que Noemi deseja que as noras 'sejam felizes'.", "segment_id": "4b49b16f-e0d0-4049-bfd1-3dc6b1026059"}]}Case 1 — AFTER (this PR's prompt), 0 of 3 runs with any finding
{"evidence_sufficient": true, "findings": []}{"evidence_sufficient": true, "findings": []}{"evidence_sufficient": true, "findings": []}Plan's criterion: before, reproduces
additionfor "felizes"/"novo" in ≥1 of 3 (came out 3 of3); after, 0 of 3
additionand 0missingfor Proposition 5 on stretch 5 (came out 0 of 3, nofinding at all). Closed without tuning anything beyond the text already shown above.
Case 2 — the calibration does not blind the analyst (this PR's prompt, same 10 stretches, only stretch 5 changes)
Without the blessing (stretch 5 = "Noemi beijou as noras e elas choraram."), 3 of 3
missingfor Proposition 5:{"evidence_sufficient": true, "findings": [{"kind": "missing", "note": "falta o conteúdo da segunda bênção de Noemi, pedindo que o Senhor lhes conceda descanso na casa de um marido", "segment_id": "4b49b16f-e0d0-4049-bfd1-3dc6b1026059"}]}(identical across the 3 runs)
With an outside actor (stretch 5 = "...cada uma na casa do seu marido, e Boaz estava lá."),
3 of 3
addition:{"evidence_sufficient": true, "findings": [{"kind": "addition", "note": "O relato afirma que 'Boaz estava lá', um detalhe que não consta no mapa para este trecho.", "segment_id": "4b49b16f-e0d0-4049-bfd1-3dc6b1026059"}, {"kind": "missing", "note": "Falta o beijo de despedida de Noemi e o choro inicial das noras após a primeira bênção.", "segment_id": "ff9d72e9-5e51-4a3f-a6eb-88fd9200f167"}]}(identical across the 3 runs; the extra
missingis expected — the override text is shorterthan the real stretch 5 and cuts the kiss/weeping the real version also carries; not what case 2
measures)
Test plan
test_the_analyst_does_not_count_a_word_as_a_change(
tests/test_internalization_room_back_translation.py), house convention: pins the anchor"Wording varies. Content does not." across the whole prompt, and isolates item 2
(
Added/addition) from the numbered list by regex — not bysplit("\n\n"), because thewhole list (items 1-8) has no blank line between items, and the calibration section itself
cites "adjective" in its example. Confirmed by simulation (self-review): removing only the
sentence in item 2 while keeping the calibration makes the test fail;
git diffrestored bycopy afterward.
1 failed(the anchor did not exist). Confirmed.tests/test_internalization_room_back_translation.py— 68 passed.2485 passed, 5 skipped, 1 xfailed(env -u DATABASE_URL, withJWT_SECRET_KEYand a sqliteDATABASE_URLon the command).ruff check .— All checks passed;ruff format --check .— clean.mypy app— Success: no issues found in 597 source files.For the reviewer
place of a description... for the same person, place, or thing") could, in theory, collide
with
preservation_violation(category 6) — a map that deliberately withholds a name (asP08-Ruth-3-1-5.mddoes with "our kinsman (moda)" in place of "redeemer") depends on category6 continuing to hold over the new calibration. Category 6's text was not touched and should
prevail in practice, but it is a real interaction in the corpus, not a hypothetical one —
worth a look from the product owner.
seed_room_promptsis never calledon this branch (there is no
ir_promptsrow for any Room prompt today), which is why editingthe
.mdis already effective —origin/mainalready resolved that dead indirection(
7b1323f1/f4b7ed75, ancestors of main but not of this branch). Not touched here; whoeverreconciles this line of work with main will run into it regardless.
PR against
henok/every-reading-leaves-a-trace(top of the stack, #318). No merge.