Skip to content

TT-7621 fix: follow real segment changes, ignore spurious ones - #536

Open
nabalone wants to merge 5 commits into
developfrom
TT-7621_pbt-follow-segment-change
Open

TT-7621 fix: follow real segment changes, ignore spurious ones#536
nabalone wants to merge 5 commits into
developfrom
TT-7621_pbt-follow-segment-change

Conversation

@nabalone

@nabalone nabalone commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Split out of #527 so that PR could merge as tests-only. #527 has since merged, so
this now targets develop directly and carries the one behaviour fix plus the
write-up that goes with it.

The bug

Reported from hand testing:

I record the first segment, then the third, and then try to go back and record
the second — it records into and replaces the third.

Clicking segment 2 moves the waveform selection and the playhead there, but the
guided-record step stays on segment 3. The next take is uploaded with segment 3's
source-segments, so it lands on top of the take already there. Nothing warns the
user; the label still reads the old segment, and the audio they just recorded for
segment 2 has silently overwritten segment 3's.

Two other reports turn out to be the same fault seen from different angles:

  • "the yellow highlighting briefly jumps to the next segment" — the waveform paints
    from the engine's index while the label comes from the step's own index, so when
    a click reaches only the engine the two disagree (briefly in the recoverable
    case, indefinitely in the one above);
  • Record being live for the segment the user thought they had just left.

Root cause

PassageDetailContext.currentSegmentIndex is written by three places using two
different numberings
, and nothing forces agreement — the setter takes a bare
index: number:

Convention Where
Writes 1-based (sortedIndex + 1, 0 = whole) business/player/usePlayerLogic.ts:113 — the waveform
Writes 1-based by accident (table row) PassageDetailMarkVerses.tsx (925, 934, 1079, 1389, 1477)
Writes 0-based clause index PassageDetailGuidedPhraseRecord.tsx — 17 call sites
Reads 1-based PassageDetailItem.tsx:246, DiscussionList.tsx:107
Reads 0-based crud/useWavesurferRegions.tsx:955

The step learns the selection moved by watching currentSegmentIndex change. It
never uses the number — it derives its own index from the region via
findClauseIndex — so the field is purely a change signal to it. That is exactly
why the collision went unnoticed: clicking segment 2 makes the waveform write
1 + 1 = 2, and the step had itself just written 2 for segment 3. Same value,
so the dependency never changed, the navigation effect never re-ran, and the step
stayed put while everything visible moved.

The fix

currentSegmentSeq — a token in PassageDetailContext incremented whenever the
current segment actually changes. The step's two navigation effects watch it
instead of inferring a change from the index. currentSegmentIndex stays in the
dependency lists because the value is still read there.

This is deliberately the small fix. Unifying the numbering is the right repair but
touches every caller across Mark Verses, the players and the guided-record step,
and Noel's call is to do it after the next release.

Second fix: an overshoot onto a recorded clause steals the selection

The same navigation effect, the other direction. Reported from hand testing of
this branch:

I record the first segment, right-arrow twice to the third, record it, then
left-arrow to the second. It auto-plays the second segment as expected, but
afterwards the third segment is highlighted, when the second should still be
highlighted for me to record.

Parking after an auto-play arms pendingOvershootSwallowRef, because one spurious
region-in on the next clause follows it — playback overshooting the clause
end, or the recorder mounting once Record is allowed. The effect only consulted
that swallow after its completed-clause branch, so when the clause the
overshoot landed on already had a take (segment 3 here) that branch took the
overshoot for a real move: the label, the yellow selection and the phase all
followed it, and segment 2 went back to pending under a user who was waiting to
record it. Pressing Record then filed the take on segment 3, over the take
already there.

The swallow is now decided before the completed-clause branch, and stops playback
itself, since it no longer inherits the stop from the code below it.

Test

PassageDetailPhraseBackTranslate.selection.cy.tsx
"follows a click back to an earlier segment and files the take there" loses its
DEFECT:/@known-defect marking and now asserts the real consequence, not just
the label: the third take's uploaded source-segments is segment 2's span
(3–6s), and segment 3 still holds exactly one take.

"keeps the parked segment when playback overshoots onto a recorded one", in the
same spec, covers the second fix. It drives the reported arrow sequence, then
uses the harness's tapSegment — an engine segment change with no click behind
it, which is what that spurious region-in looks like to the step — and asserts
that the label and the painted selection both stay on segment 2, that Record stays
armed, and that the next take files at 3–6s with segment 3 still holding exactly
one take.

All four PBT specs on this branch, with develop merged in: 35 passing, 7
pending (@known-defect), 0 failing. npm test matches develop exactly - the
same two suites fail there (MetadataView, on the pre-existing
React.SubmitEvent type error, and a VertListDnd snapshot).

ADR 0010

docs/adr/0010-current-segment-index-numbering.md records the whole situation so
the deferred work is actionable: who writes and reads which convention, what it
broke here, the recommended unification (pick 1-based; make the convention
unmissable rather than conventional), and what becomes removable when it happens —
currentSegmentSeq, its ref, and the two dependency-list entries. The other
TT-7621 fixes (#528, #529, #530, #532) are unrelated to the numbering and stay.

It also documents a separate latent off-by-one the same split causes, at
crud/useWavesurferRegions.tsx:955:

onRegionGoTo(regarray[defaultRegionIndex]?.start ?? 0);

defaultRegionIndex is the context's currentSegmentIndex, so a region load that
reads a 1-based value selects the segment after the current one. Every region
load is affected — WSAudioPlayer 889, 1042, 1370, 1517 (loadRegionsJson, which
Split, Combine, segment undo and Reset all call). It is not reproducible from PBT
(an effect there continuously re-asserts a 0-based index, so a load usually reads
the right convention by accident); Mark Verses and the generic segment player are
where to look. No fix attempted here.

Review note on the numbering

Every currentSegmentSeq touchpoint in this diff is scaffolding around the
numbering split, not something we want permanently. Reviewers: read it as "this
exists because the index cannot be trusted as a change signal", and expect it to
come out with ADR 0010's unification after the next release.

Base automatically changed from TT-7621_pbt-segment-selection-and-recorder-state to develop August 24, 2026 22:33
nabalone and others added 2 commits August 25, 2026 08:39
Recording segment 1, then segment 3, then clicking back to segment 2 filed the
next take on segment 3, on top of the take already there.

The step learns that the selection moved by watching currentSegmentIndex, but
that field's numbering is not agreed between its writers: usePlayerLogic writes
the waveform's 1-based sorted position (0 meaning "whole"), Mark Verses writes a
table row, and this component writes a 0-based clause index. So a real move can
arrive carrying the number the previous writer used - selecting segment 2 puts
1+1 there, which is what the step itself had just written for segment 3 - and
the navigation effect never re-runs. The waveform selection and playhead move,
the step does not, and the take goes wherever the step still thinks it is.

Adds currentSegmentSeq to PassageDetailContext: a token bumped whenever the
current segment actually changes. The two navigation effects watch it instead of
inferring a change from the index. The step never used the number itself - it
derives its own index from the region - so it now depends only on what it
actually needs.

Unifying the numbering would remove the need for this and is worth doing, but it
touches every caller across Mark Verses, the players and this step, so it is
deliberately not in here. There is also a latent off-by-one at
useWavesurferRegions.tsx:955 that the same discrepancy causes; see the write-up
that accompanies this branch.

Test: "follows a click back to an earlier segment and files the take there" now
passes, and asserts the take's uploaded source-segments is segment 2's span and
that segment 3 still holds exactly one take.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR 0010. Writes up the 0-based/1-based split in
PassageDetailContext.currentSegmentIndex: who writes which convention, who reads
which, what it broke on this ticket, and the latent off-by-one at
useWavesurferRegions.tsx:955 that the same split causes.

Deferred on purpose - unifying touches every caller across Mark Verses, the
players and the guided-record step, and Noel's call is to do it after the next
release. The ADR says what becomes removable then (currentSegmentSeq and the two
navigation dependency lists) and what does not (the other TT-7621 fixes, which
are unrelated to the numbering).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nabalone
nabalone force-pushed the TT-7621_pbt-follow-segment-change branch from 1cae219 to 60cd79d Compare August 25, 2026 12:40
@nabalone
nabalone marked this pull request as draft August 25, 2026 12:51
nabalone added a commit that referenced this pull request Aug 25, 2026
This branch was cut before #527 gained the `@known-defect` tagging, so its
copy of the defects spec won the merge and took the tags with it. Every
still-broken repro then ran in CI and failed the check.

Restores develop's tags on the three defects this branch does not fix, and
keeps the pause repro untagged — that one is fixed here, so it should run.

Also tags `clicking back to segment 2 leaves the step on segment 3` in the
selection spec. It is a known defect (fixed in #536) that #527 missed, so it
was failing CI untagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nabalone added a commit that referenced this pull request Aug 25, 2026
This branch was cut before #527 gained the `@known-defect` tagging, so its
copy of the defects spec won the merge and took the tags with it. Every
still-broken repro then ran in CI and failed the check.

Restores develop's tags on the three defects this branch does not fix, and
keeps the loading-hang repro untagged — that one is fixed here, so it should
run.

Also tags `clicking back to segment 2 leaves the step on segment 3` in the
selection spec. It is a known defect (fixed in #536) that #527 missed, so it
was failing CI untagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gtryus pushed a commit that referenced this pull request Aug 25, 2026
* TT-7621 fix: clear the loading flag when a load is abandoned

Leaving a segment while its take was still loading killed the recorder: the
status line stuck, and Record stayed disabled on that segment and every later
one. Only leaving the step and coming back recovered it, which is what the
hung-PBT report describes.

MediaRecord holds `loading` until an effect sees `blobReady && originalBlob`.
Both abandon paths - mediaId becoming undefined, and an explicit doReset, which
this step triggers on every segment change - call reset(), which drops
originalBlob. Once that happened mid-load the condition could never be met
again, so `loading` stayed true, and the record button is disabled by
Boolean(loading).

Abandoning a load now clears the flag it set. Kept separate from reset() on
purpose: handleLoadAudio calls reset() itself right after setting loading, so
clearing it in there would cancel every load immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* TT-7621 fix: gate the abandon on a load-only flag (Devin)

Devin flagged that abandonLoadInFlight gated on a mirror of `loading`, which the
save path sets too (MediaRecord.tsx:522). A doReset arriving mid-save - and this
step sets resetMedia from handleClearRecording, which can run during a save -
would have cleared the flag and the status text, re-enabling Record and dropping
"Saving..." while the save was still running. Correct, and reachable.

Replaced the mirror with loadInFlightRef, true only while handleLoadAudio is
fetching a take, cleared wherever that load ends. It is also set synchronously
rather than in an effect, which was Devin's second point: the [mediaId] and
[doReset] effects now see it immediately instead of one commit late.

jest MediaRecord + MediaRecord.load: 13 green. The loading-hang repro stays
fixed; edit spec 16 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* TT-7621 test: keep the still-broken defect repros out of CI

This branch was cut before #527 gained the `@known-defect` tagging, so its
copy of the defects spec won the merge and took the tags with it. Every
still-broken repro then ran in CI and failed the check.

Restores develop's tags on the three defects this branch does not fix, and
keeps the loading-hang repro untagged — that one is fixed here, so it should
run.

Also tags `clicking back to segment 2 leaves the step on segment 3` in the
selection spec. It is a known defect (fixed in #536) that #527 missed, so it
was failing CI untagged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
nabalone and others added 2 commits August 25, 2026 16:02
…ision

ADR 0010 records that currentSegmentIndex is written with two numberings. The
same field has a second problem: a change to it does not say whether the user
moved the selection or the playhead did. The guided-record step has to guess,
and four mechanisms exist to do that guessing - pendingOvershootSwallowRef, the
suppressClauseAutoPlay counter (four expected phantom events, a literal nobody
can derive), currentSegmentSeq, and the per-event onSegmentClick exemption.

ADR 0011 writes up why the intuitive fix - stopping playback precisely at the
clause boundary - does not work, since the regions plugin's membership test is
inclusive at both ends and contiguous clauses share that boundary. It then
documents the two refactors that would: tagging every setCurrentSegment write
with its source, and turning targeted region playback into an explicit
operation with a lifecycle rather than a race between region-out and a
two-frame programmatic-seek window.

Cross-referenced from ADR 0010, since both touch the same 17 call sites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed one

Parking after an auto-play arms the overshoot swallow, because one spurious
region-in on the next clause follows (playback overshoot, or the recorder
mounting). The navigation effect only consulted that swallow after its
completed-clause branch, so when the clause the overshoot landed on already had
a take the step read it as a real move: the label, the yellow selection and the
phase all followed, and the clause the user was parked on and about to record
went back to pending.

Reported from hand testing: record segment 1, arrow forward to segment 3, record
it, then arrow back to segment 2. Segment 2 auto-plays as expected, and then
segment 3 is left selected.

Decide the swallow first, and stop playback inside that branch since it no
longer inherits the stop from the code below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nabalone nabalone changed the title TT-7621 fix: follow a segment change the waveform reports TT-7621 fix: follow real segment changes, ignore spurious ones Aug 25, 2026
develop brought #530's fix and its untagged defects spec, plus TT-7631/TT-7634.

The selection spec conflicted on the segment-click test: develop still carries it
as the `DEFECT:`/`@known-defect` repro, this branch carries the fixed, untagged
version that asserts where the take lands. Kept this branch's version - the fix
it asserts is in this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nabalone
nabalone marked this pull request as ready for review August 25, 2026 21:27
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