M1 — Foundation & process core #40

Merged
gronod merged 15 commits from milestone/m1-foundation into develop 2026-09-08 19:32:36 +01:00
Owner

M1 — Foundation & process core

Native macOS rewrite foundation. Argyll is orchestrated as subprocesses; no libraries are linked.

Delivered (one PR per issue)

  • #1 (PR #33) — XcodeGen scaffold + wizard shell: SwiftUI/AppKit, com.gronod.iccery2, macOS 14, universal arm64+x86_64, dark theme, 1280×800 / min 1100×700, 270 pt sidebar with original cone logo, 5-step stepper (Stage 0 out-of-band), notice banner, settings/about slots, sandbox off + hardened runtime + USB entitlement.
  • #2 (PR #34)actor ProcessManager: runStreaming/runCaptured/sendStdin/kill/killAll, multicast events() bus, duplicate-ID rejection, ARGYLL_NOT_INTERACTIVE=1, ROW_COLORS_JSON: prefix → jsonRow, exit gated on both pipes' EOF, concurrent captured drain, sanitised spawn logging.
  • #3 (PRs #35, #36)scripts/fetch-argyll.sh (POSIX sh+curl, ARGYLL_SERVER_URL/ARGYLL_REPO/ARGYLL_RELEASE_TAG/GITEA_TOKEN; ad-hoc sign + codesign -dvv verify; quarantine strip). Verified live: 51 universal tools from v3.5.0-ICCery1.8. BinaryResolver (override → macos-universal-if-instlist → arch dir; missing → constructed path for process:error). Vendored mocks + real 448v/892f sRGB.gam. killAll via terminateLater.
  • #6 (PRs #36, #37) — dedicated pickers per purpose (no shared picker), PathSecurity, AtomicFileWriter, ArtefactProbe, Ti2Header, TiffPreview (TIFF→PNG ≤1200 px), ArtefactFiles.
  • #5 (PR #38)AppSettings/SettingsStore at ~/Library/Application Support/com.gronod.iccery2/settings.json, contract validation strings, corrupt-JSON → defaults, settingsDidChange notification; LogSink rolling 5 MiB×5 with live setLevel; full SettingsView sheet.
  • #4 (PR #39)WizardGating (disk-truth gating: 2←ti1, 3←ti1+ti2, 4←ti3 only, 5←ti3+profile; forward gated/backward free), persisted WizardState, focus revalidation (#151), basename sanitisation (#60), cwd fallback (#59).

Verification

  • xcodebuild build test -scheme ICCery -destination 'platform=macOS' ARCHS='x86_64 arm64' ONLY_ACTIVE_ARCH=NO71/71 tests pass, universal binary confirmed via lipo.
  • Bundle contains Argyll/macos-universal/ (51 signed tools), Argyll/mocks/, Argyll/reference_gamuts/sRGB.gam.
  • No Argyll binaries committed (Vendor/Argyll/ gitignored).

