Skip to content

Fix losing senses that are moved#2252

Open
myieye wants to merge 1 commit intodevelopfrom
fix-syncing-moved-senses
Open

Fix losing senses that are moved#2252
myieye wants to merge 1 commit intodevelopfrom
fix-syncing-moved-senses

Conversation

@myieye
Copy link
Copy Markdown
Collaborator

@myieye myieye commented May 4, 2026

Resolves #2251

@github-actions github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label May 4, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b6023d95-264c-476b-b447-cef1cbde1fd7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds support for moving senses between entries without losing them during synchronization. It introduces a new MoveSenseToEntryChange CRDT type, adds entry-ownership verification to sense APIs, enables sense-by-ID retrieval, and enhances the entry-sync logic to detect when senses have changed entry membership and emit the appropriate change instead of treating them as deletes/adds.

Changes

Move-Sense Support

Layer / File(s) Summary
CRDT Change Type Definition
backend/FwLite/LcmCrdt/Changes/MoveSenseToEntryChange.cs, backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
New MoveSenseToEntryChange class updates sense EntryId and Order, and marks sense as deleted if target entry is deleted; registered in kernel's change-type list.
Data Model Registration
backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt, backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt, backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
Change-type discriminator and serialization support are registered in test snapshots and deserialization test data.
API Interface & Repository
backend/FwLite/MiniLcm/IMiniLcmReadApi.cs, backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs
New GetSense(Guid id) overload added to interface and repository; repository now supports single-ID lookup without entry context.
API Implementations
backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs, backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Added GetSense(Guid id) overload and entry-ownership verification helper; updated sense create/update/delete operations and new MoveSense logic to emit MoveSenseToEntryChange when moving across entries.
Sync Logic
backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs
Enhanced to pass sense dictionaries through the sync pipeline; detects senses that moved between entries and delegates to API MoveSense during add/remove; EntriesDiffApi creates entries without senses first when moved senses are detected, then syncs them separately.
Routing
backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
GET /sense/{id} handler updated to call GetSense(id) without entry context.
Tests & Verification
backend/FwLite/MiniLcm.Tests/SenseTestsBase.cs, backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Added MoveSense_ReparentsToDifferentEntry test and expanded sync test coverage with parameterized CanSyncComponentWhenSenseMovesToDifferentEntry and new CanSyncSenseMovedToDifferentEntry theory.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #2249: Adds GET /sense/{id} route and GetSense(Guid id) API overload; both introduce sense-by-ID retrieval paths.
  • PR #2250: Addresses the same moved-sense syncing bug with overlapping changes to EntrySync logic and orderable-diff handling.
  • PR #1945: Modifies entry-sync codepaths and tests; one changes entry creation sequencing to ensure referenced senses exist while this PR detects and handles moved senses.

Suggested labels

💻 FW Lite, 🟨 Medium

Suggested reviewers

  • rmunn
  • hahn-kev

Poem

🐰 A sense hops from entry to entry with glee,
No longer lost when moved across the tree!
Detect the jump, emit the change so right,
MoveSenseToEntryChange makes syncing bright! ✨
(The rabbits approve this refactor flight)

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix losing senses that are moved' directly describes the main objective of the changeset: preventing senses from being lost when they are moved between entries during sync operations.
Description check ✅ Passed The description 'Resolves #2251' references the linked issue that documents the bug being fixed, which is related to the changeset's purpose of handling sense movement across entries.
Linked Issues check ✅ Passed The PR implements the necessary changes to prevent moved senses from being lost: adds MoveSenseToEntryChange to track sense moves, updates entry/sense sync logic to detect and handle moved senses, adds entry-membership verification, and includes comprehensive tests for the new functionality.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing the sense-movement bug: CRDT change handling, API method additions for sense membership verification, sync logic enhancements to detect moved senses, and supporting tests. No unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-syncing-moved-senses

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

UI unit Tests

  1 files  ±0   59 suites  ±0   31s ⏱️ +2s
