Skip to content

LT-22625: Add the WinForms to Avalonia conversion foundation - #964

Open
johnml1135 wants to merge 78 commits into
mainfrom
phase1-base
Open

LT-22625: Add the WinForms to Avalonia conversion foundation#964
johnml1135 wants to merge 78 commits into
mainfrom
phase1-base

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

The WinForms to Avalonia conversion foundation

LT-22625

This is the foundational change for converting FieldWorks WinForms dialogs and
views to Avalonia. It delivers three things: a documented, repeatable process
for performing a conversion, the support code that process targets, and
example conversions that prove both are usable.

Default behavior is unchanged. UIMode defaults to Legacy and both gate
predicates fail closed, so with no action FieldWorks does exactly what main
does. Open Tools -> Options -> Interface and set New UI (preview) to
New, and the converted views render in Avalonia, live, without a restart.


Support code

The reusable machinery. Nothing in this section is specific to a converted
view or dialog, and a new conversion should add nothing here.

View definitions to Avalonia controls

Src/Common/FwAvalonia/ (77 files), Src/xWorks/Avalonia/ (22 files)

  • ViewDefinition/ViewDefinitionCompiler compiles the existing
    Configuration/**/*.xml Parts and Layout files into an immutable,
    fingerprinted snapshot cached by (class, layout, type, fingerprint). Migrating
    a view edits no layout XML.
  • Composer/DetailComposer walks that snapshot against LCModel and produces a
    DetailModel plus an IDetailEditContext, applying per-project
    ViewDefinitionOverride files. Each node carries a HostRouting value
    (product / preview / unsupported), so real LCModel wiring and preview-host
    sample wiring can never be silently confused.
  • Detail/DataTree renders the model as a UserControl, with the owned
    controls beside it (FwFieldControls, FwOptionChooser,
    FwStructuredTextField, DetailMenuFlyout, DetailFocusMemory).
  • Plugins/SlicePlugins holds ISlicePlugin, keyed by the legacy layout's
    class= string. SlicePluginRegistry.Register throws on a double claim: one
    owner per legacy class, no silent overwrite.

The gate: which framework renders a tool

Src/Common/FwAvalonia/UIFramework.cs and friends,
Src/Common/FwUtils/UIModeGates.cs

  • UIFramework { Legacy, Avalonia } is the value UIFrameworkResolver.Resolve
    returns, UIFrameworkSelectionService wraps in its per-host decision, and
    RecordEditView stores to choose which of its two children to build and show.
  • LexiconFeatureCatalog is the single list of tools that have a working
    Avalonia implementation, with the display name, description, and grouping a
    settings UI shows. UIFrameworkRegistry is built from it, so the gate and
    the user-visible list cannot drift apart.
  • UIFrameworkResolver.Resolve answers, for one tool, whether Legacy WinForms
    or Avalonia renders it. Precedence: an unregistered tool is always Legacy;
    otherwise an explicit override wins (tests use this); otherwise the persisted
    UIMode decides. Only a case-insensitive "New" selects Avalonia, so null,
    blank, or an unrecognized value falls back to Legacy. The type has no
    Avalonia dependency, so the decision is unit-testable without a UI runtime.
  • UIFrameworkSelectionService.Decide classifies each host's behavior under
    the global switch (supported / explicit legacy fallback), and per-tool
    opt-outs (UIModeDisabledTools) are honored by the hosting layer, so New
    mode is not all-or-nothing.
  • UIMode is a PropertyTable string defaulting to Legacy. Call sites branch
    through a [MethodImpl(MethodImplOptions.NoInlining)] helper so the Avalonia
    assemblies are never loaded on the legacy path.

Dialog hosting

Src/Common/FwAvaloniaDialogs/ (57 files),
Src/Common/FwAvalonia/AvaloniaDialogHost.cs

  • AvaloniaDialogHost.ShowModal hosts an Avalonia UserControl inside a
    WinForms-owned modal Form, which is what makes coexistence possible: a
    converted dialog can be launched from unconverted WinForms code.
  • AvaloniaDialogLauncher<TState, TViewModel, TPayload> is the template method
    every conversion fills in (build state, create view model, create view,
    apply). It collapses a non-accepted result to cancelled in one place.
  • DialogViewModelBase supplies RequestClose, Accepted, CanOk,
    ApplyChanges, and the generated OK/Cancel commands.
  • DialogTheme and the shared sizing tokens keep converted dialogs visually
    consistent without per-dialog styling.
  • Strings live in FwAvaloniaDialogsStrings.resx. Localization stays in resx.

Preview host

Src/Common/FwAvaloniaPreviewHost/ (7 files)

Renders a converted dialog or view standalone, so layout work does not
require launching FieldWorks and opening a project.


Example conversions

Proof that the support code and the process work. These are the deliverable's
evidence, not its purpose.

Detail views (4 tools)

Enrolled in LexiconFeatureCatalog: lexiconEdit, lexiconEditPopup,
notebookEdit, posEdit. Each renders through the compiler and composer
above, from the unmodified layout XML.

Dialog bodies (9)

AddNewSenseDlg, ChooserDialog, CreateFeatureDialog, EntryGoDialog,
FeatureChooserDialog, InsertEntryDlg, LexOptionsDlg, MessageBox,
MsaCreatorDlg.

Product wiring (14 launchers)

Src/LexText/**/Avalonia/ (39 files). One launcher per legacy dialog entry
point, each selected at its call site by the resolved framework.

The reuse pattern matters more than the count: EntryGoDialog is one dialog body
re-skinned by six launchers (LcmGoToEntryDialogLauncher,
LcmLinkEntryOrSenseDialogLauncher, LcmLinkAllomorphDialogLauncher,
LcmLinkMsaDialogLauncher, LcmMergeEntryDialogLauncher,
LcmAddAllomorphDialogLauncher). Only the title, prompts, search filter, and
on-OK behavior differ. Three members of that legacy family are not yet
converted (ReversalEntryGoDlg, RecordGoDlg, WordformGoDlg); each needs a
launcher, not a view.


The process

This is what makes the change foundational rather than four screens and some
dialogs.

Agent procedure (15 skills added, 3 updated)

.claude/skills/ (37 files). convert-dialog and convert-slice drive one
conversion through analysis, developer alignment, test planning, exemplar
mapping, design, and implementation. The middle stages are gates: an interactive
question that ends the turn, not a paragraph to read past. Analysis is read-only,
so it is safe to run while a developer has the live application open.