Closes milestone M1 (issues #1–#6).

## M1 — Foundation & process core Native macOS rewrite foundation. Argyll is orchestrated as subprocesses; no libraries are linked. ### Delivered (one PR per issue) - **#1 (PR #33)** — XcodeGen scaffold + wizard shell: SwiftUI/AppKit, `com.gronod.iccery2`, macOS 14, universal `arm64`+`x86_64`, dark theme, 1280×800 / min 1100×700, 270 pt sidebar with original cone logo, 5-step stepper (Stage 0 out-of-band), notice banner, settings/about slots, sandbox off + hardened runtime + USB entitlement. - **#2 (PR #34)** — `actor ProcessManager`: `runStreaming`/`runCaptured`/`sendStdin`/`kill`/`killAll`, multicast `events()` bus, duplicate-ID rejection, `ARGYLL_NOT_INTERACTIVE=1`, `ROW_COLORS_JSON:` prefix → jsonRow, exit gated on both pipes' EOF, concurrent captured drain, sanitised spawn logging. - **#3 (PRs #35, #36)** — `scripts/fetch-argyll.sh` (POSIX sh+curl, `ARGYLL_SERVER_URL`/`ARGYLL_REPO`/`ARGYLL_RELEASE_TAG`/`GITEA_TOKEN`; ad-hoc sign + `codesign -dvv` verify; quarantine strip). Verified live: 51 universal tools from `v3.5.0-ICCery1.8`. `BinaryResolver` (override → `macos-universal`-if-instlist → arch dir; missing → constructed path for `process:error`). Vendored mocks + **real** 448v/892f `sRGB.gam`. `killAll` via `terminateLater`. - **#6 (PRs #36, #37)** — dedicated pickers per purpose (no shared picker), `PathSecurity`, `AtomicFileWriter`, `ArtefactProbe`, `Ti2Header`, `TiffPreview` (TIFF→PNG ≤1200 px), `ArtefactFiles`. - **#5 (PR #38)** — `AppSettings`/`SettingsStore` at `~/Library/Application Support/com.gronod.iccery2/settings.json`, contract validation strings, corrupt-JSON → defaults, `settingsDidChange` notification; `LogSink` rolling 5 MiB×5 with live `setLevel`; full `SettingsView` sheet. - **#4 (PR #39)** — `WizardGating` (disk-truth gating: 2←ti1, 3←ti1+ti2, 4←ti3 only, 5←ti3+profile; forward gated/backward free), persisted `WizardState`, focus revalidation (#151), basename sanitisation (#60), cwd fallback (#59). ### Verification - `xcodebuild build test -scheme ICCery -destination 'platform=macOS' ARCHS='x86_64 arm64' ONLY_ACTIVE_ARCH=NO` — **71/71 tests pass**, universal binary confirmed via `lipo`. - Bundle contains `Argyll/macos-universal/` (51 signed tools), `Argyll/mocks/`, `Argyll/reference_gamuts/sRGB.gam`. - No Argyll binaries committed (`Vendor/Argyll/` gitignored). Closes milestone M1 (issues #1–#6).
gronod added the Feature/Backend label 2026-09-08 19:32:27 +01:00
gronod added 15 commits 2026-09-08 19:32:28 +01:00
XcodeGen-managed project (project.yml), SwiftUI single-window app
1280x800 / min 1100x700, dark theme tokens from docs/21, 270pt sidebar
with logo/preset select/Calibrate/stepper 1-5, notice banner, five
stage placeholders, ICCeryCore SPM package with AppPaths + WizardStage,
Swift Testing plumbing. Sandbox off, hardened runtime on, bundle id
com.gronod.iccery2.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- actor ProcessManager: runStreaming + runCaptured (concurrent pipe
  drain — no 64 KiB deadlock), sendStdin with independent stdin map
  (#84), kill/killAll, duplicate-id rejection (#116)
- Multicast AsyncStream<ProcessEvent> bus: stdout/stderr/exit/error/
  jsonRow (ROW_COLORS_JSON: prefix stripped)
- exit emitted exactly once, gated on both pipes reaching EOF so
  buffered output is never lost on fast exits or kills
- ARGYLL_NOT_INTERACTIVE=1 on every child; argv logged with ~ home
  sanitization; subprocess stdout→info, stderr→warn
- ProcessLineDecoder (byte-split at \n, UTF-8 safe, CRLF, unterminated
  tail flush), JSONAccumulator (multiline JSON for instlist/profcheck/
  manifest), ProcessID conventions
- 19 new tests incl. large-output captured run and stdin round-trip
- Fixup: entitlements gain com.apple.security.device.usb; sidebar
  preset/Calibrate disabled and only Stage 1 enabled per #1 AC

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- scripts/fetch-argyll.sh: POSIX sh port of fetch-argyll.mjs; queries
  the Gitea release API for the *_macOS_universal_bin.tgz asset,
  extracts Argyll_V*/bin, chmod+x, strips quarantine xattr; honors
  ARGYLL_SERVER_URL / ARGYLL_REPO / ARGYLL_RELEASE_TAG / GITEA_TOKEN.
  Verified end-to-end: 51 universal tools from v3.5.0-ICCery1.8.
- BinaryResolver: settings argyll_binary_dir override (existence-gated)
  → bundled Argyll/<platform>/, macos-universal preferred when instlist
  marker present, else macos-arm64/macos-x86_64; constructed path
  returned even when absent (spawn surfaces process:error). Mock and
  reference-gamut helpers.
- Vendored tracked resources: mocks/{chartread,colprof,profcheck}.mock
  + reference_gamuts/sRGB.gam from ICCery v1, copied as a folder
  reference so the Argyll/ subtree structure survives into the bundle.
- Build phase rsyncs Vendor/Argyll/ → Contents/Resources/Argyll/.
- AppDelegate: killAll via terminateLater so children are signaled
  before teardown (#147/#149).
- 6 resolver tests + fetch script smoke-verified against real release.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- codesign -f -s - every fetched Mach-O, then codesign -dvv verify —
  an unsigned sidecar now fails the script (#165)
- Replace the 8-cusp reference_gamuts stub with the real v0.8.5
  src/assets/sRGB.gam (448 verts / 892 faces) used by the Stage 5
  gamut overlay (#185)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- PathSecurity: basename sanitisation (reject / \ .., empty); resolveSafeCwd explicit - Documents - Home - app-data (#59, #60)
- AtomicFileWriter: .tmp + rename/replaceItemAt, parent dirs (#213)
- ArtefactProbe: verify_stage_artefacts, .icm-over-.icc resolution (#69), enumeration incl. .N.tif/_NN.tif/_passN.ti3/CAL_
- FileDialogService: NSOpenPanel/NSSavePanel wrappers (v2 select_* equivalents)
- 11 new tests; 40/40 total green

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- One dedicated method per purpose: selectTargetFile (save .ti1),
  selectExistingTarget (ti1/ti2), selectProfileFile (icc/icm/mpp —
  never ti*, #172), selectSpectrumFile (.sp), selectDatasetFile
  (open-only ti3/txt/cgats/csv, #211), selectCsvSavePath, selectCalFile,
  selectDirectory. No shared generic picker API (#103/#210/#211).
- Ti2Header: TARGET_INSTRUMENT / NUMBER_OF_SETS / NUMBER_OF_PAGES +
  sibling .ti1 detection; NUMBER_OF_FIELDS explicitly not patch count
- TiffPreview: host-side TIFF→PNG thumbnail, 1200px max edge (#58)
- ArtefactFiles: defaultWorkingDirectory, readBase64, appInfo
- 7 new tests; 47/47 green

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- AppSettings: snake_case Codable model — argyll_binary_dir,
  default_instrument (stored, never applied to argv), log_level (nil ->
  Debug debug / Info release), delta_e thresholds (2.0/5.0),
  custom_presets, enable_i1pro2_leds, calibration_stale_days 30,
  default_install_location user, ask_before_overwrite_profile,
  open_color_panel_after_install
- Validation with the exact contract strings; save() refuses invalid
  settings; corrupt/missing JSON -> defaults; settingsDidChange
  notification posted on save (for #20)
- LogSink: rolling file at ~/Library/Logs/com.gronod.iccery2/
  iccery.log, 5 MiB x 5 segments, runtime setLevel applied at startup
  and on save (#158); AppLogger gates os_log+file through it
- SettingsView sheet: Argyll dir picker, instrument (display-only
  caveat), i1Pro2 LEDs, ΔE fields + inline errors, stale days, install
  location, overwrite + ColorSync toggles, log level, open-log-folder /
  copy-path / copy-excerpt
- v1 settings path never read; writes atomic via AtomicFileWriter
- 13 new tests; 60/60 green

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- WizardState: Codable persisted fields — currentStage, basename, cwd,
  printerName, sessionMode (profile|calibration), profileBasename;
  WizardStateStore writes wizard_state.json atomically
- WizardGating: pure gating — 1 always; 2 on .ti1; 3 on .ti1+.ti2; 4 on
  .ti3 only (never .ti2 — #109/#110); 5 on .ti3+profile (#69). Forward
  gated, backward always; Stage 0 a side-trip
- WizardViewModel: setTarget sanitises basename (#60) and resolves cwd
  (#59); locked forward nav shows a warning banner; windowDidBecomeKey
  re-probes and drops back to deepest unlocked when files vanish (#151)
- RootView: NSWindow.didBecomeKeyNotification -> revalidate
- Sidebar stepper enabled state now driven by artefact gating
- 11 new tests incl. gating matrix + persistence; 71/71 green

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
gronod merged commit c0f5fb8c28 into develop 2026-09-08 19:32:36 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#40