Introduce JSONFileStore and retire copy-pasted settings / wizard / history writers #81

Open
opened 2026-09-10 18:23:12 +01:00 by gronod · 0 comments
Owner

Summary

SettingsStore, WizardStateStore, and VerificationHistoryStore each build a pretty-printed, sorted-keys JSONEncoder and write via AtomicFileWriter. Load policy differs (defaults vs throw) and must stay different.

Spec refs

  • Issue 4 / 5 persistence (wizard_state.json, settings.json)
  • Issue 26 history (verification_history.json — never overwrite unreadable files)
  • Packages/ICCeryCore/Sources/ICCeryCore/Files/AtomicFileWriter.swift

Scope

In
New Packages/ICCeryCore/Sources/ICCeryCore/Files/JSONFileStore.swift:

public enum JSONCorruptPolicy: Sendable {
    case replaceWithDefault
    case throwCorrupt
}

public struct JSONFileStore<T: Codable & Sendable>: Sendable {
    public let fileURL: URL
    public init(fileURL: URL, corrupt: JSONCorruptPolicy, defaultValue: @escaping @Sendable () -> T)
    public func load() throws -> T
    public func save(_ value: T) throws
}

Shared encoder flags: [.prettyPrinted, .sortedKeys]. Date strategy parameterized (history uses ISO-8601; settings/wizard do not encode dates today).

Rewire:

  • SettingsStore.load/savecorrupt: .replaceWithDefault, default .default. Keep validate() before save and settingsDidChange notification after successful save.
  • WizardStateStore.load/save — same policy, default .default.
  • VerificationHistoryStorecorrupt: .throwCorrupt. load() still hydrates the in-memory cache; append / clear still refuse to write if load throws. Capacity trim stays in the actor.

PresetStore.export may use the same encoder helper (encodePretty(_:)) but stays a single-preset blob, not a file store.

Out

  • Changing on-disk paths or filenames.
  • Changing history capacity (1000) or ISO-8601 format.
  • Reading v1 settings paths.

Full solution

  • Keep stores as the public façade (SettingsStore is referenced by AppEnvironment, UI tests, settings dialog). Do not force view models to talk to JSONFileStore directly.
  • JSONFileStore.save must call AtomicFileWriter.write.
  • Directory creation stays in AtomicFileWriter / AppPaths — do not duplicate createDirectory.

Rewrite invariants

  • Corrupt settings.json / wizard_state.json → defaults, never crash launch.
  • Corrupt verification_history.json → throw, leave file intact (issue 26).
  • SettingsStore.save still throws SettingsError.validationFailed and writes nothing.

Dependencies

Blocks-on: none.
Unblocks: none required. Safe to land in parallel with the runner ticket.

Test

  • Extend Tests/ICCeryCoreTests/SettingsTests.swift:
    • pretty JSON keys sorted
    • corrupt file → .default
    • validation failure → file unchanged
  • Extend wizard persistence tests (today they live near WizardGatingTests / settings tests — add WizardStateStoreTests if missing):
    • corrupt file → .default
    • round-trip calibrationOriginalBasename / sessionMode
  • Extend VerificationHistoryStoreTests.swift:
    • unreadable JSON → load throws and a subsequent append also throws without replacing the file
    • ISO-8601 dates still decode
    • capacity trim still drops oldest
  • New JSONFileStoreTests.swift with a temp directory covering both policies.

Acceptance criteria

  • One encoder/write implementation.
  • Three façades keep their public methods.
  • History corrupt-file contract preserved (file bytes unchanged after failed load).
  • New + updated unit tests green.
## Summary `SettingsStore`, `WizardStateStore`, and `VerificationHistoryStore` each build a pretty-printed, sorted-keys `JSONEncoder` and write via `AtomicFileWriter`. Load policy differs (defaults vs throw) and must stay different. ## Spec refs - Issue 4 / 5 persistence (`wizard_state.json`, `settings.json`) - Issue 26 history (`verification_history.json` — never overwrite unreadable files) - `Packages/ICCeryCore/Sources/ICCeryCore/Files/AtomicFileWriter.swift` ## Scope **In** New `Packages/ICCeryCore/Sources/ICCeryCore/Files/JSONFileStore.swift`: ```swift public enum JSONCorruptPolicy: Sendable { case replaceWithDefault case throwCorrupt } public struct JSONFileStore<T: Codable & Sendable>: Sendable { public let fileURL: URL public init(fileURL: URL, corrupt: JSONCorruptPolicy, defaultValue: @escaping @Sendable () -> T) public func load() throws -> T public func save(_ value: T) throws } ``` Shared encoder flags: `[.prettyPrinted, .sortedKeys]`. Date strategy parameterized (history uses ISO-8601; settings/wizard do not encode dates today). Rewire: - `SettingsStore.load/save` — `corrupt: .replaceWithDefault`, default `.default`. Keep `validate()` before save and `settingsDidChange` notification after successful save. - `WizardStateStore.load/save` — same policy, default `.default`. - `VerificationHistoryStore` — `corrupt: .throwCorrupt`. `load()` still hydrates the in-memory cache; `append` / `clear` still refuse to write if load throws. Capacity trim stays in the actor. `PresetStore.export` may use the same encoder helper (`encodePretty(_:)`) but stays a single-preset blob, not a file store. **Out** - Changing on-disk paths or filenames. - Changing history capacity (1000) or ISO-8601 format. - Reading v1 settings paths. ## Full solution - Keep stores as the public façade (`SettingsStore` is referenced by `AppEnvironment`, UI tests, settings dialog). Do not force view models to talk to `JSONFileStore` directly. - `JSONFileStore.save` must call `AtomicFileWriter.write`. - Directory creation stays in `AtomicFileWriter` / `AppPaths` — do not duplicate `createDirectory`. ## Rewrite invariants - Corrupt `settings.json` / `wizard_state.json` → defaults, never crash launch. - Corrupt `verification_history.json` → throw, leave file intact (issue 26). - `SettingsStore.save` still throws `SettingsError.validationFailed` and writes nothing. ## Dependencies Blocks-on: none. Unblocks: none required. Safe to land in parallel with the runner ticket. ## Test - Extend `Tests/ICCeryCoreTests/SettingsTests.swift`: - pretty JSON keys sorted - corrupt file → `.default` - validation failure → file unchanged - Extend wizard persistence tests (today they live near `WizardGatingTests` / settings tests — add `WizardStateStoreTests` if missing): - corrupt file → `.default` - round-trip `calibrationOriginalBasename` / `sessionMode` - Extend `VerificationHistoryStoreTests.swift`: - unreadable JSON → `load` throws and a subsequent `append` also throws without replacing the file - ISO-8601 dates still decode - capacity trim still drops oldest - New `JSONFileStoreTests.swift` with a temp directory covering both policies. ## Acceptance criteria - [ ] One encoder/write implementation. - [ ] Three façades keep their public methods. - [ ] History corrupt-file contract preserved (file bytes unchanged after failed load). - [ ] New + updated unit tests green.
gronod added this to the M7 — Deduplicate & consolidate (develop) milestone 2026-09-10 18:23:12 +01:00
gronod self-assigned this 2026-09-10 18:23:12 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#81