From ab7e529ecfe162b28d19936510a8a12cf38d7a92 Mon Sep 17 00:00:00 2001 From: John Lambert Date: Wed, 20 May 2026 18:24:13 -0400 Subject: [PATCH] Add Lexical Edit Avalonia migration plan and coverage Add FieldWorks Avalonia migration review skills Narrow Phase 1-2 Avalonia migration foundation --- .github/instructions/avalonia.instructions.md | 94 +++++ .../skills/fieldworks-avalonia-ui/SKILL.md | 28 ++ .../fieldworks-managed-netfx-review/SKILL.md | 27 ++ .../SKILL.md | 29 ++ .../SKILL.md | 29 ++ .../fieldworks-uia2-parity-testing/SKILL.md | 25 ++ .../SKILL.md | 28 ++ .../DetailControlsTests/DataTreeTests.cs | 47 +++ .../MorphTypeAtomicLauncherTests.cs | 262 +++++++++++++ .../DetailControlsTests/SliceFactoryTests.cs | 22 ++ .../DetailControls/MorphTypeAtomicLauncher.cs | 148 ++++--- Src/xWorks/xWorksTests/BulkEditBarTests.cs | 73 ++++ .../.openspec.yaml | 2 + .../design.md | 89 +++++ .../proposal.md | 46 +++ .../spec.md | 180 +++++++++ .../tasks.md | 77 ++++ .../.openspec.yaml | 2 + .../architecture-diagrams.md | 370 ++++++++++++++++++ .../avalonia-command-focus.md | 57 +++ .../avalonia-edit-sessions.md | 55 +++ .../avalonia-lifetime.md | 68 ++++ .../avalonia-ui-scheduler.md | 63 +++ .../avalonia-undo-redo.md | 53 +++ .../avalonia-validation.md | 63 +++ .../coverage-map.md | 99 +++++ .../lexical-edit-avalonia-migration/design.md | 221 +++++++++++ .../graphite-decommissioning.md | 68 ++++ .../migration-map.md | 15 + .../override-fixtures.md | 67 ++++ .../phase2-execution-evidence.md | 113 ++++++ .../proposal.md | 57 +++ .../region-manifest.md | 121 ++++++ .../seam-recommendations.md | 153 ++++++++ .../interop/native-boundary/spec.md | 41 ++ .../testing/test-strategy/spec.md | 21 + .../ui-framework/views-rendering/spec.md | 47 +++ .../ui-framework/winforms-patterns/spec.md | 21 + .../specs/avalonia-command-focus/spec.md | 25 ++ .../specs/avalonia-edit-sessions/spec.md | 26 ++ .../specs/avalonia-lifetime/spec.md | 25 ++ .../specs/avalonia-ui-scheduler/spec.md | 25 ++ .../specs/avalonia-undo-redo/spec.md | 26 ++ .../specs/avalonia-validation/spec.md | 25 ++ .../lexical-edit-avalonia-migration/spec.md | 135 +++++++ .../lexical-edit-font-decommissioning/spec.md | 63 +++ .../lexical-edit-parity-automation/spec.md | 83 ++++ .../lexical-edit-view-definition/spec.md | 90 +++++ .../lexical-edit-avalonia-migration/tasks.md | 105 +++++ .../view-inventory.md | 68 ++++ 50 files changed, 3618 insertions(+), 59 deletions(-) create mode 100644 .github/instructions/avalonia.instructions.md create mode 100644 .github/skills/fieldworks-avalonia-ui/SKILL.md create mode 100644 .github/skills/fieldworks-managed-netfx-review/SKILL.md create mode 100644 .github/skills/fieldworks-migration-scope-review/SKILL.md create mode 100644 .github/skills/fieldworks-semantic-render-parity/SKILL.md create mode 100644 .github/skills/fieldworks-uia2-parity-testing/SKILL.md create mode 100644 .github/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md create mode 100644 openspec/changes/fieldworks-avalonia-shell-migration/.openspec.yaml create mode 100644 openspec/changes/fieldworks-avalonia-shell-migration/design.md create mode 100644 openspec/changes/fieldworks-avalonia-shell-migration/proposal.md create mode 100644 openspec/changes/fieldworks-avalonia-shell-migration/specs/fieldworks-avalonia-shell-migration/spec.md create mode 100644 openspec/changes/fieldworks-avalonia-shell-migration/tasks.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/.openspec.yaml create mode 100644 openspec/changes/lexical-edit-avalonia-migration/architecture-diagrams.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-command-focus.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-edit-sessions.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-lifetime.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-ui-scheduler.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-undo-redo.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/avalonia-validation.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/coverage-map.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/design.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/graphite-decommissioning.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/migration-map.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/override-fixtures.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/phase2-execution-evidence.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/proposal.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/region-manifest.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/seam-recommendations.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/architecture/interop/native-boundary/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/architecture/testing/test-strategy/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/views-rendering/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/winforms-patterns/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-command-focus/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-edit-sessions/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-lifetime/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-ui-scheduler/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-undo-redo/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-validation/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-avalonia-migration/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-font-decommissioning/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-parity-automation/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-view-definition/spec.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/tasks.md create mode 100644 openspec/changes/lexical-edit-avalonia-migration/view-inventory.md diff --git a/.github/instructions/avalonia.instructions.md b/.github/instructions/avalonia.instructions.md new file mode 100644 index 0000000000..e6b2590908 --- /dev/null +++ b/.github/instructions/avalonia.instructions.md @@ -0,0 +1,94 @@ +--- +applyTo: "**/*" +name: "avalonia.instructions" +description: "Guidance for FieldWorks Avalonia modules and the shared Preview Host" +--- + +# Avalonia Modules (FieldWorks) + +## Purpose & Scope +- Provide a consistent way to **create, build, test, and preview** Avalonia UI modules in FieldWorks. +- Applies to the Advanced Entry Avalonia work under `specs/010-advanced-entry-view/` and future Avalonia modules. + +## Key Rules + +### Build & test (always use repo scripts) +- Build the repo using the traversal script: + - `./build.ps1` +- Run tests using the repo test runner: + - `./test.ps1` +- Do **not** rely on `dotnet build` for repo-wide builds; FieldWorks build targets include tasks that require full Visual Studio/MSBuild. + +### Project locations & naming +- Feature modules live under `Src//.Avalonia/`. + - Example: `Src/LexText/AdvancedEntry.Avalonia/` +- Shared Avalonia utilities live under `Src/Common/FwAvalonia/`. +- Preview tooling lives under `Src/Common/FwAvaloniaPreviewHost/`. + +### Solution + traversal integration (required) +For every new Avalonia module or tool: +- Add the project(s) to the traversal build so `./build.ps1` and `./test.ps1` naturally cover them: + - `FieldWorks.proj` +- Add the project(s) to the solution so developers can open/build/debug in Visual Studio: + - `FieldWorks.sln` + +### Logging (use FieldWorks diagnostics) +- Module logging must route through the existing FieldWorks diagnostics pipeline (`System.Diagnostics`, `TraceSwitch`, `EnvVarTraceListener`). +- Add a `TraceSwitch` entry for each module/component in the dev diagnostics config: + - `Src/Common/FieldWorks/FieldWorks.Diagnostics.dev.config` + +### Preview Host diagnostics (log file) +- The Preview Host writes startup errors and trace output to a log file next to the executable: + - `Output//FieldWorks.trace.log` (e.g. `Output/Debug/FieldWorks.trace.log`) +- To override the log path, set environment variable `FW_PREVIEW_TRACE_LOG` to a full file path. + +### Preview Host (fast UI iteration) +To preview UI without launching the full FieldWorks app, use the shared Preview Host. + +**How modules opt-in** +- Register the module using an assembly-level attribute: + - `FwPreviewModuleAttribute` in `Src/Common/FwAvalonia/Preview/` +- Provide an optional data provider implementing: + - `IFwPreviewDataProvider` + +**Run the preview** +- Use the agent script (build + run): + - `./scripts/Agent/Run-AvaloniaPreview.ps1 -Module advanced-entry -Data sample` +- Supported `-Data` modes depend on the module’s data provider; the current convention is: + - `empty` (minimal/default DataContext) + - `sample` (representative sample data) + +## Expected Structure (current) + +- Module: + - `Src/LexText/AdvancedEntry.Avalonia/` +- Shared utilities/contracts: + - `Src/Common/FwAvalonia/` + - `Diagnostics/` (logging shim) + - `Preview/` (module registration + data provider contracts) +- Preview host executable: + - `Src/Common/FwAvaloniaPreviewHost/` +- Launcher script: + - `scripts/Agent/Run-AvaloniaPreview.ps1` + +## Examples + +### Build everything (recommended) +```powershell +./build.ps1 +``` + +### Run tests +```powershell +./test.ps1 +``` + +### Preview the Advanced Entry module +```powershell +./scripts/Agent/Run-AvaloniaPreview.ps1 -Module advanced-entry -Data sample +``` + +## Notes & Constraints +- Avalonia modules should remain **detached from LCModel** for preview scenarios (use DTO/view-model sample data) to keep the Preview Host lightweight. +- Keep all user-visible strings localizable (use `.resx` patterns where applicable; do not hardcode translatable UI text). +- Treat any input that crosses managed/native boundaries as untrusted; sanitize and validate per repo security guidance. diff --git a/.github/skills/fieldworks-avalonia-ui/SKILL.md b/.github/skills/fieldworks-avalonia-ui/SKILL.md new file mode 100644 index 0000000000..5322313f86 --- /dev/null +++ b/.github/skills/fieldworks-avalonia-ui/SKILL.md @@ -0,0 +1,28 @@ +--- +name: fieldworks-avalonia-ui +description: Use when creating, reviewing, or fixing Avalonia UI modules in FieldWorks, especially XAML, MVVM, preview-host, localization, accessibility, or net8 Avalonia test changes. +--- + +# FieldWorks Avalonia UI + +## Use This For +- Avalonia XAML, view models, commands, lifetimes, dispatching, and resource/style changes. +- New or changed projects under `Src/**/**/*.Avalonia/`, `Src/Common/FwAvalonia/`, and `Src/Common/FwAvaloniaPreviewHost/`. +- Preview Host module registration, sample data providers, and UI diagnostics. + +## Required Checks +- Use current Avalonia docs for uncertain APIs; do not guess dispatcher, headless, automation, or binding behavior. +- Keep product UI strings localizable; prototype hardcoded strings must be called out as gaps. +- Stable accessibility identity belongs on user-facing controls via Avalonia automation properties. +- UI work should stay in bindings/view models where practical; avoid logic-heavy code-behind. +- Keep module preview data lightweight unless the change explicitly opts into LCModel/project data. +- Preserve repo build/test entry points: `./build.ps1` and `./test.ps1`. + +## Review Red Flags +- A Common project directly references a feature module without an explicit architecture decision. +- Preview-only code is launched from product UI without a feature gate and real-project behavior story. +- Sleep-based or timing-sensitive UI tests. +- Claims of accessibility, localization, IME, or keyboard parity without executable evidence. + +## Handoff +Report exact Avalonia docs consulted, tests run, remaining prototype gaps, and whether the change is product-facing or preview-only. \ No newline at end of file diff --git a/.github/skills/fieldworks-managed-netfx-review/SKILL.md b/.github/skills/fieldworks-managed-netfx-review/SKILL.md new file mode 100644 index 0000000000..a74d3b4447 --- /dev/null +++ b/.github/skills/fieldworks-managed-netfx-review/SKILL.md @@ -0,0 +1,27 @@ +--- +name: fieldworks-managed-netfx-review +description: Use when reviewing or changing FieldWorks managed C# projects that cross .NET Framework 4.8, C# 7.3, SDK-style net8, tests, or project-file boundaries. +--- + +# FieldWorks Managed NetFx Review + +## Compatibility Split +- Legacy product code is .NET Framework 4.8 and C# 7.3 unless a project explicitly targets modern .NET. +- New Avalonia modules may target `net8.0-windows`; do not leak C# 8+ syntax or net8-only APIs into net48 projects. +- Legacy `.csproj` files require explicit source inclusion; SDK-style projects have different defaults. + +## Required Checks +- User-visible strings use `.resx` patterns where product-facing. +- UI and async code marshals to the correct UI thread and does not use sync-over-async. +- Disposable WinForms/GDI/LCModel/test resources are owned and disposed deterministically. +- Test discovery changes must be validated across both net48 and net8 test assemblies. +- Use repo scripts for evidence: `./build.ps1` and `./test.ps1`. + +## Review Red Flags +- Nullable annotations, records, file-scoped namespaces, switch expressions, or `using var` in net48/C# 7.3 projects. +- Broad project/test-runner changes justified only by one local test passing. +- Hardcoded Debug paths or absolute repo assumptions in tests. +- Skipped tests used as evidence of covered behavior. + +## Handoff +Report target frameworks touched, project-file implications, test commands/results, and any remaining compatibility risks. \ No newline at end of file diff --git a/.github/skills/fieldworks-migration-scope-review/SKILL.md b/.github/skills/fieldworks-migration-scope-review/SKILL.md new file mode 100644 index 0000000000..2c18dafefc --- /dev/null +++ b/.github/skills/fieldworks-migration-scope-review/SKILL.md @@ -0,0 +1,29 @@ +--- +name: fieldworks-migration-scope-review +description: Use when reviewing large FieldWorks migration PRs, OpenSpec changes, foundational branches, scope splits, draft PR readiness, or evidence claims. +--- + +# FieldWorks Migration Scope Review + +## Review Posture +Treat foundational migration PRs as architecture and evidence packages. The main question is whether reviewers can trust the scope, claims, and validation boundary. + +## Required Checks +- Compare PR title/body/tasks against the actual diff. +- Classify files as plan/spec, characterization test, infrastructure, prototype, product behavior, or unrelated change. +- Verify checked tasks match evidence language; downgrade claims when evidence says substitute, placeholder, skipped, future, or partial. +- Confirm validation gates are explicit: OpenSpec validation, targeted tests, `./build.ps1`, and `CI: Full local check` when ready. + +## Split Triggers +- Product-visible behavior appears in a planning/test PR. +- Common infrastructure directly depends on the first feature module without an explicit decision. +- Test-runner/build graph changes are mixed with UI migration work. +- Unrelated behavior changes require their own review context. + +## Review Red Flags +- A draft PR is so broad that each reviewer must reverse-engineer intent. +- Evidence is stale after rebase or differs from visible CI state. +- A prototype is wired as if it were a product feature. + +## Handoff +Lead with blockers, then list what to remove, split, reword, or validate before review. \ No newline at end of file diff --git a/.github/skills/fieldworks-semantic-render-parity/SKILL.md b/.github/skills/fieldworks-semantic-render-parity/SKILL.md new file mode 100644 index 0000000000..6bebbe4651 --- /dev/null +++ b/.github/skills/fieldworks-semantic-render-parity/SKILL.md @@ -0,0 +1,29 @@ +--- +name: fieldworks-semantic-render-parity +description: Use when capturing or reviewing FieldWorks semantic snapshots, render baselines, layout parity, failure artifacts, XML view definitions, or Avalonia presentation IR. +--- + +# FieldWorks Semantic Render Parity + +## Snapshot Discipline +Semantic snapshots should preserve behaviorally meaningful identity and omit incidental layout noise. + +## Include +- Stable node ID and source layout/part identity. +- Object/class binding, field/flid binding, editor kind, writing-system metadata, visibility, ghost state, expansion, focus order, localization key, and accessibility identity. +- Unsupported construct diagnostics with enough path context to fix the source layout. + +## Exclude Or Normalize +- Pixel bounds, transient generated names, timestamps, machine paths, culture-dependent ordering, and realized-control counts unless the test explicitly owns them. + +## Render Evidence +- Pixel/render tests need deterministic fixtures, clear thresholds, and failure artifacts that reviewers can inspect. +- A semantic snapshot is not a substitute for visual/render parity when typography, density, wrapping, or native rendering seams are under review. + +## Review Red Flags +- Placeholder metadata is presented as real binding or writing-system parity. +- Snapshot tests update large JSON blobs without a small behavioral explanation. +- Cache invalidation tests depend on sleeps or filesystem timestamp luck. + +## Handoff +State whether evidence is semantic, visual, accessibility, or performance parity, and identify remaining unproven axes. \ No newline at end of file diff --git a/.github/skills/fieldworks-uia2-parity-testing/SKILL.md b/.github/skills/fieldworks-uia2-parity-testing/SKILL.md new file mode 100644 index 0000000000..789d96589d --- /dev/null +++ b/.github/skills/fieldworks-uia2-parity-testing/SKILL.md @@ -0,0 +1,25 @@ +--- +name: fieldworks-uia2-parity-testing +description: Use when designing or reviewing FieldWorks UI automation, UIA2, FlaUI, Appium, WinAppDriver, Avalonia.Headless, accessibility, keyboard, focus, or IME parity tests. +--- + +# FieldWorks UIA2 Parity Testing + +## Lane Separation +- Avalonia.Headless is for fast in-process control, layout, view-model, binding, and input tests. +- UIA2/FlaUI/Appium/WinAppDriver tests require realized desktop windows and validate native accessibility trees, focus, invoke patterns, and product integration. +- Do not call a headless smoke test a UIA2 baseline. + +## Required Evidence +- Stable automation IDs or accessible names for controls under test. +- Explicit coverage of focus movement, invoke/click path, popup/chooser reachability, keyboard shortcuts, and failure artifacts. +- Clear CI lane: headless can run broadly; desktop automation needs an interactive Windows desktop or a configured automation host. + +## Review Red Flags +- “Runs in the background” used for UIA2/Appium without explaining the required desktop/session. +- Tests assert implementation internals instead of user-observable accessibility behavior. +- Automation selectors rely on localized labels when stable IDs are available or required. +- IME coverage is claimed without a real text editor/control surface and input-method evidence. + +## Handoff +Classify each test as headless, native desktop automation, or smoke substitute, and state what parity claim it can and cannot support. \ No newline at end of file diff --git a/.github/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md b/.github/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md new file mode 100644 index 0000000000..3b03dd4c30 --- /dev/null +++ b/.github/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md @@ -0,0 +1,28 @@ +--- +name: fieldworks-winforms-to-avalonia-migration +description: Use when planning, reviewing, or implementing FieldWorks WinForms/xWorks/DataTree/XMLViews migration paths to Avalonia, including seam extraction and parity coverage. +--- + +# FieldWorks WinForms To Avalonia Migration + +## Core Rule +Migrate by proving behavior first, extracting seams second, and introducing Avalonia controls only after legacy behavior has executable parity evidence. + +## Required Baselines +- Entry points: `RecordEditView`, `DataTree`, `SliceFactory`, XMLViews browse/table views, launchers, popup choosers, and command/listener wiring. +- Semantics: object/class binding, flid/field binding, labels, visibility, ghost state, expansion, focus order, writing-system metadata, accessibility identity, and localization keys. +- User workflows: create/edit/save/cancel, chooser OK/cancel, undo/redo, refresh/postponed `PropChanged`, keyboard focus restoration, and disposal/unsubscribe. + +## Architecture Checks +- Keep WinForms Designer-safe code isolated from extracted logic. +- Extract humble objects/services for modal decisions and data-loss classifiers before replacing controls. +- Put an editor registry or adapter boundary in front of legacy `SliceFactory` behavior before mixing legacy and Avalonia editors. +- Treat product command wiring as product behavior, not preview scaffolding. + +## Review Red Flags +- A PR mixes plans, tests, infrastructure, product UI wiring, and unrelated behavior changes. +- Task checkboxes claim UIA2/IME/accessibility/localization parity while evidence says substitute, placeholder, skipped, or future work. +- Avalonia preview data modifies or pretends to modify real project data without a real edit-session contract. + +## Handoff +State what is legacy baseline, what is extracted seam, what is Avalonia prototype, and what remains outside parity. \ No newline at end of file diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs index 0f74fe0003..929f9127e0 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/DataTreeTests.cs @@ -197,6 +197,31 @@ public void TwoStringAttr() Assert.That((m_dtree.Controls[1] as Slice).Label, Is.EqualTo("Bibliography")); } + [Test] + public void CfAndBib_SemanticSliceBaselineCapturesStableBindingsAndFocusOrder() + { + m_dtree.Initialize(Cache, false, m_layouts, m_parts); + m_dtree.ShowObject(m_entry, "CfAndBib", null, m_entry, false); + Assert.That(m_dtree.Controls.Count, Is.EqualTo(2)); + + AssertSemanticSlice( + m_dtree.Controls[0] as Slice, + 0, + "CitationForm", + "CitationForm", + LexEntryTags.kflidCitationForm, + "multistring", + null); + AssertSemanticSlice( + m_dtree.Controls[1] as Slice, + 1, + "Bibliography", + "Bibliography", + LexEntryTags.kflidBibliography, + "multistring", + "ifdata"); + } + /// [Test] public void LabelAbbreviations() @@ -222,6 +247,28 @@ public void LabelAbbreviations() Assert.That(abbr2 == (m_dtree.Controls[2] as Slice).Abbreviation, Is.False); } + private void AssertSemanticSlice( + Slice slice, + int focusOrder, + string label, + string field, + int flid, + string editor, + string visibility) + { + Assert.That(slice, Is.Not.Null, "Expected a realized slice at focus order {0}.", focusOrder); + Assert.That(m_dtree.Controls.IndexOf(slice), Is.EqualTo(focusOrder)); + Assert.That(slice.Label, Is.EqualTo(label)); + Assert.That(slice.Object.Hvo, Is.EqualTo(m_entry.Hvo)); + Assert.That(slice.Object.ClassID, Is.EqualTo(LexEntryTags.kClassId)); + Assert.That(slice.ConfigurationNode.Attributes["field"].Value, Is.EqualTo(field)); + Assert.That(Cache.MetaDataCacheAccessor.GetFieldId2(LexEntryTags.kClassId, field, true), Is.EqualTo(flid)); + Assert.That(slice.ConfigurationNode.Attributes["editor"].Value, Is.EqualTo(editor)); + Assert.That(slice.CallerNode.Attributes["visibility"]?.Value, Is.EqualTo(visibility)); + Assert.That(slice.Expansion, Is.EqualTo(DataTree.TreeItemState.ktisFixed)); + Assert.That(slice.Control.AccessibleName, Is.EqualTo(label)); + } + /// [Test] public void IfDataEmpty() diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs index c8560e9505..52e7c8cb5f 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs @@ -2,7 +2,10 @@ // This software is licensed under the LGPL, version 2.1 or later // (http://www.gnu.org/licenses/lgpl-2.1.html) +using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Windows.Forms; using NUnit.Framework; using SIL.FieldWorks.Common.FwUtils; @@ -161,5 +164,264 @@ public void DoNotRefresh_WithoutRefreshListNeeded_DoesNotRefresh_LT22414_BugDemo "Without RefreshListNeeded, DoNotRefresh=false does not trigger refresh; " + "slices remain stale (bibliography still visible despite no data)."); } + + [Test] + public void DoNotRefresh_ClearedRefreshListNeededBeforeRelease_DoesNotRefresh() + { + m_dtree.Initialize(Cache, false, m_layouts, m_parts); + m_dtree.ShowObject(m_entry, "CfAndBib", null, m_entry, false); + Assert.That(m_dtree.Controls.Count, Is.EqualTo(2)); + + m_dtree.DoNotRefresh = true; + m_entry.Bibliography.SetVernacularDefaultWritingSystem(""); + m_entry.Bibliography.SetAnalysisDefaultWritingSystem(""); + m_dtree.RefreshListNeeded = true; + m_dtree.RefreshListNeeded = false; + + m_dtree.DoNotRefresh = false; + + Assert.That(m_dtree.Controls.Count, Is.EqualTo(2), + "Clearing RefreshListNeeded before releasing DoNotRefresh should cancel the " + + "synchronous rebuild and preserve the current slice tree."); + Assert.That(m_dtree.RefreshListNeeded, Is.False); + } + + [TestCaseSource(nameof(StemLikeMorphTypes))] + public void IsStemType_StemLikeMorphTypes_ReturnsTrue(Guid morphTypeGuid) + { + var morphType = Cache.ServiceLocator.GetInstance().GetObject(morphTypeGuid); + + Assert.That(InvokeIsStemType(morphType), Is.True); + } + + [TestCaseSource(nameof(AffixLikeMorphTypes))] + public void IsStemType_AffixLikeMorphTypes_ReturnsFalse(Guid morphTypeGuid) + { + var morphType = Cache.ServiceLocator.GetInstance().GetObject(morphTypeGuid); + + Assert.That(InvokeIsStemType(morphType), Is.False); + } + + [Test] + public void IsStemType_NullMorphType_ReturnsFalse() + { + Assert.That(InvokeIsStemType(null), Is.False); + } + + [Test] + public void CheckForStemDataLoss_EmptyStemAndNoMorphSyntaxAnalyses_AllowsChangeWithoutPrompt() + { + var stem = Cache.ServiceLocator.GetInstance().Create(); + m_entry.LexemeFormOA = stem; + + Assert.That(InvokeCheckForStemDataLoss(stem, new List()), Is.False); + } + + [Test] + public void CheckForAffixDataLoss_EmptyAffixAndNoMorphSyntaxAnalyses_AllowsChangeWithoutPrompt() + { + var affix = Cache.ServiceLocator.GetInstance().Create(); + m_entry.LexemeFormOA = affix; + + Assert.That(InvokeCheckForAffixDataLoss(affix, new List()), Is.False); + } + + [Test] + public void GetStemDataLossKinds_StemNameAndGrammarInfo_FlagsBoth() + { + var stem = Cache.ServiceLocator.GetInstance().Create(); + m_entry.LexemeFormOA = stem; + var partOfSpeech = CreatePartOfSpeech("phase2-stem-pos"); + stem.StemNameRA = CreateStemName(partOfSpeech, "phase2-stem-name"); + var msa = Cache.ServiceLocator.GetInstance().Create(); + m_entry.MorphoSyntaxAnalysesOC.Add(msa); + msa.InflectionClassRA = CreateInflectionClass(partOfSpeech, "phase2-stem-class"); + + Assert.That( + InvokeGetStemDataLossKinds(stem, new List { msa }), + Is.EqualTo(MorphTypeDataLossKinds.StemName | MorphTypeDataLossKinds.GrammarInfo)); + } + + [Test] + public void GetAffixDataLossKinds_AffixProcessWithInflectionClassAndGrammarInfo_FlagsRuleInflectionClassAndGrammarInfo() + { + var affix = Cache.ServiceLocator.GetInstance().Create(); + m_entry.LexemeFormOA = affix; + var partOfSpeech = CreatePartOfSpeech("phase2-affix-pos"); + affix.InflectionClassesRC.Add(CreateInflectionClass(partOfSpeech, "phase2-affix-class")); + var msa = Cache.ServiceLocator.GetInstance().Create(); + m_entry.MorphoSyntaxAnalysesOC.Add(msa); + msa.AffixCategoryRA = partOfSpeech; + + Assert.That( + InvokeGetAffixDataLossKinds(affix, new List { msa }), + Is.EqualTo( + MorphTypeDataLossKinds.Rule | + MorphTypeDataLossKinds.InflectionClass | + MorphTypeDataLossKinds.GrammarInfo)); + } + + [Test] + public void GetAffixDataLossKinds_AffixAllomorphWithPositionAndMsEnv_FlagsInfixLocationAndGrammarInfo() + { + var affix = Cache.ServiceLocator.GetInstance().Create(); + m_entry.LexemeFormOA = affix; + affix.PositionRS.Add(CreateEnvironment("/ _")); + affix.MsEnvPartOfSpeechRA = CreatePartOfSpeech("phase2-infix-pos"); + + Assert.That( + InvokeGetAffixDataLossKinds(affix, new List()), + Is.EqualTo(MorphTypeDataLossKinds.InfixLocation | MorphTypeDataLossKinds.GrammarInfo)); + } + + [Test] + public void LauncherButtonClick_WithValidObject_ReachesChooserDecisionPath() + { + using (var launcher = new RecordingAtomicReferenceLauncher()) + { + launcher.Initialize(Cache, m_entry, LexEntryTags.kflidMorphoSyntaxAnalyses, "MorphoSyntaxAnalysesOC", "analysis"); + + launcher.InvokeLauncherClickForTest(); + + Assert.That(launcher.ChooserInvocationCount, Is.EqualTo(1)); + Assert.That(launcher.LauncherButton.Name, Is.EqualTo("m_btnLauncher")); + Assert.That(launcher.LauncherButton.Enabled, Is.True); + } + } + + private static IEnumerable StemLikeMorphTypes() + { + yield return new TestCaseData(MoMorphTypeTags.kguidMorphBoundRoot).SetName("IsStemType_BoundRoot_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphBoundStem).SetName("IsStemType_BoundStem_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphClitic).SetName("IsStemType_Clitic_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphDiscontiguousPhrase).SetName("IsStemType_DiscontiguousPhrase_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphEnclitic).SetName("IsStemType_Enclitic_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphParticle).SetName("IsStemType_Particle_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphPhrase).SetName("IsStemType_Phrase_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphProclitic).SetName("IsStemType_Proclitic_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphRoot).SetName("IsStemType_Root_ReturnsTrue"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphStem).SetName("IsStemType_Stem_ReturnsTrue"); + } + + private static IEnumerable AffixLikeMorphTypes() + { + yield return new TestCaseData(MoMorphTypeTags.kguidMorphCircumfix).SetName("IsStemType_Circumfix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphInfix).SetName("IsStemType_Infix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphInfixingInterfix).SetName("IsStemType_InfixingInterfix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphPrefix).SetName("IsStemType_Prefix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphPrefixingInterfix).SetName("IsStemType_PrefixingInterfix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphSimulfix).SetName("IsStemType_Simulfix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphSuffix).SetName("IsStemType_Suffix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphSuffixingInterfix).SetName("IsStemType_SuffixingInterfix_ReturnsFalse"); + yield return new TestCaseData(MoMorphTypeTags.kguidMorphSuprafix).SetName("IsStemType_Suprafix_ReturnsFalse"); + } + + private static bool InvokeIsStemType(IMoMorphType morphType) + { + var launcher = new MorphTypeAtomicLauncher(); + return (bool)GetIsStemTypeMethod().Invoke(launcher, new object[] { morphType }); + } + + private static bool InvokeCheckForStemDataLoss( + IMoStemAllomorph stem, + List morphSyntaxAnalyses) + { + var launcher = new MorphTypeAtomicLauncher(); + return (bool)GetPrivateMethod("CheckForStemDataLoss").Invoke( + launcher, + new object[] { stem, morphSyntaxAnalyses }); + } + + private static bool InvokeCheckForAffixDataLoss( + IMoAffixForm affix, + List morphSyntaxAnalyses) + { + var launcher = new MorphTypeAtomicLauncher(); + return (bool)GetPrivateMethod("CheckForAffixDataLoss").Invoke( + launcher, + new object[] { affix, morphSyntaxAnalyses }); + } + + private static MorphTypeDataLossKinds InvokeGetStemDataLossKinds( + IMoStemAllomorph stem, + List morphSyntaxAnalyses) + { + var launcher = new MorphTypeAtomicLauncher(); + return launcher.GetStemDataLossKinds(stem, morphSyntaxAnalyses); + } + + private static MorphTypeDataLossKinds InvokeGetAffixDataLossKinds( + IMoAffixForm affix, + List morphSyntaxAnalyses) + { + var launcher = new MorphTypeAtomicLauncher(); + return launcher.GetAffixDataLossKinds(affix, morphSyntaxAnalyses); + } + + private IPartOfSpeech CreatePartOfSpeech(string name) + { + var partOfSpeech = Cache.ServiceLocator.GetInstance().Create(); + Cache.LangProject.PartsOfSpeechOA.PossibilitiesOS.Add(partOfSpeech); + partOfSpeech.Name.SetAnalysisDefaultWritingSystem(name); + partOfSpeech.Abbreviation.SetAnalysisDefaultWritingSystem(name); + return partOfSpeech; + } + + private IMoStemName CreateStemName(IPartOfSpeech partOfSpeech, string name) + { + var stemName = Cache.ServiceLocator.GetInstance().Create(); + partOfSpeech.StemNamesOC.Add(stemName); + stemName.Name.SetAnalysisDefaultWritingSystem(name); + stemName.Abbreviation.SetAnalysisDefaultWritingSystem(name); + return stemName; + } + + private IMoInflClass CreateInflectionClass(IPartOfSpeech partOfSpeech, string name) + { + var inflClass = Cache.ServiceLocator.GetInstance().Create(); + partOfSpeech.InflectionClassesOC.Add(inflClass); + inflClass.Name.SetAnalysisDefaultWritingSystem(name); + inflClass.Abbreviation.SetAnalysisDefaultWritingSystem(name); + return inflClass; + } + + private IPhEnvironment CreateEnvironment(string representation) + { + var environment = Cache.ServiceLocator.GetInstance().Create(); + Cache.LanguageProject.PhonologicalDataOA.EnvironmentsOS.Add(environment); + environment.StringRepresentation = TsStringUtils.MakeString(representation, Cache.DefaultVernWs); + return environment; + } + + private static MethodInfo GetIsStemTypeMethod() + { + var method = GetPrivateMethod("IsStemType"); + Assert.That(method, Is.Not.Null, "Morph type swap extraction depends on IsStemType semantics."); + return method; + } + + private static MethodInfo GetPrivateMethod(string methodName) + { + var method = typeof(MorphTypeAtomicLauncher).GetMethod( + methodName, + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(method, Is.Not.Null, "Morph type swap extraction depends on {0} semantics.", methodName); + return method; + } + + private sealed class RecordingAtomicReferenceLauncher : MockAtomicReferenceLauncher + { + public int ChooserInvocationCount { get; private set; } + + public void InvokeLauncherClickForTest() + { + OnClick(LauncherButton, EventArgs.Empty); + } + + protected override void HandleChooser() + { + ChooserInvocationCount++; + } + } } } diff --git a/Src/Common/Controls/DetailControls/DetailControlsTests/SliceFactoryTests.cs b/Src/Common/Controls/DetailControls/DetailControlsTests/SliceFactoryTests.cs index 4fbc577334..7c6c613209 100644 --- a/Src/Common/Controls/DetailControls/DetailControlsTests/SliceFactoryTests.cs +++ b/Src/Common/Controls/DetailControls/DetailControlsTests/SliceFactoryTests.cs @@ -49,6 +49,28 @@ public void SetConfigurationDisplayPropertyIfNeeded_Works() AssertThatXmlIn.String(configurationNode.OuterXml).HasSpecifiedNumberOfMatchesForXpath("/slice/deParams[@displayProperty]", 1); } + [Test] + public void Create_UnknownEditor_ReturnsMessageSliceWithEditorAccessibleName() + { + XmlNode configurationNode = DetailControls.SliceTests.CreateXmlElementFromOuterXmlOf(""); + ICmObject cmObject = new CmObjectStub(); + + Slice slice = SliceFactory.Create( + Cache, + "unknown-phase3-editor", + 0, + configurationNode, + cmObject, + null, + null, + null, + configurationNode, + new ObjSeqHashMap()); + + Assert.That(slice, Is.TypeOf()); + Assert.That(slice.AccessibleName, Is.EqualTo("unknown-phase3-editor")); + } + class FdoServiceLocatorStub : ILcmServiceLocator { ICmPossibility m_returnObject; diff --git a/Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs b/Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs index d8e8c9b7ae..476394a3d9 100644 --- a/Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs +++ b/Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs @@ -16,6 +16,17 @@ namespace SIL.FieldWorks.Common.Framework.DetailControls { + [Flags] + internal enum MorphTypeDataLossKinds + { + None = 0, + InflectionClass = 1, + InfixLocation = 2, + GrammarInfo = 4, + Rule = 8, + StemName = 16, + } + public class MorphTypeAtomicLauncher : PossibilityAtomicReferenceLauncher { private const string m_ksPath = "/group[@id='DialogStrings']/"; @@ -225,24 +236,66 @@ private bool ChangeAffixToStem(ILexEntry entry, IMoMorphType type) private bool CheckForAffixDataLoss(IMoAffixForm affix, List rgmsaAffix) { - bool fLoseInflCls = affix.InflectionClassesRC.Count > 0; - bool fLoseInfixLoc = false; - bool fLoseGramInfo = false; - bool fLoseRule = false; + var dataLossKinds = GetAffixDataLossKinds(affix, rgmsaAffix); + bool fLoseInflCls = (dataLossKinds & MorphTypeDataLossKinds.InflectionClass) != 0; + bool fLoseInfixLoc = (dataLossKinds & MorphTypeDataLossKinds.InfixLocation) != 0; + bool fLoseGramInfo = (dataLossKinds & MorphTypeDataLossKinds.GrammarInfo) != 0; + bool fLoseRule = (dataLossKinds & MorphTypeDataLossKinds.Rule) != 0; + if (dataLossKinds != MorphTypeDataLossKinds.None) + { + string sMsg; + if (fLoseInflCls && fLoseInfixLoc && fLoseGramInfo) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsInfixLocGramInfo", m_ksPath); + else if (fLoseRule && fLoseInflCls && fLoseGramInfo) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRuleInflClsGramInfo", m_ksPath); + else if (fLoseInflCls && fLoseInfixLoc) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsInfixLoc", m_ksPath); + else if (fLoseInflCls && fLoseGramInfo) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsGramInfo", m_ksPath); + else if (fLoseInfixLoc && fLoseGramInfo) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInfixLocGramInfo", m_ksPath); + else if (fLoseRule && fLoseInflCls) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRuleInflCls", m_ksPath); + else if (fLoseRule) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRule", m_ksPath); + else if (fLoseInflCls) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflCls", m_ksPath); + else if (fLoseInfixLoc) + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInfixLoc", m_ksPath); + else + sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseGramInfo", m_ksPath); + string sCaption = StringTable.Table.GetStringWithXPath("ChangeLexemeMorphTypeCaption", m_ksPath); + DialogResult result = MessageBox.Show(sMsg, sCaption, + MessageBoxButtons.YesNo, MessageBoxIcon.Warning); + if (result == DialogResult.No) + { + return true; + } + } + return false; + } + + internal MorphTypeDataLossKinds GetAffixDataLossKinds(IMoAffixForm affix, IList rgmsaAffix) + { + var dataLossKinds = MorphTypeDataLossKinds.None; + if (affix.InflectionClassesRC.Count > 0) + dataLossKinds |= MorphTypeDataLossKinds.InflectionClass; switch (affix.ClassID) { case MoAffixProcessTags.kClassId: - fLoseRule = true; + dataLossKinds |= MorphTypeDataLossKinds.Rule; break; case MoAffixAllomorphTags.kClassId: var allo = (IMoAffixAllomorph) affix; - fLoseInfixLoc = allo.PositionRS.Count > 0; - fLoseGramInfo = allo.MsEnvPartOfSpeechRA != null || allo.MsEnvFeaturesOA != null; + if (allo.PositionRS.Count > 0) + dataLossKinds |= MorphTypeDataLossKinds.InfixLocation; + if (allo.MsEnvPartOfSpeechRA != null || allo.MsEnvFeaturesOA != null) + dataLossKinds |= MorphTypeDataLossKinds.GrammarInfo; break; } - for (int i = 0; !fLoseGramInfo && i < rgmsaAffix.Count; ++i) + for (int i = 0; (dataLossKinds & MorphTypeDataLossKinds.GrammarInfo) == 0 && i < rgmsaAffix.Count; ++i) { var msaInfl = rgmsaAffix[i] as IMoInflAffMsa; if (msaInfl != null) @@ -252,7 +305,7 @@ private bool CheckForAffixDataLoss(IMoAffixForm affix, List msaInfl.SlotsRC.Count > 0 || msaInfl.InflFeatsOA != null) { - fLoseGramInfo = true; + dataLossKinds |= MorphTypeDataLossKinds.GrammarInfo; } continue; } @@ -270,7 +323,7 @@ private bool CheckForAffixDataLoss(IMoAffixForm affix, List msaDeriv.FromMsFeaturesOA != null || msaDeriv.ToMsFeaturesOA != null) { - fLoseGramInfo = true; + dataLossKinds |= MorphTypeDataLossKinds.GrammarInfo; } continue; } @@ -282,42 +335,11 @@ private bool CheckForAffixDataLoss(IMoAffixForm affix, List msaStep.InflFeatsOA != null || msaStep.MsFeaturesOA != null) { - fLoseGramInfo = true; + dataLossKinds |= MorphTypeDataLossKinds.GrammarInfo; } } } - if (fLoseInflCls || fLoseInfixLoc || fLoseGramInfo || fLoseRule) - { - string sMsg; - if (fLoseInflCls && fLoseInfixLoc && fLoseGramInfo) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsInfixLocGramInfo", m_ksPath); - else if (fLoseRule && fLoseInflCls && fLoseGramInfo) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRuleInflClsGramInfo", m_ksPath); - else if (fLoseInflCls && fLoseInfixLoc) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsInfixLoc", m_ksPath); - else if (fLoseInflCls && fLoseGramInfo) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflClsGramInfo", m_ksPath); - else if (fLoseInfixLoc && fLoseGramInfo) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInfixLocGramInfo", m_ksPath); - else if (fLoseRule && fLoseInflCls) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRuleInflCls", m_ksPath); - else if (fLoseRule) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseRule", m_ksPath); - else if (fLoseInflCls) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInflCls", m_ksPath); - else if (fLoseInfixLoc) - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseInfixLoc", m_ksPath); - else - sMsg = StringTable.Table.GetStringWithXPath("ChangeMorphTypeLoseGramInfo", m_ksPath); - string sCaption = StringTable.Table.GetStringWithXPath("ChangeLexemeMorphTypeCaption", m_ksPath); - DialogResult result = MessageBox.Show(sMsg, sCaption, - MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (result == DialogResult.No) - { - return true; - } - } - return false; + return dataLossKinds; } private bool ChangeStemToAffix(ILexEntry entry, IMoMorphType type) @@ -344,22 +366,9 @@ private bool ChangeStemToAffix(ILexEntry entry, IMoMorphType type) private bool CheckForStemDataLoss(IMoStemAllomorph stem, List rgmsaStem) { - bool fLoseStemName = stem.StemNameRA != null; - bool fLoseGramInfo = false; - for (int i = 0; i < rgmsaStem.Count; ++i) - { - var msa = rgmsaStem[i] as IMoStemMsa; - if (msa != null && - (msa.FromPartsOfSpeechRC.Count > 0 || - msa.InflectionClassRA != null || - msa.ProdRestrictRC.Count > 0 || - msa.StratumRA != null || - msa.MsFeaturesOA != null)) - { - fLoseGramInfo = true; - break; - } - } + var dataLossKinds = GetStemDataLossKinds(stem, rgmsaStem); + bool fLoseStemName = (dataLossKinds & MorphTypeDataLossKinds.StemName) != 0; + bool fLoseGramInfo = (dataLossKinds & MorphTypeDataLossKinds.GrammarInfo) != 0; if (fLoseStemName || fLoseGramInfo) { string sMsg; @@ -380,6 +389,27 @@ private bool CheckForStemDataLoss(IMoStemAllomorph stem, List rgmsaStem) + { + var dataLossKinds = stem.StemNameRA != null + ? MorphTypeDataLossKinds.StemName + : MorphTypeDataLossKinds.None; + for (int i = 0; (dataLossKinds & MorphTypeDataLossKinds.GrammarInfo) == 0 && i < rgmsaStem.Count; ++i) + { + var msa = rgmsaStem[i] as IMoStemMsa; + if (msa != null && + (msa.FromPartsOfSpeechRC.Count > 0 || + msa.InflectionClassRA != null || + msa.ProdRestrictRC.Count > 0 || + msa.StratumRA != null || + msa.MsFeaturesOA != null)) + { + dataLossKinds |= MorphTypeDataLossKinds.GrammarInfo; + } + } + return dataLossKinds; + } + internal void SwapValues(ILexEntry entry, IMoForm origForm, IMoForm newForm, IMoMorphType type, List rgmsaOld) { diff --git a/Src/xWorks/xWorksTests/BulkEditBarTests.cs b/Src/xWorks/xWorksTests/BulkEditBarTests.cs index de1ae76dbd..866fbb7e34 100644 --- a/Src/xWorks/xWorksTests/BulkEditBarTests.cs +++ b/Src/xWorks/xWorksTests/BulkEditBarTests.cs @@ -401,6 +401,28 @@ internal FilterSortItem SetFilter(string columnName, string filterType, string q return fsiTarget; } + internal IReadOnlyList GetFilterReachabilityBaseline() + { + return m_filterBar.ColumnInfo.Select((fsi, index) => new FilterReachabilityRow( + index, + GetColumnLabel(fsi.Spec), + fsi.Combo.Name, + fsi.Combo.Enabled, + fsi.Combo.IsDisposed, + fsi.Combo.FindStringExact("Show All") >= 0, + fsi.Combo.FindStringExact("Filter for...") >= 0, + fsi.Combo.FindStringExact("Choose...") >= 0)).ToList(); + } + + private static string GetColumnLabel(XmlNode spec) + { + var header = spec.Attributes["headerlabel"] != null ? spec.Attributes["headerlabel"].Value : null; + if (!string.IsNullOrEmpty(header)) + return header; + + return spec.Attributes["label"] != null ? spec.Attributes["label"].Value : string.Empty; + } + private FilterSortItem FindColumnInfo(string columnName) { FilterSortItem fsiTarget = null; @@ -472,6 +494,38 @@ internal IList UncheckedItems() return uncheckedItems; } + + internal sealed class FilterReachabilityRow + { + internal FilterReachabilityRow( + int focusOrder, + string headerLabel, + string comboName, + bool comboEnabled, + bool comboDisposed, + bool hasShowAll, + bool hasFilterFor, + bool hasChoose) + { + FocusOrder = focusOrder; + HeaderLabel = headerLabel; + ComboName = comboName; + ComboEnabled = comboEnabled; + ComboDisposed = comboDisposed; + HasShowAll = hasShowAll; + HasFilterFor = hasFilterFor; + HasChoose = hasChoose; + } + + internal int FocusOrder { get; } + internal string HeaderLabel { get; } + internal string ComboName { get; } + internal bool ComboEnabled { get; } + internal bool ComboDisposed { get; } + internal bool HasShowAll { get; } + internal bool HasFilterFor { get; } + internal bool HasChoose { get; } + } } protected class RecordBrowseViewForTests : RecordBrowseView @@ -500,6 +554,25 @@ protected override void PersistSortSequence() public class BulkEditBarTests : BulkEditBarTestsBase { #region BulkEditEntries tests + [Test] + public void FilterBar_HeaderAndFilterControlsExposeReachableBaseline() + { + var baseline = m_bv.GetFilterReachabilityBaseline(); + var lexemeForm = baseline.Single(row => row.HeaderLabel == "Lexeme Form"); + var morphType = baseline.Single(row => row.HeaderLabel == "Morph Type"); + + Assert.That(lexemeForm.FocusOrder, Is.LessThan(morphType.FocusOrder)); + Assert.That(lexemeForm.ComboEnabled, Is.True); + Assert.That(lexemeForm.ComboDisposed, Is.False); + Assert.That(lexemeForm.HasShowAll, Is.True); + Assert.That(lexemeForm.HasFilterFor, Is.True); + + Assert.That(morphType.ComboEnabled, Is.True); + Assert.That(morphType.ComboDisposed, Is.False); + Assert.That(morphType.HasShowAll, Is.True); + Assert.That(morphType.HasChoose, Is.True); + } + [Test] public void ChoiceFilters() { diff --git a/openspec/changes/fieldworks-avalonia-shell-migration/.openspec.yaml b/openspec/changes/fieldworks-avalonia-shell-migration/.openspec.yaml new file mode 100644 index 0000000000..93831bd262 --- /dev/null +++ b/openspec/changes/fieldworks-avalonia-shell-migration/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-13 diff --git a/openspec/changes/fieldworks-avalonia-shell-migration/design.md b/openspec/changes/fieldworks-avalonia-shell-migration/design.md new file mode 100644 index 0000000000..081ff902be --- /dev/null +++ b/openspec/changes/fieldworks-avalonia-shell-migration/design.md @@ -0,0 +1,89 @@ +## Context + +FieldWorks currently starts as a WinForms/xWorks application. Startup, project selection, main-window construction, command routing, property-table state, menus, toolbars, sidebars, status panes, dialogs, and dynamic content hosting are tied to WinForms/XCore concepts. Lexical Edit Avalonia work creates a migrated region, but a fully Avalonia product requires a later shell/windowing migration that replaces the default application host and all main screens without preserving WinForms as the hidden runtime shell. + +The architecture review identified several constraints: + +- `FwApp`, `FieldWorksManager`, xWorks windows, and many dialogs assume `System.Windows.Forms.Form` ownership. +- XCore mediator/property-table behavior is both command bus and UI composition state. +- Shell composition is driven by XML configuration such as `Main.xml`, including commands, lists, menus, sidebars, toolbar items, listeners, defaults, extension includes, and localization metadata. +- Main screens outside Lexical Edit may still depend on WinForms controls, XMLViews, RootSite/native Views, Gecko, or native rendering. +- Avalonia brings different application lifetime, dispatcher, command, focus, validation, accessibility, styling, and window ownership patterns. +- Retained custom linguistics services can stay only as non-UI service dependencies. + +## Goals / Non-Goals + +**Goals:** + +- Make Avalonia the default FieldWorks application lifetime, shell, windowing system, and main-screen host. +- Preserve command IDs, shortcuts, menus, navigation semantics, status/progress behavior, localization, extension hooks, project startup behavior, and multi-window behavior. +- Keep XCore/Mediator compatibility during migration while moving UI composition to typed shell definitions and Avalonia services. +- Host migrated screens through registered Avalonia views and explicit view models/presenters. +- Keep retained native/external services outside the Avalonia display/layout/input boundary. + +**Non-Goals:** + +- No LCModel rewrite or storage schema change. +- No requirement that every feature surface migrate in the first shell skeleton. +- No permanent WinForms or native UI island in the final default app. +- No Graphite or Gecko Graphite default-path compatibility layer. + +## Decisions + +### 1. Phase two depends on regional Avalonia readiness + +The shell migration starts after Lexical Edit has proven the critical regional patterns: typed view definitions, parity automation, Graphite-free default path, edit sessions, and no native viewing/rendering inside migrated regions. The shell must not become a container for unresolved native Views or Graphite dependencies. + +### 2. Application lifetime and windowing use framework-neutral ports first + +Introduce interfaces for desktop lifetime, main window, active-window registry, dialog owner, UI dispatcher, shutdown, and modal state while WinForms remains default. Avalonia then implements those ports using classic desktop lifetime and explicit window ownership. + +### 3. Shell XML becomes typed shell definition during transition + +Existing XCore shell XML remains a migration input, not the final runtime composition model. A typed shell definition captures commands, menus, toolbars, sidebars, status panes, areas/tools, includes, listeners, shortcuts, icons, defaults, and localization metadata with diagnostics for unsupported constructs. + +### 4. Command routing is bridged before it is replaced + +XCore mediator/property-table remains a compatibility command and state bridge during migration. Avalonia commands expose stable IDs, labels, gestures, icons, visibility, enabled state, target resolution, and diagnostics independent of WinForms menu/toolstrip adapters. + +### 5. Main screens migrate by registry and manifest + +An Avalonia screen registry maps area/tool IDs to presenters and views. Each migrated screen has a manifest covering entry points, shell commands, state, accessibility, performance, legacy adapters, native boundary status, and rollback/default-switch behavior. + +### 6. Shell services are testable outside the full app + +Navigation, commands, dialog ownership, status/progress, settings persistence, accessibility metadata, and shell composition are tested through pure services, typed snapshots, and Avalonia.Headless before full-app smoke tests. + +### 7. The shell phase consumes earlier seam capabilities instead of redefining them + +The shell phase consumes the previously chosen seam capabilities from `lexical-edit-avalonia-migration` rather than reopening those decisions by default. In particular, `avalonia-command-focus` is promoted from screen-local usage to shell-global command and target routing here, while `avalonia-ui-scheduler` and `avalonia-lifetime` are promoted from local editor seams to application-wide services. If those choices later prove wrong, the pivot triggers in `lexical-edit-avalonia-migration/seam-recommendations.md` govern when to change direction. + +## Risks / Trade-offs + +- Runtime split between .NET Framework managed code and newer Avalonia projects -> Resolve host/runtime strategy early and avoid hidden cross-runtime assumptions. +- XCore extension behavior may depend on WinForms adapters -> Preserve command IDs and add typed diagnostics for unsupported UI constructs. +- WinForms dialog ownership is widespread -> Introduce dialog-owner contracts and migrate high-frequency dialogs before default switch. +- Main screens outside Lexical Edit may still use native Views or Gecko -> Keep explicit legacy boundaries and block default-path completion until each screen manifest passes. +- UI automation can become flaky -> Push deep behavior into service/snapshot tests and reserve UI automation for shell smoke, accessibility, and platform behavior. +- Browser/PDF/print scope may exceed shell work -> Treat browser/PDF as replaceable global services with their own decision gates. + +## Migration Plan + +1. Confirm Lexical Edit regional gates or track unresolved blockers explicitly. +2. Inventory current shell entry points, XML composition, command IDs, dialogs, main screens, startup/shutdown paths, native/WinForms dependencies, and browser/PDF paths. +3. Extract framework-neutral shell/lifetime/dialog/dispatcher/command/navigation/status/settings/accessibility ports while WinForms remains default, consuming `avalonia-ui-scheduler` and `avalonia-lifetime` rather than redefining them. +4. Build typed shell-definition importer and snapshot tests for `Main.xml` and area/tool includes. +5. Build an Avalonia shell preview path with sample data and migrated regions. +6. Bridge XCore commands and property state into typed Avalonia command/state services, consuming the shell-global phase of `avalonia-command-focus`. +7. Implement Avalonia navigation, content host, menus, context menus, toolbars, side panes, status/progress, and dialog service. +8. Migrate main screens area by area using screen manifests and legacy-host boundaries only for non-migrated screens. +9. Add startup/shutdown, installer/runtime, accessibility, localization, performance, and full-app smoke gates. +10. Make Avalonia shell default only after hard gates pass; then retire WinForms shell and default-path adapters. + +## Open Questions + +1. What runtime target is required for the final application host, and what bridge is needed for remaining .NET Framework projects? +2. Which shell XML extension points must remain supported for partner or add-on workflows? +3. Which docking/layout behavior requires owned controls versus a third-party Avalonia docking library? +4. Which browser/PDF engine replaces Gecko-backed preview, print, and PDF workflows? +5. Which main screens outside Lexical Edit block the first Avalonia shell default switch? \ No newline at end of file diff --git a/openspec/changes/fieldworks-avalonia-shell-migration/proposal.md b/openspec/changes/fieldworks-avalonia-shell-migration/proposal.md new file mode 100644 index 0000000000..75f2fb510b --- /dev/null +++ b/openspec/changes/fieldworks-avalonia-shell-migration/proposal.md @@ -0,0 +1,46 @@ +## Why + +The Lexical Edit Avalonia migration proves the first high-risk regional replacement, but FieldWorks will still start and compose its application through a WinForms/XCore shell. A second phase is needed to replace application lifetime, windowing, navigation, menus, dialogs, and main-screen hosting with Avalonia so the final default product is a fully Avalonia app rather than an Avalonia island inside the old shell. + +## What Changes + +- Add an Avalonia desktop shell using explicit application lifetime, main-window ownership, active-window tracking, dialog ownership, and shutdown services. +- Extract framework-neutral shell/window contracts so `FwApp`, `FieldWorksManager`, xWorks windows, project startup, multi-window behavior, and modal ownership no longer require `System.Windows.Forms.Form` in the default path. +- Compile/import existing shell configuration such as `Language Explorer/Configuration/Main.xml` into typed shell definitions for commands, lists, menus, context menus, sidebars, toolbars, status panes, listeners, extension includes, shortcuts, localization metadata, and screen/tool registrations. +- Introduce Avalonia shell composition for navigation, content hosting, record/side panes, menus, context menus, toolbars, status/progress, diagnostics, accessibility, and theme resources. +- Bridge XCore mediator/property-table behavior into typed Avalonia commands and state services during migration, then retire WinForms UI adapters from the default path. +- Add an Avalonia main-screen registry and migrate screens area by area after Lexical Edit gates are proven. +- Move global dialogs and services behind abstractions: project open/create/backup/restore, writing systems, settings, import/export, find/replace, styles, help, feedback, progress, keyboarding, clipboard, browser/PDF, print, and accessibility. +- Retire WinForms shell, WinForms dynamic content host, WinForms-only default dialogs, FlexUIAdapter default behavior, Gecko Graphite assumptions, and native viewing/rendering from the final default app. + +## Non-goals + +- No LCModel rewrite or project data schema change. +- No one-shot migration of every screen before shell seams and parity gates exist. +- No permanent WinForms embedding in the final default app. +- No removal of native/external linguistics services such as XAmple, spelling, ICU, Encoding Converters, or parser tools when isolated behind non-UI service contracts. +- No Graphite or Gecko Graphite compatibility path in the final Avalonia default UI. + +## Capabilities + +### New Capability + +- `fieldworks-avalonia-shell-migration`: Avalonia default shell, typed shell composition, application/window lifetime, command bridge, main-screen registry, validation gates, packaging, and final WinForms shell decommissioning. + +### Architecture Areas Covered + +- `architecture/layers/entry-points`: Avalonia host and dual-lifetime transition. +- `architecture/ui-framework/xcore-mediator`: Mediator compatibility bridge and final composition boundary. +- `architecture/ui-framework/winforms-patterns`: WinForms shell decommissioning gates and temporary adapter rules. +- `architecture/testing/test-strategy`: Shell contract snapshots, Avalonia.Headless shell tests, UIA baselines, and full-app smoke gates. +- `architecture/interop/native-boundary`: Retained native services outside Avalonia UI boundaries; no native viewing/rendering in the default shell. +- `architecture/build-deploy/installer`: Avalonia runtime/package harvest and WinForms/Gecko retirement gates. +- `architecture/build-deploy/localization`: Shell labels, shortcuts, tooltips, status text, and dialogs preserved through typed shell migration. + +## Impact + +- Managed shell/framework code: `Src/Common/FieldWorks/`, `Src/Common/Framework/`, `Src/XCore/`, `Src/xWorks/`, and main FLEx screens under `Src/LexText/`. +- Avalonia code: `Src/Common/FwAvalonia/`, `Src/Common/FwAvaloniaPreviewHost/`, and future FieldWorks Avalonia shell projects. +- Configuration/localization: `DistFiles/Language Explorer/Configuration/Main.xml`, area/tool XML includes, existing localization resources, and Crowdin integration. +- Native/interop: no new native UI surface; retained native/external services remain behind service boundaries; native Views/Graphite/Gecko Graphite are excluded from the default Avalonia UI. +- Build/deploy: traversal build, solution integration, installer/runtime packaging, app startup path, and dependency harvest. \ No newline at end of file diff --git a/openspec/changes/fieldworks-avalonia-shell-migration/specs/fieldworks-avalonia-shell-migration/spec.md b/openspec/changes/fieldworks-avalonia-shell-migration/specs/fieldworks-avalonia-shell-migration/spec.md new file mode 100644 index 0000000000..5047a41b16 --- /dev/null +++ b/openspec/changes/fieldworks-avalonia-shell-migration/specs/fieldworks-avalonia-shell-migration/spec.md @@ -0,0 +1,180 @@ +## ADDED Requirements + +### Requirement: FieldWorks provides an Avalonia default shell + +FieldWorks SHALL provide an Avalonia desktop shell as the final default application shell for migrated workflows. + +#### Scenario: Avalonia shell owns default chrome +- **WHEN** FieldWorks runs in the final default mode +- **THEN** the top-level shell, navigation, content host, menus, toolbars, status panes, dialogs, and application chrome SHALL be Avalonia-owned +- **AND** WinForms shell controls SHALL NOT be created for the default path + +### Requirement: Shell preserves FieldWorks workflow semantics + +The Avalonia shell SHALL preserve project startup, area/tool navigation, command IDs, shortcuts, menu semantics, status/progress behavior, localization, accessibility, and multi-window behavior from the legacy shell. + +#### Scenario: Project opens to equivalent workspace +- **WHEN** a user opens a project in the Avalonia shell +- **THEN** the shell SHALL initialize project context, area/tool selection, command state, status panes, and main content equivalent to the legacy baseline for covered workflows + +### Requirement: Shell XML imports into typed shell definition + +Existing shell configuration XML SHALL import into a typed shell definition during migration. + +#### Scenario: Main XML imports deterministically +- **WHEN** `Language Explorer/Configuration/Main.xml` and area/tool includes are imported +- **THEN** the typed shell definition SHALL include commands, lists, areas, tools, menus, context menus, sidebars, toolbars, status panes, listeners, defaults, extension includes, shortcuts, icons, and localization metadata + +### Requirement: Unsupported shell constructs are diagnostic + +Unsupported shell XML constructs SHALL produce deterministic diagnostics instead of silent omission. + +#### Scenario: Unsupported dynamic loader is reported +- **WHEN** shell import encounters a dynamic loader, listener, toolbar widget, or status panel that has no Avalonia equivalent +- **THEN** the importer SHALL report the XML path, command/tool identifier when available, migration severity, and required follow-up + +### Requirement: Typed shell definition is the runtime target + +The final Avalonia shell SHALL use typed shell definitions as its runtime composition contract, with XML retained only for migration/import/audit scenarios. + +#### Scenario: Runtime composition avoids raw XML +- **WHEN** a shell area has passed migration gates +- **THEN** the Avalonia shell SHALL compose commands, navigation, menus, toolbars, panes, and status regions from the typed definition rather than parsing runtime XML + +### Requirement: Windowing uses framework-neutral lifetime services + +Application startup, shutdown, active-window tracking, modal ownership, and UI dispatch SHALL use framework-neutral interfaces during migration. + +#### Scenario: WinForms and Avalonia lifetimes share contract +- **WHEN** shell lifetime behavior is tested +- **THEN** both WinForms compatibility and Avalonia implementations SHALL satisfy the same app lifetime, active-window, dialog-owner, dispatcher, and shutdown contracts + +### Requirement: Avalonia implementation owns final desktop lifetime + +The final default shell SHALL use Avalonia desktop lifetime and explicit top-level window ownership. + +#### Scenario: Default startup creates Avalonia main window +- **WHEN** FieldWorks starts in final default mode +- **THEN** the app SHALL create Avalonia top-level windows through the shell lifetime service +- **AND** it SHALL NOT require WinForms `Application.Run` or `Form` ownership for default shell windows + +### Requirement: Shutdown and disposal are deterministic + +The shell SHALL deterministically dispose windows, dialogs, project services, cache-bound services, background tasks, and retained native service handles. + +#### Scenario: Project shutdown releases shell resources +- **WHEN** a project window closes or the application exits +- **THEN** shell tests SHALL prove windows, dialogs, event subscriptions, cache-bound services, and background tasks are released or canceled + +### Requirement: Commands are represented by typed descriptors + +Shell commands SHALL expose stable IDs, labels, localized resources, gestures, icons, visibility, enabled state, checked state when applicable, execution targets, and diagnostics independent of WinForms menu/toolstrip adapters. + +#### Scenario: Command descriptor drives menu item +- **WHEN** a typed command appears in an Avalonia menu or toolbar +- **THEN** its label, icon, shortcut, enabled state, visibility, automation metadata, and handler target SHALL come from the typed command model + +### Requirement: XCore mediator bridges during migration + +XCore mediator and property-table behavior MAY remain as compatibility command/state infrastructure during migration, but SHALL NOT own final Avalonia UI composition. + +#### Scenario: Mediator handler executes through Avalonia command +- **WHEN** a migrated menu item invokes a legacy XCore command handler +- **THEN** the command SHALL route through an explicit mediator bridge +- **AND** the bridge SHALL expose diagnostics for target resolution and command state + +### Requirement: Command parity is validated + +The shell migration SHALL validate command availability, shortcuts, context menus, command enablement, one-at-a-time behavior, and target resolution against legacy baselines. + +#### Scenario: Shortcut parity is verified +- **WHEN** a legacy shortcut is migrated to Avalonia +- **THEN** automated or semantic tests SHALL prove the shortcut reaches the same command target and state behavior for covered workflows + +### Requirement: Main screens register through typed screen registry + +Main screens SHALL register through a typed Avalonia screen registry keyed by stable area/tool identifiers. + +#### Scenario: Area tool resolves to registered screen +- **WHEN** the user selects a migrated area/tool +- **THEN** the shell SHALL resolve the screen through the registry and create the registered presenter/view rather than a WinForms dynamic content host + +### Requirement: Each migrated screen has a manifest + +Each migrated main screen SHALL have a manifest describing commands, state, content host, shell services, legacy adapters, native-boundary status, accessibility IDs, performance budgets, rollback behavior, and default-switch gates. + +#### Scenario: Screen completion requires manifest evidence +- **WHEN** a screen is proposed for Avalonia completion +- **THEN** its manifest SHALL identify passing evidence for command routing, navigation, accessibility, localization, native-boundary status, Graphite-free behavior when relevant, and performance budgets + +### Requirement: Legacy content is explicit and temporary + +Non-migrated screens MAY be hosted through explicit legacy boundaries during transition, but the final default app SHALL NOT permanently embed WinForms or native viewing UI. + +#### Scenario: Legacy island is tracked +- **WHEN** a non-migrated screen is hosted inside the Avalonia shell during transition +- **THEN** the screen SHALL have a manifest identifying why it remains legacy, which commands it supports, and what gates remove the legacy host + +### Requirement: Entry points support Avalonia host transition + +FieldWorks entry points SHALL support a transition from WinForms application lifetime to Avalonia application lifetime without bypassing project startup, diagnostics, cache initialization, or command registration requirements. + +#### Scenario: Avalonia host follows canonical startup +- **WHEN** the Avalonia shell startup path is enabled +- **THEN** startup SHALL still initialize diagnostics, project selection/opening, LCModel cache, service registration, command infrastructure, and safe shutdown hooks through documented entry points + +### Requirement: Hidden WinForms startup is disallowed in final mode + +Final default startup SHALL NOT secretly create WinForms shell windows or run WinForms application lifetime to host Avalonia content. + +#### Scenario: Startup audit detects WinForms shell dependency +- **WHEN** default-startup validation runs +- **THEN** it SHALL fail if the default path creates the retired WinForms shell, dynamic content host, or WinForms-only main-window services + +### Requirement: Final shell excludes native UI boundaries + +The final Avalonia shell SHALL NOT use native Views, Graphite, Gecko Graphite rendering, or other native viewing/rendering/editor infrastructure for default UI composition. + +#### Scenario: Native UI dependency fails default audit +- **WHEN** default-shell dependency validation runs +- **THEN** it SHALL fail if default shell, chrome, navigation, dialogs, or migrated screens instantiate native viewing/rendering/editor infrastructure + +### Requirement: Retained native services stay outside UI composition + +Native or external linguistics services SHALL remain outside Avalonia UI composition when exposed through explicit non-UI service contracts. + +#### Scenario: Linguistics service is allowed +- **WHEN** the Avalonia shell or migrated screens invoke XAmple, spelling, parser tools, ICU, Encoding Converters, or similar services +- **THEN** those services SHALL remain outside Avalonia display, layout, hit testing, focus, selection, and editor realization responsibilities + +### Requirement: Shell migration uses layered validation + +The shell migration SHALL use shell-definition snapshots, command tests, WinForms UIA baselines, Avalonia.Headless tests, integration tests, semantic UI snapshots, full-app smoke tests, and dependency audits. + +#### Scenario: Shell behavior is frozen before replacement +- **WHEN** a shell subsystem is replaced +- **THEN** the migration SHALL identify existing baseline evidence or add shell contract, semantic, UIA, or integration tests for that behavior + +### Requirement: Full-app smoke gates protect default switch + +Before Avalonia shell becomes default, full-app smoke tests SHALL launch the app, open or create a project, switch representative areas/tools, execute representative commands, show dialogs, close windows, and shut down cleanly. + +#### Scenario: Default switch waits for smoke gates +- **WHEN** Avalonia shell is proposed as default +- **THEN** default-switch validation SHALL include full-app smoke evidence, accessibility evidence, localization evidence, performance evidence, and dependency audit evidence + +### Requirement: Installer packages Avalonia shell runtime + +Installer and packaging logic SHALL include the Avalonia shell runtime assets required by the default application host. + +#### Scenario: Avalonia shell artifacts are harvested +- **WHEN** installer packaging runs for a build where Avalonia shell is default +- **THEN** required Avalonia assemblies, native dependencies, resources, configuration, and generated shell definitions SHALL be included + +### Requirement: Shell localization survives typed migration + +Typed shell definitions SHALL preserve localizable labels, tooltips, menu text, command text, status text, dialog text, shortcut descriptions, and resource identifiers from existing shell configuration and resources. + +#### Scenario: Imported command keeps localization identity +- **WHEN** shell XML or resources define a localizable command label or tooltip +- **THEN** the typed shell definition SHALL retain localization identity so Crowdin/resource workflows can update the Avalonia shell text diff --git a/openspec/changes/fieldworks-avalonia-shell-migration/tasks.md b/openspec/changes/fieldworks-avalonia-shell-migration/tasks.md new file mode 100644 index 0000000000..4b1366bb80 --- /dev/null +++ b/openspec/changes/fieldworks-avalonia-shell-migration/tasks.md @@ -0,0 +1,77 @@ +## 1. Prerequisites and Inventory + +- [ ] 1.1 Confirm Lexical Edit Avalonia migration gates are complete or explicitly tracked as blockers. +- [ ] 1.2 Inventory shell entry points in `Src/Common/FieldWorks/`, `Src/Common/Framework/`, `Src/XCore/`, and `Src/xWorks/`. +- [ ] 1.3 Inventory `Main.xml`, area/tool XML, command IDs, menus, toolbars, sidebars, status panes, listeners, defaults, includes, and localization metadata. +- [ ] 1.4 Inventory remaining default-path WinForms, RootSite/native Views, XMLViews, Gecko, Graphite, browser/PDF, and dialog dependencies by main screen. +- [ ] 1.5 Define hard gates for a completely Avalonia default app: no default WinForms shell, no default native viewing/rendering, no Graphite, no Gecko Graphite, passing accessibility/localization/performance/smoke evidence. + +## 2. Shell Contracts + +- [ ] 2.1 Extract framework-neutral managed interfaces for app lifetime, main window, active-window registry, dialog owner, modal state, UI dispatcher, shutdown, progress, settings, and status services, following `avalonia-ui-scheduler` and `avalonia-lifetime`. +- [ ] 2.2 Add compatibility adapters for current WinForms `FwApp`, `FieldWorksManager`, xWorks window, and dialog-owner behavior. +- [ ] 2.3 Remove direct `Form`/`Control` requirements from new shell-facing contracts before Avalonia shell construction begins. +- [ ] 2.4 Add contract tests for startup, active-window tracking, dialog ownership, shutdown, and UI dispatch behavior. + +## 3. Typed Shell Composition + +- [ ] 3.1 Build typed shell-definition importer for `Main.xml`, area/tool XML, includes, extension hooks, resources, listeners, and default properties. +- [ ] 3.2 Represent commands, lists, areas/tools, menus, context menus, toolbars, status panes, shortcuts, icons, localization metadata, and screen registrations. +- [ ] 3.3 Add diagnostics for unsupported commands, listeners, dynamic loaders, toolbar widgets, status panels, and extension constructs. +- [ ] 3.4 Add deterministic shell-definition snapshot tests. + +## 4. Command Routing and State + +- [ ] 4.1 Define typed command descriptors with stable IDs, labels, gestures, icons, visibility, enabled state, target resolution, and diagnostics, following `avalonia-command-focus`. +- [ ] 4.2 Bridge XCore mediator handlers and property-table state into Avalonia commands and active-target routing, following `avalonia-command-focus`. +- [ ] 4.3 Add tests for command enable/visible state, shortcuts, one-at-a-time commands, command target selection, and mediator bridge behavior. +- [ ] 4.4 Add menu/context-menu automation metadata and localization checks. + +## 5. Avalonia Shell Skeleton + +- [ ] 5.1 Create Avalonia shell project and integrate it into `FieldWorks.sln` and `FieldWorks.proj` traversal only after runtime strategy is approved. +- [ ] 5.2 Implement main window, app lifetime, navigation regions, content host, status/progress region, diagnostics hooks, theme resources, and accessibility root metadata. +- [ ] 5.3 Run the shell in preview/sample mode before LCModel project startup. +- [ ] 5.4 Add Avalonia.Headless tests for shell creation, navigation host swapping, command dispatch, status updates, dialog ownership, focus traversal, and pane state. + +## 6. Navigation and Screen Registry + +- [ ] 6.1 Map area/tool IDs from typed shell definition to an Avalonia screen registry. +- [ ] 6.2 Implement area/tool navigation and persisted `areaChoice`/`currentContentControl` compatibility. +- [ ] 6.3 Add screen manifests for each migrated main screen, including commands, state, native-boundary status, accessibility, performance, rollback, and default-switch gates. +- [ ] 6.4 Add memory-project and sample-project navigation tests. + +## 7. Menus, Toolbars, Status, and Layout + +- [ ] 7.1 Render menu and context-menu structures with labels, shortcuts, icons, separators, extension items, visibility, and enablement. +- [ ] 7.2 Render standard/format/insert/view toolbars, including writing-system and style selectors. +- [ ] 7.3 Render status panels for message, progress, area, sort, filter, parsing, and record number. +- [ ] 7.4 Implement split panes, side panes, record-list region, content panes, collapse/restore behavior, and layout persistence. +- [ ] 7.5 Evaluate a docking library only if owned Avalonia controls cannot meet documented FieldWorks workflows. + +## 8. Dialogs and Global Services + +- [ ] 8.1 Introduce dialog service for project, writing-system, settings, import/export, find/replace, styles, help, feedback, and utility dialogs. +- [ ] 8.2 Migrate high-frequency dialogs first and retain explicit legacy adapters only while blocked. +- [ ] 8.3 Add owner/modal, cancellation, focus return, accessibility, and localization tests for migrated dialogs, following `avalonia-command-focus` and `avalonia-lifetime`. +- [ ] 8.4 Isolate browser/PDF/print behind replaceable services and select a non-Graphite default strategy. + +## 9. Main Screen Migration + +- [ ] 9.1 Migrate Lexicon screens after Lexical Edit gates. +- [ ] 9.2 Migrate Words/Interlinear screens. +- [ ] 9.3 Migrate Grammar/Morphology screens. +- [ ] 9.4 Migrate Notebook screens. +- [ ] 9.5 Migrate Lists screens. +- [ ] 9.6 Migrate dictionary preview/export, print, browser/PDF-dependent workflows, or isolate them outside the default path until replaced. + +## 10. Startup, Shutdown, Installer, and Default Switch + +- [ ] 10.1 Add Avalonia app startup path with project selection, cache creation, splash/safe-mode behavior, remote request listener, no-UI/app-server modes, and update checks accounted for. +- [ ] 10.2 Add shutdown/disposal tests for windows, caches, dialogs, background services, and retained native services. +- [ ] 10.3 Update installer/runtime packaging and dependency harvest for Avalonia shell assets. +- [ ] 10.4 Add feature flag/default selector for Avalonia shell. +- [ ] 10.5 Run full local build/test and app smoke gates before default switch. +- [ ] 10.6 Make Avalonia shell default only after hard gates pass. +- [ ] 10.7 Remove WinForms shell default path, FlexUIAdapter default dependency, WinForms dynamic content host, retired dialogs, and obsolete shell XML runtime pieces. +- [ ] 10.8 Revisit heavier reactive or region-framework alternatives only if the pivot triggers recorded in `lexical-edit-avalonia-migration/seam-recommendations.md` are met. \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/.openspec.yaml b/openspec/changes/lexical-edit-avalonia-migration/.openspec.yaml new file mode 100644 index 0000000000..93831bd262 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-13 diff --git a/openspec/changes/lexical-edit-avalonia-migration/architecture-diagrams.md b/openspec/changes/lexical-edit-avalonia-migration/architecture-diagrams.md new file mode 100644 index 0000000000..e8ee325d75 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/architecture-diagrams.md @@ -0,0 +1,370 @@ +# Lexical Edit Avalonia Migration Architecture Diagrams + +These diagrams summarize the current WinForms architecture, the migration seams, the testing strategy, the first optional Avalonia slices, the table/full Lexical Edit path, and the final default architecture after Graphite and native viewing/rendering are removed from migrated regions. + +Legend used across diagrams: + +- Red dashed nodes are decommissioning targets for completed Avalonia regions. +- Green nodes are Avalonia or future managed UI pieces. +- Blue nodes are dependency-inverted service contracts. +- Yellow nodes are validation and test layers. +- Purple nodes are model or canonical data contracts. + +## 1. Current WinForms Architecture and MVC Pressure + +The current stack mixes model access, controller behavior, view creation, refresh policy, and native rendering inside the same path. This is why it is hard to test in isolation and why wrapping it in Avalonia would preserve the wrong boundary. + +```mermaid +flowchart TB + User["User input
keyboard, mouse, menus"]:::actor + Mediator["xCore Mediator
PropertyTable
command routing"]:::controller + RecordEdit["RecordEditView
screen host"]:::mixed + DataTree["DataTree
refresh, focus, layout,
slice ownership"]:::mixed + SliceFactory["SliceFactory
XML interpretation
editor selection"]:::mixed + Slices["Slices and launchers
WinForms controls
business decisions"]:::mixed + XMLViews["XMLViews browse/table views
view definitions plus rendering"]:::mixed + RootSite["RootSite / SimpleRootSite
managed/native bridge"]:::decom + NativeViews["Native Views C++
layout, measurement,
selection, hit testing,
editing"]:::decom + Graphite["Graphite engine
Graphite feature settings"]:::decom + Gecko["Gecko/XWebBrowser/PDF
Graphite-enabled preview/export"]:::decom + XMLParts["XML Parts/Layout
customer overrides
ghosts, choosers, visibility"]:::model + LCModel["LCModel
lexicon data
transactions"]:::model + WS["Writing systems
fonts, script metadata,
legacy Graphite flags"]:::model + Tests["Hard-to-isolate tests
UIA can drive shell;
owner-drawn content is opaque"]:::test + + User --> Mediator --> RecordEdit --> DataTree + DataTree --> SliceFactory --> Slices + SliceFactory --> XMLParts + Slices --> LCModel + Slices --> RootSite --> NativeViews + XMLViews --> RootSite + NativeViews --> Graphite + DataTree --> WS + NativeViews --> WS + Gecko --> Graphite + Gecko --> WS + DataTree -. MVC violation: view owns refresh and control policy .-> LCModel + SliceFactory -. MVC violation: view factory owns editor decisions .-> LCModel + Slices -. MVC violation: launchers mix UI and business rules .-> LCModel + Tests -. brittle / broad .-> DataTree + + classDef actor fill:#f8fafc,stroke:#64748b,color:#0f172a; + classDef controller fill:#e0f2fe,stroke:#0284c7,color:#082f49; + classDef mixed fill:#fff7ed,stroke:#f97316,color:#431407; + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef test fill:#fef9c3,stroke:#ca8a04,color:#422006; + classDef decom fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,stroke-dasharray: 6 4,color:#450a0a; +``` + +## 2. Dependency Inversion Path and Better MVC + +The first architectural move is not Avalonia. It is extracting narrow ports around refresh, view definitions, editor selection, edit transactions, command/focus routing, UI dispatch, lifetime, writing-system text, diagnostics, and retained linguistics services. Legacy WinForms becomes one adapter. Avalonia becomes another adapter later. + +```mermaid +flowchart LR + subgraph Model["Model and canonical contracts"] + LCModel["LCModel
data and transactions"]:::model + Canonical["Canonical view definition
layout semantics, editor descriptors"]:::model + Presentation["Instance presentation model
stable node identity,
binding, validation, focus metadata"]:::model + XMLImport["XML import adapter
transitional compatibility"]:::adapter + end + + subgraph Ports["Dependency-inverted ports"] + Refresh["ILexicalRefreshCoordinator"]:::port + ViewDefs["IViewDefinitionSource / Importer / Compiler / Cache"]:::port + Editors["ILexicalEditorRegistry"]:::port + EditSession["IEditSession
transactions, validation,
undo/redo"]:::port + Choosers["IChooserService"]:::port + Text["IWritingSystemTextService
font and shaping capabilities"]:::port + Command["IXCoreCommandBridge"]:::port + PropertyState["IPropertyStateStore"]:::port + Navigation["IRecordNavigationContext"]:::port + Scheduler["IUiScheduler"]:::port + Lifetime["IRegionLifetime"]:::port + Linguistics["Feature-specific linguistics services
spelling, XAmple, parsers"]:::port + Capture["IViewParitySnapshotService"]:::port + end + + subgraph LegacyAdapters["Legacy adapters during migration"] + LegacyHost["RecordEditView/DataTree adapter"]:::legacy + LegacySlices["Legacy slice adapter"]:::legacy + LegacyViews["Native Views baseline adapter"]:::decom + end + + subgraph FutureAdapters["Future adapters"] + AvaloniaHost["Avalonia screen host"]:::future + AvaloniaEditors["FieldWorks-owned Avalonia editors"]:::future + TableTree["Avalonia table/tree renderer"]:::future + end + + XMLParts["XML Parts/Layout"]:::legacy --> XMLImport --> Canonical + ViewDefs --> Canonical --> Presentation + LCModel --> Presentation + Presentation --> Editors + Presentation --> Capture + Refresh --> LegacyHost + Editors --> LegacySlices + Capture --> LegacyViews + Editors --> AvaloniaEditors + EditSession --> AvaloniaEditors + Refresh --> AvaloniaHost + Text --> AvaloniaEditors + Choosers --> AvaloniaEditors + Command --> AvaloniaHost + PropertyState --> AvaloniaHost + Navigation --> AvaloniaHost + Scheduler --> AvaloniaHost + Lifetime --> AvaloniaHost + Linguistics --> AvaloniaEditors + AvaloniaHost --> TableTree + + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef port fill:#dbeafe,stroke:#2563eb,color:#1e3a8a; + classDef adapter fill:#e0f2fe,stroke:#0284c7,color:#082f49; + classDef future fill:#dcfce7,stroke:#16a34a,color:#052e16; + classDef legacy fill:#fff7ed,stroke:#f97316,color:#431407; + classDef decom fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,stroke-dasharray: 6 4,color:#450a0a; +``` + +## 3. Testing and Validation Map + +Tests are layered around the seam being proven. Deep behavior moves to unit and integration tests. UI automation stays narrow. Render verification captures both semantic and visual evidence. Avalonia.Headless covers new controls without booting the full application. + +```mermaid +flowchart TB + Requirements["Migration requirements
density, interaction, fonts,
audited default path, no native viewing"]:::model + + Unit["Unit tests
refresh state
launcher logic
editor registry"]:::test + Integration["Integration tests
XML import to typed IR
LCModel transactions
cache invalidation"]:::test + Semantic["Semantic parity snapshots
fields, labels, bindings,
ghosts, focus, accessibility"]:::test + LegacyUIA["UIA2 legacy smoke
menus, dialogs, chooser launch,
table header reachability"]:::test + Render["Render comparison
near-pixel evidence
timing buckets
failure bundles"]:::test + Headless["Avalonia.Headless
input, focus, popups,
control behavior"]:::test + NativeAudit["Native viewing seam audit
no RootSite / IVwEnv / Views
inside completed region"]:::test + GraphiteAudit["Graphite/native rendering audit
no unapproved default-path
Graphite dependency"]:::test + UndoGate["Undo/redo and transaction matrix"]:::test + A11yGate["Accessibility, keyboard/IME,
localization gates"]:::test + OverrideGate["Customer override and
dynamic editor fixtures"]:::test + PerfGate["Performance budgets
open, scroll, type, memory"]:::test + RegionManifest["Migrated-region manifest
entry points, forbidden calls,
fixtures, rollback"]:::port + + Seam1["Refactor seams"]:::port + Seam2["Typed IR and XML import"]:::port + Slice1["First optional Avalonia slices"]:::future + Slice2["Tables and Lexical Edit regions"]:::future + Default["Default Avalonia readiness"]:::future + + Requirements --> Unit --> Seam1 + Requirements --> Integration --> Seam2 + Requirements --> Semantic --> Seam2 + Requirements --> LegacyUIA --> Seam1 + Requirements --> Render --> Slice2 + Requirements --> Headless --> Slice1 + Requirements --> NativeAudit --> Default + Requirements --> GraphiteAudit --> Default + Requirements --> UndoGate --> Slice1 + Requirements --> A11yGate --> Slice1 + Requirements --> OverrideGate --> Slice2 + Requirements --> PerfGate --> Slice2 + RegionManifest --> Slice1 + RegionManifest --> Slice2 + Seam1 --> Slice1 --> Slice2 --> Default + Seam2 --> Slice1 + + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef port fill:#dbeafe,stroke:#2563eb,color:#1e3a8a; + classDef future fill:#dcfce7,stroke:#16a34a,color:#052e16; + classDef test fill:#fef9c3,stroke:#ca8a04,color:#422006; +``` + +## 4. First Optional Avalonia Slices: Hover, Popup, Simple Editors + +The first slices should be optional and low-blast-radius. They use the same ports that legacy code uses, but the rendered surface is Avalonia-owned and can run in the Preview Host or headless tests. + +```mermaid +flowchart LR + subgraph LegacyShell["Existing WinForms shell remains default"] + RecordEdit["RecordEditView/DataTree"]:::legacy + EditorRegistry["Editor registry seam"]:::port + LegacySlice["Legacy slice fallback"]:::legacy + end + + subgraph OptionalAvalonia["Optional first Avalonia slice"] + PreviewHost["Preview Host or feature flag"]:::future + SimpleEditor["Simple text/scalar editor"]:::future + Hover["Hover card / popup chooser"]:::future + Headless["Avalonia.Headless tests"]:::test + end + + subgraph Contracts["Shared contracts"] + IR["Typed IR node"]:::model + Text["Writing-system text service
proven font/shaping paths"]:::port + EditSession["Edit session
commit/cancel, validation,
undo/redo"]:::port + CommandFocus["Command, focus,
keyboard/IME routing"]:::port + SchedulerLifetime["UI scheduler and
region lifetime"]:::port + Chooser["Chooser service"]:::port + Linguistics["Linguistics service gateway
spelling/XAmple allowed"]:::port + end + + Decom["Not allowed in this slice
RootSite, IVwEnv, native Views,
Graphite"]:::decom + + RecordEdit --> EditorRegistry + EditorRegistry --> LegacySlice + EditorRegistry --> PreviewHost + PreviewHost --> SimpleEditor + PreviewHost --> Hover + IR --> SimpleEditor + Text --> SimpleEditor + EditSession --> SimpleEditor + CommandFocus --> SimpleEditor + SchedulerLifetime --> SimpleEditor + CommandFocus --> Hover + Chooser --> Hover + Linguistics --> SimpleEditor + Headless --> SimpleEditor + Headless --> Hover + SimpleEditor -. must not call .-> Decom + Hover -. must not call .-> Decom + + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef port fill:#dbeafe,stroke:#2563eb,color:#1e3a8a; + classDef future fill:#dcfce7,stroke:#16a34a,color:#052e16; + classDef legacy fill:#fff7ed,stroke:#f97316,color:#431407; + classDef test fill:#fef9c3,stroke:#ca8a04,color:#422006; + classDef decom fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,stroke-dasharray: 6 4,color:#450a0a; +``` + +## 5. Lexical Edit and Table Views Slice + +Table views and full Lexical Edit regions are meaningfully different from the first hover/simple-editor slices. They need virtualization, stable row/node identity, selection and scrolling services, table/tree templates, and stronger parity gates. + +```mermaid +flowchart TB + subgraph Inputs["Canonical inputs"] + Manifest["Migrated-region manifest
entry points, gates,
forbidden calls"]:::port + LCModel["LCModel data"]:::model + XMLImport["XML import
transition only"]:::adapter + IR["Typed view definition / IR
sections, fields, tables,
tree nodes, editor descriptors"]:::model + end + + subgraph AvaloniaRegion["Migrated table or Lexical Edit region"] + Host["Avalonia region host"]:::future + Virtualizer["Virtualized table/tree coordinator"]:::future + ControlChoice["Control choice adapter
TreeView, TreeDataGrid,
ItemsRepeater, owned controls"]:::future + Rows["Dense row/node templates
multiple writing-system alternatives"]:::future + Editors["Editor registry
cell and field editors"]:::future + Selection["Managed selection, focus,
scroll, hit-test metadata"]:::future + end + + subgraph Services["Ports and services"] + Refresh["Refresh coordinator"]:::port + Text["Writing-system text service
proven font/shaping paths"]:::port + Chooser["Chooser and popup services"]:::port + Linguistics["Custom linguistics services
XAmple/spelling/parsers"]:::port + Diagnostics["Diagnostics and parity capture"]:::port + end + + subgraph Gates["Completion gates"] + Semantic["Semantic parity"]:::test + Render["Render/timing evidence"]:::test + NativeAudit["No native viewing/rendering/editor path"]:::test + GraphiteAudit["No unapproved Graphite/native
default-path dependency"]:::test + Perf["Performance budget"]:::test + BrowserPdf["Browser/PDF decision gate"]:::test + end + + Decommissioned["Decommission for this region
DataTree slices, XMLViews runtime,
RootSite/IVwEnv/Native Views,
Graphite render engine"]:::decom + + LCModel --> IR + XMLImport --> IR + Manifest --> Host + IR --> Host --> Virtualizer --> ControlChoice --> Rows + Virtualizer --> Selection + Rows --> Editors + Refresh --> Host + Text --> Rows + Chooser --> Editors + Linguistics --> Editors + Diagnostics --> Semantic + Host --> Semantic + Host --> Render + Host --> NativeAudit + Host --> GraphiteAudit + Host --> Perf + Host --> BrowserPdf + Host -. must not call .-> Decommissioned + + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef adapter fill:#e0f2fe,stroke:#0284c7,color:#082f49; + classDef port fill:#dbeafe,stroke:#2563eb,color:#1e3a8a; + classDef future fill:#dcfce7,stroke:#16a34a,color:#052e16; + classDef test fill:#fef9c3,stroke:#ca8a04,color:#422006; + classDef decom fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,stroke-dasharray: 6 4,color:#450a0a; +``` + +## 6. Final Default Architecture After Avalonia and Graphite Decommissioning + +In the final Lexical Edit default path, MVC/MVVM boundaries are explicit: LCModel and canonical view definitions are model/data contracts; presenters and edit sessions coordinate commands, refresh, transactions, validation, and diagnostics; Avalonia controls own display and input. Graphite and native viewing/rendering are outside the default path. Retained native linguistics engines are service dependencies, not UI dependencies. Full application shell/window replacement is handled by the phase-two `fieldworks-avalonia-shell-migration` change. + +```mermaid +flowchart TB + subgraph ModelLayer["Model and canonical definitions"] + LCModel["LCModel
lexicon data and transactions"]:::model + Canonical["Canonical typed view definitions
post-XML runtime contract"]:::model + ProjectSettings["Project settings
writing systems, fonts,
font feature metadata"]:::model + end + + subgraph ControllerLayer["Controller / presenter layer"] + Presenter["Lexical Edit presenters
commands, refresh, selection policy"]:::controller + EditorRegistry["Editor registry"]:::port + Transaction["Edit session, transaction,
validation, undo/redo"]:::port + Diagnostics["Diagnostics and parity hooks"]:::port + end + + subgraph ViewLayer["Avalonia view layer"] + Shell["Avalonia Lexical Edit shell"]:::future + Detail["Dense detail editors"]:::future + Tables["Virtualized table/tree views"]:::future + Popups["Choosers, flyouts, hover cards"]:::future + end + + subgraph ServiceLayer["FieldWorks services"] + Text["Writing-system text service
proven font/shaping paths"]:::port + Linguistics["Custom linguistics gateway
XAmple, spelling, parsers,
Encoding Converters, ICU"]:::port + BrowserPdf["Non-Graphite browser/PDF strategy"]:::port + ShellPhase["Phase-two Avalonia app shell
fieldworks-avalonia-shell-migration"]:::port + end + + subgraph Decommissioned["Removed from default Avalonia path"] + DataTree["DataTree/Slice runtime"]:::decom + XMLRuntime["Runtime XML Parts/Layout"]:::decom + NativeViews["RootSite, IVwEnv,
ManagedVwWindow, Native Views"]:::decom + Graphite["Native Graphite render engines,
unapproved Graphite runtime"]:::decom + Gecko["Gecko Graphite rendering
GeckofxHtmlToPdf assumptions"]:::decom + end + + LCModel --> Presenter + Canonical --> Presenter + ProjectSettings --> Text + Presenter --> Shell + Presenter --> EditorRegistry + Presenter --> Transaction + Shell --> Detail + Shell --> Tables + Shell --> Popups + EditorRegistry --> Detail + EditorRegistry --> Tables + Text --> Detail + Text --> Tables + Linguistics --> Presenter + BrowserPdf --> Presenter + ShellPhase --> Shell + Diagnostics --> Presenter + Shell -. default path excludes .-> Decommissioned + + classDef model fill:#f3e8ff,stroke:#7e22ce,color:#3b0764; + classDef controller fill:#e0f2fe,stroke:#0284c7,color:#082f49; + classDef port fill:#dbeafe,stroke:#2563eb,color:#1e3a8a; + classDef future fill:#dcfce7,stroke:#16a34a,color:#052e16; + classDef decom fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,stroke-dasharray: 6 4,color:#450a0a; +``` \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-command-focus.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-command-focus.md new file mode 100644 index 0000000000..446c08b6b0 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-command-focus.md @@ -0,0 +1,57 @@ +# Avalonia Command and Focus Plan + +Command and focus behavior must be built in two layers: local first-slice behavior that can run in the preview host, then FieldWorks shell/XCore routing for production integration. + +## Current State + +| Item | Source | Current Behavior | +|---|---|---| +| Local key bindings | `010-advanced-entry-preview-prototype` | Prototype has local `Ctrl+S` save and `Escape` cancel bindings. | +| View-model commands | `010-advanced-entry-preview-prototype` | Prototype has save/cancel commands and close request callback. | +| Shell bridge | Not implemented | No current `IXCoreCommandBridge` or production mediator adapter. | + +## First-Slice Local Contract + +The preview-host and first editable slice must support: + +- Keyboard save/cancel commands. +- Tab/Shift+Tab navigation in layout order. +- Focus restoration after validation failure, refresh, save failure, and popup close. +- Stable automation names/IDs for controls where Avalonia exposes them. +- No dependency on XCore mediator or WinForms message routing. + +## Shell-Phase Contract + +When integrated into FieldWorks, the migrated region must additionally: + +- Route menu, toolbar, and accelerator commands through XCore without duplicating command state. +- Keep global undo/redo, save, cancel, refresh, and navigation enablement in sync with active edit-session state. +- Avoid stealing shortcuts from focused text controls when local text editing should handle them. +- Return focus to the shell/record list predictably when the migrated region closes or rolls back. + +## Required Tests + +| Test Area | Cases | +|---|---| +| Local shortcuts | `Ctrl+S` invokes save once; `Escape` invokes cancel once; disabled commands do not execute. | +| Focus order | Tab order matches Presentation IR order and legacy baseline for selected fixture. | +| Validation focus | Save with blocking error focuses first invalid materialized node and exposes error metadata. | +| Refresh focus | Refresh/rebuild keeps focus on equivalent node when possible; otherwise chooses documented fallback. | +| Popup/chooser focus | Opening and closing chooser/popup restores focus and selection/caret. | +| Text control ownership | Text-edit shortcuts remain local until command bridge explicitly routes them. | +| Shell bridge | XCore menu/toolbar/keyboard command state matches view-model/session state. | + +## Architecture Notes + +- Keep first-slice commands as view-model commands so headless tests can exercise them. +- Introduce a shell command bridge only in the integration phase, behind an interface owned by the host composition layer. +- Do not route commands by directly referencing WinForms controls from Avalonia production code. +- Focus keys should be stable Presentation IR node IDs, not visual tree indexes. + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 5 | Local command/focus tests pass for first editable slice. | +| Phase 6 | Accessibility and keyboard traversal evidence exists for selected fixture. | +| Phase 8 | Shell bridge tests prove XCore command routing without breaking preview-host isolation. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-edit-sessions.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-edit-sessions.md new file mode 100644 index 0000000000..47bf080e65 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-edit-sessions.md @@ -0,0 +1,55 @@ +# Avalonia Edit Sessions + +This plan separates the current AdvancedEntry implementation from the proposed edit-session seam needed for production Lexical Edit migration. + +## Current State + +| Item | Source | Current Behavior | +|---|---|---| +| Concrete session | `010-advanced-entry-preview-prototype` | Prototype starts a fenced LCModel undo task for a selected entry, then exposes `Save()` and `Cancel()`. | +| View-model ownership | `010-advanced-entry-preview-prototype` | Prototype loads one entry lifetime, disposes it on save/cancel, and requests window close through a callback. | +| Existing coverage | `AdvancedEntryEditSessionTests`, `MainWindowViewModelLifetimeTests` | Save/cancel behavior, nested-session rejection, and basic lifetime disposal are characterized. | + +There is no implemented `ILexicalEditSession` with `GetValue`, `SetValue`, or `Commit` semantics. Any such API is a proposed Phase 3 seam. + +## Architectural Decision Needed + +Before first editable slice work expands, choose one model and encode it in tests: + +| Option | Pros | Risks | Required Tests | +|---|---|---|---| +| Direct LCModel fenced undo task | Matches current spike and existing LCModel action-handler behavior. | UI edits affect model before save if not staged carefully; cancel must reliably roll back all touched data. | Multi-field cancel rollback, save creates one undoable action, global undo after save, nested sessions rejected before mutation. | +| Staged draft model | Cleaner validation and cancel semantics before commit. | More code; must map drafts to LCModel objects and handle stale model state. | Draft isolation, conflict/stale object detection, commit transaction, rollback on partial failure. | + +Default recommendation for Phase 3: keep the current direct fenced undo-task model for the first slice, but add tests that prove cancel/save/global undo semantics before broadening editable fields. + +## Proposed Seam Contract + +The extracted seam should be introduced only with tests. It should provide: + +- One active session per editable root object unless nested sessions are explicitly designed. +- Explicit lifecycle states: `Active`, `Saved`, `Canceled`, `Disposed`, `Faulted`. +- Main-thread LCModel write enforcement. +- Deterministic cancellation/rollback even after validation errors. +- Save result that reports changed objects/flids for refresh coordination. +- No direct UI dependency, dialog dependency, or WinForms dependency. + +## Required Tests + +| Test Area | Cases | +|---|---| +| Lifecycle | Save once, cancel once, dispose without save, double save/cancel no-op or exception by contract, nested session rejected. | +| Rollback | Single-field rollback, multi-field rollback, sequence add/remove rollback, object creation rollback, stale reference rollback. | +| Commit | Save creates one undoable action; global undo/redo restores values; failure during commit does not leave partial state. | +| Refresh | Save reports changed objects/flids; cancel reports no committed changes; disposed session does not emit late refresh. | +| Threading | LCModel writes happen on approved thread; background validation/layout cannot mutate cache. | +| Localization/diagnostics | User-facing save/cancel errors use resources; diagnostics include entry HVO/class/flid without leaking unsafe input across native boundaries. | + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 2 | Current concrete session tests pass and gaps are recorded in the coverage report. | +| Phase 3 | Introduce seam with lifecycle/rollback contract tests before moving logic out of the view model. | +| Phase 5 | First editable slice proves save/cancel/global undo/redo against real LCModel fields. | +| Phase 8 | Shell integration proves XCore/global command routing uses the same session semantics. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-lifetime.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-lifetime.md new file mode 100644 index 0000000000..bd481682b7 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-lifetime.md @@ -0,0 +1,68 @@ +# Avalonia Lifetime and Disposal Plan + +The migrated Lexical Edit region must release sessions, loaders, subscriptions, and UI resources deterministically across save, cancel, close, navigation, and shell unload. + +## Current State + +| Item | Source | Current Behavior | +|---|---|---| +| View model lifetime | `010-advanced-entry-preview-prototype` | Prototype owns the currently loaded entry lifetime and disposes it on save/cancel. | +| Window close | `010-advanced-entry-preview-prototype` | Prototype wires close behavior for the preview-host window. | +| Existing coverage | `MainWindowViewModelLifetimeTests` | Save/cancel disposal and close request basics. | +| Proposed seam | `ILexicalLifetimeManager` | Not implemented. | + +## Lifetime Contract + +Each migrated region must define owners for: + +- Active edit session. +- Loaded presentation snapshot and lazy sequence materializers. +- Validation subscriptions and async validation runs. +- UI scheduler callbacks. +- LCModel event subscriptions / `PropChanged` listeners. +- Shell command bridge registrations. +- Popup/chooser/dialog resources. + +Disposal must be idempotent, must detach event handlers, and must not allow late callbacks to mutate a closed region. + +## Close and Navigation Semantics + +| Scenario | Required Behavior | +|---|---| +| Save then close | Save completes or reports failure; successful save disposes session and closes/returns to shell once. | +| Cancel then close | Cancel rolls back, disposes session, and closes/returns once. | +| Window close while dirty | Policy must be explicit: prompt, cancel, save, or block. First slice can choose a narrow policy but must test it. | +| Navigation to another entry | Old session is saved/canceled/disposed before new session becomes active. Late loader results from old entry are ignored/disposed. | +| External shell unload | Unregister commands/events and dispose region without depending on visual tree finalizers. | +| Fault during save/cancel | Region remains in documented state with rollback/diagnostic path; no double close. | + +## Required Tests + +| Test Area | Cases | +|---|---| +| Idempotent disposal | Dispose twice, save then dispose, cancel then dispose. | +| Late loader | Loader completes after cancel/navigation; result is disposed or ignored and does not overwrite current entry. | +| Event unsubscribe | LCModel/mediator/scheduler callbacks do not fire into disposed view model. | +| Close ordering | Close requested once after save/cancel; failure does not close unless policy says so. | +| Dirty close | Dirty close policy is tested, including prompt/dialog seam when introduced. | +| Popup/chooser | Open popup resources are closed/disposed on region unload. | +| Leak smoke | Weak-reference or subscription-count smoke test for common lifetime leaks when feasible. | + +## Proposed Lifetime Manager + +If the view-model lifetime logic grows beyond first slice, extract a seam with these responsibilities: + +- Track region lifecycle state. +- Own cancellation tokens for async work. +- Dispose old sessions before loading new roots. +- Coordinate close/navigation decisions with edit session, validation, scheduler, and shell bridge. +- Expose test hooks for active subscriptions/resources. + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 2 | Current save/cancel lifetime tests pass. | +| Phase 3 | Lifetime extraction starts only with late-loader and idempotent-disposal tests. | +| Phase 5 | First editable slice has dirty close/navigation behavior tested. | +| Phase 8 | Shell unload and command bridge registrations are disposed in integration tests. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-ui-scheduler.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-ui-scheduler.md new file mode 100644 index 0000000000..ef7600530b --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-ui-scheduler.md @@ -0,0 +1,63 @@ +# Avalonia UI Scheduler Plan + +Avalonia UI work must use the UI dispatcher deliberately, and LCModel work must not be pushed onto background threads unless it operates on immutable snapshots. This plan prevents the migration from hiding threading bugs behind `Task.Run`. + +## Current State + +| Item | Source | Current Behavior | +|---|---|---| +| Dispatcher use | `010-advanced-entry-preview-prototype` | Prototype uses Avalonia dispatcher calls directly for UI-bound operations. | +| Tests | Avalonia headless tests | Tests can flush dispatcher work, but there is no scheduler seam yet. | +| Proposed seam | `IUiScheduler` | Not implemented. | + +Avalonia documentation distinguishes fire-and-forget dispatcher posting from awaited invocation. Use the awaited path when tests and lifecycle code need completion, result, cancellation, or exception propagation. + +## Rules + +| Rule | Rationale | +|---|---| +| UI-bound mutations run on Avalonia UI thread. | Keeps visual tree/view-model notifications predictable. | +| LCModel writes run through approved edit-session/main-thread path. | LCModel and native boundaries are not safe targets for casual background work. | +| Background work uses immutable snapshots. | Layout compilation and validation can be parallelized only after data is copied into immutable inputs. | +| Await completion for lifecycle-critical work. | Save, cancel, close, validation, and loader disposal need exception/cancellation visibility. | +| Fire-and-forget must be rare and logged/owned. | `Post` can hide failure and create late callbacks after disposal. | + +## Proposed Scheduler Seam + +Introduce a thin seam only when tests need it: + +```csharp +public interface IUiScheduler +{ + bool CheckAccess(); + Task InvokeAsync(Func action, CancellationToken cancellationToken); + Task InvokeAsync(Func> action, CancellationToken cancellationToken); + void Post(Action action); +} +``` + +Contract details: + +- `InvokeAsync` propagates exceptions and observes cancellation before starting work when possible. +- `Post` is for non-critical notifications only; tests must not rely on it as completed work. +- The scheduler does not own LCModel threading policy; edit-session services do. + +## Required Tests + +| Test Area | Cases | +|---|---| +| Access | `CheckAccess` returns true on UI thread and false in fake/background contexts. | +| Exception | Exception thrown in invoked action reaches caller and does not leave session half-disposed. | +| Cancellation | Canceled token prevents queued lifecycle work or reports cancellation deterministically. | +| Disposal | Late scheduled loader result is disposed/ignored after cancel/close. | +| Ordering | Save/cancel/close ordering is deterministic under queued UI work. | +| Snapshot work | Background layout/validation tests prove inputs are immutable and no live `LcmCache` mutation occurs. | + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 3 | Add scheduler seam only with fake-scheduler tests that prove lifecycle behavior. | +| Phase 4 | Layout compiler background work consumes immutable snapshots. | +| Phase 5 | Save/cancel validation paths use awaited scheduling where completion matters. | +| Phase 8 | Shell integration has cancellation and disposal tests for region unload/navigation. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-undo-redo.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-undo-redo.md new file mode 100644 index 0000000000..91e79ea580 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-undo-redo.md @@ -0,0 +1,53 @@ +# Avalonia Undo/Redo Plan + +Undo/redo is a shell and LCModel contract, not a local Avalonia convenience. The migrated editor must integrate with the existing FieldWorks action-handler behavior before it can be enabled by default. + +## Current State + +| Area | Current Behavior | +|---|---| +| Legacy app | LCModel write operations are normally wrapped in undoable tasks and participate in the global FieldWorks undo/redo stack. | +| AdvancedEntry spike | Prototype on `010-advanced-entry-preview-prototype` uses a concrete save/cancel session. Local UI currently exposes save/cancel, not a full undo/redo coordinator. | +| Tests | Edit-session tests characterize save/cancel/nested-session behavior, but do not yet prove global undo/redo after save. | + +`IUndoRedoCoordinator` is a proposed Phase 3/8 seam. It is not current implementation. + +## Contract + +The migrated region MUST: + +1. Wrap committed model changes in the same undo/redo mechanism expected by LCModel and the FieldWorks shell. +2. Avoid creating a separate Avalonia-only undo history for committed LCModel state. +3. Keep transient text-edit undo local to the focused text control only until the edit commits. +4. Disable or route global undo/redo commands while a session is in a state where replay would corrupt the draft/LCModel boundary. +5. Refresh the migrated region after external undo/redo without losing focus when possible. + +## Routing Model + +| Command Source | First-Slice Behavior | Shell-Integrated Behavior | +|---|---|---| +| `Ctrl+Z` / `Ctrl+Y` inside text control | Let the control handle local text undo while focus remains in an uncommitted editor. | Same, unless shell command routing explicitly owns the shortcut. | +| Save command | Commits through edit session and creates one LCModel undoable action. | Also updates shell command state and dirty indicators. | +| Global Undo/Redo menu/toolbar | Out of scope for preview spike. | Routed through an `IUndoRedoCoordinator` that delegates to LCModel action handler and refreshes the region. | +| Cancel | Rolls back active session and must not create a committed undo action. | Same, with shell state notification. | + +## Required Tests + +| Test Area | Cases | +|---|---| +| Commit grouping | Multiple field edits saved together produce one undoable action. | +| Global undo | After save, global undo restores all changed LCModel values and refreshes the Avalonia view. | +| Global redo | Redo reapplies saved values and refreshes without duplicating sequence items. | +| Cancel | Cancel restores values and does not add an undoable action. | +| Focus | Undo/redo after save keeps or restores a sensible focus target; destroyed editors do not receive focus. | +| Dirty state | Command enablement reflects clean, dirty, saving, canceled, and faulted states. | +| External mutation | Legacy DataTree or parser mutation during/after session is detected and refreshed or rejected safely. | + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 3 | Extract coordinator only after edit-session lifecycle tests exist. | +| Phase 5 | First editable slice has save/cancel/global undo/global redo tests on real fields. | +| Phase 8 | Shell command bridge proves XCore menu/toolbar/keyboard routing against the same coordinator. | +| Phase 9 | Default-enabled region passes external undo/redo refresh tests and retains rollback. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/avalonia-validation.md b/openspec/changes/lexical-edit-avalonia-migration/avalonia-validation.md new file mode 100644 index 0000000000..1232c347c5 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/avalonia-validation.md @@ -0,0 +1,63 @@ +# Avalonia Validation Plan + +Validation must protect LCModel data, expose useful user feedback, and remain testable without assuming UI behavior that is not yet implemented. + +## Current State + +| Item | Source | Current Behavior | +|---|---|---| +| Validation service | `010-advanced-entry-preview-prototype` | Prototype evaluates required Presentation IR nodes, skips unmaterialized lazy sequence items, returns deterministic errors in layout order. | +| Coverage | `010-advanced-entry-preview-prototype` | Prototype covers required-field ordering and lazy skip behavior. | +| UI binding | Current property-grid prototype | No complete production binding adapter yet. | + +Avalonia supports validation through binding mechanisms such as `INotifyDataErrorInfo` and `DataValidationErrors`, but the current spike has not wired a full production validation presentation layer. + +## Required Validation Model + +| Field | Requirement | +|---|---| +| `NodeId` | Stable presentation node ID for focus and error placement. | +| `ObjectId` / class / flid | Enough LCModel context for diagnostics and refresh. | +| `Severity` | Error, warning, info. Save-blocking behavior depends on severity. | +| `ResourceKey` | Localizable message key, with formatted parameters separated from text. | +| `Message` | Localized display text at presentation time. | +| `AccessibilityText` | Screen-reader-friendly error summary where Avalonia supports exposing it. | +| `Version` | Validation run/version used to ignore stale async results. | + +## Architecture + +1. Validation rules operate on immutable presentation/edit-session snapshots where possible. +2. LCModel-dependent rules run on the approved thread or use immutable metadata/value snapshots. +3. The view model exposes validation through an Avalonia-friendly adapter, preferably `INotifyDataErrorInfo` when it maps cleanly to the control surface. +4. UI controls display validation state without hardcoding production strings in XAML or code. +5. Save command enablement depends on save-blocking validation errors, not on visual state alone. + +## Required Tests + +| Test Area | Cases | +|---|---| +| Determinism | Errors ordered by layout/focus order, independent of dictionary enumeration. | +| Lazy data | Unmaterialized sequences skipped; materialized invalid child reports correct node. | +| Localization | Error carries resource key and arguments; localized message resolves in presentation layer. | +| Severity | Warnings do not block save; errors block save; policy is explicit. | +| Async/stale results | Slow validation result from older snapshot is ignored after newer edit. | +| Accessibility | Error summary/automation metadata is exposed for focused invalid controls where supported. | +| Save interaction | Save refuses blocking errors and does not partially commit; cancel remains available. | +| External mutation | Deleted or replaced LCModel objects produce deterministic stale-object diagnostics. | + +## Open Decisions + +| Decision | Notes | +|---|---| +| `INotifyDataErrorInfo` vs direct `DataValidationErrors` | Prefer the smallest adapter that lets controls and tests observe the same errors. | +| Sync vs async rules | First slice can stay synchronous for required-field rules; async rules need cancellation/versioning first. | +| Error placement for virtualized nodes | Non-materialized invalid data needs region-level summary and a way to materialize/focus the node. | + +## Phase Gates + +| Phase | Gate | +|---|---| +| Phase 2 | Core service tests pass and known UI-binding gaps are recorded. | +| Phase 5 | First editable slice exposes validation in the view model and blocks save only by explicit severity policy. | +| Phase 6 | Accessibility and keyboard navigation can reach validation feedback. | +| Phase 8 | Shell dirty/save state reflects validation state. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/coverage-map.md b/openspec/changes/lexical-edit-avalonia-migration/coverage-map.md new file mode 100644 index 0000000000..42e82820c3 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/coverage-map.md @@ -0,0 +1,99 @@ +# Coverage Map: Lexical Edit Avalonia Migration + +This map records the characterization coverage needed before refactoring the standard Lexical Edit path toward Avalonia. It separates current repo behavior from proposed seams so Phase 3 does not proceed on invented interfaces. + +This Phase 1/2 foundation branch keeps legacy WinForms/DataTree/XMLViews characterization and planning. The Avalonia Preview Host and `AdvancedEntry.Avalonia` prototype coverage referenced below lives on `010-advanced-entry-preview-prototype`; product launcher wiring lives on `010-advanced-entry-product-launcher-spike`. + +## Coverage Status Legend + +| Status | Meaning | +|---|---| +| Covered | Executable characterization exists for the current boundary. | +| Partial | Some executable coverage exists, but named edge cases remain open. | +| Planned seam | No production seam exists yet; tests must be added during or before extraction. | +| Blocked | Clean tests require a new seam, harness, or approved dependency. | + +## 1. DataTree Refresh and Slice Output + +| Surface | Current Source | Current Coverage | Missing Before Risky Refactor | +|---|---|---|---| +| `PropChanged` handling | [Src/Common/Controls/DetailControls/DataTree.cs](Src/Common/Controls/DetailControls/DataTree.cs#L639) | LT-22414 refresh tests in [MorphTypeAtomicLauncherTests.cs](Src/Common/Controls/DetailControls/DetailControlsTests/MorphTypeAtomicLauncherTests.cs) | Nested/re-entrant `DoNotRefresh`, rapid deferred notifications, focus restoration after refresh. | +| Full refresh/rebuild | [Src/Common/Controls/DetailControls/DataTree.cs](Src/Common/Controls/DetailControls/DataTree.cs#L1967) | `DoNotRefresh_*` regression coverage | Explicit refresh coordinator tests once `ILexicalRefreshCoordinator` exists. | +| Slice semantic output | [Src/Common/Controls/DetailControls/DataTree.cs](Src/Common/Controls/DetailControls/DataTree.cs#L1879) and [Src/Common/Controls/DetailControls/Slice.cs](Src/Common/Controls/DetailControls/Slice.cs) | `CfAndBib_SemanticSliceBaselineCapturesStableBindingsAndFocusOrder` | Broader fixture set covering ghost slices, nested sequences, custom fields, and accessibility identity. | + +Phase 3 must keep the legacy tests green while extracting refresh coordination. The planned `ILexicalRefreshCoordinator` is not a current repo type. + +## 2. SliceFactory and Editor Resolution + +| Surface | Current Source | Current Coverage | Missing Before Registry Extraction | +|---|---|---|---| +| Legacy editor factory | [Src/Common/Controls/DetailControls/SliceFactory.cs](Src/Common/Controls/DetailControls/SliceFactory.cs) | `SliceFactoryTests.SetConfigurationDisplayPropertyIfNeeded_Works`; `Create_UnknownEditor_ReturnsMessageSliceWithEditorAccessibleName` | Matrix for common editor keys (`string`, `multistring`, `jtview`, `possatomic`, `custom`, `customwithparams`, `autocustom`) and reuse-map compatibility. | +| Launcher dispatch | [Src/Common/Controls/DetailControls/Slice.cs](Src/Common/Controls/DetailControls/Slice.cs#L2772) plus launcher subclasses | Morph-type launcher click smoke reaches the chooser decision path without opening modal UI | Extracted chooser-result model and chooser adapter tests for full OK/Cancel semantics. | +| Proposed registry | Planned `ILexicalEditorRegistry` boundary | None yet | Contract tests proving unknown editors emit diagnostics, known editors resolve deterministically, and legacy fallback remains available. | + +Do not describe `ILexicalEditorRegistry` or a `GetEditorType` implementation as current code. They are Phase 3 extraction targets. + +## 3. Launchers, Choosers, and Morph Type Swap + +| Surface | Current Source | Current Coverage | Gap / Blocker | +|---|---|---|---| +| Morph classification | [Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs](Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs) | Full `IsStemType` GUID matrix | Covered for current extraction target. | +| Data-loss decisions | [Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs](Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs#L226) and [Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs](Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs#L345) | Empty no-prompt tests plus pure classifiers for stem-name, inflection-class, infix-location, grammatical-info, and rule loss | The final yes/no prompt response still uses `MessageBox.Show`; Phase 3 should put that behind a dialog-service seam. | +| Chooser OK/Cancel | `MorphTypeChooser`, `ReallySimpleListChooser`, `ChooserCommand` | Launcher click path reaches chooser decision code for a valid object | Full OK/Cancel selection semantics remain blocked until modal UI and selection decisions are behind a humble-object or dialog-service seam. | +| Refresh side effects | `MorphTypeAtomicLauncher.SwapValues` | LT-22414 refresh tests | Focus restoration and obsolete-slice disposal need focused tests during Phase 3. | + +The proposed `MorphTypeSwapController` does not exist. Phase 3 should extract from `MorphTypeAtomicLauncher`, starting with classification, data-loss issue detection, chooser result interpretation, and refresh/focus side-effect orchestration. + +## 4. XMLViews Browse, Tables, and Choosers + +| Surface | Current Source | Current Coverage | Missing Tests | +|---|---|---|---| +| Browse host | [Src/xWorks/RecordBrowseView.cs](Src/xWorks/RecordBrowseView.cs) | Existing xWorks tests plus `FilterBar_HeaderAndFilterControlsExposeReachableBaseline` for header order and filter/chooser reachability | Sort-state and keyboard-navigation baselines remain for table migration work. | +| XML table renderer | [Src/Common/Controls/XMLViews/XmlView.cs](Src/Common/Controls/XMLViews/XmlView.cs) | Existing XMLViews reset/refresh tests | UIA2 or equivalent smoke harness before claiming parity. | +| Chooser forms | [Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs](Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs) and [Src/Common/Controls/XMLViews/ChooserCommandBase.cs](Src/Common/Controls/XMLViews/ChooserCommandBase.cs) | Existing isolated chooser tests, not enough for migration parity | Keyboard search, expand/collapse, double-click commit, cancel, invalid target, and transaction rollback. | + +No UIA2/FlaUI/System.Windows.Automation harness was found in the repo. Phase 2 now uses an in-repo smoke substitute for launcher and XMLViews reachability; a full UIA2 parity harness remains a later infrastructure decision. + +## 5. Layout Overrides and Dictionary Configuration + +| Surface | Current Source | Existing Evidence | Required Baselines | +|---|---|---|---| +| Part/layout inventory | `DistFiles/Language Explorer/Configuration/Parts` | Prototype loader/snapshot coverage split to `010-advanced-entry-preview-prototype` | Shipped `LexEntry`, `LexSense`, `Morphology`, `CmPossibility`, and custom-field placeholder fixtures still need foundation-level parity evidence before XML retirement. | +| Runtime layout cache | `LayoutCache` / `Inventory` usage in xWorks and XMLViews | Existing xWorks migrator tests | Prototype default/override fixture coverage lives on `010-advanced-entry-preview-prototype`; selected production override fixtures still need acceptance evidence. | +| Dictionary/reversal configs | [Src/xWorks/DictionaryConfigurationMigrator.cs](Src/xWorks/DictionaryConfigurationMigrator.cs) and migrator tests | Broad migration tests under `Src/xWorks/xWorksTests/DictionaryConfigurationMigrators` | Selected customer-style config fixtures with expected typed IR and failure artifacts. | +| CSS/browser styling | [Src/xWorks/CssGenerator.cs](Src/xWorks/CssGenerator.cs) and XHTML/preview paths | Existing export tests | Explicit decision: outside migrated default path, converted to Avalonia resources, or preserved for legacy preview/export only. | + +Override handling must be evidence-first: every selected fixture needs input XML/CSS, expected typed definition or diagnostic output, and an artifact path on mismatch. + +## 6. AdvancedEntry Avalonia Seams + +The implementation and net8 test evidence for these seams has been split to `010-advanced-entry-preview-prototype`. This foundation branch keeps the seam map so Phase 3 work does not treat prototype coverage as production parity. + +| Seam | Current Source | Current Coverage | Required Before First Editable Slice | +|---|---|---|---| +| Edit session | Prototype branch | Save/cancel/nested-session tests on prototype branch | Decide direct LCModel fenced undo-task vs staged draft semantics; add global undo/redo-after-save tests before product editing. | +| Validation | Prototype branch | Required-field, deterministic order, lazy skip tests on prototype branch | `INotifyDataErrorInfo` or `DataValidationErrors` adapter, localization/resource key, severity, async stale-result suppression. | +| Command/focus | Prototype branch | Local shortcut and view-model command tests on prototype branch | Text-editor focus/caret restore and popup focus return remain Phase 6 control work. XCore bridge remains shell-phase work. | +| UI scheduling | Prototype branch | Headless dispatcher tests on prototype branch | Thin scheduler fake with cancellation, exception propagation, and no false completion for `Post`. | +| Lifetime | Prototype branch | Save/cancel lifetime, late-loader disposal, close cancellation, and DataContext unsubscribe checks on prototype branch | Broader leak instrumentation remains for shell/global lifetime work. | + +## 7. Snapshot Normalization + +| Surface | Current Source | Current Coverage | Remaining Phase 4 Work | +|---|---|---|---| +| Presentation IR semantic snapshots | Prototype branch | Normalized LexEntry detail snapshot coverage moved to `010-advanced-entry-preview-prototype` | Replace placeholders with first-class class/flid/object/writing-system metadata once the typed definition model carries them; add foundation-level fixtures before claiming production parity. | + +## 8. Hard Gates Before Phase 3 Refactor + +Phase 3 seam extraction should not start for a surface until one of these is true: + +1. The current behavior has executable characterization tests listed in this map. +2. The gap is explicitly blocked by a planned seam and the Phase 3 task includes the first test to write. +3. The behavior is consciously deferred with owner/risk notes in the relevant plan doc. + +Additional global gates: + +- `git diff --check` must be clean. +- Relevant `./test.ps1 -TestProject ...` commands must pass for touched areas. +- `openspec validate lexical-edit-avalonia-migration --strict` must pass after task/doc changes. +- Any default-path migration claim must include a forbidden-symbol audit, Graphite/native viewing proof, accessibility metadata checks, localization/resource checks, and rollback/default-off evidence. \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/design.md b/openspec/changes/lexical-edit-avalonia-migration/design.md new file mode 100644 index 0000000000..657bd6142c --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/design.md @@ -0,0 +1,221 @@ +## Context + +Lexical Edit currently depends on a WinForms/DataTree/DetailControls stack that interprets XML Parts/Layout into `Slice` controls, launchers, chooser dialogs, nested `ViewSlice` content, and Views-backed rendering. The Advanced Entry Speckit work under `specs/010-advanced-entry-view/` already proves several useful ideas: a net8 Avalonia module, Preview Host, Presentation IR, XML contract loading, caching, headless tests, and parity checklist. The new target is larger: migrate the real Lexical Edit surface while preserving user interaction, density, writing-system behavior, and customizability, then retire XML after the Avalonia switch is proven. + +This branch is the foundation slice: it documents the architecture and keeps legacy characterization tests that protect Phase 3 refactors. The net8 Preview Host/AdvancedEntry prototype is intentionally split to `010-advanced-entry-preview-prototype`, and product launcher wiring is intentionally split to `010-advanced-entry-product-launcher-spike`. + +Important current constraints: +- `DataTree`, `Slice`, `SliceFactory`, launchers, `RecordEditView`, XMLViews browse/table views, and xCore mediator behavior are tightly coupled. +- XML Parts/Layout carries real customer customizations and behavior such as custom fields, ghost items, visibility rules, and chooser hints. +- Render verification exists for WinForms/DataTree pixel and timing baselines, but it needs semantic snapshots to compare legacy, IR, and Avalonia outputs. +- Native Views/C++ viewing/rendering remains a real dependency for legacy regions. Avalonia migration is only complete for a region after that region no longer instantiates or calls native viewing/rendering/editor code for display, layout, measurement, hit testing, selection, scrolling, or editor realization. +- Graphite is present in the native Graphite/Views rendering path (`GraphiteEngine`, `GraphiteSegment`, render-engine selection), writing-system UI/storage (`IsGraphiteEnabled`, `DefaultFontFeatures`, `FontEngines.Graphite`), render tests, sample/dist assets, and build/package artifacts. +- Gecko/XULRunner is initialized during FieldWorks startup with `gfx.font_rendering.graphite.enabled = true`; `XWebBrowser` and `GeckofxHtmlToPdf` support XHTML preview, print, and PDF/export paths. +- Avalonia offers headless testing, `TextBox`, `TreeView`, `TreeDataGrid`, `ItemsRepeater`, `FlyoutBase`/context menus, styles, `FontFeatures`, and custom font hooks, but FieldWorks needs owned controls for dense, writing-system-aware editing. + +## Goals / Non-Goals + +**Goals:** +- Make Lexical Edit refactorable and testable before replacing major UI surfaces. +- Use XML Parts/Layout as an import/compatibility contract during transition, not as the final runtime abstraction. +- Introduce typed view-definition and Presentation IR interfaces suitable for dependency injection, semantic parity tests, and Avalonia rendering. +- Preserve interaction behavior, information density, writing-system fonts, OpenType/HarfBuzz shaping behavior, nested structures, popup choosers, table views, and TreeView-heavy views. +- Decommission native Graphite/rendering from the default Lexical Edit path: Graphite work starts when the migration starts, and Avalonia does not become the default screen until Graphite dependencies are classified and either replaced, retained behind legacy fallback/export boundaries, or blocked with explicit diagnostics and rollback. +- Decommission C++ viewing/rendering dependencies by migrated region so completed Avalonia regions do not use native Views, `RootSite`, `IVwEnv`, `ManagedVwWindow`, or equivalent C++ display/layout/editor infrastructure at runtime. Custom linguistics services may remain when they are exposed through explicit service seams and do not own Avalonia viewing or editing surfaces. +- Extend render verification to capture semantic output, not only pixels and timings. + +**Non-Goals:** +- No one-shot rewrite of DataTree, XMLViews, and Lexical Edit. +- No immediate XML deletion. XML retirement waits for migration tooling and parity gates. +- No global native Views deletion before all consumers are migrated or explicitly retained. During transition, native Views can remain for non-migrated regions and baseline comparison, but not inside a completed Avalonia region. +- No unproven Graphite compatibility claim in Avalonia. Graphite-only fonts and feature strings are migration inputs to audit, warn, convert where possible, replace, or explicitly block; they are not assumed safe runtime targets. +- No promise of exact pixel parity with WinForms/C++ Views. The target is near-pixel parity with stable interaction semantics and density. + +## Decisions + +### 1. Refactor first, then Avalonia + +**Decision:** Sequence the work as test coverage and seams, then simple controls/popups, then table views, then slices and full Lexical Edit views. + +**Rationale:** DataTree refresh, slice creation, launcher behavior, and XML resolution are high-risk hidden dependencies. Avalonia work that starts by wrapping `DataTree` would preserve the wrong boundary and make regressions harder to identify. + +**Alternatives considered:** +- Direct Avalonia rewrite: too risky because XML semantics, refresh behavior, and chooser logic would be reimplemented without baselines. +- Embed Avalonia inside existing slices: useful for isolated experiments, but not a migration architecture. + +### 2. Typed view definition is the long-term contract + +**Decision:** Introduce a managed typed view-definition model and Presentation IR. XML Parts/Layout is imported into this model during transition; Avalonia consumes the typed model, not XML or WinForms slices. + +**Rationale:** This keeps customer customizations alive while creating a clean boundary for DI, tests, and eventual XML retirement. It also lets the render framework compare legacy XML-derived output with future non-XML definitions. + +**Alternatives considered:** +- Keep XML as permanent contract: preserves compatibility but does not solve maintainability. +- Pure LCModel metadata-generated UI: attractive for 90% model-following, but insufficient for current grouping, ghost items, and chooser behavior without many overrides. + +### 3. Owned Avalonia controls over permanent generic PropertyGrid dependence + +**Decision:** Use the existing PropertyGrid path as a bootstrap, then move Lexical Edit to FieldWorks-owned dense controls over IR nodes. + +**Rationale:** Stock property grids are poor fits for nested senses/examples, multi-writing-system alternatives, custom chooser flyouts, dense table rows, TreeView nodes with multiple translations, and FieldWorks-specific text behavior. + +**Framework grounding:** Avalonia supports headless tests, `TextBox` input, `TreeView`/`TreeDataTemplate`, `TreeDataGrid` template columns, `ItemsRepeater`, flyouts/context menus, styles/classes, `FontFeatures`, and font-manager hooks. Those are enough for much of the UI, but the composition and editor registry should be FieldWorks-owned. + +### 4. UIA2 for legacy smoke, Avalonia.Headless for new UI + +**Decision:** Use UIA2/FlaUI-style automation to baseline WinForms workflow reachability and accessibility metadata. Use Avalonia.Headless for Avalonia control behavior, input, and selected screenshots. Put business logic in unit/integration tests. + +**Rationale:** WinForms owner-drawn and Views-backed content is not deeply inspectable via UIA. UIA2 can still drive focus, menus, chooser launch, dialogs, and table headers. Avalonia.Headless gives invisible input tests and optional frame capture for the new UI. + +### 5. Semantic parity is a first-class render artifact + +**Decision:** Extend render verification with semantic snapshots: visible fields, labels, object/flid binding, editor kind, ghost state, expansion state, focus order, accessibility identity, writing-system metadata, and timing buckets. + +**Rationale:** Pixel comparisons catch visual regressions but do not explain whether a missing field is an XML compiler issue, slice filtering issue, editor registry issue, or text rendering difference. + +### 6. C++ viewing/rendering decommissioning is a regional completion gate + +**Decision:** A migrated Avalonia region is not complete until it has no runtime dependency on native Views/C++ code that owns viewing, display, layout, measurement, hit testing, selection, scrolling, or editor realization. Legacy C++ viewing/rendering may remain temporarily for non-migrated regions and for baseline comparison, but completed Avalonia regions must render and edit through Avalonia-managed controls and text services. + +**Rationale:** This keeps the end state honest. If Avalonia renders a surface but still relies on `RootSite`, `IVwEnv`, `ManagedVwWindow`, or the native Views box/render pipeline for core display, the migration has only wrapped the old system rather than replaced it. + +**Feasibility:** This is feasible by region if we treat C++ viewing/rendering removal as a phased dependency audit rather than a single repo-wide deletion. With Graphite decommissioned instead of supported, the meaningful choices move to font replacement, OpenType feature storage, and shared native Views consumers. Custom linguistics engines such as XAmple, spelling, parser/conversion tools, ICU, or Encoding Converters can remain when wrapped as services outside the Avalonia render/editor path. Physical deletion of shared native Views code can happen only after every consumer outside the region is migrated or intentionally retained. + +**Alternatives considered:** +- Keep native Views under Avalonia for hard text/layout cases: faster short term, but violates the migration goal and keeps the C++ viewing/rendering dependency alive. +- Delete native Views globally first: not feasible because other FieldWorks regions still depend on it and would lose functionality before replacements exist. + +### 7. Graphite and native rendering are evidence-gated before Avalonia becomes default + +**Decision:** Graphite/native-rendering decommissioning begins at the start of the Lexical Edit Avalonia migration. The default Avalonia Lexical Edit path must not depend on native Graphite render engines, Gecko Graphite rendering, or unclassified Graphite-only feature settings. Legacy fallback/export consumers may remain only when explicitly classified outside the migrated default path. + +**Rationale:** Avalonia documents custom TrueType/OpenType fonts and OpenType `FontFeatures`, but that does not prove FieldWorks Graphite parity. HarfBuzz Graphite2 shaping requires HarfBuzz to be built with Graphite2 enabled, and HarfBuzz documentation says that support is not enabled by default. Graphite behavior therefore needs fixture evidence, not assumption. + +**Research map:** The decommissioning scope includes `Src/views/lib/GraphiteEngine.*`, `Src/views/lib/GraphiteSegment.*`, `RenderEngineFactory`, Graphite feature UI/storage, persisted writing-system flags/features such as `IsGraphiteEnabled` and `DefaultFontFeatures`, Graphite-specific tests/docs/sample fonts, build/package artifacts, Gecko startup preference `gfx.font_rendering.graphite.enabled`, `XWebBrowser` preview consumers, and `GeckofxHtmlToPdf`/`FieldWorksPdfMaker` print/PDF assumptions. + +**Feasibility:** Feasible by region, but intentionally disruptive for Graphite-only fonts. There is no automatic lossless Graphite-to-OpenType conversion. The migration must identify affected projects/fonts, provide replacement OpenType fonts or explicit user-facing compatibility warnings, and block default enablement for unsupported cases. + +**Alternatives considered:** +- Assume Avalonia/HarfBuzz covers Graphite: rejected because official docs make Graphite2 shaping build-dependent. +- Keep Gecko only for Graphite previews/PDFs in the default workflow: rejected for migrated default Lexical Edit, but possible as a classified legacy/export boundary. + +### 8. Region manifests and hard gates define completion + +**Decision:** Every migrated Avalonia region must have a region manifest before implementation: entry points, typed view-definition sources, allowed legacy adapters, forbidden native viewing/rendering and Graphite symbols, retained custom linguistics service dependencies, parity fixtures, customer override fixtures, accessibility IDs, performance budgets, default-switch gates, and rollback behavior. + +**Rationale:** “Migrated region” needs to be measurable. A manifest turns architecture intent into a testable contract and prevents accidental native Views, Graphite, WinForms, Gecko, or runtime XML dependencies from slipping through a narrow visible slice. + +### 9. Avalonia platform services are explicit ports + +**Decision:** Avalonia regions use explicit platform-facing ports for UI dispatch, region lifetime/disposal, focus navigation, command routing, edit sessions, validation, undo/redo grouping, design/preview data, styling resources, and accessibility metadata. These ports are introduced while WinForms remains the default so legacy behavior can be characterized first. + +**Rationale:** Avalonia has different threading, focus, command, validation, popup, and lifetime behavior than WinForms. If those seams remain implicit, the first “simple” editor will inherit DataTree and xCore assumptions through the side door. + +### 10. Full shell/window replacement is a separate phase-two change + +**Decision:** This change remains scoped to Lexical Edit regional migration. Replacement of FieldWorks startup, main windows, shell composition, menus, toolbars, navigation, dialogs, and all main screens is tracked separately by `fieldworks-avalonia-shell-migration`. + +**Rationale:** Lexical Edit proves the hardest regional rendering/editor path. The app shell touches different risks: application lifetime, multi-window behavior, xCore command routing, project startup/shutdown, dialog ownership, persisted layout, global services, installer/runtime packaging, and remaining main screens. Splitting keeps both plans reviewable and gives the shell phase concrete prerequisites. + +### 11. Seam recommendations are fixed in dedicated capability specs with explicit phase timing + +**Decision:** This change records seam recommendations in dedicated capability specs: `avalonia-edit-sessions`, `avalonia-undo-redo`, `avalonia-validation`, `avalonia-command-focus`, `avalonia-ui-scheduler`, and `avalonia-lifetime`. Detailed comparison notes, alternatives considered, current/proposed status, and source references are tracked in `seam-recommendations.md`. + +**Rationale:** Edit sessions, undo/redo, validation, command/focus, scheduler, and lifetime are the places where a migration can quietly hard-code a wrong abstraction. Freezing the recommendation and the pivot options in dedicated specs makes those choices reviewable, testable, and reusable by the later shell change. + +**Phase map:** +- Up front and before non-view Avalonia code spreads: introduce `avalonia-ui-scheduler` and `avalonia-lifetime` seams only where tests need UI-thread marshalling, cancellation, disposal, or late-callback control. +- First editable slice: apply `avalonia-edit-sessions`, `avalonia-undo-redo`, `avalonia-validation`, and the screen-local phase of `avalonia-command-focus` before scaling to broader editable regions. +- Phase-two shell migration: invoke the shell-global phase of `avalonia-command-focus`, `avalonia-ui-scheduler`, and `avalonia-lifetime` through `fieldworks-avalonia-shell-migration` instead of redefining them there. +- Deferred or separate-track options: package-first edit sessions, package-first undo/redo, and heavy region-lifetime frameworks remain available only if the pivot triggers documented in `seam-recommendations.md` are met. + +## Native Dependency Classification + +The classification rule is based on the role of the native code, not the implementation language alone. If native code owns what the user is viewing or editing, it is not brought into completed Avalonia regions. If native code supplies custom linguistics capability that supports FieldWorks' role in documenting many languages, it may remain behind an explicit service seam. + +- **Native Views layout/render/editing:** `VwRootBox`, `IVwEnv`, `IRenderEngine`, selections, hit testing, `OnTyping`, `OnExtendedKey`, table layout, interlinear layout, and RootSite editing are not mere windowing. They are the render/editor pipeline and remain a hard removal gate for migrated regions. +- **Custom linguistics services:** XAmple, spelling, parser/conversion engines, ICU, Encoding Converters, and similar language-documentation capabilities are allowed to remain in C++ or native/external form when invoked through managed service boundaries. Avalonia may consume their results, but it must not depend on their UI, rendering, or RootBox integration. +- **Spell-check interop:** `RootSite` wires `SetSpellingRepository(IGetSpellChecker)` into `VwRootBox`, while managed helpers build spelling context menus. Avalonia can keep spelling as a service, but any dependency on RootBox spell integration must be replaced for migrated regions. +- **Parser/conversion/native utility tools:** `pcpatr64.exe`, `TonePars64.exe`, `xample.dll`, Encoding Converter native files, ICU artifacts, Expat/ParserObject, and reg-free COM/proxy/stub build infrastructure are real native dependencies. They are not default Lexical Edit viewing dependencies, but migrated workflows that invoke them must wrap them as services and keep them outside Avalonia rendering/editor completion gates. + +## Interface Direction + +Early seams should stay narrow and name the FieldWorks domain they protect: + +- `ILexicalRefreshCoordinator` for refresh/postponed `PropChanged` behavior. +- `IViewDefinitionSource`, `IXmlViewDefinitionImporter`, `IViewDefinitionCompiler`, `IViewDefinitionCache`, and `IViewDefinitionDiagnostics` for the XML-to-typed transition. +- Proposed `ILexicalEditorRegistry`, `EditorDescriptor`, and `ILexicalEditorFactory` boundaries for resolving legacy slices now and future Avalonia editors later. +- Proposed `IEditSession` or `IEditTransactionCoordinator` boundaries for LCModel transactions, validation, cancellation, undo/redo grouping, and dirty-state command enablement. The current AdvancedEntry code uses a concrete fenced edit session. +- Proposed `IXCoreCommandBridge`, `IPropertyStateStore`, `IRecordNavigationContext`, `IUiScheduler`, `IFocusNavigationService`, and `IRegionLifetime` boundaries for current xCore/DataTree behaviors that must not be hidden inside a single broad context object. +- Feature-specific custom linguistics ports such as `ISpellingService`, `IMorphParserService`, and `IEncodingConversionService` only when a migrated editor actually needs them. + +## Architecture Diagrams + +See [architecture-diagrams.md](architecture-diagrams.md) for Mermaid diagrams covering the current WinForms/DataTree architecture, MVC pressure, dependency-inversion seams, testing layers, optional first Avalonia slices, table/full Lexical Edit slices, and the final audited Avalonia default architecture. + +See [seam-recommendations.md](seam-recommendations.md) for the accepted seam recommendations, the three options compared for each seam, references used, and the pivot triggers that would justify changing direction later. + +## Refactoring Split Options + +### Option A: Safety-first legacy seams + +Split by existing risk: test coverage and docs, refresh/DataTree services, launcher humble objects, editor registry, semantic render capture, then Avalonia controls. + +**Best for:** Regression safety and small PRs. +**Trade-off:** Slower time to visible Avalonia progress. + +### Option B: Contract-first migration + +Split by the new architecture: view-definition schema, XML importer, IR compiler/cache, semantic parity harness, then Avalonia renderer/editor registry. + +**Best for:** Fast progress on XML retirement and future non-XML definitions. +**Trade-off:** More risk if DataTree refresh/launcher seams are not stabilized first. + +### Option C: Vertical thin slice + +Pick a representative lexical path, such as LexEntry morph type plus nested sense gloss and one popup chooser: baseline legacy, compile XML to IR, render/edit in Avalonia Headless, compare semantic/pixel artifacts. + +**Best for:** Proving the end state early and exposing framework gaps. +**Trade-off:** Leaves broad legacy debt in place and can tempt ad hoc special cases. + +**Recommendation:** Use Option A for the first two refactor PRs, then Option C for the first full Avalonia slice, while building the typed view-definition pieces from Option B as shared infrastructure. + +## Risks / Trade-offs + +- XML import drift from legacy behavior -> Mitigate with semantic snapshots and parity tests against production layouts and user-override fixtures. +- Refresh protocol regressions -> Extract/cover refresh coordination before UI replacement. +- TreeView/table complexity -> Spike dense custom item templates, TreeDataGrid license/version implications, and owned virtualized row templates early. +- Graphite/native rendering decommissioning -> Begin the inventory at migration start and block Avalonia default until Graphite engines, feature UI/storage, sample fonts, Gecko Graphite prefs, PDF/export assumptions, and tests/docs are classified, replaced, moved behind legacy boundaries, or blocked with diagnostics. +- Gecko/browser rendering -> `XWebBrowser`, dictionary/configuration previews, interlinear configuration previews, print, and `GeckofxHtmlToPdf` need a non-Graphite replacement or an explicit non-default legacy boundary. +- PropertyGrid limits -> Treat it as a prototype path; do not let it define the final IR or UI shape. +- Automation flakiness -> Keep UIA2 tests thin; use model/semantic assertions for deep behavior. +- XML retirement too early -> Gate deletion on migration tooling, custom-field coverage, user overrides, ghost behavior, chooser parity, and fallback ability. +- C++ viewing/rendering removal exposes text/layout gaps -> Gate each region on dependency audits and replacement services for text shaping, selection, measurement, hit testing, scrolling, and printing/export behaviors where applicable. Do not count custom linguistics service calls as blockers unless they own UI viewing/editing behavior. +- Over-broad interfaces -> Prefer small domain ports proven by legacy characterization tests; avoid a single “context” service that preserves DataTree/Mediator/PropertyTable coupling. +- Undo/redo, focus, keyboard/IME, and lifecycle regressions -> Treat edit sessions, command routing, UI dispatch, focus restoration, and disposal/unsubscribe behavior as first-slice gates, not cleanup work. +- Typed IR becomes a second UI framework -> Version the core view definition, keep instance presentation state separate, and add diagnostics for behavior the IR cannot express yet. + +## Migration Plan + +1. Freeze current behavior with targeted unit/integration/render/UIA2 baselines, including undo/redo, focus, keyboard/IME, accessibility, localization, customer overrides, and disposal behavior. +2. Introduce DI-friendly services around DataTree refresh, view-definition source/import/compile/cache, editor selection, command/property/navigation state, edit sessions, UI dispatch, lifetime, LCModel access, and launcher logic, following `avalonia-ui-scheduler`, `avalonia-lifetime`, and the local phase of `avalonia-command-focus`. +3. Start Graphite/native rendering decommissioning: inventory affected project settings, fonts, render engines, Gecko/PDF paths, tests, docs, and build artifacts; prove no default-path claim depends on unverified Graphite behavior. +4. Define migrated-region manifests and hard gates for each proposed Avalonia region. +5. Extend render verification with normalized semantic snapshots, visual/timing evidence, performance budgets, and failure bundles. +6. Build typed view-definition and XML import as the compatibility compiler. +7. Replace text foundation, simple controls, edit sessions, validation, undo/redo routing, and hover/popups in Avalonia using owned editor controls, following `avalonia-edit-sessions`, `avalonia-validation`, `avalonia-undo-redo`, and the local phase of `avalonia-command-focus`. +8. Replace table/browse views with virtualized Avalonia table/tree structures. +9. Replace slices and full Lexical Edit views with Avalonia surfaces over the typed contract. +10. Audit the migrated region's runtime call graph and remove/disable native viewing/rendering/editor dependencies for that region, while classifying custom linguistics engines as service seams when they do not own the Avalonia UI surface. +11. Add managed canonical view-definition authoring and migration tooling. +12. Retire runtime XML only after parity gates pass for production layouts, custom fields, user overrides, dynamic editors, unsupported constructs, and fallback behavior. +13. Invoke the shell-global phase of `avalonia-command-focus`, `avalonia-ui-scheduler`, and `avalonia-lifetime` through `fieldworks-avalonia-shell-migration` once Lexical Edit regional seams are proven. + +## Open Questions + +1. Should the canonical post-XML view-definition format be C# builders, JSON/YAML, resources, database-backed project settings, or a hybrid? +2. Which shipped/sample/customer fonts and writing systems require replacement or migration because they depend on Graphite-only shaping or feature IDs? +3. Is `TreeDataGrid` acceptable for any Lexical Edit surface given package/licensing/version constraints, or should FieldWorks own all dense tree/table rows? +4. Which customer layout override fixtures should become mandatory migration tests? +5. Which non-Lexical Edit consumers keep native Views alive after Lexical Edit regions are migrated, and what is the repo-wide deletion plan once those consumers are addressed? +6. Which browser/PDF engine or legacy boundary will own XHTML preview, print, and PDF behavior after default Lexical Edit moves away from Gecko/Graphite assumptions? diff --git a/openspec/changes/lexical-edit-avalonia-migration/graphite-decommissioning.md b/openspec/changes/lexical-edit-avalonia-migration/graphite-decommissioning.md new file mode 100644 index 0000000000..fdcd9d6083 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/graphite-decommissioning.md @@ -0,0 +1,68 @@ +# Graphite and Native Rendering Decommissioning Plan + +This plan treats Graphite and native Views rendering as a migration risk, not as a solved problem. The key correction from re-research: Avalonia using Skia/HarfBuzz does not automatically prove Graphite parity. HarfBuzz Graphite2 shaping is optional and its own documentation says Graphite2 support is currently not enabled by default when building HarfBuzz. + +## 1. Current Repo Inventory + +| Area | Source / Symbol | Role | Migration Classification | +|---|---|---|---| +| Native Graphite render engine | [Src/views/lib/GraphiteEngine.cpp](Src/views/lib/GraphiteEngine.cpp), `GraphiteEngineClass`, `FwGrEngine`, `GraphiteSegment` | Current native Views Graphite shaping/rendering path. | Default-path blocker until Avalonia text shaping evidence exists. | +| Native Graphite segmenting | [Src/views/lib/GraphiteSegment.cpp](Src/views/lib/GraphiteSegment.cpp), [Src/views/lib/GraphiteSegment.h](Src/views/lib/GraphiteSegment.h) | Segment data, glyph metrics, and render behavior. | Baseline source for parity fixtures. | +| Render factory | [Src/Common/FwUtils/RenderEngineFactory.cs](Src/Common/FwUtils/RenderEngineFactory.cs) | Chooses Graphite vs Uniscribe render engines based on writing-system/font configuration. | Hidden dependency audit target. | +| COM/native interfaces | [Src/views/lib/Render.idh](Src/views/lib/Render.idh), `IRenderEngine`, `IRenderEngineFactory` | COM interfaces for native rendering. | Forbidden from migrated Avalonia default path unless intentionally bridged and documented. | +| Writing-system storage | `IsGraphiteEnabled`, `DefaultFontFeatures`, `FontEngines.Graphite` usages across LCModel/Common | Stores project/language rendering preferences and feature strings. | Must be preserved in snapshots or diagnostics. | +| Browser/PDF paths | `GeckofxHtmlToPdf`, `FieldWorksPdfMaker`, PDF `--graphite` flags, XHTML preview/export | Non-edit rendering/export surfaces may still require legacy Graphite behavior. | Separate from Lexical Edit default path; do not remove during first migration. | +| Packaging/build | Graphite2/HarfBuzz native libraries under build/package scripts | Determines which shaping libraries are present. | Packaging audit required before any claim of parity. | + +## 2. External Documentation Findings + +- HarfBuzz building docs: Graphite2 support is controlled by the build option `-Dgraphite=enabled`; the default is not enabled. +- HarfBuzz Graphite2 integration docs: Graphite features work only when HarfBuzz was compiled with the Graphite2 shaping engine enabled. +- Avalonia font docs: Avalonia supports TrueType/OpenType custom fonts and OpenType `FontFeatures`, but the docs do not establish FieldWorks Graphite feature parity. + +Conclusion: the migration may use Avalonia text rendering for many scripts, but Graphite parity must be proven with actual fonts, writing-system metadata, and packaged native shaping support. + +## 3. Default-Path Policy + +The migrated Lexical Edit default path MUST NOT depend on these symbols unless a specific exception is approved and tested: + +- `System.Windows.Forms.Control`, `DataTree`, `Slice`, `RootSiteControl`, `XmlView`, `BrowseViewer`. +- `IVwRootBox`, `IVwEnv`, `IVwGraphics`, `IRenderEngine`, `IRenderEngineFactory`. +- `GraphiteEngineClass`, `UniscribeEngineClass`, `FwGrEngine`, `GraphiteSegment`. +- `GeckoWebBrowser`, `XWebBrowser`, `GeckofxHtmlToPdf`, `FieldWorksPdfMaker`. +- Global COM registration or registry hacks. + +Legacy code may remain for non-migrated views, preview/export, tests, and rollback. The policy applies only to the new migrated default path. + +## 4. Required Evidence Before Decommissioning + +| Evidence | Required Checks | +|---|---| +| Repository audit | Search default-path projects for forbidden symbols; document any allowed baseline/test-only references. | +| Packaging audit | Confirm the exact HarfBuzz/Skia/native library build includes or excludes Graphite2 support; record the binary/source evidence. | +| LDML fixture scan | Identify writing systems with `IsGraphiteEnabled`, `DefaultFontFeatures`, Graphite-only features, right-to-left scripts, complex scripts, and custom fonts. | +| Rendering fixtures | Capture representative words/forms for Graphite-enabled fonts and compare legacy screenshot/metrics with Avalonia output where feasible. | +| Fallback behavior | For unsupported Graphite features, produce visible diagnostics and a rollback path rather than silently changing rendering. | +| Browser/PDF decision | Classify each browser/PDF/export path as legacy-retained, migrated later, or out of scope. | + +## 5. Phased Plan + +| Phase | Work | Exit Gate | +|---|---|---| +| Phase 1 inventory | Complete source and symbol inventory for native Views, Graphite, browser/PDF, writing-system metadata, and package inputs. | Inventory has source-backed entries and no unsupported Avalonia/HarfBuzz assumptions. | +| Phase 2 characterization | Add fixtures for Graphite-enabled writing systems and text samples, plus current native/default path coverage report. | Fixtures identify which scripts/fonts are safe, risky, or blocked. | +| Phase 3-5 first-slice migration | Keep Graphite and native Views as legacy fallback while the first Avalonia editor uses only proven text paths. | Default path forbidden-symbol audit passes or lists explicit approved exceptions. | +| Phase 6-8 parity expansion | Add measured Avalonia rendering evidence for complex scripts, IME, RTL, and Graphite feature scenarios. | User-visible rendering differences are accepted, fixed, or blocked with rollback. | +| Phase 9 retirement | Remove or disable a legacy rendering dependency only after all consumers are classified. | Browser/PDF/export and rollback owners sign off; full build/test/package checks pass. | + +## 6. Test Matrix + +| Scenario | Minimum Test | +|---|---| +| Graphite-enabled WS with feature string | Fixture loads WS metadata, text sample renders, and diagnostics record Graphite capability. | +| OpenType-only font features | Avalonia `FontFeatures` path preserves documented OpenType features where supported. | +| Graphite-required feature unsupported | UI exposes deterministic diagnostic and keeps rollback/default-off path. | +| Mixed writing systems | Per-run and per-span metadata is preserved in the typed presentation snapshot. | +| Packaging drift | CI or agent script records native shaping library versions/options for the build under test. | + +This plan intentionally avoids promising Graphite decommissioning as part of the first editable slice. The first milestone is knowing exactly where Graphite matters and preventing silent rendering regressions. \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/migration-map.md b/openspec/changes/lexical-edit-avalonia-migration/migration-map.md new file mode 100644 index 0000000000..eb67652efb --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/migration-map.md @@ -0,0 +1,15 @@ +# Migration Map: Speckit Advanced Entry to OpenSpec Lexical Edit + +This change migrates the useful Speckit material from `specs/010-advanced-entry-view/` into OpenSpec while expanding scope from Advanced New Entry to the full Lexical Edit Avalonia migration. + +| Speckit source | OpenSpec destination | Notes | +|---|---|---| +| `spec.md` | `proposal.md`, `specs/lexical-edit-avalonia-migration/spec.md` | Reframed from Advanced New Entry to full Lexical Edit migration. Existing FR/SC ideas become phased migration requirements and acceptance gates. | +| `plan.md` | `design.md`, `tasks.md` | Carries over Path 3, Preview Host, cache/async/virtualization, and headless testing, but changes XML from long-term contract to transitional import source. | +| `research.md` | `design.md`, `specs/lexical-edit-view-definition/spec.md` | Preserves Avalonia/.NET 8, diagnostics, validation, and IR direction. | +| `presentation-ir-research.md` | `design.md`, `specs/lexical-edit-view-definition/spec.md` | Preserves Inventory/LayoutCache/XMLViews reuse research and adds XML retirement gates. | +| `parity-lcmodel-ui.md` | `specs/lexical-edit-parity-automation/spec.md`, `tasks.md` | Becomes the baseline checklist for semantic parity scenarios. | +| `tasks.md` | `tasks.md` | Existing completed Advanced Entry spike tasks are treated as prior art; new tasks sequence refactor-first Lexical Edit migration. | +| `quickstart.md` | Future implementation quickstart | Not copied yet because this OpenSpec change is architectural and phased. | +| `data-model.md` | Future typed view-definition data model | Not copied verbatim; its concepts feed the IR/view-definition requirements. | +| `contracts/openapi.yaml` | Not migrated | Internal API contract is too narrow for the broader Lexical Edit architecture. Revisit if a service boundary becomes externally callable. | diff --git a/openspec/changes/lexical-edit-avalonia-migration/override-fixtures.md b/openspec/changes/lexical-edit-avalonia-migration/override-fixtures.md new file mode 100644 index 0000000000..18c5b1eab1 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/override-fixtures.md @@ -0,0 +1,67 @@ +# Customer and User Override XML Fixtures + +This document defines the fixture plan for preserving FieldWorks project/user layout overrides while migrating Lexical Edit toward typed view definitions and Avalonia controls. It is a test plan, not just an inventory. + +## 1. Override Sources to Preserve + +| Source | Examples / Paths | Why It Matters | +|---|---|---| +| Shipped detail layouts | `DistFiles/Language Explorer/Configuration/Parts/*.fwlayout`, `*Parts.xml` | Baseline LexEntry/LexSense/Morphology behavior, labels, visibility, ghost slices, and custom-field placeholders. | +| Project dictionary configs | `/Configuration/Dictionary/*.xml` | User-edited dictionary publication layouts, columns, before/between/after text, writing-system options, list options, and shared nodes. | +| Project reversal configs | `/Configuration/ReversalIndex/*.xml` | Reversal-specific labels, headword/gloss options, writing-system choices, and migrated historical config variants. | +| Historical configs | `PreHistoricMigrator`, `FirstAlphaMigrator`, `FirstBetaMigrator`, and related xWorks tests | Old projects must migrate into the typed-definition path without losing custom choices. | +| CSS overrides | `ProjectDictionaryOverrides.css`, `ProjectReversalOverrides.css` | Legacy preview/export styling. The migrated edit surface must explicitly classify this as legacy-only, translated, or unsupported with diagnostics. | +| Writing-system/font metadata | LDML writing-system store, `IsGraphiteEnabled`, `DefaultFontFeatures` | Layout and rendering behavior can change when writing-system and font metadata are ignored. | + +## 2. Concrete Fixture Families + +Each family must have input files, expected typed IR or diagnostics, and mismatch artifacts. + +| Fixture Family | Minimum Cases | Expected Assertions | +|---|---|---| +| Shipped `LexEntry-detail-Normal` | Top-level identity fields, lexeme form, citation form, pronunciations, senses | Stable node IDs, class/flid binding, editor kind, visibility, focus order, accessibility ID/name. | +| Nested senses and ghost entries | Senses with subsenses, examples, ghost labels/init methods | Ghost metadata and lazy item templates survive compilation without recursive expansion loops. | +| Custom fields | Entry/sense/allomorph custom scalar, multistring, possibility-list fields | Custom field placeholders resolve deterministically and retain flid/type/writing-system metadata. | +| Dictionary configuration migration | Current, pre-8.3, alpha, beta-style dictionary configs from xWorks tests | Migrator output remains schema-valid and typed-definition import preserves order and labels. | +| Reversal configuration migration | Reversal language variants, subentries, missing or invalid reversal writing systems | Reversal-specific labels and writing-system options are preserved or diagnosed. | +| Duplicate/shared nodes | Shared senses, referenced complex forms, duplicate custom nodes | Stable node IDs remain unique; referenced nodes do not duplicate children accidentally. | +| CSS/browser styling | Dictionary and reversal CSS override samples | Classified as legacy preview/export, converted to Avalonia resources, or reported as unsupported with a diagnostic. | + +## 3. Compiler and Merger Strategy + +The typed-definition compiler must consume an immutable snapshot of fully merged configuration data. It must not read directly from WinForms controls, live `PropertyTable` mutation state, or mutable `LcmCache` objects on background threads. + +Required pipeline: + +1. Load shipped parts/layouts. +2. Apply project/user override precedence using the same semantics as `LayoutCache`/`Inventory` and dictionary migrators. +3. Convert the merged XML to typed view definitions / Presentation IR. +4. Normalize stable comparison keys: node ID, class/flid/object binding, editor kind, writing-system metadata, visibility, ghost metadata, focus order, and accessibility identity. +5. Emit unsupported-construct diagnostics with source file, layout ID, part ref, XML path, and suggested owner. + +## 4. Test and Artifact Requirements + +| Test Type | Required Evidence | +|---|---| +| Snapshot tests | Expected JSON committed for selected fixtures; actual JSON attached on mismatch. | +| Migration tests | Existing xWorks migrator tests remain green; selected fixtures run through the AdvancedEntry typed-definition compiler. | +| Override precedence tests | Same layout/part ID in shipped and project config proves project override wins. | +| Unsupported construct tests | Dynamic/custom editor constructs produce deterministic diagnostics instead of silent drops. | +| Localization tests | User-visible labels come from layout/resource metadata and remain localizable; no new hardcoded production strings. | +| Writing-system tests | Writing-system options, font metadata, direction/culture, and missing WS cases are captured or diagnosed. | + +## 5. Phasing + +| Phase | Goal | Exit Criteria | +|---|---|---| +| Phase 1 inventory | Select fixture families and map existing migrator/layout tests. | Every fixture has source path, owner, behavior being protected, and known current test coverage. | +| Phase 2 characterization | Add semantic baselines before refactor. | DataTree/Slice and Avalonia IR baselines pass; unsupported gaps are documented. | +| Phase 4 typed import | Implement merged XML to typed-definition compiler. | Fixtures compile deterministically or emit approved diagnostics; cache invalidation/cancellation tests pass. | +| Phase 9 retirement | Disable runtime XML for a gated migrated surface. | Import/audit fallback exists, customer override fixtures pass, rollback switch remains available. | + +## 6. Non-Goals for First Slice + +- Do not translate arbitrary user CSS into full Avalonia styling until a real styling compiler is designed. +- Do not delete XML layouts while legacy DataTree/XMLViews still use them. +- Do not claim UIA2 parity without a real UI automation harness. +- Do not treat Graphite-only font settings as harmless; preserve or diagnose them until the rendering policy is proven. \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/phase2-execution-evidence.md b/openspec/changes/lexical-edit-avalonia-migration/phase2-execution-evidence.md new file mode 100644 index 0000000000..4241028126 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/phase2-execution-evidence.md @@ -0,0 +1,113 @@ +# Phase 2 Test Coverage Report + +This is a behavioral coverage report for the Phase 2 "Test Coverage Before Refactor" gate of `lexical-edit-avalonia-migration`. It is not a line-coverage percentage. The goal is to show which Phase 3 refactor surfaces now have executable characterization tests, where the tests live, and which gaps remain too large or too infrastructural to mark complete honestly. + +This narrowed Phase 1/2 branch keeps OpenSpec planning plus legacy WinForms/DataTree/XMLViews characterization coverage. The Avalonia Preview Host, `AdvancedEntry.Avalonia` prototype, and net8 Avalonia.Headless/property-grid tests have been split to `010-advanced-entry-preview-prototype`. Product command/menu wiring has been split to `010-advanced-entry-product-launcher-spike`. The unrelated `RecordList` sorting change was dropped from this scope. + +--- + +## 1. Subagent Audit Result + +Three read-only subagents audited the Phase 3 refactor surfaces before new tests were added: + +- **DataTree refresh and hosting**: found real coverage for LT-22414 but gaps around refresh cancellation, focus order, and nested refresh semantics. +- **Launcher and chooser logic**: found coverage for DataTree refresh after morph swaps, but almost no pure coverage for morph-type classification, chooser cancel/OK paths, data-loss prompts, or SliceFactory fallback behavior. +- **Avalonia edit/session and IR seams**: audited as future/prototype coverage. The implementation and net8 tests now live on `010-advanced-entry-preview-prototype`, not this foundation branch. + +The tests below are the new coverage added from that audit. + +--- + +## 2. Tests Added In This Pass + +### Legacy DetailControls / WinForms Boundary + +- `DoNotRefresh_ClearedRefreshListNeededBeforeRelease_DoesNotRefresh` + - Pins the cancellation behavior of `DataTree.DoNotRefresh` + `RefreshListNeeded` before extracting refresh coordination. +- `IsStemType_*_ReturnsTrue/False` and `IsStemType_NullMorphType_ReturnsFalse` + - Covers the full known stem-like and affix-like `MoMorphTypeTags` GUID matrix before morph-type swap logic is extracted. +- `CheckForStemDataLoss_EmptyStemAndNoMorphSyntaxAnalyses_AllowsChangeWithoutPrompt` and `CheckForAffixDataLoss_EmptyAffixAndNoMorphSyntaxAnalyses_AllowsChangeWithoutPrompt` + - Pin the non-modal no-data-loss branches before extracting prompt decisions from `MorphTypeAtomicLauncher`. +- `GetStemDataLossKinds_StemNameAndGrammarInfo_FlagsBoth` + - Extracts and pins the pure stem-side data-loss classifier for stem-name and grammatical-information loss before modal prompt extraction. +- `GetAffixDataLossKinds_AffixProcessWithInflectionClassAndGrammarInfo_FlagsRuleInflectionClassAndGrammarInfo` + - Pins the affix-process rule, inflection-class, and grammatical-information loss combination without invoking `MessageBox.Show`. +- `GetAffixDataLossKinds_AffixAllomorphWithPositionAndMsEnv_FlagsInfixLocationAndGrammarInfo` + - Pins the affix-allomorph infix-position and morphosyntactic-environment loss combination. +- `LauncherButtonClick_WithValidObject_ReachesChooserDecisionPath` + - Provides a focused WinForms launcher smoke baseline proving a valid launcher button click reaches the chooser decision path without opening the modal chooser. +- `Create_UnknownEditor_ReturnsMessageSliceWithEditorAccessibleName` + - Pins `SliceFactory` fallback behavior before adding an editor registry boundary. +- `CfAndBib_SemanticSliceBaselineCapturesStableBindingsAndFocusOrder` + - Captures legacy DataTree/Slice output order, labels, object/class binding, field/flid binding, editor kind, visibility, expansion state, and accessible control names before DataTree/Slice replacement work. +- `FilterBar_HeaderAndFilterControlsExposeReachableBaseline` + - Adds an in-repo XMLViews smoke baseline for browse-table header order and filter reachability (`Lexeme Form` filter-for path and `Morph Type` chooser filter path) without adding a new UIA2 dependency. + +### Split Avalonia Prototype Boundary + +The following coverage belongs to `010-advanced-entry-preview-prototype`, not this branch: edit-session save/cancel tests, Presentation IR snapshot tests, descriptor metadata tests, validation determinism tests, Avalonia view-model lifetime tests, and snapshot failure artifacts. This branch keeps the plan and identifies those seams, but does not claim the prototype implementation as Phase 1/2 foundation evidence. + +--- + +## 3. Phase 2 Task Status + +| Task | Status | Evidence | Remaining Gap | +| :--- | :--- | :--- | :--- | +| 2.1 DataTree refresh state transitions and postponed `PropChanged` behavior | Covered | `MorphTypeAtomicLauncherTests`: LT-22414 tests plus `DoNotRefresh_ClearedRefreshListNeededBeforeRelease_DoesNotRefresh` | Nested `DoNotRefresh` semantics are still a design question for Phase 3 extraction. | +| 2.2 Launcher pure-logic tests, morph type swap, chooser paths | Covered for Phase 2 | Full `IsStemType` matrix; no-data-loss checks; pure positive data-loss classifiers; launcher click smoke path; morph swap refresh regression tests | Full modal OK/Cancel chooser-result handling remains a Phase 3 seam-extraction target. | +| 2.3 Semantic baseline capture | Partially covered for legacy boundary | `DataTreeTests.CfAndBib_SemanticSliceBaselineCapturesStableBindingsAndFocusOrder` | Typed IR/snapshot normalization moved to the preview prototype branch; ghost-state and override fixture coverage remain future work. | +| 2.4 Focused UIA2 smoke baselines | Not complete; smoke substitute only | `LauncherButtonClick_WithValidObject_ReachesChooserDecisionPath`; `FilterBar_HeaderAndFilterControlsExposeReachableBaseline` | Full UIA2/FlaUI/Appium parity harness remains future work and must not be implied by these in-repo smoke tests. | +| 2.5 Failure artifact bundling | Not covered in this branch | None in the narrowed foundation branch | Snapshot/render artifact bundling moved with the prototype and still needs render-parity evidence later. | +| 2.6 Undo/redo and LCModel transaction characterization | Not covered in this branch | None in the narrowed foundation branch | Edit-session and commit-fence characterization moved to `010-advanced-entry-preview-prototype`. | +| 2.7 Keyboard/IME, focus restoration, accessibility metadata, localization, disposal/unsubscribe | Not covered in this branch | None in the narrowed foundation branch | True text-editor IME, popup focus restoration, accessibility metadata, localization, and disposal coverage remain future/prototype work. | +| 2.8 Snapshot normalization rules | Not covered in this branch | None in the narrowed foundation branch | Normalized Presentation IR snapshots moved to `010-advanced-entry-preview-prototype`; Phase 4 still needs first-class class/flid/object/writing-system metadata. | + +--- + +## 4. Phase 3 Refactor File Coverage + +| Refactor Surface | Files Expected To Change | Current Characterization Tests | Coverage Confidence | +| :--- | :--- | :--- | :--- | +| Refresh coordination (`ILexicalRefreshCoordinator`) | `Src/Common/Controls/DetailControls/DataTree.cs` | `DoNotRefresh_SlicesMustReflectChanges_AfterRelease_LT22414`, `DoNotRefresh_WithoutRefreshListNeeded_DoesNotRefresh_LT22414_BugDemo`, `DoNotRefresh_ClearedRefreshListNeededBeforeRelease_DoesNotRefresh`, `CfAndBib_SemanticSliceBaselineCapturesStableBindingsAndFocusOrder` | Medium. Basic gate behavior and stable slice output are covered; nested/re-entrant behavior is intentionally not locked down yet. | +| Launcher humble object extraction | `Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs` | Full `IsStemType` matrix; pure positive data-loss classifiers; no-data-loss checks; launcher click smoke path; LT-22414 swap refresh tests | Medium/High for pre-seam logic. Modal result interpretation still needs extraction before direct OK/Cancel unit tests. | +| Editor registry boundary | `Src/Common/Controls/DetailControls/SliceFactory.cs` | `SetConfigurationDisplayPropertyIfNeeded_Works`, `Create_UnknownEditor_ReturnsMessageSliceWithEditorAccessibleName` | Low/Medium. Fallback and custom display-property behavior are pinned; common editor dispatch and reuse-map compatibility need more tests before broad registry rewrites. | +| Edit-session and LCModel transaction seam | Future `AdvancedEntry`/edit-session targets | Split to `010-advanced-entry-preview-prototype` | Not foundation-branch evidence. | +| Typed IR and snapshot normalization | Future `PresentationCompiler`/IR targets | Split to `010-advanced-entry-preview-prototype` | Not foundation-branch evidence. | +| Property-grid first-slice candidates | Future property-grid/editor targets | Split to `010-advanced-entry-preview-prototype` | Not foundation-branch evidence. | +| Validation seam | Future validation service targets | Split to `010-advanced-entry-preview-prototype` | Not foundation-branch evidence. | +| Native library/bootstrap for headless tests | Future net8 test bootstrap targets | Split to `010-advanced-entry-preview-prototype` | Not foundation-branch evidence. | + +--- + +## 5. Verification Commands + +```powershell +.\test.ps1 -TestProject "Src/Common/Controls/DetailControls/DetailControlsTests/DetailControlsTests.csproj" +``` + +Result: 88 passed, 1 skipped, 0 failed. +Current Phase 2 rerun: 93 total, 92 passed, 1 skipped, 0 failed. + +```powershell +.\test.ps1 -TestProject "Src/xWorks/xWorksTests/xWorksTests.csproj" +``` + +Current Phase 2 rerun: 1202 passed, 0 failed. + +```powershell +.\test.ps1 +``` + +Earlier broad-branch rerun before the split: 4329 total, 4253 executed/passed, 0 failed. This should be rerun after the narrowed foundation branch is committed. + +```powershell +git diff --check +``` + +Result: no whitespace errors in the current diff. + +```powershell +openspec validate lexical-edit-avalonia-migration --strict +``` + +Result: change is valid. diff --git a/openspec/changes/lexical-edit-avalonia-migration/proposal.md b/openspec/changes/lexical-edit-avalonia-migration/proposal.md new file mode 100644 index 0000000000..38927c19a2 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/proposal.md @@ -0,0 +1,57 @@ +## Why + +Lexical Edit is the main editing surface in FLEx, but its current WinForms/DataTree/XMLViews architecture mixes view definition, control creation, LCModel access, refresh state, and legacy rendering concerns in ways that make the Avalonia migration risky. The existing Advanced Entry Speckit work proves useful pieces of an Avalonia path, but the broader migration needs an OpenSpec plan that treats XML Parts/Layout as a transitional compatibility contract and makes testability/refactoring the first-class work before replacing UI. + +Current branch scope: this Phase 1/2 foundation branch contains OpenSpec planning, migration-review skills, and legacy WinForms/DataTree/XMLViews characterization coverage. The Avalonia Preview Host and `AdvancedEntry.Avalonia` prototype are split to `010-advanced-entry-preview-prototype`; product command/menu wiring is split to `010-advanced-entry-product-launcher-spike`; the unrelated `RecordList` sorting change was dropped. + +## What Changes + +- Migrate the Advanced Entry Speckit research, parity checklist, and task intent into OpenSpec under a broader Lexical Edit migration change. +- Establish a phased migration contract: baseline tests first, legacy refactoring seams second, then Avalonia simple controls/popups, table views, slices, and full Lexical Edit views. +- Introduce a typed, managed view-definition/Presentation IR as the migration boundary. Existing XML Parts/Layout remains an import source during transition; long-term runtime XML dependency is retired only after parity is proven. +- Make native viewing/rendering decommissioning a completion gate for each migrated region: if native code owns display, layout, measurement, hit testing, selection, or editor realization, it SHALL NOT be brought into the completed Avalonia region. Custom linguistics engines and native services such as XAmple, spelling, parser/conversion tools, or similar language-documentation capability may remain when isolated behind service seams outside the Avalonia render/editor path. +- Start Graphite/native-rendering decommissioning with the migration. Avalonia SHALL NOT become the default Lexical Edit screen until Graphite font settings, native Graphite engines, Gecko Graphite rendering, PDF/export assumptions, tests, docs, and build/package artifacts are inventoried, classified, and either replaced, retained behind a legacy boundary, or blocked with explicit diagnostics and rollback. +- Require dependency-injected services around DataTree/Slice/Launcher behavior, view-definition source/import/compile/cache, editor selection, edit sessions, LCModel transactions, undo/redo grouping, validation, command/focus routing, UI dispatch, lifetime/disposal, diagnostics, and render/parity capture. +- Freeze seam-specific recommendations in dedicated capability specs for edit sessions, undo/redo, validation, command/focus, UI scheduling, and lifetime so phase-one lexical work and phase-two shell work consume the same decisions. +- Define migrated-region manifests and hard gates so each claimed Avalonia region has explicit entry points, allowed legacy adapters, forbidden native/Graphite call paths, custom linguistics service dependencies, parity fixtures, performance budgets, and rollback/default-switch rules. +- Extend render verification from pixel/timing snapshots to semantic parity snapshots covering legacy WinForms/DataTree, typed IR, and Avalonia output. +- Define automation strategy: UIA2/FlaUI-style tests for legacy WinForms workflow reachability; Avalonia.Headless tests for new controls; layered unit/integration tests for IR, LCModel, refresh, and transactions. +- Allow Avalonia package updates or targeted upstream/local control work when stock controls cannot preserve FieldWorks density, interaction semantics, OpenType/HarfBuzz text, or TreeView requirements. + +## Non-goals + +- Replacing the full Lexical Edit UI in one change. +- Removing XML Parts/Layout before the typed IR and migration tooling can prove parity for user overrides, custom fields, ghost items, choosers, and nested sequences. +- Replacing LCModel or changing stored lexicon data schemas. +- Treating pixel-perfect WinForms output as the target. The target is near-pixel parity with equivalent information density, font/script behavior, interaction semantics, and accessibility. +- Deleting the global native Views engine before all non-migrated consumers are accounted for. Native rendering may remain as a legacy baseline or for other regions during transition, but it is not acceptable in completed Avalonia Lexical Edit regions. + +## Capabilities + +### New Capabilities + +- `lexical-edit-avalonia-migration`: End-to-end phased migration requirements for Lexical Edit from WinForms/DataTree/XMLViews toward Avalonia. +- `lexical-edit-view-definition`: Typed view-definition and Presentation IR requirements, including XML import during transition, dynamic editor diagnostics, stable identity, virtualization/focus metadata, and XML retirement gates. +- `lexical-edit-parity-automation`: Test, UI automation, render verification, and semantic parity requirements for WinForms and Avalonia migration safety. +- `lexical-edit-font-decommissioning`: Graphite/native rendering classification, OpenType/HarfBuzz font-option migration where supported, Gecko/browser/PDF impact, and native dependency requirements. +- `avalonia-edit-sessions`: FieldWorks-owned edit-session and commit-boundary requirements for editable Avalonia regions, starting from the current direct LCModel fenced-session model. +- `avalonia-undo-redo`: Domain-authoritative undo/redo requirements with control-local leaf undo allowed only as a subordinate behavior. +- `avalonia-validation`: FieldWorks-owned validation seam requirements with Avalonia-native presentation and package-backed rule engines as subordinate options. +- `avalonia-command-focus`: Global command/focus bridge requirements for shell and popup behavior, while allowing screen-local Avalonia commands inside migrated regions. +- `avalonia-ui-scheduler`: Thin UI-thread scheduling seam requirements for non-view layers with direct Avalonia dispatcher use allowed only at the view and startup edge. +- `avalonia-lifetime`: Thin app/window/dialog lifetime seam requirements for non-view layers, with heavier region frameworks explicitly deferred. + +### Modified Capabilities + +- `architecture/ui-framework/views-rendering`: Add semantic parity capture and Avalonia comparison requirements to existing render baseline guidance. +- `architecture/ui-framework/winforms-patterns`: Add DetailControls/DataTree refactoring and UIA2 baseline expectations for legacy WinForms surfaces. +- `architecture/interop/native-boundary`: Add the requirement that migrated Avalonia regions eliminate runtime managed-to-native render interop. +- `architecture/testing/test-strategy`: Add layered UI migration testing expectations using unit/integration tests, UIA2 for WinForms, and Avalonia.Headless for Avalonia. + +## Impact + +- Managed code: `Src/Common/Controls/DetailControls/`, `Src/Common/Controls/XMLViews/`, `Src/xWorks/`, `Src/LexText/`, future/split `Src/LexText/AdvancedEntry.Avalonia/`, future/split `Src/Common/FwAvalonia/`, `Src/Common/RenderVerification/`, and related managed test projects. +- Native code: no native viewing/rendering path is planned for completed Avalonia regions. Existing Views/native rendering remains in baseline and comparison scope until replaced, but the dependency audit for each migrated region must prove there is no runtime call path through native display, layout, measurement, hit testing, selection, or editor-realization code before that region is considered complete. Native custom linguistics services that support FieldWorks' language-documentation mission, such as XAmple, spelling, parser/conversion tools, ICU, or Encoding Converters, may remain as explicit service dependencies when kept outside the Avalonia render/editor boundary. Graphite native code and render-engine selection are explicitly in inventory/decommissioning scope for the migrated default path, while legacy fallback/export consumers are classified separately. +- Browser/export code: Gecko/XULRunner initialization currently enables Graphite rendering and `XWebBrowser`/`GeckofxHtmlToPdf` support preview, print, and PDF flows. Those paths must be audited, replaced, or moved outside the default Avalonia Lexical Edit boundary before default switch. +- Configuration: `DistFiles/Language Explorer/Configuration/Parts/*.fwlayout` and `*Parts.xml` become migration inputs to a managed typed view definition rather than the long-term runtime UI format. +- Dependencies: Avalonia/Avalonia.Headless packages may be updated in sync; owned FieldWorks controls should be preferred over hard-forking third-party controls unless a narrow upstream/local patch is justified. diff --git a/openspec/changes/lexical-edit-avalonia-migration/region-manifest.md b/openspec/changes/lexical-edit-avalonia-migration/region-manifest.md new file mode 100644 index 0000000000..1eb5607207 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/region-manifest.md @@ -0,0 +1,121 @@ +# Avalonia Migration Region Manifest + +The manifest is the contract for what a migrated Lexical Edit region owns, what legacy services it may adapt, and what dependencies are forbidden from the new default path. It is not implemented yet; Phase 3 must introduce it behind a default-off switch and executable audits. + +## 1. Manifest Shape + +Each migrated region should declare: + +| Field | Meaning | +|---|---| +| `regionId` | Stable identifier such as `lexical-edit.entry.identity`. | +| `ownerProject` | Owning project/module, for this change `AdvancedEntry.Avalonia`. | +| `legacySurface` | Legacy host/slice/layout being replaced or wrapped. | +| `enabledByDefault` | `false` until all gates pass for that region. | +| `rollbackSurface` | Legacy view or command used when the migrated region is disabled or fails capability checks. | +| `allowedAdapters` | Narrow legacy services the region may call. | +| `forbiddenSymbols` | Symbols/packages prohibited from production default-path code. | +| `requiredCapabilities` | Rendering, IME, accessibility, validation, undo/redo, localization, and layout override capabilities required for enablement. | +| `testEvidence` | Test files, fixture IDs, audit commands, and last known result. | + +Example draft: + +```json +{ + "regionId": "lexical-edit.entry.identity", + "ownerProject": "AdvancedEntry.Avalonia", + "legacySurface": "LexEntry-detail-Normal identity fields in DataTree", + "enabledByDefault": false, + "rollbackSurface": "RecordEditView/DataTree", + "allowedAdapters": [ + "LcmCache main-thread read/write through approved edit session", + "metadata cache immutable snapshots", + "XCore command bridge in shell phase only", + "FieldWorks diagnostics/logging" + ], + "forbiddenSymbols": [ + "System.Windows.Forms.Control", + "DataTree", + "Slice", + "RootSiteControl", + "XmlView", + "BrowseViewer", + "IVwRootBox", + "IVwEnv", + "IRenderEngine", + "GraphiteEngineClass", + "UniscribeEngineClass", + "GeckoWebBrowser", + "XWebBrowser" + ], + "requiredCapabilities": [ + "undo-redo", + "validation", + "keyboard-focus", + "accessibility-metadata", + "localized-strings", + "layout-overrides", + "writing-system-fonts" + ] +} +``` + +## 2. Allowed Legacy Adapters + +Adapters must be explicit and testable. + +| Adapter | Rule | +|---|---| +| `LcmCache` / LCModel | Allowed only through main-thread edit sessions or immutable snapshots. Do not read mutable cache objects on background threads. | +| Metadata cache | Allowed for class/flid/type lookup; snapshot values before background compilation. | +| Undo/redo | Must route through LCModel action handler semantics; local Avalonia commands cannot bypass global undo history. | +| XCore mediator/property table | Shell-phase adapter only. First-slice preview code must stay decoupled. | +| Diagnostics | Use FieldWorks `System.Diagnostics`/trace-switch pipeline. | +| Localization | User-visible production strings must use resource patterns; test-only strings can remain in tests. | + +## 3. Forbidden Default-Path Dependencies + +The manifest audit must fail migrated production code that directly references: + +- WinForms controls or hosts: `System.Windows.Forms`, `DataTree`, `Slice`, `RootSiteControl`, `RecordEditView` internals. +- XMLViews/native Views rendering: `XmlView`, `BrowseViewer`, `IVwRootBox`, `IVwEnv`, `IVwGraphics`. +- Native render engines: `IRenderEngine`, `IRenderEngineFactory`, `GraphiteEngineClass`, `UniscribeEngineClass`, `FwGrEngine`. +- Browser/PDF preview engines: `GeckoWebBrowser`, `XWebBrowser`, `GeckofxHtmlToPdf`, `FieldWorksPdfMaker`. +- Global COM registration, registry hacks, or direct native-boundary calls with unsanitized input. + +Exceptions must be documented in the manifest with owner, reason, tests, and rollback behavior. + +## 4. Gates + +| Gate | Required Evidence | +|---|---| +| Schema gate | Manifest validates against a checked-in schema and has an owner/rollback/test evidence entry. | +| Symbol audit gate | Automated search over migrated production code finds no forbidden symbols except approved exceptions. | +| Layout gate | Typed presentation snapshot matches selected DataTree/XML layout baselines for the region. | +| Edit gate | Save, cancel, nested session rejection, undo/redo, and refresh interaction tests pass. | +| Validation gate | Required fields, deterministic order, localized message metadata, severity, async stale-result handling, and accessibility exposure pass. | +| Accessibility gate | Controls expose stable automation IDs/names/roles where Avalonia supports them; keyboard-only navigation has headless or UI automation evidence. | +| Rendering gate | Writing-system/font/Graphite capability matrix is classified and default path blocks unsupported cases with rollback. | +| Performance gate | Provisional budgets are measured against named fixtures and hardware before becoming enablement criteria. | + +## 5. Provisional Performance Budgets + +Budgets are placeholders until measured. They must not be used as pass/fail claims until each has fixture ID, machine profile, command, and artifact path. + +| Metric | Provisional Target | Notes | +|---|---|---| +| First region load | Within 20 percent of legacy baseline or explicitly accepted | Measure cold and warm separately. | +| Layout compile | Deterministic and cacheable; target under 250 ms for selected first-slice fixture | Use immutable config snapshots. | +| Save/cancel command latency | No user-visible freeze for first editable slice | Measure UI-thread work and background work separately. | +| Validation pass | Linear in materialized node count | Lazy/unmaterialized sequences must be skipped or explicitly loaded. | + +## 6. Phasing + +| Phase | Manifest Work | +|---|---| +| Phase 1 | Define manifest schema, allowed/forbidden dependency policy, and audit command design. | +| Phase 2 | Attach current coverage report and identify blocked gates. | +| Phase 3 | Introduce default-off region manifest and symbol audit in tests or agent scripts. | +| Phase 4-6 | Add region evidence as typed layout import, edit sessions, validation, commands, and focus mature. | +| Phase 7-8 | Run accessibility/performance/rendering evidence against candidate default regions. | +| Phase 9 | Enable a region by default only when all gates pass and rollback remains available. | \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/seam-recommendations.md b/openspec/changes/lexical-edit-avalonia-migration/seam-recommendations.md new file mode 100644 index 0000000000..d7f6969b16 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/seam-recommendations.md @@ -0,0 +1,153 @@ +# Avalonia Seam Recommendations + +This note records the recommended seam direction for the Lexical Edit Avalonia migration. It is advisory; the companion seam docs define the concrete gates and tests. Current implementation is deliberately distinguished from proposed seams. + +Supporting docs: + +- `avalonia-edit-sessions.md` +- `avalonia-undo-redo.md` +- `avalonia-validation.md` +- `avalonia-command-focus.md` +- `avalonia-ui-scheduler.md` +- `avalonia-lifetime.md` + +## Edit Sessions + +**Current implementation:** `AdvancedEntryEditSession` is a concrete fenced LCModel undo-task session with `Save()` and `Cancel()`. + +**Recommendation:** Keep the direct LCModel fenced undo-task model for the first editable slice, then extract a FieldWorks-owned edit-session seam only with lifecycle, rollback, and global undo/redo tests. + +**Alternatives considered:** + +1. Direct LCModel fenced session. +Pros: closest to current code and LCModel action-handler semantics. +Cons: cancel/save semantics need strong tests before many fields are editable. + +2. Staged draft model. +Pros: clean pre-commit validation and cancel behavior. +Cons: larger mapping layer and conflict/stale-state work. + +3. Package-led draft editing using ReactiveUI or similar. +Pros: good local ergonomics. +Cons: does not solve LCModel transactions and adds framework commitment. + +**Revisit trigger:** adopt staged drafts only after a first-slice test proves direct sessions create unacceptable complexity or user-visible risk. + +**References:** Avalonia data validation, Avalonia commanding/hotkeys, MVVM Toolkit, ReactiveUI commands/validation. + +## Undo and Redo + +**Current implementation:** local save/cancel exists; there is no implemented `IUndoRedoCoordinator`. + +**Recommendation:** Keep control-local text undo as leaf behavior, but make global undo/redo authoritative through FieldWorks/LCModel transaction routing. + +**Alternatives considered:** + +1. Pure FieldWorks global transaction stack. +Pros: correct persisted-state semantics. +Cons: needs control integration work. + +2. Package-first object/view-model history. +Pros: easy prototype. +Cons: risks conflicting histories and wrong LCModel source of truth. + +3. Hybrid local text undo plus global LCModel undo. +Pros: best desktop editing UX while preserving domain history. +Cons: requires explicit focus/command routing rules. + +**Revisit trigger:** only for a specific owned control that needs richer document-local undo and still commits through LCModel. + +## Validation + +**Current implementation:** `ValidationService` performs deterministic required-field checks over Presentation IR and skips unmaterialized lazy items. + +**Recommendation:** Use a FieldWorks-owned validation model with Avalonia presentation adapters, preferably `INotifyDataErrorInfo` or `DataValidationErrors` where that maps cleanly to controls. + +**Alternatives considered:** + +1. Native Avalonia validation only. +Pros: simple and idiomatic. +Cons: insufficient for cross-object rules, localization metadata, and non-materialized nodes. + +2. FluentValidation/ReactiveUI behind the seam. +Pros: strong rule composition. +Cons: should remain implementation detail, not migration contract. + +3. Domain validation seam with Avalonia adapters. +Pros: reusable across tests, preview host, and shell integration. +Cons: requires structured issue paths and localization contract. + +**Revisit trigger:** collapse to native-only validation only for isolated dialogs or surfaces with no LCModel/cross-object semantics. + +## Command and Focus + +**Current implementation:** the spike has local Avalonia key bindings and view-model commands. There is no XCore command bridge yet. + +**Recommendation:** Use local Avalonia commands for first-slice/preview behavior; introduce a FieldWorks/XCore bridge only during shell integration. + +**Alternatives considered:** + +1. Avalonia built-ins only. +Pros: fast and idiomatic. +Cons: insufficient for shell menus, command state, and active target resolution. + +2. MVVM package commands. +Pros: nice local command ergonomics. +Cons: still needs shell routing. + +3. Custom bridge to XCore/property state. +Pros: correct shell integration. +Cons: easy to over-preserve legacy quirks if introduced too early. + +**Revisit trigger:** narrow or defer the bridge if shell-global command needs are smaller than expected. + +## UI Scheduler + +**Current implementation:** dispatcher calls are used directly in the Avalonia module; no shared scheduler seam exists. + +**Recommendation:** Introduce a thin `IUiScheduler` only where non-view code needs testable UI-thread marshalling, cancellation, or exception propagation. Keep direct dispatcher use at concrete UI edges. + +**Alternatives considered:** + +1. Direct dispatcher everywhere. +Pros: simplest code. +Cons: hard to fake and leaks Avalonia into service layers. + +2. Thin wrapper. +Pros: easy to test and small. +Cons: can become pointless if it only renames APIs. + +3. Reactive scheduler abstraction. +Pros: powerful for reactive screens. +Cons: unnecessary as global default. + +**Revisit trigger:** collapse low-value wrappers that provide no test or architecture value. + +## Lifetime + +**Current implementation:** `MainWindowViewModel` owns and disposes the loaded lifetime on save/cancel; no `ILexicalLifetimeManager` exists. + +**Recommendation:** Keep ownership explicit in the view model for the first slice; extract a lifetime manager only after late-loader, idempotent-disposal, event-unsubscribe, and shell-unload tests exist. + +**Alternatives considered:** + +1. Direct Avalonia lifetime everywhere. +Pros: quickest for preview-host code. +Cons: spreads shutdown/disposal policy. + +2. Thin lifetime manager. +Pros: testable owner for sessions, loaders, callbacks, and shell registrations. +Cons: premature if first slice stays small. + +3. Heavy region/document lifetime framework. +Pros: strongest explicit ownership tree. +Cons: overdesign until repeated cross-screen lifetime failures prove need. + +**Revisit trigger:** introduce heavier framework only when repeated region/window ownership bugs appear. + +## Research References + +- Avalonia official docs: data validation, binding validation, commanding, keyboard/hotkeys, focus, dispatcher threading, headless testing, app lifetimes, windows/dialogs, accessibility automation properties. +- Microsoft docs: MVVM Toolkit commands and `ObservableValidator`. +- ReactiveUI docs: commands, validation, and scheduler testing. +- HarfBuzz official docs: Graphite2 shaping requires HarfBuzz built with Graphite2 enabled and is not enabled by default. \ No newline at end of file diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/interop/native-boundary/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/interop/native-boundary/spec.md new file mode 100644 index 0000000000..e0d801b183 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/interop/native-boundary/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Migrated Avalonia regions eliminate viewing/render interop boundary + +Completed Avalonia regions SHALL NOT use the managed-to-native Views render interop boundary for display, layout, measurement, selection, hit testing, scrolling, or editor realization. + +#### Scenario: Native render interop is absent from completed region +- **WHEN** a Lexical Edit region is marked complete for Avalonia migration +- **THEN** dependency analysis or instrumentation SHALL show no runtime use of Views COM render interfaces, `RootSite`/`SimpleRootSite`, `IVwEnv`, `ManagedVwWindow`, RootBox rendering, or equivalent native render adapters from that region + +#### Scenario: Native interop remains allowed outside completed region +- **WHEN** another FieldWorks region still depends on native Views rendering +- **THEN** that dependency SHALL remain outside the completed Avalonia region boundary +- **AND** it SHALL be tracked as a separate repo-wide native Views retirement blocker + +### Requirement: Graphite native interop is decommissioned for Avalonia default + +The Avalonia default Lexical Edit path SHALL NOT instantiate or call native Graphite render interop, including `graphite2`, `GraphiteEngine`, or Graphite-enabled `IRenderEngine` selection. + +#### Scenario: Graphite COM renderer is absent +- **WHEN** Avalonia is proposed as the default Lexical Edit screen +- **THEN** dependency analysis SHALL show no default-path creation of `GraphiteEngineClass`, no Graphite `IRenderEngine` selection, and no default-path dependency on `Lib/src/graphite2` + +### Requirement: Non-viewing native dependencies are classified by boundary + +Native dependencies that are not windowing and not Graphite SHALL be classified before default switch as migrated-region blockers, service dependencies outside the viewing/render/editor path, or repo-wide legacy dependencies. + +#### Scenario: Custom linguistics native services may remain +- **WHEN** native or external code provides custom linguistics capability such as XAmple, spelling, parser/conversion tools, ICU, or Encoding Converters +- **THEN** it SHALL be allowed to remain behind explicit service contracts +- **AND** it SHALL be kept outside Avalonia display, layout, measurement, hit testing, selection, scrolling, and editor-realization responsibilities + +#### Scenario: Spell-check interop is replaced or isolated +- **WHEN** a migrated Avalonia region offers spelling behavior +- **THEN** it SHALL use a managed service boundary or another Avalonia-compatible service +- **AND** it SHALL NOT depend on RootBox `SetSpellingRepository(IGetSpellChecker)` integration + +#### Scenario: Parser and conversion tools remain outside viewing/render completion gate +- **WHEN** Lexical Edit workflows invoke native/external parser, XAmple, encoding-converter, ICU, or reg-free COM infrastructure +- **THEN** those dependencies SHALL be documented as service/tooling dependencies outside Avalonia rendering +- **AND** they SHALL NOT be used to justify keeping native Views or Graphite in the migrated UI region diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/testing/test-strategy/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/testing/test-strategy/spec.md new file mode 100644 index 0000000000..7587b698fc --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/testing/test-strategy/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: UI migration tests are layered by responsibility + +FieldWorks UI migration tests SHALL separate pure logic, integration, semantic render verification, WinForms UIA2 workflow smoke tests, and Avalonia.Headless interaction tests. + +#### Scenario: Business logic is not asserted only through UI automation +- **WHEN** behavior can be tested through services, view-definition compilation, LCModel integration, or render semantics +- **THEN** it SHALL have a non-UIA test path rather than relying only on WinForms or Avalonia UI automation + +#### Scenario: UI automation remains focused +- **WHEN** a test uses UIA2 or Avalonia.Headless +- **THEN** it SHALL verify interaction wiring, accessibility/reachability, input handling, or visual realization that cannot be covered by lower-level tests + +### Requirement: Test plans cover coverage gaps before refactor + +Any refactor or Avalonia replacement touching Lexical Edit SHALL include either existing coverage evidence or planned tests for the affected behavior before implementation proceeds. + +#### Scenario: Coverage gap is explicit +- **WHEN** a migration task identifies missing test coverage for a legacy behavior +- **THEN** the task SHALL add coverage first or record why coverage must be deferred and what parity artifact will replace it diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/views-rendering/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/views-rendering/spec.md new file mode 100644 index 0000000000..cfb6a91907 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/views-rendering/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Render verification supports semantic migration comparison + +The render verification framework SHALL support semantic capture for migration comparisons between legacy Views/DataTree output, typed view-definition output, and Avalonia output. + +#### Scenario: Semantic capture runs beside pixel capture +- **WHEN** a render baseline scenario captures a Lexical Edit view +- **THEN** it SHALL be able to emit both visual artifacts and semantic artifacts for fields, editors, visibility, bindings, focus order, and accessibility identity + +#### Scenario: Avalonia comparison is supported +- **WHEN** an Avalonia implementation exists for the same scenario +- **THEN** the render verification framework SHALL compare legacy, typed IR, and Avalonia semantic artifacts before treating pixel differences as behavior regressions + +### Requirement: Render timing separates compilation and control creation + +Render and migration timing artifacts SHALL separate XML/import work, typed compilation, cache hits, control realization, text rendering, and capture/comparison time. + +#### Scenario: Timing identifies migration bottleneck +- **WHEN** a Lexical Edit migration benchmark runs +- **THEN** the timing output SHALL identify whether time is spent in XML import, typed compilation, cache miss, legacy control creation, Avalonia control realization, text shaping, or render capture + +### Requirement: Migrated Avalonia regions do not use native render pipeline + +The rendering architecture SHALL treat native Views/C++ viewing, layout, measurement, hit testing, selection, and editor realization as legacy-only for migrated regions. Completed Avalonia regions SHALL render and edit through Avalonia-managed controls and text services without runtime calls into native Views render code. + +#### Scenario: Native render use is visible during comparison only +- **WHEN** render verification compares a legacy region and a migrated Avalonia region +- **THEN** native Views/C++ viewing/rendering SHALL be permitted only for the legacy baseline capture +- **AND** the Avalonia capture SHALL use the managed/Avalonia rendering path exclusively + +#### Scenario: Runtime native render call fails completion audit +- **WHEN** instrumentation or dependency analysis detects a migrated Avalonia region calling native Views/C++ viewing/rendering/editor infrastructure at runtime +- **THEN** the region SHALL fail the migration completion audit + +#### Scenario: Linguistics service call does not fail render audit +- **WHEN** a migrated Avalonia region calls a native or external linguistics service through a managed contract +- **AND** the service does not own display, layout, hit testing, selection, or editor realization +- **THEN** the call SHALL be classified outside the render pipeline audit + +### Requirement: Migrated Avalonia rendering is Graphite-free + +Avalonia rendering SHALL use managed/Avalonia text services with OpenType/HarfBuzz font features and SHALL NOT use Graphite render engines, Graphite font tables, or Gecko Graphite rendering in the default Lexical Edit path. + +#### Scenario: Graphite engine use fails default-readiness audit +- **WHEN** validation detects `graphite2`, `GraphiteEngine`, Graphite-enabled `RenderEngineFactory` selection, or Gecko Graphite rendering in the default Avalonia path +- **THEN** the default-readiness audit SHALL fail diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/winforms-patterns/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/winforms-patterns/spec.md new file mode 100644 index 0000000000..e5ebdd2bd4 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/architecture/ui-framework/winforms-patterns/spec.md @@ -0,0 +1,21 @@ +## ADDED Requirements + +### Requirement: DetailControls refactors introduce explicit service seams + +WinForms DetailControls refactors SHALL introduce explicit interfaces for DataTree services, refresh coordination, editor selection, launcher behavior, LCModel access, diagnostics, and host integration before replacing equivalent UI with Avalonia. + +#### Scenario: Slice creation is reachable through an editor registry +- **WHEN** a slice/editor is created from legacy XML metadata +- **THEN** the selection of editor kind SHALL pass through a registry or service boundary that can later resolve either legacy WinForms slices or Avalonia editors + +#### Scenario: Refresh behavior is testable without full UI replacement +- **WHEN** DataTree refresh behavior is refactored +- **THEN** refresh state transitions SHALL be covered by tests independent of full Lexical Edit UI automation + +### Requirement: WinForms controls expose automation metadata for migration baselines + +Legacy WinForms controls involved in migration baselines SHALL expose stable accessible names, roles, or automation identifiers where practical. + +#### Scenario: Baseline target has stable accessible identity +- **WHEN** a UIA2 baseline targets a DataTree, slice, launcher, table header, filter, popup, or chooser control +- **THEN** the target SHALL have a stable accessible identity or a documented fallback locator strategy diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-command-focus/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-command-focus/spec.md new file mode 100644 index 0000000000..aa3fcb36b1 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-command-focus/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Global command and focus behavior uses a FieldWorks-owned bridge + +Global command routing, active-target resolution, popup focus return, and shell-level command state SHALL use a FieldWorks-owned command and focus bridge rather than relying solely on direct Avalonia control bindings. + +#### Scenario: Global command resolves active target +- **WHEN** a migrated global command such as save, delete, or find is invoked from a menu, toolbar, shortcut, or context menu +- **THEN** the command SHALL resolve the active target through the FieldWorks-owned command and focus bridge + +### Requirement: Local Avalonia commands remain allowed inside screens + +Migrated screens SHALL be allowed to use direct Avalonia `ICommand`, `KeyBinding`, `HotKey`, CommunityToolkit commands, or similar local command helpers for screen-local behavior when they do not replace the global command and focus bridge. + +#### Scenario: Local editor command stays local +- **WHEN** a screen-local editor or popup handles a command that does not require shell-wide target resolution +- **THEN** the screen MAY bind that behavior directly through local Avalonia or MVVM command helpers + +### Requirement: Command descriptors separate execution from display state + +The shell command model SHALL keep stable command identity, visibility, checked state, enabled state, gestures, and diagnostics separate from the execution mechanism. + +#### Scenario: Menu and toolbar share command descriptor +- **WHEN** a command appears in more than one shell surface +- **THEN** those surfaces SHALL be driven from a shared command descriptor rather than duplicating per-surface state logic diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-edit-sessions/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-edit-sessions/spec.md new file mode 100644 index 0000000000..f8c68d5040 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-edit-sessions/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Editable Avalonia regions use a hybrid edit-session boundary + +Editable Avalonia regions SHALL use a hybrid edit-session boundary where UI-facing draft or staged state remains detached from live LCModel mutation until an explicit FieldWorks-owned edit session commits the change. + +#### Scenario: Draft state commits through FieldWorks session +- **WHEN** a migrated editor saves changes +- **THEN** the editor SHALL apply staged changes through a FieldWorks-owned edit-session or edit-transaction service +- **AND** the commit path SHALL own LCModel transaction semantics, rollback behavior, and commit fencing + +### Requirement: The authoritative edit-session contract remains FieldWorks-owned + +The authoritative edit-session contract for migrated lexical editing SHALL remain FieldWorks-owned and LCModel-aware rather than delegated to a package-specific view-model framework. + +#### Scenario: Package helpers do not replace commit boundary +- **WHEN** CommunityToolkit, ReactiveUI, or similar UI helpers are used for draft state, commands, or validation +- **THEN** those helpers SHALL remain outside the authoritative LCModel commit and rollback boundary + +### Requirement: Simple non-persistent dialogs may use lighter draft state + +Simple non-persistent dialogs or preview-only surfaces SHALL be allowed to use lighter screen-local draft state when they do not commit directly to LCModel and do not participate in migrated lexical edit-session guarantees. + +#### Scenario: Preview host avoids live edit session +- **WHEN** a preview host or sample-data surface renders an editor without a live project cache +- **THEN** it MAY use staged or sample draft state without opening a live LCModel edit session diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-lifetime/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-lifetime/spec.md new file mode 100644 index 0000000000..8b0d650daa --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-lifetime/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Non-view code uses a thin lifetime and dialog seam + +Application lifetime, dialog ownership, shutdown requests, and non-view window coordination SHALL use a thin FieldWorks-owned lifetime seam instead of direct Avalonia lifetime calls from non-view layers. + +#### Scenario: Presenter requests dialog through seam +- **WHEN** a presenter or non-view service needs to show a dialog, request shutdown, or coordinate owner windows +- **THEN** it SHALL do so through the lifetime and dialog seam rather than directly referencing Avalonia window lifetime APIs + +### Requirement: Direct Avalonia lifetime remains allowed at the UI edge + +Direct Avalonia lifetime APIs SHALL remain allowed in `Program`, `App`, preview-host startup, headless-test setup, and concrete window or dialog classes. + +#### Scenario: App startup uses classic desktop lifetime directly +- **WHEN** the concrete Avalonia application starts or a preview host boots a top-level window +- **THEN** the concrete startup path MAY use Avalonia lifetime APIs directly at that edge + +### Requirement: Full region or document lifetime frameworks are deferred + +The migration SHALL NOT require a heavy region, document, or workspace lifetime framework up front; such a framework SHALL be introduced only if repeated cross-screen lifetime problems prove a thin seam insufficient. + +#### Scenario: Thin lifetime seam remains default until repeated need is proven +- **WHEN** initial migrated screens and shell slices can be coordinated through the thin lifetime seam +- **THEN** the migration SHALL defer a heavier region or document lifetime framework rather than introducing it by default diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-ui-scheduler/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-ui-scheduler/spec.md new file mode 100644 index 0000000000..ed30b76ef3 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-ui-scheduler/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Non-view layers use a thin UI scheduling seam + +Presenters, view models outside view code, edit sessions, and migration services that need UI-thread marshalling SHALL use a thin FieldWorks-owned UI scheduling seam instead of reaching directly to Avalonia dispatcher APIs. + +#### Scenario: Background load marshals through seam +- **WHEN** a migrated workflow completes background work and needs to publish results to the UI thread +- **THEN** it SHALL marshal through the UI scheduling seam rather than directly calling Avalonia dispatcher APIs from non-view layers + +### Requirement: Direct Avalonia dispatcher use stays at the UI edge + +Direct use of `Dispatcher.UIThread` or equivalent Avalonia dispatcher APIs SHALL remain allowed in `Program`, `App`, `Window`, `UserControl`, preview-host startup, and headless-test adapter code. + +#### Scenario: Window code uses direct dispatcher +- **WHEN** view-specific code-behind or startup code needs direct dispatcher access +- **THEN** it MAY use Avalonia dispatcher APIs without routing through the scheduling seam + +### Requirement: Reactive schedulers are optional local helpers + +Reactive or framework-specific schedulers MAY be used as local implementation details when a screen explicitly adopts them, but SHALL NOT become the global migration scheduling contract by default. + +#### Scenario: Reactive screen does not redefine app scheduler contract +- **WHEN** a migrated screen uses ReactiveUI or similar reactive helpers internally +- **THEN** that choice SHALL remain local to the screen and SHALL NOT replace the shared UI scheduling seam for the migration diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-undo-redo/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-undo-redo/spec.md new file mode 100644 index 0000000000..ea3a561365 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-undo-redo/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Global undo and redo remain domain-authoritative + +Global undo and redo for migrated lexical editing SHALL remain authoritative at the FieldWorks or LCModel transaction layer. + +#### Scenario: Domain edit participates in global undo +- **WHEN** a migrated lexical edit changes persisted project state +- **THEN** the resulting undo and redo behavior SHALL be recorded through the FieldWorks or LCModel undo infrastructure rather than a package-local view-model history + +### Requirement: Control-local undo is allowed only as leaf history + +Avalonia control-local undo and redo MAY be used as leaf editing history while focus remains inside a control, but SHALL NOT replace the global domain-authoritative undo model for persisted lexical edits. + +#### Scenario: TextBox undo stays local until commit boundary +- **WHEN** a user is actively editing text inside a focused Avalonia control +- **THEN** the control MAY expose local undo and redo behavior for in-control text changes +- **AND** persisted lexical undo history SHALL still be routed through the domain-authoritative undo boundary + +### Requirement: Grouped edits and chooser workflows use domain transactions + +Grouped edits, chooser dialogs, nested edit scopes, and other workflows that affect project state SHALL use FieldWorks-owned undo grouping and transaction boundaries. + +#### Scenario: Chooser confirm creates grouped undo item +- **WHEN** a chooser or popup confirms a change that mutates project state +- **THEN** the change SHALL participate in a grouped FieldWorks undo transaction with deterministic cancel and rollback behavior diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-validation/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-validation/spec.md new file mode 100644 index 0000000000..f3915126ae --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/avalonia-validation/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: Durable validation rules live behind a FieldWorks-owned validation seam + +Durable lexical validation rules SHALL live behind a FieldWorks-owned validation seam that can evaluate staged edits independently of Avalonia control materialization. + +#### Scenario: Validation runs without forcing editor creation +- **WHEN** a migrated lexical region validates staged data +- **THEN** validation SHALL be able to evaluate required rules, cross-field rules, and commit gates without forcing live editor or control materialization + +### Requirement: Avalonia presents validation through native binding surfaces + +Avalonia editors SHALL present validation state through native Avalonia binding and validation surfaces such as `INotifyDataErrorInfo`, `DataValidationErrors`, or equivalent UI adapters. + +#### Scenario: Field issue appears in Avalonia editor +- **WHEN** the validation seam reports a field-scoped issue for visible staged data +- **THEN** the corresponding Avalonia editor SHALL expose the error state through native Avalonia validation presentation and accessibility metadata + +### Requirement: Package validators remain subordinate to the validation seam + +DataAnnotations, CommunityToolkit validation helpers, FluentValidation, or similar libraries MAY be used behind the validation seam or for simple dialogs, but SHALL NOT replace the FieldWorks-owned validation contract for migrated lexical editing. + +#### Scenario: FluentValidation stays behind seam +- **WHEN** a validator library is used to implement cross-field, collection, async, or localized rules +- **THEN** the library SHALL feed structured validation results into the FieldWorks-owned validation seam rather than becoming the public migration contract diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-avalonia-migration/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-avalonia-migration/spec.md new file mode 100644 index 0000000000..a081ce61de --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-avalonia-migration/spec.md @@ -0,0 +1,135 @@ +## ADDED Requirements + +### Requirement: Migration is phased by risk and control complexity + +The Lexical Edit migration SHALL proceed in phases: baseline test coverage, refactoring seams, simple Avalonia controls and popup hovers, table/browse views, slices, and then full Lexical Edit views. + +#### Scenario: Refactor gates precede Avalonia replacement +- **WHEN** a migration task replaces a Lexical Edit surface with Avalonia +- **THEN** the affected legacy behavior SHALL already have unit/integration coverage or an explicit baseline plan in `lexical-edit-parity-automation` +- **AND** required service seams SHALL be identified before UI replacement begins + +#### Scenario: Control complexity determines rollout order +- **WHEN** scheduling Avalonia replacement work +- **THEN** simple editors and popup hovers SHALL be attempted before table views +- **AND** table views SHALL be attempted before slice and full Lexical Edit replacement + +### Requirement: User interaction and density are preserved + +Avalonia replacements SHALL preserve the legacy user interaction model, information density, keyboard/focus behavior, popup semantics, and layout hierarchy within documented near-pixel tolerances. + +#### Scenario: Dense field editing parity +- **WHEN** a migrated lexical entry field group is shown in Avalonia +- **THEN** it SHALL expose equivalent labels, editor affordances, focus order, hover/popup entry points, and visible data density as the legacy DataTree baseline + +#### Scenario: Pixel differences are explained +- **WHEN** Avalonia output differs visually from the WinForms baseline +- **THEN** the comparison artifact SHALL identify whether the difference is accepted near-pixel variance, font/rendering variance, missing data, or a behavior regression + +### Requirement: Avalonia text uses writing-system font settings and OpenType shaping + +Avalonia lexical editors SHALL use FieldWorks writing-system font settings, Avalonia/Skia text rendering, and HarfBuzz/OpenType feature support. Graphite SHALL NOT be supported in Avalonia. + +#### Scenario: Writing-system text editor binds font metadata +- **WHEN** a multi-writing-system field is rendered in Avalonia +- **THEN** each writing-system alternative SHALL use the configured font family, size, flow direction, culture/script metadata, and OpenType feature settings available for that writing system + +#### Scenario: Graphite-only behavior blocks default switch +- **WHEN** a legacy writing system or font depends on Graphite-only shaping or feature IDs +- **THEN** the migration SHALL block Avalonia becoming the default screen until the writing system is migrated to OpenType/HarfBuzz-compatible font settings, replaced with an acceptable font option, or documented as an unsupported legacy dependency outside the default Avalonia path + +### Requirement: Graphite is decommissioned before Avalonia becomes default + +Graphite decommissioning SHALL start when the Lexical Edit Avalonia migration starts. Avalonia SHALL NOT become the default Lexical Edit screen until Graphite runtime dependencies, font options, Gecko rendering assumptions, and default-path tests/docs are removed or converted to OpenType/HarfBuzz-only behavior. + +#### Scenario: Stealth migration still starts Graphite retirement +- **WHEN** Avalonia Lexical Edit work begins behind flags, preview hosts, or non-default entry points +- **THEN** the work SHALL include Graphite inventory and retirement tasks from the start + +#### Scenario: Default switch requires Graphite-free evidence +- **WHEN** Avalonia is proposed as the default Lexical Edit screen +- **THEN** validation SHALL prove that the default path does not call Graphite native code, create Graphite render engines, enable Gecko Graphite rendering, depend on Graphite feature strings, or require Graphite-only sample fonts + +### Requirement: FieldWorks-owned controls cover domain-specific editors + +The migration SHALL prefer FieldWorks-owned Avalonia editor controls over permanent dependence on generic property-grid behavior for multi-writing-system text, rich text, choosers, feature structures, references, nested sequences, and TreeView-heavy views. + +#### Scenario: PropertyGrid remains a bootstrap path +- **WHEN** the current Advanced Entry PropertyGrid prototype is used for migration learning +- **THEN** it SHALL NOT define the final Lexical Edit UI contract +- **AND** final editors SHALL bind to typed view-definition/IR nodes through owned editor interfaces + +#### Scenario: TreeView supports multiple translations per sense or term +- **WHEN** a migrated tree view displays senses, terms, examples, glosses, definitions, or translations +- **THEN** each tree node SHALL be able to render multiple writing-system alternatives and compact inline metadata without requiring a separate modal dialog for normal inspection + +### Requirement: Avalonia regions declare completion manifests + +Each migrated Avalonia region SHALL define a completion manifest before implementation is marked complete. The manifest SHALL list entry points, typed view-definition sources, allowed legacy adapters, forbidden native viewing/rendering and Graphite call paths, retained custom linguistics services, parity fixtures, customer override fixtures, accessibility IDs, performance budgets, and rollback/default-switch gates. + +#### Scenario: Region manifest blocks ambiguous completion +- **WHEN** a region is proposed as migrated +- **THEN** its manifest SHALL identify the tests, instrumentation, fixtures, and default-switch evidence required for that region +- **AND** missing manifest entries SHALL block completion + +### Requirement: Avalonia editors use explicit edit sessions + +Avalonia editors SHALL use explicit edit-session or edit-transaction services for staged values, validation, cancellation, LCModel commit behavior, dirty state, undo/redo grouping, and command enablement. + +#### Scenario: Edit is committed through transaction seam +- **WHEN** a migrated editor commits a value +- **THEN** the edit SHALL pass through the edit-session boundary +- **AND** validation, LCModel transaction behavior, undo/redo grouping, and refresh notifications SHALL be observable by tests + +#### Scenario: Edit is canceled without side effects +- **WHEN** a migrated editor cancels a pending edit +- **THEN** the editor SHALL restore display state without committing LCModel changes or creating undo items + +### Requirement: Avalonia platform seams are explicit + +Avalonia regions SHALL use explicit services for UI dispatch, focus navigation, command routing, region lifetime/disposal, styling resources, design/preview data, and accessibility metadata rather than reaching through WinForms, DataTree, or xCore UI objects. + +#### Scenario: Focus and command behavior are tested through services +- **WHEN** a migrated editor handles shortcuts, context menus, popup focus return, or command enablement +- **THEN** that behavior SHALL be routed through explicit command/focus services +- **AND** Avalonia.Headless or semantic parity tests SHALL cover the behavior + +### Requirement: Package updates and control hacks are gated by parity evidence + +Avalonia package updates, third-party control additions, upstream patches, or local control hacks SHALL be allowed only when tied to a specific parity, density, text, table, or automation requirement. + +#### Scenario: Package change has migration justification +- **WHEN** an Avalonia package version or control dependency changes +- **THEN** the change SHALL document the blocked requirement, package/control rationale, and validation evidence from Avalonia.Headless or render parity tests + +### Requirement: Legacy XML and native Views are not new dependencies + +New Avalonia Lexical Edit functionality SHALL NOT require WinForms slices, XMLViews rendering, or native Views runtime to operate, except through migration importers and baseline comparison harnesses. + +#### Scenario: New Avalonia editor runs from typed contract +- **WHEN** a migrated Avalonia editor is launched in the Preview Host or headless tests +- **THEN** it SHALL receive a typed view-definition/IR model and injected services +- **AND** it SHALL NOT instantiate `DataTree`, `Slice`, `RootSite`, or native Views UI components + +### Requirement: C++ viewing/rendering dependencies are decommissioned by migrated region + +A Lexical Edit region SHALL NOT be considered fully migrated to Avalonia until that region has no runtime dependency on native code that owns display, layout, measurement, hit testing, selection, scrolling, editor realization, native Views box/layout/rendering, `RootSite`, `IVwEnv`, `ManagedVwWindow`, or equivalent native render adapters. + +#### Scenario: Region completion requires native viewing/render seam audit +- **WHEN** a Lexical Edit region is proposed as fully migrated to Avalonia +- **THEN** a dependency audit SHALL show that the region renders, measures, hit-tests, scrolls, selects, and edits without calling native Views/C++ viewing/rendering/editor infrastructure +- **AND** any remaining native render usage SHALL be limited to non-migrated regions or offline baseline comparison tools + +#### Scenario: Native viewing/render bridge blocks completion +- **WHEN** an Avalonia region still relies on native Views/C++ viewing/rendering infrastructure for display, layout, text measurement, selection, hit testing, scrolling, or editor realization +- **THEN** that region SHALL remain in migration status and SHALL NOT be marked complete + +#### Scenario: Custom linguistics services remain allowed +- **WHEN** a migrated Avalonia region invokes native or external linguistics services such as XAmple, spelling, parser/conversion tools, ICU, or Encoding Converters +- **THEN** those services SHALL be accessed through explicit service contracts outside the Avalonia render/editor path +- **AND** they SHALL NOT own UI display, layout, measurement, hit testing, selection, or editor realization for the migrated region + +#### Scenario: Shared native code deletion waits for all consumers +- **WHEN** a migrated Lexical Edit region no longer uses native Views/C++ viewing/rendering infrastructure but other FieldWorks regions still do +- **THEN** the migrated region SHALL be complete for its scope +- **AND** global native Views deletion SHALL remain a separate tracked dependency until remaining consumers are migrated or intentionally retained diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-font-decommissioning/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-font-decommissioning/spec.md new file mode 100644 index 0000000000..9d833b46df --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-font-decommissioning/spec.md @@ -0,0 +1,63 @@ +## ADDED Requirements + +### Requirement: Graphite decommissioning starts with the migration + +The Lexical Edit Avalonia migration SHALL begin Graphite decommissioning immediately, even when Avalonia work is hidden behind preview hosts, feature flags, or non-default entry points. Avalonia SHALL never support Graphite. + +#### Scenario: Migration start creates Graphite retirement work +- **WHEN** implementation begins for Lexical Edit Avalonia migration +- **THEN** the task plan SHALL include inventory, migration, and validation tasks for retiring Graphite from the default path + +#### Scenario: Default screen is blocked by Graphite dependency +- **WHEN** Avalonia is proposed as the default Lexical Edit screen +- **THEN** Graphite dependencies SHALL be fully retired from the default path or explicitly classified as unsupported legacy dependencies outside that path + +### Requirement: Graphite code, settings, and assets are inventoried + +The migration SHALL inventory Graphite across native code, managed render selection, writing-system settings, UI, tests, docs, assets, browser rendering, print, PDF, and build/package files. + +#### Scenario: Inventory covers known Graphite surfaces +- **WHEN** Graphite decommissioning inventory runs +- **THEN** it SHALL cover at least `Lib/src/graphite2`, `Src/views/lib/GraphiteEngine.*`, `RenderEngineFactory`, `GraphiteFontFeatures`, `FontFeaturesButton`, `DefaultFontsControl`, `FwWritingSystemSetupModel`, `IsGraphiteEnabled`, `DefaultFontFeatures`, `FontEngines.Graphite`, Graphite-specific tests, `DistFiles/Graphite`, `Build/Windows.targets`, Gecko Graphite preferences, `XWebBrowser` preview consumers, and `GeckofxHtmlToPdf`/`FieldWorksPdfMaker` assumptions + +### Requirement: Font options migrate to OpenType and HarfBuzz + +Graphite font feature strings SHALL NOT be preserved as Avalonia runtime behavior. Avalonia font options SHALL use OpenType/HarfBuzz-compatible feature syntax and explicit font replacement or compatibility diagnostics for Graphite-only fonts. + +#### Scenario: Graphite feature string has no OpenType mapping +- **WHEN** a stored Graphite feature string or Graphite-only font cannot be mapped to OpenType/HarfBuzz behavior +- **THEN** the migration SHALL report an actionable compatibility diagnostic and SHALL NOT silently render it as equivalent in Avalonia + +#### Scenario: OpenType font features are applied per writing-system run +- **WHEN** a migrated Avalonia editor renders a writing-system run +- **THEN** it SHALL apply the writing system's OpenType/HarfBuzz-compatible feature settings, font family, fallback mapping, culture/script metadata, and flow direction + +### Requirement: Gecko and PDF rendering stop relying on Graphite + +Gecko/XULRunner and Gecko-backed PDF/print flows SHALL NOT be part of the default Avalonia Lexical Edit rendering path if they rely on Graphite rendering. + +#### Scenario: Gecko Graphite preference blocks default path +- **WHEN** default-path validation observes Gecko startup with `gfx.font_rendering.graphite.enabled` or an equivalent Graphite rendering dependency +- **THEN** Avalonia SHALL NOT become the default Lexical Edit screen until that path is replaced, disabled, or moved outside the default boundary + +#### Scenario: Browser/PDF replacement is validated +- **WHEN** XHTML preview, print, or PDF behavior is retained for migrated workflows +- **THEN** the replacement SHALL be validated with OpenType/HarfBuzz-compatible font behavior and SHALL NOT depend on `XWebBrowser` Graphite rendering or `GeckofxHtmlToPdf` Graphite shaping assumptions + +### Requirement: Remaining native dependencies are classified + +Native dependencies outside Graphite and window hosting SHALL be classified by migration impact before Avalonia becomes default. The classification SHALL distinguish native code that owns what the user views or edits from custom linguistics services that may remain behind managed contracts. + +#### Scenario: Native Views remains a render blocker +- **WHEN** a dependency uses native Views layout, selection, hit testing, editing, or interlinear rendering +- **THEN** it SHALL be treated as a migrated-region blocker, not as advanced windowing + +#### Scenario: Custom linguistics tools are isolated as services +- **WHEN** a dependency uses parser tools, XAmple, Encoding Converters, ICU, Expat/ParserObject, spelling interop, or reg-free COM tooling outside rendering +- **THEN** it SHALL be documented as a service/tooling dependency that supports FieldWorks language-documentation capability +- **AND** it SHALL be kept outside the Avalonia render/editor completion gate + +#### Scenario: Native viewing code is not imported as a service +- **WHEN** native code owns display, layout, measurement, hit testing, selection, scrolling, or editor realization +- **THEN** it SHALL be treated as viewing/rendering infrastructure +- **AND** it SHALL NOT be brought into a completed Avalonia region as a retained service dependency diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-parity-automation/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-parity-automation/spec.md new file mode 100644 index 0000000000..5ab5110a84 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-parity-automation/spec.md @@ -0,0 +1,83 @@ +## ADDED Requirements + +### Requirement: Legacy WinForms behavior has layered baselines + +Before refactoring or replacing a Lexical Edit surface, the system SHALL have unit, integration, render, semantic, or UIA2 baseline coverage appropriate to the surface risk. + +#### Scenario: Baseline plan exists before refactor +- **WHEN** a refactor touches DataTree, SliceFactory, Slice, launchers, XMLViews table views, popup choosers, or RecordEditView hosting +- **THEN** the change SHALL identify existing tests or add a baseline task covering the affected behavior + +### Requirement: UIA2 automation covers legacy workflow reachability + +Legacy WinForms automation SHALL use UIA2-style workflow tests only for stable, user-observable reachability such as focus, launcher buttons, chooser dialogs, context menus, table headers, filters, and popup windows. + +#### Scenario: UIA2 smoke test drives chooser launch +- **WHEN** a legacy reference or possibility field exposes a chooser launcher +- **THEN** the UIA2 baseline SHALL be able to focus the field, invoke the launcher, observe the chooser window, and cancel or accept it deterministically + +#### Scenario: Owner-drawn content uses fallback assertions +- **WHEN** UIA2 cannot inspect owner-drawn or Views-backed content deeply +- **THEN** the baseline SHALL pair workflow automation with render snapshots, semantic snapshots, or model assertions + +### Requirement: Avalonia.Headless covers new control interaction + +Avalonia controls SHALL have headless tests for behavior that cannot be proven by pure unit tests, including input, expand/collapse, hover/flyout activation, context menus, selection, validation state, and virtualized sequence behavior. + +#### Scenario: Headless text input updates staged state +- **WHEN** an Avalonia headless test focuses a migrated text editor and sends text input +- **THEN** the editor SHALL update the bound staged state or LCModel-backed edit session according to the active migration phase + +#### Scenario: Headless tree expansion realizes expected nodes +- **WHEN** a headless test expands a migrated tree node for senses, terms, or translations +- **THEN** the expected child nodes SHALL be realized without creating unrelated off-screen editor controls + +#### Scenario: Headless test covers editor platform seams +- **WHEN** an Avalonia editor is tested headlessly +- **THEN** the test SHALL cover command shortcuts, popup focus return, validation state, edit commit/cancel, keyboard/IME behavior where practical, accessibility metadata, and disposal/subscription cleanup + +### Requirement: Render framework captures semantic parity + +Render verification SHALL capture semantic snapshots in addition to pixel/timing artifacts for legacy WinForms, typed IR, and Avalonia outputs. + +Semantic snapshots SHALL normalize volatile values and key comparisons around stable node IDs, class/flid/object binding, editor descriptors, writing-system metadata, ghost state, focus order, accessibility identity, and migration diagnostics. + +#### Scenario: Semantic snapshot identifies fields and bindings +- **WHEN** a lexical entry view is captured +- **THEN** the semantic artifact SHALL include visible sections, field labels, editor kinds, object/class/flid or binding identity, writing-system metadata, visibility state, ghost state, expansion state, focus order, and accessibility identity where available + +#### Scenario: Legacy and Avalonia comparison reports meaningful differences +- **WHEN** legacy and Avalonia outputs are compared +- **THEN** the report SHALL distinguish missing semantic nodes, accepted visual variance, accessibility differences, timing differences, and unsupported migration gaps + +#### Scenario: Snapshot avoids incidental layout brittleness +- **WHEN** a visual layout difference does not alter stable semantic identity, editor kind, binding, focus order, accessibility identity, or accepted density thresholds +- **THEN** the parity result SHALL classify it as visual variance rather than a semantic regression + +### Requirement: Migration gates include behavior matrices + +Each region proposed for Avalonia completion SHALL provide behavior matrices for undo/redo, LCModel transactions, keyboard/focus behavior, accessibility metadata, localization/resource identity, customer overrides, dynamic editor diagnostics, performance budgets, native-call instrumentation, and Graphite-free default behavior. + +#### Scenario: Missing behavior matrix blocks completion +- **WHEN** a region is proposed as completed +- **THEN** missing required behavior matrix evidence SHALL block completion even if visual rendering appears correct + +### Requirement: Failure artifacts are actionable + +Failed parity automation SHALL preserve enough evidence to diagnose the failing layer. + +#### Scenario: Parity failure emits bundled evidence +- **WHEN** a parity test fails +- **THEN** it SHALL write or reference the relevant trace log, semantic snapshot, screenshot or diff image, timing data, and root capability/scenario id + +### Requirement: Graphite decommissioning has validation artifacts + +The migration SHALL include validation artifacts proving Graphite is absent from the Avalonia default path and that affected projects/fonts receive actionable migration results. + +#### Scenario: Graphite-dependent fixture is detected +- **WHEN** a fixture contains Graphite-enabled writing-system settings, Graphite feature strings, or Graphite-only sample fonts +- **THEN** parity automation SHALL report a decommissioning diagnostic, replacement/migration status, or explicit unsupported legacy status + +#### Scenario: Gecko/PDF Graphite path is not part of Avalonia default +- **WHEN** preview, print, or PDF flows are validated for the default Avalonia Lexical Edit path +- **THEN** the artifacts SHALL prove they do not depend on Gecko Graphite rendering, `XWebBrowser` Graphite behavior, or `GeckofxHtmlToPdf` Graphite shaping assumptions diff --git a/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-view-definition/spec.md b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-view-definition/spec.md new file mode 100644 index 0000000000..53af6dd152 --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/specs/lexical-edit-view-definition/spec.md @@ -0,0 +1,90 @@ +## ADDED Requirements + +### Requirement: Typed view definition is the canonical migration boundary + +The system SHALL define a managed typed view-definition model and Presentation IR for Lexical Edit that represents sections, fields, sequences, table regions, tree nodes, labels, visibility, ghost behavior, editor descriptors, writing-system metadata, OpenType/HarfBuzz font-feature metadata, stable node identity, localization/resource identity, accessibility identity, validation hints, focus groups, command affordances, and virtualization hints. + +#### Scenario: IR represents LexEntry layout semantics +- **WHEN** the LexEntry detail contract is compiled +- **THEN** the typed model SHALL include stable nodes for identity fields, morphology, senses, examples, references, custom fields, ghost add-first-item affordances, and nested sequences required by the parity checklist + +#### Scenario: IR is renderer independent +- **WHEN** a typed view-definition model is produced +- **THEN** it SHALL be consumable by semantic tests, legacy comparison adapters, and Avalonia renderers without exposing XML nodes or WinForms controls as the public contract + +### Requirement: XML Parts/Layout imports into the typed model during transition + +The system SHALL import existing XML Parts/Layout definitions, including override/unification behavior, into the typed view-definition model during the migration period. + +#### Scenario: Production XML imports with overrides +- **WHEN** a project uses shipped LexEntry Parts/Layout plus user overrides +- **THEN** the importer SHALL apply the same effective ordering and override/unification semantics as the legacy view resolution for covered constructs + +#### Scenario: Unsupported XML construct is explicit +- **WHEN** the importer encounters an XML construct not yet supported by the typed model +- **THEN** it SHALL emit a diagnostic tied to the layout part and node path rather than silently dropping the construct + +#### Scenario: Dynamic editor construct reports diagnostic +- **WHEN** XML import encounters dynamic editor strings, custom editor constructs, loader-based editors, fallback message/image slices, or unsupported launcher parameters +- **THEN** the importer SHALL preserve enough descriptor metadata for the legacy adapter when possible +- **AND** it SHALL emit a deterministic diagnostic with layout part, node path, editor key, and migration severity before Avalonia replacement is attempted + +### Requirement: View-definition services use dependency injection + +View-definition compilation and rendering SHALL depend on interfaces for layout source access, XML import, schema/model metadata, writing-system services, editor registry, cache, diagnostics, and LCModel access. + +#### Scenario: Compiler has replaceable services +- **WHEN** a unit test compiles a view definition +- **THEN** it SHALL be able to supply fake layout source, metadata, writing-system, editor registry, and diagnostics services without constructing WinForms controls + +### Requirement: Compilation is cacheable, deterministic, and off the UI thread + +Typed view-definition compilation SHALL be deterministic, cacheable by stable keys, cancellable, and runnable off the UI thread. + +Typed view-definition compilation SHALL operate on immutable layout, metadata, writing-system, custom-field, and override snapshots. It SHALL NOT depend on live WinForms controls, mutable `PropertyTable` state, or UI-thread-only cache traversal when compiling off the UI thread. + +#### Scenario: Warm compile reuses cache +- **WHEN** the same root class, layout id, project configuration fingerprint, writing-system profile, and override set are compiled twice +- **THEN** the second compile SHALL reuse the cached typed result + +#### Scenario: UI thread is not blocked by heavy compilation +- **WHEN** a Lexical Edit view opens or changes root layout +- **THEN** heavy XML import, custom-field expansion, and semantic compilation SHALL run outside the UI thread and support cancellation + +#### Scenario: Async compile does not touch UI state +- **WHEN** view-definition compilation runs asynchronously +- **THEN** it SHALL consume immutable inputs captured by a safe source service +- **AND** it SHALL publish results through the UI scheduling boundary only after compilation completes or is canceled + +### Requirement: XML retirement requires migration tooling and parity gates + +Runtime XML dependency SHALL be retired only after typed view-definition authoring/import/migration tooling and parity gates cover production layouts, user overrides, custom fields, ghost items, choosers, table views, and nested lexical structures. + +#### Scenario: XML retirement is blocked by uncovered behavior +- **WHEN** a covered production layout behavior cannot be represented in the typed view-definition model +- **THEN** runtime XML retirement SHALL remain blocked for that surface + +#### Scenario: Canonical view definition replaces XML at runtime +- **WHEN** a Lexical Edit surface has passed migration gates +- **THEN** the runtime UI SHALL load the canonical typed definition directly while retaining XML import only for migration/audit scenarios + +### Requirement: Typed view definitions replace native render contracts for completed regions + +Completed Avalonia regions SHALL use typed view definitions and managed renderer/editor services for display, measurement, selection metadata, hit testing, and editor realization instead of native Views/C++ viewing/rendering contracts. + +#### Scenario: View definition carries render semantics needed by Avalonia +- **WHEN** a typed view definition is produced for a migrated region +- **THEN** it SHALL include enough metadata for Avalonia controls to render, measure, virtualize, focus, and hit-test the region without consulting `IVwEnv`, RootBox, or native Views render objects + +#### Scenario: Missing native viewing/rendering replacement blocks completion +- **WHEN** the typed view-definition model cannot express behavior currently supplied only by native Views/C++ viewing/rendering +- **THEN** the region SHALL remain incomplete until a managed/Avalonia replacement service or an explicit scoped compatibility decision is added + +### Requirement: View definitions exclude Graphite runtime settings + +Canonical typed view definitions for Avalonia SHALL NOT depend on Graphite feature IDs, Graphite engine selection, or Graphite-only fonts at runtime. + +#### Scenario: Graphite settings become diagnostics or migration inputs +- **WHEN** XML import or project settings expose `IsGraphiteEnabled`, Graphite `DefaultFontFeatures`, or equivalent Graphite-only font metadata +- **THEN** the typed view-definition compiler SHALL emit migration diagnostics or mapped OpenType/HarfBuzz metadata +- **AND** it SHALL NOT preserve Graphite as an Avalonia runtime setting diff --git a/openspec/changes/lexical-edit-avalonia-migration/tasks.md b/openspec/changes/lexical-edit-avalonia-migration/tasks.md new file mode 100644 index 0000000000..f51d2893ab --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/tasks.md @@ -0,0 +1,105 @@ +# Tasks + +## 1. Migration Baseline and Spec Audit + +- [x] 1.1 Review Speckit artifacts against this OpenSpec change and keep `migration-map.md` current. +- [x] 1.2 Inventory current Lexical Edit view entry points: `RecordEditView`, `DataTree`, `SliceFactory`, XMLViews browse/table views, popup choosers, and AdvancedEntry Avalonia spike. +- [x] 1.3 Build a coverage map for DataTree refresh, SliceFactory/editor selection, launchers, popup choosers, XML table views, and render verification. +- [x] 1.4 Identify customer/user override XML fixtures that must be included before XML retirement. +- [x] 1.5 Start Graphite decommissioning inventory for writing-system settings, fonts, native render engines, Gecko/browser/PDF paths, tests, docs, sample assets, and build/package artifacts. +- [x] 1.6 Define migrated-region manifest format: entry points, allowed legacy adapters, forbidden symbols/call paths, custom linguistics service dependencies, parity fixtures, performance budgets, accessibility IDs, and rollback/default-switch gates. +- [x] 1.7 Freeze and maintain the seam capability docs `avalonia-edit-sessions`, `avalonia-undo-redo`, `avalonia-validation`, `avalonia-command-focus`, `avalonia-ui-scheduler`, `avalonia-lifetime`, and `seam-recommendations.md` as the reference playbook for both this change and the later shell change. + +## 2. Test Coverage Before Refactor + +- [x] 2.1 Add or extend unit/integration tests for DataTree refresh state transitions and postponed `PropChanged` behavior. +- [x] 2.2 Add or extend launcher pure-logic tests, prioritizing morph type swap/data-loss logic and chooser decision paths. +- [x] 2.3 Add semantic baseline capture for current DataTree/Slice output: labels, object/flid bindings, editor kind, visibility, expansion, focus order, and accessibility identity. +- [ ] 2.4 Add true UIA2/FlaUI/Appium smoke baselines for WinForms launcher/chooser workflows and XMLViews table header/filter reachability. The current branch has in-repo smoke substitutes only. +- [ ] 2.5 Add failure artifact bundling to render/parity tests where missing. +- [ ] 2.6 Add undo/redo and LCModel transaction characterization tests for editor replacement candidates. Prototype coverage moved to `010-advanced-entry-preview-prototype`. +- [ ] 2.7 Add keyboard/IME, focus restoration, accessibility metadata, localization, and disposal/unsubscribe characterization tests for first-slice candidates. Prototype coverage moved to `010-advanced-entry-preview-prototype`. +- [ ] 2.8 Add snapshot normalization rules so semantic baselines key on stable node IDs, class/flid/object binding, editor kind, writing-system metadata, ghost state, focus order, and accessibility identity instead of incidental layout noise. Prototype coverage moved to `010-advanced-entry-preview-prototype`. + +## 3. Refactor Seams First + +- [ ] 3.1 Introduce narrow DataTree service interfaces without changing behavior: `ILexicalRefreshCoordinator`, `IXCoreCommandBridge`, `IPropertyStateStore`, `IRecordNavigationContext`, diagnostics, writing-system access, and LCModel access. +- [ ] 3.2 Extract refresh coordination into a testable service or state object while preserving current behavior. +- [ ] 3.3 Put an `ILexicalEditorRegistry` boundary in front of `SliceFactory` so editor keys can resolve to legacy slices now and Avalonia editors later. +- [ ] 3.4 Extract at least one launcher humble object path, using morph type swap as the first target. +- [ ] 3.5 Define host/surface interfaces around `RecordEditView`/`DataTree` initialization, focus, context menus, and view replacement. +- [ ] 3.6 Extract edit-session and transaction seams for staged values, validation, cancellation, dirty state, undo/redo grouping, and LCModel commit behavior, following `avalonia-edit-sessions` and `avalonia-undo-redo`. +- [ ] 3.7 Extract UI scheduling, focus navigation, command routing, and region lifetime/disposal seams before introducing editable Avalonia controls, following `avalonia-command-focus`, `avalonia-ui-scheduler`, and `avalonia-lifetime`. +- [ ] 3.8 Inventory dynamic editor strings and custom editor constructs (`custom`, `customwithparams`, `autocustom`, loader-based editors, fallback slices) with diagnostics requirements. + +## 4. Typed View Definition and XML Import + +- [ ] 4.1 Define the typed view-definition model for sections, fields, sequences, tables, tree nodes, editor descriptors, visibility, ghost behavior, stable IDs, writing-system metadata, command affordances, validation hints, virtualization hints, localization/resource keys, and accessibility metadata. +- [ ] 4.2 Implement or extend XML Parts/Layout import into typed view-definition/Presentation IR using existing Inventory/LayoutCache semantics where feasible. +- [ ] 4.3 Add deterministic snapshot tests for compiled IR from LexEntry detail layouts and selected override fixtures. +- [ ] 4.4 Add unsupported-construct diagnostics with layout part and node path. +- [ ] 4.5 Add cache key, invalidation, async compile, and cancellation tests. +- [ ] 4.6 Ensure off-thread compilation uses immutable layout, metadata, writing-system, custom-field, and override snapshots rather than live WinForms controls, `PropertyTable`, or cache mutation state. + +## 5. Graphite and Font Decommissioning + +- [ ] 5.1 Inventory and classify Graphite/native rendering code/assets: `Src/views/lib/GraphiteEngine.*`, `Src/views/lib/GraphiteSegment.*`, render-engine selection, Graphite feature UI/storage, sample/dist assets, package/build artifacts, and Graphite-specific tests/docs. +- [ ] 5.2 Inventory writing-system Graphite settings and persistence: `IsGraphiteEnabled`, `DefaultFontFeatures`, `FontEngines.Graphite`, import/export formats, project fixtures, and user-visible settings. +- [ ] 5.3 Classify Graphite feature UI/storage in writing-system dialogs and define OpenType/HarfBuzz replacements where supported, plus explicit diagnostics/rollback for unsupported Graphite-only settings. +- [ ] 5.4 Define replacement font/fallback policy for Graphite-only fonts, including project diagnostics and user-facing migration evidence. +- [ ] 5.5 Prove the migrated Avalonia default path has no unapproved runtime dependency on native Graphite render engines, Graphite-enabled legacy render selection, Gecko Graphite rendering, or unclassified Graphite-only feature settings. +- [ ] 5.6 Audit Gecko/XULRunner preview, print, and PDF paths: startup Graphite preference, `XWebBrowser` consumers, dictionary/interlinear/configuration previews, `GeckofxHtmlToPdf`, and `FieldWorksPdfMaker` packaging. +- [ ] 5.7 Select and validate a non-Graphite browser/PDF strategy for default Avalonia workflows, or explicitly leave affected paths outside the default Lexical Edit boundary. +- [ ] 5.8 Add validation proving Avalonia default readiness is blocked while any unapproved default-path Graphite/native-rendering dependency or unsupported Graphite-only setting remains. + +## 6. Avalonia Control Slices + +- [ ] 6.1 Replace/prove writing-system text display/editor foundation and simple scalar editors with FieldWorks-owned Avalonia controls over IR nodes. +- [ ] 6.2 Implement writing-system-aware text editor behavior using project font settings, flow direction, culture/script metadata, supported OpenType/HarfBuzz feature settings, and diagnostics for unsupported Graphite-only settings. +- [ ] 6.3 Implement popup/hover chooser controls using Avalonia flyouts/context menus and a service-backed chooser model. +- [ ] 6.4 Spike TreeView/tree-table rendering for multiple translations per sense/term, including compact multi-writing-system node templates. +- [ ] 6.5 Record any Avalonia package update or local/upstream control patch with parity justification and test evidence. +- [ ] 6.6 Add Avalonia.Headless tests for command shortcuts, popup focus return, validation errors, edit commit/cancel, keyboard/IME behavior, accessibility metadata, and disposal cleanup. +- [ ] 6.7 Add styling/resource and density token gates for shared `FwAvalonia` resources before broad editor rollout. +- [ ] 6.8 Make the first editable Avalonia slice satisfy `avalonia-edit-sessions`, `avalonia-validation`, `avalonia-undo-redo`, and the local screen phase of `avalonia-command-focus` before expanding to more editors. + +## 7. Tables, Slices, and Lexical Edit Migration + +- [ ] 7.1 Build a virtualized Avalonia table/browse view path over typed view definitions. +- [ ] 7.2 Compare legacy XMLViews table semantics against typed IR and Avalonia table semantics. +- [ ] 7.3 Migrate one representative vertical slice: LexEntry identity + morph type + one nested sense/gloss + chooser path. +- [ ] 7.4 Expand to core P0/P1 parity checklist items from the migrated Speckit parity list. +- [ ] 7.5 Gate full Lexical Edit replacement on semantic parity, UIA2 legacy baselines, Avalonia.Headless tests, render comparison evidence, native viewing/render seam audit evidence, and no unapproved Graphite/native-rendering default-path dependency. +- [ ] 7.6 Add a control-selection decision matrix for `TreeView`, `TreeDataGrid`, `ItemsRepeater`, and owned virtualized controls using density, virtualization, selection, accessibility, licensing/version, and multi-writing-system criteria. +- [ ] 7.7 Add large-fixture performance budgets for open time, scroll/expand latency, typing latency, realized control count, memory, and cache invalidation. + +## 8. C++ Viewing/Render Seam Decommissioning + +- [ ] 8.1 Inventory all native Views/C++ viewing/rendering/editor dependencies reachable from the targeted Lexical Edit region, including `RootSite`, `IVwEnv`, RootBox/ViewSlice paths, `ManagedVwWindow`, measurement, selection, hit testing, scrolling, editor realization, and text rendering adapters. +- [ ] 8.2 Classify dependencies as baseline-only, non-migrated-region-only, custom linguistics service dependency, or blocker for the targeted migrated region. +- [ ] 8.3 Replace region-local C++ viewing/rendering/editor usage with managed/Avalonia services for text shaping, measurement, selection metadata, hit testing, scrolling, rendering, and editor realization. +- [ ] 8.4 Add tests or instrumentation proving the migrated region does not instantiate or call native Views/C++ viewing/rendering/editor infrastructure at runtime. +- [ ] 8.5 Remove or disable region-local native viewing/render adapters after replacement tests pass, while leaving shared native Views code available for non-migrated consumers. +- [ ] 8.6 Track any repo-wide native Views deletion blockers that remain outside the migrated Lexical Edit region. +- [ ] 8.7 Classify non-viewing native dependencies such as spell-check interop, parser tools, XAmple, Encoding Converters, ICU, Expat/ParserObject, and reg-free COM tooling as custom linguistics/service/tool dependencies unless they own display, layout, hit testing, selection, editor realization, or other Avalonia viewing behavior. +- [ ] 8.8 Define service seams for retained custom linguistics engines so Avalonia consumes results through managed contracts and never hosts their UI/render/editor infrastructure. + +## 9. XML Retirement Planning + +- [ ] 9.1 Design canonical post-XML view-definition authoring/storage format. +- [ ] 9.2 Build XML-to-typed-definition migration tooling and audit reports. +- [ ] 9.3 Prove migration on shipped LexEntry/LexSense layouts and selected user override fixtures. +- [ ] 9.4 Disable runtime XML for a gated migrated surface while retaining import/audit fallback. +- [ ] 9.5 Document remaining XML blockers, especially custom fields, ghost items, table views, choosers, TreeView-heavy views, and any remaining native viewing/render coupling. + +## 10. Validation + +- [ ] 10.1 Run targeted managed tests for changed areas using `./test.ps1` filters or relevant VS Code tasks. +- [ ] 10.2 Run render/parity baseline tests for affected surfaces. +- [ ] 10.3 Run native viewing/render seam audit tests/instrumentation for any region claimed as migrated. +- [ ] 10.4 Run Graphite/native-rendering default-path validation for any region proposed as default Avalonia UI. +- [ ] 10.5 Run browser/PDF replacement validation for default-path XHTML preview, print, or PDF flows. +- [ ] 10.6 Run `./build.ps1` before implementation work is considered ready for review. +- [ ] 10.7 Run `CI: Full local check` before commit/push. +- [ ] 10.8 Verify every migrated-region manifest has passing evidence for native-call instrumentation, no unapproved Graphite/native-rendering default-path dependency, undo/redo, accessibility, localization, keyboard/IME, customer override fixtures, performance budgets, and rollback behavior. +- [ ] 10.9 Invoke the shell-global phase of `avalonia-command-focus`, `avalonia-ui-scheduler`, and `avalonia-lifetime` through `fieldworks-avalonia-shell-migration` instead of redefining those seams ad hoc during shell work. diff --git a/openspec/changes/lexical-edit-avalonia-migration/view-inventory.md b/openspec/changes/lexical-edit-avalonia-migration/view-inventory.md new file mode 100644 index 0000000000..3ea69ad60f --- /dev/null +++ b/openspec/changes/lexical-edit-avalonia-migration/view-inventory.md @@ -0,0 +1,68 @@ +# Lexical Edit View Inventory + +This inventory maps the current standard Lexical Edit stack and the AdvancedEntry Avalonia spike. It is intentionally source-backed: proposed seams are called out as proposed, not as existing implementation. AdvancedEntry prototype implementation files are split to `010-advanced-entry-preview-prototype`; this foundation branch keeps only the inventory and seam expectations. + +## 1. Legacy Edit Host + +| Component | Source | Role | Migration Risk | +|---|---|---|---| +| `RecordEditView` | [Src/xWorks/RecordEditView.cs](Src/xWorks/RecordEditView.cs) | xWorks edit pane that hosts the detail tree, participates in mediator/property-table routing, and handles refresh/selection context. | Shell integration, XCore commands, focus ownership, and record navigation must remain stable while a migrated region is hosted. | +| `DataTree` | [Src/Common/Controls/DetailControls/DataTree.cs](Src/Common/Controls/DetailControls/DataTree.cs) | WinForms slice host that expands XML layouts into row controls, listens for `PropChanged`, manages refresh, selection, scroll, and slice lifecycle. | High. It combines layout interpretation, refresh coordination, focus, WinForms controls, and LCModel notifications. | +| `Slice` and subclasses | [Src/Common/Controls/DetailControls/Slice.cs](Src/Common/Controls/DetailControls/Slice.cs) and sibling files | Base row abstraction for editors, launchers, labels, accessibility names, and chooser launch routing. | High. Editor realization and launcher behavior are distributed across many subclasses. | +| `SliceFactory` | [Src/Common/Controls/DetailControls/SliceFactory.cs](Src/Common/Controls/DetailControls/SliceFactory.cs) | Static factory that maps XML part/editor attributes and field metadata to concrete legacy slices. | Registry extraction must preserve fallback diagnostics, custom editors, and reuse-map behavior. | + +## 2. Legacy Layout and Override Sources + +| Component | Source | Role | Migration Risk | +|---|---|---|---| +| Parts/layout XML | `DistFiles/Language Explorer/Configuration/Parts` | Shipped `.fwlayout` and `*Parts.xml` definitions for LexEntry, LexSense, Morphology, lists, and custom field placeholders. | Typed IR import must preserve labels, field/flid binding, visibility, ghost behavior, writing-system metadata, and custom-field insertion. | +| `Inventory` / layout cache usage | `LayoutCache`, `Inventory`, and xWorks/XMLViews callers | Runtime merge and lookup of shipped and project-specific layout definitions. | Project override precedence and conflict resolution must be tested before XML retirement. | +| Dictionary/reversal configs | [Src/xWorks/DictionaryConfigurationMigrator.cs](Src/xWorks/DictionaryConfigurationMigrator.cs) and `DictionaryConfigurationMigrators` | Migrates legacy dictionary/reversal models and preserves user customizations. | Existing migration behavior is broad and customer-sensitive; selected fixtures must be carried into typed-definition parity tests. | +| CSS overrides | [Src/xWorks/CssGenerator.cs](Src/xWorks/CssGenerator.cs) | Generates and locates `ProjectDictionaryOverrides.css` / `ProjectReversalOverrides.css` for legacy preview/export styling. | Decide whether migrated Lexical Edit ignores, translates, or leaves CSS to legacy preview/export paths. | + +## 3. Browse and XMLViews Table Surfaces + +| Component | Source | Role | Migration Risk | +|---|---|---|---| +| `RecordBrowseView` | [Src/xWorks/RecordBrowseView.cs](Src/xWorks/RecordBrowseView.cs) | xWorks wrapper for browse/table views next to edit views. | Shell and list selection interactions can affect edit-view navigation. | +| `BrowseViewer` | [Src/Common/Controls/XMLViews/BrowseViewer.cs](Src/Common/Controls/XMLViews/BrowseViewer.cs) | Tabular browse controller for columns, filters, sorts, and selection. | Requires semantic and UI automation baselines before replacing with Avalonia table controls. | +| `XmlView` | [Src/Common/Controls/XMLViews/XmlView.cs](Src/Common/Controls/XMLViews/XmlView.cs) | Native Views-backed XML rendering site for table and preview surfaces. | Native Views dependency must be classified as baseline-only or blocker for any migrated default path. | + +## 4. Choosers and Launchers + +| Component | Source | Role | Migration Risk | +|---|---|---|---| +| `ReallySimpleListChooser` / `LeafChooser` | [Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs](Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs) | Modal chooser forms for flat and hierarchical selections. | Needs a testable chooser model and dialog service before Avalonia popup parity can be claimed. | +| `ChooserCommand` | [Src/Common/Controls/XMLViews/ChooserCommandBase.cs](Src/Common/Controls/XMLViews/ChooserCommandBase.cs) | Encapsulates chooser commit behavior. | Transaction and rollback semantics must be characterized. | +| `MorphTypeAtomicLauncher` | [Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs](Src/Common/Controls/DetailControls/MorphTypeAtomicLauncher.cs) | Complex morph-type swap workflow, data-loss prompts, form swaps, MSA replacement, refresh/focus side effects. | First humble-object extraction target; modal prompts currently block full pure-logic coverage. | + +## 5. AdvancedEntry Avalonia Spike + +| Component | Source | Current Role | Gaps Before Production Migration | +|---|---|---|---| +| Project root | Split branch | net8 Avalonia module and preview target. | Must stay preview-host friendly and detached from full app shell until shell migration. | +| Presentation IR | Split branch | Immutable node model for fields, objects, sequences, sections, visibility, and ghost metadata. | Needs first-class editor kind, writing-system metadata, stable accessibility IDs, and class/flid/object binding for full semantic normalization. | +| Layout compiler | Split branch | Compiles resolved XML layout contracts into Presentation IR. | Needs override fixtures, unsupported-construct diagnostics, cache invalidation, cancellation, and immutable metadata snapshots. | +| Parts loader | Split branch | Loads shipped parts/layout XML from selected directories. | Must consume merged default + project override inputs before XML retirement. | +| Edit session | Split branch | Prototype fenced LCModel undo-task session with `Save` and `Cancel`. | Docs and tests must not assume staged draft semantics until that implementation exists in the product migration path. | +| Property-grid prototype | Split branch | First-slice candidate for descriptors, lazy sequences, and staged views. | Needs accessibility, localization, focus, keyboard/IME, and validation presentation gates before production editing. | + +## 6. Hidden Dependency Checklist + +Before declaring any migrated Lexical Edit region default-ready, search and/or instrument for these dependencies: + +- WinForms controls: `DataTree`, `Slice`, `ViewSlice`, `RootSiteControl`, `BrowseViewer`, `XmlView`. +- Native Views/rendering: `IVwRootBox`, `IVwEnv`, `IVwGraphics`, `IRenderEngine`, `GraphiteEngineClass`, `UniscribeEngineClass`. +- Browser/PDF preview/export: `GeckoWebBrowser`, `XWebBrowser`, `GeckofxHtmlToPdf`, `FieldWorksPdfMaker`, PDF `--graphite` flags. +- Global COM/registration: FieldWorks must preserve registration-free COM; no migrated path may add global COM registration or registry hacks. +- Writing systems and fonts: `IsGraphiteEnabled`, `DefaultFontFeatures`, `FontEngines.Graphite`, Graphite-only font feature settings, custom font fallback. + +## 7. Inventory Acceptance Criteria + +An inventory entry is trustworthy only when it has: + +1. A source path that exists in the current branch, or an explicit split-branch marker when the source has been moved out of this branch. +2. Current/proposed status clearly marked. +3. Known callers or consumers searched when the surface is structural. +4. Tests or planned tests listed by behavior, not only by file name. +5. A risk classification: baseline-only, first-slice blocker, shell-phase blocker, or repo-wide cleanup. \ No newline at end of file