Split published Paratext projects into a dedicated PDP factory#2342
Split published Paratext projects into a dedicated PDP factory#2342tjcouch-sil wants to merge 12 commits into
Conversation
…s unpublished lists Add GetParatextProjectInterfaces(bool isPublished) overload that returns the published list (no legacyCommentManager.comments) when isPublished is true. The no-arg overload still returns the full unpublished list for backward compatibility. This is the foundation for splitting ParatextProjectDataProviderFactory. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ourceProject Route ScrTextExtensions.GetProjectDetails() through the new GetParatextProjectInterfaces(bool isPublished) overload so each scrText reports the interface list that matches its actual capabilities. No behavior change yet for unpublished projects; published projects will be picked up by the new published factory in a later task. Co-Authored-By: Claude Code <noreply@anthropic.com>
Introduce GetAvailableUnpublishedProjectDetails() and GetAvailablePublishedProjectDetails() so each factory can claim its own slice of projects. The existing GetAllProjectDetails() is preserved (still backed by the same registration-aware GetVisibleScrTexts helper). No factory changes yet. Co-Authored-By: Claude Code <noreply@anthropic.com>
…d interfaces ParatextProjectDataProvider.GetFunctions() now only adds the 12 comment-related wire methods when ProjectDetails.Metadata.ProjectInterfaces contains legacyCommentManager.comments. Published PDPs (which don't advertise that interface) no longer expose any comment method on the wire. The existing IsResourceProject guards inside CreateComment / AddCommentToThread / UpdateComment / ResolveComment paths are intentionally left in place as defense-in-depth - they are now unreachable on published PDPs but still correct for the unpublished paths they were originally written for. The SBA and TransliterationWithEncoder guards at lines 654 and 660 remain load-bearing on unpublished PDPs (those project types stay on the regular factory; see plan finding F11 for rationale). Co-Authored-By: Claude Code <noreply@anthropic.com>
…ished projects only The factory's project list now comes from GetAvailableUnpublishedProjectDetails, and GetProjectDataProviderID throws KeyNotFoundException for published project IDs (identified by the absence of legacyCommentManager.comments in their advertised interfaces). Existing unpublished flows are unchanged. Published projects will be served by the new ParatextPublishedProjectDataProviderFactory in the next commit. Co-Authored-By: Claude Code <noreply@anthropic.com>
New factory dedicated to Paratext published projects (ResourceScrText / JoinedScrText - same set as platform.isPublished from PR #2333). Advertises the scripture + base interface list, deliberately omitting legacyCommentManager.comments because published projects cannot accept comment writes at the storage layer (ResourceProjectFileManager.SetXml throws). PDPs it constructs reuse ParatextProjectDataProvider (single PDP class) but receive a ProjectDetails whose Metadata.ProjectInterfaces drives GetFunctions() to skip all comment-method registration. Class-level doc-comment records why other restricted-interface PT9 project types (SBA, TransliterationWithEncoder, note-only types) intentionally stay on the regular factory rather than being folded in here - this factory's contract is specifically "what published projects can support", and broadening it would re-create the dishonest-interface problem. Reciprocally rejects unpublished project IDs so the two factories partition the project space cleanly. The wire registration is platform.ParatextPublished-pdpf. Co-Authored-By: Claude Code <noreply@anthropic.com>
…in Program.cs Construct and initialize the new published factory alongside the existing regular factory at startup. Both share the same LocalParatextProjects instance so initialization (ScrTextCollection refresh, project root setup) only runs once. PAPI will now route legacyCommentManager.comments lookups for published project IDs to "no factory found" instead of to the regular factory's PDP that previously silently advertised an interface it could not honor. Co-Authored-By: Claude Code <noreply@anthropic.com>
…PDPF split Plan describes splitting ParatextProjectDataProviderFactory into a regular factory (unpublished projects) and a new ParatextPublishedProjectDataProviderFactory (published projects backed by ResourceScrText / JoinedScrText). Includes investigation findings F1-F12 (PT9 source citations for CanAddNotes, IsResourceProject overrides, SBA spelling-note flow), chosen design (metadata gate in PPDP.GetFunctions), and 8 TDD tasks with explicit code samples. Co-Authored-By: Claude Code <noreply@anthropic.com>
- Extract ParatextProjectDataProviderFactoryBase abstract class to remove ~167 lines of duplication between the two Paratext PDP factories. Shared state, double-checked-locking PDP creation, and existing-PDP lookup live in the base; concrete classes now ~30 lines each. - Change ShouldServeProject discriminator from LEGACY_COMMENT membership to ScrText.IsResourceProject directly. Protects the published factory's documented contract from future drift in the unpublished interface list. - Re-implement LocalParatextProjects.GetAllProjectDetails as the concat of the two partition helpers so they remain the single source of truth. - Use explicit GetParatextProjectInterfaces(isPublished: false|true) form in both factories; remove the no-arg overload and its only remaining caller (the unused private LoadProjectDetails). - Make ParatextProjectDataProvider._commentManager a Lazy<CommentManager> so published PDPs (which never register the comment wire methods) avoid the CommentManager.Get cost. - Replace line-number references in the published factory's class doc with method-name references (VerifyUserCanCreateComments) that don't rot. - Drop two factory-level tests that only re-asserted the static interface list (already covered by LocalParatextProjectsTests) and one backward- compat shim test for the now-removed no-arg overload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two project-interface lists in LocalParatextProjects duplicated 11 of 12 entries verbatim, which is liable to drift if either list is edited without updating the other. Define the unpublished list as the published list plus LEGACY_COMMENT (via collection-expression spread) so the two stay in sync by construction. Also remove docs/superpowers/plans/2026-05-27-paratext-published-pdpf-split.md (superseded by the merged implementation). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Test helper needed by ParatextProjectDataProviderWireSurfaceTests to assert which request types are registered after PDP construction. Previously available on the ai/main branch via an unrelated test infrastructure PR that has not landed on main yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
9eb0570 to
f8ccb29
Compare
…ents
Implementation-detail mentions (ScrText.IsResourceProject, ResourceScrText,
JoinedScrText) were appearing in several places where they did not serve a
concrete educational purpose and would rot if PT9's internal representation
changed. Removed those casual mentions and kept them only where they:
- explain a concrete example or test fixture behavior (e.g. why the
partition discriminator is the ScrText flag rather than the advertised
interface list - giving the TransliterationWithEncoder example);
- annotate the actual ScrText.IsResourceProject code call so the reader
knows what the flag means at the point it is used (above the
GetAvailable{Un,}publishedProjectDetails LINQ filters).
No behavior change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
captaincrazybro
left a comment
There was a problem hiding this comment.
@captaincrazybro made 5 comments.
Reviewable status: 0 of 11 files reviewed, 5 unresolved discussions (waiting on tjcouch-sil).
c-sharp/Projects/LocalParatextProjects.cs line 123 at r1 (raw file):
/// Used by <see cref="ParatextProjectDataProviderFactory"/> to populate its project list. /// </summary> public IEnumerable<ProjectDetails> GetAvailableUnpublishedProjectDetails()
This function and the one bellow seem significantly similar and I would think you could still have one GetAvailableProjectDetails() with a parameter similar to the one below which is bool isPublished.
c-sharp/Projects/LocalParatextProjects.cs line 164 at r1 (raw file):
} private static ProjectDetails? LoadProjectDetails(
Was this function just completely unused? Curious to why this was removed.
c-sharp/Projects/ParatextProjectDataProviderFactory.cs line 18 at r1 (raw file):
papiClient, paratextProjects, LocalParatextProjects.GetParatextProjectInterfaces(isPublished: false),
Why does this need isPublished: false. Can't it just be GetParatextProjectInterfaces(false)? Similar with other places...
c-sharp/Projects/ParatextProjectDataProviderFactory.cs line 22 at r1 (raw file):
) { } protected override List<ProjectMetadata>? GetAvailableProjects(JsonElement _ignore)
Should these functions have summary comments above them like the base function bellow?
c-sharp/Projects/ParatextProjectDataProviderFactoryBase.cs line 75 at r1 (raw file):
); public override string GetProjectDataProviderID(string projectID)
Should this have a summary comment?
captaincrazybro
left a comment
There was a problem hiding this comment.
Also do any changes need to be made for the frontend after this PR?
@captaincrazybro made 1 comment.
Reviewable status: 0 of 11 files reviewed, 5 unresolved discussions (waiting on tjcouch-sil).
lyonsil
left a comment
There was a problem hiding this comment.
@lyonsil reviewed 11 files and all commit messages, and made 3 comments.
Reviewable status: all files reviewed, 6 unresolved discussions (waiting on captaincrazybro and tjcouch-sil).
c-sharp/Projects/LocalParatextProjects.cs line 164 at r1 (raw file):
Previously, captaincrazybro (Cqptain) wrote…
Was this function just completely unused? Curious to why this was removed.
I was going to add a similar comment, then I searched for "LoadProject" in the PR details and saw it was intentional. Then I looked through the project history to understand what its original purpose was. This was used when we tried to load projects directly from the "My Paratext 9 Projects" folder in Windows when on the C drive. We have since abandoned that approach, and I don't see it coming back anytime soon. So deleting makes sense to me.
c-sharp-tests/Projects/ParatextProjectDataProviderWireSurfaceTests.cs line 9 at r1 (raw file):
internal class ParatextProjectDataProviderWireSurfaceTests : PapiTestBase { private static readonly string[] AllCommentWireMethodSuffixes =
This test seems fragile if we add more comment methods. It would be nicer if this could pull all the comment method names from somewhere instead of hard coding it here.
Code Review Summary
Branch: ai/feature/paratext-published-pdpf-split-tj-05-27-2026
Base: origin/ai/main
Date: 2026-05-28
Review model: Claude Opus 4.7
Files changed: 11
Overview
This PR splits published Paratext projects (
ScrText.IsResourceProject == true—ResourceScrTextandJoinedScrText) into a dedicated PDP factoryParatextPublishedProjectDataProviderFactoryso the wire surface honestly reflects what those projects can do: published projects are read-only at the storage layer and cannot accept comment writes, so the new factory advertises every projectInterface the regular factory does EXCEPTlegacyCommentManager.comments. The 12 comment wire methods are also no longer registered on the PDP itself when its advertised interfaces do not includeLEGACY_COMMENT, so the wire-level contract and the PDP method surface stay in lockstep. Both factories cross-reject projects belonging to the sibling factory, and the wire-surface change is verified by new integration tests.During the review, the two factories were refactored to share an abstract base (
ParatextProjectDataProviderFactoryBase) so PDP-cache management, double-checked-locking creation, and the existing-PDP lookup live in one place instead of being duplicated. The cross-factory rejection discriminator was also moved from the derivedLEGACY_COMMENTmembership check toScrText.IsResourceProjectdirectly, which protects the published factory's documented contract from future drift (e.g. a non-resource project type that later dropsLEGACY_COMMENTwon't slip in). Several smaller cleanups (dead-code removal, doc-comment improvements, lazy initialization ofCommentManager, test consolidation) were folded in.API Changes
lib/platform-bible-react/,lib/platform-bible-utils/,papi.d.ts, extension*.d.tsfiles).ScrText.IsResourceProject == true) no longer advertise thelegacyCommentManager.commentsprojectInterface.getCommentThreads,createComment,addCommentToThread,deleteComment,updateComment,setIsCommentThreadRead,findAssignableUsers,canUserCreateComments,canUserAddCommentToThread,canUserAssignThread,canUserResolveThread,canUserEditOrDeleteComment.platform.ParatextPublished-pdpfis registered alongside the existingplatform.Paratext-pdpf.internal abstract class ParatextProjectDataProviderFactoryBase— shared base for both Paratext PDP factories.ParatextProjectDataProviderFactoryandParatextPublishedProjectDataProviderFactoryboth inherit from the new base; concrete classes are now ~30 lines each.LocalParatextProjects.GetAvailableUnpublishedProjectDetails()andGetAvailablePublishedProjectDetails()— new partition helpers (usescrText.IsResourceProjectas the discriminator).LocalParatextProjects.GetAllProjectDetails()— refactored toGetAvailableUnpublishedProjectDetails().Concat(GetAvailablePublishedProjectDetails())so the partition helpers are the single source of truth.LocalParatextProjects.GetParatextProjectInterfaces(bool isPublished)— new signature. The previous no-arg overload was removed (it had no production callers after the partition).LocalParatextProjects.LoadProjectDetails— removed (private, unused dead code).ParatextProjectDataProvider._commentManager— nowLazy<CommentManager>so published PDPs avoid the cost ofCommentManager.Get(whose value they never read).Findings
Critical — Must address before merge
None.
Important — Should address before merge
useProjectDataProvider('legacyCommentManager.comments', projectId)(verified during review: all callers handle the absent PDP gracefully — comment-list.web-view.tsx uses awithPdphelper that returns safe defaults; platform-scripture-editor.web-view.tsx null-checkscommentsPdpin every consumer and the context-menu "Insert comment" item isisDisabled: !canUserCreateComments; the USJ overlay PDP factory'sprojectInterfacesToLayerOverrequireslegacyCommentManager.commentsso the cascade for published projects is automatic; theinsertCommentAtSelectioncontroller method's existing try/catch absorbs the changed failure mode. The hardcoded GUID call at legacy-comment-manager/src/main.ts:291 is commented out and intentionally out of scope.)ParatextProjectDataProviderFactoryBaseabstract class. Shared state (_pdpMap,_creationLock,_random,_paratextProjects),StartFactoryAsync, the double-checked-lockingGetProjectDataProviderID, andGetExistingProjectDataProvidernow live in the base. Concrete classes provide only what actually differs:GetAvailableProjects,ShouldServeProject,GetCrossFactoryRejectionMessage,GetRegistrationTaskDescription. Concrete factories shrank from a combined ~273 lines to ~106.)GetAllProjectDetailsis a third partition path that could drift from the two helpers the factories use (fixed during review: re-implemented asGetAvailableUnpublishedProjectDetails().Concat(GetAvailablePublishedProjectDetails()). The partition helpers are now the single source of truth — any future filtering added to either partition automatically applies toGetAllProjectDetailsand toInventoryDataProvider's caller.)GetParatextProjectInterfaces(isPublished: false|true)form. The no-arg overload was removed entirely as part of the dead-code cleanup below.)Minor — Consider
ParatextPublishedProjectDataProviderFactory.GetExistingProjectDataProviderwas added "for symmetry" with no current caller (fixed during review: as a side effect of the base-class refactor,GetExistingProjectDataProvideris now a single method on the abstract base — not a duplicated symmetrical hook. The YAGNI concern dissolved.)GetParatextProjectInterfaces()shim lacks a doc-comment / is a backward-compat-only seam (fixed during review: removed entirely. The only production caller (LoadProjectDetails) was itself dead code; the only test (GetParatextProjectInterfaces_NoArgs_MatchesUnpublished) only existed to pin the no-arg behavior and is gone too.)LocalParatextProjects.LoadProjectDetails(private) might unconditionally advertise the unpublished interface list (fixed during review: deleted as unused private dead code. It had no callers in production or test code.)ParatextPublishedProjectDataProviderFactoryuses line-number references that rot (fixed during review: replacedParatextProjectDataProvider.csline 654 / 660 withParatextProjectDataProvider.VerifyUserCanCreateComments(the SBA guard) andParatextProjectDataProvider.VerifyUserCanCreateComments(the TransliterationWithEncoder guard). Also simplified the externalWordListForm.cs (lines 950, 2205)reference to justWordListForm.)ParatextProjectDataProvider._commentManagerinitialized unconditionally (fixed during review: nowLazy<CommentManager>. For published PDPs the comment wire methods are not registered, so_commentManager.Valueis never accessed andCommentManager.Getis never called — eliminating the per-PDP allocation/XML-load cost on published projects. 14 call sites updated to use.Value; thread-safety is the Lazy defaultExecutionAndPublication, matching the safety needs of the existing eager init.)ParatextProjectDataProviderFactoryTestswere soft-coupled to the static interface list and couldn't exercise the partition (DummyScrText is always unpublished) (fixed during review: removedRegularFactory_AdvertisesLegacyCommentInterfaceAsyncandRegularFactory_GetAvailableProjects_OmitsPublishedAsync. The interface-list assertion is already covered exhaustively byLocalParatextProjectsTests.GetParatextProjectInterfaces_Unpublished_IncludesLegacyComment/_Published_ExcludesLegacyCommentButKeepsScripture; the "regular factory serves an unpublished project" assertion is already covered byGetProjectDataProviderID_ReturnsIdForProviderAsync. After the removals the file's net diff againstorigin/ai/mainis empty.)LEGACY_COMMENTmembership as discriminator instead ofIsResourceProjectdirectly (fixed during review — author catch: changedShouldServeProject(ProjectDetails)toShouldServeProject(ScrText)and now checksscrText.IsResourceProjectdirectly. The base class resolves the ScrText once and derivesProjectDetailsfrom it. This keeps the published factory's documented contract honest — future restricted-interface project types that are not resource projects (e.g.TransliterationWithEncoderif it later dropsLEGACY_COMMENT) will not slip into the published factory.)Template Propagation
Shared Regions Modified
None.
Extension Config Changes
None.
Positive Observations
ParatextPublishedProjectDataProviderFactoryis unusually thorough — it explicitly enumerates which other PT9 restricted-comment project types are intentionally NOT served by this factory (SBA, TransliterationWithEncoder, note-only types) and explains the rationale for each, plus a forward-looking rule that future restricted patterns should get their own factory rather than be folded in.ParatextProjectDataProvider.GetFunctions) using the advertised interface list rather than a secondisPublishedflag — this keeps the wire surface consistent with the advertised projectInterfaces as the single source of truth.ParatextProjectDataProviderWireSurfaceTestsare the right granularity: they verify the actual observable behavior (which method names appear on the wire) rather than mocking internals.s_paratextUnpublishedProjectInterfacesrename (froms_paratextProjectInterfaces) makes the partition explicit at the data level, not just the method level."Unknown project ID: ...","Internal error adding project data provider"), so no localization regression — these are internal exception messages, not user-facing wire responses.docs/superpowers/plans/2026-05-27-paratext-published-pdpf-split.mdplan file is committed alongside the implementation, consistent with the project's "include supporting files in commits" rule.Interview Notes
Stated purpose: split published projects into a dedicated factory so they accurately represent that they do not have comments.
Key author judgments and reasoning:
LEGACY_COMMENTmembership) but only as a minor concern. The author re-raised it as an Important issue and proposed usingScrText.IsResourceProjectdirectly, citing the published factory's own documented contract: published-only, with explicit exclusion ofTransliterationWithEncoderand other restricted-interface project types. This is a non-obvious correctness improvement that protects the factory's documented contract against future drift in the unpublished interface list. The refactor went fromShouldServeProject(ProjectDetails)toShouldServeProject(ScrText)and the base class now resolves the ScrText once and derivesProjectDetailsfrom it.legacy-comment-manager/src/main.ts:291is commented out and intentionally excluded from this PR's scope). The investigation traced all five other call sites and confirmed each one handles the absent-PDP case acceptably — either through React-level null-checks, thewithPdphelper, the layering PDP factory's automatic interface-cascade, or an existing try/catch on the controller path. A pre-existing UX issue was noted (top-menu "Insert comment" / "Open Comments…" items inplatform-scripture-editorare not gated bycanUserCreateComments, so they appear for resource projects too) but it is NOT introduced by this PR — the same items already misbehaved before the split, just with a different error path._commentManagerinitialization concern, the author left the judgement call to me. I choseLazy<CommentManager>over making the field nullable because it preserves the existing semantics at every call site behind a.Valueaccessor and keeps the registration-time guard inGetFunctionsas the only place that knows about the "is this PDP a published one" condition.LocalParatextProjectsTests(interface lists, partition helpers) andParatextProjectDataProviderFactoryTests.GetProjectDataProviderID_ReturnsIdForProviderAsync(the regular factory can serve an unpublished project). After the removals the file's net diff against the base is empty.Unresolved items: None. Every Critical/Important finding was either dismissed with verified author reasoning or fixed during the review. The author demonstrated clear understanding throughout and did not defer to AI for any portion of the change.
In-Review Quality Check
dotnet build(c-sharp): clean (0 errors; pre-existing warnings unrelated to this PR).dotnet test --filter "FullyQualifiedName~Projects": 217/217 passing (was 220 before the two-overlap-test removal and one no-arg-shim-test removal — net minus 3 is expected).dotnet csharpier .: clean (no reformatting needed on this PR's files).npm run typecheck: passes cleanly.npm run lint: passes cleanly (2 pre-existingno-consolewarnings inextensions/src/platform-scripture/src/use-effective-resource-reference-list.tsare unrelated to this PR).npm run format: clean — no formatting changes applied.npm test: all non-React workspaces pass (paranext-core, eslint-plugin-paranext, papi-dts, platform-bible-utils, and the extension suites). Theplatform-bible-reactStorybook test worker crashed mid-run (42 of 93 files reported as passed before the worker died). The crash is a pre-existing flake in the Storybook component runner and has zero relationship to this PR (no TypeScript / React files were modified — all changes are C#).Suggested Review Focus
Prioritized areas for the author-reviewer meeting:
legacyCommentManager.commentswere missed. The five non-test callers in this repo were traced (comment-list.web-view.tsx,platform-scripture-editor.web-view.tsx:357,platform-scripture-editor/main.ts:548, the USJ layering PDPF, and the editor's USJ web-view reference). All handle the absent-PDP case acceptably.ShouldServeProjectdiscriminator choice — the move fromLEGACY_COMMENTmembership toScrText.IsResourceProjectwas the author's catch and protects the published factory's documented contract. Worth a quick read-through of the base class to confirm theScrTextis resolved exactly once andProjectDetailsis still derived from the sameScrText._commentManagerlaziness — confirm that thread-safety semantics ofLazy<CommentManager>(defaultExecutionAndPublication) match what the rest of the PDP expects under concurrent comment-method invocation.ParatextPublishedProjectDataProviderFactorydoc-comment is now a load-bearing decision record (which project types are/aren't served and why). Worth keeping in mind that future restricted-interface project types should update it explicitly rather than being silently rolled in.This change is