Files
OmaPhoto/TASKS.md
T
雷电芽衣andClaude 2d219dd115 Packages carry their ONNX Runtime; a DEB per Ubuntu LTS
ONNX Runtime stamps its symbols with each release's version, so the
1.0.0 RPM demanded Fedora 42's exact runtime (issue #5) and the Arch
package would break on the next update. The RPM and the Arch package
now bundle Microsoft's 1.21.0 build, as the DEB does, and the images
test against it. The RPM builds on Fedora 43. A DEB built on Ubuntu
26.04 joins 24.04's, and ubuntu.sh picks by codename.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 14:22:16 -04:00

189 KiB
Raw Blame History

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

  • 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

  • 1.1 LayerTransform, LayerAppearance, CanvasViewport, ImageLayer, CanvasDocument, NavigationTool, ImportedImage. Tests: twins of the pure-geometry cases in TransformTests, LayerAppearanceTests.
  • 1.2 DocumentHistory. Tests: HistoryTests.
  • 1.3 RasterSnapshot, BrushRaster/BrushPatch helpers. Tests: RasterSnapshotTests.
  • 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.
  • 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.
  • 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

  • 2.1a DownsampleCache: Lanczos halvings in place of vImage, threaded with QtConcurrent, rgba_clamp_premultiplied from AdjustPixels.c. Tests: the cache cases of DownsampleTests.
  • 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.
  • 2.1c From LayerMask: clipImage, MaskPlacementCache, FolderMaskClip.
  • 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.
  • 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.
  • 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).
  • 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

  • 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.
  • 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).
  • 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

  • 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.
  • 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).
  • 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.
  • 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.
  • 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.
  • 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).
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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).
  • 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.
  • 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".
  • 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)

  • 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)

  • 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.
  • 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)

  • 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.
  • 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.
  • 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.
  • 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.
  • 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)

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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)

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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)

  • 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.
  • 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.
  • 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.
  • 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.
  • 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

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.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.
  • 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.
  • 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 mouseDowns); 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().