Supporting skills cover the areas a conversion routinely gets wrong:
dialog-update (keeping a converted pair in sync), create-integration-test,
fieldworks-code-commenting (the C# comment standard, referenced from
AGENTS.md), fieldworks-localization-review,
fieldworks-semantic-render-parity, fieldworks-ui-wiring-review,
fieldworks-uia2-parity-testing, fieldworks-managed-netfx-review,
fieldworks-migration-scope-review, fieldworks-test-coverage.

Developer guides

Docs/migration/ (4 files): avalonia-migration-overview.md,
migrate-a-dialog.md, migrate-a-slice-type.md, and adjust-the-layout.md --
the hand-tuning path for a developer who wants to adjust a converted layout in
Visual Studio rather than accept what the conversion produced.

Shared language

CONTEXT.md carries the project glossary the conversion vocabulary is
grounded in. Terms that accumulated conflicting senses during the work were
renamed rather than documented around: the framework choice is UIFramework,
product-vs-preview wiring is HostRouting, and the glossary records the
retirement of the words they replaced.

Durable contracts and lessons

openspec/specs/ (41 files) holds the contracts a future change must not break.
Docs/lessons/avalonia-migration/ (7 files) is an indexed set of lesson cards
recording reusable observations and decision boundaries. The cards constrain
future research; they do not authorize restoring retired code.


Unfinished work is visible, not absent

Two mechanisms, with different scopes. Inside a converted detail view, a slice
the Avalonia path cannot yet render composes as a labeled Unsupported row
with a stable automation id, never a silent omission -- grep those rows and you
have the remaining slice work. This is why the enrolled tools can ship
before every field kind exists. Everything else that is unconverted -- dialogs
without a launcher, tools not in the registry -- shows nothing new at all: it
stays entirely on the WinForms path via the gate, so there is no half-rendered
state anywhere.


Tests

139 new test files, 1,590 test methods, +34,895 lines. Headless Avalonia tests
for views and view models, composer tests against a real LCModel cache, gate and
fallback tests, automation-id convention tests, and semantic snapshot comparisons
against the WinForms views.


Scope

476 files, +82,359 / -470, against main. Tests are counted separately from the
source rows below, not within them.

Area Files Added
Tests (all areas) 149 34,895
Src/Common/FwAvalonia (core) 77 15,715
Src/Common/FwAvaloniaDialogs 57 8,372
Src/xWorks (hosting, composer) 22 6,712
Src/LexText (launchers, gates) 39 6,362
.claude/skills (process) 37 4,788
Build and solution configuration 11 2,129
openspec/specs 41 1,111
Docs/migration 4 587
Src (gate call sites elsewhere) 22 558
Docs/lessons 9 547
Src/Common/FwAvaloniaPreviewHost 7 508
Docs (other) 1 75

The 470 deletions are gate call sites and comment cleanup. No legacy WinForms
view or dialog is removed by this change.


Verification

CI on this head is the source of truth. Locally: whole-solution build clean, and
the Avalonia and composer test projects green.

To exercise it by hand, set New UI (preview) to New and confirm the four
tools render and the converted dialogs open; then set it back to Legacy and
confirm the WinForms path is untouched.


What is deleted when the conversion is complete

Avalonia-in-WinForms hosting. Exists only to put Avalonia content inside a
WinForms parent.

AvaloniaDialogHost.cs, AvaloniaHostControlBase.cs, DetailHostControl.cs,
InputKeyClaimingAvaloniaHost.cs, FinalizerSafeSynchronizationContext.cs,
FwAvaloniaRuntime.cs, Seams/IXCoreCommandBridge.cs,
Src/xWorks/Avalonia/Hosting/RecordEditView.Avalonia.cs

Framework selection. Chooses Legacy or Avalonia per tool; one framework
leaves nothing to choose.

UIFramework.cs, UIFrameworkResolver.cs, UIFrameworkRegistry.cs,
UIFrameworkSelectionService.cs, LexiconFeatureCatalog.cs,
EditControlFactory.cs, Src/Common/FwUtils/UIModeGates.cs, the UIMode
setting and its Options-dialog editor, and every NoInlining call site

Legacy managed UI. Every WinForms dialog and control, plus the record-edit
stack behind them, replaced by the Avalonia views and dialog bodies.

163 Form subclasses, 84 UserControl subclasses, 236 .Designer.cs files and
their .resx companions, plus Src/Common/Controls/DetailControls (88 files,
DataTree and the Slice hierarchy), Src/Common/Controls/XMLViews (66),
Src/Common/RootSite (44), Src/Common/SimpleRootSite (42), and Src/XCore
(96, the mediator and XWindow)

Native Views library. The C++ COM engine that draws every legacy field;
Avalonia renders and shapes its own text, which
lexical-edit-font-decommissioning already requires.

Src/views entire -- 136 C++ files (VwRootBox, VwEnv, VwGraphics, the
lazy-box layout, lib/GraphiteEngine, lib/UniscribeEngine), the Views.idh
COM interfaces, and the STA constraint the render path imposes on the process

FieldWorks.Main changes rather than goes. Its
Application.EnableVisualStyles() and Application.Run() become
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args) -- the call the preview
host already makes -- so Avalonia owns the message loop. The
Application.ThreadException hook becomes a dispatcher handler and
Application.DoEvents moves to Dispatcher.UIThread.

Runtime XML layout parsing retires separately, view by view as parity gates pass,
leaving the typed view definitions as the runtime source
(lexical-edit-view-definition).


This change is Reviewable

@github-actions

This comment has been minimized.

johnml1135 added a commit that referenced this pull request Jul 1, 2026
…ing docs

Per deep review of the Phase-1 base PR, this removes ~10,300 lines of
openspec content that does not belong in the base PR:

Deleted entirely (self-declared superseded, or already implemented on a
sibling branch with no home needed here):
- openspec/changes/graphite-transition-support/ (self-labeled superseded
  throughout; content restated elsewhere)
- openspec/changes/fieldworks-avalonia-shell-migration/ (self-labeled
  folded into/superseded by avalonia-end-game)
- openspec/changes/avalonia-interlinear-editor/ (implemented on
  phase1-followup-interlinear, commit 3c5893e)
