Add variant model, CRDT changes, FwData bridge and sync (1/3)#2416
Add variant model, CRDT changes, FwData bridge and sync (1/3)#2416myieye wants to merge 24 commits into
Conversation
One Variant link = one LexEntryRef (RefType=Variant) with per-link Types, HideMinorEntry and Comment (LCM Summary), per VARIANTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conformance suites for both IMiniLcmApi implementations, change/usage/serialization tests, verified-file regens, AutoFaker support. LCM enforces acyclicity over the combined complex-form + variant graph, so AddVariantChange mirrors that check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerated via ProjectTemplateTests.GenerateTemplate; new CRDT projects get the 7 standard FLEx variant types (well-known guids), matching FieldWorks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Variant lists sort by composite key on both sides (deterministic, culture-free); BulkCreateEntries emits variant links; regenerated Sena3 live db + snapshots — sena-3's real variants (custom Pronunciation Variant type) now round-trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sense-targeted variant link is identified by its full composite key; the FwData bridge matched by sense guid alone, so a moved sense made the sync drop the link (new regression test covers both implementations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…riant entry id Both implementations now reject a MainSenseId that belongs to a different entry (fail before mutation); EntryValidator requires VariantEntryId on Variants items. Also corrects the stale cycle-guard decision text in VARIANTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Cycle checks now share one liblcm-faithful walk (ComponentGraph): sense-targeted links resolve to the owning entry without recursing (LexEntry.AllComponents), and AddEntryComponentChange walks the combined complex-form + variant graph like liblcm instead of complex forms only - Per-link edits split a shared (multi-component) variant ref before mutating so sibling links keep their own Types/HideMinorEntry/Comment - Variant reads skip non-entry/non-sense components instead of wedging the entry - HideMinorEntry only written when the bool view flips (LCM reserves a bitfield) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…itfield Mutation-tested the new cycle/shared-ref guards (3/3 mutants killed); the surviving would-be mutant exposed the missing HideMinorEntry bitfield test. Also docs: fleet-skew rollout note for AddEntryComponentChange, badge and type-seeding follow-ups, SubmitUpdateVariant asymmetry rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- BeaKona forwarders bypassed validation on SubmitCreateVariant and SubmitUpdateVariant; both now validate (same class of gap as #2362) - New VariantUpdateValidator rejects patches to Types (incl. by index, via new DoesNotChangePropertyOrChildren), endpoints, derived headwords, DeletedAt and Id - VariantType and ComplexFormType names now reject empty per-WS values - CreateEntryOptions.IncludeComplexFormsAndComponents renamed to IncludeEntryReferences (it gates variants too); generated TS updated - BoolDiff helper (mirrors IntegerDiff); ToVariants flattened to Concat; FluentAssert Id-exclusion no longer names one type''s members; ProjectSnapshot.VariantTypes legacy-null note sharpened Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FLEx lets users reorder a ref''s VariantEntryTypesRS (right-click, move left/right); syncing types as a set silently lost that order. Adds SetVariantTypesOrder to IMiniLcmWriteApi (both implementations, dry-run, wrappers, notify) and SetVariantTypesOrderChange (unlisted types keep their relative order after listed ones, so concurrent type adds merge). VariantSync resets order after the add/remove diff whenever it cannot have landed in after-order. Variant link lists themselves stay unordered (decision 6, amended). Covered by shared conformance tests (both impls), a CRDT change test incl. the concurrent-add merge, and a full FwData<->CRDT sync round-trip; change-model/regression verified files regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The whole-list SetVariantTypesOrderChange clobbered concurrent
reorders; per Tim''s review, type order now works like sense pictures:
Variant.Types holds VariantTypeRef objects ({typeId, fractional Order},
which also removes the stale-embedded-Name wart from decision 8),
AddVariantType takes a BetweenPosition, MoveVariantType replaces the
list-order API, and ReorderVariantTypeChange merges per type under
concurrency. VariantSync diffs types with DiffOrderable.
Regenerated: change/snapshot regression data (old branch-era shapes
auto-migrated to the legacy files; the never-shipped
SetVariantTypesOrderChange entry removed), DbModel/ChangeModels
snapshots, sena-3-live snapshot, TS types (IVariantTypeRef).
(The field-editor adaptations live in the stacked editor-UI branch.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l-sync # Conflicts: # backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.latest.verified.txt # backend/FwLite/LcmCrdt.Tests/Changes/ChangeDeserializationRegressionData.legacy.verified.txt # backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.latest.verified.txt # backend/FwLite/LcmCrdt.Tests/Data/SnapshotDeserializationRegressionData.legacy.verified.txt # backend/FwLite/LcmCrdt.Tests/DataModelSnapshotTests.VerifyDbModel.verified.txt # backend/FwLite/LcmCrdt/Migrations/LcmCrdtDbContextModelSnapshot.cs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Submit* are the sync's result-less fire-and-forget writes (delete-wins on a concurrently-deleted target). The write-normalization wrapper's un-overridden Submit* silently downgrade that to a throwing update via the interface default body, so add SubmitCreateVariant + SubmitUpdateVariantType overrides (forward to _api.Submit*), plus tests asserting normalization and Times.Never on the returning method. Documents the rule in FwLite AGENTS.md and the interface. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adding a variant type during sync (VariantSync → AddVariantType, and SubmitCreateVariant for a whole link) threw when the type had been concurrently deleted on the other replica, wedging the entire project sync. Both IMiniLcmApi implementations now drop the missing type (delete wins), matching Remove/MoveVariantType and AddVariantChange.NewEntity, which already tolerated it. New conformance tests (both implementations) fail without the fix. Also: exclude Variant.Types[].Order from the CanSyncRandomEntries round-trip comparison — it was the only Order field not excluded, and the write path normalizes it like every other orderable, which was the red CI. And correct stale VARIANTS.md type-ordering docs + soften unverified FLEx-UI comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l-sync # Conflicts: # backend/LfClassicData/LfClassicMiniLcmApi.cs
| /// </summary> | ||
| public class VariantTypeRef : IOrderable | ||
| { | ||
| public required virtual Guid Id { get; set; } |
There was a problem hiding this comment.
we should automate this, but it should be public virtual required
| /// </summary> | ||
| internal void AddVariant(ILexEntry lexVariantEntry, Variant variant) | ||
| { | ||
| if (FindVariantRef(lexVariantEntry, variant) is not null) return; //idempotent, like the CRDT side |
There was a problem hiding this comment.
there should probably be an assert at the top that lexVariantEntry.Guid == variant.VariantEntryId to catch incorrect calls early
| throw new InvalidOperationException($"Sense {variant.MainSenseId} does not belong to entry {variant.MainEntryId}, it belongs to {senseEntryId}"); | ||
| } | ||
| var typeIds = variant.Types.Select(t => t.Id).ToArray(); | ||
| var knownTypeIds = (await repo.VariantTypes.Where(t => typeIds.Contains(t.Id)).Select(t => t.Id).ToArrayAsync()).ToHashSet(); |
There was a problem hiding this comment.
by default there's very few types. Let's just simplify this to drop there Where then we can also drop typeIds
There was a problem hiding this comment.
also, when bulk adding, this querry will be run once for each variant of each entry, we should probably lift it out so it only happens once on bulk insert like we do for a few other things.
| await using var repo = await repoFactory.CreateRepoAsync(); | ||
| var existing = await repo.FindVariant(variant); | ||
| if (existing is null) return; | ||
| // type concurrently deleted on the other replica → drop the add (delete wins), matching Remove/MoveVariantType |
| // type concurrently deleted on the other replica → drop the add (delete wins), matching Remove/MoveVariantType | ||
| var variantType = await repo.VariantTypes.SingleOrDefaultAsync(vt => vt.Id == variantTypeId); | ||
| if (variantType is null) return; | ||
| await AddChange(new AddVariantTypeChange(existing.Id, new VariantTypeRef { Id = variantType.Id }, position)); |
There was a problem hiding this comment.
should you use variantType.ToRef()?
| { | ||
| await using var repo = await repoFactory.CreateRepoAsync(); | ||
| var existing = await repo.FindVariant(variant); | ||
| if (existing is null) return; |
There was a problem hiding this comment.
we should check that type type exists here too
| { | ||
| await using var repo = await repoFactory.CreateRepoAsync(); | ||
| var existing = await repo.FindVariant(variant); | ||
| if (existing is null) return; |
There was a problem hiding this comment.
the type should exist here too
| // wedge the whole project sync. The defaults forward to the returning method (correct for FwData, which still | ||
| // surfaces a genuinely-missing object). | ||
| // | ||
| // A Submit variant is only needed where the sync diff writes to a target that could have been concurrently |
There was a problem hiding this comment.
I don't think this comment is useful, at least not here
| @@ -0,0 +1,414 @@ | |||
| # Variants in FieldWorks Lite — design & work log | |||
There was a problem hiding this comment.
are we going to leave this file in?
hahn-kev
left a comment
There was a problem hiding this comment.
looks really good. I just have some questions and requests.
|
|
||
| var currentFwDataVariantTypes = await fwdataApi.GetVariantTypes().ToArrayAsync(); | ||
| // Legacy snapshots predate variant support and deserialize with an empty list; the CRDT | ||
| // genuinely has no variant types yet, so that baseline correctly imports FwData's list. |
| // Order is resolved from Between against live state at apply time (the ctor/serialized Order is not | ||
| // authoritative), so concurrent adds converge. ReorderVariantTypeChange deliberately differs — it | ||
| // stores a fixed Order — because a reorder targets a specific destination, an add just needs a slot. | ||
| Order = OrderPicker.PickOrder(entity.Types, Between); |
There was a problem hiding this comment.
I don't see a point in having an Order property when we just override it always in apply change. What's it's purpose and why are we storing it in the change if it's not used?
| variant.MainSenseId.Should().BeNull(); | ||
| variant.VariantHeadword.Should().Be("variant form"); | ||
| variant.MainHeadword.Should().Be("main entry"); | ||
| variant.Types.Should().BeEmpty(); |
There was a problem hiding this comment.
hum, by default in FLEx variants have a default type of Unspecified Variant should we match that?
| // LCM rejects circular component references (LexEntryRef.ValidateAddObjectInternal), | ||
| // so both implementations must too | ||
| await Api.CreateVariant(Variant.FromEntries(_variantEntry, _mainEntry)); | ||
| var act = async () => await Api.CreateVariant(Variant.FromEntries(_mainEntry, _variantEntry)); |
There was a problem hiding this comment.
this test and similar only fail on CRDT because create tries to get the variant it just created, and that fails because the cycle check inside the change deletes the variant. I'm not sure this is the correct behavior, or at least I think the test should use a better signal than that. Probably out of scope for this PR.
| var variant = await Api.CreateVariant(Variant.FromEntries(_variantEntry, _mainEntry)); | ||
| await Api.DeleteVariantType(type.Id); | ||
|
|
||
| await Api.AddVariantType(variant, type.Id); |
There was a problem hiding this comment.
no change is actually recorded here since the method returns early when the type doesn't exist anymore. Should we have a dedicated test that directly records a change after the type is deleted to simulate a sync?
| } | ||
|
|
||
| [Fact] | ||
| public async Task CreateVariant_CanTargetMultipleSensesOfSameEntry() |
There was a problem hiding this comment.
we've got code in Crdt that allows us to reference a sense Id which does not exist. Where do we test that?
| var variantType = new VariantType() { Id = Guid.NewGuid(), Name = new() { { "en", "test" } } }; | ||
| await Api.CreateVariantType(variantType); | ||
| var types = await Api.GetVariantTypes().ToArrayAsync(); | ||
| types.Should().ContainSingle(t => t.Id == variantType.Id); |
There was a problem hiding this comment.
need to test the queried type name
| public async Task Create_ChangingProperty_ProducesNewEntityId() | ||
| { | ||
| // When the sync diff detects a property change (e.g. MainEntryId), it does | ||
| // remove + add. The "add" reuses the same input object with the old entity ID. |
There was a problem hiding this comment.
it's not clear, does that mean this test expects that first variant has been deleted? or not.
Adds variant relationships to FwLite — model, CRDT changes, FwData bridge, and FwData↔CRDT sync. Editor UI is a follow-up.
Variantlink maps to one FieldWorksLexEntryRef(RefType=Variant): per-link orderedTypes,HideMinorEntry,Comment.AddEntryComponentChangereplay changes for every complex-form project — see VARIANTS.md (decision 7).backend/FwLite/VARIANTS.md.Tested by conformance suites on both
IMiniLcmApiimplementations, FwData↔CRDT round-trip sync, and a seeded differential oracle against liblcm's cycle rule.