Files
OmaPhoto/TASKS.md
T

485 lines
212 KiB
Markdown
Raw 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.
# Compositor for Linux — task list
This file is the progress truth. Work top to bottom. Do not skip. One item at a time, main agent only.
## Ground rules
- No PRD exists. Chapters follow the README feature sections; order bends only where a dependency forces it.
- The Swift tree is the spec and stays untouched. Each Swift file gets a Linux twin with the same type and function names.
- Swift files over 500 lines split by concern with the repo's own `Name+Part` convention.
- Done means: code, twin tests green in the dev container, codex review passed (max 3 rounds), box ticked.
- A failed third round ends an item's reviews. Its findings are fixed, proven by tests and mutants, logged as such, and named in the next item's review scope.
- Review: Herdr split pane, codex agent `reviewer` on `gpt-6-astra` at effort `max` in YOLO mode, scoped to the item's files. The user's shell aliases `codex` to `codex --yolo`, so the pane start passes only `-m gpt-6-astra -c model_reasoning_effort="max"`.
- No commits unless the user asks.
## Decisions (user-confirmed 2026-09-20)
- Stack: C++20, Qt 6 Widgets (floor: Qt 6.4, Ubuntu 24.04). CPU raster: QPainter plus the eight C kernels, compiled in place from `Compositor/Rendering/*.c`.
- Remove Background: ONNX Runtime with an open-licensed model.
- No updater on Linux. No Dokploy. Final output: packages and the acceptance table.
- Omarchy source: `~/.local/state/omarchy/current/theme/colors.toml` (Omarchy 4.x). Absent file means built-in dark. Omarchy 3.x is out of scope.
## Cut (razor)
GPU backend (unless item 8.3 fails its gate), AppImage, Flatpak, CI pipelines, theme plug-ins, theme settings UI.
## 0 Foundation
- [x] 0.1 Dev container, CMake skeleton, logging categories, `AGENTS.md`. New: `linux/Dockerfile.dev`, `linux/CMakeLists.txt`, `linux/scripts/dev.sh`, `linux/src/Logging`, `linux/src/CompositorApp.cpp` (`main`), `linux/src/ContentView` (window shell: title, 1180×780 default, 800×520 minimum). Verify: container builds, the window test passes and saves an offscreen screenshot.
## 1 Document model
- [x] 1.1 `LayerTransform`, `LayerAppearance`, `CanvasViewport`, `ImageLayer`, `CanvasDocument`, `NavigationTool`, `ImportedImage`. Tests: twins of the pure-geometry cases in `TransformTests`, `LayerAppearanceTests`.
- [x] 1.2 `DocumentHistory`. Tests: `HistoryTests`.
- [x] 1.3 `RasterSnapshot`, `BrushRaster`/`BrushPatch` helpers. Tests: `RasterSnapshotTests`.
- [x] 1.4 `LayerGroups`: `LayerHierarchy`, `hierarchyRecord`, the `CanvasDocument` hierarchy views; `ProjectLayerRecord` and `ProjectError` arrive here. Tests: `GroupTests` (hierarchy cases). The `EditorSession` extension and its tests move to 4.1.
- [x] 1.5 `LayerMask` core: the struct, `solid`, `assetFrom`, placement rules, `background`, `placed`, `drawSmooth`, `maskTransform`, mask fields in the record and in history. Tests: `LayerMaskTests` (class level). Moved on: `clipImage`, `MaskPlacementCache`, `FolderMaskClip` to 2.1; `ProjectSnapshot.mask` to 3.1; session mask editing and the Swift `LayerMaskTests` twins to 4.1; mask distort to 6.2; `placedMaskPreview` to 8.2.
- [x] 1.6 `LiveMaskGraph.validate`, `maskSourceID` on the layer and record, `MaskTracing`. Tests: `LiveMaskTests` (graph cases), `MaskTracingTests`. Moved on: the session clipping functions, `adoptClipping`, `releaseDetachedClipping`, `LiveMaskBaker`, the delete choice and the Swift `LiveMaskTests`/`MaskTransformTests` twins to 4.1; `drawLiveComposite` to 4.4; `loadMaskSelection`/`loadLayerSelection` to 7.1; the adjustment-source rule of `validate` to 9.5.
## 2 Rendering
- [x] 2.1a `DownsampleCache`: Lanczos halvings in place of vImage, threaded with QtConcurrent, `rgba_clamp_premultiplied` from `AdjustPixels.c`. Tests: the cache cases of `DownsampleTests`.
- [x] 2.1b `LayerRenderer.draw`: placement, opacity, every blend mode (the four non-separable ones by hand in `HslBlend`), soft masks without a CoreGraphics mask clip; decide `SeparableBlend` by measuring Qt's dodge and burn against the PDF formulas. Tests: the drawing cases of `DownsampleTests`, blend and mask cases. `drawCoverage` moves to 3.3, its first caller; `drawBrushPreview` to 2.2.
- [x] 2.1c From `LayerMask`: `clipImage`, `MaskPlacementCache`, `FolderMaskClip`.
- [x] 2.2a `TiledLayerRenderer` for committed rasters: `Piece`, `support`, `aligned`, `interiors`, `piece`, `TiledPieceCache`, `Frame`, `drawRaster`; `LayerRenderer`'s compose-aside path becomes shared. Tests: the committed part of `tiledLayersDrawLikeOneImage`, geometry and cache cases.
- [x] 2.2b `TiledLayerRenderer.drawStroke`, `drawMaskStroke`, `drawCropped`, `Piece.offsetBy` and the offset of `drawCommitted`, `Frame.device` and `Frame.sampling` (first read by `drawMaskStroke`), `LayerRenderer.drawBrushPreview`. Tests: the live-stroke parts of `TiledLayerTests` that need no session (`TiledStrokeTests`), `BrushPreviewTests`.
- [x] 2.3 `LiveMaskRenderer` (coverage, chains, clipping stacks, `drawComposite`), `AdjustmentSurface`; `BrushPixels.c` joins the build for its alpha kernels. Tests: `LiveMaskRendererTests`, `ClippingStackTests` (the pixel values of the Swift `LiveMaskTests`, drawn without a session), `AdjustmentSurfaceTests`. Moved on: `LiveMaskRenderer.adjust` and the adjustment hooks to 9.5 (they need `LayerAdjustment`); `LayerEffectsSurface` to 9.6 (it needs `LayerEffects` and the effects kernel).
- [x] 2.4 `ImageExporter` (flatten, PNG, JPEG, resolution metadata, atomic write); `ProjectManifest`, `ProjectSnapshot` and `ProjectSnapshot.mask` arrive here as data, because `render` takes a snapshot. Tests: `ExportTests`, `JPEGExportTests`. Moved on: layer effects in `render` to 9.6, adjustment layers in `render` to 9.5, the cancellation points of `jpeg` to 10.3 (their only caller).
## 3 IO
- [x] 3.1 `ProjectStore`: `.comp` read and write at versions 1–7 (verified in `ProjectStore.swift`: `(1...7).contains`), the manifest codec (`ProjectStore+Manifest`), every validation rule and limit, atomic replace with a two-step fallback, `rawValue` for `LayerBlendMode` and `LayerSampling`. Tests: `ProjectTests` (store level; the session cases wait for 4.1 and 4.2), `ProjectManifestTests`, `ProjectReplaceTests`. Moved on: the record fields `adjustment` (9.5), `shape` (8.8), `effects` (9.6) and `text` (8.9) with their validation rules; until then their keys are ignored on load.
- [x] 3.2 `ImageImporter`: JPEG, PNG, TIFF, HEIC (libheif), EXIF orientation, sRGB conversion, pixel budget, thumbnails, `ImageImportError`. Tests: `ImageImportTests` (decode level), HEIC fixtures from `scripts/heic-fixtures.sh`. Moved on: `importPlacementAndPartialFailure`, `dropPositionUsesDocumentCoordinates` and `queuedImportsAreNotLost` to 4.1 (the session's import queue); `fileDropProvidersReachImporterInOrder` to 10.4 (`ImageFileDrop`). Packaging (11.x) must depend on the HEVC decoder plugin by name (`libheif-plugin-libde265` on Ubuntu; Fedora keeps it in RPM Fusion as `libheif-freeworld`).
- [x] 3.3 `ImageResizer`, `CanvasResizer`, `CanvasSize`, `LayerRenderer.drawCoverage`. Tests: `ImageSizeTests`, `CanvasSizeTests` (store level), `DrawCoverageTests`. Also: `lint.py` reads the comments of shell and Python scripts, `CMakeLists.txt` and `Dockerfile.dev` (3.2 round 2). Moved on: `EditorSession.applyImageSize` and `applyDocumentSize` with the undo halves of the Swift tests to 4.1; `CanvasUnit.allCases` to 10.2, its first reader; the record fields the resizers drop or keep (shape 8.8, text 8.9, effects 9.6) are named in AGENTS.md; 7.x must give `drawCoverage` an opaque device under a soft selection clip.
## 4 Shell, canvas, theme, layers panel
- [x] 4.1a `EditorSession` core: the class and its change notification, document and tool state, the gates (`canEditLayers`, `canUseHistory`, each reading only the state that exists so far; see "Session hooks owed"), `selectLayer`, `selectTool`, history (`beginEdit`, `endEdit`, `undo`, `redo`), layers (add, delete, rename, visibility and its swipe, reorder, move), `createDocument`, `insert`, `fit`, `zoom`, and from the `LayerGroups` extension what the core's own folder branches need to be tested: `descendantIDs`, `addGroup`, `toggleGroupExpansion`. New: `EditorSessionTests`, `EditorSessionSignalTests` (the `changed()` contract). Tests: `LayerTests` (the session cases), `CompositorTests` (replace `zoomStaysWithinItsRange` with the `limitsAndNewDocumentReset` twin), the session cases of `HistoryTests`. In scope by the third-round rule: the `lint.py` fixes of 3.3 round 3. Moved on: `CanvasEntryTests` to 4.3 and 4.4 (they test `CanvasView` and `NewCanvasSheet`); `nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity` to 4.6 (`NativeLayerList`); `selectionAndRenameDoNotInvalidateCanvasButPixelChangesDo` to 4.4.
- [x] 4.1b Groups and selection in the session (the rest of the `LayerGroups` extension): `selectLayers`, `extendSelection`, `groupSelectedLayers`, `canPlaceLayer`, `placeLayer`, `moveActiveLayerOutOfGroup`, `layerRows`, and the multi-delete owed since 4.1a: `deleteSelectedLayers`, `finishDeletingLayers`. Tests: `GroupingSelectionTests`; of `GroupTests` the session cases `nestedGroupsMoveOutCollapseAndDeleteUndo` and `hiddenParentOverridesChildrenAndExportOrderFollowsGroups` (its session half, an inserted image standing in for the import); the new mutators join `EditorSessionSignalTests` and the round trip of `HistoryTests`. Moved on: the export lines of `hiddenParentOverrides…` and all of `groupsRoundTripAndSurviveImageAndCanvasResize` to 4.2 (they need `projectSnapshot`); the `canTransform` lines of both test files to 4.1c; the import in `hiddenParentOverrides…` to 4.1d; `placeLayer`'s clipping calls to 4.1e (no session call can clip a layer before `linkMask`).
- [x] 4.1c Transform edits in the session: `TransformEdit`, `TransformGroup`, `canTransform`, `beginTransform`, `previewTransform`, `commitTransform`, `cancelTransform`, `nudgeLayer`, the group box (`transformsAsGroup`, `groupTransformMembers`, `groupTransformBox`), `transformPixelSize`, `displayedTransform`, `editedTransform`, `pendingTransform`, and the hooks owed: `selectLayer`, `selectTool`, `selectLayers` and `createDocument` commit an open edit, the gates read it, `extendSelection` works through it. `DistortWarp::corners` comes forward from 6.2 (the group box is made of corners). Tests: the session cases of `TransformTests` (`blankLayersHaveNoTransformHandlesOrEditing` without its overlay lines, `previewCommitCancelAndUndoPreserveSources`, `scalePercentSetsBothSidesAboutTheCenter` through the session, `noOpInvalidValuesAndSwitchingTools`); new `TransformSessionTests` (Swift tests no group box, nudge or hook); the `canTransform` lines of `GroupTests` and `GroupingSelectionTests`; the transform calls join `EditorSessionSignalTests` and the round trip of `HistoryTests`. Moved on: `TransformEdit.persistent` with the argument of `beginTransform`, `snapGuides` and the overlay lines of `blankLayers…` to 6.1 (canvas and overlay read them); `beginDuplicateTransform`, `transformDuplicate` and `duplicateTransformPreservesOriginalAndSupportsUndoAndCancel` to 5.1 (they need `duplicateActiveLayer`); `layerListToolKeysAndTransformNudge` to 4.6 (it drives the layer table); mask-alone edits, `transformTargetsMask`, mask placements on commit, `blendPreview` and `finishOpacityEdit` to 4.1e; `corners` to 6.2; floating edits to 7.4; the grown transform of `displayedTransform` to 7.5.
- [x] 4.1d Import queue and busy gating: `importImages` (URLs in, a callback when the request is in), the queue and its drain, `waitForProjectAccess`, `waitForFileRequest`, `canStartProjectOperation`, `showsBusy` and its delay; the hooks owed: the six setters resume file requests, `isProjectBusy` frees project waiters and drives `showsBusy`, `importImages` commits a transform first. Tests: new `ImageImportSessionTests` (the twin's `ImageImportTests` stands at 480 lines) with Swift's `importPlacementAndPartialFailure`, `dropPositionUsesDocumentCoordinates`, `queuedImportsAreNotLost` and the queue's own cases (one undo step a request, waiting while busy, the pixel budget over what the canvas holds, what each callback sees, the decoding thread, a session that ends mid-import); new `SessionGateTests` (every gate of a file request, waiters that look again and keep their line, waiters that end with their session, the quarter second); an import and a long wait in `EditorSessionSignalTests`; `GroupTests::hiddenParentOverridesChildrenAndExportOrderFollowsGroups` imports its image. Moved on: `applyImageSize`, `applyDocumentSize` and the undo halves of `ImageSizeTests` and `CanvasSizeTests` to 4.2 (those tests start from `projectSnapshot`); the security scope of a URL has no Linux twin.
- [x] 4.1e1 Appearance in the session (the `LayerAppearance` extension; 4.1e is split in three because its Swift sources run to 750 lines): `blendPreview`, `displayedBlendMode`, `previewBlendMode`, `canEditAppearance`, `opacityEditLayerID`, `beginOpacityEdit`, `finishOpacityEdit`, `setLayerOpacity`, `setSelectedLayersOpacity`, `cycleBlendMode`, `setLayerBlendMode`; the hook owed: `commitTransform` clears the preview and finishes an opacity edit. Tests: of `LayerAppearanceTests` the cases `hoverPreviewIsTemporaryAndNeverChangesSavedState` (its snapshot line waits for 4.1e2) and `opacityDragIsOneUndoAndKeepsSources`; the selected layers' opacity and the blend cycle by cases of their own. Moved on: `moveToolNumberKeysSetSelectedLayersOpacityAsOneUndo` to 8.x (`typeOpacityDigit` lives in `EditorSession+Brush`); `blendModesAndOpacityMatchKnownPixels` and `appearancePersistsThroughSaveResizeAndTransparentExport` to 4.1e2 (`projectSnapshot`). `refreshCanvasPreview` has no twin: `changed()` reaches the canvas.
- [x] 4.1e2 Projects in the session (`EditorSession+Projects`, 56 lines, brought forward from 4.2): `projectSnapshot`, `installProject`, `clearProject`, `createNewProject`, with `applyImageSize` and `applyDocumentSize` from the `ImageResizer` file. It comes before the masks because the twin's document is read-only from outside: where Swift's tests write a mask straight into `session.document`, the twin's tests install a snapshot. Tests, all waiting on `projectSnapshot` until now: the export lines of `GroupTests::hiddenParentOverridesChildrenAndExportOrderFollowsGroups` and all of `groupsRoundTripAndSurviveImageAndCanvasResize`; the undo halves of `ImageSizeTests` and `CanvasSizeTests`; of `LayerAppearanceTests` the snapshot line of `hoverPreview…`, `blendModesAndOpacityMatchKnownPixels` and `appearancePersistsThroughSaveResizeAndTransparentExport`; `LayerTests::newCanvasStartsWithOneSelectedEmptyLayer` (its `canPaint` line waits for 8.x); new `ProjectSessionTests` for the five functions themselves; the session cases of `GroupTests` moved to `GroupSessionTests` (the file stood at 483 lines).
- [x] 4.1e3 Layer masks in the session (the `LayerMask` extension): `isMaskSelected`, `selectLayerTarget`, `canEditMask`, `addLayerMask`, `toggleLayerMask`, `deleteLayerMask`, `canCopyMask`, `copyMask`, `toggleMaskLink`, `displayedMaskPlacement`, `commitMaskTransform`; the hooks owed: `setActiveLayerID` and `restore` keep or drop the mask target, `installProject` and `clearProject` drop it, a mask alone transforms (`TransformEdit.mask`, `transformTargetsMask`, `beginTransform`, `editedTransform`, `pendingTransform`, `transformPixelSize`), `commitTransform` moves mask placements. Tests: new `LayerMaskSessionTests` (the seven session cases of Swift's `LayerMaskTests`, their direct writes going through `rewrite`), new `MaskTransformTests` (Swift's five), new `LayerMaskEditTests` (copying, links, the target across undo and projects, group moves, a mask alone), new `MaskSignalTests`; `tests/SessionFixtures.h` and `tests/SessionRecord.h` share fixtures and the observer's record. Moved on: `addMask` from a selection to 7.1; distorted masks (`corners` in `displayedMaskPlacement` and `commitMaskTransform`, `maskDistortPreview`) to 6.2; the grown transform to 7.5; floating edits to 7.4; `folderMaskCanBePaintedInvertedAndLoadedAsASelection`, the painting half of `placementAndLinkAreSavedAndPaintingFollowsThePlacement` and `placedMaskPreview` to 8.x; `canvasShowsAFolderMaskWhileItIsBeingPainted` and the live-composite half of `renderBoth` to 4.4.
- [x] 4.1e4 Live masks in the session (the `LiveLayerMask` extension with `LiveMaskBaker`): `canLinkMask`, `linkMask`, `adoptClipping`, `removeLiveMask`, `canToggleClippingMask`, `toggleClippingMask`, `releaseDetachedClipping`, `deleteWithLiveMaskChoice` with its alert and its bake off the UI thread, `brushError`; the hooks owed: `deleteLayer` and `deleteSelectedLayers` ask the choice, `finishDeletingLayer(s)` take `baked` and free live masks, `placeLayer` adopts and releases clipping. `PixelAdjust::thumbnail` comes forward from 9.2 for the baker. Tests: new `LiveMaskSessionTests` (the five session cases of Swift's `LiveMaskTests`), new `LiveMaskEditTests` (links and their refusals, runs freed by a release, siblings, adoption, stacks broken by a move, dependents freed baked or not, a folder with the null id, clipping under a selection of several), new `LiveMaskBakeTests` (a bake with an offset target and a translucent source, a masked source in a shape that is no square, a stretched source with a placed mask and no stated opacity, a source without pixels, a snapshot that repeats an id, thumbnails), new `LiveMaskDeleteTests` (the real alert answered three ways, the plural text, folders, a bake that queues for a worker while the caller goes on, a failing bake), clipping in `MaskSignalTests`. Moved on: the adjustment term of `canLinkMask` to 9.5; `drawLiveComposite` stays with 4.4.
- [x] 4.2a `ProjectController` without a workspace (the session's own project functions went ahead as 4.1e2): `canStart`, `begin`, `save` and Save As (the save dialog, the store off the UI thread, the session's `projectPath` setter, `markSaved`, the save generation), `open` (a supplied path or the open dialog, validation before the live document is touched, `confirmReplacement`, the reload when the confirmation's save replaced the opened file, `installProject`), `newCanvas`, `close`, `confirmQuit`, `confirmReplacement` (the alert answered three ways), `showError`, `receive` and its queue (`waitForFileRequest`, more than one project refused, a project opened before the images go in, the point only without a project). Tests: new `ProjectControllerTests`, the real dialogs driven from timers. In scope by the third-round rule: the target-mask fixture of 4.1e4 round 3 (`LiveMaskBakeTests::aBakeMapsTheSourceIntoTheTargetsOwnPixels`). Moved on: `exportPNG` and `exportJPEG` to 10.3, `canvasSize` and `imageSize` to 10.2, `cancelCrop` in `begin` to 10.1, the workspace branches to 4.2b; recent documents and security scopes have no twin.
- [x] 4.2b `ProjectWorkspace` and `ProjectTab`: tabs, `canSwitch`, `addTab`, `select`, `newCanvas`, `open` and `loadProject`, `close`, `removeTab`, `quitOrder`, `confirmQuit`, `closeWindow`, `receive`, `copyLayer`, the layer-row branch of `receiveProviders`, the controller's workspace branches (`canStart`, `open`, `newCanvas`, `close`, `receive`), the session's `skipsInitialClipboardCanvasSize`. Tests: `ProjectWorkspaceTests` (Swift's six and the tabs' own rules), new `ProjectWorkspaceFlowTests`, `ProjectCopyTests`, `ProjectWorkspaceSignalTests`. In scope by the third-round rule: the default-completion test of 4.2a round 3 (`ProjectControllerTests::aCallWithoutACompletionIsWhole`). Moved on: the file branch of `receiveProviders` to 10.4 (`ImageFileDrop`), `canReceiveDrag` to 4.3 (`ProjectTabs`), the adjustment, `pixelMove` and `colorPicker` terms of `canSwitch` to the items that bring that state.
- [x] 4.3a Tool icons (`UI/ToolIcons`, Linux only: SF Symbols have no twin): one drawn icon a tool (paths tinted from the palette, no icon theme: they differ by distro), the Swift extras that exist so far (`GradientToolIcon`, `CloneStampToolIcon`); variants that follow tool state (ellipse marquee, polygonal lasso, eraser) arrive with that state. Tests: every tool has an icon that paints pixels, follows the palette, and differs from the others.
- [x] 4.3b `ContentView` as one project's editor area (a `QWidget`; the window moves to 4.3c): the tool header slot (`ToolHeaderStyle`; the idle and eyedropper bars, `NavigationToolHeader` for Hand and Zoom; every other tool's bar arrives with its tool), the tool rail's buttons (labels as tooltips, the selected state, scrolling when short; icons from 4.3a), the canvas slot with the welcome (`NewCanvasSheet`: fields, validation, `Create canvas` as the default button, `Open project`, `Import image`, the clipboard's size skipped once for the first tab), the status bar (zoom, size, colour note, busy and importing notes, each tool's hint), the importer's file dialog, the alerts for `importError` and `brushError`. GUI test: the twin of `testCreateCanvasAndNavigation` as far as it needs no window chrome. Moved on: the canvas itself to 4.4, the layers panel to 4.6, `cropError` to 10.1, the floating panels to 9.x, drops to 10.4, `ColorPaletteControls` to 8.10.
- [x] 4.3c The window: `ProjectWorkspaceView` (a `QMainWindow`: 1180×780, minimum 800×520, the current tab's `ContentView` rebuilt on a switch and disabled while the workspace manages), the toolbar (New canvas, the `ProjectTabStrip` with its tab buttons, modified dot and close button, Fit, 100%, zoom in and out), closing through `closeWindow`, quitting through `confirmQuit`, files from argv through `receive`. GUI tests: the launch test moves here; tabs select, close and show their state. Moved on: drops on tabs and `canReceiveDrag` to 4.7 and 10.4.
- [x] 4.3d Menus and shortcuts (⌘→Ctrl, ⌥→Alt): the `CompositorApp` commands whose session functions exist (File, Edit's undo and redo with their names and the text-field rule, View, Layer); each later item adds its own entries (see "Menu items owed"). Tests: every entry's shortcut, enabling and effect.
- [x] 4.4 `EditorCanvas` display: compositing (`drawLiveComposite`), zoom, pan, pixel grid, checkerboard, focus. Paint each dirty rect through an image surface (`AdjustmentSurface`): HSL blend modes and clip coverage need an image-backed device, and device-sized coverage then costs only the dirty area. Probe `BrushRaster::visibleRect` on a widget painter at device pixel ratio 2 and test it. From `LayerMaskTests`: `canvasShowsAFolderMaskWhileItIsBeingPainted` (with 8.x) and the live-composite half of `renderBoth` in `LayerMaskSessionTests` (export and canvas must agree).
- [x] 4.5 **Omarchy theme**: `UI/OmarchyTheme` reads `colors.toml`, builds the palette (no stylesheet: Fusion follows the palette, and a stylesheet would freeze it), watches `current/`, logs each load; the canvas surround takes the palette's `Base` so a light theme's welcome reads. Tests: parse, light/dark, missing file, live switch changes grabbed pixels, a switch reaching the real window. In scope by the third-round rule: 4.4's Alt-from-any-window fix.
- [x] 4.6 Layers panel A: `LayersPanel` (heading, empty words, footer, the resize edge and its stored width), `NativeLayerList` rows (hierarchy, clipping marks, hidden fade, masks and their link), select (plain, Ctrl, Shift, Alt on the strip, thumbnails and mask targets), rename, eye swipe, the context menu, keys, thumbnails that redraw only on change, `BlendModePicker`, `LayerAppearanceControls`, `LayerMaskMenu`, `LayerIcons`. Tests: `CanvasThumbnailTests`, `BlendShortcutTests`, `NativeLayerListTests`, `LayersPanelTests`, `BlendModePickerTests`, `LayerAppearanceControlsTests`; the moved `nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity` and `layerListToolKeysAndTransformNudge`. In scope by the third-round rule: the ancestor watch and the short-read check of 4.5 round 3. Moved on: see "Layers panel owed".
- [x] 4.7 Layers panel B: drag reorder and nest (`dragData`, `dropTarget`, `acceptDrop`, the `LayerColumn` indicator), Alt-drag duplicate (`duplicateActiveLayer` and `duplicateLayer` forward from 5.1) and its cursor, the clipping cursors under Alt, mask drags (`maskType`, `copyMask`), drags between projects (`draggedLayerSource`, `canReceiveDrag`, drops on tabs, the canvas and the New canvas button, several ids a drop). Tests: `LayerDragTests`, `ProjectDropTests`, the twin of `duplicatingALayerByDraggingPlacesTheCopyAsOneStep` in `EditorSessionTests`. In scope by the third-round rule: the four fixes of 4.6 round 3. Not driven: `QDrag::exec` (no drag loop offscreen). Moved on: file and picture drops to 10.4.
### Menu items owed
The menu bar of 4.3d holds only what works. Each item adds its entries with its tests: 9.x the Image menu's adjustments (Levels… since 9.1b, Hue/Saturation… since 9.3b, Curves… and the rest since 9.4b), the Filter menu (9.4b), New Adjustment Layer and Edit Adjustment (9.5d); 10.1 nothing; 10.2 Canvas Size, Image Size, Flip Canvas (done); 10.3 Export PNG, Export JPEG (done). `Check for Updates` and the Hide entries have no twin (no updater; the desktop hides windows).
### Tool bars, hints and icons owed
`ContentView` of 4.3b shows an empty bar for a tool whose bar has not arrived, the hint of each tool's first mode, and one icon a tool. Each item adds its own, with tests: 10.1 `CropControls` and the `cropError` alert (done); 9.x the floating panels (done); 10.4 drops on the canvas. The canvas of 4.4 draws layers, masks, folder masks and live masks, pans and zooms; each item adds its own, with tests: 9.x adjustments (9.5b, done) and effects (9.6c, done) in `DisplayState` and `drawLayers`, `picking`'s hue sample mode with its press (9.3b, done), and the press clearing `effectSelection` (9.6d, done); 10.1 the crop union in `renderBounds` and the crop cursors (done).
### Layers panel owed
The panel of 4.6 shows and edits what the session holds so far. Each item adds its own, with tests: the effects rows under a layer came with 9.6e, the footer's effects button and the delete words came with 9.6d, the `deleteLayerOrMask` effects term with 9.6a; the adjustment icons, their double click and the footer's adjustment button came with 9.5d.
### Session hooks owed
`EditorSession` grows with the tools. Each item below adds its own terms to the core written in 4.1a, with tests.
- 4.1e1: `commitTransform` clears `blendPreview` and finishes an opacity edit.
- 4.1e2: `installProject` and `clearProject` drop an open transform; from 4.1e3 on they clear `isMaskSelected`, from 10.1 they cancel a crop.
- 9.5: the store's text rule refuses an adjustment layer too.
- 9.6 (done in 9.6a): where Swift rebuilds a layer from listed fields (`commitPaintSnapshot`, `commitRasterEdit`, `invertPixels`, `mergeFloatingTransform`, the filter's, Levels' and Hue/Saturation's commits), each rebuild that leaves out `effects` drops them there, as the shape and text are dropped since 8.8 and 8.9a. The adjustment needs no such drop (9.5c, checked): every rebuild edits a layer's pixels, which an adjustment layer never has, and mask edits write in place.
- 9.x: the gates read `levels` (9.1a, done), `hueSaturation` (9.3a, done) and `adjustmentEditingID` (9.5c, done); `beginLevels` commits a pending gradient first (9.1a, done) and `beginHueSaturation` resolves one (9.3a, done); `beginLevels`, `canContentAwareFill` and `beginFilter` read `hueSaturation` (9.3a, done); `updateLevels`, `cancelLevels` and `commitLevels` hand an adjustment layer's edit its preview, cancel and commit first, as `updateFilter`, `cancelFilter` and `commitFilter` do, and the Hue/Saturation three (`previewAdjustmentEditing`, `finishAdjustmentEditing`, 9.5c, done); the colour picker's effect target and `previewEffectColor` (9.6a, done); the Levels sheet's caption says underlying pixels for an adjustment layer (9.5d); `selectLayer` and `selectLayers` clear `effectSelection` (9.6a, done); `canStartProjectOperation` reads `levels` (9.1a, done) and `adjustmentEditingID` (9.5c, done), and their setters resume file requests; `selectLayerTarget` clears `effectSelection` too (9.6a, done); `canLinkMask` refuses an adjustment layer as a source (9.5a, done); `deleteLayerOrMask` gains its effects term (9.6a, done), as does `deleteKeyPressed`.
- 10.1 (done): `selectTool` cancels and starts a crop; `restore` and `beginTransform` cancel it; `canEditLayers` reads `cropRect`; `importImages` cancels a crop. `ProjectController::begin` cancels a crop; so do `beginLevels` and `beginFilter`.
- `ProjectWorkspace::canSwitch` (4.2b) gains a term with each state it reads: `pixelMove` (8.2b, done), `gradientEdit` (8.7, done), `colorPicker` (8.10a, done), `hueSaturation` (9.3a, done).
- 10.2 and 10.3 bring the controller's `canvasSize` and `imageSize` (10.2, done), `exportPNG` and `exportJPEG` (10.3, done); 10.4 the file branch of `receiveProviders`.
## 5 Layers (README)
- [x] 5.1 `LayerMerge` (`mergePlan`, `canMergeLayers`, `mergeTitle`, `mergeLayers`, with `PixelFilter::trimmed` in `Filters.cpp`), `LayerFlip` (`LayerTransform::mirrored`, `flipLayers`), `beginDuplicateTransform` and `transformDuplicate` (`duplicateActiveLayer` and `duplicateLayer` went to 4.7); the Layer menu's Duplicate Layer, Merge and Flip Layer entries. Clipping masks in the panel were done by 4.6 and 4.7 (the indent, the Alt-click and its cursors) and 4.3d (the menu entry). Tests: `LayerMergeTests`, `LayerFlipTests`, `FiltersTests`, the twin of `duplicateTransformPreservesOriginalAndSupportsUndoAndCancel` and a folder case in `TransformTests`, the entries in `CompositorMenusTests`, the calls in `EditorSessionSignalTests`. In scope by the third-round rule: the three fixes of 4.7 round 3. Moved on: `flipCanvas` to 10.2, `layerViaCopy` to 7.4.
## 6 Transform (README)
- [x] 6.1 `TransformOverlay` (`TransformOverlayGeometry`, drawn by the canvas), the Move tool's drags on the canvas (move, resize, rotate, Alt duplicate, Ctrl picking, Ctrl+Shift extending, auto-select), snapping and guides (`transformSnapTargets`, `snappedMove`, `snapGuides`), the Move cursors, `TransformInspector` with its fields' arrow steps, `TransformEdit.persistent`, `transformAutoSelect`, `showsTransformControls`, `locksTransformRatio`, the Transform Layer and Show Transform Controls entries. Tests: `TransformOverlayTests` (the two geometry cases of Swift's `TransformTests`), `TransformPressTests` (Swift's two, and the handles, snapping, auto-select, Escape, focus, the wheel and a selection), `CursorTests` (Swift's two Move cases), `TransformInspectorTests`, `TransformSnapTests`, the entries in `CompositorMenusTests`, the bar in `ContentViewTests`, the settings in `EditorSessionSignalTests`; `blankLayersHaveNoTransformHandlesOrEditing`'s overlay lines in `TransformOverlayTests`. Moved on: the distort cursor, `showsRotation` and the corner geometry to 6.2; Transform Selection to 7.4; the selection cases of `CursorTests` to 7.x.
- [x] 6.2 `Distort` (`DistortWarp`: usability, convexity, `homography`, `warp`, `warpTrimmed`, `carried`, `warpMask`; `beginDistort`, `previewCorners`, `distortPreview`, `commitDistort`, `TransformEdit.corners`), the axis lock through `TransformDrag::corners`, mask distort (`commitMaskTransform`'s corners path, `MaskDistortPreviewCache`, `maskDistortPreview`, `displayedMaskPlacement`'s corners cases), the overlay's corner geometry, the canvas's Ctrl-drag, distort cursor and previews, the inspector's rest. Tests: `DistortTests` (Swift's three and the corner order, plus folded shapes, flips, `carried`, `warpMask`, nudges, groups), `DistortMaskTests` (masks along, a mask alone, split at the 500-line rule), `DistortCanvasTests`. Moved on: `mapPath` to 7.x, `distortedEffects` and `DistortEffectsCache` to 9.x.
## 7 Selections (README)
- [x] 7.1 `Selection` (`DocumentSelection`, `SelectionClip`, `LassoKind`, `SelectionMode`, `DragBox`, `LassoDraft`, the session's outlines, modes, held keys, moves, nudges, Expand and Contract, Select All, Deselect, Inverse), `SelectionEdits` (`deleteLayerOrMask`, `canInvert`, `invertPixels` with `PixelInvert` and `PixelAdjust::coverage` and `blend`), `loadMaskSelection` and `loadLayerSelection`, `addMask` from a selection, the Select menu, the Image menu's Invert, the panel's Ctrl-click loading and its cursor; the hooks owed: `CanvasDocument.selection`, `selectTool` cancels a lasso. Tests: `SelectionTests` and `SelectionSourceTests` (Swift's session cases; the four that drive `CanvasView` move to 7.2), `SelectionEditTests` (masks from a selection, the delete target, Invert; the fill, clear, gradient, brush and pixel-move cases move to 7.4 and 8.x), new `PixelInvertTests`, `SelectionSignalTests`, `SelectMenuTests` (the menu fixture now in `tests/MenuFixtures.h`), `LayerTests::deletingAMultiSelectionRemovesEveryLayerInOneStep`, the Ctrl-click and cursor cases of `NativeLayerListTests`, `LayersPanelTests` and `LayerDragGestureTests`. In scope by the third-round rule: the three fixes of 6.2 round 3. Moved on: `pressMarqueeKey`, `pressLassoKey`, `displayedSelection`, `CanvasView.selectionCursors` and `moveSelectionCursor` to 7.2; `canEditPixels`, `fillSelection`, `clearSelectedPixels`, `deleteKeyPressed` and `brushRevision` to 8.x; `PixelMove` and its calls to 7.4.
- [x] 7.2 The Marquee and the Lasso on the canvas (`lassoMouseDown`, `moveSelectionTool`, `releaseSelectionTool`, `selectionKey`, `endSelectionGestures`, `dragSelection`, `dragMarqueeDraft`, a double click closing a polygonal outline, Shift tracked like Alt and Ctrl, `pressMarqueeKey`, `pressLassoKey`, `displayedSelection`), the marching ants and the draft drawn by `TransformOverlay` (`antsPhase`, `drawSelection`, `drawLassoDraft`, `drawnRect`), the marquee autoscroll, the cursors (`moveSelectionCursor`, `selectionCursor`, `wandCursor`, with `Rendering/SelectionIcons` shared by `ToolIcons`), `LassoControls` with `SelectionAmountField`, the ellipse Marquee's and the polygonal Lasso's rail icons and hints. Tests: new `SelectionCanvasTests` (Swift's four canvas cases of `SelectionTests`, the drafts, moves, keys, cursors, ants, autoscroll, drawing and focus), new `LassoControlsTests`; the kinds in `ToolIconTests` (the reference sheet regrown) and `ContentViewTests`. In scope by the third-round rule: the loaders' flatten guard of 7.1 round 3. Swift's `CursorTests` cases: `leavingTheCanvasRestoresTheArrowWithEveryTool` and `aDragReleasedOutsideTheCanvasRestoresTheArrow` have no twin (a Qt widget's cursor never leaves it); the layer-list cases are 4.6's and 4.7's; the selection part of `duplicateAndDistortCursors` (Ctrl and Ctrl-Alt inside the selection) waits for 7.4. Moved on: the wand's click and cursor use to 7.3; the Ctrl-drag of pixels, `pixelDragStart` and the scissors to 7.4.
- [x] 7.3 `MagicWand` (`WandSampleSize`, `WandSettings`, `MagicWandError`, `MagicWand::select` and `outline` over `WandPixels.c`; the session's `wandSettings`, `magicWand`, `wandSample`), the wand's click on the canvas and its click inside a selection, the wand's controls in `LassoControls`. Tests: new `MagicWandTests` (Swift's six, the last through the canvas; plus refusals, a document gone mid-wand, the too-detailed error and Swift's titles), the wand's controls in `LassoControlsTests`, its settings and click in `SelectionSignalTests`. In scope by the third-round rule: the three fixes of 7.2 round 3.
- 7.3 round 1: FAIL, six findings. A painted asset's flatten threw past the sample's catch (the try covers the drawing, tested with a 48 MB raster starved by `AddressSpaceLimit`); a point that is no number passed the negative bounds and deselected (Swift's positive bounds refuse it); the amount field's validator refused a seventh digit (digits alone, a long long clamped, more digits than one holds the top); the active layer's own pixels were sampled without tests of placement, hidden and faint layers (a moved, scaled, hidden, masked, faint layer and a pending transform are read as they show); the document guard had no test (a snapshot with another id installed mid-wand drops the outline); the tests read `->path` (now `.value()`).
- 7.3 round 2: FAIL, four findings. The flatten test's 48 MB canvas sample failed before the raster flattened, so the narrowed catch passed alone (a tiny canvas keeps the large raster, and `malloc_trim` returns freed heap before the limit); the overflow test began at 255 (it starts at 7); two comments ran long; one test read `*path` (now `.value()`). Also in this round: `ProjectControllerTests::nothingToSaveOrABusySessionIsRefusedUnasked` took its record before the busy indicator had dimmed and compared it after up to a quarter second of work, a ceiling that a loaded machine breaks; it now waits for the dim first.
- 7.3 round 3: FAIL, two findings, the last round allowed. A new wand outline's Anti-alias setting had no test (a hard outline is asserted); one comment ran long. By the third-round rule the reviews of 7.3 end here; both fixes join 7.4's review scope.
- 7.4 round 1: FAIL, five findings. Paste dereferenced a cleared clipboard's missing `QMimeData` (`canPaste` and `paste` check the pointer; the refusals test clears the clipboard). Another app's image kept its own colour space's numbers (`sRGBCopy` converts a tagged image to sRGB first and tags the result; a linear gray 128 pastes as 188). A mask copied through a soft selection edge took the coverage once where Swift clips the ground and the white each (the two passes are multiplied on their own; `aSoftSelectionEdgeClipsBothMaskPasses` reads 192 at half coverage). Layer via Copy kept Duplicate Layer's enablement (now `canCopyPixels` or no selection on a pixel layer; a blank layer's Select All disables it). A working copy that could not be allocated set `brushError` where Swift's `try?` only beeps (a warning; `anImageTooLargeToCopyWarnsWithoutAnAlert` starves the address space). Eleven mutants over the fixes, all caught.
- 7.4 round 2: FAIL, two findings, both test gaps. Copy Merged's clip was tested through rectangles alone (a triangle lasso now asserts a clear far corner and a soft hypotenuse). A single layer's copy was not held to Swift's rule that the pixels come as stored, at their transformed places (`aCopyTakesTheLayersOwnPixelsAtTheirTransformedPlaces`: a hidden bar at 25% under a black mask, turned 90°, copies opaque where the turn puts it). Five mutants over those rules (the merged clip, opacity, mask, visibility, rotation), all caught.
- 7.4 round 3: PASS.
- 7.5 round 1: FAIL, six findings. A commit waiting for the preview could land on a later edit (the waiter carries its edit's id and returns without applying once that edit is gone); a second waiting caller lost its callback (every waiter is kept and resumed, the first commits, the next finds it committing); a commit between the worker's end and its delivery took nothing (the wait is on `preparing`, which only the delivery clears); Return on a focused Cancel cancelled (`setAutoDefault(false)`: Return is OK's from anywhere, nothing while OK waits); the mask carry was tested with white alone (a drawn mask's block is read at its carried place); growth was not held to the layer's turn (a layer turned 90° keeps it, its red block at the same document point after the commit). Seven mutants over the fixes (the waiter's id, the waiters kept, the wait on `preparing` twice, the mask carried as white, the turn dropped, Cancel's auto-default), all caught.
- 7.5 round 2: FAIL, two findings. Preview off while a job waited its turn behind a cancelled edit's worker cleared the job and left the edit preparing for good (an automatic kind's queued job now runs on and caches unseen; the clearing of `pending` and `preparedPreview` is 9.4's, for kinds with settings). The undelivered-preview test slept where it should have waited for the worker (`QThreadPool::globalInstance()->waitForDone()`, so the finish slot is queued and not yet run). Two mutants, both caught.
- 7.5 round 3: FAIL, one finding, the last round allowed. The Preview-off test did not force its job to wait behind the cancelled worker (the pool now holds one thread with a gated task ahead, and the test asserts the job is pending before Preview goes off). By the third-round rule the reviews of 7.5 end here; the fix joins 8.1's review scope.
- [x] 7.4 `SelectionClipboard` (`PixelClipboard`, `CopiedPixels`, `selectionCopyRegion`, `canCopyPixels`, `renderSelectedPixels`, `canCopyMerged`, `renderMergedPixels`, `copyMergedSelection`, `copySelection`, `store`, `canPaste`, `paste`, `layerViaCopy`, `addPixelLayer`, `nextLayerName`), system clipboard images both ways, the Edit menu's Copy, Copy Merged and Paste, the Layer menu's Layer via Copy. Tests: new `SelectionClipboardTests` (Swift's copy, layer-via-copy, lasso and merged cases; plus a mask's gray, the region's rounding, another app's image, the refusals and the signals), the entries in `SelectMenuTests`, the counts in `CompositorMenusTests`. In scope by the third-round rule: the two fixes of 7.3 round 3. Moved on: `cutSelection`, `PixelMove` (`beginPixelMove`, `movePixels`, `finishPixelMove`, `cancelPixelMove`, `nudgePixels`, the scissors and the Ctrl-drag on the canvas) and all of `FloatingSelection` (`FloatingTransform`, `canTransformSelection`, `transformCommand`, `beginSelectionTransform`, `floatingSelectionTransform`, `mergeFloatingTransform`, `cancelFloatingTransform`, `FloatingMerge`, `TransformEdit.floating`, `displayedSelection`'s floating term, the Transform Selection entry) to 8.2, since they clear or lift pixels through the brush raster; Swift's `cutLeavesAHole…`, `transformSelectionMoves…`, `escapeRestores…`, `applyingAnUnchangedTransform…` and `scalingAndMovingPastTheLayerEdge…` go with them. `SmartEditTests` is Content-Aware Fill's and Subject Removal's: 7.5 and 9.x.
- [x] 7.5 `ContentFill` (`ContentFill.c`), extend past edges: `ContentFillError`, `ContentFill::run`; from `Filters.swift` what the fill needs: `FilterKind` (its one case), `FilterJob`, `PixelFilter::run`, `FilterEdit` (`grow`, `previewImage`, `previewJob`), the session's `canAdjustColors`, `canContentAwareFill`, `beginFilter`, `updateFilter`, `renderFilterPreview`, `cancelFilter`, `commitFilter`; the grown transform in `displayedTransform` and `displayedMaskPlacement`; the canvas's preview and Shift-Delete; `FilterSheet` for the automatic kind; the Edit menu's Content-Aware Fill…; `canEditLayers` and `ProjectWorkspace::canSwitch` read `filterEdit`. Tests: new `SmartEditTests` (Swift's three fill cases; plus the growth past the edge with the mask carried, a solid, placed and disabled mask, a soft edge, the refusals and hooks, a layer changed meanwhile, the signals), new `FilterSheetTests`, the entry in `SelectMenuTests` and `CompositorMenusTests`, the keys and the preview in `SelectionCanvasTests`, the switch in `ProjectWorkspaceTests`. Moved on: `FilterSettings`, `FilterJob.settings`, `scale` and `seed`, the preview copy (`previewSource`, `previewScale`, `previewMapping`, `previewLimit`), `blurMargin`, `growForBlur`, `grownMargin`, the other kinds of `PixelFilter::run`, `beginFilter`'s beep guard and Gradient Map start, `filterSettings`, the trim on commit, `commitBackgroundMask` and the Filter menu to 9.4 and 9.7; `cancelCrop` in `beginFilter` to the crop; `previewAdjustmentEditing`, `finishAdjustmentEditing` and `adjustmentOriginal` to 9.5; `colorPicker` to 8.10; `FloatingPanel` to 9.1; `visionRequestRunsOnAnImage` to 9.7.
## 8 Painting and retouching (README)
- [x] 8.1 Brush coverage on CPU: `Rendering/BrushCoverage` (`Tile`, `Work`, `tile`, `render`; `BrushSegment` for Swift's SIMD4), `BrushSettings`, `BrushRaster::fill`, `BrushStroke` (the constructor and its guards, `spacingFraction`, `append`, `flush`, `continuousCurve`, `continuousKeys`, `renderContinuous`, `publish`, `allocateTile`, `patches`, `dirtyDocumentRect`, `selectionClip`, `pixelLimit`, `committedBounds`, `committedTransform`, `transform(for:)`, `paintSnapshot`, `commitInput`), `BrushCommit` (`render`, `expandMask`). Tests: new `BrushIntersectionTests` (Swift's three; `exportCrossingExample` drives the session and goes with 8.2), new `BrushTests` (the stroke-level halves of Swift's `continuousStrokeCrossesTiles…`, `softBrushProducesPartialAlpha…`, `brushStaysCircular…`, `maskedImagePainting…`, `paintedBoundsTrim…`, `opacityCaps…`, `softStrokeBuilds…`, `spacedDabs…`, `sparseMouseSamples…`, `liveStrokeReaches…`, `largeBlankCanvas…`; plus a selection clip, mask painting and `expandMask`, the refusals, the hard rim and the canvas edge, growth past the layer, a painted source that never flattens), `tests/BrushFixtures.h`. In scope by the third-round rule: the fix of 7.5 round 3. Moved on: the dab fallback (dead here, see AGENTS.md); `erasing`, `healing`, `healingMode`, `clearPixels`, `paintCanvas`, `liftSelection`, `moveLifted`, the session halves of `BrushTests` and `exportCrossingExample` to 8.2; the key tests (`bracketKeys…`, `shiftBrackets…`, `numberKeys…`) to 8.3; `heal` to 8.4; `clone`, `replacesWithClone` to 8.5; `isBlur` to 8.6; `fillGradient` and `fill` to 8.7; `editName` with its first writer.
- 8.1 round 1: FAIL, five findings. Tile keys overflowed `int` on a big grid (a 1200 px layer scaled to a point far across a 10,000 px canvas; keys, sets and maps are `qint64`); a crop that could not allocate returned an empty asset where Swift throws (`image.isNull()` is `ExportError(render)`, tested under `AddressSpaceLimit`); the tests read no untouched pixel inside a touched tile (both an ordinary and a painted source now read one beside the dab, in the preview and the commit); the antialias width under a scale was untested (a 64 px source shown at 128 reads 64 at the rim); an assertion ended in `|| true` (the materialisation is asserted before the read that flattens). Six mutants over the fixes: five caught, `columns` narrowed to `int` is equivalent (only the product passes int).
- 8.1 round 2: FAIL, three findings. A tile copy that could not be allocated (the tile's context from its base, or the coverage's copy under a selection) left a null image for `fill` to refuse as a logic error, where Swift throws its render error (both detach explicitly and throw `ExportError(render)`; `aTileThatCannotBeCopiedIsARenderError` takes every free chunk under an address-space limit, so the copy must map and fails); the selection's coverage was never tested on a transformed grid (a 64 px source shown at 128 from (40, 20), a selection from x = 80, a dab at (80, 80): 85 opaque, 75 clear); a mask on its own placement was never tested (a 16 px mask shown at 32 from (80, 40) takes a dab at (88, 48) in its pixel (4, 4)). Four mutants over the fixes: three caught, the coverage's null check is equivalent (a null coverage reaches the same error through its selection coverage's context).
- 8.1 round 3: FAIL, two findings, the last round allowed. The grid's extent became an `int` before its guard (a 30,000 px strip shown at a point far across a big canvas makes three billion pixels: undefined in C++, a plain refusal in Swift's `Int`; the extent is judged as a double first). The coverage-copy test could not tell the fix from the old logic error: under exhaustion the selection coverage's own context fails with the same error, and sparing one chunk for the copy does not hold (the painter's freed buffers serve the later copy), so that mutant stays equivalent in effect and the check keeps the error at the copy. By the third-round rule the reviews of 8.1 end here; the extent guard and this note join 8.2's review scope.
- [x] 8.2 `EditorSession+Brush` (`canPaint`, `makeRasterEdit`, `beginBrush`, `continueBrush`, `shiftLineStart`, `cancelBrush`, `finishBrushImmediately`, `finishBrush`, `commitPaintSnapshot`, `commitRasterEdit`, `usesOpacityKeys`, `typeOpacityDigit`, `changeBrushHardness`, `changeBrushSize`; `brushStroke`, `brushSettings`, `brushMode`, `maskPaintWhite`, `brushRevision`), erase, mask painting, Shift lines, `placedMaskPreview`; `BrushStroke::paintCanvas`, `fill`, `clearPixels`; `SelectionEdits` (`canEditPixels`, `fillSelection`, `clearSelectedPixels`, `deleteKeyPressed`, `applyPixelEdit`), `cutSelection`; the gates and hooks that read the stroke; the canvas's live stroke and Delete key, the list's Delete key; the Edit menu's Cut, the two fills and Clear Selection Pixels. Tests: new `BrushSessionTests` and `PixelEditTests` (the session halves of Swift's `BrushTests`, `shiftBracketsStepHardnessByQuarters`, the brush half of `numberKeysSetBrushAndGradientOpacity`, `largeBlankCanvas…`, `foldersHiddenLayers…`; plus the stroke's hold on the session, the Shift line, erase, the fills and clears, the Delete key, the signals), new `RasterSnapshotSessionTests` (Swift's five), new `BrushCanvasTests` (`canvasShift` twice, the repaint per step, a placed mask's stroke, the Delete key), new `BrushRasterTests` (failed tile copies, `clearPixels` and `fill`), `folderMaskCanBePaintedInvertedAndLoadedAsASelection` in `LayerMaskSessionTests`, the painting half of `placementAndLinkAreSavedAndPaintingFollowsThePlacement`, the `canPaint` line of `LayerTests`, `moveToolNumberKeysSetSelectedLayersOpacityAsOneUndo`, the cut in `SelectionClipboardTests`, the entries in `SelectMenuTests` and `CompositorMenusTests`, the list's Delete key. In scope by the third-round rule: the two fixes of 8.1 round 3. Moved on: `PixelMove` and `FloatingSelection` (`liftSelection`, `moveLifted`, the scissors, the Ctrl-drag, Transform Selection) to 8.2b; the palette's colours in `fillSelection` and `paletteColor` to 8.10; the tips' parking (`parkedBrushTips`, `tipFamily`) to 8.5; `warpStroke` to 8.6; the gradient half of `numberKeysSetBrushAndGradientOpacity` to 8.7; the canvas's brush press, drag, release, cursor and keys, `exportCrossingExample` and the rail's eraser icon to 8.3.
- 8.2 round 1: FAIL, five findings. Invert and the fill opened mid-stroke (`canInvert` and `canAdjustColors` read the stroke; a stroke on a red layer refuses both and the cancel frees them). Painting a folder's mask showed nothing until the commit (the live clip comes from the edit's tiles, tested at 1x and 3x). Every step of a stroke repainted the whole canvas (a step repaints the stroke's dirty rect alone, two points out; a paint spy reads the first tile and then two). A raster commit's mask guard was untested (the mask replaced while a clear is in flight: nothing lands, the project is freed). The session-wide budget was untested (six 16-million layers leave a stroke 4 million, and a 2000-pixel tip is refused; `pixelLimit` became `qint64` without a clamp, as Swift's `Int`, so seven other masks leave −12 million). Eight mutants over the fixes, all caught.
- 8.2 round 2: FAIL, two findings. The live folder-mask clip drew the old mask, or a painted mask's base, at full size under nearest sampling, so a fine mask shimmered while painted (both are halved near the size drawn first, as Swift's `displayImage`; a one-pixel-striped folder mask at 25% keeps an untouched pixel's shade within 3/255 during and after two strokes, the second on the painted mask's tiles). A raster commit put back the mask's switch as the stroke had read it, so a mask switched off meanwhile came back on (the commit keeps `isEnabled` as it stands, Swift's `current.mask?.isEnabled ?? mask.isEnabled`; a mask switched off while a clear is in flight stays off). Three mutants over the fixes, all caught. Since then, for the mirror rule and for Qt 6.11: `canAdjustColors` moved to `HueSaturation.cpp`, the twin of its Swift file, and `ToolHeaderBar` gained `Q_OBJECT`. Docker could not start containers that day (another job saturated the disk), so the build and tests ran in the same image copied into RAM, through bubblewrap.
- 8.2 round 3: FAIL, one finding, which ends the item's reviews by the third-round rule after five, two and one. In a text field both fills deleted forward at the start (`del()` after an empty backward selection turned "45" into "5"); they now delete the selection when there is one, else backwards from the caret through `backspace()`, as Swift's `deleteWordBackward` and `deleteToBeginningOfLine`, so nothing goes at the start. Proven by new cases in `cutTheFillsAndClearFollowTheSelectionAndTheField` (the caret at the start, mid-text, a selection either way round) and four mutants, all caught; carried into 8.2b's review.
- [x] 8.2b `PixelMove` (`beginPixelMove`, `movePixels`, `finishPixelMove`, `cancelPixelMove`, `nudgePixels`, `displayedSelection`'s terms, `BrushStroke::liftSelection` and `moveLifted`, the scissors, the Ctrl-drag and the Ctrl-arrows on the canvas) and `FloatingSelection` (`FloatingTransform`, `canTransformSelection`, `transformCommand`, `beginSelectionTransform`, `floatingSelectionTransform`, `mergeFloatingTransform`, `cancelFloatingTransform`, `FloatingMerge`, `TransformEdit.floating`, `DistortWarp::mapPath`, the floating branches of `commitTransform`, `cancelTransform` and `transformPixelSize`, the Transform Selection entry); `canEditLayers`, `canInvert`, `canAdjustColors` and `ProjectWorkspace::canSwitch` read `pixelMove`; `brushRevision` moves at the rest of Swift's sites (the invert; the filter's preview, Preview switched, cancel and commit), so the canvas's filter-preview key went. Tests: new `PixelMoveTests` (Swift's four pixel-move cases of `SelectionEditTests`, plus the hold on the session, a failed move, a move across tiles and back, a layer scaled and hanging off the canvas, a landing between pixels computed by hand, and what a move needs and ends), new `FloatingSelectionTests` (Swift's `transformSelectionMoves…`, `escapeRestores…`, `applyingAnUnchangedTransform…` and `scalingAndMovingPastTheLayerEdge…`, plus a distorted selection, a merge that cannot grow, the refusals, the floating layer's place and look, a source gone mid-lift, and a mask grown right, left and placed apart), `mapPathCarriesCurvesFlipsAndTheFillRule` in `DistortTests`, `cutLeavesAHoleAndPasteRestoresThePixels` (8.2's cut test under Swift's name), the scissors in `CursorTests`, the Ctrl-drag and Ctrl-arrows in `SelectionCanvasTests`, the switch in `ProjectWorkspaceTests`, the entry in `SelectMenuTests`, the counter in `SmartEditTests` and `SelectionEditTests`. In scope by the third-round rule: the fills' text-field fix of 8.2 round 3.
- 8.2b round 1: FAIL, three findings. Space mid-drag showed the open hand where Swift keeps the scissors or the copy cursor (`updateCursor` picks the pixel drag's cursor ahead of Space, and `lassoCursor` lost its two dead drag branches; `CursorTests` holds Space during a move and a copy drag). A merge onto a scaled or turned source was untested (`aTransformedSourceTakesTheMergeInItsOwnGrid`: a source doubled at 20,10 keeps its grid and transform with the block where it was put, and one turned a quarter lands the block below where it was lifted). A folded distortion's dropped outline was untested (`aFoldedDistortionDropsTheOutline`: a bow tie commits, the selection goes, Undo brings it back). Five mutants over the fixes, all caught.
- 8.2b round 2: FAIL, one finding. A move from a painted layer was untested, so a lift that flattened the whole raster passed (`aPaintedLayerMovesWithoutFlattening`: a dab makes the layer a raster, and the lift, the move and the commit leave it unflattened before any pixel is read; the hole and the landing are checked after). One mutant, caught.
- 8.2b round 3: FAIL, one finding, which ends the item's reviews by the third-round rule after three, one and one. The painted-source test moved red pixels onto red, so a lift that placed nothing passed; the move now goes 60 pixels into the blue half and red is asserted there. One mutant (the raster's pixels never drawn into the lift), caught; carried into 8.3's review.
- [x] 8.3 `BrushControls`, `BrushCursorOverlay`, the canvas's brush gestures (press, drag, release, Shift lines and their axis, the right-drag tip), keys on the canvas and in the list (Tab and `cycleToolMode`, B, E, digits, brackets), the rail's eraser icon and hint (`ContentView+Status.cpp`). **Gate** passed: twin of `BrushPerformanceTests`, 4K at a scale of 2, 800 px soft; median frame 6.8–7.4 ms, from 47 (see AGENTS.md, "Brush speed"). Fixed on the way: a shown `ContentView` destroyed with its canvas focused read freed members (it stops listening first), and a replaced tab's session ended before its editor (the window holds the shown tab). Tests: new `BrushGestureTests`, `BrushKeyTests`, `BrushControlsTests`, `BrushCursorOverlayTests`, `BrushPerformanceTests` (under `BRUSH_BENCHMARK=1`), `ContentViewStatusTests` (split from `ContentViewTests`); `aSoftDabFollowsItsFalloffWithinAStep` and `aSoftClipScalesEachCoverageByteOnce` in `BrushTests`, `aMaskHalvesAsItsColorTwin` and `aMaskEdgeRepeatsItsLastColumn` in `DownsampleTests`, `tabSwitchesEachToolsShapeOrMode` in `EditorSessionTests`, `tabAndTheBrushKeysStayWithTheList` in `LayerListFocusTests`, `aShownViewClosesWithoutHearingItsSession` in `ContentViewTests`, `aReplacedTabOutlivesItsEditor` in `ProjectWorkspaceViewTests`, the eraser in `ToolIconTests`, `exportCrossingExample` in `BrushIntersectionTests` (under its variable); the key tests moved on from 8.1. In scope by the third-round rule: the painted-source test of 8.2b round 3. Moved on: the colour swatch, the x and d keys and Alt's eyedropper to 8.10; the other brush tools' circle, tip, brackets, digits and modes to 8.4–8.8; the bracket filter's `levels` term to 9.x.
- 8.3 round 1: FAIL, five findings. A halving band allocated its sums and source row inside the worker, where QtConcurrent wraps `bad_alloc` past the caller's catch (the bands now take them from the caller's scratch; `bandsTakeTheirScratchFromTheCaller` refuses any lone row of sums). Wheel and pinch panned and zoomed mid-stroke (both refuse now, as Swift's; `aStrokeHoldsTheViewStill`). A hover after a busy release painted into the kept stroke (only a left-button drag goes on; `aBusyProjectHoldsTheStroke`). Near-hard soft tips missed the falloff by up to 46 levels, because the table spread its steps over the whole radius (it spans h² to the rim now, solid inside; `BrushCoverageTests` holds 0.9995, 0.9999 and 0.99999 across the fade, the reviewer's own pixel among them); the lower clamp that the core needs runs only for segments through it, which keeps the gate's 2.75 ms model time. The bar's arrows stepped from the rounded number (they step from the session's value, as Swift's `arrowSteps`). Seventeen mutants over the fixes: eleven caught; two found the table's double build and span no better than float to 0.99999, so both are float now; four are equivalents (the lower clamp at step 1, the clamped quadrature everywhere, the Lasso fields' rounding of whole values, twice).
- 8.3 round 2: FAIL, one finding. At hardness 0.99999 and above the table still missed the shader's formula (190 for 188 on a 10 px dab, 22 levels at 0.999999 and 2000 px): its steps had passed float's spacing. Where (1 − h²) / 8192 falls under float's epsilon, `tipDensity`, the shader's own float formula, now replaces the table for dabs and segments; the table builds from the same function. A scratch probe against the shader's quadrature, replayed in float, found the kernel exact on both sides of the switch (hardness 0 to 0.999999, 10 to 2000 px) and the table off by 4 levels on a near-hard segment. New `nearFullHardnessTheFormulaReplacesTheTable` (dabs from 0.999 to 0.999999, both pixels named in the reviews) and `aStrokesSegmentsFollowTheShaderEitherSideOfTheSwitch` in `BrushCoverageTests`. Eight mutants: five caught (the table on segments only once the stroke test existed); three equivalents (the formula for every hardness and the two early returns of `tipDensity`), recorded in AGENTS.md.
- 8.3 round 3: FAIL, one finding, which ends the item's reviews by the third-round rule after five, one and one. The quadrature's template regrouped the shader's `midpoint ± halfLength · node − projection` as `(midpoint − projection) ± halfLength · node`; 8192 pixels along a 0.999999-hard 10 px stroke that gave 14 where the shader gives 17, and the test's reference, copied from the twin, repeated the regrouping. The shader's order is back in both, and `farAlongAStrokeTheNodesRoundAsTheShaders` pins 17 at (8202, 10) before and after a flush. Two mutants (the regrouped order for both nodes, and for one), both caught; carried into 8.4's review.
- [x] 8.4 Spot Healing (`HealPixels.c`): `SpotHealingMode`, `BrushSettings.healing` and `healingMode`, the wash in `publish`, `BrushStroke::heal`; the session's `spotHealingMode`, Spot Healing in `beginBrush`, the heal in `finishBrushImmediately` and its step's name, the type on Tab; Swift's `isBrushTool` for the circle, the tip drag, the brackets, the digits and the bar; `BrushControls` titled for every brush tool, with the types and Strength. Tests: new `SpotHealingTests` (Swift's `healsTheBlemishUnderTheBrushAndNothingElse` in each type; plus a mask never healed, Tab and the digits, the wash then the surface, a mask stroke, the selection's bound, a painted source, the kernel's and the coverage's failed allocations, the types' difference and random grain, opacity part way, a spot across tiles), `spotHealingTakesTheBrushGestures` in `BrushGestureTests`, `eachBrushToolTitlesTheBarAndShowsItsPicker` in `BrushControlsTests`, the four brush tools' bars in `ContentViewTests`. In scope by the third-round rule: the fix of 8.3 round 3. Moved on: the clone's Aligned, Sample and Tab to 8.5; the Smear's modes and hints to 8.6; Alt's eyedropper under Spot Healing to 8.10.
- 8.4 round 1: FAIL, one finding. `aPaintedSourceHealsFromItsTiles` gave its raster no patches, so a heal that read the raster's base alone passed every test. The raster now paints the blemished stripes over a blue base: the spot must heal to the stripes' gray and five pixels away from the brush must keep the painted stripes, with nothing flattened. Two mutants (the heal drawing the base alone, and nothing), both caught. The 8.3 fix carried in held.
- 8.4 round 2: PASS. The painted-source test holds the heal to the raster's patches; the item closes after one finding and none.
- [x] 8.5 `CloneStamp` and its preview tip: `CloneSettings`, `setCloneSource`, `cloneStrokeOffset`, `cloneSamplePoint`, `cloneSample`; the session's clone state and `parkedBrushTips` in `selectTool`; the clone in `beginBrush`, `publish` (`clonePixels`, `fillClone`) and the step's name; Tab's sample; the bar's Aligned, Sample and note; the overlay's crosshair and preview; the canvas's Alt-click, hidden pointer and preview. On the way: the session header split into per-extension `Name+Session.h` fragments (AGENTS.md, mirror rule), `displayedSelection` moved to `SelectionEdits.cpp`, its Swift file. Tests: new `CloneStampTests` (Swift's two; plus the families' opacity and the Smear's tip, the sample point and offset, a refused source, the active or every visible layer, a mask, a scaled layer, half and quarter pixels, opacity and the selection), new `CloneStampCanvasTests` (the Alt-click, the crosshair and preview through a stroke, Alt and Space, the kept preview and what redraws it, one click's softness and the tip's size, the Brush's Alt-click, zoom, the layer or every layer, a cancelled stroke), `cloneStampShowsAlignedSampleAndTheSourceNote` in `BrushControlsTests`, the marker, the cut preview and identity in `BrushCursorOverlayTests`. Moved on: `replacesWithClone` and the clone on a mask to 8.6, with their first writer.
- 8.5 round 1: FAIL, one finding. The overlay cut the tip into the preview at the preview's size, which is capped at 1024 pixels, so a 2000-pixel hard brush at 200% previewed 220 of 255 three pixels inside its rim. The overlay now draws the preview and cuts it to the tip inside `AdjustmentSurface::draw`, Swift's transparency layer at the drawing's resolution, and keeps no cut copy. `theTipCutsAtTheDrawingsResolution` draws a tip of one-pixel columns over a preview enlarged four times, each column keeping or dropping the green, and checks the enlarged preview is smoothed. Five mutants (the old cut, the tip ignored, no smoothing, a square clip, no opacity), all caught. The header split was audited: no declaration lost or changed in access.
- 8.5 round 2: PASS. The tip cuts at the drawing's resolution, the header split holds; the item closes after one finding and none.
- [x] 8.6 `BlurTool`, `SmudgeLiquify`: `BlurToolMode`, `WarpStroke` (`append`, `pickUp`, `smudge`, `push`, `weight`), `beginWarp`, `finishWarp`, `blurSample`, `PixelAdjust::gaussianBlur` (Core Image's Gaussian); `BrushStroke`'s `isBlur`, `replacesWithClone` and `editName`, gray clones; the session's `blurMode` and `warpStroke` in Swift's six places, the warp in `beginBrush`, `continueBrush`, `cancelBrush` and `finishBrushImmediately`, Tab's mode, the step names; the bar's three modes and the Smear's hints; the canvas's warp copy and its holds. Tests: new `SmudgeLiquifyTests` (the warp against Swift's arithmetic written again, four brushes along one path; the clamps; Liquify's step, what it lands and Undo; Smudge's carry; clear pixels clearing; the mask refused; the session held; a changed layer; Tab), new `BlurToolTests` (the bell curve and a line's profile, clear and clamped edges, gray and refusals, sigma by size, a mask's tone, placement and turn, Blur strokes on pixels and masks, a mask between pixels), new `SmearCanvasTests` (the copy then the commit, Escape, the held view and tip, the copy through a mask and at half opacity), `theSmearShowsItsThreeModes` in `BrushControlsTests`, the Smear's words in `ContentViewStatusTests`. Swift has no tests of these tools; the twin's are its own. The owed note that `selectLayers` and `selectLayerTarget` read the warp was wrong: Swift's read the brush stroke alone.
- 8.6 round 1: FAIL, one finding. Blur resampled a mask's gray sample in gray, so at a fractional placement the padding past the image came in as opaque black: a white mask placed half a pixel past the canvas's left edge fell to 128 in its first column under a hard Blur stroke. `clonePixels` now resamples every clone into premultiplied colour, a gray one too, so the edge keeps its coverage as alpha, and `fillClone` composites that alpha into gray masks; `shiftedByFraction` is colour-only again. `aMaskPastTheCanvasEdgeKeepsItsTone` holds 255 over white and 128 over black in that column. Two mutants (the sample's alpha ignored, and gray taken as opaque), both caught.
- 8.6 round 2: PASS. The mask edge keeps its tone; the item closes after one finding and none.
- [x] 8.7 `Gradient` (`GradientStyle`, `GradientShape`, `GradientSettings`, `GradientEdit`; `beginGradient`, `moveGradient`, `refreshGradient`, `gradientColors`, `endGradientDrag`, `cancelGradient`, `commitGradient`, `resolveGradient`; `gradientSettings` and `gradientEdit`), `BrushStroke::fillGradient`, the palette's model from 8.10 (`PaletteColor`, `foregroundColor`, `backgroundColor`, `canEditPalette`, `paletteColor`, `setPaletteColor`, `swapPaletteColors`, `resetPaletteColors`) and its colours in `fillSelection`; the hooks owed (`selectLayer`, `selectLayers`, `selectTool` and `selectLayerTarget` resolve it, `undo`, `canUndo` and `restore`, `canEditLayers`, `invertPixels` and `beginFilter` commit it first, `ProjectWorkspace::canSwitch`); Tab's shape and the digits' opacity; the overlay's line and ends; the canvas's drag, ends, Shift, keys and a folder mask's live clip; `GradientControls` with its swatch. Tests: new `GradientTests` (Swift's eight; plus the resolve on a switch of tool, layer or target, the holds on layer edits and the workspace, Invert and filters applying it first, the gradient half of `numberKeysSetBrushAndGradientOpacity` and Tab, the palette, a mask's palette, a kept target, revisions, busy and lineless commits), new `GradientCanvasTests` (the drag and its line, the ends within ten points, Shift at 45°, Escape and Return, the overlay as Swift draws it, a folder's pending mask, the bar, the swatch), the bar in `ContentViewTests`. Moved on: the Type bar's share of `setPaletteColor` to 8.9; the picker, the swatches, Alt's eyedropper and the x and d keys to 8.10.
- 8.7 round 1: FAIL, one finding. The new tests read the pending gradient, the document and the overlay's geometry through `->`, against the rule that tests read a returned optional through `.value()`: a drag that left no edit would have aborted the binary under `_GLIBCXX_ASSERTIONS` instead of failing the test. Both files now read through `.value()`, and so do four older tests that read `document()` the same way (`CloneStampTests`, `CloneStampCanvasTests`, `ProjectDropTests`, `SmudgeLiquifyTests`). A mutant that loses the drag's edit now fails with QtTest's report. The reviewer found no functional issue; its own oracle matched 128 gradients within 2/255.
- 8.7 round 2: PASS. The tests read optionals through `.value()`, a lost edit fails with a report; the item closes after one finding and none.
- [x] 8.8 `ShapeTool` (`ShapeKind`, `path`, `LayerShapeStyle`, `LayerShape`, `ShapeDraft`, `liveShape`; the session's `shapeKind`, `shapeCornerRadius`, `shapeLineWidth`, `shapeDraft`, `beginShape`, `shapeLineEnds`, `dragShape`, `cancelShape`, `toggleShapeKind`, `finishShape`, `nextShapeName`, `redrawShape`, `shapeTransformPreview`, `shapeImage`), `ImageLayer.shape`, the manifest's `shape`, the hooks owed (`selectTool`, `commitTransform`, Tab) and the rebuilds that drop a shape, the resizers, the canvas's draft, gestures and keys, the Move tool's rounded preview, `ShapeControls`, the Shape hint by kind. Tests: new `ShapeToolTests` (Swift's four; plus a line's ends, box, width, caps and dot, Shift's eighths, the budget, names, refusals, Tab and kinds, signals, `path`, flat drags, smooth edges and thin lines), new `ShapeLayerTests` (redraws at the radius, rounded and one side, masks, groups, what is not redrawn, the Move preview and its cache, painting and every rebuild dropping the shape, a mask stroke and a duplicate keeping it, projects keeping live shapes only, the manifest's refusals, the resizers, older lines against independent strokes, a line's ends, a distorted shape), new `ShapeCanvasTests` (the draft and the layer, its place above the active layer, Shift and Alt, Escape and focus, Shift-U, a line's width and caps at any zoom, a rounded draft's corners, the repainted areas, a folder mask's clip, the Move preview, the bar), the Shape bar in `ContentViewTests`, the hint by kind in `ContentViewStatusTests`. Moved on: the Fill swatch and its picker to 8.10.
- 8.8 round 1: FAIL, two findings. A line a pixel wide took QPainter's thin-line path, so a diagonal covered a pixel at 64 where CoreGraphics' stroke gives 144: lines now fill a round-capped outline (`strokedLine`, `QPainterPathStroker`) on the layer and on the draft, and a width of 1 matches 1.001 within 3/255 in `edgesAreSmoothAndLinesAPixelWide` and `aPixelWideDraftIsItsOutline`. A draft on a blank layer stayed stale when the layer moved mid-drag, since `DisplayState` skips blank layers where Swift's observation redraws: the draft's target now counts, and `aDraftFollowsItsLayerInTheStack` raises it over a sibling, hides it, and holds that another blank layer still counts for nothing. Seven mutants (the old pen twice, the dot, the cap, the width, the filter twice), all caught.
- 8.8 round 2: FAIL, one finding. A new active layer mid-drag (Undo restoring an earlier one) moved the draft below another layer without a repaint, since both layers had pixels and `ShapeShown` did not name the draft's target: it now carries the active layer's id, and `aDraftFollowsANewTarget` undoes mid-drag, expects a paint and the upper layer on top. One mutant (the target left out), caught.
- 8.8 round 3: PASS. The draft follows its target through Undo; the item closes after three findings over two rounds.
- [x] 8.9a `TypeTool` (`TextAlignment`, `LayerTextStyle`, `LayerText`, `TextDraft`, `liveText`; the session's `textDraft`, `textDefaults`, `beginText` at a point and in a box, `editActiveText`, `applyText`, `finishText`, `cancelText`, `recolorText`, `currentTextStyle`, `changeTextStyle`, `layerName`, `textBoxSize`, `textImage`), `Rendering/TextLayout` (Linux-only: fontconfig's faces by PostScript name, TextKit's lines on `QTextLayout`), `ImageLayer.text`, the manifest's `text`, the hooks owed (the gates, `canStartProjectOperation`, `selectLayer`, `selectLayers` and `selectTool` finishing text, the swatch's colour, Fill's recolouring) and the rebuilds that drop text, the resizers, `TypeControls`, the panel's text rows. Tests: new `TypeToolTests` (Swift's eight), new `TextLayoutTests` (faces, measures, boxes, wrapping and dropping, alignment, baselines, the limits, a box under its padding, colour), new `TypeSessionTests` (the gates and file requests, switching, the swatch, Fill, styles, names, a click's target, the corner and the mask, dragged boxes, the style's bounds, the signals, what `applyText` takes, recolouring), new `TypeStoreTests` (the manifest's keys and refusals, invalid styles, the store's rule, stale text, every rebuild, the resizers), new `TypeControlsTests` (the bar's fields, formats, steps, borrowed focus, Return, alignments and their ink under a new theme, faces, buttons, layout, rest, and the list's text row), the Type bar in `ContentViewTests`, a macOS text style in `ProjectManifestTests`. Fixed on the way: the Move bar's fields took half the spare room and grew their buttons, where Swift's `ScrollView` takes it all and keeps its content's size (`theFieldsTakeTheSpareRoom` in `TransformInspectorTests`). Moved on: the colour swatch and `openTextColorPicker` to 8.10.
- 8.9a round 1: FAIL, four findings. `finishText` handed `applyText` the session's own draft by reference, and `applyText` cleared it before reading it (undefined: an -O0 probe lost the text; the -O2 suite passed with it, so no test can hold it): the draft is now taken by value, as Swift passes its struct. `TextLines` parted paragraphs at LF alone: CR, CRLF and U+2029 part them too (Qt breaks at U+2028 itself). Tabs took Qt's 80-pixel default: `NSParagraphStyle`'s twelve stops 28 points apart hold now, and a tab past the last one moves down a line and leads it, the paragraph laid in pieces (`setNumColumns` left the tab hanging on the line above). Two tests read an optional through `->`; they read `.value()`. Tests: `paragraphsBreakWhereCocoaBreaksThem` and a tab test in `TextLayoutTests`. Mutants of the stops, the pieces and the separators: 17, all caught; a dead offset in the tab scan went.
- 8.9a round 2: FAIL, two findings, both in round 1's emulation of Cocoa's late tabs (a tab past the twelfth stop moving down a line): the scan measured from a line's logical start, so after right-to-left text a late tab stayed put, and each late tab laid the rest of its paragraph again, 11.7 s for 10,000 tabs on a commit. The emulation went: tabs stop every 28 points throughout (`setTabStopDistance`), Cocoa's stops up to 336, and past them the stops go on where Cocoa wraps, a known difference recorded in AGENTS.md. `tabsStopEvery28Points` in `TextLayoutTests` holds the stops, a tab after text and the thirteenth stop; two mutants of the distance, caught.
- 8.9a round 3: PASS. Tabs stop every 28 points, the known difference past 336 recorded; 10,000 tabs lay out in about a millisecond. The item closes after six findings over two rounds.
- [x] 8.9b `InlineTextEditor` (`Rendering/InlineTextEditor.h/.cpp`, `InlineTextEditor+Editing.cpp`, `InlineTextEditor+Input.cpp`), the editing half: the model (content, selection, marked text), `synchronize` (logical size, shown transform, point text growing at the layer's scale, handle size, the clamp, a fresh draft's undo and focus), the text surface drawn under the layer's transform with its flips (text, selection, caret, marked underline), the frame, the handles and the overflow plus; the keys (typing as `QInputControl` judges it, deletions, movement, selection, Return, Tab, Escape, Ctrl+Return, Alt's spacing), input methods (marked text in the content, replacements counted from the preedit, events that bring nothing), local undo, the plain-text clipboard, the 100,000-unit cap; `TextLines` gains ranges, lines, the caret, line bounds, positions, a draw with a selection and marked text, and no line past the laid text; the canvas hides the edited layer, hands the editor its keys and input methods, lets its keys win over shortcuts, keeps Shift+Tab and takes the focus for a fresh draft; the menus' Undo, Redo, Cut, Copy, Paste and Select All reach the editor while a draft is open. Tests: new `InlineTextEditorTests` (growth and handles, undo steps, a draft set over another, moves, columns, wrapped and hidden lines, deletions, Return, Tab, Escape and finishing, spacing), new `InlineTextInputTests` (what types, Shift-+ and Shift-− in open text, the cap, the clipboard, composing, replacements, input methods following the caret), new `InlineTextDrawTests` (the draft instead of its layer, the frame and handles, a turned and flipped layer, the caret and its blink, the selection, the plus, focus, keys ahead of shortcuts, the Edit menu, repaints of edits, a paste, a shrunk layer left or replaced, the caret's line, a stretched layer opened), new `InlineTextPreeditTests` (the underline, the input method's formats and caret), `linesStopWhereTheBoxDoes` and `aSelectionFollowsItsRunsToTheLinesEdge` in `TextLayoutTests`, the fixtures `InlineTextFixtures.h` and `InlineTextDrawFixtures.h`.
- 8.9b round 1: FAIL, eight findings. A final newline had no line, so Home after Return went to the line above: `TextLines` lays TextKit's extra line for the caret, even past the box, and measures leave it out. Cancelling a composition dropped the redo: history follows Qt's input method contract, the preedit recording nothing and what commits or removes text making a typing step from the text before it. Input method events ignored a zero-length replacement's offset, `Selection` attributes, `TextFormat` attributes and the `Cursor` attribute's visibility and colour: each is applied now. Arrows moved forward through right-to-left text: in such a paragraph Right goes back and Left on, words alike, and a plain arrow collapses to its own side, as Qt's fields move. A selection across directions was drawn by logical extents: it comes from Qt's visual runs per line, stretched to the fixed line, and runs on to a line's edge where it passes the line, as TextKit's. Opening a layer stretched one way left its pixels showing: a draft that begins, ends or is replaced repaints the canvas, as Swift's `needsDisplay`. Tests: the extra line, right-to-left moves over LF, CR and U+2029, the preedit's history, the zero-length replacement, the `Selection` attribute, a stretched layer, the caret line's repaint, the new `InlineTextPreeditTests`, and the selection's runs, edges and margins. Mutants of the fixes: 53, all caught once two tests came in (a tall line's full highlight, letters after a preedit left bare); `drawSelection`'s early return went, since it only saved work.
- 8.9b round 2: FAIL, five findings; round 1's eight hold. A composition outlived a draft set over it: `synchronize` clears it with the steps. `inputMethod` now follows `QWidgetLineControl::processInputMethodEvent` step by step: input takes the selection before an offset counts, a range goes whole or not at all, the cursor lands where Qt's leaves it, and a `Selection` attribute's selection stays beside a preedit as its `anchor` until the next input takes it, drawn and queried, its move ending a typing run. A final U+2028 now gets the line Qt's `createLine` makes after it, the caret's when it ends the text, and a line ends before the separator breaking it. Tests: a composition ended by another draft, a final U+2028 and the line end before it, input taking a selection, the selection beside a preedit (drawn before and after it, queried, kept by an update, taken by a commit), a Selection ending a run, a mid-text deletion, a range whole or not at all, a final CR and U+2029, U+2028 inside a paragraph and a cut box. Mutants of the fixes: 35, all caught once one test came in (a selection after the preedit read without it); a redundant anchor write went.
- 8.9b round 3: FAIL, two findings; round 2's five hold. While a preedit showed, Copy, Cut and Paste ignored the selection an input method set beside it, and Select All was ignored by the next commit. The Edit commands now work around a preedit as Qt's fields do: on the text without it and its selection (`plain`), the preedit then going back at the cursor (`settle`); the input method's formats count from the preedit's start, so it can move. The input methods and the clipboard moved to `InlineTextEditor+Input.cpp`, keeping the editing file under 500 lines. Tests: `theEditMenuWorksAroundAPreedit` (copy, cut and its undo past a later commit, paste, Select All then a commit), a paste before a preedit, a format on a preedit that does not start the text. Mutants of the fixes: 15, all caught once three tests came in (Select All ending a typing run, a paste dropping the redo, typing straight into a paste). Under the third-round rule the reviews end here; these fixes go to 8.9c's review.
- [x] 8.9c The pointer on open text (`InlineTextEditor+Pointer.cpp`): the canvas's text gestures (`beginTextGesture`, `dragTextGesture`, `finishTextGesture`, `drawTextBoxDraft`), the editor's `press` (Swift's `hitTest`: the handles' bands, the padding, the text view), `NSTextView`'s clicks (the caret, Shift, words and paragraphs by double and triple clicks, drags by the click's unit, through the text's mirror), the handles' resizing (point text becoming a box, the 16-pixel floor, the limits) and their cursors turned with the box, a click or a handle landing a preedit, `TextLines::character`; the canvas's Type-tool press, double click, drag, release and Escape, and its cursor over the editor. Tab cycles a mode only without a draft, as 8.9b's keys already route it. Tests: new `InlineTextPointerTests` (a click or a drag beginning text, the box drawn in the accent and antialiased, dropped, thin drags, a refused box repainted away; open text finished and other text opened by a click, active or not, hidden text left shut, a box widened leftwards landing where it shows; the caret, drags, Shift and its drag, a move without the button, a later line, a flipped layer's clicks; words from any letter, word drags both ways, paragraphs with CRLF, a fourth click and a far third single; a click and a handle landing a preedit), new `InlineTextHandleTests` (the eight cursors, the bands either side of each edge, a press's cursor and a release's, a narrow box's third; a press making a box, each edge dragged, the 16-pixel floor, the 30,000-pixel limit; a turned box's edges; a scaled layer's growth kept and its 300,000-pixel limit; the padding and a drag from it).
- 8.9c round 1: FAIL, five findings; both carried 8.9b findings hold. An undone New Canvas mid-drag crashed the box's repaint: Swift's guards hold (`dragTextGesture`, and `textBoxDraftRect` for the drawing), so without a document the box draws, drags and makes nothing. Qt can lose a release where AppKit cannot: a move without the left button, a lost focus or the next press ends a text gesture (`endTextGesture`: the box drawn goes, repainted away; a selection or resize stops where it is). Shift-click extended by characters after a double or triple click: the click's unit outlives its release, and Shift drags on from it while the selection it made stands; a key's selection, other text or another draft bring the caret's unit back. A triple click on an empty last line took the paragraph before it: paragraphs are found from the position itself, and an empty last line has no word either. Clicks below the lines landed on the last one and drags above the first kept their x: below is the end of the laid text, above the start. Typing while the button is down ends the drag. Tests: `anUndoneCanvasLeavesTheBoxInert`, `aLostReleaseDropsTheBox`, `shiftExtendsByTheLastClicksUnit` and `blankSpaceAndEmptyLinesTakeClicks` in `InlineTextPointerTests`, `aLostReleaseEndsAResize` in `InlineTextHandleTests`. Mutants of the fixes: 39, then 6 again, all caught but `drawTextBoxDraft`'s guard (no box maps to a null rect, which draws nothing; recorded). The survivors took out a dead term (`synchronize`'s first call is always fresh) and `release`'s return value (the canvas's own release path ends alike), and brought a test: below an overfull box's lines is the end of what shows.
- 8.9c round 2: FAIL, three findings; round 1's five hold. A far drag over a squashed layer (a 30,000-pixel box ten pixels tall, font size 1) turned a row past `int`'s range into a wild index: the row is judged as a double first. An edit kept a held gesture: typing during a resize was undone by the next move, which wrote back the draft the resize began with (Swift's drag would too), and the same letter typed over its selection left the drag running; an edit (`publish`) and a spacing key (`changeSpacing`) now end a drag or a resize, as a press does. A style change kept Shift's unit, where Swift's style sync sets the selection anew and resets NSTextView's granularity: the unit goes wherever the selection is clamped, which also ends a drag there (a fresh draft of the same style keeps it, as Swift's). Tests: `aSquashedLayerTakesFarDrags` and `anEditEndsAResize` (typing, a spacing key) in `InlineTextHandleTests`; in `shiftExtendsByTheLastClicksUnit`, the spacing keys and a style set elsewhere, a style set mid-drag and the same letter typed over its selection (a fresh draft of the same style is no longer tested for a reset: Swift keeps its unit). Mutants of the fixes: 5, all caught.
- 8.9c round 3: FAIL, one finding; round 2's three hold. A valid rotation of 10^12 degrees drew the box as 280 degrees but gave its corners the cursor of none: the count of eighths passed `int`'s range, where Swift's `Int` holds it. The rotation is reduced by 360 first. Test: a 10^12-degree layer's corner cursor in `aTurnedBoxResizesAlongItsEdges`. Mutant: 1, caught. Under the third-round rule the reviews end here; this fix goes to 8.10's review.
- [x] 8.10a The palette and its picker: the rest of `ColorPalette` (`PickerHSB`, `quantized`, `hex`, the hex parser, `ColorPickerTarget` for the palette and text, `ColorPickerState`; the session's `colorPicker`, `openColorPicker`, `typeColor`, `openTextColorPicker`, `closeColorPicker`, `setColorPickerHSB`, `sampleIntoColorPicker`, `sampleCompositeColor`), `FloatingPanel` (from 9.1), `ColorPickerSheet` in its panel, `ColorPaletteControls` under the rail (swatches, swap, defaults, the mask's choice), the Brush bar's colour swatch, the Shape bar's Fill swatch and the Type bar's colour swatch, the X and D keys on the canvas and in the list, `ProjectWorkspace::canSwitch` reading `colorPicker`. Tests: new `ColorPickerTests` (Swift's eight, the hex parser's edges, a half-clear pixel sampled exactly, the gates, the text picker with and without a draft, a mask keeping its foreground), new `ColorPickerSheetTests` (the field and the strip picking with the left button, their drawing, the entries' typing, steps, bounds and borrowed focus, Return, Escape and the close button), new `ColorPaletteControlsTests` (the swatches and their repaints, the picker's panel, swap and defaults and their ink, the mask's choice, a busy project, the rail), new `ColorSwatchTests` (the three bars' swatches, X and D), new `FloatingPanelTests` (Swift's layout pass, placing, keeping and remembering, the keys on every show, the close button and Escape, refocus by name), the picker's gate in `ProjectWorkspaceTests`, the Fill label in `ShapeCanvasTests`. Moved on: the inline editor standing aside while the picker is open (Swift's `hitTest` and cursor) to 8.10b, with the picking it serves.
- 8.10a round 1: FAIL, two findings; the carried 8.9c finding holds. The picker's controls had no accessible names: each carries Swift's label (Saturation and brightness, Hue, New color, Red, Green, Blue, Hex color), each entry Swift's title as its placeholder, and the strip the hue's rounded degrees as its description, since Qt's plain widgets hold no settable value. The hue arrows were clipped at 0° and 360°, where SwiftUI lets them overflow the strip: the sheet draws them over the strip's place and repaints five points past either end. Tests: `everyControlCarriesSwiftsLabel` (read through `QAccessible`) in `ColorPickerSheetTests`, and in `theFieldStripAndPreviewDrawAsSwifts` the arrows past both ends, their antialiased edge and their repainted room. Mutants of the fixes: 23, all caught once the edge test came in.
- 8.10a round 2: PASS. The labels and the arrows' overflow hold at 1× and 2×; the item closes after two findings over one round.
- [x] 8.10b The Eyedropper and picking on the canvas: the tool's bar with `showsSampleRing`, `SampleRingOverlay`, the eyedropper cursor (`Rendering/EyedropperIcon`, shared with the rail), sampling by the Eyedropper, by Alt under the Brush, Spot Healing and the Gradient, and into the open picker, which takes the keys back; the canvas's focus request sparing the picker; the inline editor standing aside while the picker is open (its cursor and `hitTest`, through picking's rank). Tests: new `EyedropperTests` (the composite into the foreground, off the document, busy and with a mask; the ring's place, colours, repaint and setting; Alt under the three tools and not under Clone Stamp or the Shape tool, never during a stroke or a gradient drag, and again after a refused gradient press; the open picker taking samples and giving the keys back; the cursor, its drawing and its rank over Space; picking ending mid-drag; lost releases; Space panning; the inline editor standing aside, double clicks included; the brush circle; a mounted canvas; the bar).
- 8.10b round 1: FAIL, three findings. When Alt first showed in the press itself, the foreground's signal synced the display, saw picking begin and ended the sampling just started, so the drag sampled nothing and the ring stayed after the release: `syncPicking` runs before a sampling begins and on every move (Alt let go unseen ends it there). Picking's hover swallowed a right-button drag with Alt held or the picker open: the hover counts only with no button held, as Swift's `mouseMoved`, so the tip sizes as Swift's `rightMouseDragged` does. The "Sample Ring" checkbox did not follow the session: it follows every change under a `QSignalBlocker`. The tests split into `EyedropperTests` and `PickingTests` over `EyedropperFixtures.h`. Tests: `altFirstSeenInThePressSamplesOn` (the canvas synced as `ContentView` syncs it) and `aRightDragSizesTheTipWhilePicking` in `PickingTests`, the checkbox set from the session in `theBarTogglesTheRing`. Mutants of the fixes: 11, all caught.
- 8.10b round 2: PASS. The three fixes hold against the original probes; the item closes after three findings over one round.
## 9 Adjustments and filters (README)
- [x] 9.1a Levels in the session: `Levels` (`LevelsChannel`, `LevelRange`, `LevelsSettings`, `LevelsHistogramDisplay`, `LevelsJob`, `LevelsFilter` over `LevelsPixels.c`, `LevelsEdit`; the session's `levels`, `beginLevels`, `updateLevels`, `setLevelsSampleMode`, `cancelLevels`, `commitLevels`), `LevelsAutomatic` (`LevelsSample`, `LevelsAuto`, `LevelsSettings::sampling`, `autoLevels`, `sampleLevels`), and the hooks owed: the gates, `canAdjustColors` and `canStartProjectOperation` read `levels`, `selectLayer` and `selectTool` refuse while it is open, closing it resumes file requests, and a pending gradient lands first. Tests: new `LevelsTests` (Swift's nine, the soft edge held to Swift's test), new `LevelsSessionTests` (the gates and a waiting file request, a commit's busy project and its refusals, the gradient first, the scaled preview from a painted layer's tiles and a one-pixel line, the rebuild dropping a shape, one preview at a time, Auto waiting for the histogram, the sample mode's signal, sampling through the layer's placement), new `LevelsModelTests` (clamps and defaults, the input clamped, settings stored and compared normalized, the scale's sorted finite interior, auto's thousandth tails, a single tone, the widest channel, neutral's gamma and floor, sampling past a channel's range, the composite and outputs reset), new `LevelsEdgeTests` (an open transform and lasso ended, every signal and redraw, the queue dropped with Preview, an identity commit at once, stale runs that never land, the rebuild dropping text, the preview for its own layer, truncated scaled sides, samples at the document's edge, on clear and soft pixels), new `LevelsFailureTests` (a failed scaled copy, sample and commit reaching `brushError`, a failed count and preview leaving nothing), the shared `LevelsFixtures.h`. Fixed on the way: the commit flattened a painted layer on the UI thread outside any catch; the flatten moved into the worker.
- 9.1a round 1: FAIL, two findings. The histogram packed the selection's coverage into a `std::vector`, whose failed allocation threw `std::bad_alloc` past the worker's catch and aborted the app: the kernel accumulates, so it now takes one row at a time with the mask's own row, and the histogram's bins and the filter's tables live on the stack. A commit forgot the running preview but kept the job queued behind it, which then started and landed mid-commit: a commit drops the queue too. Tests: `histogramRowsKeepTheirOwnCoverage` in `LevelsModelTests` (a two-row selection three pixels wide, where mask rows are padded), a queued preview dropped by a commit in `stalePreviewsNeverLand`. Mutants of the fixes: 4, all caught.
- 9.1a round 2: PASS. Both fixes hold against the original memory-limit and commit-race probes; the item closes after two findings over one round.
- [x] 9.1b The Levels sheet in its panel and on the canvas: `LevelsSheet` (the channel, the histogram and its loading note, the input and output handles and fields, the eyedroppers and their note, Auto, Preview with Alt+P, Reset, the caption, Cancel and OK with the spinner, resting while it commits), the panel (`levelsPanel`, cancelled by its close), the canvas's preview in place of the layer, the sample mode in `picking` and its press with the panel refocused, presses held to Space, Hand and Zoom while Levels is open, its keys (Escape, Return, Alt+P, Space alone), the brackets refused, `ContentView`'s drop guard and New canvas button, the Image menu's Levels… (Ctrl+L), Undo and Redo bare, Import Images resting. Tests: new `LevelsSheetTests` (the loading note, names and labels, the layout, the histogram's bars, colours and repaint, entries as Swift's bindings, the eyedroppers and their glyph, Auto, Preview and Reset, Cancel and the resting commit, a theme change), new `LevelsHandleTests` (the triangles, every handle's drag and bounds, the topmost press), new `LevelsCanvasTests` (the preview in place, a sample press handing the panel the keys, presses held to Space, the Hand and the Zoom, the keys, the brackets, the panel following the edit, Return from an entry or any button reaching OK), the shared `LevelsSheetFixtures.h`, the Levels panel's layout pass in `FloatingPanelTests`, the menu entry and its gates in `SelectMenuTests` and `CompositorMenusTests`, the eyedroppers' device pixels in `HighDpiCanvasTests`.
- 9.1b round 1: FAIL, two findings. The entries read and printed numbers with a period whatever the locale, so a German `2,5` went back to the old gamma, where Swift's `.number` format follows the user's locale: they read and print through the widget's locale now. Two new tests read a layer's asset through `->`: `.value()` now. Tests: `entriesReadAndWriteTheLocale` in `LevelsSheetTests`. Mutants of the fix: 2, both caught. The same gap in the Type bar's size and tracking (8.9a) is carried into 9.2.
- 9.1b round 2: PASS. Both fixes hold against the round's locale and panel probes; the item closes after two findings over one round.
- [x] 9.2 `Curves` (`CurvePoint`, `CurvesSettings`: validity, Swift's monotone Hermite `value`, `apply` through `levels_apply`), `CurvesControls` (the channel, the graph and its gesture, the readout, Remove point, Reset curve), `ImageAdjustments` (`ImageAdjustmentPixels`, `AdjustmentColor`, `ExposureSettings`, `GradientMapSettings`, `GrainSettings` over `AdjustPixels.c`); `PixelAdjust` and `PixelInvert` came forward in 7.1, 8.x and 9.1a, and Swift's `PixelInvert.thumbnail` shim has no twin (its callers call `PixelAdjust::thumbnail`). Their callers arrive with 9.4 (filters) and 9.5 (adjustment layers). Tests: new `ImageAdjustmentTests` (Swift's exposure, gradient map and grain tests, and the model half of `curvesIdentityAndImageCommandPreserveAlpha`), new `ImageAdjustmentModelTests` (curves' validity, interpolation and channels, every setting's range, normalizing and table, the gradient map's ends and colours, the grain's seed and place, the kernel's pixels), new `CurvesControlsTests`. In scope, carried from 9.1b's review: the Type bar's size and tracking read and print the locale's decimal mark, as Swift's `.number` fields (`sizeAndTrackingFollowTheLocale` in `TypeControlsTests`, whose fixture moved to `TypeControlsFixtures.h`).
- 9.2 round 1: PASS. No findings; the item closes in one round.
- [x] 9.3a Hue/Saturation in the session: `HueSaturation` (`ColorRange`, `HueBand`, `HueSampleMode`, `HueTargetDrag`, `RangeAdjustment`, `HueSaturationSettings`, `HueSaturationJob`, `AdjustedPixels`, `HueSaturationFilter` with Core Image's colour cube on the CPU, `HueSaturationEdit`; the session's `hueSaturation`, `beginHueSaturation`, `updateHueSaturation`, `commitHueSaturation`, `sampledHue`, `sampleHueRange`, the targeted adjustment, `cancelHueSaturation`), and the hooks owed: `canEditLayers`, `ProjectWorkspace::canSwitch`, `beginLevels`, `canContentAwareFill` and `beginFilter` read `hueSaturation`, `beginHueSaturation` resolves a pending gradient. Tests: `HueSaturationTests` (Swift's twelve), new `HueSaturationModelTests` (ranges, bands, settings, the HSL maths, the cube, the filter, the preview's size), `HueSaturationSessionTests` (gates, queue, commits, signals, samples, targeting), `HueSaturationFailureTests` (memory: the preview, the edit's copy, the commit's flatten, the filter's rows).
- 9.3a round 1: FAIL, one finding. A preview whose worker had finished but whose callback had not yet run counted as idle, so a new job replaced the watcher's future and Qt dropped the old result, and with it a failure's `brushError`, where Swift's task clears itself only after its await. A run now counts as running until its finish has run (`m_hueSaturationRendering`); Levels' and the filters' queues had the same gate (`isRunning`, and `isFinished` too, end with the worker: probed on 6.4.2) and take the same fix. Tests: `aDoneRenderLandsBeforeTheNextStarts` in `HueSaturationSessionTests`, `aFailureReportsThoughTheNextPreviewWaits` in `HueSaturationFailureTests`, `aDoneRunLandsBeforeTheNextStarts` in `LevelsEdgeTests`, a done but undelivered fill in `commitsThatWaitForThePreviewEachResume` (`SmartEditTests`). Mutants of the fixes: 9, all caught.
- 9.3a round 2: PASS. The fix holds against the round's reproduction, and restoring the old gate fails each queue's regression; the item closes after one finding over one round.
- [x] 9.3b The Hue/Saturation sheet in its panel and on the canvas: `HueSaturationSheet` with `SpectrumEditor`, the panel (`adjustmentPanel`), the canvas's preview, `picking`'s hue sample mode and targeting with their presses, drags and double clicks, the Image menu's Hue/Saturation… (Ctrl+U) and the `hueSaturation` term of Levels…. Tests: new `HueSaturationSheetTests` (layout, bindings, fields, toggles), `HueSaturationSpectrumTests` (the drawn glyphs, the spectrum's drags and paint), `HueSaturationCanvasTests` (preview, eyedropper, targeting, the panel, Return); `adjustmentPanelSurvivesALayoutPass` in `FloatingPanelTests`, `hueSaturationOpensOnPixelsAndRestsLevels` in `SelectMenuTests`, the shortcut in `CompositorMenusTests`.
- 9.3b round 1: FAIL, one finding. With the Brush chosen, Alt pressed during a targeted drag turned the cursor into the eyedropper, and it stayed so while the drag still changed saturation, where Swift sets the resize cursor again on every targeted drag. `targetingMove` does the same now. Test: Alt mid-drag in `targetingDragsSaturationOrWithCtrlTheHue`. Mutants of the fix: 3, all caught.
- 9.3b round 2: PASS. The fix holds against the round's reproduction, and removing it fails the new test; the item closes after one finding over one round.
- [x] 9.4a Filters in the session: `FilterKind`'s blurs, noise, lens and four image adjustments (Remove Background comes with 9.7), `FilterSettings`, `FilterJob` with settings, scale and seed, `PixelFilter::run` for each kind (`NoisePixels.c` and `LensPixels.c` compiled in place, Core Image's motion blur on the CPU), `FilterEdit`'s preview copy and blur growth, the session's remembered `filterSettings`, `beginFilter` for every kind (Gradient Map from the palette), `updateFilter(settings, preview)`, the commit's no-op closes and the blur's trim, and the colour picker's Gradient Map target. Tests: new `FilterTests` (Swift's), `FilterModelTests`, `FilterSessionTests` and `FilterFailureTests`; in `ImageAdjustmentTests` Swift's `imageMenuExposureChangesTheLayerInOneStep`, `gradientMapColorsUseTheAppColorPicker` and the command half of `curvesIdentityAndImageCommandPreserveAlpha`. A fix beyond Swift: a covering mask follows a trimmed blur's box. The Filter menu's `isImageAdjustment` and its assertion in Swift's Exposure test arrive with 9.4b.
- 9.4a round 1: no verdict. The service withheld the reviewer's output while it probed allocation failures through a malloc interposer; its probes pointed at two paths, both fixed before round 2. `PixelFilter::trimmed` handed on a null crop when the copy found no memory, so a blur could commit a null asset: it throws `ExportError(render)` now (`aTrimThatFindsNoMemoryIsARenderError`). The motion blur's row list, and the Gaussian's now that it runs in the filter worker, threw `bad_alloc` past the worker's catch: both go through `rowNumbers`, which turns it into `ExportError(render)` (`theBlursFailAsAContextWhenTheirRowsFindNoMemory`); removing either conversion, or the crop's check, fails the new tests.
- 9.4a round 2: PASS. Both allocation fixes hold, and the reviewer's four mutants of them (the crop's check, the conversion, each blur's use of it) fail the new tests; the item closes after two rounds, the first without a verdict.
- [x] 9.4b The filter sheet in its panel: `FilterSheet` (each kind's controls, `GradientMapControls`, `CurvesControls` hosted) in a `FloatingPanel` (`filterPanel`), the Image menu's Curves…, Exposure…, Gradient Map… and Grain…, and the Filter menu (with `isImageAdjustment` and the list of kinds, its readers). Tests: `FilterSheetTests` (7.5's, moved to the panel), new `FilterControlsTests`, the menus' in `SelectMenuTests` and `CompositorMenusTests`, `allFilterKinds` and `isImageAdjustment` in `FilterModelTests`, and the `isImageAdjustment` line of Swift's `imageMenuExposureChangesTheLayerInOneStep`.
- 9.4b round 1: FAIL, one finding. Arrow keys stalled on the filter sliders: a step of a thousandth rounded back to the setting, the update still notified, and the sync put the thumb back, so 100 Right presses left Radius at 1. A step that rounds to the setting it already holds now changes nothing, so the thumb keeps its place and steps add up (`arrowKeysStepTheSlidersOn`: 100 presses take Radius from 1 to 3.8 and Angle from 0 to 31; removing the guard fails it).
- 9.4b round 2: PASS. The fix clears every stall of round 1, drags that return, typed precision and Preview off hold, and removing the guard fails the regression; the item closes after one finding over one round.
- [x] 9.5a Adjustment layers in the document and the store: `AdjustmentKind`, `LayerAdjustment` (validity, `resolvedHSV`), `ImageLayer.adjustment` and everything that carries a layer's fields (the record, the snapshot and its install, `hierarchyRecord`, duplicates, copies between projects, the resizers), the manifest's `adjustment` (Swift's synthesized `Codable`: optional settings left out when absent, `ColorRange`-keyed dictionaries as key and value pairs), the store's rule (version 7, no pixels, no folder, valid; text never on an adjustment), `LiveMaskGraph.validate`'s rule that an adjustment layer is no clipping source, `canLinkMask`'s and `canPaint`'s terms, `addAdjustment` and `updateAdjustment`. Tests: Swift's `settingsSaveAndOlderAdjustmentsStillOpen` (in `ImageAdjustmentTests`) and `legacyHSVStillDecodesAndRenders` (a new `AdjustmentLayerTests`), new `LayerAdjustmentTests`, an unbaked copy in `ProjectCopyTests`.
- 9.5a round 1: FAIL, one finding. A Grain seed written `1e-400` fails Qt's whole parse (an illegal number) where Swift's decoder reads zero, and nothing recorded it. AGENTS.md's note on Qt's parser now names it, and `aSeedReadsWhatQtsParserReads` pins it beside what agrees: a whole number written `5.0` or `5e0` reads as Swift reads it.
- 9.5a round 2: PASS. The note and the pin match the round's reproduction; the item closes after one finding over one round.
- [x] 9.5b Adjustment layers drawn: `LayerAdjustment::apply` with its region, `LiveMaskRenderer.adjust` with its three hooks and the adjustment branches of `prepareStacks` and `drawComposite`, their wiring in `ImageExporter.render`, `drawLiveComposite` (through `AdjustmentSurface` when an adjustment exists) and the canvas, a clipped adjustment inside its stack. Tests: Swift's `AdjustmentLayerTests` (the curves command test moved there from `ImageAdjustmentTests`) with five of the twin's own, two cases in `LiveMaskRendererTests`, three in `CanvasDisplayTests`.
- 9.5b round 1: FAIL, three findings. `adjust` mixed bytes as RGBA while its snapshot kept the target's format, so an ARGB32 target swapped red and blue; and its copy back kept the target's pixel ratio, so a ratio-2 target drew doubled. The snapshot is now `RGBA8888_Premultiplied` at ratio one, and the copy back cancels the painter's whole device transform (`anAdjustmentTakesAnyImageTarget`: ARGB32, and ratio two against a painter scaled twice, under normal and Multiply; undoing any of the three changes fails it). The app reaches `adjust` only with such images already, so the fixes guard the renderer's own contract. Third, AGENTS.md claimed Grain stays in the document on the canvas; like Swift's, the canvas hands it the view's region: the entry now says so and `grainFollowsTheViewOnTheCanvas` pins it.
- 9.5b round 2: FAIL, one finding. Both renderer fixes hold against the round 1 probes, and the entry matches the canvas. But `grainFollowsTheViewOnTheCanvas` kept the random seed its layer was made with, and under seed 95 the pan leaves the sampled pixel at 133: the test now pins seed 7, which passes both of its checks every run.
- 9.5b round 3: PASS. The pinned seed holds; the item closes after four findings over two rounds.
- [x] 9.5c Editing adjustments in the session: `AdjustmentEditing` (`beginAdjustmentEditing` over the layers beneath, `previewAdjustmentEditing`, `finishAdjustmentEditing`), `adjustmentOriginal` and `adjustmentEditingID` with its setter, `addAdjustment` opening the editor, the hooks in Levels, Hue/Saturation and the filters, `adjustmentEditingID` in `canEditLayers` and `canStartProjectOperation`, `AdjustmentKind`'s `filterKind`. Tests: Swift's `AdjustmentEditorTests` (every kind) and `newAdjustmentLayersStartFromThePaletteRenderAndEditInThePanel`, with the twin's own for the refusals, a failed render, the gates and file requests.
- 9.5c round 1: FAIL, one finding. `finishAdjustmentEditing` called directly with a Gradient Map picker open closes the filter edit under the picker, and closing the picker then read the missing edit through `value()` and threw: 9.4a had left out `setGradientMapColor`'s guards as dead, which 9.5c revived. The guards are back as Swift has them (no filter, or another kind, writes nothing; the `committing` term stays out as dead). Test: `aGradientMapPickerOutlivesItsEditor` (OK and Cancel, the picker closed over no filter and over a Curves edit opened meanwhile). Mutants of the fix: 3, all caught. The first pass ran 76 mutants: 72 caught once four rewrites built, two equivalent, and the dead folder term removed.
- 9.5c round 2: PASS. The crash no longer reproduces on OK or Cancel, and the `committing` term's omission matches the commit's order; the item closes after one finding over one round.
- [x] 9.5d Editing adjustments in the window: the layers panel's `.task(id:)` that begins editing, its footer's New adjustment layer menu, the rows' adjustment icons (six kinds, drawn), their words and double click, no link for an adjustment's mask; the Layer menu's New Adjustment Layer and Edit Adjustment…; the sheets' captions for an adjustment. Tests: Swift's `adjustmentEditorsUseMovableNonmodalPanels` (every kind) in `FloatingPanelTests`, the panel's and the menus' entries, the rows. Also: `GradientCanvasTests::theBarSetsTheGradient` waited for the commit's step but not for the edit to end, which happens one turn of the event loop later, so under a loaded suite its mask was refused; it now waits for both.
- 9.5d round 1: PASS. The menus, rows, glyphs, captions and the editor's lifecycle match Swift and the recorded adaptations; probes of all six kinds, reopening and a panel made anew passed. The item closes with no findings.
- [x] 9.6a Layer effects in the document and the session: `LayerEffects` (the four effects, `LayerEffectKind`, `LayerEffectSelection`), the manifest's `effects`, `ImageLayer.effects` where Swift lists it (equality, the record, duplicates; the rebuilds and a copy between projects that leave it out), the session's `effectsEditing`, `effectsEditingOriginal`, `effectSelection`, `canEditEffects`, `activeEffects`, `editingEffects`, `selectedEffect`, `addEffect`, `selectEffect`, `finishEffectsEditing`, `setEffects`, `changeEffects`, `canCopyEffect`, `copyEffect`, `toggleEffect`, `removeSelectedEffect`, the colour picker's effect target (`openEffectColorPicker`, `previewEffectColor`), and the hooks (`selectLayer`, `selectLayers` and `selectLayerTarget` clear `effectSelection`; `deleteLayerOrMask` removes a selected effect first). Swift has no effects tests: the twin's pin Swift's code.
- 9.6a round 1: PASS. The code matches Swift and the recorded adaptations (the fix beyond Swift that saves and loads effects among them); probes of picker transitions, refused selections and codec edge cases passed. The item closes with no findings. Mutants: 119, all caught once ten were rewritten to build, except three `isGroup` terms removed as dead.
- [x] 9.6b Layer effects drawn: `LayerEffectsKernel` (Metal's passes on the CPU, threaded), `LayerEffectsRenderer` (`cached`, `placed`, `margin`, `render` and its coverages), effects in `ImageExporter::render` and `drawLiveComposite`. Tests: new `LayerEffectsKernelTests` (the shader's loops replayed, byte for byte) and `LayerEffectsRendererTests`.
- 9.6b round 1: FAIL, three findings. A mask smaller than its layer was scaled up nearest, so its edge went hard where CoreGraphics' clip scales smoothly: `masked` now sets `SmoothPixmapTransform` (a 2 by 1 mask over 64 by 16 ramps 123 to 132 across the middle). The composition's workers called `scanLine()` on the shared result, and `detach()` bumps a non-atomic counter: they now write through one pointer taken first (the result's detach count stays under 8 on a 4,000-row image). The spread's queues took the larger side squared, 112 MB for a 30,000 by 10 layer: each pass now needs its bands times its line length. Tests: `thinLayersAndTallOnesAreCheap` (the thin layer under a 64 MiB limit, the detach count) and the mask ramp in `renderingPadsMasksThenRunsTheKernel`; undoing each fix fails them. The other parallel workers in the tree already take their pointers first.
- 9.6b round 2: PASS. The three fixes hold against the original reproductions; the item closes after three findings over one round.
- [x] 9.6c Layer effects on the canvas: `EffectsPreviewCache` (one worker, a 60 ms wait, the newest request, seeds, the shared budget), `LayerEffectsSurface` (a painted layer's effects redone as far as the paint reaches), `DistortEffectsCache`, `distortedEffects` and `commitDistort`'s seed, the canvas's `DisplayState` effects, `prepare`, the surface's hand-over and `drawOwn`'s branches in Swift's order. Fixes beyond Swift, recorded in AGENTS.md: the inner shadow scaled in a reduced preview; the surface widened and read by a reach that counts the inner shadow, its windows stopped at its bounds; one request a draw. Tests: new `EffectsPreviewCacheTests`, `LayerEffectsSurfaceTests`, `EffectsCanvasTests`, `EffectsFailureTests`; a distorted layer's mask in `DistortCanvasTests`.
- 9.6c round 1: FAIL, two findings. A reduced preview halved a mask by its layer's reduction, so a 2 by 1 mask on a 6,000-pixel layer became one gray pixel and the whole preview half clear: each source is now reduced by its own measure (a 2 by 1 mask in `aMaskHidesPixelsInTheReducedPreview`). A surface pass that could not copy the surface painted into a null image, published it and marked its tiles taken, so the effects vanished for the rest of the stroke: a pass now paints on a copy, checks its painter, and publishes the image and marks its tiles only once whole (`aStarvedLaterPassKeepsTheLastSurface`, first in `EffectsFailureTests`, since heap memory an earlier test freed would serve the copy without a new mapping). Undoing each fix fails its test; the redundant `detach` before the painter (which detaches itself) went.
- 9.6c round 2: FAIL, one finding. The round 1 fix took the larger of the two axes' scales, so a source that shrinks across and grows down went unfiltered: a 6000 by 1 striped mask on a 1527 by 25 preview aliased to alpha 0 to 255. Each source is now scaled by `QImage::scaled`'s smooth filter, each axis on its own (a mask as alpha, so its values pass exactly); the stripes a row high reduce to alpha 125 to 129, as the same stripes a hundred rows high, in `aMaskHidesPixelsInTheReducedPreview`. Undoing the filter, the alpha route or its format fails the test.
- 9.6c round 3: PASS. The fix holds against the stripes in both directions and the earlier reproductions (allocation failure included); 44 masks of constant coverage keep their values exactly. The item closes after three findings over three rounds.
- [x] 9.6d Layer effects' panel: `EffectsSheet` (each kind's controls, the colour swatches, Cancel and OK) in `ContentView`'s `effectsPanel` with the vanished-effect check, the footer's effects menu, the delete words for an effect (the trash's help, the Layer menu's title), the canvas press that clears `effectSelection`, and the session writers they need.
- 9.6d round 1: FAIL, one finding. An arrow step changed the effect from the session's value but left typed text in the field, so Return then applied the typing over the step: with Size at 4, typing 12 and pressing Up gave 5, then 12. A step now drops the typing and shows the stepped value, even where it clamps, as the picker's entries do (`theSheetEditsItsEffect`, at 23 and at the 500 bound). Leaving out either the flag's reset or the refresh fails the test.
- 9.6d round 2: FAIL, one finding. A wheel over the Size slider changed the size while the field kept the keys and its typing, so Return then applied the typing over the wheel: from 4, typing 12 and a notch down gave 2.497, then 12. The slider's change now drops the field's typing and shows its value, as a step does (`aWheelOnTheSliderReplacesTheTyping`, then Return keeping the wheel's value). Leaving out either the flag's reset or the refresh fails the test. The older sheets keep a field's typing across their own sliders, handles and pickers too (Levels, Hue/Saturation, the filters, the colour picker); 9.6f fixes them.
- 9.6d round 3: PASS. Both fixes hold, clamped steps and wheel changes included; 48 probes of the controls passed, unrelated pending typing and the panel's layer among them. The item closes after two findings over three rounds.
- [x] 9.6e Layer effects in the list: the effect rows under a layer (eye, name, selection, a click to choose, a double click to edit, an Alt-drag copy onto another row), the rows' heights, the list's press order (the clipping strip, then an effect row, else the choice clears) and its selection hidden while an effect is chosen.
- 9.6e round 1: PASS. Window-level probes of the panel's focus, a row rebuilt under its own press, eye and row drags, the selection's painting and a stale armed press all held; 86 mutants, all caught once six were rewritten to build.
- [x] 9.6f Pending typing yields to the same value's other controls in the older sheets, as 9.6d's review found: a wheel over a slider (the filters, Hue/Saturation), a dragged Levels handle, the colour picker's field and strip. Each replaces its entry's typing and shows its value, with tests.
- 9.6f round 1: PASS. Window-level probes held, and eight independent mutants removing a reset or a refresh were caught; 14 mutants, all caught once the Levels test pressed a handle without moving.
- [x] 9.7 Remove Background: `SubjectRemoval` (U²-Net on ONNX Runtime where Swift runs Vision: the model's input and mask, `noSubject` and a `model` failure, the mask cache, Advanced's refine, shift and contrast, the preview and the commit's mask), `GuidedMatte`, the kind with its quality and three settings, the full-size preview, `commitBackgroundMask` (a fix beyond Swift: a placed mask's placement goes with its pixels), the sheet's words, Quality and Advanced rows, the Filter menu's entry. The model chosen with its licence checked on the upstream repositories: U²-Net (Apache-2.0, `linux/licenses/U-2-Net.txt`) as rembg converted it (MIT); ONNX Runtime (MIT) from Microsoft's build in the dev image, from the distributions' packages elsewhere; both fetched by pinned checksum in `Dockerfile.dev`. Tests: new `GuidedMatteTests`, `SubjectRemovalTests`, `SubjectRemovalModelTests`, `RemoveBackgroundTests`; the sheet in `FilterControlsTests`, the kind in `FilterModelTests` and both menu tests.
- 9.7 round 1: FAIL, one finding. The preview's result shared the layer's pixels until the loop wrote them, so a copy that found no memory left a null image, stored as the preview with no error (a 6000 by 4000 layer with its mask cached, 8 MiB of room). The result is now copied before the loop, and a failed copy is `ExportError(render)` (`SubjectRemovalFailureTests`, a binary of its own). Leaving out the copy or its check fails the test.
- 9.7 round 2: FAIL, one finding. The model's input buffer threw `std::bad_alloc`, which passed the workers' catches and rethrew on the UI thread (an uncached 320 by 320 image, 512 KiB of room). `vision`, the door every model step goes through, now turns `std::bad_alloc` into `ExportError(render)`, covering ONNX Runtime's wrapper as well (`aModelRunThatFindsNoMemoryIsARenderError`, glibc's mmap threshold fixed so the buffer maps memory the limit counts). Leaving out the conversion fails the test. Swept after: `subjectMask` now refuses a kept mask of another grid as a `logic_error` rather than reading past it.
- 9.7 round 3: PASS. Both earlier failures now report recoverable errors; a failed commit keeps the layer and the history; independent mutants removing either fix were caught. The item closes after two findings over three rounds.
## 10 Canvas and files (README)
- [x] 10.1 `Crop`: `CropGeometry`, `CropDrag`, `CropSnap`, the session's `cropSnapTargets`, `visibleCropRect`, `cropRatio`, `cancelCrop`, `changeCropRatio` and `commitCrop`, its state (`cropRect`, `cropRatioChoice`, `cropError`) and hooks (see "Session hooks owed"); the canvas's gestures (`EditorCanvas+Crop.cpp`), cursors, the overlay's frame and the crop union in `renderBounds`; `CropControls` and the `Couldn’t crop` alert. Tests: new `CropTests` (Swift's six session and geometry tests and the twin's own geometry), `CropSessionTests`, `CropFailureTests`, `CropCanvasTests` (Swift's `cropDraggingRedrawsOnlyTheOverlay` and the gestures), `CropControlsTests`, the bar and the alert in `ContentViewTests`. Moved on: `flipCanvas`'s `cancelCrop` to 10.2.
- 10.1 round 1: FAIL, two findings. A crop that found no memory for the resized records (1,024 layers, 512 KiB of room) threw `std::bad_alloc` past the worker's catch, which rethrew on the UI thread and left the project busy: `CanvasResizer::resize` now turns it into `ExportError(render)`, which the worker catches (`CropFailureTests`, a binary of its own whose workers share the main heap). A press that drew a new frame took its cursor after dropping the old frame, so beside the canvas's edge it showed a resize arrow where Swift keeps the crosshair it showed: the cursor is now taken first (`theCursorShowsWhatAPressTakes`, a press at 5, 150). Leaving out the conversion, the worker's catch or the early cursor fails its test.
- 10.1 round 2: FAIL, one finding. Memory could still run out on the UI thread: the snapshot copied before the worker started, and the landing (the result's copy, the new document, the history's step) after the frame was dropped, so the project stayed busy and `done` never ran. `commitCrop` now takes the snapshot and starts the worker before anything changes, `finishCropCommit` moves the result out and lands it, dropping the frame only once it has landed, and `applyDocumentSize` builds the document and records the step on a copy of the history before publishing either (`DocumentHistory::adopt`); each failure sets `cropError` to the render error and frees the project. Tests in `CropFailureTests`: the snapshot, the worker and the landing each run out in turn, and a sweep of rooms from nothing to 16 MiB finds every size change landing whole or not at all; `aCropIsOneStepThatEndsRedoAndMarksTheProject` pins what `adopt` carries. Leaving out any guard, the staging or any of `adopt`'s three fields fails a test; taking a copy of the result instead of moving it only asks more memory.
- 10.1 round 3: FAIL, one finding (P3). A test read a returned optional through `->` (`cancellationAndViewportMappingDoNotEditDocument`, three reads), against the tests' rule; they read through `.value()` now, and the crop tests hold no other such read. The runtime findings of rounds 1 and 2 were verified fixed, with eleven independent mutants caught. The item closes after three rounds, the last finding fixed.
- [x] 10.2 `CanvasSizeSheet`, `ImageSizeSheet`, the controller's `canvasSize` and `imageSize` (the sheets over the window, the resize off the UI thread, alerts), `flipCanvas`, the Image menu's Canvas Size, Image Size and Flip Canvas entries. Tests: new `CanvasSizeSheetTests`, `ImageSizeSheetTests`, `ProjectResizeTests`, `ProjectResizeFailureTests`; the flips in `LayerFlipTests`, the entries in `CompositorMenusTests`.
- 10.2 round 1: FAIL, two findings. A wheel over Units changes the unit without taking the focus, so a size still being typed was read in the new unit (50 pixels typed, Units scrolled to Inches, Return: 5,000 pixels at 100 dpi); both sheets now take the focus to Units first, so the typing commits in the unit it meant (`aWheelOverUnitsCommitsTheTypingFirst` in both sheet tests, which activate the sheet since focus moves only in the active window; two mutants, caught). Three test reads of optionals used `->` or `*`; they read through `.value()` now.
- 10.2 round 2: FAIL, one finding (P3). The sheets printed a halfway number away from zero (17 pixels at 16 per inch showed 1.063 inches) where Foundation's formatter rounds halves to even (1.062); both sheets round the thousandths with `std::nearbyint` before printing (`halfwayNumbersRoundToEven` in both sheet tests, 1.0625 and 1.1875; two mutants, caught).
- 10.2 round 3: PASS, no findings.
- [x] 10.3 `JPEGExportSheet`, the controller's `exportPNG` and `exportJPEG` (the panels, the sheet over the window, the work off the UI thread, alerts), the cancellation points of `ImageExporter.jpeg`, the render's memory door, `ByteCounts` (Foundation's byte counts, shared with the canvas sheet), the File menu's Export PNG and Export JPEG entries; Copy Merged came with 7.4. Tests: new `JPEGExportSheetTests`, `ProjectExportTests`, `ProjectExportFailureTests`, `ByteCountsTests`; the cancellation points in `JPEGExportTests`, the entries in `CompositorMenusTests`.
- 10.3 round 1: FAIL, three findings. The JPEG encoding let `std::bad_alloc` escape its worker (a 24-megapixel noisy image with 170 MiB of room); `jpeg` turns it into `ExportError(render)` at its door, as `render` does (`anEncodingWithoutRoomIsARenderError`). The PNG snapshot's copy for the worker ran unguarded in the panel's callback, leaving the project busy; it is guarded and explained, as is `exportWork`'s launch (`aPanelsHandOffThatFindsNoMemoryIsExplained`). The JPEG sheet still used a file-local byte count; it calls `ByteCounts::file`. Mutants: the two doors' conversions and the hand-off's guard, caught; the launch's own guard is recorded as unreached.
- 10.3 round 2: FAIL, one finding. The panel's callback, and the refusal's alert, still copied the completion that holds the PNG snapshot (a megabyte for 1,024 layers), so a copy could fail before the panel opened or while the refusal was prepared; `exportPanel` and `showError` move their callbacks now (`thePanelOpensWithoutCopyingTheSnapshot`, room for one snapshot but not two; `aRefusedNameCopiesNoSnapshot`; three mutants, caught).
- 10.3 round 3: PASS, no findings.
- [x] 10.4 `ImageFileDrop` (`importProviders`, `holdsImages`): files and pictures dropped from file managers and other apps, the file branch of `receiveProviders`, and files and pictures taken by the canvas, the tab buttons and the New canvas button. Tests: new `ImageFileDropTests` (Swift's `fileDropProvidersReachImporterInOrder`, a picture copied to a file, what cannot be read, the drop targets).
- 10.4 round 1: FAIL, four findings. A dropped picture's copy was trusted after `QFile::write`, which only buffers, so a copy cut short (a file size limit) imported gray pixels; the flush is checked and a failed copy removed (`aPictureThatCannotBeKeptIsUnreadable`). Several files went to the importer at once, so a second failure replaced the first; `receiveProviders` now imports one provider at a time behind the error before it, Swift's gate (`eachFileWaitsBehindTheErrorBeforeIt`). An open import alert kept its first words; `showAlert` updates it (`anOpenAlertShowsTheGrownError`). A canvas without a workspace refused files; it imports into its own session under Swift's guards (`aCanvasAloneImportsUnderSwiftsGuards`).
- 10.4 round 2: FAIL, one finding. The whole-drag provider copied every advertised format, so a lazy drag offering five pictures handed over 54 MB for a 22 KB PNG and could run out of memory past every catch; it now copies the preferred picture alone (`aLazyDragGivesOnlyThePictureChosen`, a drag that records what it is asked for). Mutants: copying every format again, caught; keeping the web addresses, equivalent and so left out.
- 10.4 round 3: PASS, no findings.
- [x] 10.5 `SliderSnap` (an application event filter that snaps a track press before Qt tracks it, installed in `main`); `NavigationToolHeader`, `ToolHeaderStyle`, Hand and Zoom came with 4.3b and 4.4. Tests: new `SliderSnapTests`.
- 10.5 round 1: FAIL, two findings. A slider whose owner rounds the value (Gaussian Blur's radius) moved the knob away from the press, so Qt took the press for a groove click and left the slider held down; the snap now presses where the knob is and consumes the original. A left press with another button held snapped, though Qt refuses it; the snap now asks for the left button alone. Tests: a rounding owner's drag and release, a held button; four mutants, caught.
- 10.5 round 2: PASS, no findings.
## 11 Fluency, packaging, distros
- [x] 11.1 Performance pass: pan/zoom frame time at 4K with 10 layers, timings logged, compared with `docs/brush-performance.md`. Done so far: `CanvasPerformanceTests` (logged medians and p95s, regression guards) and `HslBlend::blend` in parallel rows (Hue layers 3× faster); see AGENTS.md, "Canvas speed (11.1)".
- 11.1 round 1: FAIL, two findings. The parallel blend deadlocked an export worker on a one-thread pool; so did the older parallel kernels (the Gaussian blur, probed), since Qt 6.4's `blockingMap` waits on a full pool from inside it. All eleven now go through `PoolMap::blocking`, which lends a pool thread's place while it waits (`PoolMapTests`; two mutants, caught). The benchmark's "100%" set zoom 2, which is 200% at a backing scale of two; it sets `zoom(1)` now, as Actual Pixels does, and asserts it; the honest median there is 68 ms, and the guards and the entry say so.
- 11.1 round 2: PASS, no findings.
- [x] 11.2 Desktop integration: `.desktop`, icon, `.comp` MIME type, `cmake --install` (`linux/packaging`, the install rules, the desktop file name in `main`, `COMPOSITOR_MODEL`). Tests: new `DesktopIntegrationTests`.
- 11.2 round 1: FAIL, four findings. `StartupWMClass` named the app id where Qt 6.4's X11 class is `Compositor`; it says `Compositor`. The model kept its own file name; it is installed as `u2net.onnx`. The install test could not pass for a build given the model; it reads the build's cache and expects the model then (checked by hand in a second build configured with it). A direct install left the project type unregistered; it runs `update-mime-database`, required, when no `DESTDIR` stages it (`aDirectInstallRegistersTheProjectType`, a stand-in tool; two mutants, caught).
- 11.2 round 2: FAIL, two findings. A model given as a link was installed as the link, dangling once the build machine's file went; the file itself is installed now (`file(REAL_PATH)`), and the staged test asserts no link. The direct-install test inherited a caller's `DESTDIR`; it clears it. Both checked by hand, since the suite runs without a model or a `DESTDIR`: a second build given a linked model, and the suite under `DESTDIR` set.
- 11.2 round 3: PASS, no findings.
- [x] 11.3 Packaging: `PKGBUILD` (Arch, Omarchy), `flake.nix` (NixOS), CPack DEB (Ubuntu) and RPM (Fedora); the app's licence installed, `COMPOSITOR_BUNDLE_ORT` for Ubuntu, packaging tools in `Dockerfile.dev`. The DEB is built and read back here; the others are built in 11.4.
- 11.3 round 1: FAIL, three findings. The bundled runtime shipped without Microsoft's licence and notices; the image keeps them from the release and a bundling install lays them under `share/licenses/compositor/onnxruntime`, a configure without them failing. The install test failed for a bundling build; it reads `COMPOSITOR_BUNDLE_ORT` and expects the library, its version link and the notices (a second build, bundling with the model, passes). A flake comment ran to eleven words.
- 11.3 round 2: FAIL, two findings in the install test: it looked for the runtime in `lib`, where a `/usr` prefix on Ubuntu gives `lib/x86_64-linux-gnu`, and took only `ON` for the bundle option, where CMake takes any true value. CMake now hands the test its own evaluated values (`COMPOSITOR_BUNDLED`, `COMPOSITOR_LIBDIR`, `COMPOSITOR_MODEL_PATH`), and a second build bundling with `TRUE` under `/usr` passes.
- 11.3 round 3: PASS, no findings.
- [x] 11.4 Distro checks in Docker: build and full test run on Ubuntu 24.04, Fedora, Arch, NixOS. Seen early on the Arch host (Qt 6.11.2, GCC 16), in a build without warnings as errors: `findChild` needs `Q_OBJECT` on the class (fixed in 8.2: `ToolHeaderBar`); 13 of 107 targets fail or time out: BrushTests (the address-space crop test commits), HighDpiCanvasTests, ImageImportTests, TiledPieceTests, ToolIconTests, TransformInspectorTests, FilterSheetTests (Return on a focused Cancel no longer waits for OK), CompositorMenusTests; ContentViewTests, ProjectOpenTests, ProjectWorkspaceFlowTests, ProjectWorkspaceViewTests and ProjectControllerTests time out. Each is judged here: a Qt change the app must follow, or a test that leans on 6.4.
- 11.4 state: `linux/scripts/distro-check.sh arch|fedora|nixos` builds `linux/scripts/distro/<name>/Dockerfile` and runs dev.sh's configure, build and full test run in it (warnings as errors, the tree mounted read-only); NixOS builds the flake's package, then runs the tests in the flake's new `devShells.default` (plugin path, DejaVu through fontconfig, the model on the data path). Real containers start now through `sudo docker`; Ubuntu is the dev image itself. Still to do: run Arch (started), Fedora and NixOS, judge each failure, build the RPM on Fedora (`cpack -G RPM`) and the PKGBUILD on Arch (`makepkg`, as a user, not root), then review.
- 11.4 Arch run 1 (Qt 6.11.2, GCC 16, in a container): the build needed three fixes (`QImage::flipped` from Qt 6.9 in `ImageImporter`, `Q_OBJECT` on `PickerField` and `SwatchButton` for Qt 6.11's `findChild`, a local pragma for GCC 16's false `-Wmismatched-new-delete` in `DownsampleTests`' replaced operators); 193 of 205 tests pass. Failing, to judge: BrushTests, SpotHealingTests, TextLayoutTests, ImageImportTests, OmarchyThemeTests, ProjectWorkspaceViewTests, ProjectTests, EffectsCanvasTests (aborted), EffectsFailureTests, SubjectRemovalFailureTests, ProjectExportTests, ImageFileDropTests. Log: the scratchpad's arch-check.log.
- 11.4 Arch run 2 onward: running as uid 1000 (as dev.sh) cleared the four tests root broke (read-only folders and unreadable files mean nothing to root). Fixed since: TextLayoutTests counted Ubuntu's DejaVu split (now: the named faces, sorted, once each); ImageFileDropTests dropped before the last import ended (now waits for `canSwitch`); ImageImportTests (libheif 1.23's longer message; Qt 6.9's TIFF plugin logs libtiff errors as criticals, expected from 6.9 on); EffectsCanvasTests: Qt 6.11's `QTransform::map` clips w for points past the horizon where Swift divides by it whatever its sign, so `DistortWarp` projects points itself (`project` in `Distort.cpp`). Left: ProjectWorkspaceViewTests (the app launched with a file) and the memory-starvation tests (BrushTests, SpotHealingTests, EffectsFailureTests, SubjectRemovalFailureTests). A kept Arch build: `~/.cache/compositor-arch` (mount it at /build in compositor-arch).
- 11.4 Arch, later fixes: ProjectWorkspaceViewTests launched the app, whose logs a journald-built Qt sends to the journal when stderr is no console; the test sets `QT_FORCE_STDERR_LOGGING` for it. The five starvation tests (BrushTests' crop, SpotHealingTests' two, EffectsFailureTests' later pass, SubjectRemovalFailureTests' model run) place their failure by Qt 6.4's own allocations (the address limit itself works on Arch, probed: 20 MB refused under a 2 MB margin); they skip off Qt 6.4 (`placesStarvation` in `AddressSpaceLimit.h`), and BrushTests, SpotHealingTests and EffectsFailureTests fix glibc's mmap threshold as the other failure binaries do. Full Arch check rerunning.
- 11.4 Arch, the last timeout: ProjectManifestTests ran 50–120 s in the container against 0.7 s on Ubuntu, its CPU idle: files synced under a `HOME` on the container's overlay filesystem wait on this machine's busy disk (1 s with `HOME` in RAM). The check now keeps `HOME` and `TMPDIR` on a 4 GB tmpfs, as the dev image's RAM build does; an environment matter, not the app's.
- 11.4 after the reboot (real Docker, no sudo): Arch 205 of 205. Fedora 42 (Qt 6.10) 203 of 205, both fonts: Fedora splits DejaVu, so its image installs Serif and Mono beside Sans, as Ubuntu's `fonts-dejavu-core` and Arch's `ttf-dejavu` carry them; and Fedora's own default sans is Noto (`default-fonts-core-sans`), in which the Type tool's "Aa" draws, so `ToolIconTests` compares the Type cells with the DejaVu sheet only under DejaVu Sans (the other tests hold that icon's ink, box and uniqueness under any face; a band 8 points narrower fails on Fedora). Rerun: 205 of 205.
- 11.4 packages: the RPM (`cpack -G RPM` on Fedora) installs with dnf on a fresh Fedora 42 with RPM Fusion, which brings `libheif-freeworld`; its trigger registers the project type, and every library resolves. The PKGBUILD builds with `makepkg` as a user on Arch and installs with pacman, whose hook registers the project type; every library resolves. It builds from the checkout, `Compositor/Assets.xcassets` (the icons) included.
- 11.4 NixOS: the flake had no `flake.lock`, and the tree is read-only there; the lock is committed now, pinning nixpkgs. The package builds. Its first test run, as root, failed the four tests root breaks and three on fonts: the shell's `makeFontsConf` reads its rules from `/etc/fonts/conf.d`, which NixOS keeps and a bare container lacks, so Helvetica fell to DejaVu Math TeX Gyre; the shell now includes fontconfig's own `conf.d` from the store. Nix writes its store as root with root's home, and the tests then drop to uid 1000 (`chroot --userspec`, coreutils) with `HOME` and `TMPDIR` back on the tmpfs, since `nix develop` points `TMPDIR` at a folder root made.
- [x] 11.4b Standalone OmaPhoto: the port moves to its own public repository, `ZacharyZhang-NY/OmaPhoto` (the user's decision, 2026-09-26), the app at the root, the kernels in `kernels/`, the icons in `packaging/icons/`; renamed where a user sees it (name, binary, app id, desktop entry, icon, packages, logging, build options), Compositor's project format kept; the README credits Compositor; one install script per family in `scripts/install/`; `release.yml` builds and publishes the packages on a tag. Version 1.0.0.
- 11.4 and 11.4b round 1 (Fable 5.1, max): FAIL, six findings. The DEB names Ubuntu 24.04's `t64` Qt libraries, which no later Ubuntu publishes, yet the README promised "24.04 or later" and `ubuntu.sh` let 26.04 through to apt's refusal: the script now takes `UBUNTU_CODENAME=noble` alone and the README says so. AGENTS.md said `lib/compositor` for the bundled runtime (now `lib/omaphoto`). The PKGBUILD's comment said `linux/packaging/arch`. `nixos.sh` installed `main`, not the release the README named: it now resolves the newest release's tag from GitHub's redirect and installs the flake there. Nothing tied the tag to the version the files carry: the workflow's first job refuses a tag that names another (checked by hand against a matching tag, a wrong CMake, PKGBUILD and flake version). No test pinned `project`: `DistortTests::pointsPastTheHorizonDivideAsSwiftDoes` carries a box above a trapezoid's horizon (w from -2 to -1) through `carried` and `mapPath`; on Arch the body and both call sites mutated to `map.map` each fail it, on Qt 6.4 they are equivalent (6.4's `map` divides too).
- 11.4 and 11.4b round 2: PASS, no findings.
- [x] 11.4c NixOS: the flake leaves out `dev.sh`'s `build/` (the package's own build folder collided with it); ProjectExportFailureTests crashes in the full NixOS run, never alone: find the cause.
- 11.4c work: the flake's source leaves out the top-level `build/` (`cleanSourceWith`); the NixOS run built the package with it. The crash reproduced every time under ctest and run directly in a kept container with the check's own build; gdb put it in fontconfig's `FcHashTableFind`, under `QMessageBox::setText` in `showError`, under the PNG export's failed snapshot: the alert opened while the 512 KB limit held, and it was the process's first font lookup, whose failed allocation fontconfig dereferences. The app had already handled the failure (it logged the export error). `main` now resolves the alert's fonts before the tests, as the app's window does before any export: five direct runs and ctest pass in the container; with the PNG or the JPEG snapshot's alert removed (`mutate.py`), the test still fails.
- 11.4c round 1: FAIL, two findings. `thePanelOpensWithoutCopyingTheSnapshot` passed only on the heap its siblings left: alone on the dev image it failed 40 of 40 (it crashed 45 of 45 before the fix), since its limit also held the panel's first open. `initTestCase` now opens a panel and an alert once, the refusal flow, before any limit (the fonts line in `main` went); every test alone and the whole binary pass 40 of 40 on Ubuntu, Arch and NixOS. AGENTS.md put the first font lookup on NixOS alone; it is the first on every system. `mutate.py` then found the JPEG render's capture could copy the snapshot unseen (a gap since 10.3): `theJPEGRenderTakesTheSnapshotWithoutACopy` holds it to a move under a 2 MB limit, the pool thread held so the render waits; the four mutants (both snapshot alerts removed, both captures copied) each fail.
- 11.4c round 2: PASS, no findings.
- [x] 11.4d Issue #5: the RPM demands Fedora 42's exact ONNX Runtime (`libonnxruntime.so.1(VERS_1.20.1)`), and ONNX Runtime stamps a new symbol version on every release, so any other Fedora or update refuses it; the Arch package breaks at run time the same way. The user's decisions (2026-09-26): packages carry Microsoft's runtime they were built with, as the DEB does (the RPM and the Arch package bundle it; the Fedora and Arch images test against it too); the RPM builds on the oldest Fedora still supported (43); a second DEB built on Ubuntu 26.04 LTS (resolute) joins the release beside 24.04's (noble), `Dockerfile.dev` taking the base as an argument, and `ubuntu.sh` picks by `UBUNTU_CODENAME`.
- 11.4d work: the Fedora image is 43 and, like the Arch image, carries Microsoft's ONNX Runtime 1.21.0 (the dev image's block, pinned) in place of the system's; `Dockerfile.dev` takes `BASE`, and `distro-check.sh resolute` runs it on Ubuntu 26.04. The RPM and the Arch package bundle the runtime (`OMAPHOTO_BUNDLE_ORT`): the RPM keeps it out of what it provides and requires, the PKGBUILD fetches the tarball by checksum, lays it out for its CMake config in `prepare()` and drops `onnxruntime`. The release builds `omaphoto-noble_amd64.deb` and `omaphoto-resolute_amd64.deb` (a matrix), and `ubuntu.sh` takes the one its `UBUNTU_CODENAME` names. Evidence: 205 of 205 on Ubuntu 24.04, Arch, Fedora 43 and Ubuntu 26.04; the workflow's package commands build all four; each installs and starts in fresh containers (24.04, 26.04, Fedora 43, 44 and 44 beside `onnxruntime`, Arch and Arch beside `onnxruntime-cpu`), `ldd` finding the bundled runtime; 24.04's DEB is refused on 26.04; the RPM names no `onnxruntime` among its requires or provides; `ubuntu.sh` against a stand-in `curl` installs the right DEB on 24.04 and 26.04 and refuses Debian 13.
- 11.4d round 1: FAIL, three findings, all words: `arch.sh` still said pacman asks which runtime provides `onnxruntime` (the line went), the README's distro checks left out `resolute`, the workflow's heading said three packages (four).
- 11.4d round 2: PASS, no findings. Committed; pushed with 1.0.1's tag, since `ubuntu.sh` asks for its asset names.
- [x] 11.4e Issues #1 and #2: `dev.sh run` reaches the host's files (the home folder) and the GPU's render nodes, and builds before it runs.
- 11.4e work: `dev.sh run` builds first, mounts the home folder at its own path with `HOME` there (the theme's separate mount went: it now reads from its real path), and passes each render node with its group. Evidence: before, the same EGL and Zink warnings as issue #2 (`run-nodev.log`), the window whole on this Hyprland host (`run-nodev.png`), so the black areas did not reproduce here; after, `dev.sh run ~/Pictures/Wallpapers/kafka-5k2k.jpg` built, imported the wallpaper from the home folder (`run-dev.log`, `run-dev.png`), read the theme from its real path, and `docker inspect` showed both render nodes, group 987 and `HOME=/home/zacharyzhang`; the Zink error went, and this host's NVIDIA driver leaves Mesa's dri2 warning.
- 11.4e round 1: PASS, no findings.
- [x] 11.4f Release 1.0.1: the fixes above, the Node 24 actions, the install scripts rerun against the new release; the issues answered and closed.
- 11.4f work: version 1.0.1 in CMakeLists.txt, the PKGBUILD and the flake; `ProjectWorkspaceViewTests` reads the build's `OMAPHOTO_VERSION` in place of a literal. `main` and `v1.0.1` pushed together (`--atomic`), since `ubuntu.sh` asks for 1.0.1's asset names. The release run passed every job (the version check, two DEBs, the RPM, the Arch package, publish) on the Node 24 actions, no deprecation warning, and published `omaphoto-noble_amd64.deb`, `omaphoto-resolute_amd64.deb`, `omaphoto.x86_64.rpm`, `omaphoto-x86_64.pkg.tar.zst` and `SHA256SUMS`. Each install script, fetched from GitHub as the README says, in fresh containers: Ubuntu 24.04 and 26.04, Fedora 43 and 44 and Arch verified their package, installed it and started 1.0.1 with the bundled runtime; Debian 13 was refused; `nixos.sh` resolved `v1.0.1`, built its flake and started 1.0.1 (`live-installs.log`, `live-nixos.log`). Issues #1, #2 and #5 answered and closed; #2's reply says the black areas did not reproduce here. Issue #6, opened meanwhile, is another developer's proposal, not a defect: left for the user.
- 11.4f round 1: PASS, no findings.
- [x] 12.0 Port Compositor 1.3: survey the upstream changes from 1.1 to 1.3.x and split them into ordered items here before any code.
- 12.0 survey: the twin mirrors Compositor at 7451cef (the 1.1 feed); the target is tag v1.3.3, 220 commits later, 164 files (157 Swift and C, the seven app icon PNGs), about 17,400 lines added (`git diff 7451cef v1.3.3 -- Compositor CompositorTests`). The spec is checked out at `~/Documents/Github/Compositor-spec`, pinned to v1.3.3. Items follow upstream's versions, since each builds on the last; each ports its range's Swift and C changes with their tests, and a later fix to a feature travels with the item that ports the feature (named below where it crosses items), so no item ports a state upstream abandoned. A change reaches an item by the release that contains it (`git describe --contains`), not by the log's date order. Already held by the twin, so ported as no code but their AGENTS.md notes updated to say Swift now agrees: `f6a74c9` (Levels no longer converts soft edges twice; the Levels known difference), `121a15f` (Color Dodge and Color Burn in sRGB; the `SeparableBlend` note), `5775dd1` and `6230e41` (effects through snapshots; the 9.6a fix beyond Swift), `90ee29f` (Select All in text fields), `b5cb4fa` (M and L choose their tools only; the 7.2 note) in 12.5b, and `7111885` (the distort tests' folding; the 6.2 note) in 12.6. Open Recent's list lives in OmaPhoto's own settings (`QSettings`), where Swift reads `NSDocumentController`'s: Qt has no API for the desktop's recent documents. The user's decisions (2026-09-26): LibRaw decodes RAW where Swift uses `CIRAWFilter`; Qt SVG renders SVG where Swift uses `NSImage`; the Magic tool's Object mode takes U²-Net's foreground mask and selects the connected region under the click, where Swift uses Vision's `VNGenerateForegroundInstanceMaskRequest`. Left out, macOS alone: automatic updates and the appcast, the title bar's drag, "always dark" alerts and panels (Linux follows the Omarchy theme), opening a project while the app is quit.
- 12.0 round 1: FAIL, six findings: the reload on disk sat in 1.2.11 though 1.3 merged it (moved to 12.13 by `git describe --contains`), the reshaped app icon had no item (12.6), fixes crossing items were placed by version (the rule stated, `d9b3f9e` to 12.2, `5d7adba` to 12.8c, `96edd5f` to 12.8b, `b683acb` and `a5721b2` named), changes the twin holds were unmarked (listed with their AGENTS.md notes), the counts were off, Open Recent's list had no home (QSettings). Round 2: PASS, no findings.
- [x] 12.1 Versions 1.1.1–1.1.4: feathered selection modifiers, the Soft Light blend mode, the Type tool's responsiveness, a fixed-width font menu.
- 12.1 work: feather (the coverage blurred at sigma feather / 2, edges repeated; the bounds, clip, copy region and masks from a selection grow by twice the feather), the Select menu's Expand…, Contract… and Feather… through the amount sheet, the bar's Feather, Select ▸ Subject on U²-Net, Soft Light, `TypeFontPicker`, the inline editor's measure cache and `setTextDraft`'s narrower resume. Evidence: 209 of 209 in the dev image, lint clean; mutants over Selection, the sheet and bar, Select Subject, the menus, the font picker, the mask from a selection, the copy region, `whitePixels` and the renderer: every one caught but the two equivalent ones AGENTS.md names; dead terms removed (`canSelectSubject`'s document and busy, the sheet's `trimmed`, the picker's name test).
- 12.1 round 1: FAIL, two findings. Choosing the face already shown restyled, opening live text for editing, where Swift's `choose` writes nothing (the picker returns on the shown face; `theFaceShownChangesNothing`, its mutant caught); AGENTS.md named `LayerRendererTests` for the blend formulas (`BlendModeTests`).
- 12.1 round 2: PASS, no findings.
- [x] 12.2a Versions 1.1.5–1.1.6, part one: folders duplicated with their contents (Alt-drags on the canvas and in the list; the Layer menu takes folders in 12.10, as 1.2.5 does), the layer list's T key, the effects sheet's wider typed ranges and swatch rims, the text row's words, no clipping strip on a folder, the transform box without its dark line; folder opacity (effective opacity in every drawing path, `canEditOpacity`) with 1.2.0's store rule for a dimmed folder (`d9b3f9e`), the format moving to version 8 here since that rule reads it (guides arrive under that version in 12.3); the View menu's Snap.
- 12.2a work: folders duplicate with their contents (canvas Alt-drag, list; the menu still refuses a folder, as 1.1.6), no clipping strip on folders, the text row's "Text", the accent-only transform box, the effects sheet's slider and typed ranges and swatch rims; folder opacity through every drawing path with `canEditOpacity`, the format at version 8 with `d9b3f9e`'s rule; View ▸ Snap. Evidence: every test green in the dev image, lint clean; mutants over opacity, the drawing paths, the gates and controls, the store, duplication, the transform duplicate, the list, the sheet, Snap, the crop, the menu, the overlay and the cell: all caught; dead terms left out (`duplicateLayer`'s `canEditLayers`, the thumb's clamp).
- 12.2a round 1: FAIL, five findings. The layer list lacked 1.1.6's T for Type (added, tested in `NativeLayerListTests`); a stale comment on `beginDuplicateTransform` said folders just move (it copies roots with their contents); TASKS credited the Layer menu with folder copies, which arrive with 1.2.5 in 12.10 (words fixed); the 10,000-layer limit was tested past its edge only (three copies onto 9,997 now reach 10,000); the baker's folder walk was tested one folder deep (a folder around the folder now dims the source to 63). Each fix's mutant is caught; the equivalent `activeLayer()` term of `canEditOpacity` is named in AGENTS.md.
- 12.2a round 2: PASS, no findings.
- [x] 12.2b Version 1.1.6, part two: the keyboard shortcuts window, remapped shortcuts on the menus, the canvas, the layer list, the sheets' buttons and open text; Space released by its physical key.
- 12.2b work: the Keyboard Shortcuts window (Edit menu, after Paste), Swift's chords, definitions and rules in QSettings, remapped keys on the menus, the canvas, the layer list, the window's key filter, open text and every sheet's Apply and Cancel (Levels' Preview too), Space ended by its own key. Evidence: 215 of 215 in the dev image, lint clean; mutants over the model, the settings, the rewriting, `NativeShortcut`, every sheet's binding, the canvas, list and filter wrappers, the menus and the sheet: all caught but the equivalent ones AGENTS.md names; dead terms left out (the Save action's recording test, `menu`'s empty-sequence guard, `NativeShortcut`'s enabled test).
- 12.2b round 1: FAIL, four findings. The recorder claimed no key, so a live menu shortcut ran instead of being recorded (a `Qt::Tool` panel keeps its parent's shortcuts; the test's window was never shown): it claims every key while recording, the test shows the window; a synthesized press carried the unshifted text, so a remapped hardness key stepped the size (Swift's shifted text now, tested); Tab moved the focus instead of recording (the recorder takes it); Cancel moved onto Return, or Apply onto Escape, left both dead (a `NativeShortcut` rests only on its own plain key now, tested). Each fix's mutant is caught.
- 12.2b round 2: PASS, no findings.
- [x] 12.3a Version 1.1.7, part one: guides and the layout grid in the document, the session and the store (version 8's `guides`, carried by the resizers and the canvas flip), View > Snap To and the other View entries, the five new shortcut definitions, snapping to guides and the grid.
- 12.3a work: guides in the document, undo, the store (version 8's `guides` with Swift's refusals), the resizers and the canvas flip; the layout grid; guide drags in the session with snapping; View > Show, Rulers, Snap, Snap To, Lock Guides and Clear Guides; the five shortcut definitions; moves and crops snapping through Snap To. Evidence: every test green in the dev image, lint clean; mutants over the model, the session, the store, the codec, the resizers, the flip, the projects and the menus: all caught but the two equivalent ones AGENTS.md names.
- 12.3a round 1: FAIL, two findings. Five behaviours passed their tests broken: a guide scaled by the wrong axis (a 200 by 50 resize now), `canEditGuides`' Levels, Hue/Saturation and filter terms (each edit opened now), the five View shortcut definitions (each remapped and read back now), the grid's lines taken along the wrong edge (392 in xs, no 304 in ys), a guide cleared mid-drag still shown (tested now); each mutant is caught. `guideColor` had no reader yet: it waits for 12.3b.
- 12.3a round 2: PASS, no findings.
- [x] 12.3b Version 1.1.7, part two: the rulers beside the canvas, the grid and guides drawn over it, guides dragged from the rulers and with the Move tool.
- 12.3b work: the rulers beside the canvas (corner, strips, ticks and labels, drags that make and drop guides), the grid and guides drawn over the canvas, guide drags with the Move tool (cursor, Escape, rests for wheels and pinches, lost releases). Evidence: every test green in the dev image, lint clean; mutants over the rulers, the overlay, the canvas's drags, keys, cursor and repaint, and the layout: all caught but the equivalent one AGENTS.md names.
- 12.3b round 1: FAIL, three findings. Six drawn details passed their tests broken: the ruler's half-step tick, its labels' place, presence and gray, the guides' hairline width, the grid majors' ink, and Escape taking a guide drag ahead of an open transform edit (each pinned now, its mutant caught); a screenshot two tests saved in the working directory (they save beside the test binary now, the stray file removed); a no-op assertion (gone).
- 12.3b round 2: PASS, no findings.
- [x] 12.4 Version 1.1.8: the Magic tool's Object mode (U²-Net and the clicked region).
- 12.4 work: Object mode (U²-Net regions on the model grid, the guided edge, edge steps, smoothing), the session call, W and Tab, the Magic bar (modes, source, a signed Edge field), the canvas press and cursor, the rail icon and hints. Evidence: every test green in the dev image, lint clean; statement deletion over `ObjectSelection.cpp` and targeted mutants over the model, session, bar, canvas, list, hints, wand landing and icons: all caught but the equivalents AGENTS.md names.
- 12.4 round 1: FAIL, five findings. A trace the wand kernel refuses left the Object worker uncaught and aborted the app (the worker catches `MagicWandError` now; no test reaches it, named in AGENTS.md); three details passed their tests broken: the press handing the view point on (the click now lands through a zoom), the cell row read through the width (a disc low on a 4:3 picture now), the bounds rounding where Swift floors (a click in the last half pixel now), and a minus after a digit (typed now); the icon sheet saved in the working directory (beside the binary now, the stray file gone); Qt's repeated start counted toward three points (dropped first now, Swift's later drops and Chaikin's guard dead and gone); the signal record lacked the mode and settings (read now).
- 12.4 round 2: PASS. Its residuals: the three-point threshold named equivalent in AGENTS.md; the launch test's screenshot now saved beside its binary, the same cause as the icon sheet.
- [x] 12.5a Version 1.2.0, part one: Photoshop PSD import (the reader, channel coder, vectors, document builder, the conversion report sheet).
- 12.5a work: the PSD reader, PackBits, vector masks and live shapes, the document builder, the session's Photoshop import with its sheet, the importer's filter, dropped pictures named and kept apart, the desktop MIME type. Evidence: every test green in the dev image, lint clean; statement deletion over every new and changed file, then targeted mutants for the survivors: all caught but the equivalents AGENTS.md names.
- 12.5a round 1: FAIL, two findings. A file that opens but cannot be mapped (a pipe, a mount without `mmap`) was refused where Swift reads it (read when the map fails, a pipe tested); the folder's place beside an active layer inside a folder passed its test broken (pinned). Beyond Swift, recorded and fixed for the twin's graph rule: a clipped folder took a link the store refuses (no link now, the base note). AGENTS.md's log categories read `omaphoto`.
- 12.5a round 2: PASS. Its residual: a read failing after the open is named untested in AGENTS.md (Qt 6.4 refuses to open a directory, probed).
- [x] 12.5b Version 1.2.0, part two: the Outer Glow effect, brush Smoothing, and the release's other changes to what the twin holds.
- 12.5b work: the Outer Glow effect (model, manifest, kernel, margin, surface, preview cache, sheet, session), the previews kept while an effect is hidden, brush Smoothing (session and bar). Evidence: every test green in the dev image, lint clean; statement deletion over the changed code and targeted mutants: all caught but the equivalents AGENTS.md names.
- 12.5b round 1: FAIL, two findings. The notes TASKS.md 12.0 assigned here were unwritten: Swift agrees since 1.2.0 on Levels' soft edges (`f6a74c9`), effects through snapshots (`5775dd1`, `6230e41`), Color Dodge and Burn in sRGB (`121a15f`), M and L (`b5cb4fa`) and Select All (`90ee29f`), each note now saying so; the signal record lacked `smoothing` (read now).
- 12.5b round 2: FAIL, two findings. Select ▸ All rested without a canvas where Swift 1.2.0's never rests (`90ee29f`): the entry is always enabled now and selects a focused field's text (tested); the 9.4a note on Swift's Gaussian test was stale since `7c4512a` (says so now). With them, as `7c4512a` did, the invert test lost its wall-clock budget, which measured the parallel suite's load.
- 12.5b round 3: FAIL, one finding, the third and last round. With the layer list focused, Swift 1.2.0's Select All reaches its table, which selects every row with the top one primary; the twin selected the canvas. Fixed after the round: a focused list takes Select All (`NativeLayerList::selectAllRows`), tested, its two mutants caught (`mut-12.5b-r3.log`); the item closes on that, the review's three rounds spent.
- [x] 12.6a Version 1.2.1, part one: camera RAW import through LibRaw with its develop sheet.
- 12.6a round 1: FAIL, seven findings. The cached LibRaw read its own last multipliers and halved size (the camera's now come from unpack's copy, `rawdata`); the tint's sign was the inverse of Adobe's (positive is a greener white now, D65 reads +9.5); `adjust_maximum_thr` stretched frames near white (off); a white the camera cannot see was dropped by LibRaw in silence (refused); dcraw's normalization by the smallest multiplier moved brightness with temperature (anchored on green); a test comment was wrong.
- 12.6a round 2: FAIL, two findings. Unclipped highlights (round 1's anchor through `highlight` 1) turned magenta on real cameras: LibRaw clips again and the twin multiplies the smallest multiplier back, a clipped frame tested white; the refused white said Swift's words for a wrong file type: it is `ImageImportError(whiteBalance)` with words of its own.
- 12.6a round 3: PASS. One wording note taken (the frame's white at the slider's ends).
- [x] 12.6b Version 1.2.1, part two: middle-button pan, persistent tool settings, Auto Select picking the foreground layer, the reshaped app icon (`45ab8fd`) in `packaging/icons`.
- 12.6b round 1: FAIL, two findings: the middle pan's per-move point and the Auto Select branch's `underPointer` guard were held by no test (both tested now); the cosmetic notes taken (test order, the distort test's Swift name, the double pan named). A parallel run showed `SelectionCanvasTests` pressing before a nudge's move had ended; it waits for the move now.
- 12.6b round 2: PASS.
- [x] 12.7a Version 1.2.2, part one: Photoshop's remaining blend modes in its groups.
- 12.7a round 1: FAIL, one finding. A clipping stack's base and an adjustment in one of the eight new modes blend by it in the twin, where Swift's `cgMode` draws them as Normal; AGENTS.md had called Swift's cases dead. Named now as a fix beyond Swift and tested on both paths; three wordings taken. A parallel run showed `GradientCanvasTests` reading the edit a turn early; it waits now.
- 12.7a round 2: PASS. One note taken: the stack test asserts the exact 136.
- [x] 12.7b Version 1.2.2, part two: Black & White, Color Balance and Invert adjustments.
- 12.7b round 1: FAIL, three findings. A Black & White or Color Balance value past its range aborted the app: the filter's workers now catch `ProjectError`, the sheet shows it and a commit reports it with no step, as Swift; Undo and Redo double a step's ampersand; yellows and cyans are told apart in the kernel test. Headlines semibold, Preserve Luminosity's help asserted.
- 12.7b round 2: FAIL, one finding: five Color Balance manifest keys sat at zero in every fixture; nine distinct amounts now, each key's value read.
- 12.7b round 3: FAIL, one finding, the third and last round: the sheet's rows were not held to their fields. Fixed after the round: every Black & White and Color Balance slider is set to a value of its own and read back, Hue to 360; the review's three survivors fail now (`mut-12.7b-r3.log`); the item closes on that.
- 12.8a round 1: FAIL, four findings: the kernel-warning note named a fix that never came (the warnings stand through 1.3.3); 24 of 30 mutants survived on the grade's wiring (kernel argument order, the clip view on an untouched grade, Auto's linear mean, each eye's fields, Offset, Aspect and Constrain Crop, the scope's channels and hues, the samples' hue branches, the Luminance tab); `isValid` had no caller; TASKS.md's 12.8b line lacked the canvas half. `CameraRawWiringTests` added, `isValid` and the vectorscope's dead wrap removed.
- 12.8a round 2: FAIL, one finding: the curve, mixer and grading call, the sharpen-mask view and the lens flags were not held against their kernels; each is now, with the floats written out by hand, and the minor constants (Grain's scale, a dull colour's reach, Rotate, Scale, Vertical, a green sample's tone) with them.
- 12.8a round 3: PASS. Its three minor gaps (Horizontal's factor, the sharpen mask's scale, calibration's green and blue fields) pinned after the round (`mut-12.8a-r6.log`).
- [x] 12.8a Version 1.2.3, part one: the Camera Raw filter's model and pixel kernels.
- [ ] 12.8b Version 1.2.3, part two: the Camera Raw panel beside the canvas (Swift's docked placement in `FloatingPanel.swift`), its grading wheels turning the way their hue runs (1.2.9's `96edd5f`), the Filter menu's entry, and the canvas half of the tools in `EditorCanvas.swift` (the readout on hover and its clearing on leaving, the three eyedroppers and the guide's press, each refocusing the panel, the guide and targeted drags, their release, `picking`'s four terms).
- [ ] 12.8c Version 1.2.3, part three: Inner Glow, Image Trim, blur and noise adjustment layers, the layer context menu without 1.2.5's dropped Layer Effects entry (`5d7adba`).
- [x] 12.8d A `std::bad_alloc` aborted `ProjectExportFailureTests` under a parallel run (seen in 12.7b and 12.8a; alone it passed ten of ten): the panel tests held a memory limit while dialogs painted, and the panel limits sat at the edge of what the panel and its snapshot need.
- 12.8d round 1: PASS. Its notes taken: the snapshot's measured peak named in AGENTS.md, `malloc_trim` in `limited` named equivalent.
- [ ] 12.9 Version 1.2.4: the finishing filters, stepped keyboard zoom, a steady tool rail and tab strip, the project format's version in one place.
- [ ] 12.10 Versions 1.2.5–1.2.7: copying and pasting whole layers within and between projects, duplicating every selected layer, crop ratios 3:4 and 9:16 and a crop that starts at the selection, Vignette's defaults, text colour previews.
- [ ] 12.11 Versions 1.2.8–1.2.10: the document's own pixel budget, PSB, simple Photoshop text as editable text, background-only PSDs, oversized Photoshop layers cropped, clicked text starting on its first baseline, live text opened by a double click with the Move tool (`a5721b2`), marching ants at screen resolution, SVG import (Qt SVG).
- [ ] 12.12 Version 1.2.11: masks painted anywhere on the canvas, moved unlinked masks under effects, effects kept on screen while text is edited, masks painted and steps undone (`b683acb`), mask thumbnails' ground, text shown as its committed pixels, numeric label scrubbing.
- [ ] 12.13 Version 1.3: reloading a project changed on disk (`ed5ddea`, merged in 1.3), its manifest-and-sizes fingerprint (`cd4061a`), editing while a project saves, images decoded from memory, the guide for AI agents writing projects.
- [ ] 12.14 Versions 1.3.1–1.3.2: quitting and closing while text is edited, the I-beam over the canvas, File > Open Recent, Image Size keeping print sizes, Camera Raw's Color Grading layout, coloured slider tracks with double-click reset, colouring only the selected letters, sampling keeping a panel's focus, filters waiting for text editing to finish.
- [ ] 12.15 Version 1.3.3: the Dither filter.
- [ ] 12.16 Release OmaPhoto 1.3.3, the version Compositor carries (the user's decision, 2026-09-26: OmaPhoto's version follows Compositor's): README's features, AGENTS.md, the distro checks, packages, the install scripts rerun.
- [ ] 11.5 End-to-end acceptance run with screenshots, README Linux section, `AGENTS.md` sync, MVP acceptance table.
## Skills by phase
Planning (done): geju, goudi, hai-razor, hai-architecture, senior-architect, clean-architecture, ai-coding-discipline, karpathy-guidelines, be-serious, herdr. Coding (from 0.1): clean-code, code-simplification. IO (3.1, loaded): ddia-principles, applied as encoding and evolution (new code reads versions 1 to 7, unknown keys are ignored, a newer version is refused by name), reliability (a save is whole or absent, synced before the swap) and limits checked before bytes are read. UI (4.3): steve-jobs-perspective. Tests (4.3): e2e-testing-patterns, systematic-debugging. Research (9.7): deep-research. Packaging (11.2): devops-engineer. auth-implementation-patterns: loaded at 3.1; the app has no accounts, sessions or network auth, so only its one general rule applies: a project file is untrusted input, validated before use.
## Review log
One line per review: item, round, verdict, findings fixed.
- 0.1 round 1: FAIL. `dev.sh run` prefixed an absolute `WAYLAND_DISPLAY` with `XDG_RUNTIME_DIR`. Fixed.
- 0.1 round 2: PASS.
- 1.1 round 1: FAIL. `CanvasViewport` lacked `==`; `validDimension` accepted a trailing newline and an embedded NUL; no test asserted the pan update. Fixed.
- 1.1 round 2: FAIL. The viewport inequality check changed two fields at once. Fixed: one field per assertion, every `==` term mutation-checked.
- 1.1 round 3: PASS.
- 1.2 round 1: FAIL. No test proved that `reset()` issues a fresh revision. Fixed; every revision line mutation-checked.
- 1.2 round 2: PASS.
- 1.3 round 1: FAIL. `visibleRect` ignored the window mapping and device pixel ratio; the nearest-sampling test began with smoothing off; nothing proved layer equality leaves painted tiles unflattened. Fixed after measuring QPainter in the container.
- 1.3 round 2: PASS.
- 1.4 round 1: FAIL. `renderLayers` overwrote duplicate ids where Swift traps; no test used an absent `isGroup`; `visibleLayers` order unchecked. Fixed.
- 1.4 round 2: FAIL (new codex session after the Herdr workspace changed). Nested collapse, absent-flag validation and error kinds were untested. Fixed; `ProjectError` now forces the version payload.
- 1.4 round 3: PASS.
- 1.5 round 1: FAIL. A null thumbnail from `scaled()` was accepted where Swift throws; flag tests used equal values; `placed` was tested square only; painted-mask equality and masks on pixel layers were untested. Fixed.
- 1.5 round 2: PASS.
- 1.6 round 1: FAIL. The tracer converted pixels instead of drawing over a cleared bitmap (alpha mishandled, null conversion unchecked); source-id equality untested for two present ids. Fixed. Tests now read returned optionals through `.value()`.
- 1.6 round 2: FAIL. A positional `drawImage` shrank images with a device pixel ratio. Fixed; rule recorded in `AGENTS.md`.
- 1.6 round 3: PASS.
- 2.1a round 1: FAIL. Workers raced on `QImage::scanLine()`'s detach counter; a scratch `bad_alloc` escaped `halve()`; colour fixtures had equal channels. Fixed: raw row pointers, one pre-allocated scratch block, a distinct-channel test, a test-only `operator new` that fails on demand.
- 2.1a round 2: PASS.
- 2.1b round 1: FAIL. A rotated layer leaked through a black mask at antialiased edges; the HSL copy inherited painter opacity; HSL rounding could exceed alpha; a throw after `save()` skipped `restore()`; the test file passed 500 lines. Fixed: mask drawn into a zero-filled `Alpha8` surface and multiplied 1:1, absolute opacity, alpha-first rounding, throwing work before `save()`, tests split.
- 2.1b round 2: FAIL. Mask-painter antialiasing was unverified. Fixed with the coverage-squared law; every hint of every painter mutated.
- 2.1b round 3: PASS.
- 2.1c round 1: FAIL. A failed copy of shared coverage passed as "no folder masks"; the thumbnail background, `flipY` and the 64-folder limit were untested. Fixed; folder clip tests split out; the RLIMIT boilerplate became `tests/AddressSpaceLimit.h`.
- 2.1c round 2: PASS.
- 2.2a round 1: FAIL. Replacing with `Source` doubled antialiased coverage at outer edges; `mutate.py` could report "caught" when nothing ran; every fixture had a zero alignment. Fixed: pieces clear and redraw an exact half-open device-pixel region, the runner judges builds and tests apart, an aligned fixture was added.
- 2.2a round 2: FAIL. The opening for the antialiased outline shrank below a device pixel at extreme reductions. Fixed with room measured in device pixels.
- 2.2a round 3: FAIL, the last round allowed. The room used the painter's horizontal scale, so unequal painter scales under-opened the vertical outline. Fixed: the room comes from the whole device-to-grid transform. Proven by `theOutlineOpensByOneDevicePixel` (the reviewer's regression and two sheared rows) and 22 mutants of `opened` and the culling, all caught. Also in this round: the halving overhang left the opening (proved dead), `Piece.offsetBy`, the offset of `drawCommitted`, `Frame.device` and `Frame.sampling` moved to 2.2b, piece and cache tests split into `TiledPieceTests`. The fix is in 2.2b's review scope.
- 2.2b round 1: FAIL. `drawBrushPreview` drew a cropped raster's base past the source bounds; Swift clips it. I had skipped the clip on an unchecked assumption. Fixed: the base is clipped to the source bounds and the source to the grid; both pinned by tests and mutants.
- 2.2b round 2: FAIL. The grid-clipping test used a plain image only, so the nested raster clips went unchecked (`IntersectClip` to `ReplaceClip` on the base clip passed). Fixed with a painted-source row that keeps the overhanging tile and the untouched-pixel assertions.
- 2.2b round 3: PASS. The round also judged the 2.2a round-3 fix (`opened` from the whole device transform) and found nothing.
- 2.3 round 1: FAIL. The adjustment surface and a stack's group ignored the painter's own opacity and composition mode; a surface that could not be allocated threw where Swift returns; `draw` lacked Swift's save and restore around `drawOwn`; child order and rectangular devices were untested. Fixed: the surface is drawn back by hand under the painter's state, allocation failure logs and returns, a scoped save guards `drawOwn`, tests for each.
- 2.3 round 2: FAIL. `LiveMaskRendererTests.cpp` had grown to 510 lines. Split: the stack cases moved to `ClippingStackTests`, the fixtures to `LiveMaskFixtures.h`.
- 2.3 round 3: PASS.
- 2.4 round 1: FAIL. A 2400x1 JPEG asked for a 1000x0 preview and threw; the two mask flags were only tested with equal values; only width reached the 30,000 edge; comments passed ten words. Fixed: the preview keeps one pixel across, both flag combinations, a 1x30,000 render, all 93 long comments in `linux/` shortened, and `linux/scripts/lint.py` now checks file length and comments.
- 2.4 round 2: FAIL, on the new `lint.py` only: it missed block comments, read `//` inside string literals as comments, and let a 500-line file without a final newline pass. Fixed with a small tokenizer (line and block comments, string, character and raw string literals), `splitlines()` for the count, and a `--self-test` of 14 fixtures whose every rule was mutated.
- 2.4 round 3: FAIL, the last round allowed, again on `lint.py` only (the exporter drew no finding after round 1): a digit separator (`1'000`) opened a character literal and hid every later comment, and a continued string did not advance the line count. Fixed: numbers and identifiers are tokens, newlines inside literals count; proven by `--self-test` (20 fixtures) and 13 mutants of the checker, all caught; the tree is still clean. The fix is in 3.1's review scope.
- 3.1 round 1: FAIL. A parent-folder sync failing after the swap threw while the scope guard deleted the old project; a manifest read failing after partial data went unnoticed (`QFile` reports no error then); the UUID pattern's `$` let a trailing newline pass; `version`, `width` and `height` were narrowed to 32 bits where Swift's `Int` holds 64. Fixed: every way in has its way back, taken when the sync fails; the byte count is checked against the file's size; `\\A…\\z`; `qint64` with the value kept in the version error. The lint fix of 2.4 drew no finding.
- 3.1 round 2: FAIL. When the parent sync failed and the way back failed too, the scope guard still deleted the hidden sibling, which then held the old project, and the log pointed at it; `version`, `width` and `height` were read through a double, so 9007199254740993 lost a digit and 9223372036854775807 was `invalid` where Swift names the version. Fixed: a failed way back sets the guard's `keep`, nothing is deleted, and the critical log names where the old project stays (rows for an exchange, for each of the two moves back, and for a first save); integers come from Qt's exact `LongLong`, in the header and the codec alike, with rows at 2^53 + 1 and both ends of Int64. Swift's decoder was run in a `swift:6.0-jammy` container to settle the edges; one Qt parser fact is declared in AGENTS.md and pinned.
- 3.1 round 3: PASS. The reviewer ran Swift's decoder too and accepted the declared parser fact.
- 3.2 round 1: FAIL. A HEIC that names Display P3 in an `nclx` box instead of an ICC profile kept its numbers and was tagged sRGB (the reviewer swapped the fixture's profile in flight to show it); `heic-fixtures.sh` went on after a failed step and still exited 0. Fixed: the importer reads the `nclx` box (primaries by chromaticity, the sRGB or the BT.709 curve, everything else refused with its reason), proven by rows patched from one lossless fixture with independently derived values; the script runs under `set -eu` with each step on its own line, and a broken generator now exits 1 and writes nothing.
- 3.2 round 2: FAIL. Linear light and the two gamma curves were refused though Qt converts them; two new comments in `heic-fixtures.sh` ran to 11 and 12 words, and `lint.py` does not read scripts. Fixed: linear, gamma 2.2 and 2.8 go through Qt's transfer functions, xvYCC and BT.1361 share the video table, each with a row of derived values and its mutants; both comments are shortened and every comment in `linux/scripts`, `CMakeLists.txt` and `Dockerfile.dev` was counted once by hand. Teaching `lint.py` to read scripts is taken up in 3.3.
- 3.2 round 3: PASS.
- 3.3 round 1: FAIL, seven findings. `drawCoverage` forced Screen and so ignored the painter's own mode, and turned by `rotate(degrees)` where `draw` turns by `radians()`; `lint.py` missed `#` comments without a space and flagged `#` lines inside quoted strings; the tests read optionals with `*`, gave `maskEnabled` and `maskLinked` the same value, left masks out of the resolution-only case, and proved an unflattened asset by pointer only. Fixed: white with the mask as alpha under the inherited mode, opacity and clip, placed as `draw` places; real scanners for shell, CMake and Docker with the fixture generator split into real files; `.value()`, opposite flags by rows, a mask through the resolution-only path, `hasMaterializedPixels` before and after.
- 3.3 round 2: FAIL, five findings. Under Source or Clear the white-with-alpha source erased where the mask was zero, which a clip would not; the CMake scanner counted a bracket comment's closer as a word; the shell scanner took `$((1 << shift))` for a heredoc, cut hyphenated tags short, missed escaped blanks and `\#`, and skipped comments inside `"$( )"`. Fixed: each composition mode maps to the one under which alpha equals a clip's coverage, held against an oracle for all 24 modes over opaque, half clear and clear pixels, raster operations refused; the shell scanner was rewritten as a strict subset that refuses what it cannot read (113 self-test cases, 58 mutants, one equivalent); CMake escapes and bracket closers fixed.
- 3.3 round 3: FAIL, the last round allowed, on `lint.py` only (the C++ and its tests drew no finding): an escaped blank before `[[` was taken for an argument boundary in CMake, and a continued line splitting `$` from `(` hid a command substitution in shell. Fixed: the CMake scanner tracks whether it stands inside an unquoted argument, escapes included; the shell scanner reads a continued line only between words (after a blank, a tab or a newline) and refuses it anywhere else, inside double quotes after `$` too, so no operator or keyword can be split. Proven by the reviewer's two inputs as regressions among 133 self-test cases and by 74 mutants of the scanners, all caught. The fix is in 4.1's review scope.
- 4.1a round 1: FAIL, five findings. `beginEdit` closed undo and redo without telling observers; `zoom` with a value that is no number, refused by the viewport, still signalled; `descendantIDs` copied the layer list once per visited folder, quadratic on a wide folder; the tests never read back what the setters stored and read one optional through `->`; the CMake scanner of `lint.py` forgot it stood inside an unquoted argument after a quote, so `a"b"[[` opened a bracket argument. Fixed: `beginEdit` emits; `fit` and `zoom` emit only when the view changed; one lookup of children by parent, built once, walked once (`aWideFolderIsWalkedOnce` walks 9,999 children in under two seconds); `settersKeepExactlyWhatTheyAreGiven` and `observersSeeFinishedStates`; `.value()`; the quote keeps the argument state, with two regressions among 135 self-test cases and 75 scanner mutants, all caught. The standard library's assertions (`_GLIBCXX_ASSERTIONS`) are on for every target, after a mutant that read an empty optional passed by luck; with them it aborts. Nine mutants of the fixes, all caught.
- 4.1a round 2: FAIL, one finding. The transaction test read `canUndo()` only after `beginEdit` returned, so a signal sent before `history.begin` passed while observers stayed stale. Fixed as a class, not a case: `theLastSignalOfEveryChangeSeesWhatItLeaves` records everything an observer can read (active layer, selection, folded folders, tool, flags, error, rename, undo and redo with their names and count, the modified mark, the view, every layer) inside the `changed()` slot and compares it with the state each call leaves, for every mutator of the item; a call that says nothing fails too. The signal tests moved to `EditorSessionSignalTests` (the session file stood at 496 lines). 29 mutants that move a signal or a transaction's end ahead of the state it covers: 27 caught at once; two showed that redo after `insert` and `addGroup` was never checked for the active layer, so `everyLayerEditRoundTripsWithSelection` gained both steps, and all 29 die.
- 4.1a round 3: PASS.
- 4.1b round 1: FAIL, two findings, both in the tests (the code matched Swift line by line). No fixture nested deep enough: mutants that looked only at immediate parents, for the shared ancestor and for a selected ancestor, passed. And the check that taking a non-active layer out of a selection keeps the active one leaned on hash order: a mutant passed 7 of 40 runs. Fixed: `nestedSelectionsShareAnAncestorAboveTheirParents` builds G holding A(C(a)) and B(b), selects A, a and b with b first in hierarchy order, and holds the wrapper's parent, every parent link and the flat order; the deselection check takes the same third layer out of the same three twice, once under each survivor as primary, so no hash order can save a wrong choice. Added on my own reading: `wrappedLayersKeepTheirStackingOrder` (hierarchy order, not list order) and a target that beats `atBottom` in `placeLayer`. Eleven mutants of these, all caught, the reviewer's three among them.
- 4.1b round 2: PASS.
- 4.1c round 1: FAIL, two findings, both in the tests (no source line drew one). The 64-step walk from a layer up to a selected folder had no fixture at its edge: 63 and 65 passed. And no group edit ran on members with a turn, flips or sampling, so originals rebuilt from origin and size alone passed. Fixed: `aFolderReachesSixtyThreeLevelsDown` nests 64 folders around an image: the second folder carries it, the first does not; `aGroupMoveKeepsEachTurnFlipAndSampling` gives two members opposite flips, different turns and samplings, holds the saved originals, the preview and the commit of a group nudge to the whole transform, and holds a single edit's draft the same way. Added on my own reading: a nudge past the valid origins changes nothing, and a hidden folder's handles fall back to its own transform. Eleven mutants, all caught, the reviewer's three among them.
- 4.1c round 2: PASS.
- 4.1d round 1: FAIL, four findings. A request's callback ran inline, so the last caller saw `isImporting` still true and no error yet, where Swift's resumed caller sees both settled; the waiters were released on every write of `isProjectBusy`, so saying busy again could put a later waiter ahead of an earlier one (Swift releases only when busy ends); the default coarse timer fired at 243 ms; and two tests held ceilings on time (a 4 to 1 ratio for the import call, 400 ms for the indicator) that a scheduler pause breaks. Fixed: callbacks are posted to the event loop; the release stands under `if (!busy)`; the timer is precise and a child of the session; the tests hold the callback's view of both a first and a last request, the line of waiters across a repeated busy, the floor of 250 ms read inside the signal handler, the timer's remaining time right after busy is said again, and the decoding thread taken from the importer's own log. Five mutants of the fixes, all caught; 20 runs under full CPU load, no failure.
- 4.1d round 2: FAIL, three findings, all in the tests. A `qWait` before the timer was inspected could deliver the timer under a scheduler pause; a non-empty import without a callback was never run, so the guard on `done` was unpinned; and no test ended a session while a posted callback was still pending, so a callback posted to a longer-lived receiver passed. Fixed: `qSleep` holds the events back until the timer is read; an import with the default callback; a high-priority event ends the session between the drain's end and the callback's delivery, and the callback must never run. The same property is now held for both kinds of waiter (`waitersEndWithTheirSession`). The gate and busy tests moved to `SessionGateTests` (the import file stood at 481 lines). Four mutants, all caught.
- 4.1d round 3: PASS.
- 4.1e1 round 1: FAIL, four findings, all in the tests. Every display without a preview expected Normal, so a constant passed; the preview's clearing in `commitTransform` was only reached through `selectLayer`, whose later signal hid a missing one; no transform was committed inside an open opacity drag, so ending the drag only when no transform is open passed; and the selected-layers setter met NaN but no infinity. Fixed: a stored Multiply shows with and after a preview; `commitTransform` called with only a preview open must signal, and the slot must see it cleared; a nudge committed inside a drag gives two steps that undo one by one (`Transform Layer`, then `Layer Opacity`); NaN and both infinities go through both setters with opacity, history and signal count held. Five mutants, all caught.
- 4.1e1 round 2: FAIL, three findings, again in the tests. Both layers of the cross-layer case held Normal, so a fallback to the active layer's mode passed; no cycle ran under an open preview, so a cycle starting from the displayed mode passed; and no drag lost its layer, so the guard for a missing target, and a signal on that refusal, were unpinned. Fixed: two layers with different stored modes, each shown as its own with and without a preview on the other; a cycle from stored Screen under a Hue preview gives Overlay and clears the preview; a drag whose layer is deleted changes neither document nor history and says nothing, and the deletion rides in the drag's one step. Added by the same reading: a refused cycle leaves a preview and says nothing; a lone opacity step is named. Six mutants, all caught.
- 4.1e1 round 3: PASS.
- 4.1e2 round 1: FAIL, three findings, all in the tests. The accepted edges of `createNewProject` were held on one axis each (30,000 wide, 1 high) and not on the other; the fixture's assets bore their records' names, so a layer named from its asset passed; and the resolution-only resize was never applied through the session, so an `applyDocumentSize` that refused unchanged dimensions passed. Fixed: 1 by 30,000 is taken as 30,000 by 1 is; the fixture's images are named like camera files while the layers keep their own names; the resolution-only case installs, applies, holds 300 and one step, undoes and holds 72. Six mutants, all caught.
- 4.1e2 round 2: FAIL, two findings, in the tests. No project without an active layer was installed over a session that had one, so keeping the old selection passed; and no snapshot was taken of a canvas without layers, so answering none for it passed. Fixed: an unselected project clears the active layer and the selection; an empty canvas gives a snapshot with its id, size, resolution and nothing else, and installs as an empty document. Added by the same reading: applying a size leaves the session's active layer and path alone, whatever the snapshot calls active. Three mutants, all caught.
- 4.1e2 round 3: PASS.
- 4.1e3 round 1: FAIL, three findings, all in the tests. Every copy of a mask came from the active layer, so pixels or placement read from the active layer passed; the mask outside the group had no placement, so dropping it in the preview passed; and the unknown-id case stopped before asking `canEditMask`, so the guard for a missing active layer was unpinned. Fixed: a copy between two layers that are not active, with a third active and masked, holds the source's pixels, placement and flags; the outsider's mask has a placement that the preview and the commit must keep; one selected id without a layer can edit no mask, and `addLayerMask` changes nothing. Four mutants, all caught.
- 4.1e3 round 2: FAIL, two findings, in the tests, both about what Swift permits. `copyMask` and `toggleMaskLink` ask only for editable layers, yet every test ran them with one layer selected, so the stricter `canEditMask` passed; and folders appeared only as forbidden targets, never as the valid mask sources they are. Fixed: `whatOnlyNeedsEditableLayersWorksWithSeveralSelected` copies a folder's placed mask onto a layer and toggles links on a layer and on a folder, all under a selection of two; a mask target is chosen while a sheet is up. By the same reading: a mask switched off still shows its placement and rides along with its layer. Six stricter-gate mutants, all caught.
- 4.1e3 round 3: PASS.
- 4.1e4 round 1: FAIL, six findings. One bug: `releaseDetachedClipping` keyed its runs by `parentID.value_or(QUuid())`, so a folder whose id is all zeros shared the root's run and a move freed a valid link; the runs are keyed by the optional parent now. One missing trap: the baker let a snapshot with a repeated id overwrite its own records, where Swift's dictionary of unique keys traps; it throws `logic_error` after the target and its pixels are found, and the worker catches `ExportError` alone so that a bug crashes. Four test gaps: no source with a raster mask of its own in a bake; only square targets, so swapped sides passed; clipping never run under a selection of several, so the stricter `canEditMask` passed for all four gates; busy after the call did not prove a worker. Fixed with `LiveMaskBakeTests` (three by two, six mask values), `clippingWorksWithSeveralLayersSelected`, and a bake that queues behind a held thread pool with a lazy raster: the call returns before the raster flattens, and it flattens while the caller runs no event.
- 4.1e4 round 2: FAIL, four findings, all in the tests. The worker test stranded its held threads when a check failed before their release, and the process hung at exit: a scope guard lets them go and joins them now. The baker's source and target always shared their pixel size, so the target's sides passed for the source's grid: `aStretchedSourceBakesThroughItsOwnGrid` stretches three by two pixels over six by four under a placed mask and checks all 24 alphas. Every baked source stated its opacity, so `.value()` passed for `.value_or(1)`: that source states none now. No source lacked pixels, so the guard before the image read could go: `aSourceWithoutPixelsHidesTheTarget` bakes under a blank layer and under a link the snapshot lacks; both give a clear image. Looking for more of that kind showed a class: with the guard before an iterator read removed, `*end()` read wild memory and every test passed by luck (the image guard, five end-of-list guards, two sibling bounds). The reads are checked now (`indexOf`, moved to `EditorSession+Model`, with `[]` under `_GLIBCXX_ASSERTIONS`; `map.at`), so a lost guard aborts or throws; AGENTS.md makes that a code rule. `finishDeletingLayer` of an unknown id reads nothing after its guard, so its silence is asserted in `MaskSignalTests`.
- 4.1e4 round 3: FAIL, one finding, in the tests: no bake fixture gave the target a raster mask of its own, so a baker that pressed that mask into the pixels as well (applied twice after the deletion) passed. Fixed and proven: the target of `aBakeMapsTheSourceIntoTheTargetsOwnPixels` wears a 128-gray mask, the baked alpha stays 128, and the mutant that bakes the mask gives 64 and fails. The third round ends the reviews of 4.1e4; this fix is in the scope of the review of 4.2a.
- 4.2a round 1: FAIL, four findings. A use-after-free: the session keeps the drop queue's waiter and the import's callback, both of which captured the controller, so a controller that ended first was called after its death; both hold it through a `QPointer` now, and `aControllerThatEndsLeavesTheSessionSafe` ends one behind a sheet and one mid-import (the thread pool held). A save name with a foreign suffix made a project that drops then read as a picture: `.comp` is appended unless the name ends in it. A path with a trailing slash left the project without a name in the question and in Save As: `open` cleans its path. And nothing proved the store off the caller's thread: the store's own log lines are now caught with the thread they come from.
- 4.2a round 2: FAIL, two findings, both in the fixes of round 1. The suffix appended after the panel closed could land on an existing project and replace it unasked (`Kept.png` over `Kept.png.comp`): a completed name that meets anything on disk is refused with an alert now, and the old project's manifest is asserted to survive. `QDir::cleanPath` collapsed `link/../Other.comp`, which names another folder when `link` is a symbolic link: `open` trims only the slashes at the end, and a test opens distinct projects at the two places.
- 4.2a round 3: FAIL, one finding, in the tests: every save and open passed a completion, so `finish` without its `if (done)` passed, and a plain `controller.save()` then threw `std::bad_function_call` from the event loop. Fixed and proven: `aCallWithoutACompletionIsWhole` saves, opens and is refused with nobody to tell; the mutant fails it with the unhandled exception; the queue's two completion guards were checked the same way (both caught). The third round ends the reviews of 4.2a; this fix is in the scope of the review of 4.2b.
- 4.2b round 1: FAIL, five findings. `receiveNext` did not hold the tab it imported into, so `removeTab` during the import destroyed the session with its callback and left the workspace managing for good: the callback holds the tab now (tested with the decode held). `samePlace` matched any two places that no longer exist, both canonicalising to the empty string, so a missing project "opened" as the tab of another that had moved away: it falls back on the cleaned absolute path. After a copy the workspace's last signal came before the sessions were freed and showed `canSwitch` false for good: managing ends last. A project named `.comp` had an empty title: `nameWithoutSuffix` cuts as Swift does. And two reads broke the checked-read rule (the source tab's iterator, the baked map): a shared pointer, whose null read aborts, and `at`; `removeTab` erases by predicate and returns when nothing went.
- 4.2b round 2: FAIL, two findings, both in the tests. The suffix cases called only the helper, so `title()` back on `completeBaseName()` passed: they go through a session's path and `title()` now. And every workspace call passed a completion, the very gap of 4.2a round 3 one class further: `everyCallIsWholeWithoutACompletion` runs `open`, `confirmQuit`, `close`, `receive`, `receiveProviders` and `copyLayer` with nobody to tell, refused and carried out; both `finish` guards die without it. Added by the same reading: `ProjectWorkspaceSignalTests` (the last signal of every flow sees what the flow leaves; an empty drop; a workspace that ends under a question or during a copy), and the window handed to the tab that comes forward, not the one that leaves.
- 4.2b round 3: PASS.
- 4.3a round 1: FAIL, three findings, all in `ToolIcons.cpp`. The fade's `setClipPath` replaced the caller's clip, so a widget repainting a strip got ink beside it: it intersects now, and `anIconKeepsToTheCallersClip` draws every icon under a clip of eight pixels. The fade's frame was stroked after the clip ended, where Swift strokes inside it, so half the frame lay outside Swift's square: the stroke moved inside the clip, the outer margin is asserted clear, the reference sheet renewed. And `idle` was refused after the painter had been saved and changed: it is refused first, and the test holds the painter's state and the absence of Qt's warning.
- 4.3a round 2: FAIL, two findings, both leaks into the drawing. The fade's dots were filled one by one, which leaves translucent seams between neighbours at a fractional scale (alpha 173 at side 22.5): they are one path now, as in Swift, and `theFadeHasNoSeamsAtAFractionalScale` holds every pixel that lies wholly inside lit dots at 255. A caller's opaque background mode filled the marquee's dashes and the letters with its background colour: the mode is set transparent inside the saved state. By the same reading the letters no longer start from the caller's font, and `nothingOfTheCallersPainterShowsInAnIcon` draws every icon from a hostile painter (dotted red pen, blue brush, red opaque background, underlined slanted spaced font, a brush origin) and wants the very pixels a fresh painter gives, with the caller's settings back afterwards.
- 4.3a round 3: PASS.
- 4.3b round 1: FAIL, five findings. A rail button toggled itself on a click the busy session refused, so two were down: the click's slot sets every button again. The welcome was hidden and shown, so it kept its fields and its clipboard suggestion across projects where Swift makes a fresh view: it is made anew when the document goes, and the journey test clears a project with a picture copied. The Eyedropper's bar lacked its title. Return with a button in focus created nothing (`setDefault` reaches only a `QDialog`): sheet-wide shortcuts now. And the importer's test picked one file, so a handoff of the first alone passed: it picks two and checks their order.
- 4.3b round 2: FAIL, one finding, in the tests: the refusal test never showed its sheet, so Return reached no shortcut and the guard in `create` went unproven. The sheet is shown and active there now, and each refused size is pressed by button, by Return in the width, by Enter in the height and by Return with a button in focus; the guard's three mutants die.
- 4.3b round 3: PASS.
- 4.3c round 1: FAIL, five findings. The `closeConfirmed` mark stayed on the window, so a window shown again with new work closed unasked: the mark is cleared when consumed. The strip scrolled to the front tab before the row was laid out, so a tab beyond the viewport stayed out of sight; and the fix showed the worse fault under it: the resizable scroll area squeezed every tab to 40 pixels instead of scrolling; the row is sized to its tabs by hand before the scroll, tested with eleven tabs. The editor kept its 520-pixel minimum under the toolbar, so at 800 × 520 the status bar sat below the window: the window owns the minimum now, asserted with the status bar inside. The title stayed `Compositor` and lacked Qt's `[*]`, so `setWindowModified` warned: the title is the front tab's with the placeholder. And the launch test called `receive` itself, so `main` was untested: it now starts the built app with a project and a picture and reads their arrival in its log.
- 4.3c round 2: FAIL, two findings. A title's late layout widened the row after the scroll, leaving the front tab's close button outside the viewport: the row's layout request scrolls again, tested with a front title that grows at the far end. And the launch test read arrival but not order: it asserts the project's log line before the picture's; `main` with the arguments reversed fails it.
- 4.3c round 3: PASS.
- 4.3d round 1: FAIL, five findings. Focus changes never reached the menus, so Undo stayed disabled in the welcome's field and stale after leaving the zoom field: the menus sync on `QApplication::focusChanged`, tested without a `notify()`. A `QPlainTextEdit` branch advertised a text undo that both handlers lacked: gone. Three test gaps: Actual Pixels was triggered at 100% (the zoom is halved first now), Import Images' importing gate went untested, and every multi-selection carried a chosen mask, so the plural title passed as the singular (a bare pair asserts it).
- 4.3d round 2: FAIL, one finding. Alt+E opens Edit from the keyboard and the menu bar takes focus, so the field's Undo went to the document's: the menus keep the last field to hold focus while the bar borrows it (`focusMoved`), and a test opens Edit with Alt+E over the welcome and over a canvas, undoes through the field, and sees focus come back.
- 4.3d round 3: FAIL, one finding. The borrowed-focus test undid through the field but never redid, so Redo reverting to the focus widget passed: the test now triggers Redo while the bar holds focus and sees the text return; that mutant is caught. By the third-round rule the reviews of 4.3d end here; the fix joins 4.4's review scope.
- 4.4 round 1: FAIL, four findings. The pixel grid's path filled odd-even, so a crossing of two hairlines stayed unfilled (Swift fills the union): `Qt::WindingFill`, with a test half a point over where each line fills a whole pixel and their crossing fills once. Swift's window-wide key monitor was missing: an application event filter, installed while the canvas is shown, steps the blend mode on Shift-+ and Shift-− from any control of the window but a text field, and reads Alt for the Zoom cursor from any control, where the canvas alone saw its own keys; tested from the canvas, a button, a field, another window, and hidden. The smoothing test accepted the shadow's and the border's blends: it now reads the two pixels astride the boundary at 190%.
- 4.4 round 2: FAIL, four findings. Alt held before the canvas mounted, or released while it was hidden, left the Zoom cursor wrong: `showEvent` reads `QApplication::keyboardModifiers()` as Swift reads the flags when mounting, tested through the window's own key events for both sequences. The canvas slot's grid kept Qt's nine-point margins, a gutter Swift's canvas has not: zero now, and the view test holds the canvas to its container. The high-DPI surface test grabbed the child alone, whose grab drops the window offset: it grabs the window too and reads the child's corners there, so a surface placed by `combinedTransform` fails. The navigation tests zoomed about the viewport's centre, the default anchor: their pointers sit off-centre and the document point under each stays put.
- 4.4 round 3: FAIL, one finding. An Alt release delivered to an open menu, a window of its own, missed the canvas's window guard and left the Zoom cursor on the minus: the filter reads Alt from every window, as Swift's application-wide modifier monitor does, and keeps the window guard for the blend keys; the test pops a menu, releases Alt into it and sees the plus; the guard put back before Alt fails it. By the third-round rule the reviews of 4.4 end here; the fix joins 4.5's review scope.
- 4.5 round 1: FAIL, six findings. A malformed file at launch left Qt's own palette (the built-in dark is installed before the first load); a file that exists but cannot be opened counted as absent and dropped the theme (absence and a failed open are told apart; a failed open keeps the palette with a warning); single-quoted values were refused (either quote reads); a state directory that did not exist at launch was never found (the nearest existing ancestor is watched until it appears, then dropped); the tab strip's row kept the fill `setWidget` turns on (off now, with the margin's pixel held to the toolbar's); the built-in `Base` had no assertion of its own.
- 4.5 round 2: FAIL, four findings. A file replaced atomically under a held handle left the watcher on the old inode, so a later edit went unseen (the file's watch is renewed on each load); a folder without search permission made the file look absent and dropped the theme (`stat` tells ENOENT from a barred path; the latter keeps the palette); keys in quotes, valid TOML, were refused (keys read in either quote); a malformed `orange` fell to yellow in silence (a recognised key with a bare or half-quoted value refuses the file).
- 4.7 round 3: FAIL, three findings. The mask press's release filter cleared the press on the window's copy of the release before the button's own (a release seen on a `QWindow` no longer counts); another button's release ended a pending mask press (only the left one's does); the autoscroll's stop on leave and drop was untested (`autoscrolling()` is read before and after both). Tests in `LayerDragGestureTests` (Alt and Alt+Shift clicks through the window, a middle button mid-press) and `LayerDragTests`. By the third-round rule the reviews of 4.7 end here; the three fixes join 5.1's review scope.
- 5.1 round 1: FAIL, one finding. The merge tests never baked a mask, a folder mask, a blend mode or a clipping stack inside the merge, so dropping any of them from the flat copy passed. `LayerMergeTests::masksBlendsAndClipsBakeAsTheCanvasShowsThem` compares the live composite before and after each such merge and pins pixels; five mutants that clear a field of the copy are caught.
- 5.1 round 2: FAIL, three findings. The linked-mask flip asserted only the mask's centre and size (its corners through `unitToDocument` are pinned now, and mutants that zero the rotation or the flip of the followed placement are caught); the trim's sampling assertion compared defaults (the fixture is `nearest` now, a mutant forcing `high` is caught); one test comment ran to eleven words.
- 5.1 round 3: PASS.
- 6.1 round 1: FAIL, six findings. A menu borrowing a field's focus reformatted its typing (`12.` became `12`); Return and Escape did nothing from the bar's other controls; a snapped drag released under a Ctrl+T edit left its guide drawn; a Ctrl released while the canvas was hidden stayed held; Space mid-drag replaced the drag's cursor; the lock's icon kept its colour through a theme switch. Tests in `TransformInspectorTests`, `TransformPressTests` (paint events) and `CursorTests`.
- 6.1 round 2: FAIL, three findings. No canvas drag carried Shift (a Shift-resize of a locked corner now asserts 150 × 125 where the lock would give 138 × 138); no test pinched mid-drag (a `ZoomNativeGesture` during a drag now leaves the viewport as it was); one comment ran to twelve words.
- 6.1 round 3: PASS.
- 6.2 round 1: FAIL, four findings, all in `Distort.cpp`. The folded halves' path clip went soft once the image drew antialiased (a device-pixel region now, as the tiled renderer's) and drew source-over with smoothing where Swift copies (`BrushRaster::draw`); a placed linked mask under a folded shape previewed at its followed placement where the commit keeps the stored one; a mask warp that threw discarded the layer's preview where Swift catches it alone. Tests in `DistortTests` (a seam-free diagonal, a half-transparent fold, the stored placement, a mask too wide to warp).
- 6.2 round 2: FAIL, three findings. The integer polygon region dropped edge pixels a hard path clip keeps (the region is built per scanline from the triangle's floating-point edges now); `commitMaskTransform` made the mask's asset inside its transaction, so a throw left it open (the asset comes first now); no test distorted a placed linked mask under a convex shape. Tests in `DistortTests`.
- 6.2 round 3: FAIL, three findings. The two halves met a fractional shared edge from opposite ends, so rounding left holes along the diagonal (each edge's ends are ordered by y now); the transaction test threw before the asset was made (`DistortTests` now takes over `QImage::scaled` in its executable, as `ProjectReplaceTests` takes over libc, and fails the thumbnail after a successful warp); the widened right bound changed pixels beside the diagonal unseen (a patterned source pins them). By the third-round rule the reviews of 6.2 end here; the three fixes join 7.1's review scope.
- 7.1 round 1: FAIL, seven findings. An invert landing after `clearProject` read the missing document (the index is looked up only while a document exists, tested with a project cleared mid-invert); the mask half of the identity guard had no test (a mask replaced mid-invert keeps its gray and records nothing); the hard-clip test sampled beside the diagonal (it samples the pixel centres on it); the lasso tests could not tell winding from odd-even (the same square traced twice fills); `PixelAdjust::coverage`'s smooth sampling had no test (a clip enlarged 2× shows a soft boundary); the mask thumbnail's tooltip lacked Swift's Ctrl words (and the pixel thumbnail's is `configure`'s "Select image pixels", as Swift's `configure` overwrites the longer text); two record reads used `*` and `->` (now `.value()`). The three 6.2 fixes drew no finding.
- 7.1 round 2: FAIL, two findings. A selection move dragged on after `clearProject` read the missing document (`moveSelection` returns without one, tested with a project cleared mid-drag); a painted asset whose flatten fails threw past the invert's cleanup, leaving the project busy with no error and no callback (the image is fetched inside the try; a 48 MB raster starved by `AddressSpaceLimit` reports, frees the project and completes).
- 7.1 round 3: FAIL, one finding, the last round allowed. The two loaders let a painted asset's flatten failure escape their handlers (`ExportError` out of a Ctrl-click or a menu entry): each fetches the pixels inside a try, warns on `compositor.app` and returns, tested with two 48 MB rasters starved by `AddressSpaceLimit`. By the third-round rule the reviews of 7.1 end here; the fix joins 7.2's review scope.
- 7.2 round 1: FAIL, nine findings. A pan left the ants' old rect cached, so a later deselect repainted the wrong place (the overlay's rect is refreshed with every full repaint); the amount field lost its typing to a menu (borrowed focus keeps it, as `TransformValueField`); a double click while Space panned closed a polygonal outline (`!m_spaceHeld`); a held M or L's repeats chose their tools again, ending a fresh draft (repeats are swallowed, as Swift ignores them); Space mid-drag put the hand over a moving outline (the outline drag keeps its pointer like the transform drag); the ants and the draft were drawn aliased (antialiased now, as CoreGraphics draws them); the wand cursor was the rail's icon (Swift's own paths: hot spot 7, 7, the stick down right, the badge at 17, 5); the ants test could not see a missing repaint or a running timer (a paint spy counts the ticks and their rect, then their absence); the autoscroll test held no curve (each tick's 4.8 points and the 40-point cap are pinned).
- 7.2 round 2: FAIL, five findings. An ants tick repainted the whole overlay rect, the Move tool's box included (a tick repaints `selectionRect` alone, tested with the box around a layer); Qt's square caps stretched each dash by its width where CoreGraphics' butt caps do not (`FlatCap` on the ants and the cursors' dashed boxes; a 2× row shows eight on, eight off); the wand test took transparent black for a stick (the pixel's alpha is asserted); the autoscroll interval had no test (16 ms and the ants' 120 ms are read from the named timers); a popup's borrowed focus had no test (the amount field is tried with `PopupFocusReason` too).
- 7.2 round 3: FAIL, three findings, the last round allowed. The amount field's validator refused a fourth digit where Swift's binding clamps (digits only now, the bounds clamp; 1000 typed reads 500); the draft's pens kept Qt's square caps (butt caps, tested at 2× beyond an open endpoint); the cursors' dashed boxes had no test of their caps (their white gaps are counted at 2×). By the third-round rule the reviews of 7.2 end here; the three fixes join 7.3's review scope.
- 4.7 round 2: FAIL, six findings. A spent press stayed armed for presses that arm nothing (every left press first drops the earlier arm); Ctrl mid-drag made a local drop a duplicate (the list duplicates only when a copy is offered alone, and sets Move or Copy itself); an Alt mask press released in a popup lived on (its release is watched application-wide, as the eye's); the project targets accepted every move after a valid enter (a move is judged again and ignored when invalid); autoscrolling stopped with the pointer (a timer scrolls on until the pointer moves back inside or the drag ends); an Alt click that clipped left the old cursor (the cursor is set for the next click at once). Tests in `LayerDragTests`, `LayerDragGestureTests` (the gesture half, split off at 500 lines) and `ProjectDropTests`.
- 4.7 round 1: FAIL, nine findings. Thumbnail presses never armed a row drag (they arm it after targeting, as Swift's `mouseDown` falls through to the table); a stranger list's rows with the same ids moved local layers (a drag carries its list's token, a drop takes only its own); an Alt press on the clipping strip armed a copy drag (a press that clipped arms nothing); a spent press dragged from the middle button (a drag needs the left button held); drags never autoscrolled (the list scrolls by the overshoot into a 16-point margin); hover over rows and Ctrl changes left the cursor stale (every cell tracks the mouse, the filter reads hover, Alt and Ctrl, the last hover point is kept); plain drags offered Move alone while project targets copy (Move and Copy offered, project targets accept Copy); a malformed mask id could name an all-zero layer (`ProjectWorkspace::layerID` parses it). `LayerDragTests` defines `QDrag::exec` in the test executable, so presses, thresholds and actions are tested to the call; 24 mutants over the fixes.
- 4.6 round 1: FAIL, ten findings. An eye swipe cut short by a row rebuild left its transaction open (the button's destructor ends it from the event loop); a fractional opacity was applied twice, rounded the second time (Return and Escape only clear focus, the FocusOut applies once); Alt+E committed a rename and the opacity field (a FocusOut by the menu bar or a popup is no leaving); thumbnail clicks targeted before reading Shift and Alt (Swift's `mouseDown` order restored); a cancelled blend menu kept its target and refused the next keyboard choice (the queued close clears it); panel icons kept the old theme's colours (icons redraw on `PaletteChange`); Shift+Down replaced the selection and Down walked out of view (extend, and `ensureWidgetVisible`); eye swipes never autoscrolled (scroll by the overshoot); a double click on a pixel thumbnail did not rename (the thumbnail leaves it to the row); names were read as HTML (`Qt::PlainText`). Each has a test in `LayerListFocusTests` or the existing files; 16 mutants over the fixes all die.
- 4.6 round 2: FAIL, four findings. A swipe whose release landed in a popup opened mid-press stayed open (the eye watches the left button's release application-wide, as Swift's tracking loop misses none); Shift+Up after two Shift+Downs dropped the middle row (Shift ranges from the anchor, the far end of the selected run); Alt-click on a mask thumbnail chose nothing (it chooses the mask on a release without a drag, as `dragMaskCopy` does); a Shift double click on a mask renamed the layer (a double click with a modifier is a second press of the button's own). Tests in `LayerListFocusTests`; the mutants over the fixes all die.
- 4.6 round 3: FAIL, four findings. A slider drag whose release landed in a popup opened mid-drag kept its opacity step open (the controls watch the left button's release application-wide while the slider is down and let it go, so `sliderReleased` finishes the edit); a middle button's release ended an eye swipe (only the left one's does, as Swift waits for it); an Alt double click on the clipping strip renamed the layer (the second press clips again, as Swift's two `mouseDown`s); Alt+Shift on a mask targeted it without toggling (`selectMask`'s Shift toggle runs on the release). Tests in `LayerListFocusTests`, driven through the window's event path; the mutants over the fixes all die. By the third-round rule the reviews of 4.6 end here; the four fixes join 4.7's review scope.
- 4.5 round 3: FAIL, two findings. A state directory that could not be watched (no permission) was never found again once opened: `watch` climbs to the nearest ancestor `QFileSystemWatcher` accepts and lets it go once the directory is watched, tested with a barred `current/` opened later. A read cut short by an error applied its valid prefix as the theme: the bytes read are checked against the error state and the file's size, as `ProjectStore::load` checks its manifest, tested with a `read` defined in the test executable that gives 64 bytes and then `EIO`. By the third-round rule the reviews of 4.5 end here; both fixes join 4.6's review scope. Widgets take a changed application palette from the event loop: tests of a kept palette read `QApplication::palette()`.