Skip to content

Begin modeling pictures in MiniLcm#2300

Draft
rmunn wants to merge 27 commits into
developfrom
feat/pictures
Draft

Begin modeling pictures in MiniLcm#2300
rmunn wants to merge 27 commits into
developfrom
feat/pictures

Conversation

@rmunn

@rmunn rmunn commented May 27, 2026

Copy link
Copy Markdown
Contributor

WIP for #2232 and #2233.

MiniLcm API for picture support now contains the skeleton of what it needs to do, but one major feature is still lacking: correct MediaUri handling (which is the heart of the feature). It's not nearly as direct a translation from FW Classic to FW Lite, so I've implemented the easy stuff first. MediaUri handling will come next.

@rmunn rmunn self-assigned this May 27, 2026
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Picture model to MiniLcm senses with full CRUD, ordering, and sync across the CRDT, FwData bridge, LfClassic, and DryRun API layers. Extends the IMiniLcm interfaces, adds CRDT change types and a jsonb DB migration, wires PictureSync into SenseSync, and propagates the generated TypeScript IPicture interface to the frontend.

Changes

Picture Support on Senses

Layer / File(s) Summary
Picture model and API contracts
backend/FwLite/MiniLcm/Models/Picture.cs, backend/FwLite/MiniLcm/Models/Sense.cs, backend/FwLite/MiniLcm/IMiniLcmReadApi.cs, backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
Adds Picture class implementing IObjectWithId and IOrderable. Extends Sense with a Pictures list and deep-copy. Adds GetPicture to IMiniLcmReadApi and five picture CRUD/move methods to IMiniLcmWriteApi.
CRDT change types and OrderPicker extension
backend/FwLite/LcmCrdt/Changes/CreateSense*.cs, backend/FwLite/LcmCrdt/Changes/RemoveSensePictureChange.cs, backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs, backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs, backend/FwLite/LcmCrdt/OrderPicker.cs
Adds four EditChange<Sense> types for picture lifecycle with deterministic ordering. Updates CreateSenseChange to carry Pictures. Adds a synchronous PickOrder(List<T>) overload to OrderPicker.
CRDT DB persistence and migration
backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs, backend/FwLite/LcmCrdt/LcmCrdtKernel.cs, backend/FwLite/LcmCrdt/Migrations/...
Configures Sense.Pictures as a jsonb column with EF Core and LINQ-to-DB value converters for MediaUri and List<Picture>. Registers four new CRDT change types. Adds the AddPicturesColumnToSenses migration and updates the model snapshot.
DoubleDiff utility, PictureSync helper, and SenseSync integration
backend/FwLite/MiniLcm/SyncHelpers/DoubleDiff.cs, backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs, backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs, backend/FwLite/FwLiteProjectSync.Tests/DoubleDiffTests.cs
Adds DoubleDiff utility emitting JSON Patch add/replace/remove operations for double values. Adds PictureSync with collection and single-picture Sync overloads plus a DiffToUpdate builder and PicturesDiffApi adapter. Hooks PictureSync.Sync into SenseSync.Sync.
CrdtMiniLcmApi picture operations
backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Emits CreateSensePictureChange during entry/sense creation with sequential Order values. Implements CreatePicture, GetPicture, two UpdatePicture overloads, MovePicture, and DeletePicture using CRDT change emission and re-fetch.
FwData bridge picture CRUD and proxies
backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs, backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs, backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs
Adds PicturesFolder constant and LcmPictureFactory accessor, maps sense.PicturesOS to API Picture objects in FromLexSense, and implements the full picture subsystem with ownership validation, ordering, and CRUD. Adds UpdatePictureProxy forwarding Id, Caption, and MediaUri to the underlying ICmPicture.
Normalization wrapper, DryRun, and LfClassic adapters
backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs, backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs, backend/LfClassicData/Entities/Entry.cs, backend/LfClassicData/LfClassicMiniLcmApi.cs
Normalization wrapper adds NormalizePicture, NormalizePicturePatch (preserving MediaUri ops), and forwards all five picture write methods. DryRunMiniLcmApi records picture operations. LfClassicData adds Picture BSON entity and Sense.Pictures; LfClassicMiniLcmApi implements GetPicture and ToPicture conversion.
Frontend types, demo data, and test coverage
backend/FwLite/FwLiteShared/TypeGen/..., frontend/viewer/src/lib/dotnet-types/..., frontend/viewer/src/lib/utils.ts, frontend/viewer/src/lib/sandbox/Sandbox.svelte, frontend/viewer/src/project/demo/demo-entry-data.ts, backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
Registers Picture for TypeScript generation. Generates IPicture interface extending IObjectWithId and adds pictures: IPicture[] to ISense. Initializes pictures: [] in defaultSense, Sandbox, and all demo sense objects. Extends UseChangesTests with all four picture change types.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • hahn-kev
  • myieye

Poem

🐇 A bunny with a camera hops with glee,
Each sense now holds a picture, plain to see!
From CRDT changes to the LCM store,
Pictures: [] initialized — who could ask more?
MediaUri mapped, captions synced with care,
The lexicon blooms with images to share! 🖼️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.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 'Begin modeling pictures in MiniLcm' accurately describes the main objective: initiating picture support in the MiniLcm API, which is the core focus of this changeset across multiple files and layers.
Description check ✅ Passed The description relates to the changeset by explaining the WIP status for picture support in MiniLcm, referencing issues #2232 and #2233, and noting that MediaUri handling is incomplete but skeleton functionality exists.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pictures

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 github-actions Bot added the 💻 FW Lite issues related to the fw lite application, not miniLcm or crdt related label May 27, 2026
@rmunn rmunn linked an issue May 27, 2026 that may be closed by this pull request
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

UI unit Tests

  1 files  ±0   62 suites  ±0   30s ⏱️ +2s
186 tests ±0  186 ✅ ±0  0 💤 ±0  0 ❌ ±0 
258 runs  ±0  258 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 0891da0. ± Comparison against base commit 2d6b20e.

♻️ This comment has been updated with latest results.

@argos-ci

argos-ci Bot commented May 27, 2026

Copy link
Copy Markdown

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

Build Status Details Updated (UTC)
default (Inspect) ✅ No changes detected - Jun 22, 2026, 7:02 AM

@github-actions github-actions Bot added the 📦 Lexbox issues related to any server side code, fw-headless included label May 28, 2026
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

C# Unit Tests

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

Results for commit 0891da0.

♻️ This comment has been updated with latest results.

@rmunn

rmunn commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Please examine how MediaUri is handled in audio files, then suggest how to handle the MediaUri property in the Picture class.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Search for MediaUri in the codebase ==="
rg -n "MediaUri" --type cs -C 3

echo ""
echo "=== Search for audio-related model files ==="
fd -e cs -i "audio" --exec echo {}

echo ""
echo "=== Search for Picture model files ==="
fd -e cs -i "picture" --exec echo {}

echo ""
echo "=== Search for ExampleSentence or audio model definitions ==="
rg -n "class.*Audio\|AudioFile\|Sound" --type cs -C 5

Length of output: 45536


🏁 Script executed:

#!/bin/bash
echo "=== Find MiniLcm model files ==="
fd -e cs . --search-path "MiniLcm" | head -60

echo ""
echo "=== Search for all MediaUri occurrences ==="
rg -rn "MediaUri" -C 5

Length of output: 57201


🏁 Script executed:

#!/bin/bash
echo "=== Search for PictureFileRA (LCM picture file property) ==="
rg -n "PictureFileRA\|InternalPath\|PictureFolder\|Pictures folder\|TypeToLinkedFolder" --type cs -C 3

echo ""
echo "=== Look at UpdatePictureProxy in full ==="
cat backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs

echo ""
echo "=== Look at CreatePictureChange in full ==="
cat backend/FwLite/LcmCrdt/Changes/CreatePictureChange.cs

echo ""
echo "=== Look at PictureSync in full ==="
cat backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs

echo ""
echo "=== Search for TypeToLinkedFolder and AudioVisualFolder constant ==="
rg -n "TypeToLinkedFolder\|AudioVisualFolder\|PictureFolder\|Pictures\b" --type cs -C 2

Length of output: 5543


🏁 Script executed:

#!/bin/bash
echo "=== Find AudioVisualFolder and TypeToLinkedFolder in FwDataMiniLcmApi.cs ==="
grep -n "AudioVisual\|TypeToLinked\|PictureFile\|InternalPath\|Pictures\|LinkedFiles\|CmFile\|cmFile" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | head -60

echo ""
echo "=== View lines around the folder/type mapping function ==="
grep -n "TypeToLinkedFolder\|AudioVisualFolder\|const string\|private.*Folder" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Grep for picture file path references in FW LCM ==="
rg -n "PictureFile\|CmFile\|InternalPath\|\.PictureFilePath\|picture\..*[Ff]ile\|\.ImageFile" \
  backend/FwLite/FwDataMiniLcmBridge/ -C 2

echo ""
echo "=== Check what LCM picture interface looks like ==="
rg -rn "PictureFileRA\|ICmPicture\|CmPicture" --type cs -C 3 | head -80

Length of output: 3346


🏁 Script executed:

#!/bin/bash
echo "=== LcmToPicture method context ==="
grep -n "LcmToPicture\|private.*Picture\|FromLcmPicture\|ToMiniLcmPicture\|ICmPicture" \
  backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== View lines 795-830 of FwDataMiniLcmApi.cs (picture mapping) ==="
sed -n '795,835p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== View lines 1940-1960 (TypeToLinkedFolder) ==="
sed -n '1940,1960p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Search for ICmPicture file-path related properties in LCM NuGet/dlls ==="
rg -rn "\.PictureFile\|picture\.\|ICmPicture\b" --type cs backend/FwLite/ -C 2 | head -60

echo ""
echo "=== Check the SIL LCModel ICmPicture or CmPicture in dlls/packages ==="
find . -name "*.cs" | xargs grep -l "ICmPicture" 2>/dev/null | head -10

echo ""
echo "=== View LfClassicMiniLcmApi picture mapping (line ~350) ==="
grep -n "picture\.\|Picture\|MediaUri\|CmFile\|InternalPath" backend/LfClassicData/LfClassicMiniLcmApi.cs | head -30

Length of output: 4911


🏁 Script executed:

#!/bin/bash
echo "=== Search for ICmFile usage anywhere ==="
rg -rn "ICmFile\|PictureFileRA\|InternalPath\|\.PicturePath\b" --type cs -C 3 | head -60

echo ""
echo "=== Look for how FW code sets picture file path - CreatePicture context ==="
sed -n '1810,1850p' backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

echo ""
echo "=== Search for Pictures folder const or PictureFolder ==="
rg -rn "const.*[Pp]icture\|PictureFolder\b" --type cs -C 2

echo ""
echo "=== Look for nuget packages that contain LCModel ==="
find . -name "*.csproj" | xargs grep -l "LCModel\|liblcm\|SIL.LCModel" 2>/dev/null | head -5

echo ""
echo "=== Check any existing usages that access picture.PictureFile ==="
rg -rn "picture\." --type cs backend/FwLite/FwDataMiniLcmBridge/ -C 1 | head -40

Length of output: 5205



@rmunn Here's a comprehensive analysis of the audio MediaUri pattern and how to apply it to pictures.


How MediaUri works for audio files

The data model difference

