Skip to content

LT-22625 Phase-1 base: Avalonia migration spine (UIMode defaults Legacy)#964

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

LT-22625 Phase-1 base: Avalonia migration spine (UIMode defaults Legacy)#964
johnml1135 wants to merge 7 commits into
mainfrom
phase1-base

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Phase-1 base: Avalonia migration spine

https://jira.sil.org/browse/LT-22625

First of a 4-PR stack landing Phase 1 of the WinForms→Avalonia migration. It introduces the seam that lets FieldWorks host either the legacy WinForms editing surface or the new Avalonia one, gated entirely on a UIMode setting that defaults to Legacy. At that default every call site added here resolves to its original, unchanged WinForms path; the Avalonia surface activates only when a user explicitly opts in via Tools → Options.

What this PR contains

  • The Avalonia migration framework: region/composer (FullEntryRegionComposer), view-definition IR, owned controls, framework-neutral seam contracts, and the plugin registry.
  • The base detail-editor surfaces active under UIMode=New: lexiconEdit, lexiconEditPopup, notebookEdit, posEdit.
  • ~26 dialog-launcher call sites (Insert/Merge/Go To Entry, MSA and grammatical-info choosers, LexReference launchers, Interlinear's Add Allomorph/Add New Sense, and the three Options-dialog launch sites) each gated by the shared FwUtils.UIModeGates.ShouldUseAvaloniaUI(uiMode) predicate.
  • FwAvalonia/FwAvaloniaDialogs infrastructure and the landed openspec change specs, plus the migration skills.
  • A Stage-3 Avalonia browse mirror, shipped but currently inert. RecordBrowseView/BrowseViewer gained a full table overlay for list/browse tools — Configure Columns, Filter For/Restrict Date/Choose List flyouts, and a complete IBulkEditBarHost (bulk edit/copy/clear, Delete Rows, replace) — sitting on a large additive BrowseViewer API (IBrowseColumnSource + ~20 new methods). It activates per-tool through LexicalEditSurfaceResolver.SupportedAvaloniaBrowseToolNames, which is empty in this PR, so every browse/list tool falls back to the legacy BrowseViewer regardless of UIMode — asserted by InertFollowUpSurfacesFallBackToLegacy_BrowseTable. phase1-followup-table (see Stack) activates it one tool at a time.

How Legacy stays safe

  • Fail-closed gate. ShouldUseAvaloniaUI returns true only for an ordinal-ignore-case "New"; null, empty, whitespace, or any other value → Legacy. Every one of the ~26 gate sites uses this single predicate.
  • Loader isolation. The gate lives in FwUtils, which references no Avalonia type, and every gated branch that touches an Avalonia launcher is extracted into a [MethodImpl(NoInlining)] helper. Legacy execution therefore never resolves an Avalonia type unless a gate actually passes. Startup localization method-lookup has its own try/catch, so a missing/corrupt FwAvalonia.dll costs only the Avalonia dynamic strings, never the Chorus/Palaso localization managers.
  • Legacy else-branches unchanged. Every legacy branch of every gated site is byte-equivalent to main (including the LexReferenceMultiSlice restructure, traced case-by-case); the settings migration path is untouched; no static-init or reflection route reaches the new launchers ungated.
  • One shared (non-gated) site touched, but inert under Legacy. DTMenuHandler.OnDisplayDataTreeInsert's enablement check gained || m_dataEntryForm.IsExternalCommandAdapter. DataTree.IsExternalCommandAdapter defaults to false and is set true only by New-mode's hidden command-routing adapter (RecordEditView.Avalonia.cs, tasks 13.4/15.4), which exists purely to keep a legacy DataTree alive for right-click menu routing while the Avalonia surface is on screen. Legacy never sets it, so the added clause never fires there — the check evaluates identically to main — but it's a textual change to shared code outside the ~26 gate sites, so it's called out here rather than left implicit.
  • BrowseViewer's one existing-method touch is additive-only. InstallNewColumns gained one line, m_avaloniaColumnFinders = null;, invalidating a cache field that only the (currently unreachable — see the Stage-3 browse mirror above) new methods ever read. Everything else BrowseViewer gained is new members bolted onto the class; no other existing method body changed.
  • TsStringWrapper widened from internal to public, plus an additive FromXml factory/Xml property, so the cross-framework clipboard bridge (FwTsStringClipboard, task 3.13) can carry rich-text XML between the WinForms and Avalonia clipboard paths. No existing member's behavior changed.

Legacy-visible changes (things existing users see even at the default)

The gate keeps behavior on Legacy identical, but this PR does make a few changes visible to all users, called out here so review isn't surprised:

  • New Options control. A "Lexical Edit UI" mode chooser appears on Tools → Options → Interface, in both the WinForms (LexOptionsDlg) and Avalonia (OptionsDialogView) dialogs, each carrying a "Beta: the New mode is incomplete" note.
  • Two new settings. UIMode (default Legacy) and UIModeDisabledTools (default empty — a per-tool opt-out list read only after New mode is active).
  • Filter-bar accessibility identity. Legacy browse filter-bar combos now expose per-column Name/AccessibleName values instead of a uniform "FwComboBox" literal — an intentional screen-reader improvement, pinned by a WinForms UIA test.
  • Avalonia now ships with every install. FieldWorks/xWorks/LexTextControls reference FwAvalonia/FwAvaloniaDialogs, so the Avalonia binary stack deploys with every build and FwAvalonia.dll loads on core paths (behind Func indirection + the NoInlining gates). "Legacy installs don't need Avalonia" is no longer true.

New-mode parity deferrals

The New-mode (opt-in) path is narrower than the legacy dialogs it replaces in ~8 places, each marked // PARITY in-code (e.g. allomorph type-mismatch warning, link-allomorph/MSA first-item pickers, Insert Entry duplicate search not matching gloss, MGA catalog import unported). Legacy is unaffected. LcmChooserDialogLauncher ships with no production caller (private ctor, unreachable by design) — it is tested staging infrastructure for the future ReallySimpleListChooser migration (~54 sites), documented as STAGING in its doc comment.

Split out for separate assessment

A shared layout part (LexSense-Detail-Pictures) would make sense pictures appear in the legacy Lexicon Edit pane (they have been silently omitted there for over a decade). It is a plausible legacy bug-fix but an ungated, legacy-visible behavior change, so it is not in this PR — it is proposed separately so the team can decide on it independently.

Stack (each merges into the one above)

  1. thismain
  2. phase1-followup-interlinearphase1-base
  3. phase1-followup-rulephase1-followup-interlinear
  4. phase1-followup-tablephase1-followup-rule

Verification

Whole-solution build green; full CI-equivalent test run passes (7/7 CI checks). All ~26 gate sites verified fail-closed; every legacy else-branch verified equivalent to main; TonePars/XAmpleParser are byte-identical to main (this PR does not touch ParserCore/pcpatrflex). Draft until the stack is reviewed top-to-bottom.


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   11m 34s ⏱️ +58s
5 781 tests +1 479  5 701 ✅ +1 472  80 💤 +7  0 ❌ ±0 
5 790 runs  +1 479  5 710 ✅ +1 472  80 💤 +7  0 ❌ ±0 

Results for commit 454c7b1. ± Comparison against base commit 84a4850.

♻️ 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 61.68224% with 205 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.44%. Comparing base (84a4850) to head (454c7b1).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
Src/Common/FwAvalonia/AvaloniaRegionHostControl.cs 41.93% 58 Missing and 14 partials ⚠️
Src/Common/FwAvalonia/AvaloniaDialogHost.cs 53.00% 31 Missing and 16 partials ⚠️
...Controls/DetailControls/MorphTypeAtomicLauncher.cs 56.94% 19 Missing and 12 partials ⚠️
Src/Common/Controls/XMLViews/FilterBar.cs 40.47% 16 Missing and 9 partials ⚠️
Src/Common/FwAvalonia/FwAvaloniaPlatform.cs 61.53% 7 Missing and 8 partials ⚠️
Src/Common/FieldWorks/WelcomeToFieldWorksDlg.cs 0.00% 7 Missing and 1 partial ⚠️
Src/Common/FwAvalonia/FwAvaloniaApp.cs 50.00% 3 Missing ⚠️
Src/Common/FwAvalonia/FwAvaloniaRuntime.cs 80.00% 1 Missing and 2 partials ⚠️
Src/Common/FwAvalonia/FwAvaloniaStrings.cs 98.70% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #964      +/-   ##
==========================================
+ Coverage   32.99%   36.44%   +3.45%     
==========================================
  Files        1202     1363     +161     
  Lines      278168   297377   +19209     
  Branches    37151    40490    +3339     
==========================================
+ Hits        91781   108381   +16600     
- Misses     158537   159488     +951     
- Partials    27850    29508    +1658     
Files with missing lines Coverage Δ
Src/Common/Controls/DetailControls/DataTree.cs 43.76% <ø> (+6.21%) ⬆️
Src/Common/FieldWorks/FieldWorks.cs 0.73% <ø> (ø)
Src/Common/Framework/MainWindowDelegate.cs 2.40% <ø> (ø)
Src/Common/FwAvalonia/CompactDialogStyles.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.24% <ø> (ø)
Src/Common/FwAvalonia/FwRadioButtonStyle.cs 100.00% <ø> (ø)
Src/Common/FwAvalonia/FwSurfaceStyles.cs 94.28% <ø> (ø)
Src/Common/FwAvalonia/LexicalEditFeatureCatalog.cs 100.00% <ø> (ø)
... and 131 more

... and 139 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.

@johnml1135

johnml1135 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@C:/Users/johnm/AppData/Local/Temp/claude/C--Users-johnm-Documents-repos-FieldWorks-worktrees-010-advanced-entry-view/2b14e829-7fba-4b8e-9d29-78ed6e0d2c6c/scratchpad/pr964-audit-final.md

@github-actions

This comment has been minimized.

johnml1135 added a commit that referenced this pull request Jul 15, 2026
Findings from an adversarial audit of the FwUtils/LexText slice of
PR #964:

- Move the shared New-mode gate to FwUtils.UIModeGates and extract
  every gated Avalonia branch into a [MethodImpl(NoInlining)] helper.
  The predicate lived on AvaloniaOptionsDialogLauncher, whose generic
  base class forces the CLR to load FwAvaloniaDialogs/Avalonia when
  the declaring type loads, so merely checking the gate on the Legacy
  path loaded the Avalonia stack; inline branch bodies likewise forced
  the load at JIT of legacy-reachable methods. Now Legacy code paths
  never resolve Avalonia types unless a gate passes.
- Label the Options dialog's Lexical Edit UI "New" mode as beta in
  both the WinForms (LexOptionsDlg) and Avalonia (OptionsDialogView)
  dialogs, per dialog parity rules.
- Document LcmChooserDialogLauncher's intentionally-unwired staging
  status so it doesn't read as dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LRtgBkxYxQfBMGuZGKqEw
Introduces the seam that lets FieldWorks host either the legacy
WinForms editing surface or the new Avalonia one, gated entirely on
a UIMode setting that defaults to "Legacy". With that default, every
call site added by this branch resolves to its original, unchanged
WinForms code path; the Avalonia surface only activates when a user
explicitly opts in via the Options dialog.

Key pieces:
- LexicalEditSurface/LexicalEditSurfaceResolver and a fenced
  RegionEditContextHolder in RecordEditView, choosing between the
  legacy DataTree and the Avalonia LexicalEditHostControl.
- ~26 dialog-launcher call sites (Insert/Merge/Go To Entry, MSA and
  grammatical-info choosers, LexReference launchers) gated by the
  shared FwUtils.UIModeGates.ShouldUseAvaloniaUI(uiMode) predicate.
- FwAvalonia/FwAvaloniaDialogs infrastructure, wired in but inert
  until UIMode=New.

Legacy loader isolation. The gate predicate lives in FwUtils with no
Avalonia-referencing types, and every gated branch that touches an
Avalonia launcher is extracted into a [MethodImpl(NoInlining)] helper,
so Legacy execution never resolves an Avalonia type unless a gate
actually passes. Startup localization method-lookup gets its own
try/catch, so a missing/corrupt FwAvalonia.dll costs only the Avalonia
dynamic strings, never the Chorus/Palaso localization managers.

UI. A "Lexical Edit UI" mode chooser is added to Tools > Options on
both the WinForms (LexOptionsDlg) and Avalonia (OptionsDialogView)
sides, each carrying a "Beta: the New mode is incomplete" note.

Also fixes a RecordEditView.Dispose leak (m_dataEntryForm was only
detached, not disposed, if the legacy surface was never initialized),
closes a coverage gap on the UIMode PropTable-seeding path, and ships
LcmChooserDialogLauncher as tested-but-unwired staging infrastructure
(private constructor; migrates real chooser sites in a later phase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
return;

if (!m_panel.Controls.Contains(m_dataEntryForm))
m_panel.Controls.Add(m_dataEntryForm);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Disclosure (low severity, no behavior change): m_dataEntryForm now stays nested inside m_panel rather than being reparented as a sibling. This adds one level to the UI-Automation tree for Legacy users. Visually identical (Dock=Fill either way) — noted for the record.

}
}