176 tests ±0  176 ✅ ±0  0 💤 ±0  0 ❌ ±0 
245 runs  ±0  245 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 41f85de. ± Comparison against base commit a0db9ac.

♻️ This comment has been updated with latest results.

@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented May 4, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - May 4, 2026, 2:31 PM

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs (1)

705-707: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the entry path before deleting a sense.

This method still ignores entryId, so a delete sent against the old entry route will remove a sense even after it has been moved. That reopens the moved-sense data-loss case this PR is trying to close on the CRDT backend.

Suggested fix
 public async Task DeleteSense(Guid entryId, Guid senseId)
 {
+    await using var repo = await repoFactory.CreateRepoAsync();
+    var sense = await repo.GetSense(senseId) ?? throw NotFoundException.ForType<Sense>(senseId);
+    VerifySenseBelongsToEntry(entryId, sense);
     await AddChange(new DeleteChange<Sense>(senseId));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs` around lines 705 - 707, DeleteSense
currently ignores the entryId and unconditionally enqueues DeleteChange<Sense>,
which allows deleting a sense from its old entry after it was moved; update
DeleteSense to first validate that the senseId belongs to the provided entryId
before calling AddChange. Specifically, inside DeleteSense use the existing
lookup method for entries (e.g., GetEntry, FindEntryById, or equivalent in this
class) to load the entry for entryId, confirm entry.Senses (or the entry's sense
collection) contains senseId (or that the sense's parent matches entryId), and
only then call AddChange(new DeleteChange<Sense>(senseId)); otherwise
return/throw an appropriate error to reject the operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs`:
- Around line 1439-1442: The ownership check in VerifySenseBelongsToEntry uses a
non-existent ILexSense.Member EntryId; update the thrown message to use the
available properties: replace sense.Id with sense.Guid (the sense's GUID) and
replace sense.EntryId with sense.Entry.Guid (the entry's GUID) so the
NotFoundException includes the correct actual entry Id and sense Id using the
ILexSense members.

---

Outside diff comments:
In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs`:
- Around line 705-707: DeleteSense currently ignores the entryId and
unconditionally enqueues DeleteChange<Sense>, which allows deleting a sense from
its old entry after it was moved; update DeleteSense to first validate that the
senseId belongs to the provided entryId before calling AddChange. Specifically,
inside DeleteSense use the existing lookup method for entries (e.g., GetEntry,
FindEntryById, or equivalent in this class) to load the entry for entryId,
confirm entry.Senses (or the entry's sense collection) contains senseId (or that
the sense's parent matches entryId), and only then call AddChange(new
DeleteChange<Sense>(senseId)); otherwise return/throw an appropriate error to
reject the operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 025a0b68-44ac-424a-94eb-7472afc5f800

📥 Commits

Reviewing files that changed from the base of the PR and between a0db9ac and 70565f0.

📒 Files selected for processing (13)
  • backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
  • backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
  • backend/FwLite/FwLiteWeb/Routes/MiniLcmRoutes.cs
  • backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyChangeModels.verified.txt
  • backend/FwLite/LcmCrdt/Changes/MoveSenseToEntryChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/Data/MiniLcmRepository.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/MiniLcm.Tests/SenseTestsBase.cs
  • backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
  • backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs

Comment thread backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
@myieye myieye force-pushed the fix-syncing-moved-senses branch from 70565f0 to a5fcb79 Compare May 4, 2026 11:36
@github-actions github-actions Bot added the 📦 Lexbox issues related to any server side code, fw-headless included label May 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

C# Unit Tests

165 tests   165 ✅  19s ⏱️
 23 suites    0 💤
  1 files      0 ❌

Results for commit 41f85de.

♻️ This comment has been updated with latest results.

@myieye myieye force-pushed the fix-syncing-moved-senses branch 2 times, most recently from 1d8cdbd to 75edb4c Compare May 4, 2026 14:27
@myieye myieye force-pushed the fix-syncing-moved-senses branch from 75edb4c to 41f85de Compare May 4, 2026 14:28
Copy link
Copy Markdown
Contributor

@rmunn rmunn left a comment

Choose a reason for hiding this comment

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

Only just started the review so I haven't looked at the rest of the code, but I found something that needs changing in the first file. I'll go and review the rest now, but I wanted to give you this feedback ASAP so that you can work on it even if I don't manage to review the rest of the PR today.

public Task<Sense?> GetSense(Guid entryId, Guid id)
{
SenseRepository.TryGetObject(id, out var lcmSense);
if (lcmSense is not null) VerifySenseBelongsToEntry(entryId, lcmSense);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's a PR recently merged from @imnasnainaec where this API method gets called with Guid.Empty for the entry ID. We may want to check for an empty entry GUID and forward to the GetSense(Guid id) call if the entry is unknown. The code that currently calls GetSense(Guid.Empty, senseId) can be updated to just call GetSense(senseId) once this PR is in, but in the overlap period we will want to allow GetSense(Guid.Empty, senseId) not to throw an error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

#2249 is the PR I mentioned.

@rmunn
Copy link
Copy Markdown
Contributor

rmunn commented May 6, 2026

I ran the tests multiple times. Sometimes they passed, sometimes they failed. When they failed, it was always the same:

/home/rmunn/code/lexbox/backend/FwLite/FwDataMiniLcmBridge.Tests/Fixtures/MockFwProjectLoader.cs(27): error TESTERROR:
      FwLiteProjectSync.Tests.CrdtEntrySyncTests.CanSyncComponentWhenSenseMovesToDifferentEntry(entryOrderString: "complex-form,old-component,new-component", oldComponentDeleted: False, newComponentCreated: True) (1ms): Error Message: CommonServiceLocator.ActivationException : Activation error occurred while trying
       to get instance of type IDataSetup, key ""
      ---- StructureMap.Building.StructureMapBuildException : Error while building type SIL.LCModel.Infrastructure.Impl.LcmMetaDataCache.  See the inner exception for details
      1.) new LcmMetaDataCache()
      2.) SIL.LCModel.Infrastructure.Impl.LcmMetaDataCache
      3.) Instance of SIL.LCModel.Infrastructure.IFwMetaDataCacheManaged (SIL.LCModel.Infrastructure.Impl.LcmMetaDataCache)
      4.) new IdentityMap(*Default of IFwMetaDataCacheManaged*)
      5.) SIL.LCModel.Infrastructure.Impl.IdentityMap
      6.) Instance of SIL.LCModel.Infrastructure.Impl.IdentityMap
      7.) new MemoryOnlyBackendProvider(*Default of LcmCache*, *Default of IdentityMap*, *Default of ICmObjectSurrogateFactory*, *Default of IFwMetaDataCacheManagedInternal*, *Default of IDataMigrationManager*, *Default of ILcmUI*, *Default of ILcmDirectories*, *Default of LcmSettings*)
      8.) SIL.LCModel.Infrastructure.Impl.MemoryOnlyBackendProvider
      9.) Instance of SIL.LCModel.Infrastructure.IDataSetup (SIL.LCModel.Infrastructure.Impl.MemoryOnlyBackendProvider)
      10.) Container.GetInstance(SIL.LCModel.Infrastructure.IDataSetup)

      -------- System.InvalidOperationException : Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
      Stack Trace:
         at CommonServiceLocator.ServiceLocatorImplBase.GetInstance(Type serviceType, String key)
         at CommonServiceLocator.ServiceLocatorImplBase.GetService(Type serviceType)
         at SIL.LCModel.IocHelpers.GetInstance[TService](IServiceProvider provider)
         at SIL.LCModel.LcmCache.CreateCacheInternal(IProjectIdentifier projectId, String userWsIcuLocale, ILcmUI ui, ILcmDirectories dirs, LcmSettings settings, Action`1 doThe, Action`1 initialize)
         at SIL.LCModel.LcmCache.CreateCacheInternal(IProjectIdentifier projectId, String userWsIcuLocale, ILcmUI ui, ILcmDirectories dirs, LcmSettings settings, Action`1 doThe)
         at SIL.LCModel.LcmCache.CreateCacheWithNewBlankLangProj(IProjectIdentifier projectId, String analWsIcuLocale, String vernWsIcuLocale, String userWsIcuLocale, ILcmUI ui, ILcmDirectories dirs, LcmSettings settings)
         at FwDataMiniLcmBridge.Tests.Fixtures.MockFwProjectLoader.NewProject(FwDataProject project, String analysisWs, String vernacularWs) in /home/rmunn/code/lexbox/backend/FwLite/FwDataMiniLcmBridge.Tests/Fixtures/MockFwProjectLoader.cs:line 27
         at FwLiteProjectSync.Tests.Fixtures.SyncFixture.InitializeAsync() in /home/rmunn/code/lexbox/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs:line 99
         at FwLiteProjectSync.Tests.Fixtures.ExtraWritingSystemsSyncFixture.InitializeAsync() in /home/rmunn/code/lexbox/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs:line 27
      ----- Inner Stack Trace -----
         at lambda_method29(Closure, IBuildSession, IContext)
         at StructureMap.Building.BuildPlan.Build(IBuildSession session, IContext context)
         at StructureMap.BuildSession.BuildNewInSession(Type pluginType, Instance instance)
         at StructureMap.BuildSession.BuildNewInOriginalContext(Type pluginType, Instance instance)
         at StructureMap.Pipeline.LifecycleObjectCache.buildWithSession(Type pluginType, Instance instance, IBuildSession session)
         at StructureMap.Pipeline.LifecycleObjectCache.<>c__DisplayClass5_0.<Get>b__0(Int32 _)
         at StructureMap.Pipeline.LazyLifecycleObjectCacheExtensions.<>c__DisplayClass1_1`2.<GetOrAdd>b__1()
         at StructureMap.Pipeline.LazyLifecycleObject`1.CreateValue()
         at StructureMap.Pipeline.LazyLifecycleObject`1.get_Value()
         at StructureMap.Pipeline.LazyLifecycleObjectCacheExtensions.GetOrAdd[TKey,TValue](ConcurrentDictionary`2 cache, TKey key, Func`2 valueFactory)
         at StructureMap.Pipeline.LifecycleObjectCache.Get(Type pluginType, Instance instance, IBuildSession session)
         at StructureMap.BuildSession.ResolveFromLifecycle(Type pluginType, Instance instance)
         at StructureMap.SessionCache.GetObject(Type pluginType, Instance instance, ILifecycle lifecycle)
         at StructureMap.SessionCache.GetDefault(Type pluginType, IPipelineGraph pipelineGraph)
         at StructureMap.BuildSession.GetInstance(Type pluginType)
         at StructureMap.Container.DoGetInstance(Type pluginType)
         at StructureMap.Container.GetInstance(Type pluginType)
         at SIL.LCModel.IOC.StructureMapServiceLocator.DoGetInstance(Type serviceType, String key)
         at CommonServiceLocator.ServiceLocatorImplBase.GetInstance(Type serviceType, String key)
      ----- Inner Stack Trace -----
         at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
         at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)
         at SIL.LCModel.Infrastructure.Impl.CmObjectSurrogate.InitializeConstructors(List`1 cmObjectTypes)
         at SIL.LCModel.Infrastructure.Impl.LcmMetaDataCache..ctor()
         at lambda_method34(Closure, IBuildSession, IContext)

"System.InvalidOperationException : Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct." There's a race condition in the test, which sometimes passes if the two MockFwProjectLoader instances are being set up at different times. But if they run at the same time then this can trigger.

I was either seeing all tests passing, or about 45 tests failing all together with the same error. (Not surprising since the CanSyncComponentWhenSenseMovesToDifferentEntry test is a data-driven test that runs itself 24 times).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related 📦 Lexbox issues related to any server side code, fw-headless included

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Senses moved between entries are sometimes lost when syncing

2 participants