Project file: index over basename + cwd + recipe + last ΔE #149
Notifications
Due Date
No due date set.
Depends on
#146 Media library: printer + paper + ink + .cal bound to a preset
gronod/iccery-v2-mac
Reference: gronod/iccery-v2-mac#149
Reference in New Issue
Block a user
Summary
Resume-from-
.ti1/.ti2andwizard_state.jsonalready exist (WizardState/WizardStateStore). What is missing is a user-visible project they can reopen a month later: printer, paper, preset/recipe, calibration, artefact paths, last profcheck, notes. The project file is an index. Artefact gating stays on disk (#60, #151). Do not make the project a second source of truth.Spec refs
docs/06-wizard-and-artefacts.mdWizardState,WizardGating,ArtefactProbeVerificationHistoryStore/VerificationRecordMediaRecipe(M10 sibling) — optional field, not required to land firstFileDialogServiceone-picker-per-purposeJSONFileStoreatomic writes (#213)Scope
In:
ICCeryProjectCodable +.icceryprojJSON on disk (user-chosen folder)verifyStageArtefacts; stepper follows the disk, not the fileOut:
.ti1/.ti2/.ti3/.iccinto the project filewizard_state.jsonImplementation notes
Model
Snake_case keys.
schemaVersion != 1→ hard error on open (do not partial-decode v2 fields). Paths are stored absolute; on open, if cwd is missing promptselectDirectoryand rewrite the file — do not silently use Documents.Security: basename still rejects
/,\,... cwd must passPathSecurity/resolveSafeCwd. Refuse to save if basename empty or cwd empty (#59/#60).Persistence
selectProjectFileopen.icceryprojselectProjectSavePathsave.icceryprojselectPresetFile/selectDatasetFile.AppPaths.appDataDir/recent_projects.jsoncap 20, store bookmarkDataand path; drop entries whose file is gone.JSONFileStore.wizard_state.jsoncontinues to persist the live session. Opening a project writesWizardState { basename, cwd, printerName, profileBasename, sessionMode = .profile }then calls the existing gating probe. If disk says Stage 3 but the project JSON says Stage 5, the stepper shows Stage 3. Show an info banner “Project notes say profile done; artefacts on disk stop at .ti3”.Apply order
WizardViewModelmediaRecipeIDand library contains it →MediaLibraryViewModel.apply; else ifpresetID→ existing preset applytargen/colprof/chartreadSave from current session writes whatever is live, including last
VerificationHistoryStorerecord for this profile stem if any.Report
Save Report…writes{basename}-report.mdin cwd (atomic) with: name, printer, paper/recipe, artefact checklist (exists/missing), last avg/max ΔE₀₀, notes. No HTML. Filenames only.UI
There is no File menu on
develop.ICCeryAppis a singleWindow("ICCery", id: "main")with no.commands. Add the menu there. Do not invent a sixth wizard stage, and do not park Open/Save as large sidebar buttons next to Calibrate / Gamut / Spot Read — those slots are already spoken for by #146 / #148. Project chrome is the macOS File menu plus a compact status chip at the bottom of the 270 pt sidebar (aboveSpaceris wrong; put it in the spacer’s bottom, under the stepper).AppEnvironmentalready documents that XCUITest cannot driveNSOpenPanel/NSSavePanel. Every panel in this ticket goes through newFileDialogServicemethods so UI tests can inject fixtures. Do not callNSOpenPanelfrom aView.Window title
Window("ICCery", id: "main")stays the scene id (CI activates that window). Bind the represented title on theNSWindowfromProjectSession:ICCeryICCery — {project.name}project.updated→ICCery — {project.name} •Use
Text/Stringonly. Never put the raw path in the title (it overflows the traffic-light bar). Path belongs in the sidebar chip.help.File menu (
ICCeryApp.commands)Insert a
CommandGroup(replacing: .newItem)so we do not stack a second New under SwiftUI’s empty defaults.menuProjectNewresetLiveSession()menuProjectOpenselectProjectFilemenuProjectRecentsmenuProjectSaveprojectURLmenuProjectSaveAsselectProjectSavePaththen bindmenuProjectReport{basename}-report.mdin cwdmenuProjectCloseprojectURL, keep live basename/cwd/artefactsDisable Open / Save / Save As while
isChartreadRunningor aspotreadchild is live. Help copy on the disabled item is not available inCommandGroup; show aNoticeBannerif the user hits the keyboard shortcut anyway: “Finish the instrument session before opening a project.”Do not add these items to the dock menu or to Stage 1.
Menu identifiers must be set with
.accessibilityIdentifieronButtons inside theCommandGroup. Sidebar twins below use different ids sowaitFor("btnProjectOpen")does not hit two hits.Sidebar chip (bottom of
SidebarView)A 1–2 line footer, id
projectChip,Theme.panel, 8 pt padding.Text("No project")idprojectChipName+ buttonOpen…btnProjectOpenText(project.name)+ captionText(cwd.lastPathComponent)idprojectChipPath. Clicking the chip does not open Finder (accidental). Secondary click /Show in FinderbuttonbtnProjectRevealopens the project file, not cwdText("Disk behind project notes")idprojectChipStalewhen the info banner in Apply order fired (JSON claims Stage 5, artefacts stop earlier)btnProjectSavein the chip — same enable rules as ⌘S. Hidden when unbound (Save As lives in the menu only)helpOverlayon the chip: “A project remembers printer, preset and folder. The stepper still follows files on disk.” Overlay must not change sidebar height.Do not add New / Recents / Report to the chip. Menu only. The 270 pt column cannot take four more large buttons after Media + Calibrate + Gamut + Spot Read.
New Project
⌘N /
menuProjectNewpresents an.alertidprojectNewAlert, not a sheet:Text("Start a new project? The working folder and targets on disk are not deleted.")btnProjectNewCancel— no-opbtnProjectNewConfirmConfirm:
projectURLWizardState.basenameto""(empty is the legal “no target yet” state; do not write a placeholder — #60)cwdas-is (user already picked a folder this session) unless cwd is nilselectedRecipeID/ do not auto-apply a recipesessionMode = .profileICCeryDo not delete
.ti1/.ti2/.ti3/.icc/.cal. Do not wipemedia_library.json.If a child process is live, the alert does not appear; banner instead.
Open Project
selectProjectFile—NSOpenPanel,.icceryprojonly,allowsOtherFileTypes = false, title “Open ICCery Project”. Cancel is a no-op.Success path is the Apply order already in this ticket. Additional UI:
schemaVersion != 1→NoticeBannererror “This project file is not schema 1.” Do not touch live state/,..) → same, do not applyprojectRelocateSheet(not a nested open panel from the first panel):Text("The folder for {name} is missing. Choose a new working folder.")Choose Folder…btnProjectRelocate→ existingselectDirectorybtnProjectRelocateCancel— abort open, live session unchangedcwdin the project file atomically, then continue Apply orderlastVerificationexists but disk has no.icc, show info banner “Project notes say profile done; artefacts on disk stop at {last existing ext}.” Stepper follows disk. Do not jump to Stage 5Opening the same
projectURLagain is idempotent (re-probe, no second banner unless disk changed).Save / Save As
Save (
⌘S/btnProjectSave) writes the live session into the bound URL. Disabled when:projectURLCAL_*— Save must first resolveCalibrationIdentity.originalBasename; if that resolve fails, banner “Finish or exit calibration before saving a project.” and refuse. Never persistCAL_asbasenameSave As always shows
selectProjectSavePath(.icceryproj, suggested name{basename}.icceryprojin cwd). After write, bind that URL and push recents.Both writes are atomic (
JSONFileStore). Failure → error banner, bound URL unchanged.There is no auto-save on stage change.
wizard_state.jsonremains the crash-resume file.Recents submenu
menuProjectRecentslists up to 20, newest first. Each item label isText(name)— not the full path. IdprojectRecent-{bookmarkHash}(stable-ish; path can change after relocate).Clear MenumenuProjectRecentsClearat the bottom — wipesrecent_projects.json, does not delete.icceryprojfilesRecents file lives in
AppPaths.appDataDir, never next to the project.Report
Save Report…does not ask for a location. It writes{cwd}/{basename}-report.mdatomically. If the file exists, overwrite (it is generated). Banner “Wrote {basename}-report.md”. No HTML preview sheet. NoWKWebView.If basename or cwd is empty, the menu item is disabled.
Settings / other sheets
No Project tab in Settings. No project fields on Stage 1 — Stage 1 already has basename + folder pickers; a project drives those fields, it does not duplicate them.
After Open, Stage 1’s basename field and folder label must show the applied values. After New, basename field is empty.
Dirty state
Compare live
{basename, cwd, printerID, presetID, mediaRecipeID, calibrationURL}to the last savedICCeryProject. If different, title gets•andbtnProjectSaveis enabled. Opening or New with dirty state:btnProjectDirtySave/btnProjectDirtyDiscard/btnProjectDirtyCancel)UI tests (
Milestone10ProjectUITests)Inject
FileDialogServicefixtures (AppEnvironmenttest seam). Never present a real panel on CI.testNewProjectClearsBasenameDoesNotDeleteFixtureTi3— seed cwd with a.ti3; New; basename field empty; file still on disk; Stage 4 still gated on the new empty basenametestOpenProjectDiskWinsOverJsonStage— fixture.icceryprojclaims a profile; fixture folder has only.ti2; stepper Stage 5 locked;projectChipStaleexiststestSaveDisabledWithoutBasenametestCalBasenameRefused— liveCAL_basename, Save shows banner, file not written withCAL_testRecentsCapis a unit test on the store, not UIbtnProjectOpen/btnProjectSave/menuProjectNewvia the chip + alert where possibletestHelpOverlaysDoNotChangeSidebarHeightstill passes with the chip visibleIdentifiers (complete)
Rewrite invariants
Project never unlocks Stage 4 without a canonical
.ti3on disk (#109/#110). Import-dataset projects may lack.ti1/.ti2; gating already allows Stage 4 from.ti3— do not re-print.CAL_session: saving a project mid-calibration storesCalibrationIdentity.originalBasenameas basename and calibration URL separately; never persist liveCAL_as the project basename.Dependencies
Blocks-on: #4 gating, #6 dialogs, #26 history (snapshot is optional if history empty).
Soft-depends: Media library (
mediaRecipeIDignored until that issue merges —decodeIfPresent).Unblocks: none.
Test
schemaVersion2 throws.iccleaves Stage 4 locked if.ti3missing, Stage 5 locked if.iccmissing — disk wins over JSONCAL_live session saves original basenameMilestone10ProjectUITests, injected dialogs): New clears basename without deleting fixtures; Open disk-wins banner; Save disabled without basename;CAL_save refused.Acceptance criteria
.icceryprojis JSONschemaVersion1selectProjectFile/selectProjectSavePathpickers (not preset/dataset).ti3inside the projectWindow("ICCery")scene; sidebar only has a bottom chipCAL_cannot be saved as the project basenamefeat/<this>-project-file→milestone/m10-studio→developM10 implementation order (see milestone): 4 of 4. Blocked by #146 (
mediaRecipeID). Do not start until #146 apply/capture is onmilestone/m10-studio.Implementation landed in PR #154 at
1d78957d7aon milestone/m10-studio (merged asc39afe0dfd— the current milestone tip).Acceptance evidence:
.icceryprojis JSONschema_version: 1—ICCeryProjectCodable with strict version check;schema_version != 1is a hard decode error.selectProjectFile/selectProjectSavePathpickers inFileDialogService(.icceryprojonly; suggested name on save).recent_projects.jsonviaRecentProjectsStore, cap 20, dedupe by path), never inside the project file..ti3inside the project — it indexes paths, stores no measurement data.ProjectCommandson the existingWindowGroup("ICCery")scene (menuProjectNew/Open/Save/SaveAs/Report/Close,menuProjectRecents); sidebar has only the bottomProjectChip(projectChip,btnProjectOpen,btnProjectSave,btnProjectReveal) — no four large buttons.projectRelocateSheet,btnProjectRelocate); a missing project file in Recents is dropped on load.CAL_cannot be saved as the project basename —ICCeryProjectvalidation refuses it, matching the wizard invariant.•window title) + dirty-gate alert +{basename}-report.mdviaProjectReport.Verification at milestone/m10-studio
c39afe0dfd(x86_64):Milestone10ProjectUITestsgreen)xcodebuild test -scheme ICCery -destination 'platform=macOS' -derivedDataPath build/DerivedData-m10-final ARCHS=x86_64 CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-'Hardware item (quit and reopen a real project end-to-end) remains a milestone gate (Phase 8), not this issue's merge gate.
Risk register rows proven at this SHA: R18 (disk-wins artefact gating), R19 (WindowGroup commands + chip, no sidebar buttons).
Closing manually only after code and tests are present on the milestone branch.