Skip to content

Drop the reviewed/auto tier; surface the translator's per-slide notes - #152

Closed
kcarnold wants to merge 4 commits into
mainfrom
claude/compassionate-newton-jdkww1
Closed

kcarnold wants to merge 4 commits into
mainfrom
claude/compassionate-newton-jdkww1

Conversation

@kcarnold

@kcarnold kcarnold commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Superseded. This PR has been split and rebuilt on current main:

The Scripture-credit follow-up that was in a comment here is now issue #181. Close this PR once those land.


What

Two halves, same idea: stop tracking a quality tier nobody acts on, and start showing the per-slide notes that actually say "look at this one."

Deleted — the reviewed / auto status

SlideTranslationEntry is now { text, provenance }. Gone: SlideStatus, status, reviewedAt, and the viewer's "unreviewed" badge.

  • resolveSlideTranslation collapses from a two-pass quality walk to "first entry along the fallback chain."
  • slideLibrary.ts no longer stamps status: 'reviewed' / reviewedAt. It rebuilds records on load rather than storing them verbatim, so the dropped fields don't ride along into the next write of the on-disk library.
  • slide_translator.py (the Python work-ahead worker) no longer needs to recognize a protected entry. _store_translations now writes only keys that are missing. Keys are content-addressed, so an entry that exists already translates this exact text — whoever wrote it knew at least as much as this run does.
  • The review grid's per-cell chip says Saved, not Reviewed. That is what it always meant: the draft matches the library.

Surfaced — the model's per-slide notes

set_translations has always had an optional per-segment note ("a genuine ambiguity or judgement call worth flagging"). applySetTranslations threw it away, and the conversation panel summarized the call as ✍️ set translations: French (5) — so the notes existed and were visible nowhere.

Now they ride on the conversation: SlideConversation.notes, content-addressed by language + source text. Each agent run is seeded with the item's current notes and returns the whole set, so the server replaces rather than merges, and a follow-up touching one slide can't silently drop the notes on the others. A rewrite with no note withdraws the note it had; a revise_translation deliberately leaves notes alone.

The review screen is reorganized around them:

  • the service-item picker is now a row of chips instead of a <select>, each badged 📝 n with its note count — the "at a glance, which things have notes" view;
  • a flagged row is tinted and marked in the source column;
  • a flagged cell leads with its note, above the textarea;
  • a count (or "nothing flagged") sits next to the screen title.

The set_translations tool description now tells the model its note is displayed and marks the item — so it has a reason to be sparing.

Behavior change worth a look

A Haitian Creole viewer with no Creole text for a slide now sees the French one, whatever produced it. Previously an unreviewed French text was withheld and the slide read "(not translated)". With the tier gone this is the only coherent reading of the fallback chain, and in practice the gap only opens on a surprise slide the work-ahead worker hasn't reached — but it is a real change, not a refactor.

Smoke test

Touches §3 Proclaim sync — "Slide translations show for the on-air item; review screen loads its conversation." Worth adding to that pass: open an item with notes from the picker and confirm the badge count matches the flagged rows.

Verification

npm run typecheck, npm run lint, npm test (600 passed), uv run pytest — all green. New tests cover the note lifecycle in the agent (attach, withdraw, carry across an untouched follow-up, drop when the source text is gone) and the note rendering in SlideReview.

docs/slide-translations-plan.md gets an amendment at the end recording the removal, in the style of the existing post-build corrections, rather than a rewrite of 385 lines that describe the old design.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RuD8vmbn9ku8wbVQYMH5BH


Generated by Claude Code

claude and others added 3 commits September 14, 2026 18:40
The stored `status: 'reviewed' | 'auto'` never earned its keep. Viewers were
told "unreviewed" about text nobody was going to fix mid-service — noise they
can't act on — and in practice what decides whether a slide gets a human's
attention is a *note*, not a tier.

Deleted:
- `SlideStatus`, `status` and `reviewedAt` from `SlideTranslationEntry`, the
  library record, and `PerSlideTranslation`. `provenance` stays; nothing
  branches on it.
- the "unreviewed" badge in the slide viewer.
- `resolveSlideTranslation`'s two-pass quality walk — it now returns the first
  entry along the fallback chain. Behavior change: a Creole viewer with no
  Creole text now sees the French one whatever produced it, where before an
  unreviewed French text was withheld and the slide read "(not translated)".
- the Python work-ahead translator's protected-entry check. It now writes only
  keys that are *missing*, so it can't clobber anyone. Keys are
  content-addressed, so an existing entry already translates this exact text.

The per-cell chip in the review grid now says "Saved", which is what it always
meant: the draft matches what is in the library.

