Add morph-types to existing CRDT projects and refresh FTS headwords to respect those morph-types.
Implementation Steps
Step 1: Create CanonicalMorphTypes static class
File: backend/FwLite/MiniLcm/Models/CanonicalMorphTypes.cs (NEW)
Define all 19 canonical morph types (one per MorphTypeKind enum value, excluding Unknown) as a static array of MorphType objects. Include: GUIDs (matching MoMorphTypeTags.kguidMorph* from SIL.LCModel), Kind, Name/Abbreviation (en), Prefix, Postfix, SecondaryOrder.
Data source: Hardcode from LibLCM GitHub source. Add a Sena3 verification test (see Step 7).
Pattern: Follow CanonicalGuidsPartOfSpeech (MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs) for the frozen-set pattern, but include full MorphType objects since we need prefix, postfix, name, etc. for seeding.
Step 2: Add PredefinedMorphTypes to PreDefinedData
File: backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs (MODIFY)
Add PredefinedMorphTypes(DataModel dataModel, Guid clientId) method following the exact pattern of PredefinedComplexFormTypes (line 8-16). Use CreateMorphTypeChange for each canonical morph type. Use a fixed commit GUID for deterministic CRDT commits.
Note: CreateMorphTypeChange.NewEntity() (line 40-53 of LcmCrdt/Changes/CreateMorphTypeChange.cs) already handles duplicates gracefully by creating pre-deleted entities, so this is safe for CRDT-to-CRDT sync scenarios.
Step 3: Call PredefinedMorphTypes for new projects
File: backend/FwLite/LcmCrdt/CrdtProjectsService.cs (MODIFY)
Add call to PreDefinedData.PredefinedMorphTypes in SeedSystemData() (line 242-247):
internal static async Task SeedSystemData(DataModel dataModel, Guid clientId)
{
await PreDefinedData.PredefinedComplexFormTypes(dataModel, clientId);
await PreDefinedData.PredefinedPartsOfSpeech(dataModel, clientId);
await PreDefinedData.PredefinedSemanticDomains(dataModel, clientId);
await PreDefinedData.PredefinedMorphTypes(dataModel, clientId); // NEW
}
Step 4: Seed morph-types for existing projects in MigrateDb (BEFORE FTS refresh)
File: backend/FwLite/LcmCrdt/CurrentProjectService.cs (MODIFY)
Modify MigrateDb() (line 95-121) to seed morph-types between MigrateAsync() and RegenerateIfMissing():
async Task Execute()
{
try
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
await dbContext.Database.MigrateAsync();
// Seed morph-types if missing (for existing projects created before morph-type support)
// Must happen BEFORE FTS regeneration so headwords include morph-type tokens
if (!await dbContext.MorphTypes.AnyAsync())
{
var dataModel = serviceProvider.GetRequiredService<DataModel>();
var clientId = serviceProvider.GetRequiredService<CurrentProjectService>().ProjectData.ClientId;
await PreDefinedData.PredefinedMorphTypes(dataModel, clientId);
}
if (EntrySearchServiceFactory is not null)
{
await using var ess = EntrySearchServiceFactory.CreateSearchService(dbContext);
await ess.RegenerateIfMissing();
}
}
catch (Exception e) { ... }
}
Ordering guarantees:
MigrateAsync() → runs EF migrations (including FTS clear migration from Step 5)
- Morph-type seeding → canonical morph types now in CRDT
RegenerateIfMissing() → FTS rebuilt with morph-type-aware headwords via EntryQueryHelpers.ComputeHeadwords()
Note: Need to inject IServiceProvider or DataModel into CurrentProjectService (check existing DI setup). The MigrationTasks.GetOrAdd guard ensures this runs at most once per project per process lifetime.
Step 5: EF Migration to clear FTS table
File: backend/FwLite/LcmCrdt/Migrations/{timestamp}_RegenerateSearchTableForMorphTypes.cs (NEW)
Generate via dotnet ef migrations add RegenerateSearchTableForMorphTypes --project LcmCrdt:
protected override void Up(MigrationBuilder migrationBuilder)
{
// Force FTS rebuild so headwords include morph-type prefix/postfix tokens
migrationBuilder.Sql("DELETE FROM EntrySearchRecord;");
}
Step 6: Patch legacy snapshots in sync layer (CRITICAL - addresses snapshot problem)
File: backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs (MODIFY)
Problem: Projects that synced before morph-type support have snapshots with MorphTypes: []. After seeding, the CRDT has 19 morph-types but the snapshot still has []. When MorphTypeSync.Sync(snapshot.MorphTypes, currentFwDataMorphTypes, crdtApi) runs, it diffs [] vs [19 types], sees 19 additions, and calls crdtApi.CreateMorphType() which throws DuplicateObjectException.
Fix: In Sync() method (around line 81-83), before calling SyncInternal, patch the snapshot if it has empty MorphTypes but the CRDT has morph-types:
// Patch legacy snapshots that were created before morph-type support
if (projectSnapshot is not null && projectSnapshot.MorphTypes.Length == 0)
{
var currentCrdtMorphTypes = await crdtApi.GetMorphTypes().ToArrayAsync();
if (currentCrdtMorphTypes.Length > 0)
{
projectSnapshot = projectSnapshot with { MorphTypes = currentCrdtMorphTypes };
}
}
Why this works:
ProjectSnapshot is a positional record → supports with expressions
- Seeded morph-types use canonical GUIDs → they match FwData's morph-type GUIDs → diff sees no changes
- After sync completes, the snapshot is regenerated from CRDT (by the caller), so future syncs won't need this fixup
- For CRDT-only projects (no FwData): no snapshot exists, so this code path is never hit
- For projects that first sync with FwData: goes through Import path (snapshot is null),
ResumableImportApi handles duplicates by Kind
Step 7: Tests
7a. Verification test: Add Sena3-based test asserting hardcoded canonical morph-type values match FwData project values via FwDataMiniLcmApi.GetMorphTypes().
7b. Seeding tests (LcmCrdt.Tests/):
- New project with
SeedNewProjectData: true has all 19 canonical morph types
- Existing project with empty MorphType table gets canonical morph types after open
- Seeding is idempotent (opening project twice doesn't duplicate)
7c. Sync snapshot test (FwLiteProjectSync.Tests/SyncTests.cs):
- Sync with legacy snapshot (empty MorphTypes) after morph-type seeding → no errors, no duplicate morph types
7d. Migration regression: v3 SQL dump and updated regression tests per existing pattern.
Step 8: v3 SQL regression dump
File: backend/FwLite/LcmCrdt.Tests/Data/Scripts/v3.sql (NEW)
File: backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs (MODIFY)
File: backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs (MODIFY)
Follow instructions in generating-versions.MD. Add v3 to RegressionVersion enum and wire up test data.
Files Summary
| File |
Action |
Purpose |
| MiniLcm/Models/CanonicalMorphTypes.cs |
CREATE |
Canonical morph-type definitions |
| LcmCrdt/Objects/PreDefinedData.cs |
MODIFY |
Add PredefinedMorphTypes |
| LcmCrdt/CrdtProjectsService.cs |
MODIFY |
Call PredefinedMorphTypes in SeedSystemData |
| LcmCrdt/CurrentProjectService.cs |
MODIFY |
Seed morph-types in MigrateDb before FTS refresh |
| LcmCrdt/Migrations/*_RegenerateSearchTableForMorphTypes.cs |
CREATE |
Clear FTS for rebuild |
| FwLiteProjectSync/CrdtFwdataProjectSyncService.cs |
MODIFY |
Patch legacy snapshots before sync |
| LcmCrdt.Tests/Data/Scripts/v3.sql |
CREATE |
Regression dump |
| LcmCrdt.Tests/Data/RegressionTestHelper.cs |
MODIFY |
Add v3 |
| LcmCrdt.Tests/Data/MigrationTests.cs |
MODIFY |
Add v3 tests |
| Various test files |
MODIFY/CREATE |
Tests per Step 7 |
Key Existing Code to Reuse
PreDefinedData.PredefinedComplexFormTypes pattern (LcmCrdt/Objects/PreDefinedData.cs)
CreateMorphTypeChange (LcmCrdt/Changes/CreateMorphTypeChange.cs) - handles duplicate Kind by pre-deleting
EntrySearchService.RegenerateIfMissing (LcmCrdt/FullTextSearch/EntrySearchService.cs) - already in MigrateDb flow
EntryQueryHelpers.ComputeHeadwords (LcmCrdt/Data/EntryQueryHelpers.cs) - already morph-type-aware
CanonicalGuidsPartOfSpeech pattern (MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs)
Verification
dotnet test backend/FwLite/LcmCrdt.Tests - seeding, migration regression
dotnet test backend/FwLite/FwLiteProjectSync.Tests - sync with legacy snapshots
- Verify ordering: morph-types exist before FTS regeneration in MigrateDb logs
Add morph-types to existing CRDT projects and refresh FTS headwords to respect those morph-types.
Implementation Steps
Step 1: Create
CanonicalMorphTypesstatic classFile:
backend/FwLite/MiniLcm/Models/CanonicalMorphTypes.cs(NEW)Define all 19 canonical morph types (one per
MorphTypeKindenum value, excludingUnknown) as a static array ofMorphTypeobjects. Include: GUIDs (matchingMoMorphTypeTags.kguidMorph*from SIL.LCModel), Kind, Name/Abbreviation (en), Prefix, Postfix, SecondaryOrder.Data source: Hardcode from LibLCM GitHub source. Add a Sena3 verification test (see Step 7).
Pattern: Follow
CanonicalGuidsPartOfSpeech(MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs) for the frozen-set pattern, but include fullMorphTypeobjects since we need prefix, postfix, name, etc. for seeding.Step 2: Add
PredefinedMorphTypestoPreDefinedDataFile:
backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs(MODIFY)Add
PredefinedMorphTypes(DataModel dataModel, Guid clientId)method following the exact pattern ofPredefinedComplexFormTypes(line 8-16). UseCreateMorphTypeChangefor each canonical morph type. Use a fixed commit GUID for deterministic CRDT commits.Note:
CreateMorphTypeChange.NewEntity()(line 40-53 ofLcmCrdt/Changes/CreateMorphTypeChange.cs) already handles duplicates gracefully by creating pre-deleted entities, so this is safe for CRDT-to-CRDT sync scenarios.Step 3: Call
PredefinedMorphTypesfor new projectsFile:
backend/FwLite/LcmCrdt/CrdtProjectsService.cs(MODIFY)Add call to
PreDefinedData.PredefinedMorphTypesinSeedSystemData()(line 242-247):Step 4: Seed morph-types for existing projects in
MigrateDb(BEFORE FTS refresh)File:
backend/FwLite/LcmCrdt/CurrentProjectService.cs(MODIFY)Modify
MigrateDb()(line 95-121) to seed morph-types betweenMigrateAsync()andRegenerateIfMissing():Ordering guarantees:
MigrateAsync()→ runs EF migrations (including FTS clear migration from Step 5)RegenerateIfMissing()→ FTS rebuilt with morph-type-aware headwords viaEntryQueryHelpers.ComputeHeadwords()Note: Need to inject
IServiceProviderorDataModelintoCurrentProjectService(check existing DI setup). TheMigrationTasks.GetOrAddguard ensures this runs at most once per project per process lifetime.Step 5: EF Migration to clear FTS table
File:
backend/FwLite/LcmCrdt/Migrations/{timestamp}_RegenerateSearchTableForMorphTypes.cs(NEW)Generate via
dotnet ef migrations add RegenerateSearchTableForMorphTypes --project LcmCrdt:Step 6: Patch legacy snapshots in sync layer (CRITICAL - addresses snapshot problem)
File:
backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs(MODIFY)Problem: Projects that synced before morph-type support have snapshots with
MorphTypes: []. After seeding, the CRDT has 19 morph-types but the snapshot still has[]. WhenMorphTypeSync.Sync(snapshot.MorphTypes, currentFwDataMorphTypes, crdtApi)runs, it diffs[] vs [19 types], sees 19 additions, and callscrdtApi.CreateMorphType()which throwsDuplicateObjectException.Fix: In
Sync()method (around line 81-83), before callingSyncInternal, patch the snapshot if it has empty MorphTypes but the CRDT has morph-types:Why this works:
ProjectSnapshotis a positional record → supportswithexpressionsResumableImportApihandles duplicates by KindStep 7: Tests
7a. Verification test: Add Sena3-based test asserting hardcoded canonical morph-type values match FwData project values via
FwDataMiniLcmApi.GetMorphTypes().7b. Seeding tests (
LcmCrdt.Tests/):SeedNewProjectData: truehas all 19 canonical morph types7c. Sync snapshot test (
FwLiteProjectSync.Tests/SyncTests.cs):7d. Migration regression: v3 SQL dump and updated regression tests per existing pattern.
Step 8: v3 SQL regression dump
File:
backend/FwLite/LcmCrdt.Tests/Data/Scripts/v3.sql(NEW)File:
backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs(MODIFY)File:
backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs(MODIFY)Follow instructions in
generating-versions.MD. Add v3 toRegressionVersionenum and wire up test data.Files Summary
Key Existing Code to Reuse
PreDefinedData.PredefinedComplexFormTypespattern (LcmCrdt/Objects/PreDefinedData.cs)CreateMorphTypeChange(LcmCrdt/Changes/CreateMorphTypeChange.cs) - handles duplicate Kind by pre-deletingEntrySearchService.RegenerateIfMissing(LcmCrdt/FullTextSearch/EntrySearchService.cs) - already in MigrateDb flowEntryQueryHelpers.ComputeHeadwords(LcmCrdt/Data/EntryQueryHelpers.cs) - already morph-type-awareCanonicalGuidsPartOfSpeechpattern (MiniLcm/Validators/CanonicalGuidsPartOfSpeech.cs)Verification
dotnet test backend/FwLite/LcmCrdt.Tests- seeding, migration regressiondotnet test backend/FwLite/FwLiteProjectSync.Tests- sync with legacy snapshots