Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
namespace FwLiteProjectSync;

public class CrdtFwdataProjectSyncService(MiniLcmImport miniLcmImport, ILogger<CrdtFwdataProjectSyncService> logger, IOptions<CrdtConfig> crdtConfig,
MiniLcmApiValidationWrapperFactory validationWrapperFactory, MiniLcmApiStringNormalizationWrapperFactory normalizationWrapperFactory)
MiniLcmApiValidationWrapperFactory validationWrapperFactory,
MiniLcmApiStringNormalizationWrapperFactory readNormalizationWrapperFactory,
MiniLcmWriteApiNormalizationWrapperFactory writeNormalizationWrapperFactory)
{
public record DryRunSyncResult(
int CrdtChanges,
Expand Down Expand Up @@ -61,8 +63,11 @@ public async Task<SyncResult> Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataA

private async Task<SyncResult> Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi, bool dryRun, int entryCount, ProjectSnapshot? projectSnapshot)
{
crdtApi = normalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi));
fwdataApi = normalizationWrapperFactory.Create(validationWrapperFactory.Create(fwdataApi));
// Apply normalization to CRDT API (needs NFD normalization)
crdtApi = writeNormalizationWrapperFactory.Create(readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi)));

// FwData already normalizes strings internally via LCModel, so only apply read normalization and validation
fwdataApi = readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(fwdataApi));
Comment on lines +66 to +70
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrapper order inconsistency for CRDT API.

The CRDT API wrapper order here differs from MiniLcmApiHubBase:

Here (line 67):
writeNormalization(readNormalization(validation(crdtApi)))
→ Call flow: Client → WriteNorm → ReadNorm → Validation → API

In MiniLcmApiHubBase (lines 14-20):
writeNormalization(validation(api))
→ Call flow: Client → WriteNorm → Validation → API

The hub applies write normalization before validation, while this sync service applies validation before write normalization. If validators expect normalized data, this inconsistency could cause validation to behave differently between the two code paths.

🔧 Proposed fix to align wrapper order
         // Apply normalization to CRDT API (needs NFD normalization)
-        crdtApi = writeNormalizationWrapperFactory.Create(readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi)));
+        crdtApi = writeNormalizationWrapperFactory.Create(validationWrapperFactory.Create(readNormalizationWrapperFactory.Create(crdtApi)));
         
         // FwData already normalizes strings internally via LCModel, so only apply read normalization and validation
-        fwdataApi = readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(fwdataApi));
+        fwdataApi = validationWrapperFactory.Create(readNormalizationWrapperFactory.Create(fwdataApi));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Apply normalization to CRDT API (needs NFD normalization)
crdtApi = writeNormalizationWrapperFactory.Create(readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi)));
// FwData already normalizes strings internally via LCModel, so only apply read normalization and validation
fwdataApi = readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(fwdataApi));
// Apply normalization to CRDT API (needs NFD normalization)
crdtApi = writeNormalizationWrapperFactory.Create(validationWrapperFactory.Create(readNormalizationWrapperFactory.Create(crdtApi)));
// FwData already normalizes strings internally via LCModel, so only apply read normalization and validation
fwdataApi = validationWrapperFactory.Create(readNormalizationWrapperFactory.Create(fwdataApi));
🤖 Prompt for AI Agents
In `@backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs` around
lines 66 - 70, The CRDT API wrapper order is inconsistent with
MiniLcmApiHubBase: replace the current nested creation that inserts
readNormalization between writeNormalization and validation and instead wrap
crdtApi so writeNormalization directly wraps validation (i.e., use
writeNormalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi)))
to match MiniLcmApiHubBase; keep fwdataApi as-is (readNormalization +
validation). Update the line that assigns crdtApi which currently calls
writeNormalizationWrapperFactory.Create(readNormalizationWrapperFactory.Create(validationWrapperFactory.Create(crdtApi))).


