Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
43 KiB
24 — Issue tickets and rewrite invariants
Line numbers refer to ICCery v0.8.5 (
/tmp/ICCeryat analysis time) and the Gronod ArgyllCMS 3.5.0 fork.
Source corpus: Gitea issues #1–#225 (/tmp/iccery-research/issues/), issues-index.md, ROADMAP.md, git log on /tmp/ICCery (main @ e12ef41, tag v0.8.5), PR titles pages 1–2, plus sibling issue dumps argyll-issues.json and cpu-issues.json.
This is a rewrite-invariants document. Every closed bug gets a root cause → fix → invariant. Features are traced so a rewrite does not re-invent the same CLI/IPC contracts.
Open tickets #223 and #224 are implemented on main (see § Open issues). Won't-fix #90 / #96 are documented in § Won't-fix.
1. Feature genealogy (issue → ship)
The product is a printer-profiling workstation: vanilla JS + Tauri v2 wrapping ArgyllCMS sidecars over stdin/stdout JSON (-u) with a disk-artefact wizard.
| Era | Version | Issues | What landed |
|---|---|---|---|
| Scaffold | v0.1.0 |
#1, #2 |
Tauri v2 shell, AGPLv3 isolation, ProcessManager with process:stdout/stderr/exit events |
| Stage 1–2 | v0.1.3 |
#3, #4 |
targen patch sets; printtarg layout + instrument geometry |
| Stage 3 | v0.1.6 |
#5, #6 |
chartread -u state machine + live swatch grid + ΔE₀₀ |
| Stage 4–5 | v0.1.9 |
#7, #8 |
colprof cLUT; profcheck peak/avg/RMS |
| Print + UI | v0.1.10–0.1.13 |
#21, #23, #25–#28, #34, #54, #61 |
Logo; raw GDI/CUPS print; DEVMODE prefs; collapsible logs; 1280×800 window |
| Gamut + ship | v0.1.14–0.2.0 |
#9, #10, #57, #58, #60, #69 |
iccgamut + Three.js; settings/packaging; artefact-gated wizard |
| Hotfix | v0.2.1 |
#84 (from #63 regression) |
Stdin decoupled from Child::wait() |
| M8 | v0.3.0 |
#86, #87, #88, #89 |
instlist; multi-pass average; presets; 3D hull |
| Hotfixes | v0.3.1–0.3.6 |
#103, #108–#116, #119, #127, #134, #137 |
Browse dialogs, GLIBC, averaging snapshots, comm-port vs index, density vs dir, XSS/DPI, fetch-argyll, ARGYLL_NOT_INTERACTIVE, Accept/Override |
| macOS print | v0.4.0 |
#91, #92 |
Universal dmg path + CUPS raw spooler |
| Production | v0.5.0–0.5.5 |
#139–#141, #147/#149, #151, #158–#165 |
Logging, Stage 3 resume from .ti2, advanced targen, kill_all on exit, deterministic -R 1, universal sidecars |
| Enterprise colour | v0.6.0–0.7.4 |
#94, #171, #172, #175–#179, #184, #185, #188, #189 |
CGATS import, overlay tooltips, profile picker, d/u chartread keys, OBA/FWA, button system, swatch split, .gam dual-table parse, ΔE thresholds, ColorSync/vendor bypass, DMG background |
| Hardware + analytics | v0.8.1–0.8.2 |
#204, #93, #95 |
i1Pro2 -Y l; XY tables; drift history |
| Reliability | v0.8.3–0.8.4 |
#210–#215, #225 |
Native file pickers, Node-safe gamut tests, atomic history, frontend CI, Monterey WKWebView survival |
| Cal + install | v0.8.5 |
#224, #223 (tickets still open) |
Stage 0 printcal/applycal; OS profile install |
ArgyllCMS fork genealogy (why ICCery's CLI looks the way it does):
| Argyll issue | Flag / contract ICCery depends on |
|---|---|
argyll #1 |
chartread -u → ROW_COLORS_JSON: per row |
argyll #2 |
targen -u JSON progress |
argyll #3 |
printtarg -u page manifest JSON |
argyll #4 |
colprof -u JSON progress |
argyll #5 |
profcheck -u JSON ΔE report |
argyll #6 |
instlist JSON {event:"instruments", devices:[{port,name,type}]} |
argyll #19 |
printtarg -d "<label>" custom chart legend (ICCery #119) |
argyll #24 |
Windows anonymous-pipe PeekNamedPipe so chartread does not deadlock on ReadFile |
argyll #32 |
ad-hoc codesign -s - of Mach-O sidecars (ICCery #165) |
argyll #37 |
i1Pro2 LED — ICCery shipped as chartread -Y l (#204), not the originally proposed -L |
2. Closed bugs — root cause → fix → rewrite invariant
Grouped by subsystem. Issue numbers are always cited.
2.1 Process manager / IPC
#21 Process logs spill horizontally
- Root cause:
#colprofLog/#profcheckLoghad no CSS; even stages withwhite-space: pre-wrapcould not wrap Argyll's space-less progress dots. - Fix: Class selector
.log-container pre { white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; max-height: 200px; }(PR#22). Later#54made boxes collapsible. - Invariant: All subprocess consoles share one CSS class. Never style per-stage IDs. Argyll stdout is not English prose; it is long runs of
.and must wrap anywhere.
#46 Visible cmd.exe on every Argyll spawn (Windows)
- Root cause: Sidecars are
IMAGE_SUBSYSTEM_WINDOWS_CUI.tokio::process::Commandwithout creation flags allocates a console. - Fix:
command.creation_flags(0x08000000)(CREATE_NO_WINDOW) inProcessManager::spawn(#[cfg(windows)]). - Invariant: Every Windows spawn of an Argyll CUI binary must set
CREATE_NO_WINDOW. Do not "fix" this by rebuilding Argyll as WINDOWS_GUI — that would also kill stdin/stdout.
#63 Zombie children (enhancement that caused #84)
- Root cause: 100 ms
try_waitpolling never removed finishedChildhandles. - Fix (PR
#77):child.wait().awaitand reap from the map. - Invariant: Reap is required. But never
wait()while still holding theMutex<Child>thatsend_stdinneeds — see#84.
#84 P0: Child::wait() deadlocks send_stdin and closes stdin (v0.2.1)
- Root cause: PR
#77awaitedc.wait()underArc<Mutex<Child>>. Tokio'sChild::waitdrops stdin to avoid parent/child deadlock. Interactivechartreadtherefore (1) could not acquire the mutex for Calibrate/Retry/Skip/Cancel, and (2) had its pipe closed immediately after spawn. - Fix (PR
#83, v0.2.1):take()stdin/stdout/stderr at spawn; storeChildStdinin a separatestdins: HashMap<String, Arc<Mutex<ChildStdin>>>;tokio::select!betweenchild.wait()and a oneshotkill_rx. - Invariant: Interactive subprocess lifetime has three independent handles: wait task, stdin mutex, kill oneshot.
send_stdinmust never take theChildlock. Never callwait()beforetake()-ing stdin.
#85 resolve_binary ignores .exe
- Root cause: Path joins used
"targen"; on disk the file istargen.exe.exists()returned false for custom dirs and bundled resources. - Fix (PR
#97): On Windows, try{name}and{name}.exefor both settings dir and resource resolution. Unit-test both. - Invariant: Windows binary lookup is always a two-candidate search. Do not assume Unix names in
tauri.conf.jsonresources.
#116 Duplicate spawn ids steal the live child's maps
- Root cause:
stdins.insert(id)/killers.insert(id)with no occupancy check. Averaging reuseschartread_${basename}. Old wait-taskremove()s the new child's entries → "Process not found" on Calibrate. - Fix (PR
#123): Hard-error ifidis occupied; disable Start / Measure Another Sheet untilprocess:exit. Tokio test: spawn, reject duplicate, stdin still works, kill, reuse. - Invariant: Process IDs are exclusive leases. Prefer a loud error over silently replacing the occupant. After exit, the id is reusable.
#134 Calibrate button does nothing; instlist shows nothing
- Root cause: (1) Without
ARGYLL_NOT_INTERACTIVE=1, Argyll on Windows uses_getch()/ReadConsoleInput(), ignoring piped stdin — especially combined with#46's hidden console. (2) Forkedinstlistemits JSON; frontend only had a text regex. - Fix (PR
#135, v0.3.5):command.env("ARGYLL_NOT_INTERACTIVE", "1")on every spawn; parseevent === "instruments"JSON with text fallback. - Invariant: All Argyll children get
ARGYLL_NOT_INTERACTIVE=1. Frontend instrument lists parse JSON first. Related upstream: argyll#24(PeekNamedPipebeforeReadFileon anonymous pipes) — the env var is necessary but not sufficient on Windows.
#147 / #149 Orphan chartread after window close
- Root cause: Only Stage 3 Cancel called
kill_process.lib.rsused bare.run(...)with noRunEvent::Exit/CloseRequested. Spectrophotometer stays USB-locked. - Fix:
ProcessManager::kill_all(drop stdins, signal killers,start_kill); hook app/window exit. - Invariant: Host exit always drains the process table. Interactive instruments are the scary case, but
colprof/printtargmust die too.
#158 Settings log level is decorative
- Root cause:
tauri-plugin-logfilter hardcodedDebugvsInfofromcfg!(debug_assertions).AppSettings.log_levelwas persisted and ignored. - Fix: Map stored string →
LevelFilterat startup and on Save. - Invariant: A settings control that does not change observable behaviour is a bug. Log level is runtime state, not a compile-time constant.
2.2 Print (Win / Mac / Linux)
#33 Windows CI: unresolved GDI imports
- Root cause:
windowscrate 0.58 does not exportSetICMMode/ICM_OFFunderWin32::Graphics::Gdias used inprint/windows.rs. GitHubwindows-2025runner compiled the print backend and died witherror[E0432]. - Fix: Declare the GDI printing functions/structs used (
CreateDCW, ICM off,StretchDIBits, …) directly rather than depending on incomplete crate bindings (a9802ad,c9e16ab). Later also: runner label, native rustup,shell: powershell. - Invariant: Colour-management bypass (
SetICMMode(ICM_OFF)) is a link-time contract. Pin/vend the FFI; do not assumewindowscrate surface area. CI on the real Windows target is the only proof.
#36 DEVMODE preferences not applied
- Root cause: OEM dialog (
DocumentPropertiesW) writes publicDEVMODEWplusdmDriverExtraprivate bytes. The print path was not forwarding the full buffer (and mishandled the anonymous union fordmDefaultSource/dmOrientation) intoCreateDCW/StartDocW. Epson "Print Preview" etc. therefore vanished. - Fix: Retain the entire DEVMODE allocation including
dmDriverExtra; fix union field access (19449e3). - Invariant: Windows driver settings are a blob, not a parsed struct. Never memcpy only
dmSizebytes. Never reconstruct DEVMODE from a handful of public fields.
#48 CUPS/PPD "Uncorrected Passthrough" shown on Windows
- Root cause:
#cupsOptionsGrouprendered unconditionally. - Fix: Hide on
navigator.userAgentWindows (v0.1.7). - Invariant: Print UI is per-backend. Windows = DEVMODE; Unix = PPD/CUPS. Do not show controls the active spooler cannot honour.
#67 Duplicate print IPC (cleanup, not a user bug)
- Fix: Collapse
get_windows_printers/get_cups_printers/print_target_*intoget_printers/print_target. - Invariant: One IPC surface, platform switch inside Rust.
#188 macOS: Preferences opens System Settings; no way to disable driver CM (Epson XP-55, Canon Pro 9500 II)
- Root cause: "Preferences" deep-linked to the OS Printers & Scanners pane, not the selected queue's driver panel. ColorSync plus vendor filters (
CNIJIntent2, EpsonColorCorrection) re-managed the raw target. Follow-on:PMSessionSetColorMatchingMode3-arg SPI crashed. - Fix (v0.7.2–0.7.3): Native
NSPrintPanelbound to the currentPMPrinter; PPD/lpoptionsmedia-type extraction; vendor uncorrected-color keys;AP_ColorMatchingMode=AP_ApplicationColorMatching; remove the crashing 3-arg SPI and use the validated 2-arg / lock sequence (8b01c98). Windows media_types added in the same stack so CI still built. - Invariant: Raw print means three layers must all be off: OS ColorSync, driver colour, and ICM/CUPS filters. Opening the generic Printers pane is not a preferences dialog. Never call private Core Printing SPIs whose arity is guessed.
Sister-product traps (ICCery-CPU #1, #2, #5) that also apply to any CUPS rewrite:
- PPD
ChoiceName/TranslationString— UI must show the title, send the code (MediaType=13notMediaType=Epson Premium Glossy). - Tray keywords are vendor-specific (
InputSlotvs EpsonEPIJ_FdSovs CanonCNIJMediaSupply). Empty arrays are notnil— coalescing?? ["Auto"]does not fire. - Canon Super Fine is three coordinated keys (
CNIJPrintQuality=0,CNIJPrintMode2=5,CNIJPQualitySlider=5), notcupsPrintQuality=High.
2.3 Stage 1 — targen
#44 Patch count ignored → always 836
- Root cause: Frontend sent
patch_count; Rustbuild_targen_argsonly readtotal_patches.-fomitted → Argyll's built-in default 836 RGB patches. - Fix (v0.1.5, PR
#45): Honourpatch_count(fallbacktotal_patches); unit-test argv. - Invariant:
targenwithout-fis not "use the UI value". Default 836 is an Argyll formula, not an ICCery default. Serde field names must match across JS and Rust and be tested.
#59 Empty working directory
- Root cause: Basename typed without Browse →
cwd: ""→ process launch dir (read-only / undefined). Same hole in later stages. - Fix (v0.1.12): Init
currentWorkingDirtodocument_dir/home_dir; disable Generate until both basename and cwd exist; backend also refuses empty cwd. - Invariant: Empty cwd is illegal. Frontend guard and backend fallback. Never spawn Argyll in the install/resource directory.
#103 Stage 1 Browse does nothing; Generate stays disabled
- Root cause: Frontend called
window.__TAURI__.dialog(Tauri v1 API). WithwithGlobalTauri: trueon Tauri v2,window.__TAURI__.dialogis undefined. Same class of bug as#210/#211. - Fix (v0.3.1, PR
#104): Backend commandselect_target_fileusingapp.dialog(). - Invariant: Never use
window.__TAURI__.dialog. All file pickers are Rust commands with explicit filters. See also#172,#210,#211.
#162 Advanced options misaligned
- Root cause: Mixed control heights (number vs range vs checkbox-without-label) plus a full-width
-crow breaking the 2-col grid; flex-end hacks. - Fix (v0.5.5 / v0.6.1): Semantic sub-groups, normalized row heights, flexbox auto-margins.
- Invariant: Advanced
targencontrols are a 2-column grid of equal-height rows. Checkboxes get a label column. Do not usejustify-content: flex-endto fake alignment.
#172 Preconditioning browse filters .ti1/.ti2/.ti3 instead of .icc/.icm/.mpp
- Root cause: Reused
select_target_file(target artefacts) fortargen -c. - Fix: Dedicated
select_profile_file. - Invariant: Each browse button has its own backend command and extension filter. Sharing "the dialog command" is how you open a Save-
.ti1dialog for an ICC.
#211 CGATS Import opens a Save .ti1 dialog and jumps to Stage 4 with empty basename
- Root cause:
handleImportinvokedselect_target_file(save_file, filterti1). Emptycwd/basenamewere forwarded intoimport_measurement_dataset;wizardState.setTargetnever ran. - Fix (v0.8.3):
select_dataset_file(open, filtersti3/txt/cgats/csv); sync wizard target dir+basename from the imported artefact. - Invariant: Import = open dialog. After import,
basename/cwdare initialized from the dataset before any stage jump. Do not reuse the target-save picker.
2.4 Stage 2 — printtarg
#50 Redundant Stage 2 badges (cleanup)
- Fix: Remove duplicate status text. Not a functional invariant beyond "one source of layout truth".
#58 / #70 TIFF previews fail in WebView; image crate Windows-only
- Root cause: Browsers/WKWebView/WebView2 do not decode TIFF.
img.src = base64(tiff)is a blank.imagewas under[target.'cfg(windows)'.dependencies](GDI print), so Linux/macOS could not even compile a converter. - Fix: Move
image(png+tiff features) to common deps;read_tiff_preview_pngdecodes, caps ~1200 px, returns PNG base64. Keepread_file_base64for text artefacts (.gam). - Invariant: Never feed TIFF to
<img>. Preview pipeline is always TIFF→PNG in Rust. Theimagecrate is a common dependency, not Windows-only.
#68 printtarg JSON parsed by hunting for a brace
- Root cause: String-index search for
{\n "event": "manifest"instead of a structured object. - Fix: Parse a single JSON event (Argyll
#3-umanifest). - Invariant:
-utools emit one JSON object per event. Accumulate stdout,JSON.parsewhole objects; do notindexOfpretty-printed fragments.
#163 Regenerating Stage 2 shuffles patches (printed sheet ≠ .ti2)
- Root cause:
printtargrandomizes layout unless-R <seed>or-r.build_printtarg_argspassed neither. - Fix (v0.5.5): Default
-R 1; optional custom seed and raster-order-r. - Invariant: Printed charts are not reproducible without an explicit seed. Default is deterministic. Re-running Stage 2 on the same
.ti1must produce the same.ti2/.tiforchartreadwill score the wrong patches.
#119 (feature, not bug) printtarg -d is a label, not a directory
- See idiosyncrasies. Do not confuse with
iccgamut -d(#112).
2.5 Stage 3 — chartread / averaging / XY
#109 Multi-pass averaging overwrites the same .ti3
- Root cause:
#87UI shipped, but everychartreadwrote{basename}.ti3. Pass 2 clobbered pass 1.averagewas invoked asfoo.ti3 foo.ti3 foo.ti3. - Fix: After each successful exit, snapshot
{basename}.ti3→{basename}_pass{N}.ti3. Canonical{basename}.ti3is written only at Finish (copy of pass1, oraverage -v pass1 pass2 … basename.ti3). Do not change thechartreadinoutfile (still must match.ti2basename). - Invariant: Measurement identity ≠ averaging identity.
chartreadalways uses the chart basename. Pass files are snapshots. Stage 4 never sees a pass file unless Finish has run.
#110 Stage 4 unlocks on first pass, before Average
- Root cause:
process:exitof pass 1 calledwizardState.setStage3Result.verify_stage_artefactssaw{basename}.ti3and enabled Stage 4 while "Measure Another Sheet" was still offered. - Fix: Same PR as
#109.setStage3Resultonly from Finish and Average / Accept-single-pass. Prefer the canonical-file rule over a newstage3_acceptedflag. - Invariant: Wizard gates on disk artefacts with defined meaning.
{basename}.ti3means "accepted measurement", not "a pass happened".
#111 instlist ordinal passed as chartread -c
- Root cause: Dropdown
value = inst.index(1-based instrument list).build_chartread_argsemitted-c {index}. Argyll-cis a communication port list (chartread -??), a different enumeration. Works by coincidence for a single USB i1Pro on port 1. - Fix: Do not assume ordinal equality. Use
instlistas presence/health; pass-conly when the value is a documented comm-port. Tighten regex to require instrument tokens. - Invariant:
instlistindex ≠-cport. After JSONinstlist(argyll#6/ ICCery#134), the field to pass isdevice.port, never the array index, and only if it is a real comm port.
#137 (feature) Accept/Override + multi-sheet prompts
- Chartread emits multi-key warnings. UI must send the exact key (
n/y/…) the prompt asks for, not just Enter.
#175 Stage 3 cannot complete — never sends d
- Root cause: After the last strip,
chartreadwaits ford(Done) before writing.ti3. ICCery never sent it. Follow-on:snapshot_ti3IPC usedpassIndexvs the deserializer's expected key. - Fix (v0.6.7–0.6.8): Explicit Done & Save .ti3 →
d\n; Undo Strip →u\n; completion state machine;invoke("snapshot_ti3", { passIndex })camelCase to match Tauri v2 serde rename. - Invariant:
chartreadis not finished when the last row JSON arrives. Done is a keystroke. Tauri v2 IPC argument names are camelCase on the JS side. Always add a completion control rather than inferring EOF.
#93 XY tables (SpectroScan / i1iO) — feature, but its invariants are bug-shaped: classify multi-line prompts; fiducial alignment is a 4-step checklist; cancel must park the head.
#204 i1Pro2 LEDs — pass -Y l (not -L). Silent no-op on instruments without LEDs.
2.6 Stage 4 — colprof
#56 Stage 4 is broken (IPC schema / process-id mismatch)
- Root cause:
colprof.jssent{ basename, cwd }. Rust expected{ ti3_path, icc_path }. Process IDs disagreed (colprof_${basename}vscolprof_{icc_path}), so UI listeners never fired even on a successful spawn. - Fix (PR
#64, v0.1.11): Align struct, argv builder, and process id with the frontend. - Invariant: Spawn id is a protocol. Frontend listen-filters and backend spawn ids are the same string. Serde structs are tested against the JS payload, not against a parallel imagined schema.
#69 Profile extension .icm (Windows) vs .icc (Unix)
- Root cause: Hardcoded
${basename}.icc. Argyllcolprofwrites.icmon Windows.profcheck/iccgamutthen "cannot find profile". - Fix: Platform-aware resolver, or existence-check both extensions. Used by Stage 5 and gamut (
#57). - Invariant: Never hardcode
.icc. Resolve{icc, icm}on disk. This also applies to install (#223) and preconditioning (#172).
#176 OBA/FWA + viewing conditions — feature. colprof -c / -d here are illuminant / viewing condition, not directories and not printtarg -d labels.
#210 Custom .sp Browse: window.__TAURI__.dialog TypeError
- Root cause: Identical to
#103. Noselect_spectrum_filecommand. - Fix (v0.8.3): Native
select_spectrum_file(filter.sp). - Invariant: Same as
#103. Third time this class of bug shipped (#103,#210,#211). A rewrite should have a singlepick_file { filters }command and never touchwindow.__TAURI__.dialog.
2.7 Stage 5 — gamut / profcheck / history
#57 3D gamut is a dead panel
- Root cause:
extract_gamut/loadGamutMeshexisted but nothing called them aftercolprof.initGamutViewernever stored meshes. Blocked by#56and#69. - Fix (v0.1.14): Run
iccgamutafter successful profile; load printer.gam+ bundledsRGB.gam; dispose previous geometries;ResizeObserveron the hidden Stage 5 canvas. - Invariant: Gamut is a post-colprof artefact, not a demo mesh. Viewer must not allocate WebGL until the stage is shown (
#225).
#112 iccgamut -d 50.0 is surface density, not a directory
- Root cause:
extract_gamutpassed-d 50.0. In Argyll,iccgamut -d sis surface point density (useful ~1–10). Density 50 yields thousands of vertices and freezes the O(n²) hull filter. Cwd is already set on the spawn. - Fix (PR
#121):-d 10; extractbuild_iccgamut_args; unit-test argv. Do not pass-w(VRML) unless required. - Invariant:
-dis not "directory" oniccgamut. Cwd is a ProcessManager concern. Density default is 10. Argv builders exist so this cannot silently regress.
#179 Mesh empty; profcheck cards show 0.00
- Root cause:
.gamhas two CGATS tables (verticesVERTEX_NO LAB_L LAB_A LAB_B, then facesVERTEX_0 VERTEX_1 VERTEX_2). Parser kept only the firstBEGIN_DATAand produced{L,a,b}. QuickHull indexedp.x/p.y/p.z→ all undefined → empty hull. Profcheck regex expected one wording oferrors(CIEDE2000): max. =; real output variants yielded 0. - Fix (v0.7.0): Dual-table parse; map
x=a*, y=L*, z=b*; use file faces when present; broader max/avg/rms regexes + user-visible parse warnings. - Invariant:
.gamis two tables. Lab is not XYZ-style{x,y,z}until remapped. Profcheck text is not a stable API — preferprofcheck -uJSON (argyll#5) and keep regex as fallback only.
#117 Inlined QuickHull — modularize under src/js/vendor/quickhull.js (and later prefer file faces over recomputing hulls).
#185 Gamut rework — CIELAB axes + CSS2D labels, EdgesGeometry sRGB reference, per-vertex labToSrgb(), layer toggles. Invariant: reference overlay is edges, not a competing solid that hides the profile.
#212 gamut_viewer.test.js : window is not defined
- Root cause: Module top-level
const { invoke } = window.__TAURI__.core. Node test import evaluates that before any polyfill. Unlikeprofcheck.test.js/chartread.test.js, noglobalThis.windowmock and no CLI auto-run. - Fix: Guard
typeof window !== 'undefined'; polyfill in the test;node src/js/gamut_viewer.test.jsmust exit non-zero on failure. Later#215runs this in CI. - Invariant: Frontend modules that
importin Node tests cannot touchwindowat load time. Tests are first-class; if AGENTS.md documentsnode src/js/….test.js, CI must run it (#215).
#213 verification_history.json non-atomic write
- Root cause:
fs::writein place. Crash ⇒ truncated JSON.load_history_filetreats parse failure as empty vec, so the next save wipes the archive. Contradicted AGENTS.md's own atomic-write rule. - Fix (v0.8.4): Write
.tmp, flush/sync,rename. - Invariant: History files are append-only user data. Write temp+rename. Parse failure must not be indistinguishable from "no history".
#95 Drift analytics — 1,000-record cap, SVG dual-series, consecutive-breach card, RFC-4180 CSV. Relies on #213.
2.8 macOS window / WebKit
#61 Default window 800×600
- Fix: 1280×800 default, min 1100×700.
- Invariant: The wizard + TIFF cards + WebGL viewer are not a mobile layout. Set min bounds in
tauri.conf.json.
#148 Align minimumSystemVersion with Argyll
- Argyll macOS builds and Tauri 2 / WebGL do not support 10.15 in practice.
#165 Unsigned Argyll sidecars SIGKILL on Apple Silicon
- Root cause: arm64 requires at least ad-hoc signature.
fetch-argyll.mjsstaged Jam-built unsigned Mach-Os into Resources. Kernel kills withKilled: 9, no useful error. - Fix: Upstream argyll
#32(v3.5.0-ICCery.1.5)codesign -f -s -inmakepackagebin.sh+ CIcodesign -dvv, includinglipofat binaries. - Invariant: Every shipped Mach-O sidecar is signed (ad-hoc at minimum). Fetch pipeline must verify signatures, not assume "it ran on Intel".
#164 Universal binary (Intel + arm64)
- Need universal Argyll archive (argyll
#27), runtime fallback if a slice is missing, CI--no-runwhen cross-compiling tests for the other arch.
#225 Monterey white-flash loop then silent exit (v0.8.4)
- Root cause: Main window
visible: truewith no dark backing.initGamutViewer()createdTHREE.WebGLRenderer({ antialias: true, alpha: true })and started rAF atDOMContentLoadedwhile Stage 5 was hidden. On macOS 12 (esp. Intel GPU) the Web Content/GPU helper died; WKWebView respawned its white default store; after a few cycles the host tore the window down. No Crash Reporter onICCery.app.minimumSystemVersionstill claimed 10.15. - Fix: Defer WebGL until the gamut viewer is shown; survive context-lost; hide window until first paint; dark WKWebView
backgroundColor; log unexpected window destroy / Web Content death; raiseminimumSystemVersionto 12.0. - Invariant: Do not create a WebGL context at startup. Do not show a window before first paint on macOS. Helper-process death ≠ app crash — log it. Support matrix must match WKWebView/WebGL reality.
2.9 Packaging / CI
#38 Unused / duplicated crates — audit deps; don't let Windows-only crates leak (inverse of #70).
#40 Linux CI too slow
- Fix (PR
#41): Allow strip (NO_STRIPoff), rust-cacheworkspaces: "src-tauri -> target", apt archive cache,--bundles deb,appimage(skip rpm), upload-artifact v4.
#42 Windows MSI/NSIS branding.
#52 Strip foreign-platform Argyll binaries from each OS bundle.
- Invariant: A Windows installer must not ship
linux-x86_64/andmacOS/trees. Fetch-at-build (#127) makes this a packaging filter, not a git-lfs problem.
#80 Mermaid diagram does not render
- Root cause: Node label
Raw Print Subsystem (GDI / CUPS)— parentheses parsed as shape syntax. - Fix: Quote / drop parens.
- Invariant: Mermaid node text with
()must be quoted.
#91 Universal .dmg + codesign pipeline (still evolving through #189).
#108 .deb requires GLIBC_2.39 (won't run on Ubuntu 22.04)
- Root cause: Linux CI runner newer than the support target. Binary linked against glibc 2.39; 22.04 ships 2.35.
- Fix (v0.3.2): Build on Ubuntu 22.04 LTS.
- Invariant: The CI image is the minimum glibc. If you support 22.04, you build on 22.04 (or older), not
ubuntu-latest.
#127 Fetch Argyll at build time + Windows USB driver offer
- Removed vendored binaries. NSIS hook for
ArgyllCMS_install_USB.exe. Follow-on Windows NSIS bugs in the same train: LogicLib admin check, domain-user install path must be a local fixed disk, shell-folder drive mapping, skip WebView2 bootstrappermsiexecinvalid-drive. - Invariant: Sidecars are release artefacts of
gronod/argyllcms, not git blobs. Windows USB instruments need the libusb0 INF. Packaging scripts musttr -d '\r'manifest lists (argyll#21— CRLF inbinfilesdropped drivers/docs from the zip).
#189 DMG has no background image
- AppleScript Finder decoration does not run headlessly. Multiple CI attempts (
#196,#208,#219) then dmgbuild (#222, commit6976ced). - Invariant: Do not use Finder AppleScript to style a DMG on a headless runner. Use
dmgbuild.
#215 Frontend tests not in CI
- Added
npm test(profcheck, chartread, gamut_viewer) to macOS/Linux/Windows workflows. Prevents#212-class breakage.
#214 Docs drift — AGENTS/README/licenses vs code. Keep ROADMAP version = Cargo version (#115, #152).
2.10 Security (innerHTML presets)
#114 Preset manager XSS via innerHTML
- Root cause:
renderManagePresetsListinterpolatedp.name/p.descriptionintoinnerHTML. Imported JSON is attacker-controlled. Tauri WebView is a privileged surface (<img src=x onerror=…>runs with backend IPC). - Fix (PR
#122, with#113):createElement+textContent. Optional Rust reject of</ control chars. No HTML sanitizer. - Invariant: Untrusted strings (preset import, CGATS descriptors, chart labels, log excerpts) never go through
innerHTML.textContentonly.
2.11 Settings / presets / wizard chrome
#113 applyPreset never applies TIFF DPI
- Root cause: Schema had
dpi; Fast RGB Draft is 150.applyPresetskipped#tiffDpi;collectCurrentSettingsAsPresethardcodeddpi: 300. - Fix: Round-trip
#tiffDpi(72–600); show DPI in manage-presets subtitle. - Invariant: Every
ProfilingPresetfield that exists on disk has a DOM binding in both directions. Adding a Stage 1/2/4 control without a preset field (or vice versa) is a bug. (v0.7.0 also broke the build by adding fields without updating the struct —c917f05.)
#60 Wizard is not a wizard
- Root cause: Stepper allowed clicking Stage 5 with no files. Modules fell back to
"test_target". - Fix (v0.1.15 / v0.2.0): Gate
.ti1→.ti2+.tif→.ti3→.icc/.icm. Remove every|| "test_target". Completed stages remain clickable backwards. - Invariant: No placeholder basenames. Navigation = disk.
#151re-validates artefacts on stage entry and app start (deleted files must re-lock).
#171 Tooltips reflow the layout
- Root cause: Tooltip content inserted in-flow.
- Fix (v0.6.2): Absolute overlay on hover/focus; in-flow hints only when the global "show tooltip hints" toggle is on.
- Invariant: Hover must not change document flow.
#184 Configurable ΔE₀₀ thresholds — settings persistence applied to the Stage 3 traffic lights. Same "settings must actually apply" rule as #158.
2.12 UI leftovers that still encode invariants
#178 Swatch grid: honour is_pad (padding patches, id == "0"); render white patches; diagonal split intended vs measured; printtarg row/column order is canonical after bi-directional reverse. Device channels are 0–100%; XYZ in JSON is 0–100 and must be /100 before icmXYZ2Lab.
#177 Buttons: .btn-sm/.btn-md/.btn-lg/.btn-icon-sq only. No inline sizes. #223 install button must use this hierarchy.
#140 Resume: opening a .ti2 jumps to Stage 3; .ti1 to Stage 2. Parse header (instrument, patch count, colorants, pages). Do not regenerate #163-style random layouts over a chart that was already printed.
3. Open issues — implemented on main (v0.8.5)
Gitea still shows these as open. Git on main (merge e12ef41, tag v0.8.5) already contains the work. ROADMAP "Printer Calibration Release (v0.8.5)" matches.
| Ticket | State in tracker | Evidence on main |
What shipped |
|---|---|---|---|
#223 System-wide ICC/ICM install |
open | 980b44b feat(stage5): install generated ICC/ICM profiles into the OS (#223) ; PR #228 → development; PR #229 → main |
Stage 5 "Install Profile to System": copy into OS colour store (user or system), overwrite/rename/cancel, elevation guidance, note when printcal curves were applied |
#224 printcal / applycal |
open | 24318bc feat(cal): printer linearization via printcal / applycal (#224) ; PR #227 → development; PR #229 → main |
Optional Stage 0: CAL_ artefacts, Apply Calibration toggle → printtarg -K + applycal, channel-response plots, stale-cal warnings, project/library persistence |
Rewrite must treat Stage 0 calibration and Stage 5 install as shipped contracts, not backlog. Close the tickets when convenient.
4. Won't-fix
#90 Display wizard (dispwin / dispread) — Reviewed/Won't Fix
Asked for a second wizard mode: emissive monitor profiling (white point, gamma, cd/m², colorimeter loop, dispwin -I install).
Why not:
- ICCery's architecture, print engine, artefact chain (
.ti1→.tif→.ti3→.icc), and raw-print colour-bypass work are reflective printer problems. Display calibration is a different product (full-screen test patches, VCGT, per-monitor ColorSync/ICC install). dispwin/dispreadalready are interactive GUIs; wrapping them duplicates Argyll's own display tools and DisplayCAL without sharing the print pipeline.- Hardware, prompts, and OS install APIs do not reuse Stage 3's strip-reader state machine.
- Priority/Low, closed the same day it was filed during M8 triage. Printer-only scope is the product.
Do not quietly add a "Print vs Display" toggle in a rewrite. If display profiling happens, it is a separate app.
#96 i18n (en/de/fr/ja) — Reviewed/Won't Fix
ROADMAP: Closed — Won't Fix (English UI retained as standard color-management terminology).
Why not:
- The domain language is English: ΔE₀₀, cLUT, CGATS, Lab, FWA, OFPS,
targen -f. Translating chrome without translating Argyll's English prompts (Place the instrument on its reflective white reference…) produces a mixed UI. Translating prompt matchers (#137,#93,#175) is how Stage 3 silently deadlocks. - Argyll itself is English-only;
ARGYLL_NOT_INTERACTIVEkeystroke protocol is letter keys (d,u,n), not localized buttons. - Cost of four complete dictionaries + locale-aware regex across every prompt classifier exceeded the value for a specialist tool.
Keep the UI English. Do not introduce t() until prompt classification is table-driven and Argyll grows locale support — which it will not.
5. Idiosyncrasies (do not rediscover)
These are the sharp edges issues burned in. A rewrite that "simplifies" any of them will re-open the ticket.
5.1 Argyll CLI is a minefield of one-letter flags
| Tool | Flag | Means | Ticket |
|---|---|---|---|
targen |
-f N |
patch count; omit ⇒ 836 RGB | #44 |
targen |
-c file |
preconditioning profile (.icc/.icm/.mpp) |
#172 |
targen |
-u |
JSON progress | argyll #2 |
printtarg |
-d "str" |
custom label on the chart | #119 / argyll #19 |
printtarg |
-D |
8-bit TIFF dither (different letter) | argyll #19 |
printtarg |
-R N |
PRNG seed; omit ⇒ non-deterministic layout | #163 |
printtarg |
-r |
raster order, no randomize | #163 |
printtarg |
-K |
apply calibration (Stage 0) | #224 |
printtarg |
-u |
JSON page manifest | #68 / argyll #3 |
chartread |
-u |
ROW_COLORS_JSON: stream |
#5 / argyll #1 |
chartread |
-c N |
comm port, not instlist index | #111 |
chartread |
-d |
display read mode (not a directory, not a label) | argyll #37 audit |
chartread |
-Y l |
i1Pro2 LED (not -L) |
#204 |
chartread |
stdin d |
Done, write .ti3 |
#175 |
chartread |
stdin u |
undo last strip | #175 |
iccgamut |
-d s |
surface density (use 10, not 50, not a path) | #112 |
colprof |
-d |
viewing condition | #176 |
colprof |
-c |
illuminant / FWA | #176 |
colprof |
-u |
JSON progress | argyll #4 |
profcheck |
-u |
JSON ΔE report | argyll #5 |
instlist |
stdout JSON | {event:"instruments", devices:[{port,name,type}]} |
#134 / argyll #6 |
Same letter, four meanings of -d: printtarg label, iccgamut density, chartread display mode, colprof viewing condition. Never name a helper dash_d without the tool.
5.2 Process / Windows / stdin
- Argyll binaries are console subsystem →
CREATE_NO_WINDOW(0x08000000) (#46). ARGYLL_NOT_INTERACTIVE=1is mandatory or Windows ignores pipes (#134).- Even then, Argyll's
SetNamedPipeHandleState(PIPE_NOWAIT)fails on anonymous pipes; upstreamPeekNamedPipe(argyll#24) is required or the instrument trigger never fires. Child::wait()drops stdin (#84). Take streams first; store stdin separately; kill via oneshot.- Spawn ids are exclusive (
#116). - App exit must
kill_all(#147,#149) or the spectro stays locked. - Windows resolve:
targenandtargen.exe(#85). tokio::process+ GDI print FFI: don't trustwindowscrate completeness (#33).
5.3 Files, extensions, artefacts
colprofwrites.icmon Windows,.iccon Unix (#69). Always probe both.- Wizard chain:
.ti1→.ti2+.tif→.ti3→.icc/.icm(+.gam) (#60,#151). {basename}.ti3is the accepted measurement; passes live in{basename}_passN.ti3(#109,#110).- Calibration artefacts use
CAL_prefix and.cal(#224). - TIFF is not a web image (
#58). Convert in Rust. .gam= vertex table plus face table (#179).- History JSON: atomic rename or you delete the user's drift archive (
#213). - Empty cwd is forbidden (
#59).
5.4 Frontend / Tauri v2
window.__TAURI__.dialogdoes not exist. Three tickets (#103,#210,#211). File dialogs are Rust commands with dedicated filters.- Tauri v2 invoke args are camelCase (
passIndex, notpass_index) (#175). - JS payload field names must match Rust (
patch_countvstotal_patches—#44;basename/cwdvsti3_path/icc_path—#56). - Spawn id strings are part of the UI protocol (
colprof_${basename}). - Do not
innerHTMLuntrusted preset/CGATS text (#114). - Do not read
windowat module top-level if Node tests import the file (#212). - Do not create WebGL on
DOMContentLoaded(#225). - Tooltips are overlays (
#171).
5.5 Colour / print pipeline
- Raw print = unmanaged raster. Bypass OS + driver + ICM (
#25–#28,#36,#188). - Windows: full DEVMODE blob including
dmDriverExtra(#36);SetICMMode(ICM_OFF)(#33). - macOS:
AP_ApplicationColorMatching+ vendor keys (CNIJIntent2, EpsonColorCorrection); never crash-call 3-argPMSessionSetColorMatchingMode(#188). - PPD: send choice codes, display titles; vendor keywords differ (
InputSlot/EPIJ_FdSo/CNIJMediaSupply;MediaType/CNIJMediaType) — CPU#1/#2. - Hide CUPS widgets on Windows (
#48). - Default printtarg seed
-R 1(#163). - Device JSON is 0–100%; XYZ 0–100 must be divided by 100 before Lab (
ROW_COLORS_JSONcontract, argyll#1). - Padding patches:
is_pad/id == "0"(#178).
5.6 Packaging
- Linux: build on the oldest glibc you support (
#108= Ubuntu 22.04). - Strip foreign Argyll trees (
#52). - Fetch sidecars from
gronod/argyllcmsreleases (#127); verify macOS ad-hoc signatures (#165). - Manifest copy loops: strip CR (
\r) or Windows zips silently omit USB drivers (argyll#21). - Headless DMG:
dmgbuild, not Finder AppleScript (#189). minimumSystemVersionis 12.0 after Monterey WebKit reality (#225), not 10.15.
5.7 Defaults people will hardcode wrong
- Patch count default if
-fmissing: 836, not 0, not 1000 (#44). - TIFF DPI built-in Standard = 300; Fast RGB Draft preset = 150 (
#113). iccgamut -d= 10 (#112).- Window 1280×800, min 1100×700 (
#61). - Log level is a setting, not
cfg!(debug_assertions)(#158). - LED flag is
-Y l(#204).
6. Suggested rewrite test locks (from the tickets)
If the rewrite has tests, these are the ones the history says you will regret omitting:
build_targen_argsincludes-ffrompatch_count(#44).build_printtarg_argsincludes-R 1(or explicit seed) (#163).build_iccgamut_argsis-v -d 10 {profile}— never a path, never 50 (#112).build_chartread_argsdoes not pass instlist ordinal as-c(#111).- Windows
resolve_binary("targen")findstargen.exe(#85). - ProcessManager: duplicate id errors; stdin works during wait; kill_all on drop (
#84,#116,#149). - Averaging: two snapshots,
averageargv unique files, Stage 4 locked until Finish (#109,#110). - Preset round-trip includes DPI; imported
<img onerror>is text (#113,#114). - Profile resolver accepts
.iccand.icm(#69). .gamdual-table parse; profcheck JSON then regex fallback (#179).- History write is temp+rename (
#213). - Node can
importgamut/profcheck/chartread modules (#212,#215).
7. PR title index (page 2, remaining)
Page 2 of pulls?state=all&limit=50&page=2 (older half of the genealogy):
#144 Stage 1 advanced targen (#141) · #143 resume .ti2 (#140) · #142 logging (#139) · #138 Accept/Override (#137) · #136/#135 v0.3.5 ARGYLL_NOT_INTERACTIVE (#134) · #133 v0.3.4 · #132 printtarg -d (#119) · #131 v0.4.0 · #130 macOS CUPS (#92) · #129/#128 fetch-argyll (#127) · #126 v0.3.3 · #125 QuickHull module (#117) · #124 docs v0.3.2 (#115) · #123 duplicate spawn (#116) · #122 preset DPI+XSS (#113,#114) · #121 iccgamut density (#112) · #120 comm port (#111) · #118 averaging unique ti3 (#109,#110) · #107 release asset naming · #105/#104 Stage 1 browse (#103) · #102 v0.3.0 · #101 3D hull (#89) · #100 presets (#88) · #99 averaging (#87) · #98 instlist (#86) · #97 .exe (#85) · #83 process deadlock v0.2.1 (#84) · #81 mermaid (#80) · #79 print IPC consolidate (#67,#68) · #77 wait/reap (#63) · #75 wizard gating (#60) · #74 gamut+extension (#57,#69) · #73 TIFF PNG + window (#58,#61) · #72 default cwd (#59) · #64 colprof IPC (#56) · #55 collapsible logs (#54) · #53 strip foreign binaries (#52) · #51 Stage 2 cleanup (#50) · #49 hide CUPS on Windows (#48) · #47 CREATE_NO_WINDOW (#46) · #45 targen -f (#44) · #43 installer art (#42) · #41 Linux CI (#40)
Recent page-1 merges of note: #229 v0.8.5 (#223+#224) · #228 install · #227 printcal · #226 Monterey (#225) · #222 dmgbuild (#189).