Files
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

440 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Legacy: WPF Era of Azaion (`annotations` predecessor)
> **Source of truth for this doc:** `suite/annotations-research/` — a clone of
> `suite/annotations` checked out at commit `22529c2 "Revert add MediaFile"`
> (Mon Nov 17 2025), which is the LAST commit before the **big refactoring**
> (`e7ea5a8`) that started decoupling the WPF UI from the backend in
> preparation for the WPF→.NET API conversion (`9e7dc29` /
> `fbbe556 refactor .net project to API`).
>
> This document captures the system as it existed when the **annotation tool,
> the inference engine, the loader, and the UI were a single Windows desktop
> application** with .NET WPF in front and Cython sidecar processes behind it,
> all running on one machine.
>
> The current `azaion/ui` repo is the **React rewrite of the front-end half**
> of that legacy stack. The Cython parts and the .NET service code became
> separate suite submodules (`detections/`, `loader/`, `annotations/` (now
> .NET API only), `flights/`, etc.). This file exists so future maintainers
> can understand where features in the React UI come from, why some shapes
> in the data look the way they do, and what is intentionally NOT being
> ported.
---
## 1. Top-level layout (`Azaion.Suite.sln`)
The legacy repository was a single Visual Studio solution containing eight
.NET projects plus two Cython projects:
| # | Project | Type | Role |
|---|----------------------|---------------------|--------------------------------------------------------------|
| 1 | `Azaion.Suite` | .NET 8 WPF (exe) | Application host. DI container, config, module registry, key handler. |
| 2 | `Azaion.LoaderUI` | .NET 8 WPF (exe) | Login screen. Launches `Azaion.Suite.exe` with encrypted creds. |
| 3 | `Azaion.Annotator` | .NET 8 WPF (lib) | Main annotation window: video/image canvas, bounding boxes, AI detect. |
| 4 | `Azaion.Dataset` | .NET 8 WPF (lib) | Dataset Explorer window: thumbnail grid, class distribution, validation. |
| 5 | `Azaion.Common` | .NET 8 (lib) | **Tangled core**: WPF user controls + LinqToDB models + RabbitMQ + HTTP + DTOs all in one assembly. |
| 6 | `Azaion.CommonSecurity` | .NET 8 (lib) | AES helpers. Credentials persisted to disk encrypted. |
| 7 | `Azaion.Test` | .NET 8 test project | Unit tests for utilities (intervals, throttle, parallel, tile processing). |
| 8 | `Dummy` | placeholder dir | empty. |
| C1 | `Azaion.Inference` | Cython (Python) | YOLO inference (ONNX / TensorRT). Separate process, ZeroMQ link to .NET. |
| C2 | `Azaion.Loader` | Cython (Python) | Encrypted resource fetcher + hardware fingerprinting. Separate process, ZeroMQ link to .NET. |
There was no internet-facing API. Everything ran on **one Windows machine**
(operator laptop / OrangePi / Jetson).
```
+----------------------------- one Windows host -----------------------------+
| |
| Azaion.LoaderUI.exe |
| | |
| | encrypts creds -> spawns Azaion.Suite.exe -c <encrypted> |
| v |
| Azaion.Suite.exe (WPF) |
| | |
| | Microsoft.Extensions.Hosting + DI |
| | - registers Azaion.Annotator, Azaion.Dataset windows |
| | - registers Annotation/Gallery/Inference/GpsMatcher Services |
| | - registers AzaionApi (HttpClient -> remote installer/aux APIs) |
| | - registers LoaderClient + InferenceClient + GpsMatcherClient |
| | (all are ZeroMQ DealerSocket clients) |
| | |
| |--- LinqToDB --------------> SQLite file (annotations.db) |
| |--- ZeroMQ Dealer ---------> Azaion.Loader.exe (Cython) |
| | | |
| | +---> downloads encrypted resources |
| | from remote API |
| | |
| |--- ZeroMQ Dealer ---------> Azaion.Inference.exe (Cython) |
| | | |
| | +---> ONNX / TensorRT inference |
| | |
| +--- RabbitMQ.Stream client -> remote RabbitMQ (annotation sync) |
| |
+----------------------------------------------------------------------------+
```
## 2. Boot sequence
1. User runs `Azaion.LoaderUI.exe` (the launcher / login window).
2. `Login.LoginClick` → calls `IAzaionApi.Login` (HTTP) for installer-version
check, then spawns the **external** `Azaion.Loader` Cython process and
talks to it over ZeroMQ (`tcp://127.0.0.1:<port>`):
- `CommandType.Login` → loader stores credentials and a hardware-derived
key (`hardware_service.pyx`).
- `CommandType.CheckResource` → loader verifies it can decrypt the cached
encrypted resource bundle.
3. `Login` AES-encrypts `ApiCredentials` (`Azaion.CommonSecurity`) and starts
`Azaion.Suite.exe -c <encrypted>` then closes itself.
4. `Azaion.Suite.App.Start(creds)`:
- Builds a Serilog logger.
- Builds `IConfiguration` from three JSON streams: a local
`config.json`, plus `config.system.json` and `config.secured.json`
fetched from disk via `LoaderClient.LoadFile(...)` (the Cython loader
decrypts them on the fly).
- Configures the DI container (`Microsoft.Extensions.Hosting`):
- `IConfigUpdater`, `Annotator`, `DatasetExplorer`, `HelpWindow`,
`MainSuite`
- `IDbFactory`, `IAnnotationService`, `FailsafeAnnotationsProducer`,
`IGalleryService`
- `IInferenceClient`/`IInferenceService` (ZMQ → Cython inference)
- `IGpsMatcherClient`/`IGpsMatcherService` (ZMQ → GPS matcher service)
- `ISatelliteDownloader`
- `IAzaionApi` (HTTP client to remote API for installer + assets)
- `IAzaionModule` registrations (`AnnotatorModule`, `DatasetExplorerModule`)
- MediatR with assemblies from Annotator, DatasetExplorer, Common.
- Calls `Annotation.Init(directoriesConfig, detectionClassesDict)`
populates **static** state on the `Annotation` entity so that LinqToDB
hydrated rows know how to compute `ImagePath` / `LabelPath` / `ThumbPath`
/ `Colors` / `ClassName`. (This static coupling is exactly what the
`e7ea5a8` "big refactoring" set out to remove.)
- Hooks a global preview-key handler that publishes a MediatR `KeyEvent`
(with throttle) for any keyboard input that is not in a `TextBox`.
- Shows `MainSuite` (the module switcher window).
## 3. Module system
`Azaion.Suite.MainSuite` is a small chrome window with a left-hand
`ListView` of modules. Each module implements:
```csharp
public interface IAzaionModule
{
string Name { get; } // localized display name
string SvgIcon { get; } // inline SVG markup
Type MainWindowType { get; } // WPF Window subclass
WindowEnum WindowEnum { get; } // identifier
}
```
Two implementations existed at this commit:
- `AnnotatorModule``Azaion.Annotator.Annotator`
- `DatasetExplorerModule``Azaion.Dataset.DatasetExplorer`
`MainSuite` shows the icon, opens the corresponding `Window` from the DI
container, and tracks open windows in a `Dictionary<WindowEnum, Window>` so
clicking the same module twice activates instead of recreating.
The `IAzaionModule` extension point is the seed of what became, in the
post-refactor world, the **left-hand top-level navigation in the React SPA**:
Flights, Annotations, Dataset, Admin, Settings.
## 4. The Annotator window (`Azaion.Annotator.Annotator`)
The annotation surface (`Annotator.xaml.cs`, ~600 lines) is the heaviest
part of the legacy app. It owned, all in one window:
- **Video/image playback**: `LibVLCSharp` `MediaPlayer` for video, image
decoding for stills.
- **Canvas editor**: a custom WPF `Canvas` with `CanvasEditor` from
`Azaion.Common.Controls` for click-and-drag bounding boxes, 8-handle
resize, multi-select with Ctrl, zoom with Ctrl+wheel, pan with Ctrl+drag,
crosshair cursor with active-class hint.
- **Time-windowed annotation overlay** during video playback: an
`IntervalTree<TimeSpan, Annotation>` keyed by
`[Time - 50ms, Time + 150ms]`; on each VLC position update, all
overlapping intervals render and the rest clear.
- **Detection class strip** (`Azaion.Common.Controls.DetectionClasses`):
data grid of class colour + number + name, with PhotoMode switcher
(Regular=0, Winter=20, Night=40); class number pressed via keyboard (19);
class colour mixed into the bounding-box label.
- **Annotation list** (right sidebar): `DataGrid` over the in-process
`IntervalTree`, gradient-coloured by detection class, double-click seeks
the video and zooms.
- **Frame-by-frame controls**: 1, 5, 10, 30, 60-frame stepping computed
from the video's FPS; play/pause/stop; mute and volume.
- **AI Detect** (`R` key or button): spawns the Cython inference process via
`IInferenceClient` (ZMQ) and streams progress into a modal
`AutodetectDialog`.
- **Camera config side panel** (`Azaion.Common.Controls.CameraConfigControl`):
altitude / focal length / sensor width — used to compute GSD-based bounds
for valid detection sizes.
- **GPS panel** (`Azaion.Annotator.Controls.MapMatcher`): toggleable below
the canvas; ties into `IGpsMatcherClient` which talks to a separate
GPS-denied positioning Cython process.
- **Help window** (`HelpWindow.xaml` + `HelpTexts.cs`): annotation quality
guidelines.
- **Ukrainian / English localisation** via `translations.json`.
Everything in this list is owned by the same `Annotator` partial class. There
is no view-model boundary; XAML code-behind directly:
- queries `IDbFactory` for `AnnotationsDb`, then runs LinqToDB queries
against the `Annotation`, `Detection`, `MediaFile`, `AnnotationQueueRecord`
tables;
- mutates the canvas, the data grid, the VLC media player, and the GPS panel;
- publishes MediatR notifications (`AnnotationCreatedEvent`,
`AnnotationsDeletedEvent`, `KeyEvent`, `SetStatusTextEvent`,
`AnnotatorControlEvent`, `LoadErrorEvent`) which downstream services like
`AnnotationService.OnAnnotationCreated` react to (e.g. to enqueue a sync
message into the local SQLite buffer table for later RabbitMQ publish).
This is the central tangle. The same class talks to the database, the
network (RabbitMQ via mediator), the inference process, the file system,
and the WPF visual tree.
## 5. The Dataset Explorer window (`Azaion.Dataset.DatasetExplorer`)
Mirrors the annotator, but for browsing:
- Thumbnail grid (virtualised) keyed by `Annotation.ThumbPath`, regenerated
by `IGalleryService`.
- Filter bar: date range, flight, status (`AnnotationStatus`: None / Created
/ Edited / Validated).
- Class distribution chart (`Controls/ClassDistribution.xaml`): horizontal
bars, one per `DetectionClass`, coloured with the class colour.
- Inline editor tab — same `CanvasEditor` from `Azaion.Common.Controls`,
reused.
- Bulk validation: select multiple thumbnails, press `V`, status becomes
`Validated`.
- Local keyboard handlers: `19` (class), `Enter` (save), `Del` (delete
selected), `X` (delete all), `V` (validate), arrow keys + PageUp/PageDown
for navigation, `Esc` to close the editor.
## 6. `Azaion.Common`: the everything-bag
This is the assembly that the post-refactor split tries hardest to undo. At
commit `22529c2` it was a single .NET project containing:
| Folder | Concern |
|---------------|------------------------------------------------------------------------------------------------------------------------|
| `Controls/` | **WPF user controls**: `CanvasEditor`, `NumericUpDown`, `DetectionClasses`, `CameraConfigControl`, `DetectionLabelPanel`, `UpdatableProgressBar`, `DetectionControl`. |
| `Database/` | LinqToDB models + `AnnotationsDb : DataConnection` + `DbFactory` + `SchemaMigrator` + `AnnotationsDbSchemaHolder`. |
| `DTO/` | App config sections (`AppConfig`, `LoaderClientConfig`, `InferenceClientConfig`, `GpsDeniedConfig`, `MapConfig`, `QueueConfig`, `AIRecognitionConfig`, `ThumbnailConfig`, `UIConfig`, `AnnotationConfig`, `CameraConfig`, `DirectoriesConfig`), domain enums (`AffiliationEnum`, `RoleEnum`, `WindowEnum`, `Direction`, `PlaybackControlEnum`), and shared shapes (`ApiCredentials`, `BusinessExceptionDto`, `LoginResponse`, `RemoteCommand`, `User`, `DetectionClass`, `LabelInfo`, `AnnotationResult`, `AnnotationThumbnail`, `ClusterDistribution`, `Coordinates`, `SatTile`, `DownloadTilesResult`, `SelectionState`, `FormState`, `ExternalClientsConfig`). |
| `Events/` | MediatR notifications (`AnnotationCreatedEvent`, `AnnotationsDeletedEvent`, `KeyEvent`, `SetStatusTextEvent`, `AnnotatorControlEvent`, `LoadErrorEvent`). |
| `Exceptions/` | `BusinessException`. |
| `Extensions/` | Helpers — `Geo`, `ParallelExt`, `ResilienceExt`, `ThrottleExtensions`, `IntervalTree`-related, `Bitmap`, `Color`, `Cancellation`, `Queryable`, `Graphics`, `Size`, `String`, `DirectoryInfo`, `DenseDateTimeConverter`, `EnumExtensions`, `ServiceCollectionExtensions`. |
| `Services/` | `AnnotationService` (RabbitMQ.Stream + LinqToDB + MediatR), `FailsafeAnnotationsProducer`, `GalleryService` (thumbnails), `AuthProvider`, `TileProcessor`, `SatelliteDownloader`, `LoaderClient` (ZMQ), `GpsMatcher/*` (ZMQ + service + event handler + events), `Inference/InferenceClient` (ZMQ), `Inference/InferenceService` (orchestrates inference jobs), `Inference/InferenceServiceEventHandler`, `Inference/InferenceServiceEvents`, `Cache`, `HashExtensions`. |
| `Constants.cs`| `CONFIG_PATH`, suffixes, file naming conventions, the `FailsafeAppConfig` builder. |
| `Security.cs` | AES-256-CFB credentials encryption / decryption. Key is time-derived for local on-disk storage; symmetrical on both ends of the loader handoff. |
Concrete examples of the tangle, taken straight from this commit:
- `Azaion.Common.Database.Annotation` carries `[IgnoreMember] System.Windows.Media.Color`
in its computed `Colors` projection. A "database model" that imports
`System.Windows.Media`. The DTO assembly cannot exist outside WPF.
- `Annotation.Init(DirectoriesConfig, Dictionary<int, DetectionClass>)` is a
**static initializer** that the application calls once at startup.
Hydrated entities then read the static `_labelsDir`, `_imagesDir`,
`_thumbDir`, `DetectionClassesDict` to compute their own paths and class
names. Two annotation databases or two configurations cannot coexist in
the same process.
- `AppConfig` aggregates ten config sections including `UIConfig`, but is
also passed to the queue producer, the loader client, the inference
client, the satellite downloader. There is no clear seam between
"app-host concerns" and "business concerns".
- `AnnotationService` is constructed with `IDbFactory`,
`FailsafeAnnotationsProducer`, `QueueConfig`, `UIConfig`,
`IGalleryService`, `IMediator`, `IAzaionApi`, `ILogger`. It runs a
`RabbitMQ.Stream.Consumer` *inside its constructor* via `Task.Run(...).Wait()`,
publishes MediatR events into the WPF dispatcher, and uses
`_imageAccessSemaphore` and `_messageProcessingSemaphore` to serialize
cross-thread SQLite writes. Lifecycle and threading model are baked in.
The follow-on commits (`e7ea5a8`, `9e7dc29`, `fbbe556`) split this into
proper layers: repositories with interfaces, an `AnnotationPathResolver`
service replacing the static fields on `Annotation`, a separate
`Azaion.Common.Database.AnnotationRepository` + `IAnnotationRepository`,
removal of `AnnotationsDbSchemaHolder`, and finally the move from a WPF
client to a containerised .NET API exposing REST + SSE.
## 7. Cython sidecars
### `Azaion.Inference`
Standalone Python project compiled with Cython
(`build_inference.cmd` → PyInstaller → `azaion-inference.spec`). Top-level
modules at `22529c2`:
```
ai_availability_status ai_config annotation
classes.json constants_inf file_data
inference inference_engine onnx_engine
loader_client main_inference remote_command_handler_inf
```
It exposes a ZeroMQ DealerSocket port. The .NET side (`InferenceClient` in
`Azaion.Common.Services.Inference`) sends MessagePack-serialised
`RemoteCommand` envelopes; the Cython side dispatches to either the ONNX
or TensorRT engine, reads inputs from the local file system, and streams
back `DetectionEvent`-shaped progress.
State the engine reports (mapped 1:1 to the React UI's
`AIAvailabilityStatus`):
| Value | Name | Meaning |
|-------|-------------|--------------------------------------------------------|
| 0 | None | Initial. |
| 10 | Downloading | Pulling weights from Admin API / CDN via `loader_client`. |
| 20 | Converting | ONNX → TensorRT (TensorRT devices only). |
| 30 | Uploading | Uploading converted engine back to API for caching. |
| 200 | Enabled | Inference engine ready. |
| 300 | Warning | Recoverable, the engine may come back. |
| 500 | Error | Failed to initialize. |
### `Azaion.Loader`
Same shape — Cython, ZeroMQ DealerSocket, separate process. Modules:
```
api_client cdn_manager constants
credentials file_data hardware_service
main_loader remote_command remote_command_handler
security
```
It is the only component in the legacy stack with internet access. It
authenticates the user against the remote API, downloads encrypted
resource bundles (model checkpoints, `config.system.json`,
`config.secured.json`), and decrypts them on demand using a key derived
from `email + password + hardware_id` (`security.pyx` + `hardware_service.pyx`).
The .NET side never sees the raw resource files until the loader has
already decrypted them.
This Loader is exactly the component documented in
`suite/_docs/00_top_level_architecture.md` under **Binary Split Security**.
The 3 KB key fragment, the encrypted on-device archive, and the
`SHA384(fragment + hw_hash + creds)` derivation all originate here.
## 8. Data model (LinqToDB → SQLite)
| Table | Purpose |
|-------------------------------|------------------------------------------------------------------------------------------|
| `Annotations` | Per-frame label set: `Name`, `MediaHash`, `OriginalMediaName`, `Time`, `CreatedDate`, `CreatedEmail`, `CreatedRole`, `Source`, `AnnotationStatus`, `ValidateDate`, `ValidateEmail`, `Detections[]`, `Milliseconds`, `Lat`, `Lon`. |
| `Detections` | Bounding box rows: `ClassNumber`, geometry, `Confidence`. |
| `MediaFiles` | Files indexed by hash. Used to dedupe + drive the media list. |
| `AnnotationQueueRecord` (`AnnotationsQueueRecords` table) | Local **failsafe outbox** for RabbitMQ publication. `FailsafeAnnotationsProducer` drains this every 10 s. |
Schema is created/migrated in process by `SchemaMigrator` against a
SQLite file pointed to by `DirectoriesConfig`.
## 9. Annotation sync (edge → central)
The legacy code already had the eventual edge-to-central sync wired in:
```
Annotator window // user creates annotation
│ MediatR: AnnotationCreatedEvent
AnnotationService // local SQLite write
├─► Annotations row
├─► AnnotationQueueRecord row (unless SilentDetection)
FailsafeAnnotationsProducer // BackgroundService-style task
│ MessagePack + Gzip, retry on failure
RabbitMQ.Stream "azaion-annotations" // remote
└─► consumed by ai-queue-handler / Admin API in the remote tier
```
The React UI inherits the *protocol* (RabbitMQ stream, MessagePack +
Gzip, dedupe by `Annotation.Name`) but no longer owns it — it runs in
the new `annotations/` .NET API submodule of the suite.
## 10. What survived into the new world
The following concepts are direct ports of the legacy WPF design and
should be implemented in the React UI exactly the same way:
- **Module switcher** with localized name + SVG icon → top navigation bar
(Flights, Annotations, Dataset, Admin, Settings).
- **Detection-class strip** with class colour, number, name, and PhotoMode
switcher (Regular / Winter / Night, offsets 0/20/40).
`yoloId = classId + photoModeOffset`.
- **Canvas editor**: bounding-box draw / 8-handle resize / Ctrl multi-select
/ Ctrl+wheel zoom / Ctrl+drag pan / crosshair with active-class hint /
normalized-coordinate clamping.
- **Annotation row gradient** in the side list: a left-to-right gradient
composed of each detection's class colour, opacity proportional to
`Confidence`. Empty annotation → `#40DDDDDD` background.
- **Affiliation icons** (Friendly / Hostile / Unknown / None) and
**combat readiness** indicator (Ready / NotReady / Unknown) drawn next
to the bounding-box label.
- **Time-windowed annotation rendering** during video playback:
`Before = 50 ms`, `After = 150 ms`, lookup via interval tree.
- **Frame-by-frame stepping** in fixed counts (1, 5, 10, 30, 60), computed
from `1 / fps`.
- **Localized class names** (`DetectionClass.UIName` carried alongside the
English `Name`).
- **Camera config per session**: altitude / focal length / sensor width
drives GSD-based detection-size validation.
- **GPS-denied panel toggle** under the canvas (now implemented as the
GPS-Denied mode of the Flights page in the React UI).
- **Help window** with the six annotation quality rules.
- **Color scheme**: dark navy/blue primary (`#343a40`), orange accents
(`#fd7e14`), dark gray background (`#1e1e1e`), green success
(`#40c057`), blue primary buttons (`#228be6`), red danger (`#fa5252`).
- **Confirmation dialogs** for delete-media / delete-selected / delete-all
/ deactivate-user.
- **Resizable panel widths** persisted per user.
## 11. What is intentionally NOT being ported
- The DI host inside the UI process. The React app does not own a
service container, RabbitMQ consumer, SQLite database, or background
worker. All of that now lives in the per-service .NET / Python /
Cython submodules.
- LibVLCSharp. The browser's native `<video>` element with a
frame-accurate seeking shim handles playback.
- ZeroMQ DealerSockets. The browser only speaks HTTP and SSE. Inference,
GPS-matching, satellite tile fetching, and loader requests are all
exposed as REST endpoints by their respective suite services.
- The static `Annotation.Init(...)` initializer. Path/colour computation
becomes selector logic over the API DTOs, with no static state.
- The `Azaion.Common` god-assembly. Each concern is now a separate suite
submodule with its own repo, Dockerfile, and OpenAPI document.
- The `Azaion.LoaderUI` external-process handoff with encrypted creds on
the command line. The browser performs `POST /auth/login` against the
Admin API and stores a JWT.
- The Cython `Loader` and the binary-split key-fragment dance. That whole
protocol is server-side now (`loader/` submodule) and the React UI is
not involved beyond showing a progress screen.
## 12. How to read the research copy
```
cd /Users/obezdienie001/dev/azaion/suite/annotations-research
git status # detached at 22529c2 "Revert add MediaFile"
git log --oneline -n 5 # see surrounding commits
```
The folder is a plain clone of `suite/annotations` and is **not** wired
into the suite's `.gitmodules`, so the parent repository ignores it.
If you want to compare the WPF-era code to the immediately following
"big refactoring" commit, the comparison is:
```
git log --oneline --reverse 22529c2..e7ea5a8 # there is only e7ea5a8 itself
git diff 22529c2 e7ea5a8 -- Azaion.Annotator # what the cleanup changed
git diff 22529c2 e7ea5a8 -- Azaion.Common # the big assembly split prep
```
Two commits later (`fbbe556` / `9e7dc29`) the WPF projects disappear
entirely and are replaced by a containerised .NET API — that is the
state currently checked out in `suite/annotations`.