Skip to content

Add free-translation and boundary commands with parity tests - #517

Merged
alex-rawlings-yyc merged 8 commits into
mainfrom
446-boundary-commands
Oct 27, 2026
Merged

alex-rawlings-yyc merged 8 commits into
mainfrom
446-boundary-commands

Conversation

@imnasnainaec

@imnasnainaec imnasnainaec commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Part of #426; closes #446. Stacked on #516; review that first.

Summary

The segment free-translation and boundary commands, with parity tests. A split or move breaks every approved phrase the new boundary cuts, in the same ChangeSet.

interlinearizer.writeSegmentFreeTranslation(sessionId, segmentId, surfaceText, value, adoptStaleAnalysisId?) -> ChangeSet applied
interlinearizer.mergeSegments(sessionId, secondSegmentStartRef) -> ChangeSet applied
interlinearizer.splitSegment(sessionId, tokenRef) -> ChangeSet applied
interlinearizer.moveBoundary(sessionId, fromRef, toRef) -> ChangeSet applied
interlinearizer.splitPhrase(sessionId, phraseId, splitAfterTokenRef) -> ChangeSet applied   # order now built in the host
splitSegment / moveBoundary (session's turn)
  bookService.getVerseBook(source, bookOfRef(ref))
  bookIndexesOf(verseBook.segments)              # same orders the WebView builds, cached per book
  for each approved phrase phrasesStraddlingBoundary finds
    phraseCommands.splitPhrase                   # from #516
  segmentation transform; empty delta -> undefined
  buffer.putScope({ ...scope, segmentation })
  • writeSegmentFreeTranslation copies its reducer, adopt-stale branch included. It adopts through Add stale-handling commands with parity tests #511's findStaleTranslation and a new approveStaleTranslation, which keepStaleFreeTranslation now uses too (as noted on #446).
  • Boundary commands copy the WebView's boundary dispatch: segmentationDispatch in the loader, wrapped by the force-break in Interlinearizer.tsx.
  • Phrases stay inside one segment. createPhrase, updatePhrase, mergePhrases and approvePhrase refuse tokens that aren't all in one segment of the text, as the staged boundaries segment it. They read the segments through a new bookService.getBookSegments, which shares the page reads' cache.
  • Scope staging. ChangeBuffer gains getScope, putScope and toChangeSet emitting scope; AnalysisSessions.edit stamps the analysis language onto a staged scope instead of overwriting it. Undo and redo already invert scope.
  • Document order in one place. buildBookIndexes moves out of useBookIndexes into utils/book-indexes.ts; the hook and the host both call it. The host goes through bookIndexesOf, which builds once per segments array.
  • Picks by rule on both sides. The reducer's stale free-translation lookup now uses preferredLink, and phrasesStraddlingBoundary returns cut phrases in link-id order, so the WebView and the host agree whatever order links are stored in. Only tie cases change in the WebView.
  • Free-translation writes that change nothing stage nothing, on both sides. A write that leaves the language's translation as it was no longer restamps it or removes the segment's orphaned approved links. Clearing a translation's last language keeps the analysis when another link names it.

Evidence

  • 25 free-translation parity cases through runParity, including stale ties stored loser first, writes that change nothing, and a cleared translation that another link names.
  • 22 boundary parity cases through a new runBoundaryParity, which seeds analysis and segmentation, runs the WebView's composition (transform plus force-break through the reducer) against the command, and compares both plus the repository's indexes. They cover splits cutting none, one or several phrases, a discontiguous phrase whose gap holds the boundary, halves of 1 and 2+ tokens, two cut phrases sharing a payload stored out of id order, links in another book, a token the text lacks, moves, merges, edits restoring the default, and no-ops staging nothing.
  • main tests refuse each guarded phrase command over two segments, and over a token the text lacks. They also check that the guard reads the draft's boundaries in the phrase's book, keyed as the page reads key them.
  • A session test splits a segment, undoes and redoes it, and checks the segmentation and the broken phrase both come back.
  • Full suite at 100% coverage; lint and tsc clean.

Decisions with a strong alternative

  • Only approved phrases break. [3g] Add free-translation and boundary commands #446 says "every phrase the new boundary would cut"; today's dispatch breaks approved phrases only, and the commands copy that. Breaking a rejected or suggested phrase would record verdicts nobody made. A stale phrase that spans segments stays stale on reanchoring, and the guard above refuses to approve a suggested one across a boundary.
  • The segment guard lives in the host commands only. The reducers hold no segmentation, and the WebView keeps phrases inside a segment through its UI. The guard has command tests rather than parity tests.
  • splitPhrase drops tokenDocOrder (from Add phrase commands with parity tests #516): the host builds it from its verse book, so the WebView needs no book-wide token order. Nothing calls it yet. It now throws when the project has no text for the phrase's book.
  • Missing book text throws on every command that reads the book: merge, split, move, splitPhrase and the guarded phrase commands. The WebView can't reach that case.
  • getVerseBook and getBookSegments read through getSegments' held-book lookup, so a command naming a book that is neither open nor latest makes it the project's latest.
  • No-op boundary edits stage nothing. The WebView records an empty undo step and marks the draft dirty for a no-op on a custom delta; stored content is the same either way.
  • A split onto an existing segment start still breaks straddling phrases, as the WebView does. No approved phrase straddles a segment start, so nothing breaks.
  • Boundary requests take a newId generator, since the number of phrases to break isn't known up front.

Merge Danger

Door: two-way

Nothing calls the new commands. WebView behavior changes in two places. Ties now pick by rule: which stale link a free-translation write or keep adopts through, and which of two cut phrases sharing a payload keeps it. A free-translation write that changes nothing leaves the store as it was, and clearing a translation keeps an analysis that another link names.

Blast Radius: bookService, AnalysisSessions.edit, ChangeBuffer, useBookIndexes, phrasesStraddlingBoundary, the free-translation reducer, the phrase commands' main.ts wrappers. #507 (3j) also changes bookService held books and AnalysisSessions, so whichever lands later takes a merge there. 3k (#450) and 3l (#451) build on the host-side order and scope staging; 4a (#453) must handle the "no text for book" and segment-guard errors. #507 (3j) and this PR: whichever lands later runs the open book's reanchor pass after each boundary command. The WebView re-anchors after every boundary edit, which stales a free translation whose segment a split or merge changed, and 4a turns that pass off.

🤖 Generated with Claude Code


This change is Reviewable

Summary by CodeRabbit

  • New Features
    • Added segment-level free-translation editing, including options to reuse existing translations.
    • Added controls to merge segments, split them at a token, and move boundaries between tokens.
    • Splitting or moving boundaries now also separates approved phrases that cross the affected boundary.
  • Improvements
    • Phrase creation, editing, merging, and approval now enforce text-segment boundaries and require available book text.
    • Phrase splitting now determines token order from the source book automatically.

@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c43f89ac-8bbe-4f5b-9667-2c77f5e33077

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 35c5f3d3-5856-4438-afe4-79788805983b
Commits

Reviewing files that changed from the base of the PR and between ab1051d and 502e1e5.

Files selected for processing: 22
  • AGENTS.md
  • src/__tests__/main.test.ts
  • src/__tests__/services/analysisSessions.test.ts
  • src/__tests__/services/bookService.test.ts
  • src/__tests__/services/commands/boundaryCommands.parity.test.ts
  • src/__tests__/services/commands/boundaryCommands.test.ts
  • src/__tests__/services/commands/changeBuffer.test.ts
  • src/__tests__/services/commands/freeTranslationCommands.parity.test.ts
  • src/__tests__/services/commands/parity.ts
  • src/__tests__/utils/book-indexes.test.ts
  • src/hooks/useBookIndexes.ts
  • src/main.ts
  • src/services/analysisSessions.ts
  • src/services/bookService.ts
  • src/services/commands/boundaryCommands.ts
  • src/services/commands/changeBuffer.ts
  • src/services/commands/freeTranslationCommands.ts
  • src/services/commands/staleCommands.ts
  • src/store/analysisSlice.ts
  • src/types/interlinearizer.d.ts
  • src/utils/book-indexes.ts
  • src/utils/phrase-arc.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The change adds segment free-translation and boundary commands, checks phrase tokens against book segmentation, and moves phrase-splitting document-order lookup into the backend. It also adds book indexes, staged scope handling, and parity tests for the new command behavior.

Changes: Segment Commands and Phrase Boundaries

Layer / File(s) Summary
Book access and indexes
src/utils/book-indexes.ts, src/hooks/useBookIndexes.ts, src/services/bookService.ts, src/__tests__/utils/book-indexes.test.ts, src/__tests__/services/bookService.test.ts
Book services expose tokenized books and segments. Shared indexes provide token, segment, and document-order lookups, and tests cover index construction and reuse.
Phrase segment validation and ordering
src/main.ts, src/types/interlinearizer.d.ts, src/__tests__/main.test.ts
Phrase operations validate tokens against book segments. splitPhrase uses document order from the source book instead of caller-supplied order.
Staged scope and book-link lookups
src/services/commands/changeBuffer.ts, src/services/analysisSessions.ts, src/__tests__/services/commands/changeBuffer.test.ts, src/__tests__/services/analysisSessions.test.ts
ChangeBuffer stages scope records and overlays book-scoped link lookups. AnalysisSessions.edit uses a staged scope when present.
Boundary commands and parity
src/services/commands/boundaryCommands.ts, src/main.ts, src/types/interlinearizer.d.ts, src/utils/phrase-arc.ts, src/__tests__/services/commands/parity.ts, src/__tests__/services/commands/boundaryCommands*.test.ts, src/__tests__/main.test.ts, AGENTS.md
Merge, split, and move commands stage segmentation changes. Split and move also split approved phrases that cross the edited boundary. Tests compare host commands with reducer behavior and cover no-op edits.
Segment free-translation writes
src/services/commands/freeTranslationCommands.ts, src/services/commands/staleCommands.ts, src/store/analysisSlice.ts, src/main.ts, src/types/interlinearizer.d.ts, src/__tests__/services/commands/freeTranslationCommands.parity.test.ts
The command updates, clears, adopts, or creates segment translation records. Reducer updates and parity tests cover approved and stale translations.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant interlinearizer
  participant bookService
  participant boundaryCommands
  participant ChangeBuffer
  participant AnalysisSessions
  interlinearizer->>bookService: Read the token's verse book
  bookService-->>interlinearizer: Return the tokenized book
  interlinearizer->>boundaryCommands: Run merge, split, or move
  boundaryCommands->>ChangeBuffer: Read scope and stage segmentation
  boundaryCommands->>ChangeBuffer: Stage splits for approved phrases across a boundary
  AnalysisSessions->>ChangeBuffer: Build and apply the edit change set
Loading

Possibly related issues

  • sillsdev/interlinearizer-extension#445 — The change uses phrase splitting during boundary edits, but does not add every phrase command.
  • sillsdev/interlinearizer-extension#451 — The change adds phrase splitting for boundary edits, but not the separate rule for overlapping approved phrases.
  • sillsdev/interlinearizer-extension#520 — The change breaks phrases that cross new segment boundaries, which relates to the issue's phrase-span behavior.
  • sillsdev/interlinearizer-extension#450 — The new commands provide a command layer for segment-page analysis, but do not add segment-page queries.
  • sillsdev/interlinearizer-extension#453 — The change adds host commands, but does not wire the WebView to those commands.

Suggested reviewers: alex-rawlings-yyc

Merge Risk: Minimal · previous review 502e1

This PR adds host commands for segment free translations, segment merge and split, boundary moves and phrase splitting, with parity tests against the WebView behavior. I found no concrete merge-blocking defect in the supplied context. The test and coverage results come from the PR description and were not independently run, so normal CI should still confirm them before merge.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the primary changes: new free-translation and boundary commands with parity tests. It is concise and specific.
Linked Issues check Passed Direct issue #446 requires segment free-translation and boundary commands with parity tests. The reviewed changes add writeSegmentFreeTranslation, mergeSegments, splitSegment, and moveBoundary…
Out of Scope Changes check Passed The changes support issue #446. ChangeBuffer, scope staging, book lookup, document indexes, phrase guards, stale-translation helpers, reducer tie rules, WebView parity behavior, and tests provide th…
Docstring Coverage Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviv…
Full details: Linked Issues check

Explanation

Direct issue #446 requires segment free-translation and boundary commands with parity tests. The reviewed changes add writeSegmentFreeTranslation, mergeSegments, splitSegment, and moveBoundary. Boundary commands stage segmentation and split approved phrases cut by split or move in the same ChangeBuffer change set. The parity suites compare host and WebView results for free-translation and boundary cases. The focused boundary implementation and parity test confirm this behavior.

Full details: Out of Scope Changes check

Explanation

The changes support issue #446. ChangeBuffer, scope staging, book lookup, document indexes, phrase guards, stale-translation helpers, reducer tie rules, WebView parity behavior, and tests provide the command implementation or its parity coverage. The reviewed changes show no demonstrated unrelated product change.

Full details: Docstring Coverage

Explanation

Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 60 functions across 21 files. (1 skipped: 1 unsupported.)

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@imnasnainaec
imnasnainaec added this pull request to stack #518 October 26, 2026 21:46
@imnasnainaec

Copy link
Copy Markdown
Contributor Author

Open decisions for whoever takes this over. The current branch takes the first option in each.

  1. Picks by rule in the WebView. The reducer's stale free-translation lookup now uses preferredLink, and phrasesStraddlingBoundary returns cut phrases in link-id order. Only ties change: which stale link a write or keep adopts through, and which of two cut phrases sharing a payload keeps it.
    • Keep: the plan's "Picks by rule", and parity holds whatever order links are stored in.
    • Revert: the host would have to copy list order, which the repository doesn't promise.
  2. splitPhrase drops tokenDocOrder here, changing Add phrase commands with parity tests #516's API. The host builds the order from its verse book. It now throws when the project has no text for the phrase's book.
  3. Only approved phrases break. [3g] Add free-translation and boundary commands #446 says "every phrase the new boundary would cut". Today's dispatch breaks approved phrases only, so a pending or stale phrase can span the new boundary.
    • Copy today.
    • Break every status, in the WebView force-break and here.
  4. A split or move onto an existing segment start still breaks straddling phrases, though the boundary doesn't change. Copied from the WebView. Keep it, or skip the breaks when the transform is a no-op (both sides).
  5. getVerseBook shares getSegments' held-book lookup. A command naming a book that is neither open nor latest makes it the project's latest and drops the previous one's watch. Commands target the open book in practice.
    • Keep the shared lookup.
    • Add a read that leaves the held book alone.
  6. Missing book text throws on merge, split, move and splitPhrase. The WebView can't reach this case. Throw, or stage nothing. [4a] Wire the WebView to the host behind a feature flag #453 has to handle whichever it is.
  7. Free-translation quirks the command copies. For each: keep it, or fix both sides.
    • A blank write that clears nothing still deletes approved segment links naming no stored analysis.
    • Every write restamps updatedAt on the analysis and link, even when the value is unchanged.
    • Clearing the last language deletes the analysis even if another link still names it. This relies on the one-link-per-segment-analysis convention Add stale-handling commands with parity tests #511 documents.
  8. Each boundary command builds every index in buildBookIndexes and uses two of them, uncached. Keep it simple, or cache by book identity as segmentation.ts caches its lookups.

Comment thread src/utils/book-indexes.ts Resolved

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alex-rawlings-yyc reviewed 22 files and all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on imnasnainaec and jasonleenaylor).

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@imnasnainaec reviewed 22 files and all commit messages, and resolved 1 discussion.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on jasonleenaylor).

