51 KiB
PhotoCraft scorecard
Generated by cargo xtask scorecard from perf/budgets.toml, perf/baseline.json, crates/io/tests/corpus.rs, scorecard/*.toml and the source tree. Do not edit by hand: change the sources and regenerate (CI runs cargo xtask scorecard --check). Checklist statuses are verified against the code; numbers here supersede estimates elsewhere.
Summary
| Area | Done | Partial | Missing | Key numbers |
|---|---|---|---|---|
| Performance | 3 | 22 | 13 | 47 scenarios: 3 within budget, 22 over budget or failing, 15 tracked only, 6 not run, 7 not measurable yet (baseline 2026-10-05) |
| File compatibility | 3 | 11 | 13 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
| Tools | 9 | 14 | 8 | |
| Workspace and UI | 0 | 5 | 18 | settings that do nothing: 56 of 140 (target 0) |
| Type | 0 | 3 | 6 | |
| Automation | 3 | 2 | 6 | 627 of 627 menu items live (100.0%) |
| Reliability | 4 | 1 | 2 | never-crash lints on 27 of 27 crates; panic_hunt present |
| Distribution | 4 | 4 | 3 | |
| Open bugs | 12 | 4 | 16 |
Performance
Budgets: perf/budgets.toml (targets from #209, #210, #211). Numbers: the committed baseline (perf/baseline.json, full run), updated only by cargo xtask perf --update-baseline. The nightly perf-nightly workflow runs cargo xtask perf on a fixed macOS runner and fails on a broken enforced budget or a p50 regression over 15 % against a baseline from the same machine class. "Over budget" rows are targets the code doesn't meet yet: they are reported, and only their regressions fail.
Baseline: 2026-10-05, commit f5d2bcb54a, Apple M4 Pro (48 GB), GPU Apple M4 Pro, macOS 15.6; load average 224.90 / 226.31 / 196.03 (1/5/15 min) at the start of the run.
| Id | Scenario | Target | p50 ms | p95 ms | max ms | Peak RSS | GPU | Status | Issue |
|---|---|---|---|---|---|---|---|---|---|
| P1 | Move a layer with layer styles (per drag update) | ≤ 25 ms p95 | 321 | 535 | 535 | 825 MB | 409 MB | over budget (target not met yet) | #209 |
| P2 | Change layer opacity | ≤ 20 ms p95 | 276 | 443 | 443 | 639 MB | 409 MB | over budget (target not met yet) | #209 |
| P3 | Change layer fill | ≤ 20 ms p95 | 279 | 467 | 467 | 506 MB | 409 MB | over budget (target not met yet) | #209 |
| P4 | Change blend mode | ≤ 20 ms p95 | 206 | 423 | 423 | 579 MB | 409 MB | over budget (target not met yet) | #209 |
| P5 | Toggle visibility | ≤ 10 ms p95 | 178 | 425 | 425 | 668 MB | 409 MB | over budget (target not met yet) | #209 |
| P6 | Undo a move | ≤ 25 ms p95 | 256 | 530 | 530 | 650 MB | 409 MB | over budget (target not met yet) | #209 |
| P7 | Redo a property change | ≤ 25 ms p95 | 144 | 212 | 212 | 669 MB | 409 MB | over budget (target not met yet) | #209 |
| P8 | Duplicate a 12 MP layer (first) | ≤ 100 ms p95 | 508 | 699 | 699 | 907 MB | 919 MB | over budget (target not met yet) | #209 |
| P9 | Duplicate a 12 MP layer (repeat) | ≤ 20 ms p95 | 406 | 627 | 627 | 749 MB | 919 MB | over budget (target not met yet) | #209 |
| P10 | Paste a 12 MP layer (first) | ≤ 100 ms p95 | 1232 | 3136 | 3136 | 1.3 GB | 1.4 GB | over budget (target not met yet) | #209 |
| P11 | Paste a 12 MP layer (repeat) | ≤ 20 ms p95 | 445 | 1174 | 1174 | 958 MB | 1.4 GB | over budget (target not met yet) | #209 |
| P12 | Arrow-key nudge in a 150-layer document | ≤ 30 ms p95 | – | – | – | – | – | missing: panicked: wgpu error: Validation Error / Caused by: / In Queue::write_buffer / Buffer with 'pc_compose_uniforms' label is invalid | #209 |
| P13 | Brush dab on a 15000×10000 16-bit document | ≤ 80 ms p95 | 6.42 | 146 | 146 | 839 MB | 2.0 GB | over budget (target not met yet) | #209 |
| P14 | Move a layer on a 15000×10000 16-bit document | ≤ 80 ms p95 | 2698 | 4318 | 4318 | 1.5 GB | 2.0 GB | over budget (target not met yet) | #209 |
| P16 | Brush dab on a 32-bit document with 20 layers | ≤ 100 ms p95 | 21.5 | 161 | 161 | 793 MB | 1.6 GB | over budget (target not met yet) | #209 |
| P17 | Move a layer on a 32-bit document with 20 layers | ≤ 100 ms p95 | 386 | 681 | 681 | 1.2 GB | 1.6 GB | over budget (target not met yet) | #209 |
| P18 | Open a 20 MP layered PSD: first pixels (today: fully opened) | ≤ 1000 ms p95 | 1753 | 2794 | 2794 | 2.2 GB | 1.9 GB | over budget (target not met yet) | #210 |
| P19 | Open a 20 MP layered PSD: editable | ≤ 3000 ms p95 | 1753 | 2794 | 2794 | 2.2 GB | 1.9 GB | pass | #210 |
| P21 | Content-Aware Fill on 10 MP | ≤ 1500 ms p95 | 218 | 256 | 256 | 1.8 GB | 1.8 GB | pass | #210 |
| P22 | Spot Healing stroke on 10 MP | ≤ 500 ms p95 | 157 | 247 | 247 | 1.8 GB | 1.9 GB | pass | #210 |
| P24 | Import 500 brushes (longest UI-thread step) | ≤ 100 ms max | – | – | – | – | – | not run | #210 |
| P25 | Cancel a Gaussian Blur job on 24 MP (until the worker exits) | ≤ 200 ms max | – | – | – | – | – | not run | #210 |
| P49 | Cancel a Content-Aware Fill job on 24 MP (until the worker exits) | ≤ 200 ms max | – | – | – | – | – | not run | #210 |
| P50 | App frame time while a 24 MP Gaussian Blur job runs | ≤ 16 ms p95 | – | – | – | – | – | not run | #210 |
| P26 | Select › Modify › Smooth on 24 MP (radius 50) | ≤ 100 ms p95 | 99.9 | 216 | 251 | 1.0 GB | – | over budget (target not met yet) | #211 |
| P27 | Select › Modify › Feather on 24 MP (radius 50) | ≤ 100 ms p95 | 122 | 357 | 360 | 1.1 GB | – | over budget (target not met yet) | #211 |
| P28 | Content-Aware Scale 24 MP (to 90 % width) | ≤ 3000 ms p95 | 954192 | 954192 | 954192 | 2.1 GB | – | over budget (target not met yet) | #211 |
| P29 | Select Subject on 24 MP, repeated | ≤ 100 ms p95 | 559 | 693 | 693 | 1.4 GB | – | over budget (target not met yet) | #211 |
| P31 | Camera Raw exposure change (proxy) | ≤ 10 ms | – | – | – | – | – | not run | #211 |
| P33 | Brush dab on A4 300 ppi CMYK (dab to screen) | ≤ 6 ms p95 | 34.7 | 235 | 235 | 775 MB | 87 MB | over budget (target not met yet) | #211 |
| P36 | Camera Raw exposure change with all SDR scopes (CPU) | ≤ 10 ms p95 | – | – | – | – | – | not run | #211 |
| P40 | Brush dab 60 px on 36 MP | track only | 1.45 | 3.19 | 3.19 | 1.3 GB | – | tracked | |
| P41 | Gaussian Blur r 40 on 36 MP | track only | 2963 | 3069 | 3069 | 1.9 GB | – | tracked | |
| P42 | Curves dialog change, layer preview, 36 MP | track only | 38.7 | 158 | 158 | 1.1 GB | – | tracked | |
| P43 | Save .psd, 36 MP, 3 layers | track only | 3047 | 3446 | 3446 | 1.2 GB | – | tracked | |
| P44 | Open .psd, 36 MP, 3 layers | track only | 3535 | 4547 | 4547 | 1.6 GB | – | tracked | |
| P45 | Full refresh with layer effects, warm caches, 36 MP (GPU) | track only | 105 | 160 | 160 | 714 MB | – | tracked | |
| P46 | Brush dab on a layer with effects, 36 MP (GPU) | track only | 30.6 | 83.3 | 83.3 | 863 MB | – | tracked | |
| P47 | Font-size drag step, 50 type layers | track only | 23.3 | 60.5 | 71.2 | 600 MB | – | tracked | |
| P48 | Full refresh of a 14000×14000 document | track only | 8715 | 9897 | 9897 | – | – | tracked |
Not measurable yet (the code can't do it yet; no number is recorded):
| Id | Scenario | Target | Why | Issue |
|---|---|---|---|---|
| P15 | Zoom / pan frame time on a 15000×10000 16-bit document | ≤ 16 ms per frame | no headless canvas frame harness yet (zoom and pan sample the GPU texture in the egui paint callback) | #209 |
| P20 | Open a 2 GB PSD / 4–5 GB PSB | opens with progress, editable, never blank | opens run in the background with progress (#210), but there are no generated large-file fixtures yet (#216) | #210 |
| P23 | Warp / Puppet / Perspective preview at 4K | ≤ 16 ms per frame, commit in the background | the previews run inside the UI tools; no headless frame harness, no background commit | #210 |
| P30 | Liquify 800 px brush on 3500×2200 at full resolution | ≤ 1 ms per update | the Liquify proxy is capped at 1600 px and the stroke path lives in the UI tool | #211 |
| P32 | Brush release on a long stroke | no hitch at mouse-up | the live renderer and the release re-render are UI-side; needs a frame harness | #211 |
| P34 | Brush preview memory, small dab on an 8K viewport | ≤ 2 MiB | the live brush preview buffers are UI-side; no headless counter yet | #211 |
| P35 | Marquee drag on 24 MP with Alt/Shift | ≤ 17 ms p95 | Shift/Alt marquee modifiers don't exist yet (#208) and the drag preview is UI-side | #211 |
File compatibility
Corpus floors (crates/io/tests/corpus.rs): the enforced minimums; the corpus tests (cargo feature corpus, cargo xtask test-corpus, always run in CI) fail below them.
| Corpus | Directory | Files | Oracle pass floor | Round-trip floor |
|---|---|---|---|---|
| io corpus | corpus/psd |
170 | 146 | 169 |
| psd-tools corpus | corpus/psd-tools |
309 | 229 | 307 |
| photoshop oracles | corpus/photoshop |
256 | 133 | 258 |
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| FILE-215-1 | Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels | done | #215 | psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded |
| FILE-215-2 | Smart Filters survive PSD save and re-open (filterFX descriptor read and written) | partial | #215 | smart_map.rs: 14 filters plus a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning |
| FILE-215-3 | Filter mask can be painted directly, with undo | missing | #215 | smart_cmds.rs:382 mask only from the selection |
| FILE-215-4 | Adjustments (Hue/Sat + Colorize, Curves, Levels, Shadows/Highlights) apply as Smart Filters | partial | #215 | PSD Levels/Curves/Shadows-Highlights smart filters render (adjust_cmds.rs adjust_as_filter); Image › Adjustments on a smart object doesn't add one yet |
| FILE-215-5 | Smart Filter rows: collapse, mask thumb, blend icon, drag reorder, context menu, live preview | partial | #215 | smart_ui.rs:31 rows + visibility + editor; no collapse, drag or context menu |
| FILE-215-6 | Smart Object corner skew and perspective (quad or homography transform) | missing | #215 | doc lib.rs:312 SmartObject.transform is an Affine |
| FILE-215-7 | Layered TIFF read and write (tag 37724), honouring the ask-before-saving pref | partial | #215 | psd tiff.rs (ImageSourceData, both byte orders), io tiff_layers.rs, ui tiff_options_ui.rs (Discard Layers and Save a Copy); extra alpha channels and a Quick Mask are not written (the TIFF codec holds one transparency sample) |
| FILE-215-8 | Large and BigTIFF import with banded decode across all IFDs | done | #215 | codecs tiff_ifd.rs (bounds-checked IFD chain + SubIFD walk, classic and BigTIFF, cycle-safe; tiff_info / decode_tiff_page), tiff.rs banded parallel strip/tile decode, planar 1 and 2, BigTIFF write (tiff_bigtiff or > 4 GiB); io import_tiff_page; 30 MP RGB8 Deflate 345 -> 32 ms; no page picker in the UI yet |
| FILE-215-9 | SVG opens and places as shape layers or a vector Smart Object | done | #215 | io svg.rs: paths, groups, text outlines as shape layers (gradients approximated; clip/mask/filter/pattern/image subtrees rasterised); smart_cmds.rs renders an SVG source at its placement scale |
| FILE-215-10 | Multi-page PDF save, PDF presentation, layer comps to PDF, vector/text output, PDF import | missing | #215 | Only print_cmds.rs:133 print_pdf (raster print page) |
| FILE-215-11 | RAW: lossy DNG, highlight modes, .xmp sidecars, RAW prefs wired, per-camera corpus | missing | #215 | raw sensor.rs:210 lossy DNG unsupported; raw_defaults unread; no xmp |
| FILE-215-12 | Camera Raw dialog shows a histogram; settings persist as a PSD Smart Filter | partial | #215 | Interactive RGB/Lab scopes. PSD WB/Light/Presence, curves, HSL, grading, sharpening/noise detail, grain and numeric vignette controls mapped with descriptor preservation. Active manual Optics and unverified vignette style keep the updated ACR 18.4 fixture opaque; PSD/.pcraft saves preserve it. HDR, remaining settings and Photoshop acceptance still open |
| FILE-215-13 | Video layer frames persist in .pcraft; PSD video layers mapped | partial | #215 | format convert.rs:89 + tests/roundtrip.rs:282 persist frames; no PSD video mapping |
| FILE-215-14 | User swatch model with ACO/ASE import and export | missing | #215 | panels.rs:954 const SWATCHES: [[u8; 3]; 40] |
| FILE-215-15 | Layer Style contour picker, curve editor, presets, .shc import/export | missing | #215 | doc effects.rs:57 Contour model only |
| FILE-215-16 | Advanced Blending (knockout, blend as group, mask hides effects) end to end | missing | #215 | No knockout / blend_interior / transparency_shapes in doc, compose or io |
| FILE-215-17 | CMYK/RGB documents preview spot inks; spot plates export (TIFF extra samples, PDF) | missing | #215 | compose multichannel.rs:1 spot preview for Multichannel only |
| FILE-215-18 | Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling | partial | #215 | file_ui.rs:188 last_web in-session only; export_dialog.rs:16 fixed defaults |
| FILE-215-19 | Save As JPEG opens a JPEG Options dialog (quality 0-12, format options) | missing | #215 | ExportSettings has jpeg_quality 1-100 only (lib.rs:118) |
| FILE-215-20 | Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages | partial | #215 | artboard_ui.rs:1 names/properties only; no tool, + buttons or duplicate |
| FILE-215-21 | Local libraries of colours, patterns, images and layers, as linked items or copies | missing | #215 | Only preset libraries (preset_store.rs) |
| FILE-216-1 | Every corpus fetched and enforced in CI, cached by pin | missing | #216 | No corpus step in ci.yml; in progress in #190 |
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | partial | #216 | 4-5 GB PSB fixtures: psd example big_psb writes them (raw and RLE, planes past 2 GiB), io example big_psb_check opens and checks them; opt-in io tests/large_psb.rs (2.5 GB merged image). No 2 GB PSD, 150-layer or 20 MP fixture, nothing in CI |
| FILE-216-3 | TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported | partial | #216 | EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report |
| FILE-216-4 | Exports re-open in a third-party oracle editor | missing | #216 | The round trip is our export -> our import |
| FILE-216-5 | Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure | missing | #216 | Features absent; no atomic-save failure-injection test |
| FILE-216-6 | 30-50 end-to-end workflow acceptance tasks driven through MCP or commands | partial | #216 | automation/tests/agent_tasks.rs: 10 MCP tasks |
Tools
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| TOOL-213-1 | Pencil tool: aliased 1 px painting, square pixel-aligned cursor | done | #213 | Tool::Pencil (B group): live aliased strokes on the pixel grid, square cursor, Auto Erase; pencil_tests.rs |
| TOOL-213-2 | Mixer Brush tool on the toolbar | done | #213 | Tool::MixerBrush in the Brush (B) flyout; Wet/Load/Mix/Flow/Sample All Layers options, command/control dispatch and selection-clipping tests |
| TOOL-213-3 | Patch tool with a live preview of the patched area | done | #213 | paint.patch (retouch_cmds/patch.rs, source/destination) + Tool::Patch (J group: lasso, then drag the selection); the drag shows the heal live (patch_preview.rs, coarse colour fit: 1000x1000 in 52 ms) |
| TOOL-213-4 | Content-Aware Move tool reusing inpaint completion | partial | #213 | paint.contentAwareMove (retouch_cmds/content_aware_move.rs: move/extend, Structure 1-7, Color 0-10, Sample All Layers; background job) + Tool::ContentAwareMove (J group: lasso, then drag). Missing: Transform On Drop, a live preview of the result (the drag shows the outline only) |
| TOOL-213-5 | Painting a type/shape/smart/fill layer offers to rasterize, then continues the stroke | done | #213 | rasterize_prompt.rs: OK rasterizes then paints (two history states), Cancel does nothing; drivable via ui.dialog.confirm |
| TOOL-213-6 | Fill dialog: contents, blend, opacity, preserve transparency; remembered; Shift+Backspace | done | #213 | fill_ui.rs dialog over edit.fill (fill_cmds.rs): all contents, mode, opacity, preserve; remembered in prefs; Shift+F5 / Shift+Backspace |
| TOOL-213-7 | Live stroke preview for every brush-based retouch tool | missing | #213 | Live stroke only for Brush and Eraser (canvas.rs:1684) |
| TOOL-213-8 | Clone Source: W/H link, drawn overlay (opacity/blend/clip/auto-hide), Alt target cursor | partial | #213 | Clone Source overlay drawn with opacity, inversion, circular clipping and auto-hide; W/H link, custom-tip clipping and overlay blend modes remain missing |
| TOOL-213-9 | Eyedropper comparison ring (new colour above, current below) while sampling | done | #213 | eyedropper_ring (canvas.rs): the sampled colour on the upper arc, the foreground below, drawn for the Eyedropper and Alt-sampling while the button is held; the Precise-cursor preference keeps the crosshair |
| TOOL-213-10 | Ctrl+Alt drag resizes brush size/hardness with an on-canvas readout | missing | #213 | Only [ ] resize (shortcuts.rs:270) |
| TOOL-213-11 | Editable pressure curve, global and per control | missing | #213 | No pressure curve in engine, paint or ui-egui |
| TOOL-213-12 | Import an image file as a brush tip | missing | #213 | Only brush.defineFromSelection and brush.presets.importAbr |
| TOOL-213-13 | Brush regression tests: no seams, hard edge, zero-scatter defaults, ABR never black | done | #213 | Seam, hard-edge and zero-scatter tests in brush_tests.rs; ABR tip colour test in abr_map/tests.rs |
| TOOL-213-14 | Tablet: no OS arrow cursor over the canvas on macOS, Windows and Linux | partial | #213 | Brush tools set CursorIcon::None (canvas.rs:1259-1275); not verified per OS |
| TOOL-214-1 | Select and Mask workspace: view modes, Refine Edge brush, 6 outputs | partial | #214 | Opens the generic select.refineEdge dialog (menus.rs:308) |
| TOOL-214-2 | Marquee: Shift square, Alt from centre, live W x H readout | partial | #214 | marquee_end supports shift (chrome_ui.rs:167) but canvas.rs passes false (#208) |
| TOOL-214-3 | Color Range test: Sampled Colors at Fuzziness 0 selects the exact match only | partial | #214 | Fuzziness-0 tests cover midtones/shadows only (selection_cmds.rs:444-447) |
| TOOL-214-4 | Move tool: box-select layers, Auto-Select hover outline, double-click fit, drag to a tab | missing | #214 | move_mods.rs covers Shift/Alt/arrows only |
| TOOL-214-5 | Right-click menu during Free Transform (scale, rotate, skew, warp, flip, rotate 90/180) | missing | #214 | No context menu in canvas.rs or transform_tool.rs |
| TOOL-214-6 | Puppet Warp mesh follows the object outline (constrained triangulation) | missing | #214 | algo/puppet.rs:136 builds a block-grid mesh |
| TOOL-214-7 | Content-Aware Scale through the Free Transform box with a live proxy preview | partial | #214 | edit.contentAwareScale (edit_menu_cmds.rs:899) behind a parameter dialog |
| TOOL-214-8 | A measured ruler angle prefills Image Rotation > Arbitrary (0.01 deg) | missing | #214 | Arbitrary rotation is a generic dialog; ruler only feeds straighten |
| TOOL-214-9 | Crop: W x H x Resolution with units, saved crop presets, Straighten in the options bar | partial | #214 | Ratio dropdown and fields only (panels.rs:551-579) |
| TOOL-214-10 | Pen: Alt breaks a handle, Shift 45 deg, Ctrl direct-selects, Backspace, close-drag handle | partial | #214 | Ctrl/Cmd = Direct Selection and Alt = Convert Point on existing paths (direct_select.rs, #790); no Shift 45 deg, Backspace or Alt while drawing |
| TOOL-214-11 | Stroke Path dialog: pick any brush-based tool, Simulate Pressure | partial | #214 | path.stroke brush, pencil, eraser (vector_cmds.rs:1170); brush-only button, no dialog |
| TOOL-214-12 | Shapes: on-canvas corner radius handles, shape feather, selection to shape with tolerance | partial | #214 | select.convertToShape with tolerance (vector_cmds.rs:1256); radius as a field only |
| TOOL-214-13 | Mask Properties: Density and Feather controls | partial | #214 | Vector-mask density/feather in the engine (vector_cmds.rs:948); no UI controls |
| TOOL-214-14 | Vanishing Point window: draw and edit planes, paste, clone in perspective | partial | #214 | filter.vanishingPoint (vp_cmds.rs:15) behind a parameter dialog |
| TOOL-214-15 | Content-Aware Fill workspace: paint the sampling area, preview, rotate/scale/mirror, new layer | partial | #214 | Engine sampling auto, rect, custom (edit_menu_cmds.rs:324); generic dialog |
| TOOL-214-16 | Liquify: redo (Cmd+Shift+Z) | done | #214 | liquify_ui.rs supports bounded redo through Cmd+Shift+Z and the control channel; new strokes clear redo |
| TOOL-213-17 | Red Eye tool: click a pupil to neutralize the red reflection | done | #213 | paint.redEye (redeye_cmds.rs) + Tool::RedEye in the J flyout; Pupil Size and Darken Amount; catchlights kept; a miss is an error |
Workspace and UI
Settings that do nothing: 56 of 140 (target 0, #204). Counted by the prefs audit (xtask/src/scorecard.rs): a Preferences field counts as read when code outside the prefs model (prefs.rs except its impl Session blocks; tests excluded; the schema-driven dialog never names a field) accesses section.field, or .section and .field in the same file, or calls a section method that reads self.field. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: general.color_picker, general.beep_when_done, general.export_clipboard, general.resize_image_during_place, general.always_create_smart_objects_when_placing, general.animated_zoom, general.zoom_resizes_windows, interface.show_channels_in_color, interface.dynamic_color_sliders, workspace.auto_collapse_icon_panels, workspace.auto_show_hidden_panels, workspace.open_documents_as_tabs, workspace.enable_floating_document_window_docking, workspace.large_tabs, workspace.enable_narrow_options_bar, tools.enable_flick_panning, tools.vary_round_brush_hardness_on_hud, tools.show_transformation_values, tools.double_click_layer_mask_launches_select_and_mask, file_handling.image_previews, file_handling.lowercase_extension, file_handling.save_in_background, file_handling.ignore_exif_profile_tag, file_handling.maximize_psd_compatibility, performance.cache_levels, performance.legacy_compositing, scratch_disks.disks, cursors.brush_preview_color, units_and_rulers.type_units, units_and_rulers.column_width, units_and_rulers.gutter, units_and_rulers.print_resolution, units_and_rulers.screen_resolution, plug_ins.show_extension_panels, plug_ins.allow_scripts_to_connect, plug_ins.generator_enabled, type_.smart_quotes, type_.missing_glyph_protection, type_.show_font_names_in_english, type_.use_esc_to_commit, type_.text_engine, type_.font_preview, type_.fill_new_type_layers_with_placeholder, type_.recent_fonts, enhanced_controls.scrubby_slider_acceleration, enhanced_controls.touch_gestures, enhanced_controls.zoom_with_trackpad_pinch, enhanced_controls.rotate_view_with_trackpad, raw_defaults.color_space, raw_defaults.bit_depth, raw_defaults.resolution, raw_defaults.sharpen_for, raw_defaults.open_as_smart_object, raw_defaults.apply_auto_tone, integrations.allow_agent_control, integrations.control_port.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| UI-217-1 | Tool options, last Export As / Save for Web / Fill settings and font recents persist | partial | #217 | Fill (fill_ui.rs:72) and Liquify (liquify_ui.rs:64, #418) keep their settings in Preferences (prefs.dialogs); no tool_options/export fields in Preferences; export_dialog.rs:21 png/85 |
| UI-217-2 | Window size and dock width remembered across restarts | missing | #217 | main.rs:46 native_options(): fixed 1440x900, centred on the main monitor (#419); dock.rs:529 snapshot omits the width |
| UI-217-3 | File > Open starts in the last-used folder | missing | #217 | services.rs:110 FileDialog::new() without set_directory |
| UI-217-4 | Preferences all take effect; recent files persist and honour recent_file_count (#204) | missing | #217 | See the prefs audit and BUG-204-* |
| UI-217-5 | Panel groups can float (menu + drag out of the dock) and are restored at launch | missing | #217 | No floating groups in dock.rs/panels.rs |
| UI-217-6 | Document tabs: overflow list, context menu (Close/Others/All/Reveal), drag to reorder | partial | #217 | canvas.rs: Close/Others/All context menu with unsaved prompt; overflow, Reveal, drag reorder still missing |
| UI-217-7 | Command palette shows recently run commands on an empty query | missing | #217 | palette.rs:83-108 fuzzy hits only |
| UI-217-8 | Tool-specific canvas right-click menu; right-click spelling suggestions in text fields | partial | #217 | paint_mouse.rs brush picker and canvas_tool_menu.rs selection actions; other tools and text spelling menus missing |
| UI-217-9 | UI scale presets 100/115/125/150/200 % through zoom_factor, remembered | missing | #217 | prefs.rs:61 UiScale unused; input_tests.rs:71 asserts zoom_factor stays 1.0 |
| UI-217-10 | Appearance: animation toggle, canvas pattern toggle per theme, start-screen recents | missing | #217 | No such prefs; start screen (canvas.rs:825) has no recents |
| UI-217-11 | Soft dark and soft light themes with picker previews; outlined knobs in light themes | missing | #217 | theme.rs:28 five themes |
| UI-217-12 | Save custom New Document presets; a real Recent category | missing | #217 | new_doc_ui.rs:17 'Recent' is a static list |
| UI-217-13 | Numeric fields accept arithmetic and units; scrubby labels; double-click resets a slider | partial | #217 | widgets.rs:132 DragValue scrubs and takes + - * / arithmetic; no units in expressions, label scrub or reset |
| UI-217-14 | Layers panel: wheel steps blend modes, drop on New/Trash duplicates/deletes, checker behind art | missing | #217 | panels.rs:1108 plain dropdown; trash is click-only (panels.rs:1213) |
| UI-217-15 | Curves: smoothing / pencil mode, black/grey/white eyedroppers, tooltips | missing | #217 | adjust_editors.rs:397 has none of them |
| UI-217-16 | Hue/Saturation: Saturation and Lightness sliders get hue-following colour tracks | missing | #217 | Only the Hue slider has a gradient |
| UI-217-17 | Color panel: wheel plus RGB/HSB/CMYK/Lab/Gray/Web slider modes; resizes with its group | missing | #217 | panels.rs:1837 single field + hex/RGB readout |
| UI-217-18 | Alt+wheel zooms in gentle ~5 % steps; zoom controls uncramped | missing | #217 | canvas.rs:1107-1125 no Alt handling |
| UI-217-19 | Shortcut hints rendered per platform; Reveal label per OS | missing | #217 | ~33 hard-coded modifier glyph strings in ui-egui; menu_catalog.rs:341 one label |
| UI-217-20 | 'Show rulers in new documents' preference; guides stored as f64 | missing | #217 | No rulers pref; doc lib.rs:523 guides are Vec |
| UI-217-21 | One Preset Manager for every preset kind with folders, search, reorder, load/save sets | partial | #217 | edit_menu_cmds.rs:714 covers brushes, custom shapes, patterns with reorder |
| UI-217-22 | Stamp Visible into a new layer keeping originals; Alt+Merge keeps originals | missing | #217 | Only layer.mergeVisible (edit_cmds.rs:416) |
| UI-217-23 | Layer clipboard: copy/paste whole layers (masks, styles, type) between documents | missing | #217 | Only copy/paste layer style (extra_cmds.rs:592) |
Type
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| TYPE-218-1 | Type on a path: path shape, glyphs along the curve, caret follows, PSD round trip | missing | #218 | doc text.rs:209 TextShape has Point and Box only |
| TYPE-218-2 | Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret | missing | #218 | Engine has TextDirection (text.rs:155); no UI |
| TYPE-218-3 | Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL | missing | #218 | fonts.rs:28 fallback covers Arabic/Hebrew; no per-script golden tests (only a bidi test) |
| TYPE-218-4 | Variable font axis sliders from the font's axes; opsz follows the point size | partial | #218 | type_cmds.rs:184 accepts variations; FaceInfo.axes unused in the UI |
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | #218 | Type > OpenType toggles (type_extra_cmds.rs:512); no panel buttons or submenus |
| TYPE-218-6 | Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) | missing | #218 | Optical renders as Metrics (text.rs:21); Alt+arrow moves by word |
| TYPE-218-7 | Warp Text dialog previews live and opens with the layer's current warp | missing | #218 | view_cmds.rs:834 always seeds arc, bend 50 |
| TYPE-218-8 | Text editing: select all, double/triple/quadruple-click selection, paste, with tests | partial | #218 | Cmd+A, paste and double-click exist; no triple/quadruple click, no tests |
| TYPE-218-9 | Offer 'Rasterize and continue?' for Distort/Perspective/Warp on live type | missing | #218 | No such prompt |
Automation
Menu wiring (docs/parity.md): 627 of 627 menu items live (100.0%). This counts dispatch, not behaviour.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| AUTO-1 | Every menu item dispatches a command id (UI, CLI, control channel and MCP share them) | done | docs/parity.md: all menu items live; menus.rs is_live | |
| AUTO-219-1 | Record actions in a document-independent form (implicit layer, merged type keystrokes) | missing | #219 | actions.rs:47 copies raw journal entries with explicit layer ids |
| AUTO-219-2 | Actions panel: step and dialog toggles, reorder, delete a step, Stop, sets, .atn import | missing | #219 | Panel deletes whole actions only (actions.rs:146) |
| AUTO-219-3 | actions.record/stop/play/list commands for agents and the CLI | done | #219 | actions.list/get/record/stop/play/delete in actions_cmds.rs; the panel, CLI and MCP share them. Nested steps pass the session authorize hook. |
| AUTO-219-4 | Replay tests for crop, filters, selections and styled text on a second document | missing | #219 | One replay test (actions.rs:171: new layer, rect select, fill) |
| AUTO-219-5 | Batch: subfolders, open documents, naming template, progress/cancel, error log, stop | partial | #219 | file_cmds.rs:590-631 folder/array input, per-file errors; no recursion, template or cancel |
| AUTO-219-6 | Plug-in ABI v2: panel kind, progress/cancel calls, read other layers, Last Filter repeats | missing | #219 | runtime.rs:15 ABI_VERSION = 1; manifest.rs:18 Kind::Filter only |
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | missing | #219 | No ONNX runtime dependency |
| AUTO-219-8 | xtask check that every panel button routes through a command id | missing | #219 | No such xtask check |
| AUTO-219-9 | Headless commands for in-canvas type editing, Liquify/Puppet strokes, transform handles | done | #219 | type.hitTest, type.caret, type.navigate; filter.liquify strokes, edit.puppetWarp pins, type.edit, edit.transform |
| AUTO-219-10 | Agent acceptance suite grown to 30-50 workflows | partial | #219 | automation/tests/agent_tasks.rs runs 10 workflows over MCP |
Reliability
Never-crash attribute on 27 of 27 crate roots.
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| REL-1 | panic_hunt fuzzes every command with adversarial params | done | engine/tests/panic_hunt.rs: every command, own thread, 4 s timeout; #[ignore], run in the gate | |
| REL-2 | Every workspace crate carries the never-crash deny(clippy::unwrap_used, ...) attribute | done | Counted automatically above (crate roots under crates/ and apps/); testkit gained it in #221 | |
| REL-3 | The app catches escaped panics around command dispatch and import/export | partial | engine lib.rs:336 dispatch; crash_guard.rs:21 import/export; the dispatch guard is untested | |
| REL-4 | Atomic save (temp, fsync, rename) for every document write | done | #203 | photocraft_format::atomic_write (format/src/atomic.rs, #230): same-dir temp, sync_all, rename with Windows PermissionDenied retry, Unix dir fsync; every document write site routes through it and format/src/atomic/tests.rs failure-injects write, rename and read-only-dir errors |
| REL-212-1 | Fallible allocation for paste/erase/fill/transform/save/import, clear out-of-memory error | missing | #212 | try_reserve only in io/flat.rs:221; codec max_alloc limits cover decode only |
| REL-212-2 | Export switches to PSB when the estimated file size exceeds 2 GB | done | #212 | PSD export uses checked size estimates for raster, masks, channels, and retained metadata |
| REL-212-3 | Tests: an allocation-limit injection makes each command Err with the document unchanged | missing | #212 | Only codec/raw max_alloc tests (codecs/tests/limits.rs:48) |
Distribution
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| DIST-1 | macOS universal build (aarch64 + x86_64), signed and notarized | done | release.yml:66,75,93 (macos-15); 0.2.0 shipped notarized | |
| DIST-2 | Windows x64 and x86 builds | partial | release.yml:113-114; signing skipped with a warning until the signing material exists | |
| DIST-3 | Linux x86_64 and aarch64: AppImage, deb, rpm, tar.gz | done | release.yml:162-163,186 | |
| DIST-4 | Flatpak single-file bundles, x86_64 and aarch64 | done | release.yml:204-216 (#163) | |
| DIST-5 | Web build (wasm32, trunk) | done | release.yml:312 | |
| DIST-6 | FreeBSD 14 x86_64 tar.gz (/usr/local tree) built in a FreeBSD VM | partial | release.yml:242-283 (freebsd job) + packaging/freebsd/package.sh; layout checked with --dry-run, not yet produced by a release run | |
| DIST-220-1 | Help > Check for Updates plus an opt-out periodic release-feed check on all desktops | missing | #220 | No update check in crates |
| DIST-220-2 | Windows ARM64 (aarch64-pc-windows-msvc) in the release matrix, signed like x64 | missing | #220 | release.yml:113-114 only x86_64 and i686 msvc |
| DIST-220-3 | In-app bug report with reviewable diagnostics that opens a prefilled issue | missing | #220 | No bug report dialog in crates |
| DIST-220-4 | Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew | partial | #220 | cargo xtask i18n-coverage reports per-language coverage (#427); no RTL layout yet |
| DIST-220-5 | Linux AppImage x86_64 + aarch64 ship; Flatpak kept; Flathub later (#173) | partial | #220 | AppImage and Flatpak ship (release.yml:162-216); Flathub not done |
Open bugs
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| BUG-203-1 | Every document write goes through one atomic_write (temp, fsync, rename) | done | #203 | photocraft_format::atomic_write (format/src/atomic.rs, #230/8227071); every write site routes through it: engine file_cmds.rs write_file and preset_store.rs, automation files.rs/headless.rs/workspace.rs, app services.rs write_atomic, format store.rs and autosave.rs |
| BUG-203-2 | Temp file in the same dir, sync_all, rename over target, fsync dir on Unix | done | #203 | atomic.rs: temp_name in the same dir, create_new + write_all + sync_all, Unix directory fsync after the rename |
| BUG-203-3 | Windows: retry the rename with backoff on PermissionDenied | done | #203 | atomic.rs RenameRetry::platform(): seven retries from 10 ms on Windows; retry_rename is shared with automation's cap-std workspace write |
| BUG-203-4 | Unwritable directory errors clearly, never falls back to writing in place | done | #203 | atomic.rs maps read-only dir, disk-full and cross-device rename errors to messages that say the original was not changed; there is no in-place fallback path |
| BUG-203-5 | On any failure the original file is untouched and the temp file removed | done | #203 | atomic.rs removes the temp file on every failure; tests assert the original bytes survive write and rename failures |
| BUG-203-6 | Failure-injection tests: mid-write, rename and read-only dir errors keep original bytes | done | #203 | format/src/atomic/tests.rs: Faulty AtomicFs injects mid-write StorageFull, rename failure and a read-only dir; locked-target retries, symlink and file-mode preservation covered too |
| BUG-204-1 | Interface prefs (UI scale, font size, channels in colour, sliders, tooltips) wired or hidden | missing | #204 | prefs.rs:196-202; no reader outside prefs.rs (see the prefs audit) |
| BUG-204-2 | workspace.large_tabs, animated_zoom, auto_show_home_screen wired or hidden | missing | #204 | prefs.rs:163,166,229; no reader outside prefs.rs |
| BUG-204-3 | RAW prefs and ask_before_saving_layered_tiff read, or hidden | partial | #204 | ask_before_saving_layered_tiff drives the TIFF Options prompt (ui-egui tiff_options_ui.rs); raw_defaults (prefs.rs:712) still has no readers |
| BUG-204-4 | Recent files persisted through prefs across restarts | done | #204 | crates/ui-egui/src/lib.rs::push_recent stores the list in preferences; prefs_ui::load restores it and recent_files_survive_a_restart_and_honour_the_count verifies reload |
| BUG-204-5 | Recent-files list honours recent_file_count (default 20) | done | #204 | lib.rs push_recent caps the list with recent_cap (recent_file_count); menus.rs covers default and custom limits |
| BUG-204-6 | Test: every Prefs field is read somewhere or on a hidden allowlist | partial | #204 | The scorecard's prefs audit counts unread fields; no engine test or allowlist yet |
| BUG-205-1 | Free Transform with a selection moves only the selected part of a linked mask | missing | #205 | transform_cmds.rs:127-130 warps the whole linked mask even when the selection split pixels |
| BUG-205-2 | Live transform preview draws the layer with its mask applied | missing | #205 | transform_tool.rs:173 preview_image uses the raw surface |
| BUG-205-3 | edit.transform honours target mask: transforms only a targeted, unlinked mask | missing | #205 | transform_cmds.rs:214-270 never reads a target param |
| BUG-205-4 | Test: a linked raster mask with no selection moves with the pixels | partial | #205 | Implemented at transform_cmds.rs:127; no raster-mask test |
| BUG-205-5 | Tests: linked mask + selection stays aligned; unlinked stays; targeted mask only | missing | #205 | Unlinked case honoured (m.linked check, l.128) but untested; the other two not implemented |
| BUG-206-1 | CharStyle models per-character manual kerning separately from the auto-kern mode | missing | #206 | doc/text.rs:76 only kerning: Kerning enum (Metrics/Optical/Off) |
| BUG-206-2 | PSD import keeps Optical and manual Kerning values | missing | #206 | text/psd.rs:264 maps anything not Off to Metrics; Kerning value ignored |
| BUG-206-3 | PSD export writes the real AutoKerning mode and Kerning values | missing | #206 | text/psd.rs:380-381 AutoKerning bool only; Kerning hard-coded 0 |
| BUG-206-4 | Alt+Left/Right at a collapsed caret adjusts pair kerning; Character panel field | missing | #206 | type_tool.rs:896-912 only a mode combo |
| BUG-206-5 | Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus | missing | #206 | No kerning round-trip test |
| BUG-207-1 | Retouch tools respect the paint target (mask, alpha channel, Quick Mask) | missing | #207 | retouch_cmds.rs:83 run_stroke always uses l.surface_mut(); no paint_target() call |
| BUG-207-2 | Sample All Layers applies to Blur, Sharpen and Smudge | missing | #207 | Checkbox at retouch_ui.rs:171-190 is not passed to the stroke commands |
| BUG-207-3 | Smudge mixes premultiplied RGBA (no dark fringe at transparent edges) | missing | #207 | No premultiply in paint/src/retouch.rs |
| BUG-207-4 | edit.transform transforms a targeted alpha channel | missing | #207 | transform_cmds.rs transform() has no target handling |
| BUG-207-5 | Tests: per-tool mask/channel targeting, Sample All Layers, no smudge fringe | missing | #207 | Only smudge_drags_colour_along_the_stroke (retouch.rs:407) |
| BUG-208-1 | Shift while dragging a Rect/Elliptical Marquee constrains to a square/circle | done | #208 | canvas.rs marquee_corners applies Shift via Drag::marquee_mods; marquee_tests.rs::shift_during_the_drag_draws_a_square_and_the_readout_follows covers the drag |
| BUG-208-2 | Alt while dragging a marquee draws from the centre | done | #208 | canvas.rs marquee_corners centres Alt drags; marquee_tests.rs::alt_draws_from_the_centre_and_shift_alt_a_centred_circle verifies rectangles and ellipses |
| BUG-208-3 | W x H px readout beside the cursor while dragging a marquee | done | #208 | canvas.rs marquee_readout draws the readout; marquee_tests.rs asserts dimensions while dragging and its removal after release |
| BUG-208-4 | Add/subtract/intersect from modifiers pressed before the drag, kept by a test | partial | #208 | Mods captured at press (canvas.rs:1679, used :1779); only a badge test (tool_feedback.rs:195) |
| BUG-208-5 | Kittest: Shift drag gives a square, Alt drag is centred, readout matches the size | done | #208 | crates/ui-egui/src/marquee_tests.rs covers Shift, Alt, selection bounds, W/H readout and drag completion |