Surfaced instead: `set_translations` has always let the model attach a
per-segment `note` — "a genuine ambiguity or judgement call worth flagging" —
and we dropped it on the floor. Notes now ride on the conversation
(`SlideConversation.notes`, content-addressed by language + source text,
seeded into each agent run and replaced wholesale by its result), and the
review screen is organized around them: the service-item list is a row of
chips badged with each item's note count, flagged rows are tinted and marked,
and a flagged cell leads with its note above the textarea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuD8vmbn9ku8wbVQYMH5BH
It existed only for Phase C-import's `firstDraftBySlide` path, which the
alignment rethink removed — the value outlived its writer and was never
stored by anything.

`'bible'` and `'creed'` stay, unwritten, with a note on what they are for:
the agent claiming a slide is the published wording rather than its own
rendering. That claim is worth storing because it is checkable — the
canonical text `lookup_bible_passage` returns is what a harness would check
it against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RuD8vmbn9ku8wbVQYMH5BH
The docstrings on the Python worker, its tests, and the agent prompt helpers
still said the worker protected `reviewed` entries and that the library was
the "already-reviewed" context. The tier is gone: the worker fills missing
keys only, and the library is just what a human saved. Three test fakes were
still returning a `status` field nothing reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`llm` was the initial draft and `llm-agent` a follow-up revision — but the
initial draft is the same agent, so the split named nothing. Nothing read it.

The button ran the same agent the Proclaim service already ran for every
item, without the French-screen grounding the service passes, and replaced
the service's conversation. Calling it "Suggest" hid that; "Re-draft" (or
"Draft" when nothing has drafted the item yet) says what it does. Removing
it altogether is #153.

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

kcarnold commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator Author

A follow-up plan once 169/170 land:

Credit Scripture versions on translated slides

Context

The slide agent (draftItemTranslations / runSlideTranslationAgent in nlp.ts) grounds
Scripture slides in published Bibles fetched through lookup_bible_passage (bible.ts,
BIBLE_TRANSLATIONS). Nothing records which published text a slide came from, so viewers
never see a credit. Some of these texts carry licenses that require one; for example, hatbsa
is CC BY-SA 4.0 (ebible.org/Scriptures/details.php?id=hatbsa). The goal is a small,
unobtrusive credit under the translated slide, shown only when the slide reproduces Scripture
(at least about one verse). A phrase borrowed for a song gets no credit.

Decisions made: the model declares the credit, and the viewer shows version + license
only
(no reference). On the base: it was "stack on PR 152", but 169/170 now move the
pipeline too (see below). You expect 169/170 to land soon, so: build on main after 169/170. If 152 is still
open then, stack on 152 (rebased onto that main), since it owns the entry shape and the notes
UI this extends.

Design

1. Sources registry: src/textSources.ts (new, pure)

  • TextSource = { id, kind: 'scripture' | 'creed', name, publisher, license, licenseUrl, sourceUrl }
  • TEXT_SOURCES: Record<string, TextSource>, keyed by helloao id (hatbsa, fra_ncl, spa_r09).
    Creeds and confessions are added here later under kind: 'creed', with no schema change.
  • formatCredit(source) returns e.g. Bib la — Société Biblique Haïtienne · CC BY-SA 4.0.
  • Entries store the id, not the rendered string. If a credit turns out wrong, we fix the
    registry once and every stored slide picks up the fix.
  • bible.ts keeps BIBLE_TRANSLATIONS, and a test asserts that every id in it has a
    TEXT_SOURCES entry. Adding a Bible without its credit then fails CI.

2. Data model (on top of PR 152)

  • SlideTranslationEntry in src/slideTranslation.ts gets optional sourceId?: string and
    adapted?: boolean; both ride the same path below.
  • It is threaded through TranslationBlockResult (nlp.ts) → translateItem
    (src/slideItemTranslation.ts) → the returned entries → slideLibrary.ts (persist, and keep
    it through the record rebuild that PR 152 added) → the /api/slideLibrary upsert body.
  • slide_translator.py _store_translations: copy sourceId when present.

3. Model declares it: nlp.ts

  • Add two optional fields to the set_translations segment schema, both named generically:
    • source, an enum. It is 'bible' today and gains 'creed' once a creed lookup tool
      exists. The description gives the rule: set only when this slide reproduces at least
      one full verse (or sentence) of text you looked up; omit it for allusions, short phrases,
      and song lyrics.
    • adapted, a boolean: whether the slide alters the published wording. We trust the agent
      on this.
  • The model doesn't echo a reference. I checked what a lookup returns:
    { reference: "PSA 23", passages: { French: … } }, where the reference is the requested
    range
    . That is often wider than one slide (a chapter lookup, with the slide holding verses
    1–3). So echoing it would be loose matching at best. It also isn't needed: the credit
    depends only on which version, and that's fixed by the language (BIBLE_TRANSLATIONS).
  • Server-side guard: attach sourceId = BIBLE_TRANSLATIONS[language] only if the
    conversation messages contain a lookup_bible_passage response that returned a passage
    in that language. Scanning messages, rather than a per-run list, covers follow-up runs
    whose lookup happened in an earlier round. The model can't name a license, and it can't
    get a credit for a version it never fetched.
  • Store sourceId (and adapted) at write time, so a later change to BIBLE_TRANSLATIONS
    doesn't relabel old slides.
  • Working-copy semantics follow PR 152's notes: a rewrite without source withdraws the
    credit, and revise_translation leaves it alone.
  • One sentence goes in bibleSection of buildSlideTranslationPrompt pointing at the fields.