- openspec/changes/avalonia-rule-formula-editor/ (implemented on
  phase1-followup-rule, commit 2c142bc)

Deleted here for relocation to phase1-docs (speculative future-phase
planning with real future value, not needed to review/merge this PR):
- openspec/changes/avalonia-migration-roadmap/complete-migration-program.md,
  epics/**, reviews/** (JIRA-epic drafting for unstarted stages 5-13)
- openspec/changes/legacy-screenshot-capture/ (dev tooling supporting a
  Docs/migration/ effort this PR isn't carrying)
- openspec/changes/avalonia-end-game/ (depends on a Phase-1 burn-down this
  PR hasn't finished)

Trimmed/deleted within datatree-model-view-separation (this change's own
proposal.md/hybrid-alignment.md already carry a 2026-06-09 supersession
note saying DataTreeModel/SliceSpec/IDataTreeView "should not be built";
these were the pieces that never caught up to that note):
- Deleted specs/datatree-model/spec.md (asserted the abandoned
  DataTreeModel/SliceSpec/IDataTreeView requirements as live ADDED
  reqs); kept specs/datatree-partial-split/spec.md (the partial-class-split
  slice the proposal says remains valid as optional legacy maintenance)
- Deleted three overlapping draft test plans for the abandoned design:
  testing-approach-2.md, test-plan-forms.md, test-plan-forms-future.md
- Deleted stale coverage-gap planning docs:
  specs/changes-from-test-before-refactor/coverage-wave2-test-matrix.md
  and ".../tests to fix coverage gaps.md"
- Trimmed datatree-mental-model.md to the current-state description only,
  removing the "target shape after split" section describing the
  abandoned architecture

Fixed two stale artifacts that never caught up to their sibling
supersession notes:
- avalonia-migration-roadmap/specs/avalonia-migration-roadmap/spec.md:
  added a supersession note to the "DataTree split is the first migrated
  region" requirement and added an as-built scenario describing the
  actual region-model path (ViewDefinitionModel/LexicalEditRegionModel),
  matching the note already in this change's own design.md
- lexical-edit-avalonia-migration/architecture-diagrams.md: removed the
  IPropertyStateStore port node (never built per task 18.6; state flows
  through IRecordNavigationContext + host PropertyTable), annotating the
  Navigation port instead

Kept as-is per review: avalonia-migration-roadmap/{proposal,design,
tasks}.md + specs/ (the ordered roadmap every later stack PR needs),
avalonia-multi-writing-system-text-foundation/ (substantially shipped,
not speculative), and the rest of datatree-model-view-separation
(design.md/tasks.md/proposal.md already carry accurate snapshot framing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@johnml1135
johnml1135 force-pushed the phase1-base branch 2 times, most recently from 10d7181 to af20c5b Compare July 1, 2026 16:23
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

NUnit Tests

    1 files  ±    0      1 suites  ±0   8m 16s ⏱️ - 2m 44s
5 757 tests +1 444  5 676 ✅ +1 436  81 💤 +8  0 ❌ ±0 
5 766 runs  +1 444  5 685 ✅ +1 436  81 💤 +8  0 ❌ ±0 

Results for commit c32a2bd. ± Comparison against base commit 8d54b33.

♻️ This comment has been updated with latest results.

@github-actions

This comment has been minimized.

johnml1135 added a commit that referenced this pull request Jul 3, 2026
…ss, openspec

Records accumulated phase-1 working-tree changes not part of the Options-dialog
work: adoption of the shared AvaloniaDialogTestHarness across the dialog test
suites, LexicalBrowse* / ViewDefinition test updates, the FwAvalonia Preview/**
compile exclusion (PR #964 review §4 finding F), preview-host project ref, and
openspec roadmap edits (incl. removing the superseded datatree-model-view-
separation change). Bundled as one checkpoint; the tree builds and all touched
test suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

johnml1135 added a commit that referenced this pull request Jul 3, 2026
Records accumulated phase-1 working-tree changes not part of the Options-dialog
work: adoption of the shared AvaloniaDialogTestHarness across the dialog test
suites, LexicalBrowse* / ViewDefinition test updates, the FwAvalonia Preview/**
compile exclusion (PR #964 review §4 finding F), preview-host project ref, and
openspec roadmap edits (incl. removing the superseded datatree-model-view-
separation change). Bundled as one checkpoint; the tree builds and all touched
test suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@johnml1135
johnml1135 force-pushed the phase1-base branch 2 times, most recently from 6636e17 to 40ae9d1 Compare July 3, 2026 20:54
johnml1135 added a commit that referenced this pull request Jul 4, 2026
Stacked on the interlinear follow-up. Restore the rule-formula surface
and activate it:
- add back the 29 rule-formula files (the 5 plugins,
  RuleFormulaModel/RegionEditor + RuleCellCommands, PhEnvironment +
  BasicIPASymbol editors, projector/sinks/options + deriver, and all
  their tests incl. SupportingEditorComposeTests)
- restore the 5 plugin registrations in RegionEditorPlugins
- restore the 5 rule class names in the burn-down census
- FLIP: register the 6 rule tools (PhonologicalRuleEdit,
  EnvironmentEdit, compoundRuleAdvancedEdit, naturalClassedit,
  phonemeEdit, AdhocCoprohibEdit) in LexicalEditFeatureCatalog under a
  new "Grammar rule editors" group (restoring the canonical pre-split
  active set + parity notes); Phase1FollowUpSurfaceTools is now empty
  (both edit follow-ups landed; browse table gated separately)
- add the 6 rule TestCase rows to RegisteredRecordEditTools_ResolveToAvalonia

Rebased onto the squashed phase1-base (post PR #964 review): tool
registration is now catalog-driven, so this flip also adds a new
"Grammar rule editors" group (6 rows) to the Tools->Options "Manage
Individual Features" dialog under UIMode=New.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 4, 2026
Restore the interlinear surface on top of phase1-base and activate it:
- add back the 11 interlinear files (InterlinearRegionEditor + analysis
  model + projector/write-back + plugin + their tests, incl. the
  FwAvalonia model and Visual tests)
- restore the InterlinearSlicePlugin registration in RegionEditorPlugins
- restore the interlinear class name + resolve assertion in the
  burn-down census
- FLIP: register "Analyses" in LexicalEditFeatureCatalog (with new
  display-name/description strings), so it flows into the catalog-
  sourced DefaultSupportedTools and the Words Analyses detail editor
  resolves to Avalonia under UIMode=New; removed from
  Phase1FollowUpSurfaceTools
- add the "Analyses" TestCase back to
  RegisteredRecordEditTools_ResolveToAvalonia

The browse "Analyses" list pane stays inert (table follow-up territory).

Rebased onto the squashed phase1-base (post PR #964 review): the base PR
had since refactored tool registration to be catalog-driven, so this
flip now also adds a "Words Analyses" row to the Tools->Options "Manage
Individual Features" dialog under UIMode=New.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 4, 2026
Stacked on the interlinear follow-up. Restore the rule-formula surface
and activate it:
- add back the 29 rule-formula files (the 5 plugins,
  RuleFormulaModel/RegionEditor + RuleCellCommands, PhEnvironment +
  BasicIPASymbol editors, projector/sinks/options + deriver, and all
  their tests incl. SupportingEditorComposeTests)
- restore the 5 plugin registrations in RegionEditorPlugins
- restore the 5 rule class names in the burn-down census
- FLIP: register the 6 rule tools (PhonologicalRuleEdit,
  EnvironmentEdit, compoundRuleAdvancedEdit, naturalClassedit,
  phonemeEdit, AdhocCoprohibEdit) in LexicalEditFeatureCatalog under a
  new "Grammar rule editors" group (restoring the canonical pre-split
  active set + parity notes); Phase1FollowUpSurfaceTools is now empty
  (both edit follow-ups landed; browse table gated separately)
- add the 6 rule TestCase rows to RegisteredRecordEditTools_ResolveToAvalonia

Rebased onto the squashed phase1-base (post PR #964 review): tool
registration is now catalog-driven, so this flip also adds a new
"Grammar rule editors" group (6 rows) to the Tools->Options "Manage
Individual Features" dialog under UIMode=New.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 4, 2026
Restore the interlinear surface on top of phase1-base and activate it:
- add back the 11 interlinear files (InterlinearRegionEditor + analysis
  model + projector/write-back + plugin + their tests, incl. the
  FwAvalonia model and Visual tests)
- restore the InterlinearSlicePlugin registration in RegionEditorPlugins
- restore the interlinear class name + resolve assertion in the
  burn-down census
- FLIP: register "Analyses" in LexicalEditFeatureCatalog (with new
  display-name/description strings), so it flows into the catalog-
  sourced DefaultSupportedTools and the Words Analyses detail editor
  resolves to Avalonia under UIMode=New; removed from
  Phase1FollowUpSurfaceTools
- add the "Analyses" TestCase back to
  RegisteredRecordEditTools_ResolveToAvalonia

The browse "Analyses" list pane stays inert (table follow-up territory).

Rebased onto the squashed phase1-base (post PR #964 review): the base PR
had since refactored tool registration to be catalog-driven, so this
flip now also adds a "Words Analyses" row to the Tools->Options "Manage
Individual Features" dialog under UIMode=New.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 4, 2026
Stacked on the interlinear follow-up. Restore the rule-formula surface
and activate it:
- add back the 29 rule-formula files (the 5 plugins,
  RuleFormulaModel/RegionEditor + RuleCellCommands, PhEnvironment +
  BasicIPASymbol editors, projector/sinks/options + deriver, and all
  their tests incl. SupportingEditorComposeTests)
- restore the 5 plugin registrations in RegionEditorPlugins
- restore the 5 rule class names in the burn-down census
- FLIP: register the 6 rule tools (PhonologicalRuleEdit,
  EnvironmentEdit, compoundRuleAdvancedEdit, naturalClassedit,
  phonemeEdit, AdhocCoprohibEdit) in LexicalEditFeatureCatalog under a
  new "Grammar rule editors" group (restoring the canonical pre-split
  active set + parity notes); Phase1FollowUpSurfaceTools is now empty
  (both edit follow-ups landed; browse table gated separately)
- add the 6 rule TestCase rows to RegisteredRecordEditTools_ResolveToAvalonia

Rebased onto the squashed phase1-base (post PR #964 review): tool
registration is now catalog-driven, so this flip also adds a new
"Grammar rule editors" group (6 rows) to the Tools->Options "Manage
Individual Features" dialog under UIMode=New.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 6, 2026
- VersionInfoProvider: copyright year no longer freezes at whatever year the
  constant was last edited, ApplicationVersion resolves from the correct
  assembly instead of always falling back to the entry assembly, and
  MajorVersion/ParseInformationalVersion index defensively instead of
  assuming a fixed part count. Covered by new VersionInfoProviderTests.cs.
- RegFree.targets: removes a dangling ManagedVwWindow.dll entry; the project
  was already retired in #904/#906, so the entry pointed at nothing.
- opsx-*.prompt.md: replace inlined instructions with delegation to the
  existing .claude/skills/openspec-*/SKILL.md files, per this repo's
  skills-over-inline-prompts convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 7, 2026
