Skip to content

Proj structure - #681

Open
TheJoeFin wants to merge 54 commits into
devfrom
proj-structure
Open

Proj structure#681
TheJoeFin wants to merge 54 commits into
devfrom
proj-structure

Conversation

@TheJoeFin

Copy link
Copy Markdown
Owner

organize pure logic out into a .core project and windows related features into a .windows project

TheJoeFin and others added 30 commits August 26, 2026 21:39
Phase 0 of the v5 workflow engine roadmap: adds the class-library split
(pure Core, Windows-TFM Core.Windows) and a fast pure-unit-test project
that later phases will move logic into, with no production code moved
yet. Both new libraries needed explicit RuntimeIdentifiers matching the
app so the MSIX wapproj restores project references per-RID.
Phase 1 of the v5 workflow engine roadmap: StringMethods.cs was already a
pure extension-method class with only System.* dependencies, making it a
zero-risk first move. It needed CurrentCase and SpotInLine, so those two
enums are split out of Text-Grab/Enums.cs into Text-Grab.Core/Enums.cs
(same Text_Grab namespace, no using changes needed anywhere); the rest of
Enums.cs stays in the app since those enums are UI/settings concerns.

Tests/StringMethodTests.cs is left in place unchanged - it still compiles
and passes via the transitive Tests -> Text-Grab -> Text-Grab.Core
reference chain. It wasn't moved to Tests.Core because one test
(TestReverseString) exercises StringBuilderExtensions.ReverseWordsForRightToLeft,
which lives in a separate app-side file not covered by this move.
…Core

Continuing Phase 1 of the v5 workflow engine roadmap. Both were already
pure (BCL-only usings), making them straightforward moves alongside
StringMethods.

TextSearchUtilities was internal, visible to the app only through the
same-assembly rule and to Tests via Text-Grab's InternalsVisibleTo. Once
it moved to Core it's a different assembly from its main consumers
(FindAndReplaceWindow, GrabFrame, EditTextWindow), so internal no longer
fit; made it public instead, matching StringMethods/CharacterUtilities.
Continuing Phase 1. Scoped to RecognizerExecutor only this round -
PatternExecutor stays in the app since PatternItem.GetAll()/GetByName()
are wired to the settings singleton, which would cascade into a wider
refactor (splitting PatternItem, updating ~6 UI call sites).

Both RecognizerExecutor and PatternExecutor called into
GrabTemplateExecutor.ExtractMatchesByMode, an internal pure method buried
in an otherwise WPF/OCR-coupled 660-line class. Extracted just that
method (+ its ExtractByIndices helper) into a new Text-Grab.Core
MatchModeSelector so RecognizerExecutor doesn't need to reach back into
the app; GrabTemplateExecutor and PatternExecutor now call the Core
version too, removing the duplicate logic instead of leaving it behind.

Added Microsoft.Recognizers.Text.* package references (+ the existing
NuGet.CommandLine exclusion workaround) to Text-Grab.Core, and dropped
the now-redundant direct references from Text-Grab.csproj since they
flow in transitively through the project reference.
Continuing Phase 1. StoredRegex.cs was already fully pure, so it moved
as-is. PatternItem.cs needed a real split: the class/PatternKind enum are
pure, but its GetAll()/GetByName() static methods are wired to
AppUtilities.TextGrabSettingsService (the settings singleton).

- Text-Grab.Core keeps the PatternItem class (properties + internal
  constructors + PatternKind enum) - no settings dependency.
- New Text-Grab/Models/PatternItemCatalog.cs (staying app-side) carries
  GetAll()/GetByName(), unchanged apart from the new name.
- Added InternalsVisibleTo("Text-Grab") and InternalsVisibleTo("Tests")
  to Text-Grab.Core so PatternItemCatalog and RegexManager.xaml.cs (which
  also constructs PatternItem directly) can still reach the internal
  constructors, and so Tests/PatternExecutorTests.cs (which constructs
  PatternItem directly for deterministic fixtures) still compiles.
- Updated every PatternItem.GetAll()/GetByName() call site to
  PatternItemCatalog: SearchBar, TextOnlyTemplateDialog,
  SplitColumnWindow, EditTextWindow, GrabFrame, and the test file.

PatternExecutor.cs stays in the app for now - this split is what
unblocks moving it next.
First move into Core.Windows, starting Phase 4 of the v5 workflow engine
roadmap. Both are pure interface contracts (ILanguage needs
Windows.Globalization, IOcrLinesWords/IOcrLine/IOcrWord need
Windows.Foundation.Rect) with zero implementation logic and zero WPF
coupling - the Windows SDK projections resolve automatically from the
net10.0-windows10.0.22621.0 TFM, no extra package needed.

No consumer call sites needed touching: implementers (TessLang,
GlobalLang, WinRtOcrLinesWords, etc.) stay in the app and keep resolving
the interfaces transitively through the existing project reference.
Continuing Phase 4. Moved the clean tier identified after mapping the
whole OCR/HDR area: TessLang, GlobalLang, WinAiOcrLinesWords (all
already dependency-free beyond ILanguage/IOcrLinesWords, which moved
earlier), and WinRtOcrLinesWords.

WinRtOcrLinesWords needed one fix: its constructor called
OcrUtilities.GetBoundingRect() - an app-side extension method that
computes the union of a line's word rects but returns a WPF
System.Windows.Rect, which the constructor then converted back into a
WinRT Windows.Foundation.Rect. Since every input (OcrWord.BoundingRect)
is already a WinRT Rect, that was a pointless WPF round-trip and the
actual blocker to moving the file. Replaced it with a local private
helper that computes the same union directly in Windows.Foundation.Rect.
OcrUtilities.GetBoundingRect itself is untouched - other app code may
still use it.

Also extracted HocrReader + TessOcrLine (the last ~53 lines of
TesseractHelper.cs) into Text-Grab.Core - pure string/regex hOCR
parsing, already cleanly separated from the rest of the file, going to
plain Core (not .Windows) since it's not Windows-specific. The rest of
TesseractHelper.cs stays in the app; nothing else currently calls it.

Added Microsoft.WindowsAppSDK.AI to Text-Grab.Core.Windows.csproj for
WinAiOcrLinesWords' RecognizedText/RecognizedLine/RecognizedWord types.

Deferred (each documented in the plan with its specific blocker):
OcrUtilities.cs (the actual "pivotal cut" - 1061 lines, mixed
headless/UI-adapter code plus a hidden WPF dependency in
LoadBitmapFromFile), the TesseractHelper class itself (settings-write
side effect in GetTesseractPath), LanguageService.cs and
WindowsAiUtilities.cs (both settings+WPF coupled, each dragging in
several more untouched types), BarcodeUtilities.cs (blocked on
OcrOutput.CleanOutput() reading settings directly), and the HDR/WGC
capture code (separate area, mostly clean already).
Wave 0a of the Core split. No files move; this only puts the substitute
type and the boundary conversions in place so later waves have somewhere
to land.

System.Windows.Rect/Point/Size live in WindowsBase.dll and only exist
with UseWPF=true. They appear in roughly thirty otherwise-portable files
and are the second-biggest blocker to moving code out of the app, after
settings access. Text-Grab.Core targets plain net10.0 and
Text-Grab.Core.Windows deliberately keeps UseWPF=false, so neither can
use them.

RectangleF/PointF/SizeF are the substitute. They come from
System.Drawing.Primitives, which is part of the shared framework and
genuinely cross-platform - not to be confused with System.Drawing.Common
(Bitmap, Graphics, Icon), which is Windows-only and belongs in
Text-Grab.Core.Windows. A bare "using System.Drawing" says nothing about
which of the two a file needs; check the types.

- Text-Grab.Core/Utilities/RectangleFExtensions.cs mirrors the portable
  half of the app's ShapeExtensions: IsGood, CenterPoint,
  GetScaledUpByFraction, GetScaleSizeByFraction. Each one already has a
  Rect-typed caller in the app, so none of these are speculative. Union
  is the exception - it is new, and skips empty operands instead of
  dragging the result back to the origin so it can be folded over a
  sequence of word rects, which is what ResultTable and the OCR line
  assembly will want in wave 4.

- ShapeExtensions gains the conversions across the boundary: AsRect /
  AsRectangleF, AsPoint / AsPointF, AsSize / AsSizeF. It already carried
  Rectangle <-> Rect, so this extends the existing seam rather than
  standing up a parallel WpfGeometryExtensions next to it.

Tests.Core covers the new helpers, which also gives that project real
tests instead of only the scaffolding smoke test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wave 0b of the Core split. No files move; this unblocks the ones that
could not move because they read settings.

48 app files reach settings through AppUtilities.TextGrabSettings, which
returns Text_Grab.Properties.Settings - an internal, sealed, generated
ApplicationSettingsBase with 104 properties. That accessor is the single
largest thing tying logic to the app assembly, and it is why
OcrUtilities, TesseractHelper, LanguageService, WindowsAiUtilities and
BarcodeUtilities are all sitting in the deferred ledger.

The useful detail is that the coupling is much narrower than the file
count suggests: only 28 distinct properties are read through that
accessor across the entire app.

- Text-Grab.Core/Interfaces/ITextGrabSettings.cs declares the slice
  portable code may read. It is seeded from the actual near-term
  consumers rather than guessed at: CorrectErrors, CorrectToLatin,
  ParagraphDetection, RemoveFurigana, TryToReadBarcodes and
  UiAutomationFallbackToOcr are what OcrUtilities reads via its
  DefaultSettings field; TesseractPath and Save() are what
  TesseractHelper.GetTesseractPath needs; CorrectToLatin and
  CorrectErrors are what OcrOutput.CleanOutput reads, which is the
  specific thing blocking BarcodeUtilities.

- The app implements it by declaring it. Settings.Designer.cs already
  generates properties with matching names and types, and Save() comes
  from ApplicationSettingsBase, so the hand-written partial in
  Properties/Settings.cs satisfies the whole interface with no
  forwarding code. An internal class implementing a public interface is
  fine, and this keeps working across SettingsSingleFileGenerator runs.

- SettingsAccess holds a resolver delegate rather than an instance. The
  app's settings object hangs off Singleton<SettingsService>.Instance,
  which is lazy and does real work on first touch - reads user.config,
  seeds automation profiles, loads JSON sidecars. Storing a delegate
  keeps module initialization free of all that while leaving the
  existing lazy behavior exactly as it was.

- Registration is a [ModuleInitializer] in the app, not a call in
  App.appStartup. The Tests host loads the app assembly and exercises
  its code without ever raising the WPF Startup event; wiring it at
  module load covers both paths by construction. SettingsAccessTests
  asserts precisely that - it passes without any startup call.

Keep the interface small. Add a property when a move demands it; if one
file would need more than a handful, prefer the facade split instead -
pure logic to Core, thin settings-reading wrapper left in the app, the
way PatternItem / PatternItemCatalog went in e677b54.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wave 0c of the Core split, and the last of the foundation work before
files start moving.

- New Tests.Core.Windows project for the headless Windows tier, mirroring
  Text-Grab.Core.Windows: same TFM, UseWPF and UseWindowsForms both off,
  so a test can never pass against a reference the library itself cannot
  use. It needs an explicit Platform - without one the Windows App SDK
  targets pulled in transitively via Microsoft.WindowsAppSDK.AI fail with
  "WindowsAppSDKSelfContained requires a supported Windows architecture".
  Build and test it with -p:Platform=x64.

- TierBoundaryTests enforces the layering by reflection rather than by
  review: Text-Grab.Core references no WPF, WinForms, System.Drawing.Common
  or Windows App SDK assembly; Text-Grab.Core.Windows references no WPF
  assembly; and Core does not reference Core.Windows. This was not in the
  original plan, but the risk register lists "a mover flips UseWPF=true to
  unblock itself" as a real failure mode, and a csproj diff is easy to
  wave through in review. Now it fails the build instead.

- InternalsVisibleTo brought up to date on both libraries, so the two new
  test projects can reach internal types as more code moves.

- Both workflows now run all three test projects. The pure tiers run
  first: they need no display and finish in about a second each, so a
  logic regression fails the job before the slow WPF/STA suite starts.

- docs/Core-Split-Plan.md is the standing plan for waves 1 through 7 and
  the contract every mover agent reads before touching anything. Its
  section 3 is updated to describe what wave 0 actually built rather than
  what it proposed, including the three places the result differed:
  ShapeExtensions was extended instead of adding a parallel
  WpfGeometryExtensions, settings registration moved to a
  [ModuleInitializer] so the test host is covered, and the tier guards
  were added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wave 0 is done and the plan's remaining file lists were unreliable, so
this replaces them with lists derived from reading every candidate file
end-to-end rather than from grepping using directives.

The original lists were wrong about roughly a dozen files in both
directions, all for the same four reasons - now written down as
invariant 8, because they will keep recurring:

  - A file with no System.Windows using can still be WPF-bound.
    UndoRedo.cs, ChangeWord.cs and ResizeWordBorder.cs reach WPF through
    "using Text_Grab.Controls;", because WordBorder IS a WPF Control.
  - A fully-qualified type appears in no using at all. LookupItem.cs
    holds a Wpf.Ui.Controls.SymbolRegular property.
  - System.Drawing is two things: portable primitives vs Windows-only
    GDI+.
  - Rect is two things: Windows.Foundation.Rect (WinRT, fine in
    Core.Windows) vs System.Windows.Rect (WindowsBase, not). edefeaa
    already hit this once.

Those four files leave the wave lists for the never-move list, along
with ImplementAppOptions (app lifecycle, casts to the WPF App),
MagickHelpers, CameraCaptureUtilities, SettingsImportExportUtilities and
DiagnosticsUtilities - the last of which reads ~70 settings properties,
fourteen times the threshold at which the plan already says to prefer a
facade.

Two files move the other way. The never-move list was written before B2
landed, and WordBorderInfo and TemplateRegion are WPF-bound *only*
through Rect, which is exactly what RectangleF now solves. WordBorderInfo
is already the portable projection of the WordBorder control - that is
its whole job - so converting BorderRect and leaving the
WordBorderInfo(WordBorder) constructor behind as a factory unblocks
ResultTable's clustering algorithm. TemplateRegion likewise unblocks
GrabTemplate and OcrDirectoryOptions.

New Wave 1, which did not exist before and is now the highest-leverage
batch in the plan: a handful of tiny leaves (Singleton, WrappingStream,
NullAsyncResult, Json, IoUtilities' pure half, StorageFileExtensions)
plus extracting AppUtilities.IsPackaged()/GetAppVersion() into a
Core.Windows PackageIdentity. Three separate recon passes independently
hit these same files as blockers; no wave owned them. Waves 3, 5 and 6
all stall without them, because Core.Windows cannot reach back into the
app.

Other corrections worth naming:

  - OSInterop.cs (1292 lines) is blocked by one dead line.
    System.Windows.Forms appears exactly once in the file, in a
    GetAsyncKeyState(Keys) overload with no callers; the live caller uses
    the int overload.
  - AutomationProfile and AutomationSettingsProvider go to plain Core,
    not Core.Windows. Verified by building and running a probe:
    ApplicationSettingsBase and LocalFileSettingsProvider resolve and run
    on net10.0 with a System.Configuration.ConfigurationManager package
    reference. AutomationProfile needs only ApplySeed widened to take
    ApplicationSettingsBase.
  - ResultTable's OcrResult coupling is dead code, not a blocker; its
    live path already consumes IOcrLinesWords.
  - TesseractHelper's settings write-back needs no redesign -
    ITextGrabSettings.Save() covers it. The real blocker is
    AutomationProfile via one method.
  - PdfDocumentRenderer moves to wave 5; its blocker is BitmapSource
    currency, the same as ImageMethods, not anything OCR-specific.
  - Batch 4c cannot precede wave 3 - OcrEngine.cs will not compile until
    the language models land.
  - Hdr/* was recorded as "mostly clean already". HdrScreenCapture.cs:472
    reaches System.Windows.Application.Current.Dispatcher to pump a
    consent dialog.

Section 4.0 records what changed and why. Section 8 is new: five pieces
of verified dead code, each with zero call sites, two of which unblock a
move outright. Section 7's ledger rows now carry specific blockers with
line numbers instead of "settings + WPF coupled".

B4 closes a loophole the recon found in invariant 2: FrameworkReference
to Microsoft.WindowsDesktop.App resolves System.Windows.Automation
without setting UseWPF, which would have let UIAutomationUtilities move.
Declining it - it drags WindowsBase in and puts System.Windows.Rect back
within reach of Core.Windows, defeating B2. TierBoundaryTests already
fails the build on that, since it checks assembly names.

Nine ITextGrabSettings additions cover everything movable in the whole
plan, taking the interface from 10 members to 19. The Load*/Save*
families are not candidates - they are SettingsService methods, and the
PatternItemCatalog facade handles them with no interface change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…b.Core

Wave 1a - shared leaves that were blocking Waves 3, 5 and 6 independently.
All four verified dependency-free per invariant 8:

- Singleton.cs and StreamWrapper.cs (WrappingStream) moved as-is; the
  latter constructs NullAsyncResult, which moved in the same commit to
  keep the pair compiling together.
- Json.cs kept its Text_Grab.Helpers namespace unchanged even though it
  physically lived under Utilities/ - not "fixed" to match the folder.

IoUtilities.cs needed a real split. The extension lists and the
Is*File/Is*FileExtension predicates plus ListFilesFoldersInDirectory are
pure string/List<string> logic and moved to Text-Grab.Core as-is.
GetEditorModeForPath and GetOpenContentKindForPath return EtwEditorMode
and OpenContentKind, both still in the app's Enums.cs (that merge is
Wave 2a, not this batch), so they can't move yet. GetContentFromPath and
TryToOpenTextFile also stay app-side - they call OcrUtilities and show
Wpf.Ui/WinForms MessageBoxes. All four impure members moved into a new
Text-Grab/Utilities/FileOpenUtilities.cs facade (the IoUtilities name
had to move with the pure half to avoid a duplicate-type collision once
the app references Core). Updated the four call sites this renamed:
EditTextWindow.xaml.cs, App.xaml.cs, and Tests/FilesIoTests.cs (both
enum-returning test cases).

No files deferred from this batch's list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rab.Core.Windows

Wave 1b - the two remaining shared leaves blocking Waves 3 and 5.

AppUtilities.IsPackaged()/GetAppVersion() needed only Windows.ApplicationModel.
Package; they were unreachable from Core.Windows purely because they shared a
class with TextGrabSettings/TextGrabSettingsService, which must stay in the
app. Extracted both into new Text-Grab.Core.Windows/Utilities/PackageIdentity.cs
(namespace Text_Grab.Utilities, unchanged), keeping the same try/catch and
version-string logic verbatim. AppUtilities.IsPackaged()/GetAppVersion() are
now one-line forwarders to PackageIdentity, so the ~30 existing call sites
across the app (GeneralSettings, LanguageSettings, SettingsService,
FileUtilities, WindowsAiUtilities, etc.) did not need to change.

StorageFileExtensions.cs moved as-is via git mv - verified dependency-free
(System, System.IO, Windows.Storage, Windows.Storage.Streams only, no
Text-Grab types). Namespace Text_Grab.Extensions unchanged.

No files deferred from this batch's list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GetAppVersion moved into Text-Grab.Core.Windows in the previous commit, which
silently changed what GetExecutingAssembly() resolves to. In AppUtilities it was
Text-Grab.exe and its <Version>4.15.0</Version>; from Core.Windows it is a library
with no version property, so the unpackaged path reported 1.0.0.0 to the settings
page and to the diagnostics payload. The entry assembly is the app either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 2a. Verified all 17 enums in Text-Grab/Enums.cs are plain
int/short-backed with no attributes, and confirmed zero name collisions
with Core's existing CurrentCase/SpotInLine before merging them into
Text-Grab.Core/Enums.cs. Deleted the now-empty app Enums.cs.

Revisited FileOpenUtilities.cs as directed. GetOpenContentKindForPath
returns OpenContentKind, which just landed in Core, so it moved to
Text-Grab.Core/Utilities/IoUtilities.cs alongside the extension
predicates it already calls; FileOpenUtilities.GetContentFromPath and
Tests/FilesIoTests.cs were updated to call it there.

GetEditorModeForPath did NOT move, despite the plan's premise that both
methods were blocked only by Enums.cs. Verified by reading
EditTextTableDocument.cs and checking `git log`: EtwEditorMode has never
lived in Enums.cs - it was declared directly in
Text-Grab/Models/EditTextTableDocument.cs since that file was created.
GetEditorModeForPath stays in FileOpenUtilities.cs until batch 2b moves
EditTextTableDocument.cs (and EtwEditorMode with it).

Gate: Tests.Core 14/14, Tests.Core.Windows 3/3, Tests 1493/7 skipped/0
failed - matches baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ities split

Batch 2b. All ten files read in full and verified clean per invariant 8:
AsyncOcrFileResult, EditTextTableDocument (moved first - FindResult calls
its static GetSpreadsheetColumnLabel), ExtractedPattern (uses
StringMethods.ExtractSimplePattern, already in Core), FindResult,
GrabFrameTableEditState, GrabFrameWordGroupingMode, SpreadsheetUndoHistory
(internal - already covered by Core's existing InternalsVisibleTo("Tests")
and ("Text-Grab")), TemplatePatternMatch, TemplateRecognizerMatch (uses
RecognizerOutputKind, already in Core), ThirdPartyPackageInfo. No call
sites needed fixing - everything resolved through the existing
Text_Grab.Models namespace.

This unblocks the other half of the FileOpenUtilities revisit deferred
in the previous commit: EtwEditorMode moved to Core along with
EditTextTableDocument.cs, so GetEditorModeForPath now moves too, joining
GetOpenContentKindForPath in Text-Grab.Core/Utilities/IoUtilities.cs.
FileOpenUtilities.cs is left with only GetContentFromPath and
TryToOpenTextFile, which stay app-side (OcrUtilities call, Wpf.Ui/WinForms
MessageBoxes). Updated call sites: EditTextWindow.xaml.cs and
Tests/FilesIoTests.cs.

Gate: Tests.Core 14/14, Tests.Core.Windows 3/3, Tests 1493/7 skipped/0
failed - matches baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… more

Batch 2c. All nine files read in full per invariant 8.

Straight moves (PatternExecutor first, then ColumnSplitUtilities which
calls it): PatternExecutor, ColumnSplitUtilities, NumericUtilities,
LanguageHeuristics (internal - already covered by Core's existing
InternalsVisibleTo("Text-Grab")/("Tests")), Extensions/NumberExtensions,
Extensions/StringBuilderExtensions (uses StringMethods, already in Core),
Interfaces/ITtsEngine. No call sites needed fixing.

Two splits:

- ProtocolUtilities: IsProtocolUri/TryParseProtocolUri and the Scheme
  const are pure and keep the name in Core. TryGetSafeProtocolFilePath
  (needs AutomationProfile) and EnsureProtocolRegistration (needs
  Registry + FileUtilities) - both still app-side - moved into a new
  Text-Grab/Utilities/ProtocolHandlerUtilities.cs. Updated call sites:
  App.xaml.cs and the impure half of Tests/ProtocolUtilitiesTests.cs
  (the pure-half tests are untouched and still compile against Core
  through the existing InternalsVisibleTo; migrating them to Tests.Core
  is Wave 7a, not this batch).

- ThirdPartyNoticeUtilities: the Packages catalog and its constants are
  pure and keep the name in Core. GetBuiltWithFilePath/
  GetNoticesDirectoryPath/GetNoticeTarget/Open* all need
  FileUtilities.GetExePath() and moved into a new
  Text-Grab/Utilities/ThirdPartyNoticeLauncher.cs. Updated the one call
  site, LicensesWindow.xaml.cs. All three existing tests touch only
  Packages, so ThirdPartyNoticeUtilitiesTests.cs moved to Tests.Core in
  this same commit (namespace updated to Text_Grab.Tests.Core to match
  that project's convention).

Gate: Tests.Core 17/17 (14 baseline + 3 migrated), Tests.Core.Windows
3/3, Tests 1490/7 skipped/0 failed (1493 baseline - 3 migrated) -
matches baseline net of the test migration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rabTemplate/OcrDirectoryOptions into Core

Batch 2d - the geometry-currency payoff from B2 (30af90f).

- WordBorderInfo.BorderRect: System.Windows.Rect -> System.Drawing.RectangleF.
  The WordBorder(WordBorder) constructor was the class's only WPF tie and moved
  out to a new app-side Text-Grab/Models/WordBorderInfoFactory.cs
  (WordBorderInfoFactory.Create(WordBorder)), same shape as PatternItem/
  PatternItemCatalog in e677b54. Default changed from Rect.Empty to
  RectangleF.Empty - verified safe: grepped every BorderRect-adjacent emptiness
  check in the app and found none; all the `== Rect.Empty` call sites in the
  repo test PositionRect/CaptureRegion/other Rects, never BorderRect.

- TemplateRegion.ToAbsoluteRect/FromAbsoluteRect: Rect -> RectangleF. Its only
  production call site (GrabFrame.xaml.cs:283) converts back with .AsRect().
  FromAbsoluteRect has zero call sites anywhere in the repo (verified by grep,
  not just usings) - its signature changed anyway since it's public API on a
  moved type.

- GrabTemplate.cs and OcrDirectoryOptions.cs: moved as pure renames, no
  content changes. Both were already clean once their TemplateRegion
  dependency could move.

Call sites fixed (all compiler-flagged, no unrequested edits):
- Text-Grab/Models/ResultTable.cs: BorderRect construction now uses
  lineRect.AsRectangleF(); two Median() calls needed an explicit (double) cast
  since IEnumerable<float> no longer flows into an IEnumerable<double> param.
- Text-Grab/Views/GrabFrame.xaml.cs: three `new WordBorderInfo(wb)` call sites
  -> WordBorderInfoFactory.Create(wb); the TemplateRegion.ToAbsoluteRect call
  and the history-rescale block convert at the boundary with .AsRect()/
  .AsRectangleF() rather than widening GrabFrame's own Rect usage.
- Tests/*.cs (GrabFrameFileTests, HistoryServiceTests, ResultTableBenchmarks,
  ResultTableManualSeparatorTests): BorderRect literals changed from
  `new Rect(...)` to `new RectangleF(...)`; two of these needed explicit
  double->float casts on their arguments (precision-loss risk, see below).
  Removed now-unused `using Rect = System.Windows.Rect;` and
  `using System.Windows;` where nothing else in the file needed them.

Semantic risk (compiles and passes tests, flagging per instructions):
- double->float precision loss at every BorderRect write site: the
  ResultTableBenchmarks/ResultTableManualSeparatorTests literals, the
  WordBorderInfoFactory conversion from WPF Left/Top/Width/Height (double),
  and the GrabFrame history-rescale multiply-then-AsRectangleF() path. All
  are screen/OCR pixel coordinates well within float's exact-integer range
  (2^24), so this is not expected to be visible, but it is a real behavior
  change from the previous all-double path.
- BorderRect's default changed from Rect.Empty (WPF: NaN-based, IsEmpty
  semantics) to RectangleF.Empty (all-zeros). No current call site tests
  BorderRect for emptiness either way, so this is currently inert, but a
  future `== RectangleF.Empty` check on a legitimately-zero-sized rect would
  behave differently than the old `== Rect.Empty` would have on a
  legitimately-empty one.
- ParseOcrResultIntoWordBorderInfos still builds its intermediate lineRect as
  a WPF Rect (unrelated OCR math untouched) and only converts to RectangleF at
  the final assignment - kept for minimal diff, not a behavior change.

Deferred (not in this batch's file list, left alone per invariant 5):
- Utilities/GrabTemplateManager.cs and Utilities/GrabTemplateExecutor.cs still
  have real blockers (BitmapSource/Wpf.Ui in the manager; a non-scalar
  LoadStoredRegexes() seam and the OCR half's Wave-3/4c dependency in the
  executor) unrelated to TemplateRegion. Updated their section 7 rows to drop
  the now-resolved "template models not in Core" clause.

Gate: Tests.Core 17/17, Tests.Core.Windows 3/3, Tests 1490 passed / 7 skipped
/ 0 failed - matches baseline exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 2e - all three CalculationService files (~2000 lines) were fully
pure: NCalc, UnitsNet, System.* only, plus one call into
NumericUtilities.TryParseFlexibleDouble (already in Core as of 2c).
No WPF/Windows types anywhere in the trio. Moved as a clean rename,
namespace unchanged (Text_Grab.Services).

- Text-Grab.Core/Services/CalculationService.cs
- Text-Grab.Core/Services/CalculationService.DateTimeMath.cs
- Text-Grab.Core/Services/CalculationService.UnitMath.cs

Moved the NCalcAsync and UnitsNet PackageReferences from
Text-Grab.csproj to Text-Grab.Core.csproj - repo-wide grep confirmed
no other app file uses either package (BarcodeUtilitiesTests.cs has an
unused `using UnitsNet;` but no UnitsNet types), so the app gets both
transitively through the project reference. Left Tests.csproj's own
NCalcAsync reference alone per the plan; Tests.Core needs no NCalcAsync
reference of its own since it gets it transitively through
Text-Grab.Core's new PackageReference.

Moved CalculatorTests.cs and UnitConversionTests.cs from Tests/ to
Tests.Core/ in the same commit, updating their namespace from `Tests`
to `Text_Grab.Tests.Core` to match the convention
ThirdPartyNoticeUtilitiesTests.cs established (f6a1cd0). No WPF
dependencies found in either test file.

Test counts, migration shift accounted for:
- Tests.Core: 17 -> 487 (+470)
- Tests.Core.Windows: 3 -> 3 (unchanged)
- Tests: 1490 -> 1020 passed / 7 skipped (-470 passed, migrated out)
470 + 1020 = 1490, so no regression hid inside the shift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 2f. About 150 pure lines out of 1168. Verified each of the
plan's listed members individually against invariant 8 rather than
trusting the list; all checked out as pure Markdig/regex/string code
with no FlowDocument or other System.Windows.Documents types:
LooksLikeMarkdown, ShouldPromoteLiveBlock, ShouldPromoteLiveMarkdown,
NormalizeDocumentText, NormalizeNewlines, EscapeMarkdownText,
EscapeLinkDestination, ApplyQuotePrefix, GetQuotePrefix,
GetOrderedListStart, ResolveContentSpan, GetSourceSlice,
GetCodeSpanContentRawStart, GetCodeBlockText, the MarkdownPipeline
field, and the three [GeneratedRegex] partial methods.

A partial class cannot span assemblies, so the split needed a new
type name for the impure half, per the PatternItem/PatternItemCatalog
shape from e677b54:

- Text-Grab.Core/Utilities/MarkdownDocumentUtilities.cs keeps the
  original type name and namespace (Text_Grab.Utilities), holding only
  the members above. The three now-private-in-Core regex-backed static
  fields (LiveBlockTriggerRegex etc.) moved with the methods that use
  them. Previously-private members that the app-side facade still
  calls became `internal` (MarkdownPipeline field included) - Core
  already has InternalsVisibleTo("Text-Grab") from e677b54, so no new
  visibility plumbing was needed.

- Text-Grab/Utilities/MarkdownFlowDocumentUtilities.cs (renamed from
  the original file) keeps everything FlowDocument-bound: CreateFlowDocument,
  SerializeToMarkdown, GetDocumentPlainText, BuildOffsetMap,
  MapRawOffsetToPosition and their private helpers, ApplyTheme, the
  AppendBlock/WriteBlock tree, and all the attached DependencyProperty
  plumbing (its typeof() owner arguments now point at the new type,
  since that's the class actually declaring them). Every call from
  here into the moved pure helpers is qualified with
  `MarkdownDocumentUtilities.`.

Call sites fixed (all compiler-flagged after the rename, no
unrequested edits): Text-Grab/Views/EditTextWindow.xaml.cs - the three
pure calls (ShouldPromoteLiveMarkdown, ShouldPromoteLiveBlock,
LooksLikeMarkdown) keep referring to MarkdownDocumentUtilities; the
FlowDocument-touching calls (CreateFlowDocument, BuildOffsetMap,
SerializeToMarkdown, ApplyTheme, MapRawOffsetToPosition,
GetDocumentPlainText, the MarkdownOffsetMap field type) were renamed
to MarkdownFlowDocumentUtilities.

Markdig stays referenced by both halves, as the plan calls out: the
app side pattern-matches on Markdig AST types directly in
AppendBlock/AppendInline, so its own PackageReference had to stay -
verified this is a real compile need, not just relying on
transitivity, before leaving it in place. Added the same Markdig
1.3.2 PackageReference to Text-Grab.Core.csproj so the pure half's
Markdig.Syntax/Markdig.Syntax.Inlines/Markdig.Extensions.AutoIdentifiers
usages resolve.

Extracted the 6 pure test methods (all [Theory], no WpfFact, no
FlowDocument) into Tests.Core/MarkdownParsingTests.cs, namespace
Text_Grab.Tests.Core matching the ThirdPartyNoticeUtilitiesTests
convention. Renamed the remaining 33 FlowDocument-dependent call sites
in Tests/MarkdownDocumentUtilitiesTests.cs from
MarkdownDocumentUtilities to MarkdownFlowDocumentUtilities;
mechanical, no other changes.

Test counts, migration shift accounted for:
- Tests.Core: 487 -> 514 (+27, the 6 Theory methods' InlineData cases:
  6+3+5+4+6+3)
- Tests.Core.Windows: 3 -> 3 (unchanged)
- Tests: 1020 -> 993 passed / 7 skipped (-27 passed, migrated out)
27 + 993 = 1020, so no regression hid inside the shift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 3a. Verified each file's actual type usage against invariant 8
rather than trusting the plan's list:

- NativeMethods.cs, OSInterop.cs (both file-scoped, no namespace) - pure
  P/Invoke over user32/gdi32/shcore/shell32/kernel32/dwmapi, System and
  System.Runtime.InteropServices only.
- OSInterop.cs's GetAsyncKeyState(System.Windows.Forms.Keys) overload
  (line 125) was the file's only System.Windows.Forms reference in 1292
  lines, confirmed zero call sites (WindowUtilities.cs:289 uses the
  int overload). Deleted as part of the move, per plan section 8.
- RegistryMonitor.cs (namespace RegistryUtils, vendored - kept exactly)
  - pure P/Invoke over advapi32 plus Microsoft.Win32.Registry types.
- DesktopNotificationManagerCompat.cs (namespace Text_Grab) - COM
  activation and toast notification plumbing over Microsoft.Win32,
  Windows.UI.Notifications (WinRT) and P/Invoke; no WPF types.
- Models/GeneratedOcrLinesWords.cs - uses Windows.Foundation.Rect
  (WinRT), not System.Windows.Rect; confirmed the distinction invariant
  8 calls out. IOcrLinesWords/IOcrLine/IOcrWord it implements already
  live in Core.Windows.
- Models/UiAutomationLang.cs, WindowsAiLang.cs, WindowsAiDescriptionLang.cs
  - plain ILanguage implementations (Text_Grab.Interfaces.ILanguage,
  already in Core.Windows) over Windows.Globalization only.

All eight kept their original namespace and moved via git mv with no
call-site changes needed - every consumer is elsewhere in the app,
which reaches these types transitively through the existing project
reference, and Core.Windows already has InternalsVisibleTo("Text-Grab")
for the two internal P/Invoke classes.

One project setting had to move: NativeMethods.cs and OSInterop.cs use
LibraryImportAttribute-generated marshalling stubs, which require
AllowUnsafeBlocks. Added it to Text-Grab.Core.Windows.csproj (already
set in Text-Grab.csproj, so no behavior change for the app).

No files deferred from this batch's list.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp
… Text-Grab.Core.Windows

Batch 3b, unblocked by Wave 1's Json.cs landing in Core.

- Extensions/SettingsStorageExtensions.cs (namespace Text_Grab.Helpers,
  kept exactly) - WinRT Windows.Storage/Windows.Storage.Streams helpers
  for ApplicationData/StorageFolder/ApplicationDataContainer, calling
  Json.StringifyAsync/ToObjectAsync from the same namespace (Core's
  Utilities/Json.cs, batch 1a). No other type dependencies.
- Utilities/LimitedAccessFeatureUtilities.cs (namespace Text_Grab.Utilities,
  kept exactly) - the first link in the Windows AI chain 3c continues.
  Pure Windows.ApplicationModel (LimitedAccessFeatures, Package.Current)
  plus reflection over its own assembly's AssemblyMetadataAttribute.
  Stayed internal; Core.Windows already has InternalsVisibleTo("Text-Grab")
  so its one remaining app-side caller, WinAiLanguageModel.cs, is
  unaffected until 3c moves it too.

LimitedAccessFeatureUtilities.GetSetting reads
typeof(LimitedAccessFeatureUtilities).Assembly's AssemblyMetadata, which
would have silently started reading the wrong (now-empty) assembly's
metadata once the type moved - the LafToken/LafPublisherId MSBuild
wiring that populates it was only in Text-Grab.csproj. Moved that
PropertyGroup/ItemGroup pair to Text-Grab.Core.Windows.csproj instead of
leaving it behind: Text-Grab.csproj's ProjectReference to Core.Windows
carries the same global MSBuild properties into its build by default,
so `dotnet build -p:LafToken=... -p:LafPublisherId=...` against
Text-Grab.csproj, the wapproj, or CI's publish steps all still reach the
new home unchanged. Verified end-to-end: built with
-p:LafToken=TESTTOKEN -p:LafPublisherId=TESTPUB and confirmed both
AssemblyMetadata attributes land on Text-Grab.Core.Windows.dll via its
generated AssemblyInfo.cs. Updated the doc comment in
LimitedAccessFeatureUtilities.cs and
docs/Configuring-LAF-Environment-Variables.md to match.

No files deferred from this batch's list.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp
Batch 3c, continuing the WinAI chain that 3b started.

- Utilities/WinAiLanguageModel.cs (namespace Text_Grab.Utilities, kept
  exactly) - the shared Phi Silica LanguageModel wrapper. Its three
  dependencies had all already landed: PackageIdentity (1b),
  OSInterop.IsWindows10() (3a), LimitedAccessFeatureUtilities (3b).
  Microsoft.WindowsAppSDK.AI was already referenced by
  Text-Grab.Core.Windows.csproj (added earlier for WinAiOcrLinesWords),
  so no package changes were needed.
- Utilities/WinAiTranslator.cs, Utilities/WinAiMeetingNotes.cs (same
  namespace, kept exactly) - both needed only WinAiLanguageModel and,
  for the translator, the already-Core LanguageHeuristics. Moved
  unchanged.

Self-assembly-reflection audit (the specific risk flagged after 3b's
LimitedAccessFeatureUtilities near-miss): none of the three files read
their own assembly. WinAiLanguageModel's one call into app-side state
was AppUtilities.IsPackaged(), a plain forwarder to
PackageIdentity.IsPackaged() (Core.Windows, same Text_Grab.Utilities
namespace) - swapped to call PackageIdentity directly since
AppUtilities itself stays in the app. WinAiTranslator and
WinAiMeetingNotes call no assembly-scoped APIs at all. Both are
internal static classes; Core.Windows's existing
InternalsVisibleTo("Text-Grab") keeps every app call site (GrabFrame,
EditTextWindow, and others) compiling unchanged.

No package references moved - Microsoft.WindowsAppSDK.AI stays in
Text-Grab.csproj too, since OcrUtilities.cs and WindowsAiUtilities.cs
(both still app-side, both on the deferred ledger) use the same
Microsoft.Windows.AI namespaces.

No files deferred from this batch's list.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp
…ide RTL facade

Batch 3d. XmlLanguage comes from PresentationCore (WPF), so it cannot
cross into Text-Grab.Core.Windows, and reconnaissance confirmed the RTL
path is reachable in production from GrabFrame, EditTextWindow and
OcrUtilities - not a dead using.

- Text-Grab.Core.Windows/Extensions/LanguageExtensions.cs (namespace
  Text_Grab, class name kept exactly) - IsSpaceJoining (both the
  Windows.Globalization.Language and ILanguage overloads), IsLatinBased,
  AsLanguage, AsILanguage. Keeping the original type name means every
  existing call site in the app (OcrUtilities, GrabFrame, HistoryInfo,
  LanguageService, PdfDocumentRenderer, ...) resolves unchanged through
  the existing project reference - no call-site edits were needed at
  all, confirmed by a clean Tests.csproj build.
- Text-Grab/Extensions/LanguageRtlExtensions.cs (new file, same
  Text_Grab namespace, new type name) - IsRightToLeft(this Language)
  (the XmlLanguage lookup) and IsRightToLeft(this ILanguage) (the
  GlobalLang branch calls the Language overload, so the two stay
  together). Named distinctly rather than reusing LanguageExtensions:
  since both classes end up in the same Text_Grab namespace once the
  app references Core.Windows, a same-named extension method in both
  would have been a CS0121 ambiguity at every call site instead of one
  clean split.

No package references or settings touched. No files deferred from this
batch's list.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp
BuildTextFromOcrLines is listed in 4c's portable subset but calls IsRightToLeft,
which 3d left in the app with the rest of the XmlLanguage-bound code. Writing the
three resolution options down now so 4c does not rediscover it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 6a of the Core split, pulled ahead of Waves 4-5 because
reconnaissance found it blocks TesseractHelper (4b), ContextMenuUtilities
and FileAssociationUtilities (3a-deferred), and FileUtilities (5).

- Text-Grab.Core/Utilities/AutomationSettingsProvider.cs - pure rename,
  zero content changes, exactly as the plan predicted.
- Text-Grab.Core/Utilities/AutomationProfile.cs - pure rename plus one
  mechanical change: ApplySeed(Properties.Settings) widened to
  ApplySeed(ApplicationSettingsBase), since Properties.Settings is the
  app's internal, concrete ApplicationSettingsBase subclass and cannot
  move (see B1 in the plan). Every direct typed-property write
  (settings.FirstRun = false, etc.) became an indexer write
  (settings["FirstRun"] = false) - behavior-preserving, not just type
  erasure: Settings.Designer.cs generates each property as a thin wrapper
  whose setter is exactly `this["PropertyName"] = value;`, so the indexer
  call is the same call the typed property would have made. The file's
  own actual location was Utilities/, not Models/ as an earlier planning
  note assumed; the namespace (Text_Grab.Utilities) is unchanged either
  way per invariant 1, so it moved to Text-Grab.Core/Utilities/ to match.

- Added System.Configuration.ConfigurationManager 10.0.11 to
  Text-Grab.Core.csproj. Confirmed empirically (not guessed): the app
  itself carries no explicit PackageReference for this package today -
  ApplicationSettingsBase/LocalFileSettingsProvider resolve there for
  free from the Microsoft.WindowsDesktop.App shared framework, which
  plain net10.0 Core does not get. 10.0.11 is nuget.org's latest stable
  10.0.x release and matches the newest installed net10.0 runtime
  (10.0.11); restoring it against the live feed succeeded cleanly.

Self-assembly-reflection audit (the specific failure mode that already
bit GetAppVersion() and the LimitedAccessFeatureUtilities LAF token
earlier in this reorganization): grepped both moved files for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x)
.Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata -
zero matches in either file. The one place this class of bug could still
hide is the LocalFileSettingsProvider base class's own user.config path
resolution (used when AutomationProfile.Current is null), but that
resolution keys off the process's entry assembly, not the declaring
assembly of the SettingsProvider type - moving the provider to a
different assembly does not change what the running Text-Grab.exe
process's entry assembly is, so existing users' user.config is not
orphaned by this move.

No files deferred from this batch's list; both files moved cleanly.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing WinRT.Runtime/Vortice MSB3277 conflict warnings
only, unrelated to this batch).

Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved. Verified specifically that
AutomationProfileTests, AutomationSettingsProviderTests (including the
SettingsService seeding round-trip that exercises the widened ApplySeed)
and SettingsAccessTests are all in the passing set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134QrcpPT91ZegkhjqLGLUp
Batch 4a of the Core split. Both files were already effectively pure
Core.Windows types - ILanguage/OcrEngineKind/OcrOutputKind were already
there or in Core, and Bitmap/SoftwareBitmap (GDI+ / WinRT) are fine in
Core.Windows per B3.

- Models/OcrOutput.cs: CleanOutput() swapped AppUtilities.TextGrabSettings
  (the app's internal Settings type, via an `is not Settings userSettings`
  cast) for SettingsAccess.Current, reading CorrectToLatin/CorrectErrors -
  both already on ITextGrabSettings from Wave 0. No interface changes
  needed.
- Utilities/BarcodeUtilities.cs moved as-is - pure GDI+ Bitmap and ZXing,
  no settings or WPF touchpoints.

Added ZXing.Net.Bindings.Windows.Compatibility to
Text-Grab.Core.Windows.csproj for BarcodeReader/BarcodeWriter. Left
ZXing.Net and ZXing.Net.Bindings.Windows.Compatibility in Text-Grab.csproj
unchanged - verified by grep that QrCodeWindow.xaml.cs and
GrabFrame.xaml.cs still reference ZXing types directly.

Self-assembly-reflection audit: grepped both files for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x)
.Assembly, Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and
AppContext.BaseDirectory - zero matches in either file.

No files deferred from this batch's list; both moved cleanly.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oader

Batch 4b of the Core split. TesseractHelper.cs was blocked only on
AutomationProfile via TempImagePath() - that landed in Text-Grab.Core in
93c1729, so this batch was purely mechanical:

- TesseractHelper: dropped the cached `Settings DefaultSettings` field
  (AppUtilities.TextGrabSettings, the app's internal settings type) and
  read SettingsAccess.Current inline in GetTesseractPath() instead, same
  shape as every other moved settings touchpoint - TesseractPath and
  Save() are already on ITextGrabSettings, no interface change needed.
  AutomationProfile.Current/.GetTemporaryDirectory() in TempImagePath()
  need no using - same namespace (Text_Grab.Utilities), same as the app.
- TesseractGitHubFileDownloader (the second class in the same file) was
  fully portable - HttpClient, File I/O, no Windows types - so it split
  out to Text-Grab.Core/Utilities/TesseractGitHubFileDownloader.cs rather
  than riding along in Core.Windows. Namespace kept as Text_Grab.Utilities
  per invariant 1; its two app call sites (LanguageSettings.xaml.cs) and
  the test file (OcrTests.cs) need no changes since the app already
  references Core.Windows -> Core transitively.

Added CliWrap to Text-Grab.Core.Windows.csproj. Left CliWrap in
Text-Grab.csproj unchanged - QuickSimpleLookup.xaml.cs still uses it
directly.

Self-assembly-reflection audit: grepped both files for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x)
.Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata -
zero matches. TempImagePath()'s System.AppContext.BaseDirectory fallback
(used only when AutomationProfile.Current is null) is process entry-point
based, not declaring-assembly based, so it means the same thing after the
move - same reasoning as the LocalFileSettingsProvider check in 93c1729.

No files deferred from this batch's list; TesseractHelper moved cleanly
and the downloader split cleanly.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 4d of the Core split.

Deleted first, as its own step, after re-verifying zero call sites for
each (the OcrResult property was private, ParseOcrResultWordsIntoRects/
CalculateResultRows/MergeTheseRowIDs were private with no internal
callers, and the ref-parameter constructor was never called - GrabFrame's
`AnalyzedResultTable = new()` and `ResultTable tmp = new()` both use the
parameterless constructor):
- The `OcrResult` property and `ParseOcrResultWordsIntoRects()`
- The `ResultTable(ref List<WordBorderInfo>, DpiScale)` constructor
- `CalculateResultRows` and `MergeTheseRowIDs`

Then split what was left. `WordBorderInfo` (2d) and `GrabFrameTableEditState`
(2b) were already in Core, so the clustering algorithm itself - row/column
center-clustering, manual separator handling, and the pure text-from-cells
serializer - had no remaining blocker. Two things did:

- `BoundingRect` was `System.Windows.Rect` (WindowsBase, not portable).
  Changed to `System.Drawing.RectangleF`, the established B2 currency -
  same conversion WordBorderInfo.BorderRect already made. GrabFrame's one
  external read (`AnalyzedResultTable.BoundingRect`) picked up `.AsRect()`
  at the call site.
- `TableLines`/`DrawTable()` build a WPF Canvas - genuinely app-bound, not
  part of the algorithm. Extracted to a new app-side
  Text-Grab/Utilities/ResultTableRenderer.cs (`BuildTableLines`), called
  from GrabFrame right after `AnalyzeAsTable` instead of internally via a
  `drawTable` bool. That bool and the two `AnalyzeAsTable` overloads that
  carried it are gone (both overloads now just do the analysis); the six
  test/benchmark call sites that passed `drawTable: false` had that
  argument dropped, and GrabFrame's `RemoveTableLines()` still finds the
  canvas by its unchanged "TableLines" Tag string.

`ParseOcrResultIntoWordBorderInfos` did not move - it depends on
`IOcrLinesWords`/`IOcrLine` (Text-Grab.Core.Windows, fine on its own) but
also calls `GetTextFromOcrLine`, an app-only extension method in
OcrUtilities.cs that reads settings and is itself blocked pending Wave 4c
(OcrUtilities.cs as a whole, per the deferred ledger). That's a blocker
outside this batch's file list, so the method moved sideways instead of
up: into OcrUtilities.cs itself, next to GetTextFromOcrLine, with its
unused `DpiScale dpi` parameter dropped (never read in the body - the
`Rect` it built was immediately discarded into a `RectangleF` anyway, so
the method now builds the RectangleF directly). Updated its four call
sites (two in OcrUtilities.cs, two in Tests/OcrTests.cs) and removed the
two now-unused DpiScale locals that fed it.

Self-assembly-reflection audit: grepped every file touched for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x)
.Assembly, Assembly.GetName/.Location/.CodeBase, and AssemblyMetadata -
zero matches.

No files deferred from this batch's list - ResultTable.cs moved and split
in full; nothing added to section 7.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 warnings only).

Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved. No test deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 4c of the Core split, plus the 3d RTL blocker it depended on.

The split went the direction the call-site census pointed rather than the
one the file's size suggests. Tests/OcrTests.cs alone held 43 of
OcrUtilities' ~80 references and every one is against the text-assembly
subset, so that half keeps the OcrUtilities name and moves to
Text-Grab.Core.Windows: GetTextFromOcrLine, FilterFurigana,
FilterFuriganaLines, OrderLinesForReadingFlow, BuildTextFromOcrLines,
ShouldUseParagraphDetection, GroupWrappedParagraphLines, UnionRectangles,
IsWrappedLine, IsWrappedParagraph, GetStringFromOcrOutputs,
ParseOcrResultIntoWordBorderInfos, the nested PositionedOcrLine and
GroupedOcrLines, and the SpaceJoiningWordRegex it all runs on. OcrTests
therefore needed no edit for those, and rides into Tests.Core.Windows for
free in 7a.

The app-coupled half took the new name OcrSourceUtilities - screen and
window capture, engine dispatch, file and BitmapSource sources. Its ~35
references across nine files were renamed. It still needs WPF throughout,
and its engine dispatch needs WindowsAiUtilities and LanguageUtilities,
neither of which has moved.

The moved half's four settings reads (CorrectErrors, CorrectToLatin,
ParagraphDetection, RemoveFurigana) were already on ITextGrabSettings, so
this needed no interface addition - the static DefaultSettings field
became SettingsAccess.Current reads, same as 4a and 4b.

Deferred, as section 4.4 said to: LoadBitmapFromFile builds a WPF
BitmapImage to apply EXIF rotation, so it and its two callers
(OcrAbsoluteFilePathAsync, OcrFile) stay in the app. Section 7 updated.

BuildTextFromOcrLines calls language.IsRightToLeft(), which 3d had left
app-side because XmlLanguage comes from PresentationCore. Settled the
behaviour question with a throwaway WPF probe rather than by reasoning:
XmlLanguage.GetLanguage(tag).GetEquivalentCulture().TextInfo.IsRightToLeft
and CultureInfo.GetCultureInfo(tag).TextInfo.IsRightToLeft agreed on all
24 tags probed - ar, ar-EG, ar-SA, he, he-IL, ur, ur-PK, fa, fa-IR, ckb,
ps-AF, sd-Arab-PK, yi, he-Hebr-IL, ar-XX, en, en-US, ja, zh-Hans, de-DE,
and the unresolvable xx, xx-YY, und and "". So the ILanguage overload
moved into Core.Windows LanguageExtensions with a CultureInfo lookup,
guarded by a CultureNotFoundException catch returning false (XmlLanguage
fell back to the invariant culture, which is LTR). That left the Language
overload with zero call sites - all five live IsRightToLeft calls are on
ILanguage - so Extensions/LanguageRtlExtensions.cs is deleted outright.

Deleted GetBoundingRect(this OcrLine) as section 8 dead code, re-verified
first: WinRtOcrLinesWords has its own private GetBoundingRect and
UIAutomationUtilities calls the unrelated TextPatternRange
GetBoundingRectangles.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in any of the three files.

Also recorded in the plan: 4e cannot precede 5a. LanguageService touches
InputLanguageManager in exactly one private method and is otherwise WinRT,
so the whole class can move behind a registered resolver - but
GetAllLanguages and GetOCRLanguage call WindowsAiUtilities, which is
deferred on SoftwareBitmapExtensions in 5a.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TheJoeFin and others added 24 commits August 29, 2026 23:17
Batch 5a of the Core split. Six move-now files, no splits needed:

- Utilities/Hdr/HdrToneMapper.cs -> Text-Grab.Core (nothing but System).
- Utilities/Hdr/DisplayHdrInfo.cs -> Text-Grab.Core.Windows. Added
  Vortice.Direct3D11 and Vortice.DXGI (3.8.3, matching the app's pinned
  versions) for the IDXGIOutput6 enumeration.
- Extensions/ImageExtensions.cs -> Text-Grab.Core.Windows. Deleted the dead
  ExifRotate first (section 8): re-verified zero call sites by grep - the
  only reference to the name was its own internal call to
  GetRotateFlipType(this Image), which is unrelated and still live via
  ImageMethods.GetRotateFlipType(string).
- Utilities/ImageChangeDetector.cs -> Text-Grab.Core.Windows. Added
  Magick.NET-Q16-AnyCPU and Magick.NET.SystemDrawing (14.16.0 / 8.0.25,
  matching the app).
- Models/DragDataObject.cs -> Text-Grab.Core.Windows. Deleted the dead
  BitmapSourceToBitmap first (section 8): re-verified zero call sites -
  every BitmapSourceToBitmap call in the repo resolves to the live,
  identically-named ImageMethods.BitmapSourceToBitmap, a different method on
  a different type that a bare grep would conflate with this one.
- Extensions/SoftwareBitmapExtensions.cs -> Text-Grab.Core.Windows. Needed
  StorageFileExtensions and WrappingStream (both already in Core.Windows/
  Core from Wave 1), plus one dependency the plan didn't list:
  Microsoft.UI.Xaml.Media.Imaging.SoftwareBitmapSource (ToSourceAsync) comes
  from the WinUI3 projection, which the app pulls in via
  Microsoft.WindowsAppSDK.WinUI. Added that package (2.3.6, matching the
  app) to Core.Windows - the compiler flagged it as CS0234/CS0246, and it's
  a WinAppSDK/WinRT projection, not WPF, so it doesn't touch the UseWPF=false
  boundary TierBoundaryTests enforces.

Self-assembly-reflection audit: grepped all six files for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly,
Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and
AppContext.BaseDirectory - zero matches.

No files deferred from this batch's list; all six moved cleanly.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 5c of the Core split.

Call-site census before deciding the split direction: PadImage (0 external
callers - only used internally by GetRegionOfScreenAsBitmap, which stays),
GetBitmapFromIRandomAccessStream (3: BarcodeUtilitiesTests, OcrSourceUtilities,
PdfDocumentRenderer) and GetRotateFlipType(string) (2: GrabFrame.xaml.cs,
OcrSourceUtilities) total 5 external call sites. Everything else in the file
- BitmapImageToBitmap, BitmapToImageSource, CachedBitmapToBitmapImage,
GetRegionOfScreenAsBitmap, GetWindowsBoundsBitmap, GetWindowBoundsImage,
ScaleBitmapUniform, InteropBitmapToBitmap, BitmapSourceToBitmap,
ImageSourceToBitmap, GetBitmapImageFromIRandomAccessStream, RotateImage - has
on the order of 60 call sites across GrabFrame.xaml.cs, FullscreenGrab,
MagickHelpers, NotifyIconUtilities, OcrSourceUtilities and PdfDocumentRenderer.
Unlike 4c, the census here points the other way: the portable half is the
minority, so it took the new name (Text-Grab.Core.Windows/Utilities/
BitmapUtilities.cs) and the app-bound majority kept ImageMethods.

CaptureScreenRegion did not move, and is recorded as a new row in section 7.
It calls HdrScreenCapture.TryCaptureRegion when HdrCaptureCorrection is set,
and HdrScreenCapture.cs is explicitly out of this batch's scope (batch 5b,
being done separately, needs a settable dispatcher hook first). Core.Windows
cannot reference the app, so CaptureScreenRegion cannot move until
HdrScreenCapture does. Its only two callers (GetRegionOfScreenAsBitmap,
GetWindowsBoundsBitmap) already stay behind for their own reasons (invariant
5's HistoryService caching, and the GrabFrame View pattern-match), so nothing
else was blocked by leaving it. Because CaptureScreenRegion stayed, its
AppUtilities.TextGrabSettings.HdrCaptureCorrection read stayed with it -
HdrCaptureCorrection was deliberately NOT added to ITextGrabSettings, since
nothing that actually moved reads it.

BitmapUtilities.cs keeps ImageMethods' original namespace (Text_Grab, not
Text_Grab.Utilities - the file was always oddly namespaced) per invariant 1.
GetRotateFlipType(string) still calls the GetRotateFlipType(this Image)
extension from Extensions/ImageExtensions.cs, already in Core.Windows since
5a.

Self-assembly-reflection audit: grepped BitmapUtilities.cs for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly,
Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and
AppContext.BaseDirectory - zero matches.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 5d of the Core split.

~330 of the file's 464 lines were a pure CF_HTML table parser with no
clipboard, WPF, WinRT or GDI+ dependency: BuildCfHtmlTable,
WrapHtmlFragmentAsCfHtml, ConvertHtmlToTabSeparated, ExtractHtmlFragment,
ParseHtmlTableToGrid, FindNextFreeColumnRange, ParseHtmlRowCells,
ParseSpanAttribute, CleanHtmlCellContent, and the MaxHtmlTableSpan constant.
Moved as-is to Text-Grab.Core/Utilities/CfHtmlTableUtilities.cs, keeping the
Text_Grab.Utilities namespace. WrapHtmlFragmentAsCfHtml and
ConvertHtmlToTabSeparated stayed internal - Text-Grab.Core already grants
InternalsVisibleTo to Text-Grab and Tests, so both the app's remaining
TryGetHtmlTableAsTabSeparated (now calling
CfHtmlTableUtilities.ConvertHtmlToTabSeparated) and the existing tests kept
working with no visibility change.

The clipboard-touching methods stayed in Text-Grab/Utilities/ClipboardUtilities.cs:
TryGetClipboardText, TryGetImageFromClipboard, GetBase64ClipboardContentAsImageSource,
ClipboardContainsBase64Image, CleanTeamsBase64Image, TryGetHtmlTableAsTabSeparated,
base64ImageExtension.

Separately, as instructed regardless of the split: line 64's
System.Windows.Forms.DataFormats.Bitmap (the file's only WinForms use) was
swapped for System.Windows.DataFormats.Bitmap - the identical string
constant, now sourced from WPF instead of WinForms.

Call sites updated: EditTextWindow.xaml.cs (BuildCfHtmlTable),
Tests/ClipboardUtilitiesTests.cs (BuildCfHtmlTable and
ConvertHtmlToTabSeparated, throughout), Tests/EditTextWindowSpreadsheetTests.cs
(the fully-qualified ConvertHtmlToTabSeparated call). None of these test
files moved to Tests.Core in this batch - that migration is Wave 7a's job,
not this one's.

Self-assembly-reflection audit: grepped both files for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly,
Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and
AppContext.BaseDirectory - zero matches.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ndows

Batch 5e of the Core split.

Call-site census before naming: GetBounds and BuildGeometry each have 3
external callers (Tests/FreeformCaptureUtilitiesTests.cs plus two call sites
in FullscreenGrab.SelectionStyles.cs) and return WPF rendering types (Rect,
PathGeometry), so they stay in the app under the original FreeformCaptureUtilities
name. CreateMaskedBitmap has 2 (the same test file, one call site) - the
minority - so it took a new name, Text-Grab.Core.Windows/Utilities/
BitmapMaskUtilities.cs, same as 5c's direction. Keeping both classes named
FreeformCaptureUtilities in the same Text_Grab.Utilities namespace across two
assemblies the app references would have been a straight CS0433 collision,
not just a style choice.

CreateMaskedBitmap's parameter changed from IReadOnlyList<System.Windows.Point>
to IReadOnlyList<System.Drawing.PointF> - GDI+'s GraphicsPath.AddPolygon
already wanted PointF internally, so the method previously converted
Point->PointF itself; now the caller does. The single call site,
FullscreenGrab.SelectionStyles.cs's CreateFreeformSelectionResult, converts
via the existing ShapeExtensions.AsPointF() (added in the B2 geometry work)
before calling in. FreeformCaptureUtilitiesTests.cs's CreateMaskedBitmap test
updated the same way, constructing PointF literals directly.

Self-assembly-reflection audit: grepped BitmapMaskUtilities.cs for
GetExecutingAssembly/GetEntryAssembly/GetCallingAssembly, typeof(x).Assembly,
Assembly.GetName/.Location/.CodeBase, AssemblyMetadata, and
AppContext.BaseDirectory - zero matches.

No files deferred from this batch's list; the split moved cleanly.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean
(0 errors; pre-existing MSB3277 assembly-conflict warnings only).
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped -
identical to baseline, total 1510 conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to batch 5a. SoftwareBitmapExtensions.ToSourceAsync returns a
Microsoft.UI.Xaml.Media.Imaging.SoftwareBitmapSource, which is why 5a added
a Microsoft.WindowsAppSDK.WinUI PackageReference to Text-Grab.Core.Windows
to get the file to compile there.

The method has zero call sites - anywhere, in any project, including XAML.
The file carries a Microsoft sample header; ToSourceAsync is the stock WinUI
sample helper, copied in with the rest and never used. Text-Grab is a WPF
app and has no WinUI surface for a SoftwareBitmapSource to bind to.

So the package was being added to a Core project purely to satisfy dead
code, and at a version (2.3.6) that did not even match the WindowsAppSDK.AI
reference already there (2.4.4). Deleted the method, its using, and the
PackageReference.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 5b of the Core split, plus 5c's deferred CaptureScreenRegion, which
this unblocks.

Two blockers, as section 4.5 predicted.

HdrBorderlessGranted and HdrCaptureCorrection joined ITextGrabSettings.
Both already existed in Settings.settings, so the generated partial
satisfies them with no .settings edit and no forwarding.

The Application.Current.Dispatcher hop became Text-Grab.Core/Services/
UiThreadAccess.cs, the same delegate-resolver shape as SettingsAccess and
for the same reason - Dispatcher lives in WindowsBase, which Core cannot
see. The app registers a poster from a [ModuleInitializer], mirroring
SettingsAccessInitializer, so the Tests host is covered without an
App.appStartup call.

A delegate rather than a stored dispatcher matters here: Application.Current
is null at module-load time and only becomes non-null once WPF starts, so
the poster resolves it on each call. That preserves the original late
binding exactly. TryPost returning false is the old `dispatcher is null`
branch - nothing to post to, nothing happens - and _borderlessRequestStarted
is still set before the post either way, so a process with no UI thread does
not re-fire the consent request on every capture.

With HdrScreenCapture in Core.Windows, CaptureScreenRegion could follow. It
moved into BitmapUtilities as internal rather than public: its only two
callers, GetRegionOfScreenAsBitmap and GetWindowsBoundsBitmap, stay in the
app - the first writes to HistoryService, the second pattern-matches on the
GrabFrame view - and Core.Windows already grants InternalsVisibleTo to
Text-Grab. Its section 7 row is removed.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in any file touched.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 4e of the Core split, run after wave 5 rather than inside wave 4 -
the reason is recorded in section 4.4.

WindowsAiUtilities went first, because LanguageService calls it. All three
of its deferred blockers were gone: AutomationProfile landed in Core in 6a,
SoftwareBitmapExtensions in 5a, and OverrideAiArchCheck is added to
ITextGrabSettings here (it already existed in Settings.settings). It read
that one through Settings.Default rather than AppUtilities.TextGrabSettings,
which is the same object in production - SettingsService.ClassicSettings is
constructed from Properties.Settings.Default - so routing it through
SettingsAccess is identical at runtime and consistent with every other
settings read in the moved code. Its remaining app call,
AppUtilities.IsPackaged(), is a one-line forwarder to
PackageIdentity.IsPackaged(), which has been in Core.Windows since 1b.

The language chain then moved unsplit - LanguageService, LanguageUtilities
and CaptureLanguageUtilities, all keeping their names - so none of their 155
call sites needed an edit. UiAutomationEnabled and WindowsAiDescriptionEnabled
joined ITextGrabSettings as the consolidated table predicted. Singleton<T>
was already in Core.

The InputLanguageManager blocker became Text-Grab.Core/Services/
InputLanguageAccess.cs, the third use of the delegate-resolver shape after
SettingsAccess and UiThreadAccess. The NullReferenceException catch that
guarded the read stayed on the app side, inside the registered resolver -
that is the only side that knows InputLanguageManager exists, and the
manager throws it from its own internals in some hosts. A null tag, whether
from no resolver or no input language, still falls through to
CultureInfo.CurrentUICulture and then to en-US, exactly as before.

The plan proposed extracting the four pure switch helpers and leaving the
input-language reader behind. That turned out to be unnecessary: the reader
was the only thing in the class that was not already portable.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in any of the four moved files.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 6b of the Core split. WindowsSpeechEngine moved to Core.Windows
(WinRT SpeechSynthesizer/MediaPlayer); TtsService moved to plain Core,
keeping their names.

TtsService's blocker was a field initializer -
`private ITtsEngine _engine = new WindowsSpeechEngine();` - naming a type
that now lives one tier down. Resolved with Text-Grab.Core/Services/
TtsEngineAccess.cs, the fourth delegate-resolver shape after SettingsAccess,
UiThreadAccess and InputLanguageAccess: it holds a Func<ITtsEngine> factory,
and TtsService's constructor calls TtsEngineAccess.CreateDefault() as its
first statement, so the engine is still built at the exact moment it always
was - when a TtsService is constructed, not lazily on first Speak. The app
registers `static () => new WindowsSpeechEngine()` from a
[ModuleInitializer] (TtsEngineAccessInitializer.cs), covering the Tests
host the same way SettingsAccessInitializer does. An unregistered resolver
throws InvalidOperationException, matching SettingsAccess's behaviour -
unreachable in production, since the module initializer always covers it.

TtsSpeakWordLimit, TtsVoiceName and TtsSpeakingRate joined
ITextGrabSettings; all three already existed in Settings.settings, so no
.settings edit was needed. WindowsSpeechEngine's two settings reads and
TtsService's one moved from Properties.Settings.Default to
SettingsAccess.Current.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in any file touched.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 6c of the Core split. 1115 lines, fully headless NAudio + Whisper.net
(local Whisper transcription, live capture, VAD-gated streaming) with zero
WPF/WinRT dependencies, moved unsplit to Core.Windows keeping its name and
namespace.

Its one settings touchpoint - CurrentModelChoice reading
AudioTranscriptionModel - switched from AppUtilities.TextGrabSettings to
SettingsAccess.Current. AudioTranscriptionModel joined ITextGrabSettings;
it already existed in Settings.settings, so no .settings edit was needed.
Verified this is the file's only settings read rather than trusting the
plan's claim.

Unlike the ZXing/CliWrap/Magick.NET precedent - where the app keeps a
package because app code still calls those types directly - a repo-wide
grep found no remaining app-side use of any NAudio or Whisper.net type, so
NAudio, Whisper.net and Whisper.net.Runtime moved to
Text-Grab.Core.Windows.csproj outright instead of staying duplicated in
Text-Grab.csproj. The two consumers, EditTextWindow.xaml.cs and
OpenMediaWindow.xaml.cs, only call AudioTranscriptionUtilities/
LiveAudioTranscriber members and needed no changes.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in the moved file.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Batch 6d of the Core split, the PatternItem/PatternItemCatalog shape from
e677b54.

The settings coupling turned out to be larger than "static accessors": it
was on the instance members DefaultSearcher/WebSearchers (and their private
backing fields), read through Singleton<WebSearchUrlModel>.Instance at
every call site. The three static helpers - GetWebSearchUrls,
SaveWebSearchUrls, GetDefaultWebSearchUrls - had zero external call sites;
they existed only to back those instance properties. So the whole
settings-touching unit moved together into a new app-side
Text-Grab/Models/WebSearchUrlCatalog.cs, unchanged apart from the name.
Text-Grab.Core/Models/WebSearchUrlModel.cs kept only Name, Url and
ToString().

Call-site census: 12 references use WebSearchUrlModel purely as a data
type (List<WebSearchUrlModel>, foreach, pattern matches, construction) and
needed no edit, since the namespace didn't change. 6 references were
Singleton<WebSearchUrlModel>.Instance.{DefaultSearcher,WebSearchers} across
GeneralSettings.xaml.cs, PostGrabActionManager.cs and
EditTextWindow.xaml.cs, updated to Singleton<WebSearchUrlCatalog>. The
data half's majority is why the original name stayed with it.

No ITextGrabSettings changes - the settings-touching code stayed entirely
on the app side of the seam.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in either half.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… stale rows

Re-read every section 7 row against the current tree. Three rows named
blockers that had since landed:

- Utilities/FileUtilities.cs (blocked on AutomationProfile.Current, which
  landed in Core in 6a).
- Utilities/ContextMenuUtilities.cs (blocked on AutomationProfile.Current,
  FileUtilities.GetExePath(), and the IoUtilities split - all resolved).
- Utilities/FileAssociationUtilities.cs (blocked on FileUtilities.GetExePath()).

ContextMenuUtilities.cs moved unsplit to Core.Windows.

FileUtilities.cs needed one split: GetOpenDocumentFilter() also calls
GrabFrameFileUtilities (.GrabFrameFileExtension, .GetGrabFrameFileFilter()),
which stays app-side - blocked on HistoryInfo per its own section 7 row,
untouched here since Services/HistoryService.cs is explicitly out of scope
for this sweep. Everything else in FileUtilities (12 other members, a
dozen-plus call sites across the app) moved to Core.Windows keeping the
name. GetOpenDocumentFilter() alone (3 call sites: App.xaml.cs,
EditTextWindow.xaml.cs, one test) moved into a new app-side
Text-Grab/Utilities/OpenDocumentFilterUtilities.cs, calling back into two
of FileUtilities's helpers (GetVisualDocumentFilterPattern,
GetExtensionsFilterPattern) widened from private to internal for exactly
that caller. AppUtilities.IsPackaged() calls in FileUtilities became
PackageIdentity.IsPackaged() - the established 4e substitution, since
AppUtilities lives in the app and Core.Windows cannot see it.

FileAssociationUtilities.cs did not move: its GrabFrameExtensionKeyPath
constant references GrabFrameFileUtilities.GrabFrameFileExtension
directly - the same HistoryInfo blocker one level removed, not previously
recorded. Its section 7 row was rewritten with the real blocker rather
than forcing the move.

Utilities/TesseractHelper.cs's row was stale - it moved to Core.Windows in
batch 4b and the row was simply never removed - deleted.

Self-assembly-reflection audit: no GetExecutingAssembly/GetEntryAssembly/
GetCallingAssembly, typeof(x).Assembly, Assembly.GetName/.Location/
.CodeBase, AssemblyMetadata, AppContext.BaseDirectory or manifest-resource
lookups in either moved file. FileUtilities.GetExePath() already used
Environment.ProcessPath rather than assembly-location reflection, so it
needed no change for this.

Gate: dotnet build Tests/Tests.csproj -c Debug -p:Platform=x64 - clean.
Tests.Core 514, Tests.Core.Windows 3, Tests 993 passed/7 skipped - 1510
total, conserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HistoryInfo.PositionRect was the last System.Windows.Rect standing between the
history model and Core.Windows, and it is what blocked 6e (the HistoryService
split) and the GrabFrameFileUtilities row in section 7.

PositionRect was never a stored field - it is a projection over the persisted
RectAsString - so B2's currency change costs nothing on disk. It is now a
System.Drawing.RectangleF with hand-rolled parse/format helpers that keep the
"x,y,width,height" text Rect.ToString() used to write, plus the literal "Empty".
Writing is invariant-culture now, and reading tolerates the ';' separator and
comma decimals Rect.ToString() emitted under cultures whose decimal separator is
',' - strings the old invariant-only Rect.Parse threw on rather than read.

Call sites convert at the edge through ShapeExtensions (AsRect/AsRectangleF), the
established B2 pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
HistoryService was a headless JSON pipeline tangled up with a WPF service. The
pipeline half is now Text-Grab.Core.Windows/Utilities/HistoryFileUtilities.cs -
loading and writing the two history files, item-by-item recovery when one is
corrupt, the LanguageKind converter and its AsyncLocal rewrite flag, the
word-border sidecar files, the two normalization passes, retention, and artifact
deletion. All static, no state past the serializer options.

HistoryService keeps its name (every call site is Singleton<HistoryService>) and
keeps what actually held it in the app: the in-memory lists, the two
DispatcherTimers that debounce writes and release the idle cache, the cached
fullscreen bitmap, the recent-grabs MenuItem building, and the SaveToHistory
overloads that take a GrabFrame and an EditTextWindow.

Two behaviour-preserving details worth naming:

- NormalizeHistoryIds used to call MarkHistoryDirty itself; it now returns a bool
  so it can be static. Its callers evaluate it and NormalizeHistoryCompatibilityData
  into locals first - both normalizers mutate, so neither may be short-circuited
  away by the other, which || would have done.
- GetWordBorderInfosAsync stays on the service as a wrapper, because the
  TouchHistoryCache() it starts with is cache bookkeeping, not file work.

PersistWordBorderData reads EnableFileBackedManagedSettings, so that joins
ITextGrabSettings. GetMostRecentGrab and GetExcessVisualHistoryItems moved with
the pipeline, so HistoryServiceTests calls them on the new type.

Also strips BOMs this session's tooling added to five files that never had one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Documents what 6e actually did rather than what it forecast, including the two
behaviour-preserving details (NormalizeHistoryIds returning a bool instead of
calling MarkHistoryDirty, and why GetWordBorderInfosAsync kept a wrapper).

HistoryInfo landing in Core.Windows also removes the root blocker under the
GrabFrameFileUtilities and FileAssociationUtilities rows, so both are rewritten
rather than left claiming a blocker that no longer exists. The HistoryService row
is gone - resolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Straightforward moves for batch 7a - no production code, no test code, and no
csproj changed besides the namespace line (Tests -> Text_Grab.Tests.Core). Each
suite exercises only types already living in Text-Grab.Core: StringMethodTests,
TextSearchUtilitiesTests, ColumnSplitUtilitiesTests, SpreadsheetUndoHistoryTests,
EditTextTableDocumentTests, GrabFrameTableEditStateTests, ExtractedPatternTests.

Verified each file's usings against invariant 8 before moving - none touch
SettingsAccess, a [Collection] fixture, or any Windows-only type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
LanguageTests, WindowsAiUtilitiesTests, QrCodeTests, HdrScreenCaptureTests and
ImageChangeDetectorTests move unsplit - each exercises only types already in
Text-Grab.Core.Windows (TessLang/GlobalLang/WindowsAiLang,
WindowsAiUtilities.CleanRegexResult, BarcodeUtilities' SVG path,
HdrScreenCapture.BuildCaptureSegments, ImageChangeDetector) with no settings,
collection fixture, or WPF touch. Namespace changed to
Text_Grab.Tests.Core.Windows; no other code changed.

Tests.Core.Windows.csproj gained two asset items: FontTest.png and
font_sample.png stay physically in Tests\Images (Tests\ImageMethodsTests.cs,
Tests\OcrSourceTests.cs and Tests\FilesIoTests.cs still read them there) but
are linked in here for ImageChangeDetectorTests, which reads them by the same
relative path FileUtilities.GetPathToLocalFile resolves at either project's
output directory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Five suites turned out to be mixed on closer read (invariant 8's warning about
trusting a using-directive census applies to tests too) - each pure half moved
under the naming rule from e677b54: the half with more test methods keeps the
original name, the smaller half takes a new one.

RecognizerExecutorTests (Tests.Core, kept name, 23 methods) vs the
GrabTemplateExecutor-backed recognizer-placeholder tests (7 methods) - merged
into the existing Tests/GrabTemplateExecutorTests.cs instead of a new class,
since GrabTemplateExecutor needs System.Windows.Rect and that file already
covers it comprehensively.

PatternExecutorTests (Tests.Core, kept name, 10 methods) vs
PatternItemCatalogTests (Tests, new, 3 methods) - PatternItemCatalog is the
app-side half of the PatternItem split from e677b54.

ProtocolUtilitiesTests (Tests.Core, kept name, 9 methods) vs
ProtocolHandlerUtilitiesTests (Tests, new, 6 methods) - ProtocolHandlerUtilities
is internal and app-side (path validation against the filesystem).

BarcodeUtilitiesTests (Tests.Core.Windows, kept name, 3 methods) vs
BarcodeUtilitiesImageTests (Tests, new, 1 method) - the one method is
[WpfFact]-tagged; Xunit.StaFact cannot be referenced from Tests.Core.Windows,
confirmed empirically by loading its net8.0-windows7.0 assembly and checking
GetReferencedAssemblies() - it names WindowsBase and System.Windows.Forms,
which TierBoundaryTests bans.

FilesIoTests (Tests, kept name, 9 methods stay app/WPF-bound) split two ways:
IoUtilitiesTests (Tests.Core, new, 3 methods against plain-Core IoUtilities)
and FileUtilitiesTests (Tests.Core.Windows, new, 1 method against
FileUtilities.GetVisualDocumentFilter).

OcrTests is the big one (27 methods vs 15). The pure half kept the OcrTests
name and moved to Tests.Core.Windows; the app-coupled half - OcrSourceUtilities
calls, WPF BitmapImage construction, direct AppUtilities.TextGrabSettings reads
- became Tests/OcrSourceTests.cs. Four methods (OcrComplexTableTestImage,
GetTessLanguages, GetTesseractStrongLanguages, GetTesseractGitHubLanguage) were
tagged [WpfFact] in the original but never touch a WPF type once OcrUtilities/
TesseractHelper/TesseractGitHubFileDownloader had already moved to
Core/Core.Windows in earlier waves; they moved as [Fact]/[Fact(Skip=...)] with
no behavior change; [Fact(Skip=...)] tests still compile-check but skip their
body, so this only mattered for OcrComplexTableTestImage, which now runs.

Two more things came out of reading OcrUtilities in full rather than trusting
the method-body census: BuildTextFromOcrLines reads SettingsAccess.Current
unconditionally as the first operand of a short-circuited && (so the four
BuildTextFromOcrLines_* tests need a resolver even though their assertions
don't obviously depend on a setting value), and this project's own namespace
ending in ".Windows" makes an unqualified "Windows.Foundation.Rect" resolve
against itself and fail to compile - fixed with an explicit
"using Windows.Foundation;" and dropping the qualification.

Tests.Core.Windows/FakeTextGrabSettings.cs is the resolver those four tests
need: a plain ITextGrabSettings POCO seeded from Settings.settings's shipped
defaults (RemoveFurigana=true matters concretely - one test's expected output
depends on furigana actually being filtered), registered via
[ModuleInitializer] the same way the app's SettingsAccessInitializer does.

Table-Complex-WordBorders.json moved with OcrComplexTableTestImage since
nothing else reads it; Table-Complex.png stayed put (and unused - dead even
before this batch, per its own unreferenced private const).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
It was placeholder scaffolding proving Tests.Core could build and run against
Text-Grab.Core before any real suite had moved. That's now true many times
over. Baseline total drops from 1517 to 1516 - the only legitimate change to
the total this batch makes; every other count shift is tests moving between
projects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Tests.Core/ProtocolUtilitiesTests.cs came out of the split with LF endings; every
source file in this repo is CRLF, and a whole-file ending flip is the one kind of
damage git diff will not show you.

The four [WpfFact] -> [Fact] rewrites in Tests.Core.Windows/OcrTests.cs dropped
the blank line that separated each attribute from the method above it.

Both are cosmetic. The attribute conversions themselves check out:
OcrComplexTableTestImage reads a JSON sidecar, deserializes WordBorderInfo and
drives ResultTable over a System.Drawing.Rectangle - no WPF anywhere - and the
three Tesseract methods are Skip'd regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Tests.csproj carried a `Content Update="Images\Table-Complex.png"` with no
matching `Content Include` for it anywhere in the project - the image and the
private const that pointed at it were both dead before batch 7a even ran.
7a's own commit message (5585199) already recorded this when it moved the
Table-Complex-WordBorders.json companion to Tests.Core.Windows: "Table-Complex.png
stayed put (and unused - dead even before this batch, per its own unreferenced
private const)". This batch verifies that independently (git log -S confirms no
test has referenced the .png since OcrComplexTableTestImage switched to reading
the .json word-border fixture instead) and removes both the orphaned csproj item
and the 1.4 MB PNG itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
…Windows

Section 7 had both files marked "unblocked by a8591aa, never audited past
that blocker" - HistoryInfo (their root pin to the app) moved to Core.Windows
in that commit, but nobody had re-read either file since to check for a
second blocker. This batch is that audit, and the answer is: no second
blocker, both move clean.

GrabFrameFileUtilities.cs needed zero code changes - it already traffics
entirely in System.Drawing.Bitmap (Core.Windows-legal per B3),
System.IO/System.IO.Compression, System.Text.Json, and HistoryInfo/
WordBorderInfo, both already in Core.Windows. Its only non-BCL call
(AutomationProfile.GetTemporaryDirectory()) has been internal-and-Core since
6a, and Core.Windows already has InternalsVisibleTo from Core for exactly
this kind of call.

FileAssociationUtilities.cs needed one substitution, the same one 4e/6a
established for this exact pattern: AppUtilities.IsPackaged() ->
PackageIdentity.IsPackaged(). Everything else (Microsoft.Win32.Registry,
FileUtilities.GetExePath(), GrabFrameFileUtilities.GrabFrameFileExtension)
was already Core.Windows-legal. Text-Grab.csproj's InternalsVisibleTo grant
to itself is irrelevant now that the class lives in Core.Windows, which
already grants InternalsVisibleTo("Text-Grab"), so the app's one call site
(App.xaml.cs) needed no change.

That move also retired Text-Grab/Utilities/OpenDocumentFilterUtilities.cs,
which existed for exactly one reason - GetOpenDocumentFilter() needed
GrabFrameFileUtilities, which needed to stay app-side. With that gone,
GetOpenDocumentFilter() folded back into FileUtilities.GetOpenDocumentFilter()
in Core.Windows (its two now-unused-elsewhere helpers, GetExtensionsFilterPattern
and GetVisualDocumentFilterPattern, went back to private), and the two call
sites (App.xaml.cs, EditTextWindow.xaml.cs) call FileUtilities directly.

Tests followed the production code: GrabFrameFileTests.cs (13 test cases,
all already plain [Fact]/[Theory] with no WPF dependency - its `using
System.Windows;` was unused even before this move) moved wholesale to
Tests.Core.Windows. GetOpenDocumentFilter_IncludesVisualAndTextOptions moved
from Tests/FilesIoTests.cs into Tests.Core.Windows/FileUtilitiesTests.cs
alongside the other FileUtilities filter test. Net effect on the gate: Tests
475 (was 489), Tests.Core.Windows 158 (was 144), same 14 cases moved across,
total still 1516.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Two things had gone stale in docs/Core-Split-Plan.md since B2 (geometry
currency) landed and since a8591aa moved HistoryInfo out of the app: the
never-move list was written before either event, and section 7 carried rows
whose blockers needed re-verifying against the current tree rather than
whatever batch first wrote them down.

Section 7: resolves the FileAssociationUtilities.cs and GrabFrameFileUtilities.cs
rows (both moved in 3f4b222, recorded here with what the move actually needed).
Re-verified the other five rows by reading each file again - all five keep the
blocker they were already recorded with (OcrSourceUtilities's BitmapImage,
SettingsService's ButtonInfo/ShortcutKeySet clone, GrabTemplateManager/Executor's
split-plus-facade need, PdfDocumentRenderer's BitmapSource return). Adds two new
rows this sweep found: WindowSelectionUtilities/WindowSelectionCandidate and
GrabFrameViewScaleUtilities were sitting on the never-move list without ever
being checked against B2 - both turn out to be pure Rect/Point/Size math with no
other WPF coupling, the same shape B2 already solved for WordBorderInfo and
TemplateRegion. Not moving them here (invariant 5 - the conversion surface at
their call sites is real work, not a leaf), just recording that the blocker is
gone and what is left to do.

Never-move list: every remaining entry was re-read and annotated with the
actual type it is blocked on, rather than left as a bare filename. The three
UI-Automation overlay models (UiAutomationOptions/Item/Snapshot) turned out to
be pure Rect/Point data too, same as the two files above, but they stay on the
list anyway - their only consumers are UIAutomationUtilities (a real B4
blocker: System.Windows.Automation) and the views directly, so moving three
data models would free nothing. WindowSelectionCandidate is different: its
consumer, WindowSelectionUtilities, has no such second blocker, so it moved to
section 7 instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Two closeout items from the 7b brief that were still missing from the plan
itself, not just from commit messages.

Section 4.7 gains a "7a as executed" table: which of the 12 suites 7a left
in Tests, and the specific reason each one stayed - Settings.Default reads,
HistoryService being app-side by design, a fake that would need two engines,
a collection fixture, WPF types, or a production file that has not moved yet.
Two of those reasons (GrabFrameViewScaleUtilitiesTests, WindowSelectionUtilitiesTests)
already needed updating in the same edit, since the companion commit
(688f100) just found their production types are not actually never-move -
worth stating plainly rather than leaving a stale "never" in a document
someone will read after this branch is old. Also documents
Tests.Core.Windows/FakeTextGrabSettings.cs as the template for any future
Core-tier test that needs settings with no app assembly to fall back on -
it was a real design decision, not a one-off, and belongs in the plan.

A "7b as executed" paragraph closes out the batch: what shim removal and the
ledger re-derivation actually did, and where the final layer map lives.

Section 9 (definition of done) gets an honest pass/fail against 7b, with an
explicit callout that the MSIX wapproj build remains unverified - it needs a
full Visual Studio install, which nothing in this chain of agents has had
access to at any point.

New section 10 (final layer map) is the piece written for someone with no
memory of this conversion: what each of the three tiers holds and why, the
four delegate-resolver seams (SettingsAccess, UiThreadAccess,
InputLanguageAccess, TtsEngineAccess) and how each is registered via
[ModuleInitializer], the ITextGrabSettings surface (20 members, the rule
that kept it from sprawling), where TierBoundaryTests.cs enforces the tier
boundary and what it actually checks, and a grouped, honest accounting of
what stayed in the app and the real reason each group is there - not a
single "not done yet."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
Tests.Core.Windows was added with every platform-specific configuration
pointing at Debug|Any CPU / Release|Any CPU, but the project declares
Platforms x64;x86;ARM64 and has no Any CPU configuration, so Visual
Studio flagged the mappings on every solution load. Map them
platform-for-platform, matching how Tests (same Platforms set) is
already wired. The four Any CPU rows stay as-is, also matching Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2kHsUmBkEoSNR3YDJiHLv
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.

1 participant