From 917b27dad7b8f2fd3bbccf2d4166a1f3d3d8578d Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 30 Jul 2026 17:53:45 +0200 Subject: [PATCH 1/4] Make every dry-run record identify its object, and every write explicit A dry run's only output is its records, so a record that can't be traced to an object is worthless. Many couldn't: creates were labelled by headword, gloss or name (none unique) with no id; AddSemanticDomainToSense and SetSensePartOfSpeech never recorded the sense at all; writing system records carried the type but not the WsId. Example sentence and picture records printed "MiniLcm.Models.RichString" because RichString had no ToString, so their content was silently empty. Only 5 of 17 update methods included the patch summary. Every record now carries the object's id, its parent when the method knows it, and the patch summary when there is one. Complex form component records name both entries and the link id: the component side alone doesn't identify a link, which made distinct records look like duplicates. The Submit* interface defaults are gone, which is what stops this recurring. They let a wrapper that must observe every write silently inherit one and record under the method it forwards to, naming an API the caller never used. They're now abstract, so each implementer states its own behaviour: FwData forwards (unchanged, moved out of the interface), and the recorder, WriteIgnoringMiniLcmApi and MiniLcmApiWriteNormalizationWrapper are compile-forced to be explicit. That last one is a behaviour fix. The normalization wrapper declared only SubmitUpdatePicture, so the other 11 fell through to its returning UpdateX and the CRDT re-read the object, throwing on one the other side had deleted instead of letting the delete win. Co-Authored-By: Claude Opus 5 (1M context) --- .../Api/FwDataMiniLcmApi.cs | 17 ++ .../FwLiteProjectSync/RecordingMiniLcmApi.cs | 193 ++++++++++++------ .../WriteIgnoringMiniLcmApi.cs | 10 + backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 31 +-- backend/FwLite/MiniLcm/JsonPatchExtensions.cs | 13 +- backend/FwLite/MiniLcm/Models/RichString.cs | 4 + .../MiniLcmApiWriteNormalizationWrapper.cs | 55 +++++ 7 files changed, 233 insertions(+), 90 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index f2256115b5..c3954e7e60 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -2003,6 +2003,23 @@ public async Task SaveFile(Stream stream, LcmFileMetadata me } } + #region Submit (result-less write variants) + // The CRDT implements these to skip the read-back so a deleted object stays deleted. Here there's nothing + // to skip: liblcm already has the object in memory, and a genuinely missing one should still throw. + public Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) => UpdateEntry(id, update); + public Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) => CreateComplexFormComponent(complexFormComponent, position); + public Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) => MoveComplexFormComponent(complexFormComponent, between); + public Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) => CreateSense(entryId, sense, position); + public Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) => UpdateSense(entryId, senseId, update); + public Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) => CreateExampleSentence(entryId, senseId, exampleSentence, position); + public Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) => UpdateExampleSentence(entryId, senseId, exampleSentenceId, update); + public Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) => UpdatePartOfSpeech(id, update); + public Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) => UpdatePicture(entryId, senseId, pictureId, update); + public Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) => UpdatePublication(id, update); + public Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) => UpdateSemanticDomain(id, update); + public Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) => UpdateComplexFormType(id, update); + #endregion + private string TypeToLinkedFolder(string mimeType) { return mimeType switch diff --git a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs index 522b57fe87..ccca651be7 100644 --- a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs @@ -4,6 +4,13 @@ namespace FwLiteProjectSync; +/// +/// Records every write a dry-run sync would make. A record is the only evidence a dry run leaves, so +/// each description must (a) identify the object by id, not just by a human label, since headwords, +/// glosses and names are all non-unique, (b) name the parent object when the method knows it, and +/// (c) carry the patch summary when the method takes one. Analogous methods say the same things in the +/// same order; the Submit* variants repeat their non-Submit twin's description verbatim. +/// public partial class RecordingMiniLcmApi(IMiniLcmApi api) : IMiniLcmApi { @@ -25,7 +32,7 @@ public void Dispose() public async Task CreateWritingSystem(WritingSystem writingSystem, BetweenPosition? position = null) { RunRecords.Add(new RunRecord(nameof(CreateWritingSystem), - $"Create writing system {writingSystem.Type} between {position?.Previous} and {position?.Next}")); + $"Create {writingSystem.Type} writing system {writingSystem.WsId} ({writingSystem.Name}) {Position(position)}")); return await _api.CreateWritingSystem(writingSystem, position); } @@ -34,7 +41,7 @@ public async Task UpdateWritingSystem(WritingSystemId id, UpdateObjectInput update) { RunRecords.Add(new RunRecord(nameof(UpdateWritingSystem), - $"Update writing system {type}, changes: {update.Summarize()}")); + $"Update {type} writing system {id}, changes: {update.Summarize()}")); return await _api.UpdateWritingSystem(id, type, update); } @@ -46,19 +53,19 @@ public async Task UpdateWritingSystem(WritingSystem before, Writi public async Task MoveWritingSystem(WritingSystemId id, WritingSystemType type, BetweenPosition between) { - RunRecords.Add(new RunRecord(nameof(MoveWritingSystem), $"Move writing system {id} between {between.Previous} and {between.Next}")); + RunRecords.Add(new RunRecord(nameof(MoveWritingSystem), $"Move {type} writing system {id} {Position(between)}")); await _api.MoveWritingSystem(id, type, between); } public async Task CreatePartOfSpeech(PartOfSpeech partOfSpeech) { - RunRecords.Add(new RunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name}")); + RunRecords.Add(new RunRecord(nameof(CreatePartOfSpeech), $"Create part of speech {partOfSpeech.Name} ({partOfSpeech.Id})")); return await _api.CreatePartOfSpeech(partOfSpeech); } public async Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdatePartOfSpeech), $"Update part of speech {id}")); + RunRecords.Add(new RunRecord(nameof(UpdatePartOfSpeech), $"Update part of speech {id}, changes: {update.Summarize()}")); return await _api.UpdatePartOfSpeech(id, update); } @@ -77,13 +84,13 @@ public async Task DeletePartOfSpeech(Guid id) public async Task CreateSemanticDomain(SemanticDomain semanticDomain) { RunRecords.Add(new RunRecord(nameof(CreateSemanticDomain), - $"Create semantic domain {semanticDomain.Name}")); + $"Create semantic domain {semanticDomain.Code} {semanticDomain.Name} ({semanticDomain.Id})")); return await _api.CreateSemanticDomain(semanticDomain); } public async Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdateSemanticDomain), $"Update semantic domain {id}")); + RunRecords.Add(new RunRecord(nameof(UpdateSemanticDomain), $"Update semantic domain {id}, changes: {update.Summarize()}")); return await _api.UpdateSemanticDomain(id, update); } @@ -102,13 +109,13 @@ public async Task DeleteSemanticDomain(Guid id) public async Task CreateComplexFormType(ComplexFormType complexFormType) { RunRecords.Add(new RunRecord(nameof(CreateComplexFormType), - $"Create complex form type {complexFormType.Name}")); + $"Create complex form type {complexFormType.Name} ({complexFormType.Id})")); return await _api.CreateComplexFormType(complexFormType); } public async Task UpdateComplexFormType(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdateComplexFormType), $"Update complex form type {id}")); + RunRecords.Add(new RunRecord(nameof(UpdateComplexFormType), $"Update complex form type {id}, changes: {update.Summarize()}")); return await _api.UpdateComplexFormType(id, update); } @@ -132,7 +139,7 @@ public async Task CreateMorphType(MorphType morphType) public async Task UpdateMorphType(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdateMorphType), $"Update morph type {id}")); + RunRecords.Add(new RunRecord(nameof(UpdateMorphType), $"Update morph type {id}, changes: {update.Summarize()}")); return await _api.UpdateMorphType(id, update); } @@ -144,19 +151,20 @@ public async Task UpdateMorphType(MorphType before, MorphType after, public async Task CreateEntry(Entry entry, CreateEntryOptions? options = null) { - RunRecords.Add(new RunRecord(nameof(CreateEntry), $"Create entry {entry.Headword()} ({options ?? new CreateEntryOptions()})")); + RunRecords.Add(new RunRecord(nameof(CreateEntry), + $"Create entry {entry.Headword()} ({entry.Id}), options: {options ?? new CreateEntryOptions()}")); return await _api.CreateEntry(entry, options); } public async Task UpdateEntry(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdateEntry), $"Update entry {id}")); + RunRecords.Add(new RunRecord(nameof(UpdateEntry), $"Update entry {id}, changes: {update.Summarize()}")); return await _api.UpdateEntry(id, update); } public async Task UpdateEntry(Entry before, Entry after, IMiniLcmApi? api) { - RunRecords.Add(new RunRecord(nameof(UpdateEntry), $"Update entry {after.Id}")); + RunRecords.Add(new RunRecord(nameof(UpdateEntry), $"Update entry {after.Headword()} ({after.Id})")); return await _api.UpdateEntry(before, after, api); } @@ -168,63 +176,68 @@ public async Task DeleteEntry(Guid id) public async Task RemoveComplexFormType(Guid entryId, Guid complexFormTypeId) { - RunRecords.Add(new RunRecord(nameof(RemoveComplexFormType), $"Remove complex form type {complexFormTypeId}, from entry {entryId}")); + RunRecords.Add(new RunRecord(nameof(RemoveComplexFormType), $"Remove complex form type {complexFormTypeId} from entry {entryId}")); await _api.RemoveComplexFormType(entryId, complexFormTypeId); } public async Task CreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(CreateSense), $"Create sense {sense.Gloss} between {position?.Previous} and {position?.Next}")); + RunRecords.Add(new RunRecord(nameof(CreateSense), + $"Create sense {sense.Gloss} ({sense.Id}) in entry {entryId} {Position(position)}")); return await _api.CreateSense(entryId, sense, position); } public async Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) { RunRecords.Add(new RunRecord(nameof(UpdateSense), - $"Update sense {senseId}, changes: {update.Summarize()}")); + $"Update sense {senseId} in entry {entryId}, changes: {update.Summarize()}")); return await _api.UpdateSense(entryId, senseId, update); } public async Task UpdateSense(Guid entryId, Sense before, Sense after, IMiniLcmApi? api) { RunRecords.Add(new RunRecord(nameof(UpdateSense), - $"Update sense {after.Id}")); + $"Update sense {after.Gloss} ({after.Id}) in entry {entryId}")); return await _api.UpdateSense(entryId, before, after, api); } public async Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between) { - RunRecords.Add(new RunRecord(nameof(MoveSense), $"Move sense {senseId} between {between.Previous} and {between.Next}")); + RunRecords.Add(new RunRecord(nameof(MoveSense), $"Move sense {senseId} in entry {entryId} {Position(between)}")); await _api.MoveSense(entryId, senseId, between); } public async Task DeleteSense(Guid entryId, Guid senseId) { - RunRecords.Add(new RunRecord(nameof(DeleteSense), $"Delete sense {senseId}")); + RunRecords.Add(new RunRecord(nameof(DeleteSense), $"Delete sense {senseId} in entry {entryId}")); await _api.DeleteSense(entryId, senseId); } public async Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain) { - RunRecords.Add(new RunRecord(nameof(AddSemanticDomainToSense), $"Add semantic domain {semanticDomain.Name}")); + RunRecords.Add(new RunRecord(nameof(AddSemanticDomainToSense), + $"Add semantic domain {semanticDomain.Code} {semanticDomain.Name} ({semanticDomain.Id}) to sense {senseId}")); await _api.AddSemanticDomainToSense(senseId, semanticDomain); } public async Task RemoveSemanticDomainFromSense(Guid senseId, Guid semanticDomainId) { - RunRecords.Add(new RunRecord(nameof(RemoveSemanticDomainFromSense), $"Remove semantic domain {semanticDomainId}")); + RunRecords.Add(new RunRecord(nameof(RemoveSemanticDomainFromSense), + $"Remove semantic domain {semanticDomainId} from sense {senseId}")); await _api.RemoveSemanticDomainFromSense(senseId, semanticDomainId); } public async Task SetSensePartOfSpeech(Guid senseId, Guid? partOfSpeechId) { - RunRecords.Add(new RunRecord(nameof(SetSensePartOfSpeech), $"Set part of speech {partOfSpeechId}")); + RunRecords.Add(new RunRecord(nameof(SetSensePartOfSpeech), + $"Set part of speech {OrNull(partOfSpeechId)} on sense {senseId}")); await _api.SetSensePartOfSpeech(senseId, partOfSpeechId); } public async Task CreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(CreateExampleSentence), $"Create example sentence {exampleSentence.Sentence} between {position?.Previous} and {position?.Next}")); + RunRecords.Add(new RunRecord(nameof(CreateExampleSentence), + $"Create example sentence {exampleSentence.Sentence} ({exampleSentence.Id}) in sense {senseId} {Position(position)}")); return await _api.CreateExampleSentence(entryId, senseId, exampleSentence, position); } @@ -234,7 +247,7 @@ public async Task UpdateExampleSentence(Guid entryId, UpdateObjectInput update) { RunRecords.Add(new RunRecord(nameof(UpdateExampleSentence), - $"Update example sentence {exampleSentenceId}, changes: {update.Summarize()}")); + $"Update example sentence {exampleSentenceId} in sense {senseId}, changes: {update.Summarize()}")); return await _api.UpdateExampleSentence(entryId, senseId, exampleSentenceId, update); } @@ -244,31 +257,36 @@ public async Task UpdateExampleSentence(Guid entryId, ExampleSentence after, IMiniLcmApi? api) { - RunRecords.Add(new RunRecord(nameof(UpdateExampleSentence), $"Update example sentence {after.Id}")); + RunRecords.Add(new RunRecord(nameof(UpdateExampleSentence), + $"Update example sentence {after.Sentence} ({after.Id}) in sense {senseId}")); return await _api.UpdateExampleSentence(entryId, senseId, before, after, api); } public async Task MoveExampleSentence(Guid entryId, Guid senseId, Guid exampleId, BetweenPosition between) { - RunRecords.Add(new RunRecord(nameof(MoveExampleSentence), $"Move example sentence {exampleId} between {between.Previous} and {between.Next}")); + RunRecords.Add(new RunRecord(nameof(MoveExampleSentence), + $"Move example sentence {exampleId} in sense {senseId} {Position(between)}")); await _api.MoveExampleSentence(entryId, senseId, exampleId, between); } public async Task DeleteExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId) { - RunRecords.Add(new RunRecord(nameof(DeleteExampleSentence), $"Delete example sentence {exampleSentenceId}")); + RunRecords.Add(new RunRecord(nameof(DeleteExampleSentence), + $"Delete example sentence {exampleSentenceId} in sense {senseId}")); await _api.DeleteExampleSentence(entryId, senseId, exampleSentenceId); } public async Task AddTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Translation translation) { - RunRecords.Add(new RunRecord(nameof(AddTranslation), $"Add translation {translation.Id} to example sentence {exampleSentenceId}")); + RunRecords.Add(new RunRecord(nameof(AddTranslation), + $"Add translation {translation.Text} ({translation.Id}) to example sentence {exampleSentenceId}")); await _api.AddTranslation(entryId, senseId, exampleSentenceId, translation); } public async Task RemoveTranslation(Guid entryId, Guid senseId, Guid exampleSentenceId, Guid translationId) { - RunRecords.Add(new RunRecord(nameof(RemoveTranslation), $"Remove translation {translationId} from example sentence {exampleSentenceId}")); + RunRecords.Add(new RunRecord(nameof(RemoveTranslation), + $"Remove translation {translationId} from example sentence {exampleSentenceId}")); await _api.RemoveTranslation(entryId, senseId, exampleSentenceId, translationId); } @@ -278,14 +296,16 @@ public async Task UpdateTranslation(Guid entryId, Guid translationId, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdateTranslation), $"Update translation {translationId} in example sentence {exampleSentenceId}")); + RunRecords.Add(new RunRecord(nameof(UpdateTranslation), + $"Update translation {translationId} in example sentence {exampleSentenceId}, changes: {update.Summarize()}")); await _api.UpdateTranslation(entryId, senseId, exampleSentenceId, translationId, update); } public async Task CreatePicture(Guid entryId, Guid senseId, Picture picture, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(CreatePicture), $"Create picture {picture.Caption} between {position?.Previous} and {position?.Next}")); + RunRecords.Add(new RunRecord(nameof(CreatePicture), + $"Create picture {picture.Caption} ({picture.Id}) in sense {senseId} {Position(position)}")); return await _api.CreatePicture(entryId, senseId, picture, position); } @@ -295,7 +315,7 @@ public async Task UpdatePicture(Guid entryId, UpdateObjectInput update) { RunRecords.Add(new RunRecord(nameof(UpdatePicture), - $"Update picture {pictureId}, changes: {update.Summarize()}")); + $"Update picture {pictureId} in sense {senseId}, changes: {update.Summarize()}")); return await _api.UpdatePicture(entryId, senseId, pictureId, update); } @@ -305,69 +325,64 @@ public async Task UpdatePicture(Guid entryId, Picture after, IMiniLcmApi? api) { - RunRecords.Add(new RunRecord(nameof(UpdatePicture), $"Update picture {after.Id}")); + RunRecords.Add(new RunRecord(nameof(UpdatePicture), $"Update picture {after.Caption} ({after.Id}) in sense {senseId}")); return await _api.UpdatePicture(entryId, senseId, before, after, api); } - public async Task MovePicture(Guid entryId, Guid senseId, Guid exampleId, BetweenPosition between) + public async Task MovePicture(Guid entryId, Guid senseId, Guid pictureId, BetweenPosition between) { - RunRecords.Add(new RunRecord(nameof(MovePicture), $"Move picture {exampleId} between {between.Previous} and {between.Next}")); - await _api.MovePicture(entryId, senseId, exampleId, between); + RunRecords.Add(new RunRecord(nameof(MovePicture), $"Move picture {pictureId} in sense {senseId} {Position(between)}")); + await _api.MovePicture(entryId, senseId, pictureId, between); } public async Task DeletePicture(Guid entryId, Guid senseId, Guid pictureId) { - RunRecords.Add(new RunRecord(nameof(DeletePicture), $"Delete picture {pictureId}")); + RunRecords.Add(new RunRecord(nameof(DeletePicture), $"Delete picture {pictureId} in sense {senseId}")); await _api.DeletePicture(entryId, senseId, pictureId); } public async Task CreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? between = null) { - var complexFormName = ComplexFormName(complexFormComponent); - var componentName = ComplexFormComponentName(complexFormComponent); - var previous = ComplexFormComponentName(between?.Previous); - var next = ComplexFormComponentName(between?.Next); - RunRecords.Add(new RunRecord(nameof(CreateComplexFormComponent), $"Create complex form component complex entry: {complexFormName}, component entry: {componentName}, between {previous} and {next}")); + RunRecords.Add(new RunRecord(nameof(CreateComplexFormComponent), + $"Create complex form component {ComplexFormLink(complexFormComponent)} {Position(between)}")); return await _api.CreateComplexFormComponent(complexFormComponent, between); } public async Task MoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) { - var componentName = ComplexFormComponentName(complexFormComponent); - var previous = ComplexFormComponentName(between.Previous); - var next = ComplexFormComponentName(between.Next); - RunRecords.Add(new RunRecord(nameof(MoveComplexFormComponent), $"Move complex form component {componentName} between {previous} and {next}")); + RunRecords.Add(new RunRecord(nameof(MoveComplexFormComponent), + $"Move complex form component {ComplexFormLink(complexFormComponent)} {Position(between)}")); await _api.MoveComplexFormComponent(complexFormComponent, between); } public async Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) { - var componentName = ComplexFormComponentName(complexFormComponent); - RunRecords.Add(new RunRecord(nameof(DeleteComplexFormComponent), $"Delete complex form component: {componentName}")); + RunRecords.Add(new RunRecord(nameof(DeleteComplexFormComponent), + $"Delete complex form component {ComplexFormLink(complexFormComponent)}")); await _api.DeleteComplexFormComponent(complexFormComponent); } public async Task AddComplexFormType(Guid entryId, Guid complexFormTypeId) { - RunRecords.Add(new RunRecord(nameof(AddComplexFormType), $"Add complex form type {complexFormTypeId}, to entry {entryId}")); + RunRecords.Add(new RunRecord(nameof(AddComplexFormType), $"Add complex form type {complexFormTypeId} to entry {entryId}")); await _api.AddComplexFormType(entryId, complexFormTypeId); } public async Task CreatePublication(Publication pub) { - RunRecords.Add(new RunRecord(nameof(CreatePublication), $"Create publication {pub.Id}")); + RunRecords.Add(new RunRecord(nameof(CreatePublication), $"Create publication {pub.Name} ({pub.Id})")); return await _api.CreatePublication(pub); } public async Task UpdatePublication(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(UpdatePublication), $"Update publication {id}")); + RunRecords.Add(new RunRecord(nameof(UpdatePublication), $"Update publication {id}, changes: {update.Summarize()}")); return await _api.UpdatePublication(id, update); } public async Task UpdatePublication(Publication before, Publication after, IMiniLcmApi? api = null) { - RunRecords.Add(new RunRecord(nameof(UpdatePublication), $"Update publication {before.Id}")); + RunRecords.Add(new RunRecord(nameof(UpdatePublication), $"Update publication {after.Id}")); return await _api.UpdatePublication(before, after, api); } @@ -390,78 +405,122 @@ public async Task RemovePublication(Guid entryId, Guid publicationId) } #region Submit (sync's result-less write variants) - // Submit* are writes, so they're implemented here to record and forward. Any not listed falls back to the - // interface default, which routes to the recording Update*/Move* above. + // All of them, and the compiler enforces that: Submit* are abstract on IMiniLcmWriteApi, so none of these + // can be silently skipped. Forwarding to an Update*/Move* instead would record a method the sync never called. public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdateEntry), $"Update entry {id}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdateEntry), $"Update entry {id}, changes: {update.Summarize()}")); await _api.SubmitUpdateEntry(id, update); } public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdateSense), $"Update sense {senseId}, changes: {update.Summarize()}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdateSense), + $"Update sense {senseId} in entry {entryId}, changes: {update.Summarize()}")); await _api.SubmitUpdateSense(entryId, senseId, update); } public async Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdateExampleSentence), $"Update example sentence {exampleSentenceId}, changes: {update.Summarize()}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdateExampleSentence), + $"Update example sentence {exampleSentenceId} in sense {senseId}, changes: {update.Summarize()}")); await _api.SubmitUpdateExampleSentence(entryId, senseId, exampleSentenceId, update); } + public async Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) + { + RunRecords.Add(new RunRecord(nameof(SubmitUpdatePicture), + $"Update picture {pictureId} in sense {senseId}, changes: {update.Summarize()}")); + await _api.SubmitUpdatePicture(entryId, senseId, pictureId, update); + } + public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(SubmitCreateSense), $"Create sense {sense.Gloss}")); + RunRecords.Add(new RunRecord(nameof(SubmitCreateSense), + $"Create sense {sense.Gloss} ({sense.Id}) in entry {entryId} {Position(position)}")); await _api.SubmitCreateSense(entryId, sense, position); } public async Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(SubmitCreateExampleSentence), $"Create example sentence {exampleSentence.Sentence}")); + RunRecords.Add(new RunRecord(nameof(SubmitCreateExampleSentence), + $"Create example sentence {exampleSentence.Sentence} ({exampleSentence.Id}) in sense {senseId} {Position(position)}")); await _api.SubmitCreateExampleSentence(entryId, senseId, exampleSentence, position); } public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) { - RunRecords.Add(new RunRecord(nameof(SubmitCreateComplexFormComponent), $"Create complex form component {ComplexFormComponentName(complexFormComponent)}")); + RunRecords.Add(new RunRecord(nameof(SubmitCreateComplexFormComponent), + $"Create complex form component {ComplexFormLink(complexFormComponent)} {Position(position)}")); await _api.SubmitCreateComplexFormComponent(complexFormComponent, position); } + public async Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) + { + RunRecords.Add(new RunRecord(nameof(SubmitMoveComplexFormComponent), + $"Move complex form component {ComplexFormLink(complexFormComponent)} {Position(between)}")); + await _api.SubmitMoveComplexFormComponent(complexFormComponent, between); + } + public async Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdatePartOfSpeech), $"Update part of speech {id}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdatePartOfSpeech), $"Update part of speech {id}, changes: {update.Summarize()}")); await _api.SubmitUpdatePartOfSpeech(id, update); } public async Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdatePublication), $"Update publication {id}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdatePublication), $"Update publication {id}, changes: {update.Summarize()}")); await _api.SubmitUpdatePublication(id, update); } public async Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdateSemanticDomain), $"Update semantic domain {id}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdateSemanticDomain), $"Update semantic domain {id}, changes: {update.Summarize()}")); await _api.SubmitUpdateSemanticDomain(id, update); } public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) { - RunRecords.Add(new RunRecord(nameof(SubmitUpdateComplexFormType), $"Update complex form type {id}")); + RunRecords.Add(new RunRecord(nameof(SubmitUpdateComplexFormType), $"Update complex form type {id}, changes: {update.Summarize()}")); await _api.SubmitUpdateComplexFormType(id, update); } #endregion - private string ComplexFormComponentName(ComplexFormComponent? component) + // The component side alone doesn't identify a link: the same component is usually joined to several + // complex forms, which made distinct dry-run lines look like duplicates (docs/sync-fidelity/README.md). + // The link id is null for fwdata components, so the entry/sense ids are what actually identify it. + private static string ComplexFormLink(ComplexFormComponent component) + { + return $"complex form {ComplexFormName(component)}, component {ComplexFormComponentName(component)} (link {OrNull(component.MaybeId)})"; + } + + private static string ComplexFormComponentName(ComplexFormComponent? component) { if (component == null) return "null"; - return $"{component.ComponentHeadword} ({component.ComponentEntryId}:{component.ComponentSenseId})"; + return $"{component.ComponentHeadword} (entry {component.ComponentEntryId}, sense {OrNull(component.ComponentSenseId)})"; } - private string ComplexFormName(ComplexFormComponent? component) + private static string ComplexFormName(ComplexFormComponent? component) { if (component == null) return "null"; return $"{component.ComplexFormHeadword} ({component.ComplexFormEntryId})"; } + + private static string Position(BetweenPosition? position) + { + if (position is null) return "at the end"; + return $"between {OrNull(position.Previous)} and {OrNull(position.Next)}"; + } + + private static string Position(BetweenPosition? position) + { + if (position is null) return "at the end"; + return $"between {ComplexFormComponentName(position.Previous)} and {ComplexFormComponentName(position.Next)}"; + } + + private static string OrNull(T? value) + { + return value is null ? "null" : value.ToString() ?? "null"; + } } diff --git a/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs index e7d903a445..14106d1f62 100644 --- a/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs @@ -359,6 +359,16 @@ public Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormCom return Task.CompletedTask; } + public Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) + { + return Task.CompletedTask; + } + + public Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) + { + return Task.CompletedTask; + } + public Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) { return Task.CompletedTask; diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index a09f955573..f358a02693 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -140,23 +140,24 @@ Task UpdatePicture(Guid entryId, #region Submit (fire-and-forget write variants for sync) // Result-less write variants the sync uses instead of the returning Update/Create methods above. The CRDT - // overrides them to submit the change without fetching the result, so applying to an object the other side - // deleted leaves it deleted (delete wins) rather than throwing. The defaults forward to the returning - // method (correct for FwData, which still surfaces a genuinely-missing object). - Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) => UpdateEntry(id, update); - Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) => CreateComplexFormComponent(complexFormComponent, position); - Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) => MoveComplexFormComponent(complexFormComponent, between); - Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) => CreateSense(entryId, sense, position); - Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) => UpdateSense(entryId, senseId, update); - Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) => CreateExampleSentence(entryId, senseId, exampleSentence, position); - Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) => UpdateExampleSentence(entryId, senseId, exampleSentenceId, update); + // implements them to submit the change without fetching the result, so applying to an object the other side + // deleted leaves it deleted (delete wins) rather than throwing. + // Declared abstract on purpose: no default forwarding to the returning method, so a wrapper that has to + // see every write (recording, write-ignoring, normalizing) can't inherit one silently. + Task SubmitUpdateEntry(Guid id, UpdateObjectInput update); + Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null); + Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between); + Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null); + Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update); + Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null); + Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update); // Dependency types too (they sync before entries, outside EntrySync's try/catch). WritingSystem is omitted // (its update resolves the entity id, so it can't be a blind submit); MorphType is omitted (not deletable). - Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) => UpdatePartOfSpeech(id, update); - Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) => UpdatePicture(entryId, senseId, pictureId, update); - Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) => UpdatePublication(id, update); - Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) => UpdateSemanticDomain(id, update); - Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) => UpdateComplexFormType(id, update); + Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update); + Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update); + Task SubmitUpdatePublication(Guid id, UpdateObjectInput update); + Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update); + Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update); #endregion #region CustomView diff --git a/backend/FwLite/MiniLcm/JsonPatchExtensions.cs b/backend/FwLite/MiniLcm/JsonPatchExtensions.cs index 66de5cd5c9..d5b1ac3d70 100644 --- a/backend/FwLite/MiniLcm/JsonPatchExtensions.cs +++ b/backend/FwLite/MiniLcm/JsonPatchExtensions.cs @@ -1,4 +1,3 @@ -using System.Text; using SystemTextJsonPatch; namespace MiniLcm; @@ -31,14 +30,12 @@ public static bool TryGetPropertyChange(this UpdateObjectInput upd } return false; } + /// + /// One line, so callers can log a patch without breaking per-line grep. Doesn't name the patched + /// type; callers already do. + /// public static string Summarize(this JsonPatchDocument document) where T : class { - var sb = new StringBuilder(); - sb.AppendLine($"Update: {typeof(T).Name}"); - foreach (var op in document.Operations) - { - sb.AppendLine($"{op.OperationType} {op.Path}: {op.Value}"); - } - return sb.ToString(); + return string.Join(", ", document.Operations.Select(op => $"{op.OperationType} {op.Path}: {op.Value}")); } } diff --git a/backend/FwLite/MiniLcm/Models/RichString.cs b/backend/FwLite/MiniLcm/Models/RichString.cs index 02e755d5c1..4f74b822a8 100644 --- a/backend/FwLite/MiniLcm/Models/RichString.cs +++ b/backend/FwLite/MiniLcm/Models/RichString.cs @@ -46,6 +46,10 @@ public string GetPlainText() return string.Join("", Spans.Select(s => s.Text)); } + // Without this, RichMultiString.ToString() prints the type name for every value, which silently + // emptied the dry-run sync records of their example sentence and picture caption text. + public override string ToString() => GetPlainText(); + public void EnsureWs(WritingSystemId ws) { foreach (var span in Spans) diff --git a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs index a43847bb50..6aa0f4cba8 100644 --- a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs +++ b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs @@ -89,6 +89,11 @@ public Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update) + { + return _api.SubmitUpdatePartOfSpeech(id, NormalizePatch(update)); + } + public async Task UpdatePartOfSpeech(PartOfSpeech before, PartOfSpeech after, IMiniLcmApi? api = null) { @@ -121,6 +126,11 @@ public Task UpdatePublication(Guid id, UpdateObjectInput update) + { + return _api.SubmitUpdatePublication(id, NormalizePatch(update)); + } + public async Task UpdatePublication(Publication before, Publication after, IMiniLcmApi? api = null) { @@ -153,6 +163,11 @@ public Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) + { + return _api.SubmitUpdateSemanticDomain(id, NormalizePatch(update)); + } + public async Task UpdateSemanticDomain(SemanticDomain before, SemanticDomain after, IMiniLcmApi? api = null) { @@ -186,6 +201,11 @@ public Task UpdateComplexFormType(Guid id, UpdateObjectInput update) + { + return _api.SubmitUpdateComplexFormType(id, NormalizePatch(update)); + } + public async Task UpdateComplexFormType(ComplexFormType before, ComplexFormType after, IMiniLcmApi? api = null) { @@ -250,6 +270,11 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return _api.UpdateEntry(id, NormalizePatch(update)); } + public Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) + { + return _api.SubmitUpdateEntry(id, NormalizePatch(update)); + } + public async Task UpdateEntry(Entry before, Entry after, IMiniLcmApi? api = null) { @@ -266,11 +291,21 @@ public async Task CreateComplexFormComponent(ComplexFormCo return await _api.CreateComplexFormComponent(NormalizeComplexFormComponent(complexFormComponent), position); } + public Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) + { + return _api.SubmitCreateComplexFormComponent(NormalizeComplexFormComponent(complexFormComponent), position); + } + public Task MoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) { return _api.MoveComplexFormComponent(complexFormComponent, between); } + public Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) + { + return _api.SubmitMoveComplexFormComponent(complexFormComponent, between); + } + public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) { return _api.DeleteComplexFormComponent(complexFormComponent); @@ -328,11 +363,21 @@ public async Task CreateSense(Guid entryId, Sense sense, BetweenPosition? return await _api.CreateSense(entryId, NormalizeSense(sense), position); } + public Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) + { + return _api.SubmitCreateSense(entryId, NormalizeSense(sense), position); + } + public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) { return _api.UpdateSense(entryId, senseId, NormalizePatch(update)); } + public Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) + { + return _api.SubmitUpdateSense(entryId, senseId, NormalizePatch(update)); + } + public async Task UpdateSense(Guid entryId, Sense before, Sense after, IMiniLcmApi? api = null) { @@ -385,11 +430,21 @@ public async Task CreateExampleSentence(Guid entryId, Guid sens return await _api.CreateExampleSentence(entryId, senseId, NormalizeExampleSentence(exampleSentence), position); } + public Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) + { + return _api.SubmitCreateExampleSentence(entryId, senseId, NormalizeExampleSentence(exampleSentence), position); + } + public Task UpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) { return _api.UpdateExampleSentence(entryId, senseId, exampleSentenceId, NormalizePatch(update)); } + public Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) + { + return _api.SubmitUpdateExampleSentence(entryId, senseId, exampleSentenceId, NormalizePatch(update)); + } + public async Task UpdateExampleSentence(Guid entryId, Guid senseId, ExampleSentence before, ExampleSentence after, IMiniLcmApi? api = null) { From 3c8186096227999c8656e64472f3abfa2f2c0851 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 30 Jul 2026 18:03:39 +0200 Subject: [PATCH 2/4] Drop a comment's pointer to a doc that isn't in the repo The sync-fidelity notes aren't on develop, so the pointer led nowhere. The reason it gave is short enough to state in place. Co-Authored-By: Claude Opus 5 (1M context) --- backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs index ccca651be7..30f4bd0040 100644 --- a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs @@ -488,7 +488,7 @@ public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput Date: Thu, 30 Jul 2026 18:13:52 +0200 Subject: [PATCH 3/4] Await in the new write forwarders so they stay on the stack trace backend/AGENTS.md asks for `return await` unless the method is a hot path; these run once per synced object, so the exemption doesn't apply. Covers the pre-existing SubmitUpdatePicture in the normalization wrapper too, so the block reads uniformly. Co-Authored-By: Claude Opus 5 (1M context) --- .../Api/FwDataMiniLcmApi.cs | 24 +++++----- .../MiniLcmApiWriteNormalizationWrapper.cs | 48 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index c3954e7e60..635bf5d2c6 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -2006,18 +2006,18 @@ public async Task SaveFile(Stream stream, LcmFileMetadata me #region Submit (result-less write variants) // The CRDT implements these to skip the read-back so a deleted object stays deleted. Here there's nothing // to skip: liblcm already has the object in memory, and a genuinely missing one should still throw. - public Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) => UpdateEntry(id, update); - public Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) => CreateComplexFormComponent(complexFormComponent, position); - public Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) => MoveComplexFormComponent(complexFormComponent, between); - public Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) => CreateSense(entryId, sense, position); - public Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) => UpdateSense(entryId, senseId, update); - public Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) => CreateExampleSentence(entryId, senseId, exampleSentence, position); - public Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) => UpdateExampleSentence(entryId, senseId, exampleSentenceId, update); - public Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) => UpdatePartOfSpeech(id, update); - public Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) => UpdatePicture(entryId, senseId, pictureId, update); - public Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) => UpdatePublication(id, update); - public Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) => UpdateSemanticDomain(id, update); - public Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) => UpdateComplexFormType(id, update); + public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) => await UpdateEntry(id, update); + public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) => await CreateComplexFormComponent(complexFormComponent, position); + public async Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) => await MoveComplexFormComponent(complexFormComponent, between); + public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) => await CreateSense(entryId, sense, position); + public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) => await UpdateSense(entryId, senseId, update); + public async Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) => await CreateExampleSentence(entryId, senseId, exampleSentence, position); + public async Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) => await UpdateExampleSentence(entryId, senseId, exampleSentenceId, update); + public async Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) => await UpdatePartOfSpeech(id, update); + public async Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) => await UpdatePicture(entryId, senseId, pictureId, update); + public async Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) => await UpdatePublication(id, update); + public async Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) => await UpdateSemanticDomain(id, update); + public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) => await UpdateComplexFormType(id, update); #endregion private string TypeToLinkedFolder(string mimeType) diff --git a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs index 6aa0f4cba8..2da2e0e965 100644 --- a/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs +++ b/backend/FwLite/MiniLcm/Normalization/MiniLcmApiWriteNormalizationWrapper.cs @@ -89,9 +89,9 @@ public Task UpdatePartOfSpeech(Guid id, UpdateObjectInput update) + public async Task SubmitUpdatePartOfSpeech(Guid id, UpdateObjectInput update) { - return _api.SubmitUpdatePartOfSpeech(id, NormalizePatch(update)); + await _api.SubmitUpdatePartOfSpeech(id, NormalizePatch(update)); } @@ -126,9 +126,9 @@ public Task UpdatePublication(Guid id, UpdateObjectInput update) + public async Task SubmitUpdatePublication(Guid id, UpdateObjectInput update) { - return _api.SubmitUpdatePublication(id, NormalizePatch(update)); + await _api.SubmitUpdatePublication(id, NormalizePatch(update)); } @@ -163,9 +163,9 @@ public Task UpdateSemanticDomain(Guid id, UpdateObjectInput update) + public async Task SubmitUpdateSemanticDomain(Guid id, UpdateObjectInput update) { - return _api.SubmitUpdateSemanticDomain(id, NormalizePatch(update)); + await _api.SubmitUpdateSemanticDomain(id, NormalizePatch(update)); } @@ -201,9 +201,9 @@ public Task UpdateComplexFormType(Guid id, UpdateObjectInput update) + public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) { - return _api.SubmitUpdateComplexFormType(id, NormalizePatch(update)); + await _api.SubmitUpdateComplexFormType(id, NormalizePatch(update)); } @@ -270,9 +270,9 @@ public Task UpdateEntry(Guid id, UpdateObjectInput update) return _api.UpdateEntry(id, NormalizePatch(update)); } - public Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) + public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) { - return _api.SubmitUpdateEntry(id, NormalizePatch(update)); + await _api.SubmitUpdateEntry(id, NormalizePatch(update)); } @@ -291,9 +291,9 @@ public async Task CreateComplexFormComponent(ComplexFormCo return await _api.CreateComplexFormComponent(NormalizeComplexFormComponent(complexFormComponent), position); } - public Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) + public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) { - return _api.SubmitCreateComplexFormComponent(NormalizeComplexFormComponent(complexFormComponent), position); + await _api.SubmitCreateComplexFormComponent(NormalizeComplexFormComponent(complexFormComponent), position); } public Task MoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) @@ -301,9 +301,9 @@ public Task MoveComplexFormComponent(ComplexFormComponent complexFormComponent, return _api.MoveComplexFormComponent(complexFormComponent, between); } - public Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) + public async Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) { - return _api.SubmitMoveComplexFormComponent(complexFormComponent, between); + await _api.SubmitMoveComplexFormComponent(complexFormComponent, between); } public Task DeleteComplexFormComponent(ComplexFormComponent complexFormComponent) @@ -363,9 +363,9 @@ public async Task CreateSense(Guid entryId, Sense sense, BetweenPosition? return await _api.CreateSense(entryId, NormalizeSense(sense), position); } - public Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) + public async Task SubmitCreateSense(Guid entryId, Sense sense, BetweenPosition? position = null) { - return _api.SubmitCreateSense(entryId, NormalizeSense(sense), position); + await _api.SubmitCreateSense(entryId, NormalizeSense(sense), position); } public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) @@ -373,9 +373,9 @@ public Task UpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) + public async Task SubmitUpdateSense(Guid entryId, Guid senseId, UpdateObjectInput update) { - return _api.SubmitUpdateSense(entryId, senseId, NormalizePatch(update)); + await _api.SubmitUpdateSense(entryId, senseId, NormalizePatch(update)); } @@ -430,9 +430,9 @@ public async Task CreateExampleSentence(Guid entryId, Guid sens return await _api.CreateExampleSentence(entryId, senseId, NormalizeExampleSentence(exampleSentence), position); } - public Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) + public async Task SubmitCreateExampleSentence(Guid entryId, Guid senseId, ExampleSentence exampleSentence, BetweenPosition? position = null) { - return _api.SubmitCreateExampleSentence(entryId, senseId, NormalizeExampleSentence(exampleSentence), position); + await _api.SubmitCreateExampleSentence(entryId, senseId, NormalizeExampleSentence(exampleSentence), position); } public Task UpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) @@ -440,9 +440,9 @@ public Task UpdateExampleSentence(Guid entryId, Guid senseId, G return _api.UpdateExampleSentence(entryId, senseId, exampleSentenceId, NormalizePatch(update)); } - public Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) + public async Task SubmitUpdateExampleSentence(Guid entryId, Guid senseId, Guid exampleSentenceId, UpdateObjectInput update) { - return _api.SubmitUpdateExampleSentence(entryId, senseId, exampleSentenceId, NormalizePatch(update)); + await _api.SubmitUpdateExampleSentence(entryId, senseId, exampleSentenceId, NormalizePatch(update)); } @@ -516,9 +516,9 @@ public Task UpdatePicture(Guid entryId, Guid senseId, Guid pictureId, U return _api.UpdatePicture(entryId, senseId, pictureId, NormalizePatch(update)); } - public Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) + public async Task SubmitUpdatePicture(Guid entryId, Guid senseId, Guid pictureId, UpdateObjectInput update) { - return _api.SubmitUpdatePicture(entryId, senseId, pictureId, NormalizePatch(update)); + await _api.SubmitUpdatePicture(entryId, senseId, pictureId, NormalizePatch(update)); } public async Task UpdatePicture(Guid entryId, Guid senseId, Picture before, Picture after, IMiniLcmApi? api = null) From 1e85480fccef4b9769c194cd76867386b3439f3d Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Thu, 30 Jul 2026 21:05:51 +0200 Subject: [PATCH 4/4] Cut the comments back to what the code can't say Dropped the convention essay on the recorder, the rationale duplicated between the interface and its implementers, and the notes that only described the change. Also drops a WriteIgnoring comment about inheriting an interface default, which no longer exists. Co-Authored-By: Claude Opus 5 (1M context) --- .../FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs | 3 +-- .../FwLiteProjectSync/RecordingMiniLcmApi.cs | 14 ++++---------- .../FwLiteProjectSync/WriteIgnoringMiniLcmApi.cs | 2 -- backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs | 3 +-- backend/FwLite/MiniLcm/JsonPatchExtensions.cs | 5 +---- backend/FwLite/MiniLcm/Models/RichString.cs | 2 -- 6 files changed, 7 insertions(+), 22 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs index 635bf5d2c6..fd51dd14e7 100644 --- a/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs +++ b/backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs @@ -2004,8 +2004,7 @@ public async Task SaveFile(Stream stream, LcmFileMetadata me } #region Submit (result-less write variants) - // The CRDT implements these to skip the read-back so a deleted object stays deleted. Here there's nothing - // to skip: liblcm already has the object in memory, and a genuinely missing one should still throw. + // Nothing to skip here: liblcm holds the object already, and a genuinely missing one should still throw. public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) => await UpdateEntry(id, update); public async Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null) => await CreateComplexFormComponent(complexFormComponent, position); public async Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between) => await MoveComplexFormComponent(complexFormComponent, between); diff --git a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs index 30f4bd0040..9ab93a431f 100644 --- a/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs +++ b/backend/FwLite/FwLiteProjectSync/RecordingMiniLcmApi.cs @@ -5,11 +5,8 @@ namespace FwLiteProjectSync; /// -/// Records every write a dry-run sync would make. A record is the only evidence a dry run leaves, so -/// each description must (a) identify the object by id, not just by a human label, since headwords, -/// glosses and names are all non-unique, (b) name the parent object when the method knows it, and -/// (c) carry the patch summary when the method takes one. Analogous methods say the same things in the -/// same order; the Submit* variants repeat their non-Submit twin's description verbatim. +/// Records every write a dry-run sync would make. The records are the run's only output, so each one must +/// identify its object by id: headwords, glosses and names are not unique. /// public partial class RecordingMiniLcmApi(IMiniLcmApi api) : IMiniLcmApi { @@ -405,8 +402,6 @@ public async Task RemovePublication(Guid entryId, Guid publicationId) } #region Submit (sync's result-less write variants) - // All of them, and the compiler enforces that: Submit* are abstract on IMiniLcmWriteApi, so none of these - // can be silently skipped. Forwarding to an Update*/Move* instead would record a method the sync never called. public async Task SubmitUpdateEntry(Guid id, UpdateObjectInput update) { RunRecords.Add(new RunRecord(nameof(SubmitUpdateEntry), $"Update entry {id}, changes: {update.Summarize()}")); @@ -487,9 +482,8 @@ public async Task SubmitUpdateComplexFormType(Guid id, UpdateObjectInput update) { return Task.CompletedTask; diff --git a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs index f358a02693..4cc8f2fae5 100644 --- a/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs +++ b/backend/FwLite/MiniLcm/IMiniLcmWriteApi.cs @@ -142,8 +142,7 @@ Task UpdatePicture(Guid entryId, // Result-less write variants the sync uses instead of the returning Update/Create methods above. The CRDT // implements them to submit the change without fetching the result, so applying to an object the other side // deleted leaves it deleted (delete wins) rather than throwing. - // Declared abstract on purpose: no default forwarding to the returning method, so a wrapper that has to - // see every write (recording, write-ignoring, normalizing) can't inherit one silently. + // No defaults on purpose: a wrapper that has to see every write can't then inherit one silently. Task SubmitUpdateEntry(Guid id, UpdateObjectInput update); Task SubmitCreateComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition? position = null); Task SubmitMoveComplexFormComponent(ComplexFormComponent complexFormComponent, BetweenPosition between); diff --git a/backend/FwLite/MiniLcm/JsonPatchExtensions.cs b/backend/FwLite/MiniLcm/JsonPatchExtensions.cs index d5b1ac3d70..6c50bd01c5 100644 --- a/backend/FwLite/MiniLcm/JsonPatchExtensions.cs +++ b/backend/FwLite/MiniLcm/JsonPatchExtensions.cs @@ -30,10 +30,7 @@ public static bool TryGetPropertyChange(this UpdateObjectInput upd } return false; } - /// - /// One line, so callers can log a patch without breaking per-line grep. Doesn't name the patched - /// type; callers already do. - /// + /// One line: a multi-line summary breaks per-line grep of the logs it ends up in. public static string Summarize(this JsonPatchDocument document) where T : class { return string.Join(", ", document.Operations.Select(op => $"{op.OperationType} {op.Path}: {op.Value}")); diff --git a/backend/FwLite/MiniLcm/Models/RichString.cs b/backend/FwLite/MiniLcm/Models/RichString.cs index 4f74b822a8..b7c7d18764 100644 --- a/backend/FwLite/MiniLcm/Models/RichString.cs +++ b/backend/FwLite/MiniLcm/Models/RichString.cs @@ -46,8 +46,6 @@ public string GetPlainText() return string.Join("", Spans.Select(s => s.Text)); } - // Without this, RichMultiString.ToString() prints the type name for every value, which silently - // emptied the dry-run sync records of their example sentence and picture caption text. public override string ToString() => GetPlainText(); public void EnsureWs(WritingSystemId ws)