Skip to content

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

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

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

Conversation

@johnml1135

@johnml1135 johnml1135 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

The Avalonia foundation for FieldWorks

LT-22625

FieldWorks can now convert a WinForms surface to Avalonia by a specified
procedure instead of by improvisation. Four detail surfaces and a family of
dialogs are here as proof that the procedure works — but the procedure, the
architecture it targets, and the rules that keep it honest are the deliverable.

Open Tools → Options → Interface, set New UI (preview) to New, and
Lexicon Edit, the Lexicon Edit popup, Notebook, and Grammar's Parts of Speech
render in Avalonia, live, without a restart. Leave it at the default and
FieldWorks does exactly what main does.

What "foundation" means here

A conversion is now a procedure with real stops. Understanding the dialog
or slice → agreeing on how it works → deciding what to prove → planning the
replacement → building it → proving it works. The middle three 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 explores the live
app; scaffolds never merge until the last stage passes.

Unfinished work is visible, not absent. Anything the Avalonia path cannot
yet render becomes a labeled Unsupported row with a stable automation id —
never a silent omission. Grep for those rows and you have the remaining
worklist. SlicePluginRegistry.Register throws on a double claim for the same
reason: one owner per legacy class, no silent overwrite.

Only reachable code ships. The Avalonia browse/bulk-edit surface was
removed rather than merged inert, and six scalar field kinds were deleted
rather than half-built. What is here is reachable and honest; what is not here
says so on screen.

The extension points are small. A new slice is an ISlicePlugin keyed by
the legacy layout's class= string, so migrating one edits no layout XML. A new surface enters the feature catalog only after its current product route, plugin, lifecycle, accessibility, localization, fallback, and workflow evidence all agree.

Scope, stack, verification

485 files, +82,962 / −465. UIMode defaults to Legacy, and both gate
predicates fail closed, so the default path is main's. PRs #965-#967 are
retired experiments, not a merge stack; their branches remain only for
archaeology. #968 is DRAFT, never merge.

Whole-solution build and full test verification were green before the final
documentation-only lesson update. Current CI is the source of truth for this
head.


Reading this a year from now — start here

The sections below are the decision record for the start of the FieldWorks
WinForms→Avalonia transition. They exist because the branch's working
documents — research, audits, rejected designs — were deliberately deleted
from the tree rather than merged. The durable contracts were promoted into
openspec/specs/; the reasoning that explains them is here.

The indexed Avalonia migration lesson cards
preserve the reusable observations, rejected assumptions, evidence needs, and
decision boundaries from the retired follow-ups. They constrain future research;
they do not authorize restoring old code.

The layer cake — how an XML view definition becomes an Avalonia control
Configuration/**/*.xml  (Parts + Layout)
  → ViewDefinitionCompiler        Src/Common/FwAvalonia/ViewDefinition/
       immutable snapshot, fingerprinted, cached by
       (className, layoutName, layoutType, fingerprint)
  → ViewDefinitionModel           the typed IR (a ViewNode tree)
  → DetailComposer.Compose()      Src/xWorks/Avalonia/Composer/
       LCModel-backed; walks object/sequence nodes, MaxDepth 12,
       applies per-project ViewDefinitionOverride
  → ComposedDetail { DetailModel, IDetailEditContext }
  → DataTree : UserControl        Src/Common/FwAvalonia/Detail/
       renders one row per DetailField
  → SliceFactory.Build()          switches on DetailFieldKind, or
       resolves ISlicePlugin, or builds the Unsupported row
  → DetailHostControl             hosts the Avalonia tree in-process
  → RecordEditView.Avalonia.cs    the product entry point

DetailFieldKind is deliberately small: Text, Chooser, ReferenceVector,
Header, Custom, Unsupported.

Two properties worth preserving. Composition is separate from rendering
the new DataTree only renders a pre-built DetailModel snapshot; it does not
walk XML and does not hold live state across data changes. And the view layer
is LCModel-free
: DetailField/DetailModel carry primitives, strings and
delegates, never an ICmObject, which is what lets the whole detail path be
tested headlessly without a language project.

Compilation is memoized process-wide, deliberately not with Lazy<T>, so an IO
failure is not cached as permanent. CompileAsync exists and is tested, but the
product path still composes synchronously on the UI thread — first-compile
latency is absorbed there today, mitigated only by the cache on later shows.

Decisions, and why

The UI gate is split across two assemblies on purpose.
FwUtils.UIModeGates.ShouldUseAvaloniaUI (26 call sites, 20 files) references
no Avalonia type, so a legacy-mode process evaluates it without the CLR loading
Avalonia at all. Per-tool resolution (EditSurfaceResolver,
EditSurfaceRegistry, EditSurfaceSelectionService) lives in FwAvalonia and
is only reachable from code that has already loaded it. Every legacy call site
pairs the check with a [MethodImpl(NoInlining)] helper holding the Avalonia
branch, so the JIT never resolves an Avalonia type on a Legacy path. Write the
obvious inline if (ShouldUseAvaloniaUI(...)) new AvaloniaThing() and you
defeat the whole arrangement — every non-adopting user starts paying to load
Avalonia.

net48 pins Avalonia to the 11.3.x line. 11.3.x is the last line shipping
netstandard2.0 assemblies, which is what lets Avalonia load in-process beside
WinForms on net48. A bump past it breaks hosting, and not necessarily at
compile time. Stay on 11.x until WinForms is gone.

No Avalonia modal windows during coexistence. Avalonia modals are not
supported while WinForms owns the message loop, so a converted dialog is a
WinForms-owned modal hosting Avalonia content (AvaloniaDialogHost), and
ShowModal throws off the WinForms UI thread. A greenfield-Avalonia
Window.ShowDialog() port would corrupt the message loop.

The process is deliberately DPI-unaware. A WPF dialog once triggered DPI
awareness and blurred the entire application. DPI awareness arrives only after
all WinForms is gone; expect shimming until then.

FinalizerSafeSynchronizationContext prevents a process kill, not a glitch.
Avalonia's MicroCom proxies capture the ambient SynchronizationContext and
post native Release calls from finalizer threads. If that lands after the
WinForms marshaling window is torn down — project switch, shutdown, idle GC —
the post throws on the finalizer thread and takes the process with it. It is
installed once, before SetupWithoutStarting(). Reordering the hosting
bootstrap can silently reintroduce a GC-timing-dependent crash.

Routing keys off the legacy class= attribute. ISlicePlugin.LegacyClassName
reuses the exact legacy string, so migrating a slice edits zero layout XML. The
alternative — minting new ids in the IR and updating every layout file — is a
far larger and riskier diff per slice.

DataTree and Slice are reused as names for restructured types. These are
deliberate cross-namespace twins, disambiguated with using aliases at call
sites; the rule is never to rename a type to dodge the collision. They cover
about half of what the legacy types did — state came out of the rows,
composition came out of the tree, writes came out of the keystroke path.

Seams are narrow by design. A seam is a substitution point in Feathers'
sense: product implementation and test double swap without editing a call site.
Seams/ holds six of them and nothing else — an eviction pass moved a
synchronization-context guard and a morph-type helper out to live beside their
consumers. Direct Avalonia APIs remain explicitly allowed at the UI edge. This
is not "wrap everything."

TsString stays the shared text model. Clipboard and drag/drop between
WinForms and Avalonia use the existing "TsString" clipboard format carrying
the same XML native Views copy/paste already used. The migration replaces
rendering and editing, not the data model.

FieldWorks-owned Avalonia strings are .resx, not XLIFF. This reversed an
earlier decision that routed them through LocalizationManager dynamic strings
under the Palaso app id. L10NSharp stays reserved for Palaso/FlexBridge/Chorus
UI.

Paths not taken — the section that stops a dead end being re-proposed

Plan A: extracting a model layer out of DataTree. The roadmap originally
specified pulling DataTreeModel/SliceSpec/IDataTreeView out of DataTree
so both frameworks would share model types. Execution went the other way —
straight to the typed IR, bypassing DataTree internals entirely. What tipped
it: wiring new ports into DataTree internals only serves code that gets
deleted at cutover, so it is throwaway work by construction. DataTree is
frozen legacy, slated for wholesale deletion at end of coexistence.
openspec/specs/avalonia-migration-roadmap/spec.md carries a superseded-requirement
banner pointing here for this rationale.

Every off-the-shelf dense tree/table control. TreeDataGrid — its FOSS line
was archived 2025-10-13 and editing moved behind the commercial Avalonia
Accelerate license, incompatible with FieldWorks' redistribution model; this is
a permanent exclusion, not a revisit-later. ItemsRepeater — on an upstream
deprecation path, no selection or automation-peer support.
Avalonia.Controls.DataGrid — unmaintained Silverlight port with known
virtualization and editing defects. Stock TreeView — does not virtualize;
retained only for small bounded popups.

Canonical view-definition storage. The recommendation was layered JSON:
shipped definitions plus sparse per-project patches keyed by StableId.
Rejected: YAML (new net48 dependency, whitespace hazards); new-schema XML
(indistinguishable at a glance from legacy .fwlayout, which invites
copy-pasting the very vocabulary the migration exists to retire); DB-backed
definitions (opaque to diff, breaks the off-thread immutable-snapshot compile,
removes support staff's inspect-and-delete recovery); C# builders as the sole
store (cannot express customer overrides). This was a recommendation, not a
closed decision.

A launcher/companion-strip middle tier for slice conversion. Rejected so
resolution stays strictly plugin-registry-or-Unsupported and the unmigrated
backlog stays visible instead of hiding behind a half-migrated tier.

A shared base class for the filterable dropdowns. The three controls diverge
in content shape, popup hosting and selection model, so a common base would
fight the divergent parts. A static helper instead.

Lazy DataTree construction, considered to shrink the native-Views blast
radius in RecordEditView: 67 call sites plus a subclass injecting its own
DataTree, for zero benefit — the empty constructor already realizes no native
Views.

Reversals — built, then removed

The Avalonia browse / bulk-edit surface. A complete owned browse stack —
sortable headers, inline cell edit, checkbox bulk-edit, filtering, multi-sort,
drag-reorder columns, RDE new-row entry, diacritic-aware find/replace, CSV
export, per-cell UIA peers, ~200 tests — was built, reviewed, and then pulled
rather than merged inert. Sort, filter, bulk-edit and selection were never
wired through to the real RecordClerk, so no UI mode could reach the
interactive features; it only ever rendered alongside the hidden-but-functional
legacy BrowseViewer. Browse and list tools use the legacy BrowseViewer, full stop. Retired #967
assumed the dormant implementation still existed and therefore cannot be
rebased or cherry-picked. The browse lesson card
records the reusable contracts and the evidence required before any new design.

Three disconnected tracks, converged. Before this branch opened there were
three: a clean seam layer built but wired to nothing, a typed view-definition
IR consumed by nobody, and the only working end-to-end renderer — a spike stack
on a hand-written three-field lossy DTO — ignoring both. The spike was retired
and the preview host moved onto the same detail-model boundary as product code,
so there is no duplicate UI stack to keep in sync. Surviving runtime types were
renamed by role rather than spike history. This convergence is the reason the
branch exists in the shape it does, and it is visible in no single commit.

The Chorus notes bar. Built, including a reverse-engineered LibChorus
file-format compatibility contract, then removed unregistered along with the
browse surface. The Messages slice keeps its census route.

The dialog-launcher-as-slice-editor seam. Removed in favor of the simpler
plugin-registry-or-Unsupported model.

Six scalar field kinds and the picture/media path. Boolean, Date,
EnumCombo, Integer, Image, Command, plus the picture editor and its
media seam, were deleted rather than half-built. Each now renders as an
Unsupported row — a scope narrowing, not a defect.

Free Popup dropdowns → Flyout. Free popups render in their own top-level
and misplace under fractional display scaling. A regression test now asserts no
Popup exists in the logical tree while a dropdown is open, so the pattern
cannot silently return.

The migration's own vocabulary. "Region" — a word invented during the
migration — was replaced throughout with "Detail", matching the legacy
DataTree/Slice idiom, and the invented "Lexical" prefix was dropped
wherever the concept is generic. The user-facing toggle stopped being labeled
"Lexical Edit UI", which named the internal project rather than what the user
was choosing. Migration-invented words are a tax on everyone who arrives later.

Surprising findings — where investigation contradicted the assumption
  • Legacy DataTree silently drops the same part references the new importer
    drops
    — roughly 63 of them. The importer's "unresolved" list matches a
    pre-existing legacy gap exactly; it is not a regression.
  • Shipped XML layouts import far less cleanly than expected — measured at
    ~70.5% of elements and ~51.1% of attribute occurrences. That number drove the
    explicit per-node diagnostic-drop taxonomy instead of silent loss.
  • The IR models neither ghost items nor if/ifnot conditionals — 230
    occurrences across shipped files. Ghost, conditional and chooser-link metadata
    was reserved in the schema before the format froze, specifically to avoid an
    early breaking formatVersion bump when those land.
  • A GenDate editing bug shipped green because every test used year-only
    dates. Composing from GenDate.ToLongString() let the parser seed the year
    from the first digit run, silently corrupting day- or month-precise dates.
  • Span/link/ORC edit affordances were a production no-op: focusable trigger
    buttons stole focus from the text box before reading its selection, collapsing
    it to a caret. Invisible to tests that set selection directly instead of
    clicking — a warning about that entire class of test.
  • Lexicon Browse cell editing is effectively dormant even where wired — no
    shipping browse column is both editable="true" and a simple field. The value
    is in the Edit view and in bulk-edit.
  • Typing-latency evidence measured only the managed model layer (~0.01
    ms/key). That is not an end-to-end claim; real layout, shaping, render and IME
    latency remain unmeasured. Do not cite it as proof the UI is fast.
What this does NOT authorize — the limits of the XML retirement

XML retirement is per-surface, never global. A surface may disable runtime XML
only when every applicable blocker family clears for the layouts it loads:
custom fields, ghosts, conditionals, menus and hotlinks, styling, table/browse
XML, chooser metadata, parameter substitution, cross-class part resolution,
dynamic editors, native-viewing coupling.

Repo-wide XML deletion additionally waits on browse views in every area,
Interlinear, Notebook/Grammar/Lists detail, and publishing layouts — none of
which this branch authorizes. Any global "XML is gone" claim would be false for
years.

The same applies to the native boundary: a region is not migrated while it
still calls RootSite/IVwEnv/native Views for display, layout, hit-testing,
selection or editor realization. Linguistics services — XAmple, spelling, ICU,
encoding converters, parsers — may remain behind explicit service contracts,
because they do not own the UI. Without that distinction "no native code" would
have been unachievable and therefore meaningless.

Deferred, and what would unblock it
  • New mode is narrower than the dialogs it replaces — 20 // PARITY markers
    across 18 files (allomorph type-mismatch warning, link-allomorph and MSA
    first-item pickers, Insert Entry duplicate search not matching gloss, MGA
    catalog import). Legacy is unaffected by all of them.
  • Seven follow-up tool names remain reserved, but their retired surface code
    does not ship here.
    Analyses and the six rule-formula routes stay Legacy.
    PRs Phase-1 follow-up: Avalonia interlinear editor (Words Analyses) #965 and Phase-1 follow-up: Avalonia rule-formula editors (Grammar) #966 are archaeological sources only; the
    interlinear
    and rule-formula
    cards require fresh characterization and current-tree design before a route
    can be proposed for activation.
  • StText multi-paragraph editing — designed, not wired. ORC-embedded runs
    stay permanently read-only by design. A content census found such content only
    in sttext fields; the trigger to revisit is real project data showing it in
    string/multistring fields.
  • Feature-structure editor — control and tests ship LCModel-free; wiring to
    the MSA/MGA dialogs and the create-new-feature catalogs is deferred.
  • Command/focus bridge to xCore — deferred to the shell phase, to avoid
    over-preserving legacy quirks while the surface is still moving. The hidden
    DataTree + DTMenuHandler that routes legacy right-click commands is a
    contract-gated command-routing exception, not a viewing exception, and is
    named for removal then.
  • WebView2 for the ~10 Gecko/XULRunner surfaces (dictionary preview,
    configure-dictionary, MGA help, print/PDF). Blocked on making XULRunner
    startup lazy first — today a missing Firefox folder is an unconditional hard
    crash at app start — and on validating per-surface Graphite-loss warnings. The
    expensive part is the DOM/JS interop ports, not swapping the browser.
  • Graphite is supported through the transition with graded per-writing-system
    warnings rather than retired first. The re-research that forced that reversal:
    HarfBuzz's Graphite2 shaping is opt-in, not default-enabled, so "Avalonia uses
    Skia/HarfBuzz" does not by itself prove shaping parity.
  • Mouse-drag selection over-grow in the multi-WS field is open and
    documented, not fixed: the headless backend does no text hit-testing, so the
    defect cannot be reproduced or self-validated there. It needs a real rendered
    window.
How it stays off by default — the gate, in detail

Two predicates, both ordinal-ignore-case equality against a single "New"
literal, both failing closed on null, empty, whitespace, a corrupt settings
file, or any other value. Neither has a negation or fallback that could return
New accidentally.

Gate 2 is never more permissive than gate 1: before the preference is consulted,
an unregistered tool forces WinForms, and a per-tool opt-out
(UIModeDisabledTools) forces WinForms. A blank tool name is the one case where
gate 2 adds no narrowing — it defers to the preference, which still defaults to
Legacy.

Every legacy else-branch is equivalent to main, including the
LexReferenceMultiSlice restructure, traced case by case. The settings-migration
path is untouched.

Shared code changed outside the gates, in full.
DTMenuHandler.OnDisplayDataTreeInsert gained
|| m_dataEntryForm.IsExternalCommandAdapter; DataTree.IsExternalCommandAdapter
defaults to false and is set true only by New mode's hidden command-routing
adapter, so the clause never fires under Legacy and the check evaluates
identically to main. TsStringWrapper went internalpublic with an
additive FromXml factory and Xml property for the clipboard bridge; no
existing member changed behavior.

Visible at the default anyway. The new Options chooser in both the WinForms
and Avalonia dialogs; two new settings (UIMode, UIModeDisabledTools);
per-column Name/AccessibleName on legacy browse filter-bar combos instead of
a uniform "FwComboBox" literal, an intentional screen-reader improvement
pinned by a WinForms UIA test; and FwAvalonia/FwAvaloniaDialogs now
referenced by FieldWorks, xWorks and LexTextControls, so the Avalonia
binaries deploy with every build.

Known defect, not fixed here: three <see cref> references to the deleted
LcmChooserDialogLauncher survive and will warn as CS1574.

How this is tested

Everything is net48 — there is no separate build lane for the Avalonia work,
because the legacy-vs-Avalonia choice is a runtime product switch, not a build
contract. ./build.ps1 and ./test.ps1 remain the only entry points.

Three verification surfaces, in the order a migrator uses them:

  1. Headless testsAvalonia.Headless.NUnit's [AvaloniaTest], with a
    TestAppBuilder configured .UseSkia() and UseHeadlessDrawing = false, so
    tests capture real rendered frames rather than layout alone.
    FwAvaloniaRuntime.AppBuilderOverride is the seam that lets integration
    tests driving RecordEditView run without popping real Win32 windows.
  2. The preview host (Src/Common/FwAvaloniaPreviewHost/) — runs a surface
    standalone, no language project needed. It isolates whether a bug is in our
    code or in the WinForms-hosting plumbing.
  3. Render snapshotsDialogSnapshot.Capture always writes a real Skia PNG
    to a gitignored folder even on a passing test, for eyeballing, paired with
    DialogLayoutAssert as the deterministic tripwire for overlap, zero-area
    text and crowding.

ViewDefinitionModel.ToSnapshot() is the semantic lane: a deterministic text
dump of the typed IR. EngineIsolationAuditTests pins isolation from native
Graphite/Views. A dedicated pin test guards FwAvaloniaPlatform.IsHeadless,
which resolves Avalonia internals by reflection — an Avalonia bump could
relocate them and make headless detection silently return false forever with no
compile error.


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 53s ⏱️ - 2m 7s
5 776 tests +1 463  5 695 ✅ +1 455  81 💤 +8  0 ❌ ±0 
5 785 runs  +1 463  5 704 ✅ +1 455  81 💤 +8  0 ❌ ±0 

Results for commit 93feb6e. ± 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.67623% with 903 lines in your changes missing coverage. Please review.
✅ Project coverage is 36.42%. Comparing base (84a4850) to head (93feb6e).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
Src/Common/FwAvalonia/Detail/DetailModel.cs 78.09% 77 Missing and 105 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 48.80% 33 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.42%   +3.43%     
==========================================
  Files        1202     1361     +159     
  Lines      278168   296281   +18113     
  Branches    37151    40311    +3160     
==========================================
+ Hits        91781   107930   +16149     
- Misses     158537   159005     +468     
- Partials    27850    29346    +1496     
Files with missing lines Coverage Δ
Src/Common/Controls/DetailControls/DataTree.cs 43.73% <ø> (+6.18%) ⬆️
Src/Common/FieldWorks/FieldWorks.cs 0.73% <ø> (ø)
Src/Common/Framework/MainWindowDelegate.cs 2.40% <ø> (ø)
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% <ø> (ø)
... and 117 more

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

jasonleenaylor and others added 19 commits July 29, 2026 12:23
Move the 17 LexTextControls files that bridge to the Avalonia dialog kit out of
the project root into a flat Avalonia/ folder, so the Avalonia
launchers/adapters read as one area instead of being interleaved with the legacy
WinForms controls. Every moved file references FwAvaloniaDialogs; EntryGoDlg.cs
and EntryGoSearchEngine.cs (no Avalonia-kit reference) stay in the root.

  Avalonia/ AvaloniaOptionsDialogLauncher, EntryGoLauncherShared,
  FwFeatureStructureAdapter, LcmInflectionFeatureCreateWiring,
  LcmAddAllomorphDialogLauncher, LcmAddNewSenseDialogLauncher,
  LcmCreateFeatureLauncher, LcmCreatePartOfSpeechLauncher,
  LcmGoToEntryDialogLauncher, LcmInflectionFeatureChooserLauncher,
  LcmInsertEntryDialogLauncher, LcmLinkAllomorphDialogLauncher,
  LcmLinkEntryOrSenseDialogLauncher, LcmLinkMsaDialogLauncher,
  LcmMergeEntryDialogLauncher, LcmMsaCreatorDialogLauncher,
  LcmPhonologicalFeatureChooserLauncher

Pure git mv (R100); LexTextControls.csproj is SDK-glob (only Compile Remove is
LexTextControlsTests/**), so no compile-item edits are needed.

Behavior-preserving folder move. build.ps1 -SkipNative -BuildTests: 0 errors, 0
warnings (LexTextControls + LexTextControlsTests compile from the new layout).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the region-bridge and Avalonia-launcher test files into Avalonia/ trees
that mirror the source reorg (3A/3B), so each test sits next to the area it
covers. The moved sets are exactly the test files added on this branch relative
to main (35 in xWorksTests, 17 in LexTextControlsTests); the two pre-existing
files touched on the branch (xWorksTests/XWorksAppTestBase, BulkEditBarTests)
stay in their roots.

xWorksTests/Avalonia/ mirrors 3A (best-fit by SUT): Composer/
FieldTypeComposerTests, FullEntryRegionOverrideTests,
FullEntryRegionReferenceChooserTests, FullEntryRegionVoiceWsTests,
LexiconFirstSliceEditContextEdgeCaseTests, RegionEditContextEditingTests,
RegionEditSessionLifecycleTests, StructuredText{Adapter,Integration,
Workflow}Tests, BackRefVectorTests, EntryReferenceVectorTests,
GhostLexRefSliceTests, LexReferenceMultiSliceTests, AdhocCoProhibComposeTests,
CompoundRuleComposeTests, NaturalClassComposeTests, RegionConsolidationTests,
RegionOverrideMigrationTests Plugins/ LexemeEditorBurnDownTests Hosting/
RecordEditViewActiveHostContractTests, RecordEditViewSwitchTests,
RecordClerkNavigationContextTests, FwTsStringClipboardTests,
FwXWindowUIModeSeedingTests, RegionCommandAdapterHardeningTests,
RegionContextMenuCompositionTests, RegionObjectCommandExecutionTests,
RegionEditLinkDispatchTests, RegionEditGuardAndSchedulingTests,
RegionDataTreeMoveReachabilityTests, WinFormsUiaSmokeTests,
AvaloniaHeadlessPlatformTests, AvaloniaHeadlessSetUpFixture,
TestLocalizationManagerBootstrap

LexTextControlsTests/Avalonia/ (flat) mirrors 3B: the 15 launcher/adapter test
files plus LexOptionsDlgTests and ScreenshotHarnessTests (the other two new
Avalonia-dialog test files).

Slotting notes (folder-only, no behavior effect):
- Root-level SUT tests (RegionEditContext*/RegionOverrideMigration) are placed
  in the closest of Composer/Hosting since the test mirror uses only the three
  subfolders.
- Cross-cutting test infrastructure (AvaloniaHeadlessSetUpFixture,
  AvaloniaHeadlessPlatformTests, TestLocalizationManagerBootstrap) is parked in
  Hosting. The [SetUpFixture] keeps its code-declared namespace, so its scope is
  unchanged by the move.

Pure git mv (R100); both test projects are SDK-glob, so no compile-item edits.
Behavior-preserving. build.ps1 -SkipNative -BuildTests: 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reviewer chose the bare legacy stem over the qualified RegionSliceFactory,
making the FwAvalonia.Region type a deliberate cross-namespace twin of
DetailControls.SliceFactory; no CS0104 alias was needed (no site references the
bare stem while importing DetailControls), and the engine-isolation audit's
source denylist drops SliceFactory since FwAvalonia.Region now owns the twin
while the assembly-level guard still enforces isolation; behavior-preserving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Seams namespace keeps only true substitution seams, their
implementations, and boundary data contracts:

- FinalizerSafeSynchronizationContext moves to the FwAvalonia root
  beside FwAvaloniaRuntime, which installs it (a hosting crash guard,
  not a seam).
- MorphTypeSwapLogic (with MorphTypeKind) moves to FwAvalonia.Region
  beside its RegionComposer consumer; the DetailControls pointer
  comment now names the new location.
- EditSurfaceKind is extracted from ActiveHostContract.cs into its own
  file at the FwAvalonia root, anchoring the EditSurface* family.
- ISeams.cs (a grab-bag named after no type) splits into one file per
  interface: IEditSession, IRegionRefreshCoordinator, IUiScheduler,
  IRegionLifetime, IXCoreCommandBridge, IRecordNavigationContext.

Behavior preserving; doc comments carried verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three whole-identifier renames, no behavior change:

- ISettlePendingEdits -> IPrepareToGoAway (method SettlePendingEdits ->
  PrepareToGoAway, still void). Named for the legacy DataTree/
  BrowseViewer PrepareToGoAway idiom; the summaries now spell out the
  deliberate contract difference — this seam cannot veto and always
  settles the open edit. RecordEditView implements it explicitly
  because the class already carries the legacy vetoing
  bool PrepareToGoAway() override.
- FwOptionPicker -> FwOptionChooser, unifying on the FieldWorks
  "chooser" vocabulary (FwOptionPickerTests -> FwOptionChooserTests).
  Diagnostic string literals keep the old name: the "FwOptionPicker"
  TraceSwitch id (paired with FieldWorks.Diagnostics.dev.config), the
  [FwOptionPicker] trace prefixes, the FwOptionPicker-NN dialog
  snapshot names, and three assertion-message mentions.
- LexemeEditorBurnDownTests -> LexemeEditorInventoryTests, dropping
  task-management vocabulary from the census fixture; the
  xWorksTests.csproj pointer comment follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Options label for the lexical-edit UI mode chooser said
"Lexical Edit UI", which names the internal migration rather than what
the user is choosing. Both twins now read "New UI (preview)":

- Avalonia: FwAvaloniaDialogsStrings.LexicalEditUiLabel ->
  NewUiPreviewLabel (resx key FwAvaloniaDialogs.NewUiPreviewLabel,
  renamed in place to keep the append-only order), value
  "New UI (preview)"; LexOptionsDlgView.axaml follows the accessor.
- Legacy WinForms twin (PR-added entry): LexTextControls.resx
  UiModeGroupTitle keeps its neutral key, value becomes
  "New UI (preview):"; the LexOptionsDlg in-code fallback copy of the
  label follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage (a) of the Region->Detail rename: the FwAvalonia core model/view
types (DetailModel, DetailField, DetailTextRun, DetailRichTextValue,
DetailWsValue, DetailChoiceOption, DetailMenu*, DetailViewing*, and the
rest of the mechanical table), IDetailEditContext, the seams
(IDetailRefreshCoordinator, IDetailLifetime/DetailLifetime), the host
pair (AvaloniaHostControlBase / DetailHostControl), DetailEditorCategory
with ClassifyDetailFieldKind, and RegionDataTree as the bare legacy twin
DataTree. The namespace moves from FwAvalonia.Region to
FwAvalonia.Detail and the folder follows via git mv.

RecordEditView's two partials pin bare DataTree to the legacy
DetailControls type with a using alias (the anticipated CS0104 twin).
The engine-isolation audit's forbidden-symbol list drops DataTree under
the same reclaimed-twin exemption it already documents for SliceFactory;
assembly-level isolation still enforces the real dependency ban. A stale
SetRegionContent cref now points at the real member, SetHostContent.
String literals (automation ids, trace text, snapshot capture names)
are deliberately unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage (b) of the Region->Detail rename: DetailComposer, ComposedDetail,
ComposedDetailEditContext, DetailEditContextBase/Holder, DetailValueFactory
(with its DetailRichTextAdapter helper), LcmDetailEditSession,
DetailOverrideMigration, AvaloniaDetailRefreshController, and
ReversalDetailEditContext; the slice-plugin family becomes ISlicePlugin /
SlicePluginRegistry / SlicePluginBuildContext (file SlicePlugins.cs). Members
and methods follow: ShowDetail, SettleDetailEdits, RefreshAvaloniaDetail,
OnAvaloniaDetailEditCompleted, OnDetailMenuRequested, OnDetailLinkRequested,
RaiseDetailEditCompleted, DetailEditCompleted, RefreshedDetailFieldCount,
m_detailEditContext, and the bare region/regionField locals.

The one reflection literal on the renamed method
(GetMethod("RefreshAvaloniaRegion")) chases the new name so the private-invoke
test keeps binding; trace-message literals that embed the old type names stay
unchanged by the literal rule. The two csproj comments naming the plugin
contract now say ISlicePlugin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage (c) of the Region->Detail rename: the preview support family
(DetailPreviewWindow / DetailPreviewScenario / DetailPreviewDataProvider
in DetailPreviewSupport.cs), the workflow driver DetailEditorDriver, the
test doubles (PreviewDetailEditContext, InMemoryDetailEditContext,
FakeDetailEditContext, Rich/UnsupportedRich/Fake DetailValueProvider,
RealisticDetailModel, DetailDefinition), and the EntryGo dialog's
DescriptionPane family (HasDescriptionPane,
OnDataContextChangedRemoveDescriptionPaneIfUnused, the DescriptionPane_*
test names, and the axaml comments' "description pane" wording).

RegionKeyboard becomes WritingSystemKeyboards.Activate, joining the
writing-system-centered keyboard vocabulary; its summary now describes
the per-WS keyboard activation for editor rows on the Avalonia surface
and keeps the lives-in-xWorks-because-LCModel note. Both callers
(RecordEditView.Avalonia.cs, ReversalIndexEntryPlugin.cs) follow. The
EntryGo automation ids ("EntryGo.DescriptionRegion",
"EntryGo.ResultsRegion") and snapshot capture names stay unchanged by
the literal rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage (d) of the Region->Detail rename: every Region-named test fixture
follows its subject (DetailEditingTests, DetailModelTests,
DetailMenuTests, DetailOrcTests, DetailEditContextEditingTests,
DetailComposerTests, DetailConsolidationTests, the Detail* hosting
fixtures, DataTreeTests / DataTreeMoveReachabilityTests for the bare
twin, EditorKindMapDetailCategoryTests, SlicePluginResolutionOrderTests,
and the FullEntryRegion* fixtures as DetailComposer*Tests), along with
every Region-bearing test method name (DetailEditView_*,
Detail_ScrollsLikeLegacyAutoScroll, ClassifyDetailFieldKind_*, and the
rest). Files rename to match their fixtures; the two csproj comments
naming RegionPreviewTests.cs now say DetailPreviewTests.cs, and stale
comment references to the old fixture names are updated.

Snapshot artifacts are transient (*.received.png / *.diff.png are
gitignored) and no committed baseline embeds a renamed test name.
Automation-id and assertion-message literals that mention the old names
stay unchanged by the literal rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage (e), the final pass of the Region->Detail rename: the resx row
FwAvalonia.RegionName becomes FwAvalonia.DetailAreaName with the value
"Detail area" (in place, order preserved), the FwAvaloniaStrings
accessor follows, and DataTree's automation Name consumer reads the new
accessor. Comment prose across FwAvalonia, FwAvaloniaDialogs,
xWorks/Avalonia, and the launcher files drops the region coinage for
"detail view" / "detail area" / "detail surface" wording
(sentence-level), and the EntryGo dialog family says "description pane".
Project-file and axaml comments follow.

Left deliberately unchanged: runtime string literals (the
RegionDataTree/RegionEditor automation ids and their test lookups, the
DetailComposer/DetailEditContextHolder trace prefixes that still say
RegionComposer/RegionEditContextHolder, DialogSnapshot capture names,
assertion messages, the avalonia-region-first-slice.png artifact name,
and the EntryGo.*Region automation ids), the region-manifest.md document
references, geographic "region" test data, and #region pragmas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Region->Detail identifier sweep deliberately left type-derived
string literals unchanged; this commit moves them, keeping every
literal and every lookup/assertion that reads it in the same change:

- Automation ids: RegionDataTree(.Scroll/.Splitter) -> DataTree(...),
  RegionEditor.* -> DetailEditor.*, RegionPreviewWindow ->
  DetailPreviewWindow, EntryGo.DescriptionRegion/ResultsRegion ->
  EntryGo.DescriptionPane/ResultsPane, with all test lookups.
- WinForms identity: Name "RegionHostControl" -> "DetailHostControl";
  DataTree Name/id "RegionDataTree" -> "DataTree". AccessibleName
  "RecordEditView.AvaloniaHost" is intentionally unchanged.
- Log/exception text: "RegionComposer:" -> "DetailComposer:",
  "RegionEditContextHolder.Settle:" -> "DetailEditContextHolder.
  Settle:", "A region editor plugin must claim..." -> "A slice plugin
  must claim...", the Region-worded Logger messages in
  RecordEditView.Avalonia.cs, and SliceFactory's "Custom region field"
  traces -> "Custom slice field".
- DialogSnapshot capture names (gitignored artifacts): Region-* ->
  Detail-*, EntryGo-09-no-description-region -> ...-pane,
  avalonia-region-first-slice.png -> avalonia-detail-first-slice.png.
- Assertion messages and the DetailViewingServices descriptors that
  said "region" in the detail sense now use detail wording; stale type
  mentions RegionLinkRequest/RefreshAvaloniaRegion/RegionSelectionRange
  now name DetailLinkRequest/RefreshAvaloniaDetail/DetailSelectionRange.
- IFwClipboard.cs: reworded the fidelity note so "legacy" no longer
  reads as describing TsString itself; the shared format is the
  existing "TsString" clipboard format native-Views copy/paste uses.

Deliberately untouched: FwOptionPicker trace-switch id and
FieldWorks.Diagnostics.dev.config, BCP-47 Region* (writing systems),
RegionsOC/LIFT/morphology XSL, #region pragmas, region-manifest.md
references, image-diff "region" wording in RenderVerification, and
geographic test data.

Gates: build 0 errors; FwAvaloniaTests 633/634 (1 skip);
FwAvaloniaDialogsTests 298/298; FwAvaloniaPreviewHostTests 3/3 (UIA);
xWorksTests ~Detail|~Composer|~RecordEditView|~Slice|~DataTree 225/225
(226/226 with ~Region, matching the pre-rename baseline set);
LexTextControlsTests 348/351 (3 skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanical name/path staleness fixes across .claude/skills/**: every
old type/file/path token is swapped for its verified landed equivalent
(each target confirmed to exist in the tree before writing):

- LexicalEditRegion{Model,Mapper,View,Field} -> DetailModel,
  DetailModelProjector, DataTree, DetailField;
  FullEntryRegionComposer -> DetailComposer (with its
  Src/xWorks/Avalonia/Composer/ path); ComposedEntryRegion ->
  ComposedDetail; RegionEditContext{Base,Holder} ->
  DetailEditContext{Base,Holder}; ComposedRegionEditContext ->
  ComposedDetailEditContext; IRegionEditContext -> IDetailEditContext;
  IRegionValueProvider -> IDetailValueProvider; LcmRegionEditSession ->
  LcmDetailEditSession.
- Plugin registry: RegionEditorPlugins(.cs) -> SlicePlugins(.cs) at
  Src/xWorks/Avalonia/Plugins/; IRegionEditorPlugin -> ISlicePlugin;
  RegionEditorBuildContext -> SlicePluginBuildContext;
  RegionEditorPluginRegistry -> SlicePluginRegistry;
  RegionFieldControlFactory -> SliceFactory; RegionFieldKind ->
  DetailFieldKind; LexemeEditorBurnDownTests ->
  LexemeEditorInventoryTests (Avalonia/Plugins path).
- Surfaces and features: LexicalEditSurface* -> EditSurface*;
  LexicalEditFeature* -> LexiconFeature*; LexicalEditorDriver ->
  DetailEditorDriver.
- Controls and seams: FwOptionPicker -> FwOptionChooser;
  RegionMenuFlyout -> DetailMenuFlyout; RegionFocusMemory(Tests) ->
  DetailFocusMemory(Tests); RegionWsValue -> DetailWsValue;
  IRegionLifetime -> IDetailLifetime; ILexicalRefreshCoordinator ->
  IDetailRefreshCoordinator; ISettlePendingEdits -> IPrepareToGoAway;
  Region*Tests -> Detail*Tests.
- Dialog kit files: OptionsDialogView/ViewModel + OptionsState ->
  LexOptionsDlgView/ViewModel/State; InsertEntryDialogView/ViewModel ->
  InsertEntryDlgView/ViewModel; AddNewSenseDialogView/ViewModel and
  MsaCreatorDialogView/ViewModel -> the *Dlg* names; FwMsaGroupBox.cs
  -> MSAGroupBox.cs. Landed test/launcher names that keep the old
  spelling (OptionsDialogTests, InsertEntryDialogTests,
  AvaloniaOptionsDialogLauncher, Lcm*DialogLauncher) are untouched.
- Paths: Src/Common/FwAvalonia/Region/ -> .../Detail/; the moved
  launcher (LexTextControls/Avalonia/), preview-host tests
  (FwAvaloniaPreviewHost/FwAvaloniaPreviewHostTests/), and relocated
  xWorks tests (Avalonia/Hosting/ for WinFormsUiaSmokeTests and
  RecordEditViewActiveHostContractTests); ISeams.cs citations now point
  at Src/Common/FwAvalonia/Seams/ (the file was split per interface).

Deliberately left (no landed equivalent to swap to; for the separate
alignment pass): browse-table machinery mentions
(SupportedAvaloniaBrowseToolNames, Phase1FollowUpBrowseTools,
ResolveBrowse_* tests, BrowseTableDriver, BrowseEditorIntegrationTests,
ClerkRoutedFilterTests), backed-out feature references (media seams,
RegionPictureEditor, FwGenDateField, rule-formula/interlinear types,
TreeSpikeAndRtlTests), prose uses of "region", and openspec/** by
decision.

Docs-only change; no code touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three conversion skills drive the human-in-the-loop migration: convert-dialog
and convert-slice (developer-gated phases: analysis, alignment, grilled
integration-test plan, exemplar mapping with the capability-table verdict for
custom controls, scaffold/implement, verify), and create-integration-test
(drive + assert + snapshot per plan item, locations by the test-mirroring
rule). Working documents live under Docs/migration/working/ (gitignored) with
state-manifest resume and self-re-evaluating dependency gates (children and
custom controls convert first). fieldworks-code-commenting encodes the repo
comment standard. Docs/migration/ carries the design overview and the two
developer playbooks the skills serve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Initial testing surfaced two defects. The live-app capture route owns the
FieldWorks lifecycle (relaunch-per-tool, CloseMainWindow), so pointing a
conversion at it killed the session the developer was exploring in; the
conversion skills now forbid touching a running instance, require asking
before any build or test run (a build fails on binaries the app holds
locked), mark the analysis phase read-only so it runs alongside the
developer, and default screenshot capture to the developer while they are in
the app. The winapp skill warns at its own entry point.

Gates were prose, so the agent reported, linked, summarized, and continued
past them. A gate is now an interactive question that ends the turn, and
summarizing a report in chat is banned -- the document is the artifact, and a
summary invites skipping the review the gate exists for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VM also reads as virtual machine, so it is expanded everywhere in the docs
and skills; the XAML xmlns:vm namespace alias stays, being real syntax. The
comment standard now bans ambiguous abbreviations generally, in comments,
documents, commit messages, and developer-facing communication.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The skills reported progress as "Phase 2" and "phase 4 gate passed", which
means nothing to a developer who has not read the skill file. Each stage now
has a name that says what is happening -- Understanding the dialog, Agreeing
on how it works, Deciding what to prove, Planning the replacement, Building
it, Proving it works -- and both skills carry a rule to use those names in
plain language and never a numbered-stage shorthand. The playbooks list the
stage names so the developer recognizes them.

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