Skip to content

SF-3817 Partial book drafting#3964

Open
Nateowami wants to merge 13 commits into
masterfrom
feature/SF-3817-partial-book-drafting
Open

SF-3817 Partial book drafting#3964
Nateowami wants to merge 13 commits into
masterfrom
feature/SF-3817-partial-book-drafting

Conversation

@Nateowami

@Nateowami Nateowami commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

This PR creates a new implementation of the stepper for generating drafts, while leaving the old one alone. I chose an architecture where as much of the business logic as possible is put into NewDraftLogicHandler, where it can be unit tested without UI concerns. It is used by NewDraftComponent to determine which books and chapters are available for training or drafting.

New components

  • NewDraftComponent: Stepper that uses NewDraftLogicHandler
  • DraftPendingUpdatesComponent: Prompts user to sync projects before generating a draft (pretty basic implementation; could use UX improvement)
  • ExperimentalFeaturesDialogComponent: Allows users to enable experimental features (in this case partial book drafting being the only available feature)

Back end changes

  • Back end had to be updated to accept an explicit target training range (rather than inferring from source training ranges), while still accepting the format the legacy stepper provides.

Front end changes

  • Front-end now prepends book ID to USFM files if a user is downloading a book that didn't draft chapter 1
  • Import dialog updated to only import drafted chapters
  • Draft preview books component updated to state which chapters were drafted
  • Book multi select needed to be updated to be a bit more flexible (and prevent browser hang I've run into a number of times)

Data model changes

  • New property added to remember which files were available at the last build, so a later build can distinguish newly-added files (default to selected) from files the user deliberately deselected (it would probably be better to have a key/value record indicating which files were selected, but given the existing implementation leaves a gap it's easier to just add a second array).
  • Project progress now reports at a chapter level, rather than just a book level

Other

  • New VerboseScriptureRange tracks selection at a chapter level (It's verbose in the sense that expressing which chapters are selected for each book isn't merely something it can do, rather chapters must always be listed. This contrasts with a standard scripture range where "GEN" means "all chapters of Genesis" and in a VerboseScriptureRange it would mean "no chapters of Genesis". In order to implement chapter-level logic correctly, chapters have to be explicit since they're used for more than just filtering)
  • Some helper functions created to prevent duplication
  • E2E test for partial book drafting

This change is Reviewable

@Nateowami Nateowami added will require testing PR should not be merged until testers confirm testing is complete e2e Run e2e tests for this pull request labels Jun 23, 2026
@Nateowami Nateowami temporarily deployed to screenshot_diff June 23, 2026 18:32 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

📸 Screenshot diff deployed! (25 changes)

View the visual diff at: https://pr-3964--sf-screenshot-diffs.netlify.app

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.11888% with 216 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.06%. Comparing base (6e818b7) to head (09405c0).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../draft-generation/new-draft/new-draft.component.ts 72.26% 76 Missing and 23 partials ⚠️
...ft-generation/new-draft/new-draft-logic-handler.ts 82.50% 43 Missing and 3 partials ⚠️
.../SIL.XForge.Scripture/Services/SFProjectService.cs 0.00% 28 Missing ⚠️
...ate/draft-generation/draft-generation.component.ts 37.50% 5 Missing ⚠️
...pture/Services/NullableSequenceEqualityComparer.cs 0.00% 4 Missing and 1 partial ⚠️
src/SIL.XForge.Scripture/Models/BookProgress.cs 0.00% 4 Missing ⚠️
...XForge.Scripture/Services/MachineProjectService.cs 80.00% 1 Missing and 3 partials ⚠️
...ipture/ClientApp/src/app/shared/scripture-range.ts 97.50% 2 Missing and 1 partial ⚠️
...aft-preview-books/draft-preview-books.component.ts 84.21% 2 Missing and 1 partial ⚠️
...pending-updates/draft-pending-updates.component.ts 94.44% 1 Missing and 2 partials ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3964      +/-   ##
==========================================
+ Coverage   81.03%   81.06%   +0.02%     
==========================================
  Files         645      657      +12     
  Lines       41522    42510     +988     
  Branches     6763     6968     +205     
==========================================
+ Hits        33649    34462     +813     
- Misses       6761     6901     +140     
- Partials     1112     1147      +35     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Nateowami Nateowami left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@Nateowami made 1 comment.
Reviewable status: 0 of 75 files reviewed, 1 unresolved discussion.


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 151 at r1 (raw file):

   * Overridable on the class (NewDraftLogicHandler.ALLOW_DRAFTING_BOOKS_NOT_IN_TARGET) so tests can exercise both branches.
   */
  static ALLOW_DRAFTING_BOOKS_NOT_IN_TARGET = false;

Once #3965 is merged (likely before this PR is merged) this can be changed ripped out and all code paths that depend on it can assume it's always true.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new “New Draft” wizard/stepper to support partial-book (chapter-level) drafting & training behind an experimental feature flag, while keeping the legacy draft-generation flow intact. It also extends the backend build/config plumbing to accept an explicit target training scripture range and persists additional client selection state (available training data files), plus enhances progress reporting to include chapter-level detail.

Changes:

  • Added a new draft-generation wizard flow (UI + supporting utilities), plus an E2E workflow covering partial-book drafting.
  • Updated build configuration handling end-to-end (JSON converter, API persistence, training-range derivation) to support explicit target training ranges and “available training files”.
  • Extended project progress reporting to return chapter-level progress, and updated client code to consume/format chapter-level draft ranges.

Reviewed changes

Copilot reviewed 75 out of 75 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/SIL.XForge.Scripture.Tests/Services/MachineProjectServiceTests.cs Adds test for explicit target training range behavior
test/SIL.XForge.Scripture.Tests/Services/MachineApiServiceTests.cs Adds test for persisting available training data files
test/SIL.XForge.Scripture.Tests/Services/BuildConfigJsonConverterTests.cs Adds serialization/deserialization coverage for available training files
src/SIL.XForge.Scripture/Services/SFProjectService.cs Adds chapter-level progress aggregation + mapping
src/SIL.XForge.Scripture/Services/MachineProjectService.cs Supports explicit target training scripture range (fallback for legacy)
src/SIL.XForge.Scripture/Services/MachineApiService.cs Persists last-available training files when reported
src/SIL.XForge.Scripture/Services/BuildConfigJsonConverter.cs Serializes AvailableTrainingDataFiles when present
src/SIL.XForge.Scripture/Models/DraftConfig.cs Adds LastAvailableTrainingDataFiles persisted field
src/SIL.XForge.Scripture/Models/BuildConfig.cs Adds AvailableTrainingDataFiles to request model
src/SIL.XForge.Scripture/Models/BookProgress.cs Adds chapter-level progress DTOs
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/util/set-util.ts Adds set composition helpers (polyfill-style)
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/util/set-util.spec.ts Unit tests for set helpers
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/i18n.service.ts Caches Intl.ListFormat; adds formatToParts helper
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/feature-flags/feature-flag.service.ts Adds PartialBookDrafting feature flag
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features.service.ts Adds experimental-features wrapper/service
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features-dialog.component.ts Adds experimental features dialog component
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features-dialog.component.spec.ts Unit tests for experimental-features dialog
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features-dialog.component.scss Styles for experimental-features dialog
src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features-dialog.component.html Dialog template for toggling experimental features
src/SIL.XForge.Scripture/ClientApp/src/assets/i18n/non_checking_en.json Adds NewDraft translation keys (non-checking bundle)
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/training-data-summary.ts Chapter-aware training-book summary formatting
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/training-data-summary.spec.ts Tests for training-book summary formatting
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/training-data-file-selection.ts Default selection logic for training data files
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/training-data-file-selection.spec.ts Tests for default training-file selection logic
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft.component.stories.ts Storybook coverage for NewDraft wizard states
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft.component.scss Styles for NewDraft wizard
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft.component.html NewDraft wizard template (steps, chapter inputs, summary)
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/draft-pending-updates/draft-pending-updates.component.ts Adds pre-step interstitial to sync pending updates
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/draft-pending-updates/draft-pending-updates.component.stories.ts Storybook scenarios for pending-updates interstitial
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/draft-pending-updates/draft-pending-updates.component.spec.ts Unit tests for pending-updates interstitial logic
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/draft-pending-updates/draft-pending-updates.component.scss Styles for pending-updates interstitial
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/draft-pending-updates/draft-pending-updates.component.html Template for pending-updates interstitial
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.ts Filters out unloaded targetDoc emissions to avoid empty latch
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-sources.service.spec.ts Updates tests for new filter behavior
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-source.ts Dedupes copyright banners; adds helper
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-signup-form/draft-onboarding-form.component.html Adjusts BookMultiSelect usage (projectName binding removed)
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-preview-books/draft-preview-books.component.ts Displays drafted chapter ranges; navigates to first drafted chapter
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-preview-books/draft-preview-books.component.spec.ts Tests for partial-book preview behavior
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-preview-books/draft-preview-books.component.html Shows drafted chapter range in book button
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts Imports only drafted chapters; scopes overwrite warnings
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.spec.ts Tests for partial-draft import scoping
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.ts Extends frontend BuildConfig with availableTrainingDataFiles
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.ts Prepends \id when missing for partial chapter downloads
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.service.spec.ts Tests for \id prepending behavior
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.ts Routes to NewDraft when feature flag enabled
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation.component.spec.ts Fixes/updates assertion around existing draft run behavior
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-generation-steps/draft-generation-steps.component.ts Reuses shared “book appears complete” logic + copyright helper
src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/confirm-sources/confirm-sources.component.scss Makes h1 margin configurable via CSS var
src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.ts Changes expandNumbers to use ChapterSet + return null on invalid
src/SIL.XForge.Scripture/ClientApp/src/app/shared/utils.spec.ts Updates expandNumbers expectations for invalid input
src/SIL.XForge.Scripture/ClientApp/src/app/shared/scripture-range.ts Adds ChapterSet + VerboseScriptureRange chapter-level model
src/SIL.XForge.Scripture/ClientApp/src/app/shared/scripture-range.spec.ts Tests for ChapterSet + VerboseScriptureRange
src/SIL.XForge.Scripture/ClientApp/src/app/shared/progress-service/progress.service.ts Adds chapter progress types; improves request coalescing semantics
src/SIL.XForge.Scripture/ClientApp/src/app/shared/progress-service/progress.service.spec.ts Updates tests for new progress types and refresh behavior
src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.ts Avoids repeated progress fetches; prevents change-detection loops
src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.spec.ts Adds tests for progress-fetch caching behavior
src/SIL.XForge.Scripture/ClientApp/src/app/shared/book-multi-select/book-multi-select.component.html Tweaks header rendering conditions and scope selection block
src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-builds.component.ts Reuses ChapterSet for compact range formatting
src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-builds.component.spec.ts Minor test adjustment (explicit expect().nothing())
src/SIL.XForge.Scripture/ClientApp/src/app/serval-administration/serval-build-report.ts Treats invalid chapter notation as whole-book fallback
src/SIL.XForge.Scripture/ClientApp/src/app/core/sf-project.service.ts Updates progress API typing + expandNumbers null-handling
src/SIL.XForge.Scripture/ClientApp/src/app/app.routes.ts Adds route for NewDraft wizard
src/SIL.XForge.Scripture/ClientApp/src/app/app.component.ts Adds experimental-features dialog wiring
src/SIL.XForge.Scripture/ClientApp/src/app/app.component.html Adds menu item to open experimental features when available
src/SIL.XForge.Scripture/ClientApp/e2e/workflows/partial-book-drafting.ts Adds E2E workflow for partial-book drafting
src/SIL.XForge.Scripture/ClientApp/e2e/test-definitions.ts Registers partial-book drafting E2E test
src/SIL.XForge.Scripture/ClientApp/e2e/test_data/partial_draft_training_1.tsv Adds E2E training-data fixture
src/SIL.XForge.Scripture/ClientApp/e2e/test_data/partial_draft_training_2.tsv Adds E2E training-data fixture
src/SIL.XForge.Scripture/ClientApp/e2e/test_characterization.json Adds characterization stats for new E2E test
src/SIL.XForge.Scripture/ClientApp/e2e/characterize-tests.mts Adjusts characterization retry threshold
src/RealtimeServer/scriptureforge/models/translate-config.ts Adds lastAvailableTrainingDataFiles to DraftConfig interface

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +22
} @empty {
<p>No experimental features are currently available.</p>
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was deliberate because the dialog should never be shown unless there are actually experimental features available. However, since the dialog can't enforce that itself, I defined the empty state for completeness. I don't want to waste translators' time by having them translate strings users won't actually see.

Comment on lines +346 to +353
if (this.featureFlags.partialBookDrafting.enabled) {
const projectId = this.activatedProject.projectId;
if (projectId != null) {
await this.router.navigate(['/projects', projectId, 'draft-generation', 'new-draft']);
}
} else {
this.currentPage = 'steps';
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is merely theoretical. It shouldn't be possible to be on this page and have this.activatedProject.projectId be nullish except in a very weird edge case/race condition. And if we reach that, this function won't crash, and the button is the least of our worries.

Comment on lines +96 to +109
async initBookOptions(): Promise<void> {
// Only load progress if not in basic mode
let progress: ProjectProgress | undefined;
if (this.basicMode === false) {
// projectId may arrive asynchronously; show loading state until it arrives.
if (this.projectId == null) {
throw new Error('app-book-multi-select requires a projectId input to initialize when not in basic mode');
this.loaded = false;
return;
}
if (this.projectId !== this.loadedProgressProjectId) {
this.cachedProgress = await this.progressService.getProgress(this.projectId, { maxStalenessMs: 30_000 });
this.loadedProgressProjectId = this.projectId;
}
progress = await this.progressService.getProgress(this.projectId, { maxStalenessMs: 30_000 });
progress = this.cachedProgress;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is really quite an edge case (project ID really shouldn't change), but the fix is simple so I've added a followup commit.

@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from 50de8dd to 5a323bc Compare June 25, 2026 19:11
@Nateowami Nateowami temporarily deployed to screenshot_diff June 25, 2026 19:19 — with GitHub Actions Inactive
@Nateowami Nateowami temporarily deployed to screenshot_diff June 25, 2026 21:31 — with GitHub Actions Inactive
@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from d33d165 to 0baa9a6 Compare June 25, 2026 22:02
@Nateowami Nateowami temporarily deployed to screenshot_diff June 25, 2026 22:08 — with GitHub Actions Inactive

@RaymondLuong3 RaymondLuong3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am about halfway through this review. Nothing major to point out.

@RaymondLuong3 partially reviewed 41 files and made 7 comments.
Reviewable status: 40 of 75 files reviewed, 10 unresolved discussions (waiting on Nateowami).


src/SIL.XForge.Scripture/ClientApp/e2e/workflows/partial-book-drafting.ts line 156 at r1 (raw file):

  await chapterInput.fill('23-21');
  await chapterInput.blur();
  await expect(page.locator('.partial-book-drafting-table .chapter-error')).toBeVisible();

Nit: to directly contrast between this and 3 lines below, this could be written as .toHaveCount(1).

Code quote:

.toBeVisible();

src/SIL.XForge.Scripture/ClientApp/src/app/shared/scripture-range.ts line 6 at r1 (raw file):

export class ChapterSet {
  static chapterRangeSeparator = ',';
  static chapterRangeStartEndSeparator = '-';

These need to be readonly too.

Code quote:

  static chapterRangeSeparator = ',';
  static chapterRangeStartEndSeparator = '-';

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts line 320 at r1 (raw file):

    this.draftedScriptureRange = VerboseScriptureRange.fromCombinedRanges(
      (this.data.additionalInfo?.translationScriptureRanges ?? []).map(range => range.scriptureRange)
    );

I think this initializes a draftedScriptureRange with no chapters instead of all chapters assuming that translationScriptureRanges returns the previous ScriptureRange that does not require specific chapters. I see below it accounts for this an the empty ChapterSet is treated as book level drafting.

Code quote:

    this.draftedScriptureRange = VerboseScriptureRange.fromCombinedRanges(
      (this.data.additionalInfo?.translationScriptureRanges ?? []).map(range => range.scriptureRange)
    );

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 232 at r1 (raw file):

  /**
   * Sets up the state by loading the project, checking for changes in Paratext that haven't synced to SF yet, loading
   * progress data, and setting up subscripts that watch for changes that should result in bailing out (forcing the

Nit: Should say subscriptions

Code quote:

setting up subscripts 

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 308 at r1 (raw file):

    const staleness = (id: string): { maxStalenessMs?: number } =>
      freshProjectIds.has(id) ? { maxStalenessMs: 0 } : {};

This syntax is really hard for me to understand. It looks like the type for staleness is a function that accepts an projectId and returns an object with maxStalenessMs. Can this be made into a function of the class?

Code quote:

    const staleness = (id: string): { maxStalenessMs?: number } =>
      freshProjectIds.has(id) ? { maxStalenessMs: 0 } : {};

src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 513 at r1 (raw file):

      sourceChapterCount >= MIN_SOURCE_CHAPTERS_FOR_PARTIAL_DRAFTING &&
      targetChaptersWithContent != null &&
      targetChaptersWithContent >= 1

This looks like we only allow books for target drafting if the book has content in at least one chapter. I understand it here, but it might be tricky communicating with users why some books allows partial drafting and others do not. Just noting that here.

Code quote:

      targetChaptersWithContent >= 1

@Nateowami Nateowami left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you so much for reviewing Raymond. This PR is basically a marathon to review.

@Nateowami made 7 comments and resolved 1 discussion.
Reviewable status: 34 of 80 files reviewed, 9 unresolved discussions (waiting on RaymondLuong3).


src/SIL.XForge.Scripture/ClientApp/e2e/workflows/partial-book-drafting.ts line 156 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

Nit: to directly contrast between this and 3 lines below, this could be written as .toHaveCount(1).

Done


src/SIL.XForge.Scripture/ClientApp/src/app/shared/scripture-range.ts line 6 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

These need to be readonly too.

Good point. Done.


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/draft-import-wizard/draft-import-wizard.component.ts line 320 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

I think this initializes a draftedScriptureRange with no chapters instead of all chapters assuming that translationScriptureRanges returns the previous ScriptureRange that does not require specific chapters. I see below it accounts for this an the empty ChapterSet is treated as book level drafting.

Yes, that's correct. We store translationScriptureRanges as an array (even though there's really only one range; theoretically in the future we could draft from multiple projects), then join them together.

It's really quite ugly that an empty set is treated as meaning "everything", even though as it's used here the logic all holds together.

It might be less ugly if we represented "chapters not specified" as a null or undefined ChapterSet (or defined the ChapterSet type as something like type ChapterSet = Set<number> | 'all_chapters', But then the difference set operation will be undefined (You can't do (all chapters in Genesis) - (Genesis 14-28) and get an answer without knowing the number of chapters in the project the chapters are in. In practice though, there are only two calls to .difference, both in the same method, so probably I should have just handled the difficult edge case there. I'm not sure. Alternatively, we could have ScriptureRange and VerboseScriptureRange, where VerboseScriptureRange can be converted to ScriptureRange, but you can't convert the other way, and difference is only defined on VerboseScriptureRange.

I'm open to making a change if we can figure out what the actual best approach would be.


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 232 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

Nit: Should say subscriptions

Good catch. Done.


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 308 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

This syntax is really hard for me to understand. It looks like the type for staleness is a function that accepts an projectId and returns an object with maxStalenessMs. Can this be made into a function of the class?

I realize it's a bit ugly, but I think it's the most elegant solution. It's a local helper function that takes an id arg, and also can access freshProjectIds, which is a local variable. If it were a method on the class it would have to take two arguments. And nothing outside this method would care about it.


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 513 at r1 (raw file):

Previously, RaymondLuong3 (Raymond Luong) wrote…

This looks like we only allow books for target drafting if the book has content in at least one chapter. I understand it here, but it might be tricky communicating with users why some books allows partial drafting and others do not. Just noting that here.

Yes, that's a design decision that has tradeoffs. The reason is that we don't want to offer partial book drafting to users unless there might be a reason for them to use it, in the interest of keeping the UI simple when possible.

I do think we might need to change the logic somewhat; there's a threshold for "does this chapter have enough content to be drafted or used for training", which is quite minimal. If you have a few verses and you want to draft them, or use them for training, we're not going to stop you.

But maybe it shouldn't be the same threshold for deciding what should be selected by default. If a chapter has only a couple verses, it probably makes more sense to draft it than to keep it as training data. I haven't fully worked out whether that's the right option or not. The spec we came up with before I implemented was pretty clear about what to do with chapters with content vs not content, but less clear on drawing the exact lines for measuring content.

@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from 43da4d3 to 1c6ad39 Compare June 26, 2026 16:59
@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from b35e51e to 0aa4927 Compare July 10, 2026 02:46

@Nateowami Nateowami left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've just pushed changes for responding to code review, and more major changes responding to what testers found (and then what I found). Mostly around showing correct drafting/training ranges throughout the application.

@Nateowami made 4 comments and resolved 1 discussion.
Reviewable status: 57 of 95 files reviewed, 9 unresolved discussions (waiting on pmachapman and RaymondLuong3).


src/SIL.XForge.Scripture/ClientApp/src/app/translate/draft-generation/new-draft/new-draft-logic-handler.ts line 60 at r4 (raw file):

Previously, pmachapman (Peter Chapman) wrote…

Should this be in a separate file?

Yes, I should do this.


src/SIL.XForge.Scripture/ClientApp/src/xforge-common/experimental-features/experimental-features.service.ts line 27 at r4 (raw file):

Previously, pmachapman (Peter Chapman) wrote…

Do you think these strings should be localized?

No, not really. We really don't expect that many people to turn on the experimental feature (we'll ask specific people to try it), and won't be able to get effective feedback from non-English speakers anyway.


src/SIL.XForge.Scripture/ClientApp/src/xforge-common/util/set-util.ts line 8 at r4 (raw file):

Previously, pmachapman (Peter Chapman) wrote…

Do we want to implement these as polyfills?

For example, if you change the signature to:

export function intersection<T>(this: Set<T>, b: ReadonlySet<T>): Set<T>

You can then add this to polyfills.ts:

import { intersection } from 'xforge-common/util/set-util';

if (!Set.prototype.intersection) {
  Set.prototype.intersection = intersection;
}

And use the ES2024 syntax everywhere you use intersect.

I prefer not to. Implementing them as polyfills means:

  • Mutating global state
  • Making actual code path differ depending on browsers
  • Not actually exercising the code in tests because our tests run in newer browsers

(A lot of JavaScript developers frown on polyfills)

If, on the other hand, we had a dependency that needed them, it would make sense to polyfill, though I would want to use an implementation that has been carefully crafted to meet web standards so there aren't subtle differences in how edge cases are handled.

@pmachapman pmachapman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@pmachapman reviewed 38 files and all commit messages, and resolved 2 discussions.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on Nateowami and RaymondLuong3).

@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from 0aa4927 to 99b0f2a Compare July 13, 2026 20:56

@pmachapman pmachapman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@pmachapman reviewed 12 files and all commit messages.
Reviewable status: all files reviewed, 7 unresolved discussions (waiting on Nateowami and RaymondLuong3).

@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from 99b0f2a to c9e20d9 Compare July 14, 2026 02:52
@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from c9e20d9 to 903277f Compare July 14, 2026 02:57
@Nateowami Nateowami temporarily deployed to screenshot_diff July 14, 2026 03:04 — with GitHub Actions Inactive
@Nateowami Nateowami force-pushed the feature/SF-3817-partial-book-drafting branch from 903277f to 09405c0 Compare July 14, 2026 14:59
@Nateowami Nateowami deployed to e2e_tests July 14, 2026 15:02 — with GitHub Actions Active
@Nateowami Nateowami deployed to screenshot_diff July 14, 2026 15:07 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

e2e Run e2e tests for this pull request will require testing PR should not be merged until testers confirm testing is complete

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants