Proj structure - #681
Open
TheJoeFin wants to merge 54 commits into
Open
Conversation
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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
organize pure logic out into a .core project and windows related features into a .windows project