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.
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.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
SettingsStore,WizardStateStore, andVerificationHistoryStoreeach build a pretty-printed, sorted-keysJSONEncoderand write viaAtomicFileWriter. Load policy differs (defaults vs throw) and must stay different.Spec refs
wizard_state.json,settings.json)verification_history.json— never overwrite unreadable files)Packages/ICCeryCore/Sources/ICCeryCore/Files/AtomicFileWriter.swiftScope
In
New
Packages/ICCeryCore/Sources/ICCeryCore/Files/JSONFileStore.swift: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. Keepvalidate()before save andsettingsDidChangenotification after successful save.WizardStateStore.load/save— same policy, default.default.VerificationHistoryStore—corrupt: .throwCorrupt.load()still hydrates the in-memory cache;append/clearstill refuse to write if load throws. Capacity trim stays in the actor.PresetStore.exportmay use the same encoder helper (encodePretty(_:)) but stays a single-preset blob, not a file store.Out
Full solution
SettingsStoreis referenced byAppEnvironment, UI tests, settings dialog). Do not force view models to talk toJSONFileStoredirectly.JSONFileStore.savemust callAtomicFileWriter.write.AtomicFileWriter/AppPaths— do not duplicatecreateDirectory.Rewrite invariants
settings.json/wizard_state.json→ defaults, never crash launch.verification_history.json→ throw, leave file intact (issue 26).SettingsStore.savestill throwsSettingsError.validationFailedand writes nothing.Dependencies
Blocks-on: none.
Unblocks: none required. Safe to land in parallel with the runner ticket.
Test
Tests/ICCeryCoreTests/SettingsTests.swift:.defaultWizardGatingTests/ settings tests — addWizardStateStoreTestsif missing):.defaultcalibrationOriginalBasename/sessionModeVerificationHistoryStoreTests.swift:loadthrows and a subsequentappendalso throws without replacing the fileJSONFileStoreTests.swiftwith a temp directory covering both policies.Acceptance criteria