Six EXE projects (FieldWorks, LCMBrowser, UnicodeCharEditor,
GenerateHCConfig, ComManifestTestHost, NativeBuild) each import
RegFree.targets and generate manifests for the same shared managed
assemblies (FwUtils.dll, SimpleRootSite.dll, ManagedVwWindow.dll) into
the same $(OutDir). Under a parallel MSBuild build, their
CreateComponentManifests targets can run in different MSBuild worker
processes at the same time and race to read/write the exact same
manifest file, throwing an IOException that fails the whole build
(observed intermittently in CI, e.g. FieldWorks PR #964).

Wrap RegFree.Execute()'s read-modify-write of the manifest file in a
cross-process named Mutex keyed by the resolved output path, so
concurrent invocations targeting the same file serialize instead of
racing; invocations for different manifest files are unaffected and
still run fully in parallel. string.GetHashCode() is deliberately not
used for the mutex name since .NET randomizes it per process, which
would defeat cross-process synchronization - MD5 is used instead as a
deterministic fingerprint.

Added a regression test that runs 12 concurrent RegFree.Execute() calls
against the same manifest path and asserts they all succeed and produce
valid, uncorrupted XML. Verified it actually catches the regression:
temporarily reverted the mutex fix, confirmed the test fails reliably
(3/3 runs), then restored the fix and confirmed it passes reliably
(5/5 runs).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.64863% with 901 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.38%. Comparing base (84a4850) to head (c32a2bd).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
Src/Common/FwAvalonia/Detail/DetailModel.cs 77.87% 77 Missing and 104 partials ⚠️
Src/Common/FwAvalonia/Detail/FwFieldControls.cs 81.65% 115 Missing and 64 partials ⚠️
.../Common/FwAvalonia/Detail/FwStructuredTextField.cs 78.34% 41 Missing and 48 partials ⚠️
Src/Common/FwAvalonia/Detail/FwOptionChooser.cs 84.04% 32 Missing and 46 partials ⚠️
Src/Common/FwAvalonia/AvaloniaDialogHost.cs 54.54% 34 Missing and 16 partials ⚠️
Src/Common/FwAvalonia/AvaloniaHostControlBase.cs 49.39% 32 Missing and 10 partials ⚠️
...Controls/DetailControls/MorphTypeAtomicLauncher.cs 56.94% 19 Missing and 12 partials ⚠️
Src/Common/FwAvalonia/Detail/DetailFocusMemory.cs 71.15% 14 Missing and 16 partials ⚠️
Src/Common/Controls/XMLViews/FilterBar.cs 40.47% 16 Missing and 9 partials ⚠️
Src/Common/FwAvalonia/Detail/LexiconFirstSlice.cs 76.34% 16 Missing and 6 partials ⚠️
... and 19 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #964      +/-   ##
==========================================
+ Coverage   32.99%   36.38%   +3.39%     
==========================================
  Files        1202     1354     +152     
  Lines      278168   296056   +17888     
  Branches    37151    40284    +3133     
==========================================
+ Hits        91781   107732   +15951     
- Misses     158537   158989     +452     
- Partials    27850    29335    +1485     
Files with missing lines Coverage Δ
Src/Common/Controls/DetailControls/DataTree.cs 43.73% <ø> (+6.18%) ⬆️
Src/Common/FieldWorks/FieldWorks.cs 0.73% <ø> (ø)
Src/Common/FwAvalonia/CompactDialogStyles.cs 100.00% <100.00%> (ø)
...c/Common/FwAvalonia/Detail/DetailStructureRules.cs 100.00% <100.00%> (ø)
.../Common/FwAvalonia/Detail/DetailViewingServices.cs 100.00% <100.00%> (ø)
Src/Common/FwAvalonia/Detail/MorphTypeSwapLogic.cs 100.00% <100.00%> (ø)
Src/Common/FwAvalonia/FwAvaloniaDensity.cs 100.00% <100.00%> (ø)
Src/Common/FwAvalonia/FwCheckBoxStyle.cs 100.00% <ø> (ø)
Src/Common/FwAvalonia/FwPosChooser.cs 87.82% <ø> (ø)
Src/Common/FwAvalonia/FwRadioButtonStyle.cs 100.00% <ø> (ø)
... and 109 more

... and 166 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

This comment has been minimized.

The ViewModel wording sweep re-encoded six skill files and turned every
em-dash, arrow, and section sign into a literal '?'. Restore each affected
line's original characters; the sweep's wording itself is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 31, 2026
Remove the openspec change folders for the phase-1 migration research
(avalonia-migration-roadmap, avalonia-multi-writing-system-text-foundation,
lexical-edit-avalonia-migration, shared-editable-virtualized-table). Their
durable contracts were synced into openspec/specs/ (12 new spec folders);
the rest of the record lives in git history and in the provenance comment
on PR #964.

Align the migration skills, docs, and agent instructions to the code as it
exists on this branch: fix stale type and path references, drop claims the
alignment sweep proved false, and repoint provenance citations at the synced
specs or git history instead of the removed change folders.

Add the pr-pitch skill and wire pr-preflight to it as the single entrypoint
for composing PR bodies and provenance comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
johnml1135 and others added 3 commits July 31, 2026 05:55
The pitch had no length budget, so it grew into the audit it was meant to
replace: PR #964's body ran 8,700 characters and buried its own argument.
Give Phase 3 a hard 200-400 word budget scaled by risk rather than diff
size, split the reviewer's gap into known and unknown unknowns, and add
the overflow rule -- a section that will not fit was provenance-comment
material, so it moves and leaves a link.

The evidence behind each "where to look" bullet now has a home: the first
collapsed section of the provenance comment, written for a reviewer who
wants to check rather than trust.

Make that comment sticky. Phase 4 said to use the markers but never said
how, so a re-run could stack duplicates and rot the body's link. Phase 5
now gives the marker-match lookup and edits the comment in place, with
the reason not to use --edit-last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A second comment split the record for no gain. The description is the one
place a reader always looks, it is inherently sticky -- editing it is in
place and the URL never changes -- and it is the only part of a PR that
survives being read a year later without scrolling a thread. The pitch
keeps its 200-400 word budget as the top zone; the accordions move below
it, as long as the reasoning deserves, since a closed details costs a
reader nothing.

Phase 5 loses the marker-match comment machinery and gains the fold-and-
delete path for PRs that already have a provenance comment, including the
grep for tree references that would dangle after the delete.

Refresh the CONTEXT.md Avalonia vocabulary, which predated the rename
sweep: Region became Detail, and LexicalEditRegionModel,
LexicalEditSurfaceResolver, LcmRegionEditSession, RegionEditContextHolder,
IRegionEditContext and IBrowseColumnSource no longer resolve anywhere in
Src. Add the terms the vocabulary was missing -- view definition,
cross-namespace twin, slice plugin, Unsupported row.

Repoint the roadmap spec's superseded-requirement banner at the PR
description now that the comment it cited is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A comment that cites a design document is a dangling reference waiting to
happen: the document gets archived or deleted and the comment is left
pointing at nothing. Several already had. The comment standard bans them
outright, with LT-##### as the one sanctioned durable pointer.

Removed across 39 files:

- Markdown filenames: winforms-free-lexeme-editor.md, dialog-ownership.md,
  style-system.md, xml-retirement-blockers, and a cpp-build-modernization.md
  under a specs/ directory that no longer exists.
- Finding and task labels defined only in those documents: D1-D4, B7, B8,
  GAP 1, A11Y-01 through A11Y-04, NETFX-01, section 19b/19d, task N.N,
  and numbered Stage/Phase coordinates.
- Tree paths used as citations: openspec/, Docs/migration/, and named
  skills under .claude/skills.
- PR #964 review findings cited as the place the reasoning lives.

Each comment keeps its WHY, restated so it stands alone. Where the pointer
was the whole comment, the pointer goes and the sentence stays.

Two fixes fell out of touching these lines: a stale IRegionEditorPlugin
reference became ISlicePlugin, and two comments narrating what code "now"
does were rewritten to state what it does.

String literals were left alone per the comment standard -- assertion
messages, an ArgumentException message, and one Ignore reason still carry
D1/D2/D3 and similar labels. Those are a separate pass.

Comment-only: no code construct changed, and all 21 edited XML build files
still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 31, 2026
The pitch had no length budget, so it grew into the audit it was meant to
replace: PR #964's body ran 8,700 characters and buried its own argument.
Give Phase 3 a hard 200-400 word budget scaled by risk rather than diff
size, split the reviewer's gap into known and unknown unknowns, and add
the overflow rule -- a section that will not fit was provenance-comment
material, so it moves and leaves a link.

The evidence behind each "where to look" bullet now has a home: the first
collapsed section of the provenance comment, written for a reviewer who
wants to check rather than trust.

Make that comment sticky. Phase 4 said to use the markers but never said
how, so a re-run could stack duplicates and rot the body's link. Phase 5
now gives the marker-match lookup and edits the comment in place, with
the reason not to use --edit-last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
johnml1135 added a commit that referenced this pull request Jul 31, 2026
A comment that cites a design document is a dangling reference waiting to
happen: the document gets archived or deleted and the comment is left
pointing at nothing. Several already had. The comment standard bans them
outright, with LT-##### as the one sanctioned durable pointer.

Removed across 39 files:

- Markdown filenames: winforms-free-lexeme-editor.md, dialog-ownership.md,
  style-system.md, xml-retirement-blockers, and a cpp-build-modernization.md
  under a specs/ directory that no longer exists.
- Finding and task labels defined only in those documents: D1-D4, B7, B8,
  GAP 1, A11Y-01 through A11Y-04, NETFX-01, section 19b/19d, task N.N,
  and numbered Stage/Phase coordinates.
- Tree paths used as citations: openspec/, Docs/migration/, and named
  skills under .claude/skills.
- PR #964 review findings cited as the place the reasoning lives.

Each comment keeps its WHY, restated so it stands alone. Where the pointer
was the whole comment, the pointer goes and the sentence stays.

Two fixes fell out of touching these lines: a stale IRegionEditorPlugin
reference became ISlicePlugin, and two comments narrating what code "now"
does were rewritten to state what it does.

String literals were left alone per the comment standard -- assertion
messages, an ArgumentException message, and one Ignore reason still carry
D1/D2/D3 and similar labels. Those are a separate pass.

Comment-only: no code construct changed, and all 21 edited XML build files
still parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jasonleenaylor and others added 17 commits August 3, 2026 08:02
Picture editing was cut from the Avalonia detail view earlier in this
branch, which left the Avalonia picture-properties dialog with no consumer
on the new path: nothing outside its own test constructed the view or
view-model. Delete the dialog, its test, and the localization rows that
existed only to label it.

The two LCModel-free exchange types the dialog used
(DetailPictureMetadata / DetailPictureDialogResult) had no other
reference in production or test code once the dialog was gone, and no
edit-context member carried them, so they go too.

Accessors and .resx rows are removed together because
AvaloniaLocalizationTests reflects over every accessor and asserts it
resolves from the neutral resx; splitting the pair across commits would
break that test.

Legacy FwCoreDlgs/PicturePropertiesDialog and DetailControls/PictureSlice
are untouched -- the WinForms picture path still ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "Manage Individual Features" dialog is a feature-catalog management
utility reached from Tools > Options. It is unrelated to the Lexicon Edit
detail view and to the InsertEntry dialog family, so it comes out with
its Options entry point rather than shipping as a stray New-mode-only
utility.

LexiconFeatureCatalog and LexiconFeatureDescriptor STAY. The catalog is
shared: EditSurfaceRegistry.DefaultSupportedTools is built from
LexiconFeatureCatalog.ToolNames, so it remains the single list of tools
that ship with a working Avalonia surface. Only the dialog set that
rendered the catalog as a checkbox list is deleted, along with the four
FeatureManager* strings and the ManageIndividualFeatures button string.

The Options wiring is unwound end to end so no dead affordance is left:
the button in LexOptionsDlgView, the view-model's ManageFeaturesCommand
and ManageFeaturesVisible gate, the LexOptionsDlgState.ManageFeatures
callback, and the launcher's ShowManageFeaturesDialog.

LexOptionsDlgState.UIModeDisabledTools is kept. That setting is the
per-tool opt-out EditSurfaceResolver still honours (seeded into the
PropertyTable by FwXWindow and read by RecordEditView); the Options
dialog now carries it through untouched instead of editing it. Comments
that pointed at the deleted dialog are retargeted at the setting itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Picture editing was cut from the Avalonia detail view earlier in this
branch and the picture-properties dialog came out with it. Three
picture-only remnants survived with no consumer left on the new path.

FwAvaloniaStrings.PictureInsert and PictureDelete labelled the
insert/delete affordances on a picture row. Nothing constructs those
affordances now that the detail view no longer edits pictures, and grep
finds no reference to either accessor outside its own declaration.
Accessor and .resx row are removed together because
AvaloniaLocalizationTests reflects over every accessor and asserts it
resolves from the neutral resx; splitting the pair would break it.
Surviving rows keep their order.

AvaloniaHostControlBase.HostedContent existed so the host could hand the
Avalonia IStorageProvider to the picture media seam's file picker. Its
only caller was the LcmRegionMediaServices construction in
RecordEditView.Avalonia.cs, which went away with the media seam, so the
accessor is now unreferenced. CurrentContent stays -- DetailHostControl
still captures focus through it.

Kept: the composer routing that renders a picture slice as a labelled
Unsupported worklist row (EditorKindMap picture/image ->
DetailEditorCategory.Picture -> WalkUnsupported) is current behaviour,
as is the DetailOrcKind.Picture classification that makes an embedded
picture render and delete inside rich text. Legacy WinForms
PicturePropertiesDialog and PictureSlice are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The six FwAvalonia* projects sat inside Src/Common solution folders while the
other ~138 projects in FieldWorks.sln are flat at the top level, so Solution
Explorer showed them nested for no reason a reader could infer.

dotnet sln add creates solution folders mirroring the directory hierarchy
unless --in-root is passed, which is how the nesting arrived.

Removes the Common solution folder and its seven NestedProjects entries only.
Solution folders and NestedProjects are Solution Explorer presentation: the
FieldWorks.proj traversal globs Src/** and never reads the sln layout, and the
configuration sections key off project GUIDs, not folder GUIDs. Every project
declaration is untouched. The historical Src/FXT folder is left as it is.
A generated design document cited "GAP section 3.5". That number belonged to
the gap register in control-exemplar-map.md, but the citing document had its
own numbered sections, so the reference read as a pointer into the citing
document, resolved to nothing, and could not be clicked. Numbered citations
also rot the moment either document is renumbered.

Give each gap a stable slug id (GAP-PROGRESS, GAP-WIZARD, GAP-WS-SELECTOR,
GAP-TRISTATE, GAP-F1-HELP, GAP-BROWSE-GRID, GAP-ROOTSITE-VIEWS). An explicit
<a id="..."> anchor sits above each heading so links survive a heading
rename, and the id leads the heading text so a reader can quote it. The five
control-map rows that cited a gap by number now link to it by name and id.

Record the rule in convert-dialog and convert-slice so generated documents
follow it: references are links named for their target, never bare section
numbers; cross-document links carry the stable id; the anchor is verified
against the target's heading before it is written; relative paths are computed
from the working document's own directory. Convert the three bare section
references in the migration overview to verified anchored links.

Scoped to the documents that describe the conversion workflow and to the gap
register they cite. The wider sweep of lessons-learned, migration-checklist,
dialog-conversion, and the migration SKILL is deliberately not included here.
All ten anchored links were verified by resolving each fragment against the
target file's actual headings and explicit anchors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A developer taking an AI-generated dialog, slice, or control had no documented
way to adjust its visual result. The gap is not only tooling: the appearance of
a converted dialog comes from four places -- launcher C# for window size, the
axaml for structure, DialogTheme tokens for spacing, and the view-model for
what appears at all -- and editing the wrong one wastes the most time.

Add Docs/migration/adjust-the-layout.md, a third playbook beside the dialog and
slice ones and linked from both. It states up front that Avalonia has no WYSIWYG
designer, so nobody hunts for one: you edit markup and watch it render. It maps
each visual property to the file that owns it, gives three loops in preference
order, explains why a converted surface is previewable at all (the kit's
LCModel-free view-model and UserControl view are what make it possible, so
difficulty here is a design signal), and records the gotchas that cost real
time.

Make the middle loop real rather than described. FwAvaloniaPreviewHost gains a
DialogPreviewWindow base and two registered surfaces, so a developer can open
one converted dialog at its launcher's client size with the runtime's compact
density and no FLEx launch. Registration is one assembly attribute: the host's
module catalog reflects over every assembly in its own output folder, which is
its own bin rather than the shared Output, so the host now references the dialog
kit to put that assembly where the catalog can load it.

That reference also makes this Avalonia-capable exe a candidate host for the
Visual Studio previewer, which needs a BuildAvaloniaApp entry point that
FieldWorks.exe does not have -- the cause of "Unable to create AppBuilder from
type SIL.FieldWorks.FieldWorks". Whether the previewer process itself works
against this net48 target is recorded as unverified rather than claimed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dotnet sln add manufactures solution folders mirroring the directory hierarchy
unless --in-root is passed; its help states the default outright ("Place
project in root of the solution, rather than creating a solution folder.
[default: False]"). The instruction in section 3 omitted the flag, so following
it nests the new project under Src/Common while the solution's other 142
projects sit flat at the top level.

Add the flag to the command and say why: consistency with what is already
there, not a claim that solution folders are wrong, so nobody "helpfully"
re-nests later. Note that the drift is invisible in review, since a .sln diff
of that size is unreadable -- which is why the command itself has to be right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three commits on this branch removed the Avalonia picture-properties
dialog, its host remnants, and the lexicon feature-manager dialog. Each
explained itself in its commit message, but a message about a deleted
file is reachable only by archaeology, and AGENTS.md directs a planner
to Docs/lessons before working in a covered area. The knowledge was
not findable where it is meant to be looked for.

Add two cards on the established template. Picture editing covers both
picture removals, which share a cause: the capability was cut from the
detail view and the dialog and host affordances were remnants. The
feature manager gets its own card because its lesson is different -- a
New-mode-only utility with no WinForms counterpart is a standing parity
divergence, and its catalog and setting survived because neither
depended on the editor.

The picture card's first lesson is the one this branch paid for twice:
a view plus view-model with no launcher is dormant code that reads as
finished, because it compiles and has tests. A later reader took that
surface for a deliberate design and had to be corrected.

Every survivor and removal claim was checked against the tree:
LexiconFeatureCatalog, LexiconFeatureDescriptor, UIModeDisabledTools
and DefaultSupportedTools are present, DefaultSupportedTools is built
from the catalog, the six removed symbols resolve to nothing, and the
legacy WinForms picture path is intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"kit" appeared 134 times across skills, docs, code comments and XML doc
blocks, and carried at least five different meanings: a specific reusable
dialog, the shared implementation as opposed to the consumer, the
FwAvaloniaDialogs assembly, a style authority, and a copy-this exemplar
pointer. It was never defined in the terminology section, yet it decided
naming conventions ("kits keep general names"). A phrase-window census
found 65 distinct collocations across 80 windowed uses -- the term almost
never appeared in a fixed phrase, which is what let the senses drift.

Two further reasons not to keep it. GoF documents "Kit" as the alternate
name for Abstract Factory, which this is not: there is no interface
creating families of related products, only one presentation model and
several adapters. And TECkit, an unrelated SIL encoding-conversion
library, is referenced in this same repository, so a bare "kit" is
ambiguous on a grep.

No replacement collective noun is introduced, because the parts already
have standard names: the view-model is Fowler's Presentation Model (which
MVVM renamed), the Input/Payload pair are Data Transfer Objects, each
launcher is an Adapter over LCModel, and AvaloniaDialogLauncher is a
Template Method. Per sense: redundant uses lose the word ("the reusable
entry-search dialog"); where a noun was load-bearing the types are named
instead; shared-versus-consumer becomes "shared"; the assembly becomes
FwAvaloniaDialogs or "the dialog stack"; normative uses become "dialog
rule"/"dialog convention"; exemplar pointers become "converted dialog
view".

134 insertions and 134 deletions: a line-for-line swap with no
restructuring. build.ps1 -SkipNative is clean, which matters because
about 40 of the edits sit inside XML doc summary and see-cref blocks the
compiler parses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The NOT-migrated notes restated what the gated code already shows: a legacy
dialog stays on the WinForms path. They go ahead of finishing the migration,
since the gate is the fact and the comment only duplicated it.

The four notes reviewed individually are resolved in place. The PARITY label
comes off where the note describes achieved behavior, and the UI-language note
on AvaloniaOptionsDialogLauncher is replaced with what is actually undecided
rather than asserting an equivalence that was never established. Repoint
control-exemplar-map.md's GAP-WS-SELECTOR evidence at
EntryGoLauncherShared.BuildVernacularSearchFieldSpec so it cites code instead
of a comment that no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auditing the remaining PARITY notes against legacy source found the
MSAGroupBox/FwSandboxMsa claims to be wrong. Legacy InsertEntryDlg has no
inflection-class picker: the property is written only by SetMsa and read back by
SetEntryMsa, with no control in the form or its resx. Legacy MSAGroupBox has no
inflection-feature editor at all; its four widgets are affix-type, main POS,
slots and secondary POS. Both Avalonia panels are additions rather than parity
mappings, which MSAGroupBox.cs already stated correctly in one place while
contradicting itself in four others.

Three gaps that were buried in prose become TODOs naming what blocks them: the
missing "Add features to <POS>" link, whose two legacy outcomes are LT-5913 and
LT-7167 and whose jump plumbing already exists, plus the two round-trips
LcmMsaCreatorDialogLauncher does not perform. Comments that only restated the
code beneath them are gone, and so is the DetailComposer claim that the visited
set caps recursion at one pass per object and layout: the set is added to and
removed from around each descent, so it guards re-entry, not repetition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A comment claimed the nested Members rows do not compose. They do. With one
Morpheme Rule member present, every part of its EditAdHocGroup layout composes
(Key Morpheme, Cannot Occur, Other Morpheme(s), Active), bound to the member and
indented below the group's own rows. The claim went unchecked because the
existing test builds an empty group, where there is nothing to descend into and
the sequence node composes as a lone text row.

Assert the structure instead: each layout part composes, the rows bind to the
member by ObjectHvo, they nest deeper than the group's own rows, and they arrive
as Unsupported worklist rows rather than editors. Implementing the custom-slice
editors will fail that last assertion and get a deliberate update, which is the
accurate version of what the deleted comment was reaching for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A census found five senses sharing the word: which framework renders a tool,
a UI region of the app, product-vs-preview wiring, the drawn background of a
control, and the plain verb. The first three each get their own name; the
styling sense and the verb keep the word, as do the linguistic term "surface
form" and ordinary idioms.

The framework axis had three vocabularies for one binary - the UIMode strings
Legacy/New, EditSurface { WinForms, Avalonia }, and EditSurfaceKind
{ Legacy, Avalonia }. Both enums merge into UIFramework { Legacy, Avalonia },
resolved by UIFrameworkResolver over UIFrameworkRegistry and decided per host
by UIFrameworkSelectionService; only the persisted UIMode string still says
"New", normalized at the boundary as before. Members that held realized
controls are named for what they hold: EnsureDataTreeInitialized,
TearDownAvaloniaEntryForm, EditControlFactory. SurfaceRouting becomes
HostRouting; its persisted member names and the JSON "routing" key are
unchanged, and the unused XML attribute the importer accepted as "surface"
now matches that key. Prose throughout code, skills, docs, and openspec specs
names the concrete referent instead - view, detail view, entry form, dialog,
tool, host, framework, layout, or prefix.

CONTEXT.md retires the Surface glossary entry in place, recording why: its
own defining documents disagreed on whether dialogs were included. Stale
Region-era vocabulary found along the way is swept to Detail terms, snapshot
docs now show the Detail- prefixes the tests actually emit, and the one
user-facing resx string using the word (the dormant feature-catalog
description) now says "view".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace "gate on" and its inflections with plain verbs -- "branch on",
"assert on", "limit activation to", "must pass before". Gate stays as a noun
where it names a real thing: the confirmation gates in a playbook, the opt-in
gate, the detail view's UIMode gate.

Drop the "worklist row" compound in favor of "Unsupported row" and delete the
glossary entry that defined it, which also removes the only forward reference
in that list. Lower-case CHILD and CUSTOM in the dialog playbook, where the
sentence already carries the emphasis.
InitializeUIModeControls runs after InitializeComponent, so AutoScaleMode.Font
never scales the group it injects. Its children inherited the scaled font while
keeping unscaled coordinates, which put the Mode label on top of the combo at
any scaling above 100 percent.

Derive every offset from the inherited font height or a measured edge. The beta
warning now autosizes within the combo's width rather than a fixed 30px box,
and the group takes its height from that content instead of a literal 100.
Structural and DRY fixes from a review of the conversion support code:

- Rename the Avalonia factory entry points to Create*, matching the LCM factory
  convention already used across the repository.
- Extract ComposedDetailEditContext and FieldEditHandler from DetailComposer.
- Move the chooser's empty-item label into FwAvaloniaStrings.resx so the
  Avalonia code owns its own strings instead of reading a legacy resx.
- Wire EditorKindMap's KnownEditors set to the constants beside it.
- Log the writing-system keyboard failure and the composer lookup fallbacks
  rather than swallowing them.

Density and project configuration:

- Match the slice and dialog body font at 11pt, and route DialogTheme's six
  FontSize setters through its DialogFontSize resource, which had no consumers.
  The C# and XAML copies must stay in step: Avalonia's compiled XAML rejects
  x:Static as a resource declaration and drops the file without failing the
  build.
- Remove properties the new project files restated from Directory.Build.props.
  LangVersion stays, with a comment: the view models use init-only setters. The
  preview host keeps its own output folder because it must not ship.

Also revert the unused FieldWorks.cs usings, rename
DataTreeDisposalCharacterizationTests to DataTreeObservableBehaviorTests for
what it asserts, and bring the touched comments in line with the commenting
skill.
@jasonleenaylor jasonleenaylor changed the title LT-22625 Phase-1 base: Avalonia migration spine (UIMode defaults Legacy) LT-22625: Add the WinForms to Avalonia conversion foundation Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants