Skip to content

Commit 76bce97

Browse files
committed
Fix bug where book can be selected for drafting and training without chapter selection
1 parent dc76539 commit 76bce97

2 files changed

Lines changed: 59 additions & 4 deletions

File tree

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,32 @@ describe('NewDraftLogicHandler', () => {
643643
expect(env.selectedTrainingSourceBooks['training-source-1-id']).toContain('MAT');
644644
expect(env.selectedTrainingSourceBooks['training-source-2-id']).not.toContain('MAT');
645645
});
646+
647+
it('fully excludes a whole-book draft from training, even chapters the source cannot draft', async () => {
648+
// The drafting source has only a couple of chapters of Mark (below the partial-drafting threshold), so Mark
649+
// can only be drafted whole. The target has more chapters of Mark translated than the source contains.
650+
const env = new TestEnvironment({
651+
lastSelectedTranslationScriptureRanges: undefined,
652+
previouslySelectedTrainingScriptureRanges: undefined,
653+
draftingSourceBooksChapters: 'MRK1-2',
654+
targetProjectBooksChapters: 'MRK1-7;LUK1-24',
655+
trainingSourcesBooksChapters: {
656+
'training-source-1-id': 'MRK1-16;LUK1-24'
657+
}
658+
});
659+
await env.waitForInit();
660+
661+
// Mark's source has only 2 chapters of content (< 12), so Mark is not eligible for partial drafting. It is
662+
// drafted whole, with no chapter selector offered.
663+
env.logicHandler.selectDraftingBooks(['MRK']);
664+
expect(env.booksOfferedForPartialDrafting).not.toContain('MRK');
665+
666+
env.logicHandler.setInputMode('training_books');
667+
668+
// Mark is being drafted as a whole book, so it must not be available for training at all. That includes the
669+
// target chapters beyond the source's 2 (MRK3-7), which is what previously leaked through.
670+
expect(env.logicHandler.availableTargetTrainingScriptureRange.books.has('MRK')).toBe(false);
671+
});
646672
});
647673

648674
describe('auto-selecting training books on first visit', () => {

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,12 +699,42 @@ export class NewDraftLogicHandler {
699699
}
700700
}
701701

702+
/**
703+
* The scripture range to withhold from the training options, so that training data never overlaps what is being
704+
* drafted. It is not simply selectedDraftingScriptureRange, which holds only the chapters the source can draft and
705+
* is submitted verbatim as the draft request. What counts as "being drafted" for this purpose depends on how the
706+
* book was selected:
707+
*
708+
* - Partial-eligible book: the user was shown a chapter selector, so only the chapters they chose count. Those are
709+
* withheld, and the book's remaining chapters stay available for training (the "draft some, train on the rest" case).
710+
* - Whole-book draft: no chapter selector was offered, so the whole book counts as being drafted, including the
711+
* chapters the source cannot draft. The entire book is withheld, so those chapters are not silently offered as
712+
* training data.
713+
*/
714+
private draftingRangeExcludedFromTraining(): VerboseScriptureRange {
715+
const excluded = new VerboseScriptureRange();
716+
for (const [bookId, draftedChapters] of this.selectedDraftingScriptureRange.books) {
717+
if (this.isBookEligibleForPartialDrafting(bookId)) {
718+
excluded.books.set(bookId, draftedChapters.clone());
719+
} else {
720+
// Withhold the whole book. Use its chapters in the target, since the training options are derived from the
721+
// target's content; a book with no target content has nothing to withhold.
722+
const targetChapters = this.targetProjectScriptureRange.books.get(bookId);
723+
if (targetChapters != null) {
724+
excluded.books.set(bookId, targetChapters.clone());
725+
}
726+
}
727+
}
728+
return excluded;
729+
}
730+
702731
private limitAvailableTrainingRangeBasedOnSelectedDraftingRange(): void {
703732
// Available target training books are the target's content minus what's being drafted, further limited to books
704733
// that exist in at least one training source: a target book can only be used as training data if a source
705734
// provides the matching book to pair it with. Books with no such source are recorded
706735
// (targetTrainingBooksWithoutSource) so the UI can explain why they aren't offered.
707-
const targetTrainingRange = this.targetProjectScriptureRange.difference(this.selectedDraftingScriptureRange);
736+
const excludedFromTraining = this.draftingRangeExcludedFromTraining();
737+
const targetTrainingRange = this.targetProjectScriptureRange.difference(excludedFromTraining);
708738
const booksInAnyTrainingSource = new Set(Object.values(this.trainingSourceBooks).flat());
709739

710740
const availableTargetTrainingRange = new VerboseScriptureRange();
@@ -718,9 +748,8 @@ export class NewDraftLogicHandler {
718748
}
719749
this.availableTargetTrainingScriptureRange = availableTargetTrainingRange;
720750
this.targetTrainingBooksWithoutSource = booksWithoutSource;
721-
this.selectedTargetTrainingScriptureRange = this.selectedTargetTrainingScriptureRange.difference(
722-
this.selectedDraftingScriptureRange
723-
);
751+
this.selectedTargetTrainingScriptureRange =
752+
this.selectedTargetTrainingScriptureRange.difference(excludedFromTraining);
724753

725754
// Limit available and selected training source books to not exceed available target training scripture range
726755
const availableTargetRange = this.availableTargetTrainingScriptureRange;

0 commit comments

Comments
 (0)