private void EnsureLegacySurfaceInitialized()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Disclosure (low severity, no behavior change): legacy DataTree setup moved from Init() to lazy first-record-show via EnsureLegacySurfaceInitialized(). Same code, later timing — the surface is now built when the first record is shown rather than during Init().

assembly 9.0.0.2). Keep this until the host resolves DiagnosticSource directly
through the main package graph. -->
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Disclosure (low severity): new process-wide binding redirect for System.Diagnostics.DiagnosticSource, added because FwAvalonia pulls it in. Versions are CPM-pinned solution-wide so there is no conflict today; noting it because it affects assembly resolution for everyone, Legacy included.

@johnml1135
johnml1135 marked this pull request as ready for review July 15, 2026 18:23
@johnml1135 johnml1135 changed the title Phase-1 base: Avalonia migration spine (UIMode defaults Legacy) LT-22625 Phase-1 base: Avalonia migration spine (UIMode defaults Legacy) Jul 15, 2026
jasonleenaylor and others added 6 commits July 23, 2026 20:15
Review follow-up on the Phase-1 base PR (terminology and documentation
only; no behavior changes).

Terminology: the coined word "lane" is replaced everywhere with plain
vocabulary chosen per sense — "evidence type" (semantic/visual/
workflow), "environment" (headless vs desktop), "path"/"case" (composer
handling paths), "route" (burn-down census), "format" (clipboard
payload), and "companion strip" (the mechanism's own name). "Chrome" in
prose becomes plain words (decorations, frame, shell); identifiers keep
their names. Renames: LexemeEditorBurnDown.LaneAbsorbedClassNames ->
ComposerAbsorbedClassNames (with its tests) and
MessagesCompanionLaneTests -> MessagesCompanionStripTests. The
localization-policy sections and string accessors still carry the old
wording; the follow-up localization commit rewrites them together with
the code they describe.

Skills consistency (review-audit findings):
- fieldworks-winapp: stale hidden-desktop guidance removed everywhere;
  the visible-desktop reality (HEADLESS=false) is stated once in
  headless-rendering.md with a three-senses-of-headless note; the
  migration-capture campaign moved to
  navigation/migration-doc-capture.md.
- Parity screenshot naming unified on parity-evidence.md section 6
  (visual.legacy.png / visual.avalonia.png / visual.diff.png; manual
  bundleId form documented); the 01-winforms/02-avalonia scheme is
  removed.
- fieldworks-avalonia-ui: snapshot path corrected to the flat folder
  the code writes; MinHeight 22 -> 24; Margin literals replaced by
  tokens in the template; duplicated rules collapsed into their
  canonical homes.
- The stale claim that XAML projects are excluded from FieldWorks.proj
  is corrected (they build through the normal Src glob); build.ps1's
  duplicate Avalonia comment is deleted; avalonia.instructions.md now
  says new projects are picked up via the solution.
- dialog-update: Options-dialog symbols marked as worked examples; the
  skill is now referenced from the hub and fieldworks-avalonia-ui.
- Dead seam-catalog glob fixed; architecture-patterns TOC includes
  section 13; density-vs-styling cross-links added.

CONTEXT.md gains glossary entries for the migration's core vocabulary
(region, surface, seam, fenced edit session, settle, parity). AGENTS.md
points at .mcp.json's documentation (strict JSON, no comments).
Directory.Packages.props' Avalonia exception comment now states the
dependency precisely (IRegionEditorPlugin.BuildControl returns
Avalonia.Controls.Control; compile-only reference).

Known residuals: Path3BundleTests emits a JSON key named "lanes"
(bundle schema, not prose) and openspec historical records keep their
original wording by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review decision (PR #964): FieldWorks-owned UI text localizes via .resx
+ ResourceManager + satellite assemblies, matching the official Avalonia
localization guidance and the repo convention; L10NSharp/XLIFF stays
reserved for UI supplied by Palaso, FlexBridge, or Chorus. No additional
L10NSharp usage is permitted, including borrowing Palaso/Chorus catalog
ids from FieldWorks-owned code. This reverses the earlier decision that
routed FwAvalonia strings through LocalizationManager dynamic strings
under the "Palaso" app id (recorded in architecture-patterns.md sec 11).

Code:
- FwAvaloniaStrings.resx / FwAvaloniaDialogsStrings.resx are populated
  from the accessors' inline English (141 + 153 entries) and become the
  single English source of truth; the previously emptied files carried
  the seed text only in C#.
- FwAvaloniaStrings / FwAvaloniaDialogsStrings keep every property but
  resolve via ResourceManager; a missing entry falls back to the string
  id so drift is visible. Borrowed ids renamed to owned keys
  (Common.Cancel/OK/Help/Error -> FwAvalonia.*/FwAvaloniaDialogs.*).
- FwAvaloniaLocalization and FwAvaloniaLocalizationBootstrap are
  deleted; TestAppBuilder/PreviewHost/xWorksTests bootstrap callers no
  longer need them (resx satellites load with CurrentUICulture; the
  xWorksTests bootstrap keeps only its Chorus manager for the legacy
  NotesBar).
- FieldWorks.InitializeLocalizationManager no longer registers the
  FwAvalonia namespaces with the Palaso manager nor reflects Avalonia
  methods — FieldWorks strings stop appearing in the translator-facing
  Palaso XLIFF catalog, and FwAvalonia.dll no longer loads at startup
  for Legacy users.
- L10NSharp package references dropped from FwAvalonia and
  FwAvaloniaTests; the central L10NSharp.Windows.Forms pin stays (it
  pins the transitive version) with its comment corrected.
- AvaloniaLocalizationTests rewritten: every accessor property must
  resolve from the neutral resx, and resolution falls back to neutral
  English under a culture with no satellite. UI-language changes keep
  the existing restart-required behavior (LexOptionsDlg), so the
  known resx/x:Static no-live-update caveat changes nothing.

Docs updated with the code: CONTEXT.md localization strategy entries;
fieldworks-localization-review rewritten around the three strategies
(StringTable for XML-configuration labels, .resx for FieldWorks-owned
UI text, L10NSharp for Palaso/FlexBridge/Chorus UI);
architecture-patterns.md sec 11 records the dated reversal;
migration-checklist Phase 8, the migration hub step 8,
fieldworks-avalonia-ui, and dialog-conversion.md sec 5 now teach the
resx strategy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review decision (PR #964): the base PR ships only reachable surfaces.
The Avalonia browse table was inert by design (no tool registered), so
every piece that existed solely to serve it is removed rather than
shipped dormant; a future PR reintroduces the browse surface as a whole.

Removed (no reachable caller under any UIMode):
- The four browse dialogs and their view models, inputs, and tests:
  Configure Columns, Filter For, Date Range Filter, Find/Replace.
- The Avalonia browse view layer: LexicalBrowseView, BulkEditBarView,
  LexicalBrowseHostControl, BrowseCellRenderer, BrowseCsvExporter, the
  IBrowse* capability interfaces, BrowseColumnModel/ConfigStore, and
  their test suites (about 200 tests).
- The xWorks browse adapters: ClerkBrowseRowSource,
  ClerkBrowseEditContext, BrowseColumnSpec, BrowseColumnEditSpec, and
  their tests.
- RecordBrowseView's inert overlay and IBulkEditBarHost implementation
  and BrowseViewer's IBrowseColumnSource implementation - both files are
  restored to their pre-PR state (the FilterBar accessible-name change
  is unrelated and stays).
- LexicalEditSurfaceResolver.ResolveBrowse and the empty/staged browse
  tool arrays, plus their resolver tests.
- LcmChooserDialogLauncher (self-documented STAGING, private ctor, no
  caller) and its tests.
- The unregistered Chorus notes-bar stack (ChorusNotesPlugin,
  ChorusNotesBarControl/Store, contract tests). The Messages slice
  keeps its ExplicitlyDeferred census route with an updated citation;
  the composer's read-only placeholder behavior is unchanged and stays
  tested (MessagesCompanionStripTests).

Dead-code sweep driven by compiler and reference greps: 108 string
accessor properties and their resx entries (browse/bulk/filter,
unshipped interlinear labels, Chorus notes strings), the browse-only
density tokens, the browse fixtures in VisualSnapshotTests and the
BrowseTableDriver in the headless workflow harness, and stale
browse/ChorusNotes claims in the skills and code comments.

Whole-solution build (including test projects) green; FwAvaloniaTests
635/635; burn-down census, companion-strip, and edge-case fixtures
21/21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review resolutions (PR #964), each independently verified:

UIMode startup ordering (the New-mode restart bug): LoadUI runs inside
the FwXWindow constructor and RecordEditView resolves its surface while
the content views are being built, but the UIMode property was seeded
into the PropertyTable only after NewMainAppWnd returned (and without
broadcast) - so a persisted UIMode=New came up on the Legacy surface
until the setting was toggled again. FwXWindow.InitMediatorValues now
seeds UIMode/UIModeDisabledTools from the app settings before LoadUI,
via a testable helper; the too-late seeding in
FieldWorks.CreateAndInitNewMainWindow is removed. Both Options dialogs
now compare the chosen mode against the PropertyTable's live value
(falling back to settings), so OK heals a table/settings disagreement
instead of skipping it as "no change". The three separate
normalize-UI-mode implementations collapse onto the new
LexicalEditSurfaceResolver.NormalizeUIMode (fail-closed; only "New"
selects New). New tests: FwXWindowUIModeSeedingTests and resolver
normalization cases.

Edit-session task identity: LcmRegionEditSession detected "the clerk
force-ended my task" (LT-16673) by undo-stack depth alone; if another
component opened a new task before the stale session settled, a stale
Commit/Cancel would end or roll back the unrelated task. The guard now
also pins the undoable-sequence count captured at open, so the stale
closers no-op unless the open task is provably the session's own. Two
new lifecycle tests pin the interloper scenarios.

Finalizer-safe context diagnostics: the ambient wrapper swallowed every
failed Post, including async continuations dropped during teardown (a
silent hang with skipped finally blocks). Swallowed posts are now
classified: MicroCom finalizer Releases (the crash class the wrapper
exists for, identified by the callback's MicroCom.* declaring type)
trace at verbose level; anything else logs a warning through the
FwAvaloniaHostInterop trace switch and routes through a loud-in-Debug
drop handler (Debug.Fail by default; replaceable in tests). A pin test
fails if an Avalonia bump relocates MicroCom.Runtime.MicroComProxyBase.

Legacy persistence key restored: DataTreePersistContext now reproduces
the original inline logic byte-for-byte - a config without a
persistContext attribute yields "<vector>..DataTree" again, so users'
persisted DataTree state is not silently reset.

AvaloniaDialogHostTests: documented the sanctioned exception to the
no-WinForms-Forms-in-tests rule (bare never-shown Form property bags
whose subject IS Form property manipulation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review resolutions (PR #964) for the two New-mode parity deferrals that
could silently pick the wrong object.

Dependent auxiliary selection (the missing Go-family pattern): legacy
BaseGoDlg children add per-entry controls under the matching list
(LinkMSADlg's grammatical-info combo, LinkAllomorphDlg's allomorph
combo); the Avalonia EntryGo dialog had no equivalent and both
launchers silently used the entry's FIRST MSA/form. EntryGoDialogInput
gains an opt-in auxiliary spec (label + a resolver returning
key/display options for the selected result). When supplied, the
dialog becomes two-stage: picking a result populates the auxiliary
picker (a lone option auto-selects), Enter/double-click selects rather
than commits, and OK is enabled only when both an entry and an option
are chosen; the result carries the chosen key. Consumers without the
spec keep the single-stage commit-on-select behavior unchanged
(including the absent OK button). LcmLinkMsaDialogLauncher offers ALL
of the chosen entry's MSAs (InterlinearName, legacy order) and applies
the chosen one; LcmLinkAllomorphDialogLauncher offers the non-abstract
forms (lexeme form first) and applies the chosen one. The pattern is
documented as the Go-family exemplar in dialog-conversion.md 2c.

Allomorph type-mismatch prompt: the legacy AddAllomorphDlg flow warns
(CreateAllomorphTypeMismatchDlg) when the typed form's inferred morph
type conflicts with the entry's existing allomorphs; the Avalonia
launcher skipped it. The legacy condition is ported verbatim and the
decision runs through an injectable confirmation seam (defaulting to
an owner-parented FwMessageBox Yes/No with the legacy wording): No
adds nothing, Yes creates the allomorph with the legacy MSA-ensuring
switch in one undoable step.

New strings follow the .resx strategy (9 accessor/resx pairs, seeded
from the legacy wording). Tests: 8 new EntryGo headless tests plus
new launcher fixtures (5 LinkMsa, 6 LinkAllomorph, 7 AddAllomorph);
full FwAvaloniaDialogsTests suite green (279/279).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment-only cleanup of the preceding commits (one asserted string pair
changed in step): review/stage markers dropped ("LT-22625 review",
"(review, July 2026)", "Task 21", wave/D-label narration, "FUTURE PRs"
roadmaps); "in this PR" phrasing replaced with state-based wording
("not yet migrated"), including the ExplicitlyDeferred reason string
that LexemeEditorBurnDownTests asserts and two assertion messages;
change-narration removed ("preserving the prior behavior",
"byte-identical to the original inline logic" - the persisted-key
constraint now stands on its own; "parity with the pre-auxiliary
surface" - duplicate comment dropped); test-name narration in
FinalizerSafeSynchronizationContext corrected to a fixture-agnostic
pin-test note. Legacy-parity references (combo order, fallback
selection, wording sources) are kept: they document behavioral
contracts, not history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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