Single CalibrationIdentity and one .icc/.icm resolver #83

Closed
opened 2026-09-10 18:23:13 +01:00 by gronod · 1 comment
Owner

Summary

The CAL_ basename policy is implemented in three places and can disagree after Force Quit:

  • WizardViewModel launch restore / enterCalibration / restoreCalibration
  • CalibrationViewModel.calBasename + generate snapshot
  • ArgyllRunner.runCalibrationTargen / runPrintcal (hasPrefix("CAL_") ? x : "CAL_\(x)")

Profile extension swap (#69) is also duplicated:

  • ArtefactProbe.resolveProfile(basename:cwd:) (.icm wins over .icc)
  • ArgyllRunner.resolveProfileForVerification (flip extension if the given URL is missing)

Spec refs

  • docs/07-stage0-calibration.md, issue 29
  • docs/06-wizard-and-artefacts.md (#60 basename, #59 cwd)
  • Issue 24 / 25 profile path after colprof
  • Wizard persist field calibrationOriginalBasename

Scope

In

  1. Packages/ICCeryCore/Sources/ICCeryCore/Profile/CalibrationIdentity.swift (name flexible):
public struct CalibrationIdentity: Equatable, Sendable {
    public var originalBasename: String   // never CAL_
    public var calibrationBasename: String // always CAL_{original}
    public static func parse(liveBasename: String, persistedOriginal: String) -> CalibrationIdentity
    public static func prefix(_ original: String) -> String
}

Rules:

  • prefix is the only place that adds CAL_.
  • parse if liveBasename already has CAL_ and persistedOriginal is non-empty → use persisted original (Force Quit path, issue 29).
  • If live is CAL_* and persisted original is empty → strip the prefix (current CalibrationViewModel fallback dropFirst(4)).
  • If live has no prefix → original = live, cal = CAL_ + live.
  • Reject empty original; do not invent "target" (#60).

Wire:

  • WizardViewModel init restore uses CalibrationIdentity.parse.
  • CalibrationViewModel uses identity.calibrationBasename for .ti3 / .cal URLs and passes originalBasename into CalibrationTargenConfig.basename (runner may still prefix — after this ticket the runner must call CalibrationIdentity.prefix instead of its own ternary).
  • ArgyllRunner.runCalibrationTargen / runPrintcal call CalibrationIdentity.prefix.
  1. ArtefactProbe.resolveProfile(_ url: URL) -> URL (and keep basename+cwd wrapper):
  • If url exists, return it.
  • Else try flipped .icc.icm.
  • Basename+cwd API remains .icm wins if both exist (current resolveProfile(basename:cwd:)).
  • Delete ArgyllRunner.resolveProfileForVerification; runProfcheck uses the probe.

Out

  • Changing printcal argv, applycal tmp-replace, or Stage 0 UI layout.
  • Allowing navigation to Stage 4/5 while live basename is CAL_ (existing wizard guard stays).

Full solution

  • Persist only calibrationOriginalBasename + live basename as today; identity is derived, not a new JSON key.
  • runCalibrationTargen process id stays targen_{CAL_stem} so a profile targen and a cal targen cannot collide (#116).
  • Tests that write CAL_foo.ti1 next to foo.ti1 must keep working.

Rewrite invariants

  • #29 Force Quit mid-calibration restores original basename, sessionMode = .profile, stage .generate when that is what develop does today — do not “helpfully” stay on Stage 0 if the current init explicitly leaves it.
  • #69 .icm preferred when probing by basename; explicit URL that exists wins even if the other extension is also present.
  • Calibration chart must never embed its own curves (PrinttargArgs already skips -K/-I for CAL_ basenames).

Dependencies

Blocks-on: ArgyllRunner streaming-loop ticket (touch runCalibrationTargen / runPrintcal after that extract so the prefix call is one line in a thin wrapper).
Unblocks: none.

Test

  • New Tests/ICCeryCoreTests/CalibrationIdentityTests.swift:
    • live foo, persisted "" → original foo, cal CAL_foo
    • live CAL_foo, persisted foo → original foo
    • live CAL_foo, persisted "" → original foo
    • live CAL_foo, persisted bar → original bar (persisted wins; document this)
    • empty live basename does not invent a name
  • Update WizardCalibrationSessionTests.swift for launch restore using the identity helper.
  • Update ArgyllRunnerCalibrationTests.swift: passing basename foo still expects artefact CAL_foo.ti1 / process id targen_CAL_foo.
  • Update ArtefactFilesTests.swift / probe tests:
    • only .icc exists
    • only .icm exists
    • both exist → .icm for basename probe
    • concrete URL to missing .icc but sibling .icm exists → returns .icm
  • Milestone6CalibrationUITests.swift: generate wedge still advances to layout; compute curves still restores original basename.

Acceptance criteria

  • No hasPrefix("CAL_") ternary outside CalibrationIdentity.
  • No second profile-extension flip in ArgyllRunner.
  • Force Quit restore unit tests green.
  • Calibration UI tests green.
## Summary The `CAL_` basename policy is implemented in three places and can disagree after Force Quit: - `WizardViewModel` launch restore / `enterCalibration` / `restoreCalibration` - `CalibrationViewModel.calBasename` + generate snapshot - `ArgyllRunner.runCalibrationTargen` / `runPrintcal` (`hasPrefix("CAL_") ? x : "CAL_\(x)"`) Profile extension swap (#69) is also duplicated: - `ArtefactProbe.resolveProfile(basename:cwd:)` (`.icm` wins over `.icc`) - `ArgyllRunner.resolveProfileForVerification` (flip extension if the given URL is missing) ## Spec refs - `docs/07-stage0-calibration.md`, issue 29 - `docs/06-wizard-and-artefacts.md` (#60 basename, #59 cwd) - Issue 24 / 25 profile path after colprof - Wizard persist field `calibrationOriginalBasename` ## Scope **In** 1. `Packages/ICCeryCore/Sources/ICCeryCore/Profile/CalibrationIdentity.swift` (name flexible): ```swift public struct CalibrationIdentity: Equatable, Sendable { public var originalBasename: String // never CAL_ public var calibrationBasename: String // always CAL_{original} public static func parse(liveBasename: String, persistedOriginal: String) -> CalibrationIdentity public static func prefix(_ original: String) -> String } ``` Rules: - `prefix` is the only place that adds `CAL_`. - `parse` if `liveBasename` already has `CAL_` and `persistedOriginal` is non-empty → use persisted original (Force Quit path, issue 29). - If live is `CAL_*` and persisted original is empty → strip the prefix (current CalibrationViewModel fallback `dropFirst(4)`). - If live has no prefix → original = live, cal = `CAL_` + live. - Reject empty original; do not invent `"target"` (#60). Wire: - `WizardViewModel` init restore uses `CalibrationIdentity.parse`. - `CalibrationViewModel` uses `identity.calibrationBasename` for `.ti3` / `.cal` URLs and passes `originalBasename` into `CalibrationTargenConfig.basename` (runner may still prefix — after this ticket the runner must call `CalibrationIdentity.prefix` instead of its own ternary). - `ArgyllRunner.runCalibrationTargen` / `runPrintcal` call `CalibrationIdentity.prefix`. 2. `ArtefactProbe.resolveProfile(_ url: URL) -> URL` (and keep basename+cwd wrapper): - If `url` exists, return it. - Else try flipped `.icc` ↔ `.icm`. - Basename+cwd API remains `.icm` wins if both exist (current `resolveProfile(basename:cwd:)`). - Delete `ArgyllRunner.resolveProfileForVerification`; `runProfcheck` uses the probe. **Out** - Changing printcal argv, applycal tmp-replace, or Stage 0 UI layout. - Allowing navigation to Stage 4/5 while live basename is `CAL_` (existing wizard guard stays). ## Full solution - Persist only `calibrationOriginalBasename` + live `basename` as today; identity is derived, not a new JSON key. - `runCalibrationTargen` process id stays `targen_{CAL_stem}` so a profile `targen` and a cal `targen` cannot collide (#116). - Tests that write `CAL_foo.ti1` next to `foo.ti1` must keep working. ## Rewrite invariants - #29 Force Quit mid-calibration restores original basename, `sessionMode = .profile`, stage `.generate` when that is what `develop` does today — do not “helpfully” stay on Stage 0 if the current init explicitly leaves it. - #69 `.icm` preferred when probing by basename; explicit URL that exists wins even if the other extension is also present. - Calibration chart must never embed its own curves (`PrinttargArgs` already skips `-K`/`-I` for `CAL_` basenames). ## Dependencies Blocks-on: ArgyllRunner streaming-loop ticket (touch `runCalibrationTargen` / `runPrintcal` after that extract so the prefix call is one line in a thin wrapper). Unblocks: none. ## Test - New `Tests/ICCeryCoreTests/CalibrationIdentityTests.swift`: - live `foo`, persisted `""` → original `foo`, cal `CAL_foo` - live `CAL_foo`, persisted `foo` → original `foo` - live `CAL_foo`, persisted `""` → original `foo` - live `CAL_foo`, persisted `bar` → original `bar` (persisted wins; document this) - empty live basename does not invent a name - Update `WizardCalibrationSessionTests.swift` for launch restore using the identity helper. - Update `ArgyllRunnerCalibrationTests.swift`: passing basename `foo` still expects artefact `CAL_foo.ti1` / process id `targen_CAL_foo`. - Update `ArtefactFilesTests.swift` / probe tests: - only `.icc` exists - only `.icm` exists - both exist → `.icm` for basename probe - concrete URL to missing `.icc` but sibling `.icm` exists → returns `.icm` - `Milestone6CalibrationUITests.swift`: generate wedge still advances to layout; compute curves still restores original basename. ## Acceptance criteria - [ ] No `hasPrefix("CAL_")` ternary outside `CalibrationIdentity`. - [ ] No second profile-extension flip in `ArgyllRunner`. - [ ] Force Quit restore unit tests green. - [ ] Calibration UI tests green.
gronod added this to the M7 — Deduplicate & consolidate (develop) milestone 2026-09-10 18:23:13 +01:00
gronod added the Kind/Enhancement
Priority
High
2
Project/ICCery-v2Feature/Backend
labels 2026-09-10 18:23:13 +01:00
gronod self-assigned this 2026-09-10 18:23:13 +01:00
Author
Owner

Implementation originally landed in stacked commit d4261ba via PR #87.
Completion/verification landed in PR #99 at c539507.

Acceptance evidence:

[x] Restricted ArtefactProbe profile URL flipping strictly to .icc <-> .icm; unrelated extensions unmodified.

[x] Existing explicit URLs take precedence over sibling probe resolution.

[x] Empty live basename produces empty CalibrationIdentity (no resurrecting persisted original).

[x] targen_CAL_ process ID contract asserted.

Verification at milestone/m8-consolidation 891a504ee7:

targeted suites: passed (28 tests)

full universal ICCeryCoreTests: 339 passed, 0 failed

full ICCeryUITests: 29 passed, 0 failed

Closing manually after code and tests are present on the milestone branch.

Implementation originally landed in stacked commit d4261ba via PR #87. Completion/verification landed in PR #99 at c539507. Acceptance evidence: [x] Restricted ArtefactProbe profile URL flipping strictly to .icc <-> .icm; unrelated extensions unmodified. [x] Existing explicit URLs take precedence over sibling probe resolution. [x] Empty live basename produces empty CalibrationIdentity (no resurrecting persisted original). [x] targen_CAL_ process ID contract asserted. Verification at milestone/m8-consolidation 891a504ee722037ab15e01c1ae801a4d4cb9415a: targeted suites: passed (28 tests) full universal ICCeryCoreTests: 339 passed, 0 failed full ICCeryUITests: 29 passed, 0 failed Closing manually after code and tests are present on the milestone branch.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/iccery-v2-mac#83