In FieldWorks LCM:

  • Audio: Stored as a relative filename (e.g. "recording.wav") inside a MultiString on a writing system tagged with IsAudio. Path is under LinkedFilesRootDir/AudioVisual/.
  • Picture: Stored via ICmPicture.PictureFileRA, which is an ICmFile object whose InternalPath is relative to LinkedFilesRootDir. Files land in LinkedFilesRootDir/Pictures/ (already mapped in TypeToLinkedFolder for image/*).

The audio translation pipeline

Reading (FW → MiniLcm)ToMediaUri() in FwDataMiniLcmApi.cs:

// relative filename from MultiString → full path → MediaUri
var fullFilePath = Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder, tsString);
return mediaAdapter.MediaUriFromPath(fullFilePath, Cache).ToString();

Writing (MiniLcm → FW)FromMediaUri() in FwDataMiniLcmApi.cs:

// MediaUri string → full path → relative path back to AudioVisual subfolder
var mediaUri = new MediaUri(mediaUriString);
var path = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
return Path.GetRelativePath(Path.Join(Cache.LangProject.LinkedFilesRootDir, AudioVisualFolder), path);

Guard on writeUpdateDictionaryProxy.ShouldSet():

if (!wsId.IsAudio) return true;
return value != MediaUri.NotFoundString; // skip writing back NotFound URIs

Suggested implementation for Picture.MediaUri

The pattern is the same; the only difference is the FW storage mechanism (PictureFileRA/ICmFile instead of a MultiString value).

1. Reading — FromLcmPicture in FwDataMiniLcmApi.cs

private Picture FromLcmPicture(Guid senseGuid, ICmPicture picture)
{
    var mediaUri = MediaUri.NotFound;
    var internalPath = picture.PictureFileRA?.InternalPath;
    if (!string.IsNullOrEmpty(internalPath))
    {
        var fullPath = Path.Join(Cache.LangProject.LinkedFilesRootDir, internalPath);
        mediaUri = mediaAdapter.MediaUriFromPath(fullPath, Cache);
    }
    return new Picture
    {
        Id = picture.Guid,
        Caption = FromLcmMultiString(picture.Caption),
        // SenseId = senseGuid,
        MediaUri = mediaUri,
    };
}

2. Writing — CreatePicture in FwDataMiniLcmApi.cs

internal void CreatePicture(ILexSense lexSense, Picture picture, BetweenPosition? between = null)
{
    if (picture.Id == default) picture.Id = Guid.NewGuid();
    var lcmPicture = LcmPictureFactory.Create(picture.Id);
    UpdateLcmMultiString(lcmPicture.Caption, picture.Caption);
    SetLcmPictureFile(lcmPicture, picture.MediaUri);
    InsertPicture(lexSense, lcmPicture, between);
}

private void SetLcmPictureFile(ICmPicture lcmPicture, MediaUri mediaUri)
{
    if (mediaUri == MediaUri.NotFound) return;
    var fullPath = mediaAdapter.PathFromMediaUri(mediaUri, Cache);
    if (fullPath is null) return;
    // Reuse an existing ICmFile with the same path, or create a new one
    var relativePath = Path.GetRelativePath(Cache.LangProject.LinkedFilesRootDir, fullPath);
    var fileRepo = Cache.ServiceLocator.GetInstance<IRepository<ICmFile>>();
    var cmFile = fileRepo.AllInstances().FirstOrDefault(f => f.InternalPath == relativePath);
    if (cmFile is null)
    {
        cmFile = Cache.ServiceLocator.GetInstance<ICmFileFactory>().Create();
        Cache.LangProject.MediaFilesOA.FilesOC.Add(cmFile);
        cmFile.InternalPath = relativePath;
    }
    lcmPicture.PictureFileRA = cmFile;
}

Note: SetLcmPictureFile can be made internal so UpdatePictureProxy can call it directly.

3. Update proxy — UpdatePictureProxy.cs

public override MediaUri MediaUri
{
    get
    {
        var internalPath = picture.PictureFileRA?.InternalPath;
        if (string.IsNullOrEmpty(internalPath)) return MediaUri.NotFound;
        var fullPath = Path.Join(lexboxLcmApi.Cache.LangProject.LinkedFilesRootDir, internalPath);
        return lexboxLcmApi.mediaAdapter.MediaUriFromPath(fullPath, lexboxLcmApi.Cache);
    }
    set => lexboxLcmApi.SetLcmPictureFile(picture, value);
}

4. Sync diff — PictureSync.DiffToUpdate

public static UpdateObjectInput<Picture>? DiffToUpdate(Picture beforePicture, Picture afterPicture)
{
    JsonPatchDocument<Picture> patchDocument = new();
    patchDocument.Operations.AddRange(MultiStringDiff.GetMultiStringDiff<Picture>(
        nameof(Picture.Caption),
        beforePicture.Caption,
        afterPicture.Caption));

    if (beforePicture.MediaUri != afterPicture.MediaUri)
        patchDocument.Replace(p => p.MediaUri, afterPicture.MediaUri);

    if (patchDocument.Operations.Count == 0) return null;
    return new UpdateObjectInput<Picture>(patchDocument);
}

5. CRDT change — CreatePictureChange.cs

public MediaUri MediaUri { get; set; }

public CreatePictureChange(Picture picture, Guid senseId)
    : base(picture.Id == Guid.Empty ? Guid.NewGuid() : picture.Id)
{
    picture.Id = EntityId;
    Order = picture.Order;
    Caption = picture.Caption;
    MediaUri = picture.MediaUri;
}

public override async ValueTask<Picture> NewEntity(Commit commit, IChangeContext context)
{
    return new Picture
    {
        Id = EntityId,
        Order = Order,
        Caption = Caption ?? new(),
        MediaUri = MediaUri,
        DeletedAt = null,
    };
}

6. LF Classic — LfClassicMiniLcmApi.ToPicture

LF Classic's Entities.Picture likely has an ImageFilePath (or similar) field storing a filename. The pattern would be the same: build the full path using the project's linked files root, then call through mediaAdapter:

private static MiniLcm.Models.Picture ToPicture(Guid senseId, Entities.Picture picture)
{
    return new MiniLcm.Models.Picture
    {
        Id = picture.Guid,
        Caption = ToRichMultiString(picture.Caption),
        MediaUri = !string.IsNullOrEmpty(picture.ImageFilePath)
            ? new MediaUri(picture.ImageFilePath)   // or via adapter if one exists for LF Classic
            : MediaUri.NotFound,
    };
}

LF Classic may need its own adapter (or a simpler direct-path approach), since it doesn't go through IMediaAdapter/LCM like the FW bridge does. You'll want to confirm what field on Entities.Picture holds the file reference.


Summary of the pattern

Step Audio Picture
FW storage MultiString[audioWs] = relative filename ICmPicture.PictureFileRA.InternalPath = relative path
Subfolder LinkedFilesRootDir/AudioVisual/ LinkedFilesRootDir/Pictures/
Read (FW→MiniLcm) mediaAdapter.MediaUriFromPath(fullPath) same
Write (MiniLcm→FW) mediaAdapter.PathFromMediaUri() → update string same, but find/create ICmFile, set PictureFileRA
NotFound guard UpdateDictionaryProxy.ShouldSet skips write SetLcmPictureFile returns early if MediaUri.NotFound

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Current code is partly working, but I haven't yet told Entity Framework how to map a Picture object into the DB. Which results in this exception when I run a sync:

System.InvalidOperationException: The property 'Picture.MediaUri' could not be mapped because it is of type 'MediaUri', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IConventionTypeBase structuralType, IConventionModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidatePropertyMapping(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Sqlite.Infrastructure.Internal.SqliteModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkRelationalServicesBuilder.<>c.<TryAddCoreServices>b__3_3(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.Microsoft.EntityFrameworkCore.Storage.IDatabaseFacadeDependenciesAccessor.get_Dependencies()
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.GetFacadeDependencies(DatabaseFacade databaseFacade)
   at Microsoft.EntityFrameworkCore.RelationalDatabaseFacadeExtensions.CloseConnectionAsync(DatabaseFacade databaseFacade)
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 181
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in /src/backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at FwHeadless.Services.SyncWorker.SetupCrdtProject(String crdtFile, IProjectLookupService projectLookupService, Guid projectId, CrdtProjectsService projectsService, String projectFolder, Guid fwProjectId, String lexboxUrl) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 371
   at FwHeadless.Services.SyncWorker.ExecuteSync(CancellationToken stoppingToken, Boolean onlyHarmony) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 188
   at FwHeadless.Services.SyncWorker.ExecuteSync(CancellationToken stoppingToken, Boolean onlyHarmony) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 276
   at FwHeadless.Services.SyncHostedService.ExecuteAsync(CancellationToken stoppingToken) in /src/backend/FwHeadless/Services/SyncHostedService.cs:line 33

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Look at the exception in #2300 (comment) and look at the Entity Framework Core documentation. Summarize how to map the MediaUri property, and provide links to the documentation explaining how to map properties in EF Core.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Find MediaUri type definition ==="
rg -n "class MediaUri\|struct MediaUri\|record MediaUri" --type cs -C 5

echo ""
echo "=== Find existing EF Core value converters in LcmCrdt ==="
rg -n "ValueConverter\|HasConversion\|OnModelCreating\|EntityTypeBuilder\|modelBuilder" --type cs backend/FwLite/LcmCrdt/ -C 3 | head -100

echo ""
echo "=== Find DbContext in LcmCrdt ==="
fd -e cs . backend/FwLite/LcmCrdt/ | xargs grep -l "DbContext\|OnModelCreating" 2>/dev/null

echo ""
echo "=== Check if MediaUri implements any interface or has implicit conversion ==="
rg -n "implicit\|explicit\|operator\|ToString\|Parse\|TryParse" --type cs -l | xargs grep -l "MediaUri" 2>/dev/null

Length of output: 3199


🌐 Web query:

Entity Framework Core value converters custom type mapping documentation HasConversion

Length of output: 20413


🏁 Script executed:

#!/bin/bash
echo "=== MediaUri definition - search all file types ==="
rg -rn "MediaUri" --type cs | grep -i "class\|record\|struct" | head -20

echo ""
echo "=== Find the file that defines MediaUri ==="
rg -rn "MediaUri" --type cs -l

echo ""
echo "=== Read LcmCrdtDbContext.cs ==="
cat backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs

echo ""
echo "=== Read MiniLcm MediaUri source file ==="
fd -e cs "MediaUri" | head -5

Length of output: 6757


🏁 Script executed:

#!/bin/bash
echo "=== Read MediaUri.cs ==="
cat backend/FwLite/MiniLcm/Media/MediaUri.cs

Length of output: 1864



@rmunn — Great news: LcmCrdtDbContext already has the exact pattern you need. The WritingSystemIdConverter does WritingSystemId ↔ string in the same way MediaUri needs to be handled, since MediaUri serializes to/from a plain URI string via ToString() / new MediaUri(str).


The fix

Add a converter class and register it in ConfigureConventions inside LcmCrdtDbContext:

// In LcmCrdtDbContext.ConfigureConventions:
protected override void ConfigureConventions(ModelConfigurationBuilder builder)
{
    builder.Properties<MultiString>()
        .HaveColumnType("jsonb")
        .HaveConversion<MultiStringDbConverter>();
    builder.Properties<RichString?>()
        .HaveColumnType("jsonb")
        .HaveConversion<RichStringDbConverter>();
    builder.Properties<RichMultiString>()
        .HaveColumnType("jsonb")
        .HaveConversion<RichMultiStringDbConverter>();
    builder.Properties<WritingSystemId>()
        .HaveConversion<WritingSystemIdConverter>();

    // Add this:
    builder.Properties<MediaUri>()
        .HaveConversion<MediaUriConverter>();
}

// Add this converter class alongside the others:
private class MediaUriConverter() : ValueConverter<MediaUri, string>(
    uri => uri.ToString(),
    str => new MediaUri(str));

HaveConversion in ConfigureConventions is the pre-convention (global) approach — it applies the converter to every MediaUri property across all entity types automatically, so you don't need to touch OnModelCreating for each entity.


Relevant EF Core documentation

Topic Link
Value Conversions (main doc — HasConversion, ValueConverter<TModel,TProvider>, pre-convention config) https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions
Pre-convention model configuration (ConfigureConventions / HaveConversion) https://learn.microsoft.com/en-us/ef/core/modeling/bulk-configuration#pre-convention-configuration
PropertyBuilder.HasConversion API reference https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.builders.propertybuilder.hasconversion
ValueConverter<TModel,TProvider> API reference https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.storage.valueconversion.valueconverter-2

After adding the converter, you'll also need to create a new EF migration (dotnet ef migrations add AddMediaUriConverter or similar) so the schema change is tracked.

@rmunn

rmunn commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - When I run task fw-lite-web, I am unable to login to the local Lexbox server inside the FW Lite Web application. Clicking on the "log in to see projects" button in the "Lexbox Local Server" category at http://localhost:5137/ tries to go to http://localhost:5137/api/auth/login/localhost:3050, which produces an error page. The error message is Microsoft.Identity.Client.MsalServiceException: Issuer validation failed for authority: https://localhost:3050/ . Issuer from OIDC endpoint does not match any expected pattern: http://localhost:3050/.

Please analyze the port-forwarding setup for the task fw-lite-web process, and determine why it's using HTTP on port 3050 to forward to the local lexbox server, instead of HTTPS on port 3050. Suggest a solution.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Find Taskfile for fw-lite-web ==="
fd -i "Taskfile" | head -20

echo ""
echo "=== Search for fw-lite-web in Taskfiles ==="
rg -n "fw-lite-web\|fw_lite_web" --type yaml -C 5

echo ""
echo "=== Search for port 3050 references ==="
rg -rn "3050" -C 3 | head -60

echo ""
echo "=== Search for port forwarding or devtunnel setup ==="
rg -rn "devtunnel\|port.forward\|portforward\|forward.*3050\|3050.*forward" -C 5 | head -60

Length of output: 28033


🏁 Script executed:

#!/bin/bash
echo "=== Search for OIDC/auth configuration with localhost ==="
rg -rn "localhost:3050\|localhost.*3050\|3050.*localhost" -C 3 | head -80

echo ""
echo "=== Search for OidcAuthority or Authority config ==="
rg -rn "Authority\|OidcAuthority\|OpenIdConnect\|oidc" --type cs -C 3 | head -80

echo ""
echo "=== Search for auth login endpoint ==="
rg -rn "api/auth/login\|AuthController\|login.*controller" --type cs -C 5 | head -60

Length of output: 637


🏁 Script executed:

#!/bin/bash
echo "=== Root Taskfile.yml - search for fw-lite-web or related ==="
grep -n "fw-lite-web\|fw_lite_web\|fw-lite\|fwlite\|5137\|3050\|https-proxy\|devtunnel\|forward" Taskfile.yml | head -60

echo ""
echo "=== backend/FwLite/Taskfile.yml - full content ==="
cat backend/FwLite/Taskfile.yml

echo ""
echo "=== frontend/https-proxy/vite.config.ts - full content ==="
cat frontend/https-proxy/vite.config.ts

echo ""
echo "=== backend/Taskfile.yml - search for fw-lite or 3050 or 5137 ==="
grep -n "fw-lite\|3050\|5137\|https-proxy\|devtunnel\|forward" backend/Taskfile.yml | head -40

Length of output: 10948


🏁 Script executed:

#!/bin/bash
echo "=== Search for 5137 in all files ==="
rg -rn "5137" -C 2 | head -60

echo ""
echo "=== Search for lexbox server URL config in appsettings or launchSettings ==="
fd -e json "appsettings\|launchSettings" backend/FwLite/ | head -20
fd -e json "appsettings\|launchSettings" backend/FwLite/ | xargs grep -l "localhost\|lexbox\|3050\|Authority\|oidc\|auth" 2>/dev/null | head -10

echo ""
echo "=== Search for auth login in FwLite web project ==="
rg -rn "login\|auth\|oidc\|Authority" backend/FwLite/FwLiteWeb/ -C 3 | head -80

echo ""
echo "=== Find FwLiteWeb project structure ==="
fd . backend/FwLite/FwLiteWeb/ | head -40

Length of output: 14226


🏁 Script executed:

#!/bin/bash
echo "=== AuthRoutes.cs ==="
cat backend/FwLite/FwLiteWeb/Routes/AuthRoutes.cs

echo ""
echo "=== Program.cs ==="
cat backend/FwLite/FwLiteWeb/Program.cs

echo ""
echo "=== appsettings.json files in FwLiteWeb ==="
fd -e json "appsettings" backend/FwLite/FwLiteWeb/ | xargs -I{} sh -c 'echo "--- {} ---"; cat {}'

echo ""
echo "=== frontend Taskfile.yml - fw-lite-infr task ==="
grep -n "fw-lite-infr\|https-proxy\|3050\|lexbox" frontend/Taskfile.yml -A 10 | head -60

echo ""
echo "=== FwLiteWebConfig.cs ==="
cat backend/FwLite/FwLiteWeb/FwLiteWebConfig.cs

echo ""
echo "=== ServerRedirectUrlProvider.cs ==="
cat backend/FwLite/FwLiteWeb/Services/ServerRedirectUrlProvider.cs

Length of output: 9618


🏁 Script executed:

#!/bin/bash
echo "=== Find AuthService and AuthConfig ==="
fd -e cs "AuthService\|AuthConfig\|OAuthService" backend/FwLite/ | head -10

echo ""
echo "=== AuthConfig.cs ==="
fd -e cs "AuthConfig" backend/FwLite/ | xargs cat

echo ""
echo "=== AuthService.cs ==="
fd -e cs "AuthService" backend/FwLite/ | xargs cat

echo ""
echo "=== OAuthService.cs ==="
fd -e cs "OAuthService" backend/FwLite/ | xargs cat

echo ""
echo "=== UrlContext.cs ==="
cat backend/FwLite/FwLiteWeb/UrlContext.cs

echo ""
echo "=== Search for ValidateAuthority or ValidIssuers or MSAL config ==="
rg -rn "ValidateAuthority\|ValidIssuers\|PublicClientApplicationBuilder\|WithAuthority\|OidcWellKnown\|openid-configuration" --type cs -C 5 | head -80

echo ""
echo "=== Root Taskfile lines 113-120 (fw-lite-web task definition) ==="
sed -n '100,130p' Taskfile.yml

Length of output: 22755


🏁 Script executed:

#!/bin/bash
echo "=== Find OAuthClientFactory and OAuthClient ==="
fd -e cs "OAuthClient" backend/FwLite/ | head -10

echo ""
echo "=== OAuthClientFactory.cs ==="
fd -e cs "OAuthClientFactory" backend/FwLite/ | xargs cat

echo ""
echo "=== OAuthClient.cs ==="
fd -e cs -x echo {} backend/FwLite/ | xargs grep -l "OAuthClient" | grep -v "Factory\|Service\|Test" | head -5 | xargs cat

echo ""
echo "=== Search for PublicClientApplication or WithAuthority in FwLiteShared ==="
rg -rn "PublicClientApplication\|WithAuthority\|BuildApplication\|IPublicClientApplication" --type cs backend/FwLite/ -C 5 | head -80

echo ""
echo "=== Search for ValidateIssuer or authority in appsettings for the local lexbox ==="
fd -e json "appsettings" backend/ | xargs grep -l "localhost\|3050\|3000\|authority\|Authority" 2>/dev/null | head -5

echo ""
echo "=== Check if local lexbox server config has ASPNETCORE_FORWARDEDHEADERS ==="
fd -i "docker-compose" . | xargs grep -l "lexbox\|3000\|3050" 2>/dev/null | head -5
fd "docker-compose" . | head -10

Length of output: 60547



@rmunn Here's the complete analysis.


Root Cause

The data flow is:

Browser → https://localhost:3050 (vite HTTPS proxy)
                  ↓ (forwards to)
          http://localhost:3000 (local lexbox server, plain HTTP)

The chain of events that causes the mismatch:

  1. OAuthClient.cs builds the MSAL application with .WithOidcAuthority("https://localhost:3050/") — because LexboxServer.Authority is the HTTPS proxy URL.
  2. MSAL fetches the OIDC discovery document from https://localhost:3050/.well-known/openid-configuration.
  3. The vite proxy (with changeOrigin: false) forwards this to http://localhost:3000/.well-known/openid-configuration, passing the browser's Host: localhost:3050 header through unchanged.
  4. The local lexbox server (ASP.NET Core on HTTP) builds its issuer URL as: own scheme (http://) + Host header (localhost:3050) → "issuer": "http://localhost:3050/".
  5. The x-forwarded-proto: https header IS sent by the vite proxy, but the lexbox server's ForwardedHeaders middleware is not configured to honor it in development.
  6. protocolRewrite: 'https' in the vite config only rewrites Location headers in redirects — it does not rewrite JSON response bodies. The OIDC discovery JSON is untouched.
  7. MSAL validates authority https://localhost:3050/ against issuer http://localhost:3050/mismatch → exception.

Fix Options

Option A — Fix in the vite proxy (self-contained, no server changes needed)

Intercept the OIDC discovery response and rewrite http://https:// in the JSON body. Add this to the configure callback in frontend/https-proxy/vite.config.ts:

import { gunzipSync } from 'zlib';

// Inside the lexboxServer ProxyOptions configure callback, after existing code:
proxy.on('proxyRes', (proxyRes, req, res) => {
    if (!req.url?.includes('openid-configuration')) return;

    const chunks: Buffer[] = [];
    const originalWrite = res.write.bind(res);
    const originalEnd = res.end.bind(res);

    // Intercept the streamed response
    proxyRes.on('data', (chunk: Buffer) => chunks.push(chunk));
    proxyRes.on('end', () => {
        const body = Buffer.concat(chunks).toString('utf-8');
        const rewritten = body.replaceAll(
            /"issuer"\s*:\s*"http:\/\//g,
            '"issuer":"https://'
        );
        const newBody = Buffer.from(rewritten, 'utf-8');
        res.setHeader('Content-Length', newBody.length);
        originalWrite(newBody);
        originalEnd();
    });
});

Note: Vite uses http-proxy under the hood; you may need to set selfHandleResponse: true on the proxy options to take full control of the response stream, or use the simpler approach of hooking into proxyRes before headers are flushed.

A simpler alternative using vite's built-in transformer hook (if supported by your vite version):

// In vite.config.ts server.proxy options:
'/': {
    ...lexboxServer,
    // Add a response rewriter for OIDC discovery only
    rewrite: (path) => path,
}

Option B — Fix in the local lexbox server (correct architectural fix)

Enable ForwardedHeaders middleware in the lexbox API so it honors X-Forwarded-Proto: https. The easiest way in development is to set an environment variable in the dev docker-compose or k8s manifest:

environment:
  ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"

This makes ASP.NET Core automatically enable forwarded headers, so the lexbox server generates https://localhost:3050/ as the OIDC issuer when it receives X-Forwarded-Proto: https.

Or in backend/LexBoxApi/appsettings.Development.json:

{
  "ForwardedHeaders": {
    "ForwardedHeaders": "XForwardedFor, XForwardedProto"
  }
}

Option C — Fix in OAuthClient.cs (pragmatic dev workaround)

For localhost authorities specifically, skip MSAL's authority validation:

var builder = PublicClientApplicationBuilder.Create(options.Value.ClientId)
    .WithExperimentalFeatures()
    .WithLogging(loggerAdapter, hostEnvironment?.IsDevelopment() ?? false)
    .WithHttpClientFactory(new HttpClientFactoryAdapter(httpMessageHandlerFactory))
    .WithParentActivityOrWindow(() => options.Value.ParentActivityOrWindow)
    .WithOidcAuthority(lexboxServer.Authority.ToString());

// For localhost dev servers, the HTTPS proxy wraps HTTP, so the issuer will be http:// 
// even though the authority is https://. Skip validation in this case.
if (lexboxServer.Authority.IsLoopback)
    builder.WithValidateAuthority(false);

Recommendation

Option B is the most architecturally correct fix — the lexbox server should honor X-Forwarded-Proto, especially since the vite proxy already sends it. Setting ASPNETCORE_FORWARDEDHEADERS_ENABLED=true in the dev environment is a one-line change with no code modifications.

Option A is useful if you can't or don't want to touch the lexbox server dev config, since the vite proxy was designed specifically to handle this HTTPS wrapping concern.

Option C is a quick local workaround but slightly weakens MSAL's validation for all localhost servers, not just this specific scenario.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

C# FwHeadless Unit Tests

48 tests   48 ✅  15s ⏱️
 5 suites   0 💤
 1 files     0 ❌

Results for commit 0891da0.

♻️ This comment has been updated with latest results.

@rmunn

rmunn commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit - Syncing pictures using commit 3a729a7 produced the following exception:

System.NotSupportedException: type JsonPatchChange`1 does not support NewEntity, because it inherits from EditChange, this means it must be called with a from an existing entity, not a newly generated one

at SIL.Harmony.Changes.EditChange`1.NewEntity(Commit commit, IChangeContext context) in backend/harmony/src/SIL.Harmony/Changes/EditChange.cs:line 8
   at SIL.Harmony.Changes.Change`1.SIL.Harmony.Changes.IChange.NewEntity(Commit commit, IChangeContext context) in backend/harmony/src/SIL.Harmony/Changes/Change.cs:line 51
   at SIL.Harmony.SnapshotWorker.ApplyCommitChanges(SortedSet`1 commits) in backend/harmony/src/SIL.Harmony/SnapshotWorker.cs:line 78
   at SIL.Harmony.SnapshotWorker.UpdateSnapshots(SortedSet`1 commits) in backend/harmony/src/SIL.Harmony/SnapshotWorker.cs:line 54
   at SIL.Harmony.DataModel.UpdateSnapshots(CrdtRepository repo, SortedSet`1 commitsToApply) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 217
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 159
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 161
   at SIL.Harmony.DataModel.SIL.Harmony.ISyncable.AddRangeFromSync(IEnumerable`1 commits) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 161
   at SIL.Harmony.SyncHelper.SyncWith(ISyncable localModel, ISyncable remoteModel, JsonSerializerOptions serializerOptions) in backend/harmony/src/SIL.Harmony/SyncHelper.cs:line 42
   at SIL.Harmony.DataModel.SyncWith(ISyncable remoteModel) in backend/harmony/src/SIL.Harmony/DataModel.cs:line 377
   at FwLiteShared.Sync.SyncService.ExecuteSync(Boolean skipNotifications) in backend/FwLite/FwLiteShared/Sync/SyncService.cs:line 89
   at FwLiteShared.Projects.CombinedProjectsService.<>c.<<DownloadProject>b__16_1>d.MoveNext() in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 202
--- End of stack trace from previous location ---
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 175
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 191
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at LcmCrdt.CrdtProjectsService.CreateProject(CreateProjectRequest request) in backend/FwLite/LcmCrdt/CrdtProjectsService.cs:line 194
   at FwLiteShared.Projects.CombinedProjectsService.<>c__DisplayClass16_0.<<DownloadProject>b__0>d.MoveNext() in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 196
--- End of stack trace from previous location ---
   at FwLiteShared.Projects.CombinedProjectsService.DownloadProject(ProjectModel project) in backend/FwLite/FwLiteShared/Projects/CombinedProjectsService.cs:line 196

If possible, analyse why NewEntity is being called on a JsonPatchChange<Picture> instead of a CreatePictureChange being used. Summarise the reason and suggest a solution.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PictureSync.cs ==="
fd -e cs "PictureSync" | xargs cat

echo ""
echo "=== CreatePictureChange.cs ==="
fd -e cs "CreatePictureChange" | xargs cat

echo ""
echo "=== Search for where pictures are synced from FW to CRDT ==="
rg -n "CreatePicture\|AddPicture\|picture.*sync\|sync.*picture" --type cs -i backend/FwLite/ -C 3 | head -80

echo ""
echo "=== How other entity syncs look - e.g. EntrySync ==="
fd -e cs "EntrySync" backend/FwLite/ | xargs cat 2>/dev/null | head -100

echo ""
echo "=== How other entity syncs look - e.g. SenseSync ==="
fd -e cs "SenseSync" backend/FwLite/ | xargs cat 2>/dev/null | head -100

Length of output: 10820


🏁 Script executed:

#!/bin/bash
echo "=== Find CrdtMiniLcmApi picture-related methods ==="
fd -e cs "CrdtMiniLcmApi" backend/FwLite/ | xargs grep -n "Picture\|picture" | head -60

echo ""
echo "=== Find CreatePicture and UpdatePicture implementations in LcmCrdt ==="
rg -n "CreatePicture\|UpdatePicture\|DeletePicture\|MovePicture" --type cs backend/FwLite/LcmCrdt/ -C 5

echo ""
echo "=== Find how JsonPatchChange is created/used ==="
rg -n "JsonPatchChange\b" --type cs backend/FwLite/ -C 3

echo ""
echo "=== CrdtFwdataProjectSyncService - picture related sync code ==="
rg -n "Picture\|picture" --type cs backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs -C 3

echo ""
echo "=== Check UpdatePicture in LcmCrdt and what change type it creates ==="
rg -n "async.*UpdatePicture\|Task.*UpdatePicture" --type cs backend/FwLite/ -C 10

Length of output: 14639



@rmunn — Here's the analysis.

Root cause

CrdtMiniLcmApi has two UpdatePicture overloads:

// Overload 1 — takes an UpdateObjectInput, creates JsonPatchChange<Picture>
public async Task<Picture> UpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput<Picture> update)
{
    var patchChange = new JsonPatchChange<Picture>(pictureId, jsonPatch);  // ← EditChange, no NewEntity support
    await AddChange(patchChange);
    ...
}

// Overload 2 — takes before/after Pictures, calls PictureSync.Sync (single-picture version)
public async Task<Picture> UpdatePicture(Guid entryId, Guid senseId, Picture before, Picture after, ...)
{
    await PictureSync.Sync(entryId, senseId, before, after, api ?? this);  // ← ONLY calls UpdatePicture, NEVER CreatePicture
    ...
}

The single-picture PictureSync.Sync overload only ever calls api.UpdatePicture(...). It never calls api.CreatePicture. So if this overload is invoked for a picture that doesn't yet exist in the CRDT, it commits a JsonPatchChange<Picture> for an entity that has no CreatePictureChange — and when Harmony's SnapshotWorker tries to apply it, it calls NewEntity on the JsonPatchChange, which throws.

The sync code on the FW headless server is taking a path that calls the "before/after Picture" UpdatePicture overload (or uses PictureSync.Sync single-picture overload) for pictures that are new to the CRDT — treating them as updates rather than creates.


Where to look

The FW headless sync must have new code (in this PR) in either SenseSync.cs or CrdtFwdataProjectSyncService.cs that processes pictures. The bug will be that for senses containing pictures, instead of going through the collection-based PictureSync.Sync:

// CORRECT — uses DiffOrderable, calls CreatePicture for new pictures
await PictureSync.Sync(entryId, senseId, beforeSense.Pictures, afterSense.Pictures, api);

…it's going through something like:

// WRONG — single-picture overload, always emits JsonPatchChange, never CreatePicture
foreach (var (before, after) in matchedPictures)
    await api.UpdatePicture(entryId, senseId, before, after);
// or
await PictureSync.Sync(entryId, senseId, beforePicture, afterPicture, api);

Suggested fix

Option A — In SenseSync.Sync, add the collection-based picture sync (the same pattern already used for ExampleSentenceSync):

// In SenseSync.Sync, alongside the ExampleSentenceSync call:
changes += await PictureSync.Sync(entryId,
    beforeSense.Id,
    beforeSense.Pictures,
    afterSense.Pictures,
    api);

Option B — If the FW sync has a custom loop in CrdtFwdataProjectSyncService, replace any per-picture UpdatePicture call with a call to the collection overload of PictureSync.Sync, so DiffOrderable can correctly route to CreatePicture (Add) vs UpdatePicture (Replace).

The key invariant to enforce: api.CreatePicture must be called for any picture that doesn't yet exist in the CRDT, and api.UpdatePicture must only be called for pictures that already have a CreatePictureChange in the commit log.


🧠 Learnings used
Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs : In FwLiteProjectSync/CrdtFwdataProjectSyncService.cs, regenerate ProjectSnapshot from CRDT after sync (see issue `#1912`)

Learnt from: rmunn
Repo: sillsdev/languageforge-lexbox PR: 2192
File: backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs:125-127
Timestamp: 2026-03-11T04:58:21.676Z
Learning: In sillsdev/languageforge-lexbox backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs SyncInternal, the two-pass sync pattern is intentional and lossless: pass 1 (e.g., line 126 for MorphTypeData) updates the live CRDT from FW data using the project snapshot as "before"; pass 2 (e.g., line 127) reads the *already-updated* live CRDT and pushes it to FW. This means a null value in the CRDT snapshot cannot destructively wipe an FW value — it will be reconciled to the FW value in pass 1 before pass 2 ever runs. A null reaching FW in pass 2 only happens when the user intentionally deleted it in CRDT, which is correct bidirectional sync behavior.

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/*.cs : Explicitly call fwdataApi.Save() after applying changes to FwData in sync operations

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: Compare CRDT and FwData states manually when debugging sync failures; if ProjectSnapshot is incorrect, sync results will be incorrect

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: The most dangerous sync logic is located in `backend/FwLite/FwLiteProjectSync/` with the core algorithm in `CrdtFwdataProjectSyncService.cs`; refer to `backend/FwLite/AGENTS.md` for detailed sync documentation

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync.Tests/**/*Tests.cs : Add sync round-trip tests in FwLiteProjectSync.Tests/ for new sync features to verify bidirectional consistency

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs : Use two-phase sync for complex forms in FwLiteProjectSync: SyncWithoutComplexFormsAndComponents first, then SyncComplexFormsAndComponents

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwHeadless/AGENTS.md:0-0
Timestamp: 2025-12-05T10:28:05.034Z
Learning: Applies to backend/FwHeadless/Services/CrdtSyncService.cs : Always check `syncResults.IsSynced`, `syncResults.MissingFromLocal`, and `syncResults.MissingFromRemote` after calling `dataModel.SyncWith()` in CrdtSyncService; if sync fails silently, users lose data

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : Profile CRDT change classes with realistic data (1000+ entries) before committing if adding complex operations

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : In Harmony Change classes, check if referenced entities are deleted (DeletedAt is not null) before processing

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/LcmCrdt/Changes/*.cs : For new simple field changes in CRDT, use JsonPatchChange<T> instead of creating dedicated change classes

Learnt from: CR
Repo: sillsdev/languageforge-lexbox PR: 0
File: backend/FwLite/AGENTS.md:0-0
Timestamp: 2026-06-03T12:11:28.780Z
Learning: Applies to backend/FwLite/MiniLcm.Tests/**/*Tests.cs : Add test cases in MiniLcm.Tests/ for base model behavior when adding new model features

rmunn added 3 commits June 11, 2026 13:58
Compiles, but some logic (most notably MediaUri handling) is incomplete
Next up will be writing tests to make sure we can properly two-way sync
a picture from FW Classic, i.e. that the MediaUri handling is correct.
@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on top of current develop; hopefully this will let me test syncing now.

@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Huh. Look commit 2ce7ec8 and, specifically, at the DB migration. Why did it add the HomographNumber property? I might have had a local DB that's not up-to-date. I might have to delete that migration and create a new one.

@rmunn

rmunn commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Force-pushed to get rid of commit 2ce7ec8 so the migration won't add a duplicate HomographNumber column.

rmunn added 3 commits June 11, 2026 15:53
MiniLcmApiWriteNormalizationWrapper is new on develop branch since I
started this PR, so I didn't have the Picture CRUD API calls for it.
rmunn added 4 commits June 18, 2026 09:26
Now that `pictures` is a required property of ISense, the demo data
shape needs to change to include it as well. No actual pictures in the
demo yet, that can come later.
Since Picture is no longer a Harmony top-level type (i.e. one that's
stored in its own DB table), CreateChange and DeleteChange are no longer
applicable. We had changed CreateChange but missed DeleteChange earlier.

Also renamed the CreatePictureChange file to match the class name.
Two more places needed to be updated so ISense will typecheck.
@rmunn

rmunn commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit fullreview

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

@rmunn Sure, I'll kick off a full review of the PR now!

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

🧹 Nitpick comments (2)
backend/FwLite/MiniLcm/Models/Picture.cs (1)

5-7: ⚡ Quick win

Outdated TODO comment.

The TODO at lines 5-6 asks whether to implement IOrderable, but line 7 already implements it. The comment should be removed or updated to reflect the current implementation status.

🧹 Suggested cleanup
-// TODO: Determine whether FW allows users to add multiple pictures to a single sense, and whether users can then reorder pictures
-// If so, implement IOrderable
 public class Picture : IObjectWithId<Picture>, IOrderable
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/MiniLcm/Models/Picture.cs` around lines 5 - 7, The TODO
comment in the Picture class is outdated. The comment questions whether to
implement IOrderable, but the very next line shows the Picture class already
implements IOrderable. Remove the TODO comment entirely since the implementation
decision has already been made and is reflected in the class definition.
backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs (1)

12-20: 💤 Low value

Order assignment is immediately overwritten in ApplyChange.

Line 16 assigns Order = picture.Order, but line 35 in ApplyChange recomputes it via OrderPicker.PickOrder(entity.Pictures, Between). The constructor assignment is misleading since the value is never used—ApplyChange always determines the final order.

Consider removing line 16 or adding a comment explaining that the initial order is overwritten during application.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs` around lines 12 -
20, The Order property is being assigned from picture.Order in the
CreateSensePictureChange constructor on line 16, but this value is immediately
overwritten in the ApplyChange method when OrderPicker.PickOrder is called. This
makes the constructor assignment misleading and unnecessary. Either remove the
Order assignment from the constructor, or add a comment explaining that the
initial order value is only temporary and will be recalculated during
ApplyChange when the actual order positioning is determined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs`:
- Around line 1833-1837: The GetPicture method is missing ownership validation
for the entryId and senseId parameters. Currently it retrieves a picture by
global ID without verifying that the picture actually belongs to the specified
entry and sense. Add validation after retrieving the picture via
PictureRepository.TryGetObject to confirm the lcmPicture belongs to the provided
entryId and senseId before calling FromLcmPicture, similar to how
update/move/delete operations enforce these boundary checks. Apply the same
ownership validation to the related picture creation path in the method around
lines 1857-1868 where only the sense existence is checked.

In `@backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs`:
- Around line 52-56: The Pictures property getter and setter in the
UpdateSenseProxy class both throw NotImplementedException, which causes runtime
crashes when update operations attempt to access this property. Instead of
throwing NotImplementedException for both the get and set accessors of the
Pictures property, implement them to either support the update operation
properly or throw a more meaningful exception that cleanly rejects the update
with proper context about why pictures cannot be updated through this proxy.

In `@backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs`:
- Around line 250-251: The removePictureChange variable is created but never
emitted in the yield statement, and instead setPictureOrderChange is being
yielded a second time. Change the yield statement for ChangeWithDependencies to
use removePictureChange instead of the duplicate setPictureOrderChange to ensure
the RemoveSensePictureChange is properly tested and avoid potential
duplicate-key failures when the changes are collected into a dictionary.

In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs`:
- Around line 879-888: The UpdatePicture method incorrectly uses
JsonPatchChange<Picture> which assumes Picture is a standalone CRDT entity, but
Picture is actually a JSON-stored value within the Sense entity. Create a new
UpdateSensePictureChange class that extends EditChange<Sense> (following the
pattern of CreateSensePictureChange, RemoveSensePictureChange, and
ReorderSensePictureChange). This new class should accept the picture ID, sense
ID, and JsonPatch operations, then in its ApplyChange method locate the picture
in sense.Pictures by ID and apply the patch operations to it. Replace the
JsonPatchChange<Picture> instantiation in the UpdatePicture method with an
instance of this new UpdateSensePictureChange class.
- Around line 862-870: The CreatePicture method returns the input picture
parameter directly, but CreateSensePictureChange may generate a new ID when
picture.Id is Guid.Empty and computes the Order value from the Between position
during ApplyChange, resulting in stale values being returned to the caller.
Follow the pattern established in CreateExampleSentence by fetching the picture
from the repository after adding the change via AddChange to ensure the returned
picture contains the correct Id and Order values that were computed during the
change application. You may need to capture the ID generated by
CreateSensePictureChange and use it to retrieve the picture from the repository.

In `@backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs`:
- Around line 106-108: The PictureListDbConverter class deserializes JSON
strings but the migration sets a default value of empty string, which is invalid
JSON and will throw a JsonException. The null-coalescing operator only handles
null returns, not exceptions. Add an explicit check for empty or null strings in
the deserialization lambda of the ValueConverter (the second parameter that
currently has the JsonSerializer.Deserialize call) before attempting
deserialization, similar to the pattern used in LcmCrdtKernel.cs, or
alternatively change the migration default value in file
20260617073518_PicturesStoredAsJsonOnSenses.cs from empty string to the valid
JSON array "[]".

In
`@backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs`:
- Around line 13-18: The AddColumn call for the Pictures column in the migration
has an invalid default value of an empty string which is not valid JSON and will
cause JsonSerializer.Deserialize to fail when PictureListDbConverter attempts to
process existing rows. Change the defaultValue parameter from an empty string to
a valid empty JSON array representation to ensure the deserializer receives
syntactically correct JSON.

In `@backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs`:
- Around line 445-451: The NormalizePicture method at line 445 is not being
called within the NormalizeSense method, which is the primary entry point for
normalizing sense data. To fix this, integrate NormalizePicture into the
NormalizeSense method by ensuring that any Picture objects in the Sense.Pictures
collection are normalized using the NormalizePicture helper. This will ensure
picture captions are properly normalized when senses are written through the
normalization flow.
- Around line 458-461: The UpdatePicture method currently uses NormalizePatch
which normalizes all string fields including mediaUri, which conflicts with the
design pattern established in NormalizePicture that explicitly avoids mutating
media identifiers. Instead of using NormalizePatch, apply a selective
normalization approach that excludes the mediaUri field from normalization,
similar to how NormalizePicture handles Picture objects to preserve media
identifiers.

In `@backend/LfClassicData/LfClassicMiniLcmApi.cs`:
- Around line 450-459: The GetPicture method is not discoverable because the
sense projection in this adapter does not expose the Sense.Pictures collection,
preventing clients from enumerating picture IDs through normal sense read flows.
Locate the sense projection or query mapping configuration used when fetching
senses, and ensure that the Sense.Pictures collection is included in the
projection so clients can access picture IDs and subsequently call the
GetPicture method with valid picture identifiers.

---

Nitpick comments:
In `@backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs`:
- Around line 12-20: The Order property is being assigned from picture.Order in
the CreateSensePictureChange constructor on line 16, but this value is
immediately overwritten in the ApplyChange method when OrderPicker.PickOrder is
called. This makes the constructor assignment misleading and unnecessary. Either
remove the Order assignment from the constructor, or add a comment explaining
that the initial order value is only temporary and will be recalculated during
ApplyChange when the actual order positioning is determined.

In `@backend/FwLite/MiniLcm/Models/Picture.cs`:
- Around line 5-7: The TODO comment in the Picture class is outdated. The
comment questions whether to implement IOrderable, but the very next line shows
the Picture class already implements IOrderable. Remove the TODO comment
entirely since the implementation decision has already been made and is
reflected in the class definition.
🪄 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: 1a0eaee0-11d3-4424-856a-85b1f6c8e0b9

📥 Commits

Reviewing files that changed from the base of the PR and between fa14cfa and b618feb.

📒 Files selected for processing (31)
  • backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs
  • backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
  • backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/RemoveSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.Designer.cs
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs
  • backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
  • backend/FwLite/LcmCrdt/OrderPicker.cs
  • backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
  • backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
  • backend/FwLite/MiniLcm/Models/Picture.cs
  • backend/FwLite/MiniLcm/Models/Sense.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs
  • backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs
  • backend/LfClassicData/Entities/Entry.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IPicture.ts
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/ISense.ts
  • frontend/viewer/src/lib/sandbox/Sandbox.svelte
  • frontend/viewer/src/lib/utils.ts
  • frontend/viewer/src/project/demo/demo-entry-data.ts

Comment thread backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Comment on lines +52 to +56
public override List<Picture> Pictures
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pictures proxy accessors hard-fail on update paths.

Line 54 and Line 55 throw NotImplementedException, so any UpdateSense patch touching /pictures will crash at runtime instead of being handled or rejected cleanly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs`
around lines 52 - 56, The Pictures property getter and setter in the
UpdateSenseProxy class both throw NotImplementedException, which causes runtime
crashes when update operations attempt to access this property. Instead of
throwing NotImplementedException for both the get and set accessors of the
Pictures property, implement them to either support the update operation
properly or throw a more meaningful exception that cleanly rejects the update
with proper context about why pictures cannot be updated through this proxy.

Comment thread backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs Outdated
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Comment thread backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
Comment thread backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs Outdated
Comment thread backend/LfClassicData/LfClassicMiniLcmApi.cs
rmunn added 6 commits June 18, 2026 15:49
Since pictures aren't a top-level Harmony object, they need a dedicated
change type in order to update them; can't just use `JsonPatch<Picture>`
because Harmony will look for the picture via its ID (alone, no senseId)
and when it doesn't find it, try to call NewEntity() which fails.

So now the JsonPatchDocument is carried in an UpdateSensePictureChange,
which also records the senseId so that it can retrieve the sense and get
the picture from the sense. Then the JsonPatch is correctly applied.
The NormalizePatch method will get special handling for MediaUri values,
to ensure that they remain exactly as found in the .fwdata file and are
not accidentally changed.
No LfClassicMediaAdapter yet, so this won't be able to find the files,
but at least the Sense objects returned will have Pictures lists that
are non-empty. Later we'll create a LfClassicMediaAdapter to find the
picture files in Language Forge.
A jsonb property that's a list should have default value "[]".
Need to check if this is correct, because it might be a chicken-and-egg
situation where the AddChange won't have actually applied the change yet.
@rmunn

rmunn commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs`:
- Around line 28-30: The UpdateSensePictureChange class currently applies a
patch to a picture found only by PictureId without checking if it has been
soft-deleted. After the FirstOrDefault call that retrieves the picture by
PictureId, add an additional null check to verify that the picture's DeletedAt
property is null before calling Patch.ApplyTo. If the picture's DeletedAt is not
null, the method should return without applying the patch, preventing mutations
to soft-deleted picture records.
🪄 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: 6fb0470f-da98-4579-b826-9238606e508a

📥 Commits

Reviewing files that changed from the base of the PR and between b618feb and fa043be.

📒 Files selected for processing (8)
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs

Comment thread backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs Outdated
Updates on deleted pictures, like other entities, should become no-ops.
@rmunn

rmunn commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit fullreview

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

@rmunn Sure, I'll kick off a full review of the PR now!

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs (1)

21-21: ⚡ Quick win

Align CreateSenseChange picture payload with materialization behavior.

Line 21 and Line 36 persist Pictures on the change, but Line 51 always materializes Sense.Pictures as empty. That creates a misleading/ignored payload path and extra commit bloat. Prefer removing Pictures from CreateSenseChange (since picture creation is already handled by CreateSensePictureChange) to keep one source of truth.

Suggested diff
 public CreateSenseChange(Sense sense, Guid entryId) : base(sense.Id == Guid.Empty ? Guid.NewGuid() : sense.Id)
 {
     sense.Id = EntityId;
     EntryId = entryId;
     Order = sense.Order;
     Definition = sense.Definition;
     SemanticDomains = sense.SemanticDomains;
     Gloss = sense.Gloss;
     PartOfSpeechId = sense.PartOfSpeech?.Id ?? sense.PartOfSpeechId;
-    Pictures = sense.Pictures;
 }
@@
 public Guid? PartOfSpeechId { get; set; }
 public IList<SemanticDomain>? SemanticDomains { get; set; }
-public List<Picture>? Pictures { get; set; }

Also applies to: 36-36, 51-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs` at line 21, Remove the
Pictures property assignments from the CreateSenseChange class to align with its
materialization behavior. Specifically, delete the `Pictures = sense.Pictures;`
assignments (which appear at lines 21 and 36 in the provided context) since the
materialization at line 51 always creates an empty Pictures collection, and
picture creation is already handled separately by the CreateSensePictureChange
class. This eliminates the misleading payload and reduces unnecessary commit
bloat by maintaining a single source of truth for picture persistence.
backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs (1)

565-583: The path matching in NormalizePicturePatch is actually safe. The codebase does not apply a camelCase naming policy to JSON Patch operations. Patches use SystemTextJsonPatch with the default JsonSerializerOptions (no PropertyNamingPolicy configured), which preserves PascalCase property names. Picture patch paths will consistently be /MediaUri, not /mediaUri, so the exact-case string comparison correctly identifies all MediaUri updates.

That said, if you want to make the code more robust to future naming policy changes, you could still extract the path comparison to a helper method using StringComparison.OrdinalIgnoreCase. This is optional but recommended as defensive programming.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs`
around lines 565 - 583, To make the code more robust to future naming policy
changes in the NormalizePicturePatch method, extract the path comparison logic
into a helper method that uses case-insensitive string comparison. Create a
private helper method that checks if a given operation path matches the MediaUri
property name using StringComparison.OrdinalIgnoreCase, then replace all
occurrences of the exact-case string equality checks (op.Path ==
$"/{nameof(Picture.MediaUri)}") throughout NormalizePicturePatch with calls to
this new helper method to ensure consistent and resilient path matching.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs`:
- Around line 1792-1805: The ValidateOwnership method is performing both
sense-level and entry-level ownership checks, but sense-owned objects like
pictures should not be validated against the caller-supplied entryId since
senses can be legitimately re-parented to different entries. Remove the entry
ownership validation (the check for sense.Entry.Guid != entryId), remove the
entryId parameter from the ValidateOwnership method signature, and update all
call sites (at lines 1876, 1913, and 1928) to remove the entryId argument being
passed to ValidateOwnership, keeping only the picture and senseId parameters.
- Around line 1833-1837: The GetPicture method lacks a sense ownership
validation, allowing any picture with a valid ID to be returned regardless of
whether it belongs to the specified senseId. After retrieving the lcmPicture
from PictureRepository.TryGetObject, add a security check to verify that the
picture's owner is an ILexSense instance and that the owner's Guid matches the
provided senseId parameter before calling FromLcmPicture. If the ownership check
fails, return null instead of the mapped picture to prevent unauthorized access
to unrelated pictures.

In `@backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs`:
- Around line 15-19: In the ReorderSensePictureChange class, the FirstOrDefault
query that searches for the picture by PictureId only checks the picture ID but
does not verify that the picture has not been soft-deleted. Add a DeletedAt is
null check to the LINQ condition in the FirstOrDefault call to ensure that
soft-deleted pictures are skipped during the reordering operation, preventing
mutation of deleted entities.

In `@backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs`:
- Around line 26-31: In the ApplyChange method of UpdateSensePictureChange
class, after calling Patch.ApplyTo(picture), the entity.Pictures collection is
not re-sorted even though the patch can modify the Order property of the
picture. Add code after the Patch.ApplyTo(picture) call to re-sort the
entity.Pictures collection to preserve ordering invariants and maintain
consistent state.

In `@backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs`:
- Around line 862-870: The CreatePicture method calls AddChange with a
CreateSensePictureChange that can generate a new ID when the input picture.Id is
Guid.Empty, but then attempts to read back using the original picture.Id
parameter which may not match the actual created ID. After calling AddChange,
capture the ID that was actually generated and used by the change (either by
checking if the picture object was mutated with the new ID or by returning the
ID from AddChange), then use that generated ID in the GetPicture call instead of
the original picture.Id parameter.
- Around line 885-888: The UpdatePicture method needs to validate that the
jsonPatch from update.Patch does not contain any patch operations that modify
the Order field, as UpdateSensePictureChange does not re-sort sense.Pictures and
can break ordering invariants. Before creating the UpdateSensePictureChange with
jsonPatch, check if any patch operations target the Order field and reject the
request if found, directing the caller to use MovePicture instead for reordering
operations.
- Around line 862-914: Add ownership validation in the picture write methods
CreatePicture, both UpdatePicture overloads, MovePicture, and DeletePicture to
ensure the sense with the provided senseId actually belongs to the entry with
the provided entryId before calling AddChange(...). This can be accomplished by
fetching the sense from the repository and verifying that sense.EntryId matches
the entryId parameter, throwing NotFoundException if the sense does not exist or
does not belong to the specified entry. This validation should occur before any
write changes are applied to maintain route contract consistency and prevent
mismatched ID operations.

---

Nitpick comments:
In `@backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs`:
- Line 21: Remove the Pictures property assignments from the CreateSenseChange
class to align with its materialization behavior. Specifically, delete the
`Pictures = sense.Pictures;` assignments (which appear at lines 21 and 36 in the
provided context) since the materialization at line 51 always creates an empty
Pictures collection, and picture creation is already handled separately by the
CreateSensePictureChange class. This eliminates the misleading payload and
reduces unnecessary commit bloat by maintaining a single source of truth for
picture persistence.

In `@backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs`:
- Around line 565-583: To make the code more robust to future naming policy
changes in the NormalizePicturePatch method, extract the path comparison logic
into a helper method that uses case-insensitive string comparison. Create a
private helper method that checks if a given operation path matches the MediaUri
property name using StringComparison.OrdinalIgnoreCase, then replace all
occurrences of the exact-case string equality checks (op.Path ==
$"/{nameof(Picture.MediaUri)}") throughout NormalizePicturePatch with calls to
this new helper method to ensure consistent and resilient path matching.
🪄 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: ce8f808b-37b0-4370-8041-b7d0b44c9008

📥 Commits

Reviewing files that changed from the base of the PR and between fa14cfa and 0812b11.

📒 Files selected for processing (32)
  • backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs
  • backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
  • backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/RemoveSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.Designer.cs
  • backend/FwLite/LcmCrdt/Migrations/20260617073518_PicturesStoredAsJsonOnSenses.cs
  • backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
  • backend/FwLite/LcmCrdt/OrderPicker.cs
  • backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
  • backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
  • backend/FwLite/MiniLcm/Models/Picture.cs
  • backend/FwLite/MiniLcm/Models/Sense.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs
  • backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs
  • backend/LfClassicData/Entities/Entry.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IPicture.ts
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/ISense.ts
  • frontend/viewer/src/lib/sandbox/Sandbox.svelte
  • frontend/viewer/src/lib/utils.ts
  • frontend/viewer/src/project/demo/demo-entry-data.ts

Comment thread backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Comment thread backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Comment thread backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs Outdated
Comment thread backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs Outdated
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
Comment thread backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
rmunn added 2 commits June 19, 2026 13:46
The .HasDefaultValue is expecting a C# value, it's .HasDefaultValueSql
that takes a string in SQL syntax.
Pictures belong to a sense, but that sense might move to a different
owning entry. So the pictures should validate the sense that owns them,
but not the entry that owns the sense.
rmunn added 3 commits June 19, 2026 16:17
If a picture has already been deleted, its ReorderSensePictureChange
should become a no-op.
This ensures that if the picture was created via code that assigns an
empty GUID and waits to see what ID will come back from the database,
the new GUID (which is created in CreateSensePictureChange) will be
found and the correct Picture object will be returned.
@rmunn

rmunn commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@CodeRabbit fullreview

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

@rmunn Sure, I'll kick off a full review of the PR now!

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs`:
- Around line 1874-1883: The UpdatePictureProxy class used in the update.Apply()
call lacks persistence mapping for picture ordering, causing /Order operations
to silently succeed without reordering the sense picture sequence. Add explicit
field mapping for picture ordering in the UpdatePictureProxy class to properly
persist order changes to the LCM data structure, or implement a guard that fails
fast when order updates are attempted, ensuring that order changes are either
properly handled or explicitly rejected rather than silently ignored.
- Around line 1831-1835: In the GetPicture method, the ownership validation is
being performed on lcmPicture before checking if it is actually null. The
TryGetObject call on line 1833 may fail to find an object, leaving lcmPicture as
null, but ValidateOwnership is still called with this null value which can cause
a crash. Add a null check for lcmPicture immediately after the TryGetObject
call, and return null early if lcmPicture is null, before calling
ValidateOwnership.

In `@backend/FwLite/FwLiteProjectSync.Tests/DoubleDiffTests.cs`:
- Around line 10-51: Add comprehensive round-trip tests for picture sync
operations in FwLiteProjectSync.Tests to verify bidirectional consistency.
Create tests that validate picture operations (create, update, move, delete)
work correctly when syncing the Sense model's Pictures collection in both
directions, ensuring the sync infrastructure handles picture changes
consistently between local and remote states, similar to how the existing
DoubleDiff tests validate double value changes.

In `@backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs`:
- Around line 22-31: The MediaUri property in the CreateSensePictureChange class
is declared as non-nullable but has no initializer, violating the nullable
reference type contract and causing a CS8618 compilation error. To fix this, add
an initializer to the MediaUri property declaration (such as = default), change
it to be nullable by using MediaUri?, or mark it as required. Follow the pattern
used in UpdateSensePictureChange.cs where properties are properly initialized
with their declarations.

In `@backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs`:
- Around line 102-104: The MediaUriDbConverter class lacks defensive null and
empty checks in its deserialization logic, which could cause exceptions during
materialization. Modify the deserialization lambda in MediaUriDbConverter to
check if the input json parameter is null or empty before attempting
deserialization, and return MediaUri.NotFound as a safe fallback in those cases.
This approach aligns with the existing MediaUri convention and is consistent
with other converters in the codebase like RichStringDbConverter and
PictureListDbConverter that include similar safeguards.

In `@backend/FwLite/LcmCrdt/LcmCrdtKernel.cs`:
- Around line 226-229: The HasConversion configurations for both the Pictures
and SemanticDomains properties in LcmCrdtKernel.cs are missing null/empty JSON
payload protection that exists in the LinqToDB mapping at line 143. Add a
string.IsNullOrEmpty check in the deserialization lambda for both the Pictures
property conversion (around lines 226-229) and the SemanticDomains property
conversion (around lines 222-225) to handle empty or null JSON values gracefully
by returning a new empty list instead of attempting deserialization that would
fail on malformed data.

In `@backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs`:
- Line 19: The ProductVersion annotation in the LcmCrdtDbContextModelSnapshot.cs
file has been updated to 10.0.8, but this EF Core upgrade was already applied in
a prior commit and should not be included in the current PR. Revert the
ProductVersion value in the modelBuilder.HasAnnotation call back to its original
version so that this PR contains only picture-support-related changes and
excludes the unrelated EF Core version upgrade.

In `@backend/FwLite/MiniLcm/Models/Picture.cs`:
- Around line 11-12: The MediaUri property in the Picture class is declared as
non-nullable but is not initialized or marked as required, causing CS8618
compilation errors. Fix this by adding the `required` keyword to the MediaUri
property declaration to enforce initialization at construction time, making it
consistent with other mandatory properties like WritingSystem and
ComplexFormComponent in the codebase.

In `@backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs`:
- Around line 40-44: The code is currently diffing Picture.MediaUri by calling
ToString() directly on the URI objects without resolving them to their canonical
form first, as indicated by the TODO comment. To fix this, before calling
SimpleStringDiff.GetStringDiff with nameof(Picture.MediaUri), resolve
beforePicture.MediaUri and afterPicture.MediaUri to their canonical
representations (rather than using ToString() directly), then pass the canonical
string values to the GetStringDiff method to ensure consistent and correct patch
operations.

In `@backend/LfClassicData/LfClassicMiniLcmApi.cs`:
- Around line 354-362: The ToPicture method hardcodes MediaUri.NotFound
regardless of whether the picture has a persisted filename, causing it to ignore
the FileName property from the Entities.Picture object. Replace the hardcoded
MediaUri.NotFound assignment with logic that checks if picture.FileName exists
and constructs an appropriate MediaUri from it; only use MediaUri.NotFound as a
fallback when no filename is present.
🪄 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: 7a6c0e6e-bd9e-48f2-b739-94f24198b77f

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6b20e and 0891da0.

📒 Files selected for processing (34)
  • backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdatePictureProxy.cs
  • backend/FwLite/FwDataMiniLcmBridge/Api/UpdateProxy/UpdateSenseProxy.cs
  • backend/FwLite/FwLiteProjectSync.Tests/DoubleDiffTests.cs
  • backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
  • backend/FwLite/FwLiteShared/TypeGen/ReinforcedFwLiteTypingConfig.cs
  • backend/FwLite/LcmCrdt.Tests/Changes/UseChangesTests.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSenseChange.cs
  • backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/RemoveSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/ReorderSensePictureChange.cs
  • backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs
  • backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
  • backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs
  • backend/FwLite/LcmCrdt/LcmCrdtKernel.cs
  • backend/FwLite/LcmCrdt/Migrations/20260619022528_AddPicturesColumnToSenses.Designer.cs
  • backend/FwLite/LcmCrdt/Migrations/20260619022528_AddPicturesColumnToSenses.cs
  • backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
  • backend/FwLite/LcmCrdt/OrderPicker.cs
  • backend/FwLite/MiniLcm/IMiniLcmReadApi.cs
  • backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs
  • backend/FwLite/MiniLcm/Models/Picture.cs
  • backend/FwLite/MiniLcm/Models/Sense.cs
  • backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs
  • backend/FwLite/MiniLcm/SyncHelpers/DoubleDiff.cs
  • backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs
  • backend/FwLite/MiniLcm/SyncHelpers/SenseSync.cs
  • backend/LfClassicData/Entities/Entry.cs
  • backend/LfClassicData/LfClassicMiniLcmApi.cs
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/IPicture.ts
  • frontend/viewer/src/lib/dotnet-types/generated-types/MiniLcm/Models/ISense.ts
  • frontend/viewer/src/lib/sandbox/Sandbox.svelte
  • frontend/viewer/src/lib/utils.ts
  • frontend/viewer/src/project/demo/demo-entry-data.ts

Comment on lines +1831 to +1835
public Task<Picture?> GetPicture(Guid entryId, Guid senseId, Guid id)
{
PictureRepository.TryGetObject(id, out var lcmPicture);
ValidateOwnership(lcmPicture, entryId, senseId);
return Task.FromResult(lcmPicture is null ? null : FromLcmPicture(senseId, lcmPicture));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Null-check lcmPicture before ownership validation in GetPicture.

Line 1834 validates ownership before checking whether TryGetObject actually found a picture, so unknown IDs can crash instead of returning null.

Suggested fix
 public Task<Picture?> GetPicture(Guid entryId, Guid senseId, Guid id)
 {
-    PictureRepository.TryGetObject(id, out var lcmPicture);
-    ValidateOwnership(lcmPicture, entryId, senseId);
-    return Task.FromResult(lcmPicture is null ? null : FromLcmPicture(senseId, lcmPicture));
+    PictureRepository.TryGetObject(id, out var lcmPicture);
+    if (lcmPicture is null) return Task.FromResult<Picture?>(null);
+    ValidateOwnership(lcmPicture, entryId, senseId);
+    return Task.FromResult<Picture?>(FromLcmPicture(senseId, lcmPicture));
 }
📝 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
public Task<Picture?> GetPicture(Guid entryId, Guid senseId, Guid id)
{
PictureRepository.TryGetObject(id, out var lcmPicture);
ValidateOwnership(lcmPicture, entryId, senseId);
return Task.FromResult(lcmPicture is null ? null : FromLcmPicture(senseId, lcmPicture));
public Task<Picture?> GetPicture(Guid entryId, Guid senseId, Guid id)
{
PictureRepository.TryGetObject(id, out var lcmPicture);
if (lcmPicture is null) return Task.FromResult<Picture?>(null);
ValidateOwnership(lcmPicture, senseId);
return Task.FromResult<Picture?>(FromLcmPicture(senseId, lcmPicture));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs` around lines 1831
- 1835, In the GetPicture method, the ownership validation is being performed on
lcmPicture before checking if it is actually null. The TryGetObject call on line
1833 may fail to find an object, leaving lcmPicture as null, but
ValidateOwnership is still called with this null value which can cause a crash.
Add a null check for lcmPicture immediately after the TryGetObject call, and
return null early if lcmPicture is null, before calling ValidateOwnership.

Comment on lines +1874 to +1883
var lcmPicture = PictureRepository.GetObject(pictureId);
ValidateOwnership(lcmPicture, entryId, senseId);
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update picture",
"Revert picture",
Cache.ServiceLocator.ActionHandler,
() =>
{
var updateProxy = new UpdatePictureProxy(lcmPicture, this);
update.Apply(updateProxy);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

UpdatePicture patch flow can silently ignore /Order updates.

update.Apply(new UpdatePictureProxy(...)) has no persistence mapping for picture ordering, so /Order ops may report success without reordering the sense picture sequence.

Suggested guard (fail fast until order is explicitly mapped)
 public Task<Picture> UpdatePicture(Guid entryId,
     Guid senseId,
     Guid pictureId,
     UpdateObjectInput<Picture> update)
 {
     var lcmPicture = PictureRepository.GetObject(pictureId);
     ValidateOwnership(lcmPicture, entryId, senseId);
+    if (update.Patch.Operations.Any(op => op.Path == $"/{nameof(Picture.Order)}"))
+        throw new NotSupportedException("Use MovePicture to change picture order.");
     UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update picture",
         "Revert picture",
         Cache.ServiceLocator.ActionHandler,
         () =>
         {
             var updateProxy = new UpdatePictureProxy(lcmPicture, this);
             update.Apply(updateProxy);
         });

As per coding guidelines: “Map new fields to/from LCM (FieldWorks) data structures in FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs` around lines 1874
- 1883, The UpdatePictureProxy class used in the update.Apply() call lacks
persistence mapping for picture ordering, causing /Order operations to silently
succeed without reordering the sense picture sequence. Add explicit field
mapping for picture ordering in the UpdatePictureProxy class to properly persist
order changes to the LCM data structure, or implement a guard that fails fast
when order updates are attempted, ensuring that order changes are either
properly handled or explicitly rejected rather than silently ignored.

Source: Coding guidelines

Comment on lines +10 to +51
[Fact]
public void DiffEmptyDoublesDoNothing()
{
double? before = null;
double? after = null;
var result = DoubleDiff.GetDoubleDiff<Placeholder>("test", before, after);
result.Should().BeEmpty();
}

[Fact]
public void DiffOneToEmptyAddsOne()
{
double? before = null;
var after = 1.5;
var result = DoubleDiff.GetDoubleDiff<Placeholder>("test", before, after);
result.Should().BeEquivalentTo([
new Operation<Placeholder>("add", "/test", null, 1.5)
]);
}

[Fact]
public void DiffOneToTwoReplacesOne()
{
var before = 1.5;
var after = 2.5;
var result = DoubleDiff.GetDoubleDiff<Placeholder>("test", before, after);
result.Should().BeEquivalentTo([
new Operation<Placeholder>("replace", "/test", null, 2.5)
]);
}

[Fact]
public void DiffNoneToOneRemovesOne()
{
var before = 1.5;
double? after = null;
var result = DoubleDiff.GetDoubleDiff<Placeholder>("test", before, after);
result.Should().BeEquivalentTo([
new Operation<Placeholder>("remove", "/test", null)
]);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate candidate sync tests for picture-related flows in FwLiteProjectSync.Tests
fd -t f --glob '*Tests.cs' backend/FwLite/FwLiteProjectSync.Tests

# Inspect references to picture/sense sync operations in that test project
rg -n --type cs -C3 '\b(PictureSync|SenseSync\.Sync|CreatePicture|UpdatePicture|MovePicture|DeletePicture)\b' backend/FwLite/FwLiteProjectSync.Tests

Repository: sillsdev/languageforge-lexbox

Length of output: 1053


🏁 Script executed:

# Search for "picture" more broadly (case-insensitive) in test files
rg -i -n 'picture' backend/FwLite/FwLiteProjectSync.Tests --type cs

# Check what SenseSync and related changes actually involve
rg -i -n 'sensesync|picture' backend/FwLite/FwLiteProjectSync --type cs | head -50

# Check if there are any test files specifically for picture or sense sync
fd -i picture backend/FwLite/FwLiteProjectSync.Tests
fd -i sense backend/FwLite/FwLiteProjectSync.Tests

Repository: sillsdev/languageforge-lexbox

Length of output: 2471


🏁 Script executed:

# Check what tests actually exist for picture sync or sense sync in the test project
rg -i 'picture|sense' backend/FwLite/FwLiteProjectSync.Tests --type cs

# Look at EntrySyncTests.cs to understand the scope of what's being tested
head -100 backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs

# Check SyncTests.cs for picture/sense coverage
rg -i 'picture|sense' backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs -A 2 -B 2

Repository: sillsdev/languageforge-lexbox

Length of output: 21000


🏁 Script executed:

# Check if Picture is a property of Sense or ExampleSentence
rg -n 'Pictures|Picture' backend/FwLite/FwLiteProjectSync.Tests --type cs -A 2

# Check the Sense model to understand its structure
fd -t f 'Sense.cs' backend --exec head -50 {} \;

# Check ExampleSentence model structure
fd -t f 'ExampleSentence.cs' backend --exec head -50 {} \;

# Search for any test files mentioning or testing pictures anywhere
find backend/FwLite/FwLiteProjectSync.Tests -name '*.cs' -exec grep -l -i 'picture' {} \;

Repository: sillsdev/languageforge-lexbox

Length of output: 3247


Add picture sync round-trip tests to FwLiteProjectSync.Tests.

Picture operations (create, update, move, delete) are implemented in the sync infrastructure and the Sense model includes a Pictures collection, but there are no sync round-trip tests for picture operations in FwLiteProjectSync.Tests. Per coding guidelines, add targeted picture sync tests to verify bidirectional consistency between sync implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/FwLiteProjectSync.Tests/DoubleDiffTests.cs` around lines 10 -
51, Add comprehensive round-trip tests for picture sync operations in
FwLiteProjectSync.Tests to verify bidirectional consistency. Create tests that
validate picture operations (create, update, move, delete) work correctly when
syncing the Sense model's Pictures collection in both directions, ensuring the
sync infrastructure handles picture changes consistently between local and
remote states, similar to how the existing DoubleDiff tests validate double
value changes.

Source: Coding guidelines

Comment on lines +22 to +31
[JsonConstructor]
private CreateSensePictureChange(Guid entityId) : base(entityId)
{
}

public Guid PictureId { get; set; }
public double Order { get; set; }
public RichMultiString? Caption { get; set; }
public MediaUri MediaUri { get; set; }
public BetweenPosition? Between { get; set; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find backend -name "CreateSensePictureChange.cs" -type f

Repository: sillsdev/languageforge-lexbox

Length of output: 132


🏁 Script executed:

fd -t f "CreateSensePictureChange.cs" backend/

Repository: sillsdev/languageforge-lexbox

Length of output: 132


🏁 Script executed:

cat -n backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1941


🏁 Script executed:

# Check for .csproj file in the directory to see nullable settings
find backend/FwLite/LcmCrdt -name "*.csproj" | head -5

Repository: sillsdev/languageforge-lexbox

Length of output: 111


🏁 Script executed:

# Check if there's a global Directory.Build.props that sets nullable
find backend -name "Directory.Build.props" -o -name "*.csproj" | grep -E "(Directory\.Build|FwLite.*csproj)" | head -5

Repository: sillsdev/languageforge-lexbox

Length of output: 339


🏁 Script executed:

cat -n backend/Directory.Build.props

Repository: sillsdev/languageforge-lexbox

Length of output: 2901


🏁 Script executed:

cat -n backend/FwLite/LcmCrdt/LcmCrdt.csproj

Repository: sillsdev/languageforge-lexbox

Length of output: 2011


🏁 Script executed:

# Let's also look at other change classes to see how they handle the JSON constructor pattern
ls -la backend/FwLite/LcmCrdt/Changes/ | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 1441


🏁 Script executed:

# Check a few other change classes to understand the pattern
rg "JsonConstructor" backend/FwLite/LcmCrdt/Changes/ -A 3 -B 1 | head -60

Repository: sillsdev/languageforge-lexbox

Length of output: 4366


🏁 Script executed:

# Check UpdateSensePictureChange.cs to see if it has the same issue
cat -n backend/FwLite/LcmCrdt/Changes/UpdateSensePictureChange.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1655


🏁 Script executed:

# Search for null-forgiving operators or pragmas in CreateSensePictureChange
rg "MediaUri" backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs -B 2 -A 2

Repository: sillsdev/languageforge-lexbox

Length of output: 550


🏁 Script executed:

# Check if MediaUri type has a default value or special handling
rg "class MediaUri|struct MediaUri" backend/

Repository: sillsdev/languageforge-lexbox

Length of output: 388


🏁 Script executed:

cat -n backend/FwLite/MiniLcm/Media/MediaUri.cs | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 1498


MediaUri property violates nullable reference type contract with no initializer.

The private JSON constructor does not initialize MediaUri, and the property has no initializer despite being non-nullable. With <WarningsAsErrors>Nullable</WarningsAsErrors> enabled, this should cause a CS8618 compilation error. Either initialize the property in its declaration (e.g., = default), make it nullable (MediaUri?), or mark it as required. See UpdateSensePictureChange.cs line 24 for the correct pattern: public JsonPatchDocument<Picture> Patch { get; init; } = new();

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Changes/CreateSensePictureChange.cs` around lines 22 -
31, The MediaUri property in the CreateSensePictureChange class is declared as
non-nullable but has no initializer, violating the nullable reference type
contract and causing a CS8618 compilation error. To fix this, add an initializer
to the MediaUri property declaration (such as = default), change it to be
nullable by using MediaUri?, or mark it as required. Follow the pattern used in
UpdateSensePictureChange.cs where properties are properly initialized with their
declarations.

Source: Coding guidelines

Comment on lines +102 to +104
private class MediaUriDbConverter() : ValueConverter<MiniLcm.Media.MediaUri, string>(
mul => JsonSerializer.Serialize(mul, (JsonSerializerOptions?)null),
json => JsonSerializer.Deserialize<MiniLcm.Media.MediaUri>(json, (JsonSerializerOptions?)null));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the LcmCrdtDbContext.cs file around the specified lines
cat -n backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | sed -n '95,115p'

Repository: sillsdev/languageforge-lexbox

Length of output: 1057


🏁 Script executed:

# Let's also check the full context of the MediaUriDbConverter and surrounding code
cat -n backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | sed -n '95,130p'

Repository: sillsdev/languageforge-lexbox

Length of output: 1057


🏁 Script executed:

# Check how MediaUri is defined in the MiniLcm.Media namespace
find . -type f -name "*.cs" | xargs rg -l "class MediaUri|record MediaUri" | head -5

Repository: sillsdev/languageforge-lexbox

Length of output: 5122


🏁 Script executed:

# Look for usage of MediaUri in the codebase
rg "MediaUri" --type cs -A 2 -B 2 | head -50

Repository: sillsdev/languageforge-lexbox

Length of output: 4955


🏁 Script executed:

# Check if there are other ValueConverters in the file for comparison
rg "ValueConverter" backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs -B 2 -A 3

Repository: sillsdev/languageforge-lexbox

Length of output: 1561


🏁 Script executed:

# Check if MediaUri is used in model configurations and whether it's nullable
rg "Media\.MediaUri" backend/FwLite --type cs -B 2 -A 2 | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 1245


🏁 Script executed:

# Examine the MediaUri definition
cat -n backend/FwLite/MiniLcm/Media/MediaUri.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 2185


🏁 Script executed:

# Check if MediaUri is nullable in the Picture model or used elsewhere
rg "MediaUri" backend/FwLite/LcmCrdt --type cs -B 1 -A 1 | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 2502


🏁 Script executed:

# Check the Picture model definition to see if MediaUri is nullable
cat -n backend/FwLite/MiniLcm/Models/Picture.cs | head -50

Repository: sillsdev/languageforge-lexbox

Length of output: 1285


🏁 Script executed:

# Check how RichStringDbConverter handles null - compare with MediaUriDbConverter
cat -n backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | sed -n '80,100p'

Repository: sillsdev/languageforge-lexbox

Length of output: 1031


🏁 Script executed:

# Check if there are any tests or error handling patterns for MediaUri deserialization
rg "MediaUri.*Deserialize|MediaUri.*try|MediaUri.*catch" --type cs -B 2 -A 2

Repository: sillsdev/languageforge-lexbox

Length of output: 577


🏁 Script executed:

# Look for any existing error cases or known issues with MediaUri
rg "MediaUri\.NotFound" --type cs -B 2 -A 2 | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 3280


🏁 Script executed:

# Check if JsonSerializer options are explicitly configured anywhere for MediaUri
rg "JsonSerializerOptions|JsonConverter" backend/FwLite/LcmCrdt --type cs -B 1 -A 1 | head -50

Repository: sillsdev/languageforge-lexbox

Length of output: 4947


🏁 Script executed:

# Verify whether nullable MediaUri ever appears in the codebase (e.g., MediaUri?)
rg "MediaUri\?" --type cs | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 163


🏁 Script executed:

# Check if JsonSerializer.Deserialize with null options respects [JsonConverter] attribute
python3 << 'PY'
# According to .NET documentation, when options is null, 
# JsonSerializer uses the default settings
# Default settings DO respect [JsonConverter] attributes on the type

# Let's verify this by checking if there are any JSON deserialization failures in tests
import subprocess
result = subprocess.run(
    ["rg", "JsonException|InvalidOperationException", "backend/FwLite", "--type", "cs", "-l"],
    capture_output=True,
    text=True
)
print("Files with JsonException handling:")
print(result.stdout)
PY

Repository: sillsdev/languageforge-lexbox

Length of output: 3680


🏁 Script executed:

# Look at how MediaUri is actually used - does it ever need to be null/NotFound?
rg "Picture.*MediaUri|MediaUri.*NotFound" --type cs -B 1 -A 1 | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 3744


🏁 Script executed:

# Check database constraints - is MediaUri column nullable in the schema?
find . -name "*Migration*" -type f -name "*.cs" | xargs rg -l "MediaUri" | head -3

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Check if there's a pattern of null-checking before deserialization in similar contexts
rg "string.IsNullOrWhiteSpace.*Deserialize|string.IsNullOrEmpty.*Deserialize" --type cs -B 1 -A 1

Repository: sillsdev/languageforge-lexbox

Length of output: 881


🏁 Script executed:

# Verify what happens when JsonSerializer.Deserialize is called with null/empty string on a type with [JsonConverter]
cat -n backend/FwLite/MiniLcm/Media/MediaUri.cs | sed -n '45,58p'

Repository: sillsdev/languageforge-lexbox

Length of output: 648


🏁 Script executed:

# Check if there are any actual database issues or test failures related to MediaUri
rg "MediaUri.*null|MediaUri.*empty" backend/FwLite --type cs -B 2 -A 2 | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 644


🏁 Script executed:

# Check what happens when Picture is loaded from database with bad MediaUri data
rg "Picture\s" backend/FwLite/LcmCrdt --type cs -A 3 -B 1 | grep -A 3 "MediaUri" | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Check if MediaUri in Picture could ever be stored as null in the database
rg "HasColumnName|HasColumnType" backend/FwLite/LcmCrdt --type cs -B 3 -A 3 | grep -A 5 -B 5 "MediaUri"

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Check EF Core ValueConverter behavior - does it properly use [JsonConverter] attributes?
# Let's look at how RichStringDbConverter handles this differently
cat -n backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | sed -n '75,92p'

Repository: sillsdev/languageforge-lexbox

Length of output: 872


🏁 Script executed:

# Verify if there's documentation or comments about why MediaUriDbConverter doesn't have null-checks
cat -n backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs | sed -n '98,109p'

Repository: sillsdev/languageforge-lexbox

Length of output: 852


🏁 Script executed:

# Check if there are any actual issues filed or test cases that would break with null/empty MediaUri
rg "test.*MediaUri|Test.*MediaUri" --type cs -i -l

Repository: sillsdev/languageforge-lexbox

Length of output: 132


Add defensive null/empty checks to MediaUriDbConverter.

The converter currently deserializes without guarding against null or empty payloads, inconsistent with similar converters in the codebase (e.g., RichStringDbConverter includes try-catch; PictureListDbConverter uses ?? new()). While MediaUri's [JsonConverter] attribute handles null gracefully by returning MediaUri.NotFound, the ValueConverter may not properly delegate when the input is null/empty, potentially throwing during materialization.

Consider returning MediaUri.NotFound as a safe fallback rather than throwing, which aligns with the existing MediaUri convention:

 private class MediaUriDbConverter() : ValueConverter<MiniLcm.Media.MediaUri, string>(
     mul => JsonSerializer.Serialize(mul, (JsonSerializerOptions?)null),
-    json => JsonSerializer.Deserialize<MiniLcm.Media.MediaUri>(json, (JsonSerializerOptions?)null));
+    json => string.IsNullOrWhiteSpace(json)
+        ? MediaUri.NotFound
+        : JsonSerializer.Deserialize<MiniLcm.Media.MediaUri>(json, (JsonSerializerOptions?)null)
+          ?? MediaUri.NotFound);
📝 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
private class MediaUriDbConverter() : ValueConverter<MiniLcm.Media.MediaUri, string>(
mul => JsonSerializer.Serialize(mul, (JsonSerializerOptions?)null),
json => JsonSerializer.Deserialize<MiniLcm.Media.MediaUri>(json, (JsonSerializerOptions?)null));
private class MediaUriDbConverter() : ValueConverter<MiniLcm.Media.MediaUri, string>(
mul => JsonSerializer.Serialize(mul, (JsonSerializerOptions?)null),
json => string.IsNullOrWhiteSpace(json)
? MediaUri.NotFound
: JsonSerializer.Deserialize<MiniLcm.Media.MediaUri>(json, (JsonSerializerOptions?)null)
?? MediaUri.NotFound);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/LcmCrdtDbContext.cs` around lines 102 - 104, The
MediaUriDbConverter class lacks defensive null and empty checks in its
deserialization logic, which could cause exceptions during materialization.
Modify the deserialization lambda in MediaUriDbConverter to check if the input
json parameter is null or empty before attempting deserialization, and return
MediaUri.NotFound as a safe fallback in those cases. This approach aligns with
the existing MediaUri convention and is consistent with other converters in the
codebase like RichStringDbConverter and PictureListDbConverter that include
similar safeguards.

Comment on lines +226 to +229
builder.Property(s => s.Pictures)
.HasColumnType("jsonb")
.HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => JsonSerializer.Deserialize<List<Picture>>(json, (JsonSerializerOptions?)null) ?? new());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let me find and read the LcmCrdtKernel.cs file
fd "LcmCrdtKernel.cs" --type f

Repository: sillsdev/languageforge-lexbox

Length of output: 113


🏁 Script executed:

# Read the relevant sections of the file
cat -n backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | sed -n '135,155p'

Repository: sillsdev/languageforge-lexbox

Length of output: 1867


🏁 Script executed:

# Read lines around 226-229
cat -n backend/FwLite/LcmCrdt/LcmCrdtKernel.cs | sed -n '220,235p'

Repository: sillsdev/languageforge-lexbox

Length of output: 1181


Make Sense.Pictures and Sense.SemanticDomains conversions tolerant to empty payloads (parity with line 143).

Lines 143-145 include a string.IsNullOrEmpty(json) check before deserializing List<Picture> in the LinqToDB mapping schema, but the Entity Framework conversions at lines 226-229 (Pictures) and 222-225 (SemanticDomains) lack this protection. This inconsistency can cause deserialization exceptions at read time when encountering null or empty JSON values from legacy or malformed data.

Suggested fix
 builder.Property(s => s.Pictures)
     .HasColumnType("jsonb")
     .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
-        json => JsonSerializer.Deserialize<List<Picture>>(json, (JsonSerializerOptions?)null) ?? new());
+        json => string.IsNullOrWhiteSpace(json)
+            ? new()
+            : JsonSerializer.Deserialize<List<Picture>>(json, (JsonSerializerOptions?)null) ?? new());
 builder.Property(s => s.SemanticDomains)
     .HasColumnType("jsonb")
     .HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
-        json => JsonSerializer.Deserialize<List<SemanticDomain>>(json, (JsonSerializerOptions?)null) ?? new());
+        json => string.IsNullOrWhiteSpace(json)
+            ? new()
+            : JsonSerializer.Deserialize<List<SemanticDomain>>(json, (JsonSerializerOptions?)null) ?? new());
📝 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
builder.Property(s => s.Pictures)
.HasColumnType("jsonb")
.HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => JsonSerializer.Deserialize<List<Picture>>(json, (JsonSerializerOptions?)null) ?? new());
builder.Property(s => s.Pictures)
.HasColumnType("jsonb")
.HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => string.IsNullOrWhiteSpace(json)
? new()
: JsonSerializer.Deserialize<List<Picture>>(json, (JsonSerializerOptions?)null) ?? new());
builder.Property(s => s.SemanticDomains)
.HasColumnType("jsonb")
.HasConversion(list => JsonSerializer.Serialize(list, (JsonSerializerOptions?)null),
json => string.IsNullOrWhiteSpace(json)
? new()
: JsonSerializer.Deserialize<List<SemanticDomain>>(json, (JsonSerializerOptions?)null) ?? new());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/LcmCrdtKernel.cs` around lines 226 - 229, The
HasConversion configurations for both the Pictures and SemanticDomains
properties in LcmCrdtKernel.cs are missing null/empty JSON payload protection
that exists in the LinqToDB mapping at line 143. Add a string.IsNullOrEmpty
check in the deserialization lambda for both the Pictures property conversion
(around lines 226-229) and the SemanticDomains property conversion (around lines
222-225) to handle empty or null JSON values gracefully by returning a new empty
list instead of attempting deserialization that would fail on malformed data.

{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.16");
modelBuilder.HasAnnotation("ProductVersion", "10.0.8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check EF Core package versions in all csproj files
fd -e csproj -x grep -H "Microsoft.EntityFrameworkCore" {} \; | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 705


🏁 Script executed:

#!/bin/bash
# Check the actual ProductVersion in the snapshot file
echo "=== ProductVersion in ModelSnapshot ==="
sed -n '19p' backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs

# Check the csproj files for explicit version numbers
echo "=== LcmCrdt.csproj EntityFrameworkCore packages ==="
grep -A 1 "Microsoft.EntityFrameworkCore" backend/FwLite/LcmCrdt/LcmCrdt.csproj | grep -E "Version|Include"

# Also check the Directory.Build.props for centralized versioning
echo "=== Directory.Build.props (if exists) ==="
if [ -f "Directory.Build.props" ]; then
  grep -i "entityframework" Directory.Build.props
fi

# Check for any global package references
echo "=== backend/Directory.Build.props ==="
if [ -f "backend/Directory.Build.props" ]; then
  grep -i "entityframework" backend/Directory.Build.props
fi

Repository: sillsdev/languageforge-lexbox

Length of output: 532


🏁 Script executed:

#!/bin/bash
# Check ComplexProperty signature at line 554
echo "=== ComplexProperty signature at line 554 ==="
sed -n '550,560p' backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 655


🌐 Web query:

EF Core 10 ComplexProperty API signature changes from EF Core 9

💡 Result:

EF Core 10: the ComplexProperty “API signature changes from EF Core 9” that matter are not about the delegate/lambda overloads changing; rather, EF Core 10 introduces breaking behavior in complex-type column/property naming and some related convention signatures. 1) Complex property mapping behavior changed (breaking) - EF Core 10 uniquifies complex type column names by appending a number when the same column name would otherwise collide on the table. [1] - EF Core 10 also uses the full path for nested complex type property column names (e.g., EntityType.Complex.NestedComplex.Property maps to Complex_NestedComplex_Property). [1] Impact: code that relied on prior column naming (e.g., raw SQL, manual projections, migrations/hand-written column names, or expectations about generated column names) may need adjustment. 2) EF Core 10 complex-type column naming improvements are documented as breaking changes The EF Core 10 breaking changes list explicitly calls out: - “Complex type column names are now uniquified” [1] - “Nested complex type properties use full path in column names” [1] 3) ComplexProperty builder signatures: generally still available via EntityTypeBuilder.ComplexProperty and nested configuration builders - EF Core 10 docs show the primary ComplexProperty configuration API on EntityTypeBuilder (for mapping complex properties, optionally to JSON via ToJson). [2] - EF Core 10 docs also show configuration APIs like ComplexPropertyBuilder.Property(...) for configuring properties inside a complex type. [3] These indicate the modeling entry points still exist, but the important migration concerns are the schema/column naming changes above. What to do when upgrading EF Core 9 -> 10 - Re-check any code/migrations that depend on specific generated column names for complex and nested complex types; EF Core 10 may now generate different names because of uniquification and full-path naming. [1] - Validate custom conventions if you wrote infrastructure-level conventions related to discriminator property sets (a separate breaking change in EF Core 10 is that IDiscriminatorPropertySetConvention’s signature changed). This is not a ComplexProperty API itself, but it is another common “signature change” encountered during upgrades. [1] Direct answer to the phrasing in your question - There isn’t a single obvious “ComplexProperty method signature changed” item called out in the EF Core 10 breaking changes docs; instead, EF Core 10’s breaking change for complex types is the generated column naming behavior. [1] If you share the exact EF Core 9 ComplexProperty overload/signature you’re using (e.g., expression vs string-based, nested types, ToJson vs non-JSON, or EF Core 9-specific custom builder/convention code), I can tell you precisely what (if anything) you need to change and map it to the EF Core 10 docs.

Citations:


🏁 Script executed:

#!/bin/bash
# Check git history for previous ProductVersion values in this file
echo "=== Git log for ProductVersion changes ==="
git log --oneline -n 20 -- backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs

echo "=== Check if there's a previous snapshot or revision ==="
git show HEAD~1:backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs 2>/dev/null | grep -A 2 "ProductVersion" | head -5

Repository: sillsdev/languageforge-lexbox

Length of output: 1481


🏁 Script executed:

#!/bin/bash
# Check if EntityFrameworkCore packages have explicit versions in csproj
echo "=== Full EntityFrameworkCore section from LcmCrdt.csproj ==="
sed -n '/<ItemGroup>/,/<\/ItemGroup>/p' backend/FwLite/LcmCrdt/LcmCrdt.csproj | grep -A 1 -B 1 "EntityFrameworkCore"

# Check for version management files
echo "=== Check global.json for SDK version ==="
if [ -f "global.json" ]; then
  cat global.json
fi

Repository: sillsdev/languageforge-lexbox

Length of output: 709


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about the EF Core upgrade in recent commits
echo "=== Recent commits mentioning EF or EntityFramework ==="
git log --all --oneline --grep -i "ef\|entityframework" -n 10 2>/dev/null || echo "No matching commits found"

echo "=== Check for PR description or branch info ==="
git log --format="%B" -n 1 | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 252


🏁 Script executed:

#!/bin/bash
# Check the ".NET 10 packages bump" commit to understand the upgrade
echo "=== Details of 'Bump pretty much ALL packages for .NET 10' commit ==="
git show --stat b1ab8210 | head -50

echo "=== Check if EF Core version was explicitly updated in that commit ==="
git show b1ab8210 -- "*csproj" | grep -i "entityframework" | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 3135


🏁 Script executed:

#!/bin/bash
# Check the PR `#2264` for context about the upgrade
echo "=== Check for any mention of EF Core 10 in commits around package bumping ==="
git log --format="%H %s" --all | grep -i "bump\|package" | head -10

# Get more details on the actual version bumps
echo "=== Check Directory.Build.props or paket files for version management ==="
fd -e "paket\|props" -x grep -l "EntityFrameworkCore\|dotnet-version" {} \;

Repository: sillsdev/languageforge-lexbox

Length of output: 1098


Remove EF Core version from this PR—it was already upgraded in a prior commit.

The ProductVersion change to 10.0.8 occurred in commit #2264 ("Bump pretty much ALL packages for .NET 10"), which is a separate upgrade from the current PR. This snapshot file should not have been modified by the current changes. The ComplexProperty call at line 554 is not an API signature change—it's standard EF Core 10 migration snapshot syntax. Verify that only picture-support-related modifications are included in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs` at line
19, The ProductVersion annotation in the LcmCrdtDbContextModelSnapshot.cs file
has been updated to 10.0.8, but this EF Core upgrade was already applied in a
prior commit and should not be included in the current PR. Revert the
ProductVersion value in the modelBuilder.HasAnnotation call back to its original
version so that this PR contains only picture-support-related changes and
excludes the unrelated EF Core version upgrade.

Comment on lines +11 to +12
public virtual MediaUri MediaUri { get; set; }
public virtual RichMultiString Caption { get; set; } = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
proj="$(fd -i 'LcmCrdt.csproj' backend | head -n1)"
dotnet build "$proj" -warnaserror

Repository: sillsdev/languageforge-lexbox

Length of output: 120


🏁 Script executed:

cd backend/FwLite/MiniLcm/Models && cat -n Picture.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1285


🏁 Script executed:

# Check the project file for nullable reference type settings
cat -n backend/FwLite/MiniLcm/MiniLcm.csproj | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 1023


🏁 Script executed:

# Look at other MiniLcm model files to see how they handle non-nullable properties
fd -t f '\.cs$' backend/FwLite/MiniLcm/Models/ -exec basename {} \; | head -10

Repository: sillsdev/languageforge-lexbox

Length of output: 225


🏁 Script executed:

# Correct fd command to list model files
fd -t f '\.cs$' backend/FwLite/MiniLcm/Models/

Repository: sillsdev/languageforge-lexbox

Length of output: 1045


🏁 Script executed:

# Check other model files to see initialization patterns
for file in $(fd -t f '\.cs$' backend/FwLite/MiniLcm/Models/ | head -5); do
  echo "=== $(basename $file) ==="
  grep -n "public virtual.*{ get; set; }" "$file" | head -5
done

Repository: sillsdev/languageforge-lexbox

Length of output: 861


🏁 Script executed:

# Check for nullable directive in the MiniLcm project
grep -r "nullable" backend/FwLite/MiniLcm/ --include="*.cs" --include="*.csproj" | head -10

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Look at the MediaUri class definition
fd -t f '\.cs$' backend/FwLite/MiniLcm/ -exec grep -l "class MediaUri\|record MediaUri" {} \;

Repository: sillsdev/languageforge-lexbox

Length of output: 299


🏁 Script executed:

# Check the parent directory for Directory.Build.props or similar global settings
cat -n backend/Directory.Build.props 2>/dev/null || echo "File not found"

Repository: sillsdev/languageforge-lexbox

Length of output: 2901


🏁 Script executed:

# Find MediaUri definition
rg "class MediaUri|record MediaUri|struct MediaUri" backend/FwLite/MiniLcm/

Repository: sillsdev/languageforge-lexbox

Length of output: 255


🏁 Script executed:

# Check how Picture is instantiated/initialized in the codebase
rg "new Picture" backend/FwLite/ -B 2 -A 3 | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 2384


🏁 Script executed:

# Check Sense.cs to see another model pattern
cat -n backend/FwLite/MiniLcm/Models/Sense.cs | head -40

Repository: sillsdev/languageforge-lexbox

Length of output: 1914


🏁 Script executed:

# Check if Picture has any constructor that initializes MediaUri
rg "Picture()" backend/FwLite/ -B 3 -A 5

Repository: sillsdev/languageforge-lexbox

Length of output: 50385


🏁 Script executed:

# Check if MediaUri has a default/empty constructor
cat -n backend/FwLite/MiniLcm/Media/MediaUri.cs | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 1230


🏁 Script executed:

# Check if Picture has any constructor that might initialize MediaUri
cat -n backend/FwLite/MiniLcm/Models/Picture.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 1285


🏁 Script executed:

# Check how other similar value types are initialized in the codebase
rg "public virtual.*{ get; set; } = " backend/FwLite/MiniLcm/Models/ --type cs | head -15

Repository: sillsdev/languageforge-lexbox

Length of output: 206


🏁 Script executed:

# Check how Picture instances are created in tests
rg "new Picture\s*{" backend/FwLite/LcmCrdt.Tests/ -A 5 | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 167


🏁 Script executed:

# Check how Picture instances are created in implementation
rg "new Picture\s*{" backend/FwLite/LcmCrdt/ -A 5 | head -30

Repository: sillsdev/languageforge-lexbox

Length of output: 167


🏁 Script executed:

# Check if there are any nullable suppression directives used for MediaUri
rg "MediaUri\?" backend/FwLite/MiniLcm/Models/Picture.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Look at how other non-nullable properties without initializers are handled
rg "public virtual.*\{ get; set; \}" backend/FwLite/MiniLcm/Models/Entry.cs

Repository: sillsdev/languageforge-lexbox

Length of output: 832


🏁 Script executed:

# Verify all Picture creations set MediaUri explicitly
rg "new Picture" backend/FwLite -A 10 | grep -E "(new Picture|MediaUri =)" | head -20

Repository: sillsdev/languageforge-lexbox

Length of output: 871


🏁 Script executed:

# Check if any code relies on MediaUri being uninitialized/null
rg "MediaUri\s*==" backend/FwLite -B 2 -A 2

Repository: sillsdev/languageforge-lexbox

Length of output: 55


🏁 Script executed:

# Check if the test file compiles or if there's a nullable warning about MediaUri
rg "new Picture.*Caption" backend/FwLite/LcmCrdt.Tests/ -A 1 -B 1

Repository: sillsdev/languageforge-lexbox

Length of output: 456


🏁 Script executed:

# Examine what properties are marked as required in the codebase
rg "public virtual required" backend/FwLite/MiniLcm/Models/

Repository: sillsdev/languageforge-lexbox

Length of output: 944


Initialize or mark MediaUri as required.

MediaUri is declared non-nullable but not initialized, which violates nullable reference type constraints enabled globally with WarningsAsErrors. The test case in UseChangesTests.cs creates Picture without setting MediaUri, which triggers CS8618 errors. Either mark the property as required (consistent with other mandatory properties in the codebase like WritingSystem, ComplexFormComponent) to enforce initialization at construction, or initialize it with a default value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/MiniLcm/Models/Picture.cs` around lines 11 - 12, The MediaUri
property in the Picture class is declared as non-nullable but is not initialized
or marked as required, causing CS8618 compilation errors. Fix this by adding the
`required` keyword to the MediaUri property declaration to enforce
initialization at construction time, making it consistent with other mandatory
properties like WritingSystem and ComplexFormComponent in the codebase.

Source: Coding guidelines

Comment on lines +40 to +44
// TODO: Determine if MediaUri.ToString() is correct here
patchDocument.Operations.AddRange(SimpleStringDiff.GetStringDiff<Picture>(
nameof(Picture.MediaUri),
beforePicture.MediaUri.ToString(),
afterPicture.MediaUri.ToString()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Resolve MediaUri canonical mapping before emitting patch ops.

DiffToUpdate currently diffs Picture.MediaUri via ToString() while the code itself flags uncertainty. In this PR context, MediaUri is explicitly the non-trivial mapping; patching raw stringified values risks persisting non-canonical media identifiers and causing sync drift across layers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/FwLite/MiniLcm/SyncHelpers/PictureSync.cs` around lines 40 - 44, The
code is currently diffing Picture.MediaUri by calling ToString() directly on the
URI objects without resolving them to their canonical form first, as indicated
by the TODO comment. To fix this, before calling SimpleStringDiff.GetStringDiff
with nameof(Picture.MediaUri), resolve beforePicture.MediaUri and
afterPicture.MediaUri to their canonical representations (rather than using
ToString() directly), then pass the canonical string values to the GetStringDiff
method to ensure consistent and correct patch operations.

Comment on lines +354 to +362
private static MiniLcm.Models.Picture ToPicture(Guid senseId, Entities.Picture picture)
{
return new MiniLcm.Models.Picture
{
Id = picture.Guid,
Caption = ToRichMultiString(picture.Caption),
// TODO: Create a LfClassicMediaAdapter that can create MediaUris for LF URLs, then use that rather than returning NotFound
MediaUri = MediaUri.NotFound,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

ToPicture drops persisted file linkage by always returning MediaUri.NotFound.

Line 361 ignores Entities.Picture.FileName, so GetPicture can never return a usable media reference even when LF has one persisted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/LfClassicData/LfClassicMiniLcmApi.cs` around lines 354 - 362, The
ToPicture method hardcodes MediaUri.NotFound regardless of whether the picture
has a persisted filename, causing it to ignore the FileName property from the
Entities.Picture object. Replace the hardcoded MediaUri.NotFound assignment with
logic that checks if picture.FileName exists and constructs an appropriate
MediaUri from it; only use MediaUri.NotFound as a fallback when no filename is
present.

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.

Model images in FW-Lite

1 participant