4. Viewer: src/SlideTranslationViewer.tsx

  • When resolved.entry.sourceId maps to a source, render a footer line through SlideText.
    Add a footer prop next to header if SlideText lacks one. Style it text-xs, low
    opacity, and keep it outside the fit-text measurement so it doesn't shrink the slide.
  • The license name links to licenseUrl. This matters for phone viewers; on the projector it
    is just text.
  • No label for a verbatim quote: the line itself (version · publisher · license) is
    self-explanatory. There is one localized string, adaptedFrom ("adapted from" / "adapté
    de" / …), in src/strings.ts for all four locales. It is used only when adapted is true.
    It follows the viewer's UI locale like every other string.

5. Review screen: src/SlideReview.tsx

  • Show the same credit line in the cell, next to PR 152's note, so the reviewer can see the
    claim. A × to clear it is optional and goes in only if it turns out trivial.

6. Follow-up issue (not built)

  • "Require a Scripture credit on BiblePassage items". itemKind already reaches
    proclaimPresentations (yjs_publisher.py) but is not sent to /api/translateItem
    (only itemTitle is, from proclaim_service.py and src/slideTranslationApi.ts). Per your
    answer, this goes to an issue. Also include: the source-language credit (Proclaim's own
    English version on currentSlide), and localized references if that's wanted later.

If PRs 169/170 land first (server-owned slide sync)

Most of the plan is untouched. 169 doesn't change nlp.ts (the agent, its tools, the
prompt), the viewer, the review screen, or slideLibrary.ts. The changes are:

  • slide_translator.py is deleted. Its entry write moves to writeSlideTranslations in
    slideSync.ts, so sourceId/adapted get copied there. Python is no longer involved.
  • One entry point instead of two. Both /api/translateItem and the server's
    translate-ahead now go through draftItem in server.ts, so the credit is threaded once.
  • The BiblePassage rule becomes cheap. The snapshot now carries itemKind to the server
    (slideSync.ts), and proclaimPresentations already stores it for the review screen. So
    passing itemKind into draftItem → buildSlideTranslationPrompt is a few lines: "this
    item is a Bible passage; every slide must declare source: 'bible'". This moves from
    follow-up issue into scope.
  • Ordering conflict to watch: 169 still skips status === 'reviewed' entries, and 152
    deletes status. Whichever lands second has to reconcile that. This work is cleaner on
    main after both than stacked on either one.

Things to settle while implementing (flagged, not assumed)

  • Registry values (supplied by you; hatbsa's publisher name still gets copied verbatim
    from its ebible page):
    • fra_ncl: Sainte Bible néo-Crampon Libre, © 2022 Fraternité de Tibériade, CC BY-SA 4.0.
      Its terms say an adapted text must indicate the change, which the adapted → "adapted
      from" line covers.
    • spa_r09: Reina-Valera 1909, public domain. No credit is legally required, but we
      still show "Reina-Valera 1909 · dominio público". Crediting only the licensed versions
      would read as inconsistent to a viewer, and the line costs nothing.
    • hatbsa: Bib la (Haitian Bible Society), CC BY-SA 4.0.
  • ShareAlike. Under CC BY-SA an adapted slide is itself under CC BY-SA. The credit line
    already names that license, so nothing extra is shown, but it's worth one line in the PR.

Files

src/textSources.ts (new), bible.ts, nlp.ts, src/slideTranslation.ts,
src/slideItemTranslation.ts, slideLibrary.ts, server.ts (upsert body),
slide_translator.py (or, after 169, slideSync.ts + draftItem in server.ts),
src/SlideTranslationViewer.tsx, src/SlideText.tsx,
src/SlideReview.tsx, src/strings.ts, and a short note in docs/slide-translations-plan.md
in PR 152's amendment style.

Verification

  • Unit tests: registry covers BIBLE_TRANSLATIONS; source: 'bible' attaches sourceId
    when the history holds a lookup for that language (including one from an earlier round),
    and is dropped when it doesn't; a rewrite without source withdraws it; adapted renders
    "adapted from"; slideLibrary round-trips sourceId; the viewer renders the footer
    only when sourceId resolves; Python _store_translations copies it.
  • npm run typecheck, npm run lint, npm test -- --no-color, uv run pytest.
  • Smoke §3 (Proclaim sync): replay tests/fixtures/synthetic_service.jsonl and confirm a
    Scripture item shows the credit and a song doesn't.
  • Push to claude/vibrant-albattani-lk70bu based on PR 152's head, and open a PR noting it
    stacks on PR 152.

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