Base automatically changed from 445-phrase-commands to main October 27, 2026 19:04
alex-rawlings-yyc and others added 8 commits October 27, 2026 13:06
writeSegmentFreeTranslation ports its reducer step for step, keeping the
stale-translation helpers module-private for the stale-handling commands
to reuse. mergeSegments, splitSegment and moveBoundary copy the WebView's
boundary dispatch: a split or move breaks every approved phrase the new
boundary would cut in the same change set as the boundary change, which
rides in the scope record. ChangeBuffer stages and reads the scope and
finds links by book, and a session edit keeps a staged scope when it
lists the analysis language.

The host reads the verse-tokenized book through bookService and builds
document order with the index builder extracted from useBookIndexes, so
splitPhrase no longer takes a caller-supplied token order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WebView's stale free-translation lookup, used to adopt, keep, clear
or discard a stale translation, now picks the stale link an approval tie
picks rather than the first one listed.

A boundary split or move now breaks the phrases it cuts in link-id order
in the WebView as on the host, so when two cut phrases share a payload,
the same one forks on both sides.

Only tie cases change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stale commands already find a stale translation and approve it for a
segment; the free-translation write adopting one now goes through the
same two functions, so keeping and adopting can't drift apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alex-rawlings-yyc
alex-rawlings-yyc merged commit 4c25c13 into main Oct 27, 2026
8 checks passed
@alex-rawlings-yyc
alex-rawlings-yyc deleted the 446-boundary-commands branch October 27, 2026 19:07
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.

[3g] Add free-translation and boundary commands

2 participants