commit 9fbe2c04ff9fa781ed42a797d8160be57d3232ae
parent 0a20c1bf3600ea51dbf4112026d5f65069cc043c
Author: Michael Camilleri <[email protected]>
Date: Sat, 23 May 2026 17:12:37 +0900
Restore retry on failed NYT puzzle upgrades
NYTPuzzleUpgrader.plan was keyed off entity.puzzleCmVersion, but
preparePuzzleForLoad already stamps that field to currentCmVersion on the first
open of any stale row regardless of whether the source was actually rewritten.
The net effect: when apply returned .failed (e.g. a transient fetch error), the
entity got bumped to the current version anyway, and on the next open the
planner saw 'nothing to do' and returned nil. The bad source was latched in
place forever even though the upgrader's whole point was to retry on .failed.
plan(for:store:) now parses the source up front and tests xd.cmVersion against
XD.currentCmVersion. replacePuzzleSource in the .upgraded path already writes
the freshly-converted XD (which carries the new CmVer header), so success
closes the loop naturally; .failed leaves the source untouched and the planner
fires again next open. .mismatched no longer suppresses retries via
bumpPuzzleCmVersion — a converter regression on a specific puzzle now costs one
wasted fetch per open until the source or the converter moves on, which is
acceptable given how rare structural mismatches should be.
XD.currentCmVersion is bumped 4 -> 5 to rescue rows that were already stamped
at 4 by the old plumbing. Their stored source still carries 'CmVer: 3' (or
earlier) internally, so they'll re-enter the planner once the new build runs.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/Crossmate/Models/XD.swift b/Crossmate/Models/XD.swift
@@ -4,7 +4,7 @@ import Foundation
/// full specification. Supports just enough of the format to parse our
/// bundled puzzles: metadata, grid (with rebus), and across/down clues.
struct XD: Sendable {
- static let currentCmVersion = 4
+ static let currentCmVersion = 5
let title: String?
let publisher: String?
diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift
@@ -717,7 +717,6 @@ final class GameStore {
struct PuzzleInfo: Sendable {
let gameID: UUID
let source: String
- let cmVersion: Int64
let isOwned: Bool
}
@@ -731,7 +730,6 @@ final class GameStore {
return PuzzleInfo(
gameID: id,
source: source,
- cmVersion: entity.puzzleCmVersion,
isOwned: entity.databaseScope == 0
)
}
diff --git a/Crossmate/Services/NYTPuzzleUpgrader.swift b/Crossmate/Services/NYTPuzzleUpgrader.swift
@@ -72,8 +72,8 @@ enum NYTPuzzleUpgrader {
static func plan(for id: UUID, store: GameStore) -> Plan? {
guard let info = store.puzzleInfo(for: id),
info.isOwned,
- info.cmVersion != Int64(XD.currentCmVersion),
let xd = try? XD.parse(info.source),
+ xd.cmVersion != XD.currentCmVersion,
xd.publisher == "New York Times",
let date = xd.date
else { return nil }