if (dryRun)
{
Expand Down
5 changes: 3 additions & 2 deletions backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ public class MiniLcmJsInvokable(
ILogger<MiniLcmJsInvokable> logger,
MiniLcmApiNotifyWrapperFactory notificationWrapperFactory,
MiniLcmApiValidationWrapperFactory validationWrapperFactory,
MiniLcmApiStringNormalizationWrapperFactory normalizationWrapperFactory
MiniLcmApiStringNormalizationWrapperFactory readNormalizationWrapperFactory,
MiniLcmWriteApiNormalizationWrapperFactory writeNormalizationWrapperFactory
) : IDisposable
{
private readonly IMiniLcmApi _wrappedApi = api.WrapWith([normalizationWrapperFactory, validationWrapperFactory, notificationWrapperFactory], project);
private readonly IMiniLcmApi _wrappedApi = api.WrapWith([writeNormalizationWrapperFactory, readNormalizationWrapperFactory, validationWrapperFactory, notificationWrapperFactory], project);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find WrapWith implementation
find backend -name "*.cs" -type f | xargs grep -l "WrapWith" | head -5

Repository: sillsdev/languageforge-lexbox

Length of output: 4552


🏁 Script executed:

# Search for WrapWith extension method definition
rg -B2 -A10 "public static.*WrapWith" backend/ --type cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1073


🏁 Script executed:

# Examine MiniLcmApiHubBase.cs to see the comparison
find backend -name "MiniLcmApiHubBase.cs" -type f

Repository: sillsdev/languageforge-lexbox

Length of output: 123


🏁 Script executed:

# Check the context around line 26 in MiniLcmJsInvokable.cs
cat -n backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 2127


🏁 Script executed:

cat -n backend/FwLite/FwLiteWeb/Hubs/MiniLcmApiHubBase.cs | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 1648


🏁 Script executed:

# Check for any tests or documentation about wrapper order
rg -B2 -A5 "WrapWith|wrapper.*order" backend/FwLite/MiniLcm.Tests/ --type cs

Repository: sillsdev/languageforge-lexbox

Length of output: 712


🏁 Script executed:

# Look for wrapper factory implementations to understand their purpose
rg -B1 -A8 "class MiniLcmApiValidationWrapperFactory|class MiniLcmWriteApiNormalizationWrapperFactory|class MiniLcmApiStringNormalizationWrapperFactory" backend/ --type cs

Repository: sillsdev/languageforge-lexbox

Length of output: 3182


🏁 Script executed:

# Check for comments in the MiniLcmJsInvokable file explaining the wrapper order
cat -n backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 1523


🏁 Script executed:

# Look for any tests that validate the wrapper order or provide insights
rg -B3 -A10 "writeNormalization|readNormalization" backend/FwLite/MiniLcm.Tests/ --type cs | head -50

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Search for any git blame or comments explaining the wrapper order choice
rg -B5 -A5 "notificationWrapperFactory" backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs --type cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1048


🏁 Script executed:

# Check if there's a README or documentation explaining wrapper order
find backend -name "README*" -o -name "*wrapper*readme*" -o -name "*normalization*readme*" | xargs cat 2>/dev/null

Repository: sillsdev/languageforge-lexbox

Length of output: 17006


🏁 Script executed:

# Look for issue discussions or comments about wrapper ordering
rg -i "wrapper.*order|order.*wrapper|validation.*before|normalization.*order" backend/FwLite/ --type cs

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Check the notification wrapper to understand what it does
rg -B2 -A15 "class MiniLcmApiNotifyWrapper" backend/ --type cs | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 2562


Fix wrapper application order to match documented architecture.

The WrapWith extension reverses the factory list, resulting in this execution chain:

notification → validation → readNormalization → writeNormalization → api

However, the documented architecture in backend/FwLite/README.md specifies:

Client → Write Normalization → Read Normalization → Validation → Core API

This means validation is currently running before write normalization, causing validation to see un-normalized input. The correct factory order should be:

[notificationWrapperFactory, validationWrapperFactory, readNormalizationWrapperFactory, writeNormalizationWrapperFactory]

This aligns with MiniLcmApiHubBase (lines 15-21), which correctly applies write normalization before validation.

🤖 Prompt for AI Agents
In `@backend/FwLite/FwLiteShared/Services/MiniLcmJsInvokable.cs` at line 26, The
wrapper factories are applied in reverse by WrapWith, causing validation to run
before write normalization; update the factory list passed to WrapWith when
constructing _wrappedApi so the runtime order becomes Client → Write
Normalization → Read Normalization → Validation → Core API by using the factory
sequence [notificationWrapperFactory, validationWrapperFactory,
readNormalizationWrapperFactory, writeNormalizationWrapperFactory] (so after
WrapWith reversal it yields the documented write→read→validation order); ensure
this change in IMiniLcmApi _wrappedApi construction mirrors the ordering used in
MiniLcmApiHubBase.


public record MiniLcmFeatures(bool? History, bool? Write, bool? OpenWithFlex, bool? Feedback, bool? Sync, bool? Audio);
private bool SupportsSync => project.DataFormat == ProjectDataFormat.Harmony && api is CrdtMiniLcmApi;
Expand Down
6 changes: 4 additions & 2 deletions backend/FwLite/FwLiteWeb/Hubs/CrdtMiniLcmApiHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Extensions.Caching.Memory;
using MiniLcm;
using MiniLcm.Models;
using MiniLcm.Normalization;
using MiniLcm.Validators;
using SystemTextJsonPatch;

Expand All @@ -23,8 +24,9 @@ public class CrdtMiniLcmApiHub(
LexboxProjectService lexboxProjectService,
IMemoryCache memoryCache,
IHubContext<CrdtMiniLcmApiHub, ILexboxHubClient> hubContext,
MiniLcmApiValidationWrapperFactory validationWrapperFactory
) : MiniLcmApiHubBase(miniLcmApi, validationWrapperFactory)
MiniLcmApiValidationWrapperFactory validationWrapperFactory,
MiniLcmWriteApiNormalizationWrapperFactory writeNormalizationWrapperFactory
) : MiniLcmApiHubBase(miniLcmApi, validationWrapperFactory, writeNormalizationWrapperFactory)
{
public const string ProjectRouteKey = "project";
public static string ProjectGroup(string projectName) => "crdt-" + projectName;
Expand Down
8 changes: 7 additions & 1 deletion backend/FwLite/FwLiteWeb/Hubs/FwDataMiniLcmHub.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Normalization;
using MiniLcm.Validators;
using SIL.LCModel;
using SystemTextJsonPatch;
Expand All @@ -14,8 +16,12 @@ public class FwDataMiniLcmHub(
FwDataFactory fwDataFactory,
FwDataProjectContext context,
MiniLcmApiValidationWrapperFactory validationWrapperFactory
) : MiniLcmApiHubBase(miniLcmApi, validationWrapperFactory)
) : MiniLcmApiHubBase(miniLcmApi, validationWrapperFactory, skipWriteNormalization: true)
{
// Note: FwData already handles string normalization internally (via LCModel),
// so we skip the write normalization wrapper for FwData APIs.
// See: https://github.com/sillsdev/languageforge-lexbox/issues/[issue-number]

public const string ProjectRouteKey = "fwdata";
public override async Task OnConnectedAsync()
{
Expand Down
30 changes: 27 additions & 3 deletions backend/FwLite/FwLiteWeb/Hubs/MiniLcmApiHubBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,39 @@
using Microsoft.Extensions.Options;
using MiniLcm;
using MiniLcm.Models;
using MiniLcm.Normalization;
using MiniLcm.Validators;
using SystemTextJsonPatch;

namespace FwLiteWeb.Hubs;

public abstract class MiniLcmApiHubBase(IMiniLcmApi miniLcmApi,
MiniLcmApiValidationWrapperFactory validationWrapperFactory) : Hub<ILexboxHubClient>
public abstract class MiniLcmApiHubBase : Hub<ILexboxHubClient>
{
private readonly IMiniLcmApi _miniLcmApi = validationWrapperFactory.Create(miniLcmApi);
private readonly IMiniLcmApi _miniLcmApi;

protected MiniLcmApiHubBase(
IMiniLcmApi miniLcmApi,
MiniLcmApiValidationWrapperFactory validationWrapperFactory,
MiniLcmWriteApiNormalizationWrapperFactory writeNormalizationWrapperFactory)
{
_miniLcmApi = writeNormalizationWrapperFactory.Create(validationWrapperFactory.Create(miniLcmApi));
}

protected MiniLcmApiHubBase(
IMiniLcmApi miniLcmApi,
MiniLcmApiValidationWrapperFactory validationWrapperFactory,
bool skipWriteNormalization)
{
if (skipWriteNormalization)
{
// FwData APIs already normalize strings internally via LCModel
_miniLcmApi = validationWrapperFactory.Create(miniLcmApi);
}
else
{
throw new ArgumentException("Use the other constructor with writeNormalizationWrapperFactory", nameof(skipWriteNormalization));
}
}

public async Task<WritingSystems> GetWritingSystems()
{
Expand Down
Loading