<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>crossmate, branch HEAD</title>
<subtitle>A collaborative crossword app for iOS
</subtitle>
<entry>
<id>cae1b44919c366db6a3cf9e9336f35431aa14909</id>
<published>2026-05-12T20:31:21Z</published>
<updated>2026-05-12T20:31:21Z</updated>
<title type="text">Skip the cell-cache rewrite when refreshing the current game</title>
<link rel="alternate" type="text/html" href="commit/cae1b44919c366db6a3cf9e9336f35431aa14909.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit cae1b44919c366db6a3cf9e9336f35431aa14909
parent 3b48d55d61110ac1e82d95a5a9e06c5234d5358f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 05:31:21 +0900

Skip the cell-cache rewrite when refreshing the current game

When a remote Moves record lands, the SyncEngine inbound path already runs
replayCellCache(for:in:) on its background context and saves the CellEntity
rows atomically with the inbound MovesEntity. The main context auto-merges
those changes via automaticallyMergesChangesFromParent, so by the time
refreshCurrentGame&#39;s downstream restore() reaches its updateCellCache step, the
CellEntity rows it would write are already the values it would write — same
merge, same input, same output.

Core Data&#39;s snapshot tracking elides the no-op save, but restore() still pays
for a full grid walk plus the save check on the main thread on every remote
keystroke during a co-solve. That cost is small but unnecessary. restore() now
takes an updateCache flag (default true so loadGame and the sample-seed path
are unchanged); refreshCurrentGame passes false. The in-memory game.squares
update and recomputeCompletionCache still run — those are what drives the UI
redraw — but the redundant main-context cell-cache pass is gone.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>3b48d55d61110ac1e82d95a5a9e06c5234d5358f</id>
<published>2026-05-12T20:09:58Z</published>
<updated>2026-05-12T20:09:58Z</updated>
<title type="text">Make MovesUpdater debounce timing test-injectable</title>
<link rel="alternate" type="text/html" href="commit/3b48d55d61110ac1e82d95a5a9e06c5234d5358f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 3b48d55d61110ac1e82d95a5a9e06c5234d5358f
parent d537bc60350fdd2b614fb698f183cb1e5b7c59ee
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 05:09:58 +0900

Make MovesUpdater debounce timing test-injectable

debounceCoalescesRapidEnqueues was racing the test&#39;s actor hop between two
enqueue() calls against the 50ms Task.sleep spawned by the first enqueue&#39;s
scheduleDebounce. On a contended simulator, that sleep would sometimes wake
before the test&#39;s second enqueue had been processed by the actor: the cancelled
debounce task got to debouncedFlush() first with the stale &#39;A&#39; value, the
test&#39;s waitForFlushCount(1) saw flushCount already at one, and the assertion
that the cell ended at &#39;B&#39; failed because the flush of &#39;B&#39; hadn&#39;t happened yet.
The previous flake-mitigation commit only bumped a polling timeout, which
didn&#39;t address the underlying race — the race window is independent of how long
the test is willing to wait.

MovesUpdater now takes an injectable sleep closure (defaulting to Task.sleep)
and the debounce timer goes through it. The test passes a ManualDebounceSleep
that captures every sleep continuation and releases them on demand, so the test
can buffer both enqueues before any debounce task wakes. The cancelled task
wakes, observes Task.isCancelled, and returns without flushing; the live task
wakes and flushes once with &#39;B&#39;. The behaviour the test is verifying is
unchanged; it just no longer depends on wall-clock timing.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>d537bc60350fdd2b614fb698f183cb1e5b7c59ee</id>
<published>2026-05-12T20:08:25Z</published>
<updated>2026-05-12T20:08:25Z</updated>
<title type="text">Parallelise refreshLibrary across database scopes</title>
<link rel="alternate" type="text/html" href="commit/d537bc60350fdd2b614fb698f183cb1e5b7c59ee.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit d537bc60350fdd2b614fb698f183cb1e5b7c59ee
parent 7434e62513449d783bce6112fbaee72a21a5f4a6
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 05:08:25 +0900

Parallelise refreshLibrary across database scopes

Pull-to-refresh ran five phases strictly back-to-back: private discovery,
shared discovery, private known-zone updates, shared known-zone updates, and a
final engine fetch. The four direct-fetch phases hit two independent CloudKit
databases, so the private and shared sides had no reason to wait for each other
— but the serial structure made the wall-time the sum of all four rather than
the slowest pair.

The two scope-specific pipelines now run concurrently via async let.  Discovery
still completes before known-zone updates within a scope so any zone that
discovery just inserted is included in the same refresh — the known-zone list
is read from Core Data at the start of its phase, so overlapping the two within
a scope would leave new zones for the next pull. Engine fetch keeps its
position as the final step; it already parallelises private and shared
internally, so wrapping it would gain nothing.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>7434e62513449d783bce6112fbaee72a21a5f4a6</id>
<published>2026-05-12T19:48:46Z</published>
<updated>2026-05-12T19:48:46Z</updated>
<title type="text">Parallelise discoverNewZonesDirect across zones and record types</title>
<link rel="alternate" type="text/html" href="commit/7434e62513449d783bce6112fbaee72a21a5f4a6.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 7434e62513449d783bce6112fbaee72a21a5f4a6
parent f1b12ffae6f3ee74572bf538ceb23149b3b333cc
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 04:48:46 +0900

Parallelise discoverNewZonesDirect across zones and record types

The zone-discovery path was doubly serial: candidate zones were processed one
at a time, and within each zone the Game / Moves / Player queries ran
back-to-back. For a device first encountering ten unseen shared games that
worked out to thirty sequential CKDatabase round-trips before the library
populated — the worst case being a fresh install that just accepted a few
shares.

Two layers of concurrency now apply. The per-zone work is dispatched through a
TaskGroup so the zones fan out instead of queueing. Within each zone the three
record-type queries are kicked off with async let; the Game query still gates
whether the zone is treated as hosting a Crossmate puzzle, but Moves and Player
against a non-puzzle zone return empty, so firing all three in parallel and
discarding when Game is empty is cheaper than waiting on Game first. Per-zone
errors are caught and traced rather than propagated, matching the pattern used
by fetchKnownZoneUpdatesDirect and fetchPushPingsDirect, so one bad zone
doesn&#39;t abort the batch.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>f1b12ffae6f3ee74572bf538ceb23149b3b333cc</id>
<published>2026-05-12T19:44:33Z</published>
<updated>2026-05-12T19:44:33Z</updated>
<title type="text">Parallelise fetchPushPingsDirect across zones</title>
<link rel="alternate" type="text/html" href="commit/f1b12ffae6f3ee74572bf538ceb23149b3b333cc.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit f1b12ffae6f3ee74572bf538ceb23149b3b333cc
parent ccf6c5adbaec793931f41ddd45b1fa748b4a51ef
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 04:44:33 +0900

Parallelise fetchPushPingsDirect across zones

The ping fast-path looped serially over every known zone in the database scope,
issuing one CKQuery per zone and appending the results. For a user with sixteen
active games that meant sixteen sequential round-trips on every silent-push
wake — the work blocking a collaborator&#39;s &#39;I just joined&#39; or &#39;I just solved&#39;
toast from reaching the screen.

The per-zone Ping queries are now dispatched through a TaskGroup, mirroring the
pattern fetchKnownZoneUpdatesDirect already uses. The actor&#39;s await points
release isolation between round-trips, so the per-zone CK requests actually
overlap. Per-zone errors are caught and traced rather than propagated, so one
transient failure doesn&#39;t suppress notifications from healthy zones.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>ccf6c5adbaec793931f41ddd45b1fa748b4a51ef</id>
<published>2026-05-12T19:42:14Z</published>
<updated>2026-05-12T19:42:14Z</updated>
<title type="text">Lower MovesUpdater debounce from 1500ms to 500ms</title>
<link rel="alternate" type="text/html" href="commit/ccf6c5adbaec793931f41ddd45b1fa748b4a51ef.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit ccf6c5adbaec793931f41ddd45b1fa748b4a51ef
parent a08b60856f1386c13aacb1f564214c9486e9dd76
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 04:42:14 +0900

Lower MovesUpdater debounce from 1500ms to 500ms

In normal typing, the debounce only delays the last letter of each run — a
cell-change forces an immediate flush for every prior letter — so the old
1500ms value meant the trailing keystroke of each word a collaborator typed
took roughly a second and a half longer to surface than the rest. 500ms is
still well clear of a typical typist&#39;s intra-word pause, so the number of
record writes per unit of typing is essentially unchanged.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>a08b60856f1386c13aacb1f564214c9486e9dd76</id>
<published>2026-05-12T19:29:16Z</published>
<updated>2026-05-12T19:29:16Z</updated>
<title type="text">Kick sendChanges() on player, game and delete enqueues</title>
<link rel="alternate" type="text/html" href="commit/a08b60856f1386c13aacb1f564214c9486e9dd76.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a08b60856f1386c13aacb1f564214c9486e9dd76
parent 89e85099e3d7809eb1953b191e5b45d2dcb74860
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 04:29:16 +0900

Kick sendChanges() on player, game and delete enqueues

enqueueMoves and enqueuePing already trail their state.add with an explicit
Task { try? await engine.sendChanges() } because, as the comment on
enqueueMoves explains, repeated state.add calls against the same CKRecord.ID
get coalesced by the framework&#39;s scheduler and can sit in the queue for a
noticeable while before going out. enqueuePlayerRecord, enqueueGame and
enqueueDeleteGame had the same shape but no kick — selection updates re-target
the same player record on every cursor move and hit exactly the same trap, and
game create / update / delete fan out from one-shot user actions where waiting
on the scheduler is pure latency.

All three now match the existing pattern. PlayerSelectionPublisher&#39;s 300ms
debounce upstream still coalesces cursor bursts, so the change just hands
each debounced burst to CloudKit immediately instead of letting it idle.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>89e85099e3d7809eb1953b191e5b45d2dcb74860</id>
<published>2026-05-12T19:24:33Z</published>
<updated>2026-05-12T19:24:33Z</updated>
<title type="text">Parallelise the silent-push handler and skip wasteful zone discovery</title>
<link rel="alternate" type="text/html" href="commit/89e85099e3d7809eb1953b191e5b45d2dcb74860.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 89e85099e3d7809eb1953b191e5b45d2dcb74860
parent c4d448911d854f6a20ee148810a1bfd982a460ff
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed, 13 May 2026 04:24:33 +0900

Parallelise the silent-push handler and skip wasteful zone discovery

handleRemoteNotification ran three independent CloudKit round-trips back to
back on every silent-push wake: discoverNewZonesDirect, fetchPushPingsDirect,
then either fetchPushChangesDirect for the active game or a fallback engine
fetch. The active-game direct fetch — the round-trip that actually drives the
&#39;collaborator typed a letter&#39; path — only started after the other two had
returned, even though it touches a different zone and a different record set.
End-to-end push-to-pixel latency was the sum of all three rather than the
slowest one.

Zone discovery is also pure overhead on the collaborative hot path. By the time
a push arrives for a game the user has open, that game&#39;s zone is by definition
already known locally; allRecordZones() finds nothing new but costs a
round-trip on every keystroke a partner makes.

When there is an active game in the pushed database scope, the handler now
skips zone discovery entirely and runs fetchPushChangesDirect and
fetchPushPingsDirect concurrently via async let. The two read different record
types in different zones and don&#39;t share state, so the wall time drops from t₁
+ t₂ to max(t₁, t₂). Brand-new shared games arriving during a session aren&#39;t
surfaced until the next push or a foreground sync — an accepted trade-off,
since the latency-sensitive case is the puzzle that&#39;s already open. When no
game is open, the handler keeps the original discovery → pings → engine-fetch
order so a background wake still catches new shares.

Database subscriptions don&#39;t carry a zone ID in their push payload, so a
finer-grained &quot;skip discovery if the pushed zone is known&quot; check isn&#39;t
available without switching to per-zone subscriptions; the active-game
heuristic captures the same intent for the case that matters.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>c4d448911d854f6a20ee148810a1bfd982a460ff</id>
<published>2026-05-12T10:16:00Z</published>
<updated>2026-05-12T10:16:00Z</updated>
<title type="text">Increase timeout for flakey test</title>
<link rel="alternate" type="text/html" href="commit/c4d448911d854f6a20ee148810a1bfd982a460ff.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit c4d448911d854f6a20ee148810a1bfd982a460ff
parent 4fc8e83ff8147e968aa84826074bb072945a092a
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 19:16:00 +0900

Increase timeout for flakey test

</content>
</entry>
<entry>
<id>4fc8e83ff8147e968aa84826074bb072945a092a</id>
<published>2026-05-12T10:11:09Z</published>
<updated>2026-05-12T10:11:09Z</updated>
<title type="text">Parallelise the library refresh and skip completed games</title>
<link rel="alternate" type="text/html" href="commit/4fc8e83ff8147e968aa84826074bb072945a092a.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 4fc8e83ff8147e968aa84826074bb072945a092a
parent c32b4f1b00aee2a86fcf00abd2419f51d1663b29
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 19:11:09 +0900

Parallelise the library refresh and skip completed games

The pull-to-refresh action was running serially: for each of the user&#39;s known
games it issued three sequential CKDatabase round-trips (Game by ID, Moves
query, Player query), and the per-game blocks ran one after the other. A
library with sixteen active games took roughly sixteen seconds end-to-end, even
when nothing had changed on the server — most of which was spent waiting for
round-trips that could have run at the same time.

Two layers of concurrency address this. Inside fetchPushChangesDirect, the Game
fetch and the Moves/Player queries are now kicked off with `async let` and
joined together, since they&#39;re independent and target the same zone. Inside
fetchKnownZoneUpdatesDirect, the per-game calls are now dispatched through a
TaskGroup so all known games fan out concurrently; per-game errors are still
caught and traced so one bad zone doesn&#39;t abort the batch. The actor&#39;s await
points release isolation between round-trips, so the network operations
actually overlap.

knownGameIDs now also filters out games with a non-nil completedAt. Once a
game is complete no further Moves or Player records can arrive, so refreshing
those zones is wasted work. Completed games are still visible in the library;
they&#39;re just skipped during the refresh fan-out.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>c32b4f1b00aee2a86fcf00abd2419f51d1663b29</id>
<published>2026-05-12T09:10:33Z</published>
<updated>2026-05-12T09:10:33Z</updated>
<title type="text">Pull updates for known game zones on library refresh</title>
<link rel="alternate" type="text/html" href="commit/c32b4f1b00aee2a86fcf00abd2419f51d1663b29.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit c32b4f1b00aee2a86fcf00abd2419f51d1663b29
parent 763050477d91745f8695c0d4ec92e131d6cd2aaf
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 18:10:33 +0900

Pull updates for known game zones on library refresh

The pull-to-refresh action added in the previous commit only covered half of
&#39;what might have changed elsewhere&#39;: new zones the device had never seen.
Updates to games already in the library — new moves, player roster changes —
still relied on CKSyncEngine&#39;s eventual fetch, which is the very thing the
manual refresh is meant to compensate for. The effect was that pulling on a
device with stale state did nothing visible until the engine caught up on its
own schedule.

This commit adds SyncEngine.fetchKnownZoneUpdatesDirect(scope:), which
iterates the locally-known games for the scope (via a new
knownGameIDs(forScope:in:) helper that mirrors knownZones) and routes
each one through the existing fetchPushChangesDirect. That path fetches
the Game record by ID and queries Moves and Player incrementally
against liveQueryCheckpoints, so successive refreshes only pull records
modified since the previous run. Per-game errors are caught and traced
so one failing zone doesn&#39;t abort the rest of the batch.

AppServices.refreshLibrary now runs the four direct phases (discovery and
known-zone updates, on both database scopes) before falling through to the
engine fetch.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>763050477d91745f8695c0d4ec92e131d6cd2aaf</id>
<published>2026-05-12T08:40:00Z</published>
<updated>2026-05-12T08:40:00Z</updated>
<title type="text">Discover unseen game zones without waiting on CKSyncEngine</title>
<link rel="alternate" type="text/html" href="commit/763050477d91745f8695c0d4ec92e131d6cd2aaf.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 763050477d91745f8695c0d4ec92e131d6cd2aaf
parent 2d868d03075a3e36ba4a2c873adb2c8bcc8a92d7
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 17:40:00 +0900

Discover unseen game zones without waiting on CKSyncEngine

CKSyncEngine is responsible for delivering database-scope change events that
announce new zones (a game created on another device of the same iCloud user,
or a freshly-accepted share), but those events can lag well behind reality — on
a silent-push wake they are sometimes withheld until the next foreground, and
on an idle engine they may not arrive for a long time at all. The symptom is a
second device whose library stays empty while the first device clearly has
games in the same private database.

This commit adds SyncEngine.discoverNewZonesDirect(scope:), which enumerates
zones via CKDatabase.allRecordZones(), diffs against the locally-known set from
knownZones(forScope:in:), and pulls Game / Moves / Player records for each new
zone directly. The records flow through the existing
applyDirectRecordZoneChanges pipeline, so upserts, cell-cache replay, and the
playerRosterShouldRefresh notification all behave the same as on an
engine-driven fetch. The Game query keeps the path independent of the
&quot;game-&lt;UUID&gt;&quot; zone-naming convention.

Two entry points wire it in. The remote-notification handler runs zone
discovery before the existing Ping fast-path, so a ping that arrives for a
brand-new game can resolve against a freshly-created GameEntity. A new
AppServices.refreshLibrary() runs discovery on both scopes followed by a normal
engine fetch, and is attached as the .refreshable action on GameListView so the
user has a manual fallback when the engine is silent.  The empty-state
ContentUnavailableView moves into an overlay above an always-present List so
pull-to-refresh works even with zero games, which is the case this gesture most
needs to cover.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>2d868d03075a3e36ba4a2c873adb2c8bcc8a92d7</id>
<published>2026-05-12T08:07:04Z</published>
<updated>2026-05-12T08:07:04Z</updated>
<title type="text">Bump Crossmate-flavoured XD version</title>
<link rel="alternate" type="text/html" href="commit/2d868d03075a3e36ba4a2c873adb2c8bcc8a92d7.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 2d868d03075a3e36ba4a2c873adb2c8bcc8a92d7
parent c6dfbc0e018dc319859f86298278f737823e1d90
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 17:07:04 +0900

Bump Crossmate-flavoured XD version

</content>
</entry>
<entry>
<id>c6dfbc0e018dc319859f86298278f737823e1d90</id>
<published>2026-05-12T08:04:50Z</published>
<updated>2026-05-12T08:04:50Z</updated>
<title type="text">Render circled cells in converted puzzles</title>
<link rel="alternate" type="text/html" href="commit/c6dfbc0e018dc319859f86298278f737823e1d90.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit c6dfbc0e018dc319859f86298278f737823e1d90
parent d64acf0bd3beeba45cd25f02d5ac1ba338e2aaab
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 17:04:50 +0900

Render circled cells in converted puzzles

This commit replaces specialCellIndices(body:) with specialCellInfo(body:)
which scans each per-cell &lt;g&gt; in certain JSON input files for both markers; the
same rect-fill check as prior to this commit plus a &lt;circle&gt; child element. The
function returns the affected indices together with the kind (&quot;shaded&quot; or
&quot;circle&quot;), and the metadata writer now emits the matching Special: header.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>d64acf0bd3beeba45cd25f02d5ac1ba338e2aaab</id>
<published>2026-05-12T07:19:28Z</published>
<updated>2026-05-12T07:19:28Z</updated>
<title type="text">Surface collaborator pings on background push wakes</title>
<link rel="alternate" type="text/html" href="commit/d64acf0bd3beeba45cd25f02d5ac1ba338e2aaab.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit d64acf0bd3beeba45cd25f02d5ac1ba338e2aaab
parent 290ad1a3b776b7c8255abe6e09e9a07584adf0b0
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 16:19:28 +0900

Surface collaborator pings on background push wakes

CKSyncEngine.fetchChanges() can return successfully from a silent-push wake
without delivering record-zone events until the app is next foregrounded (the
result can then be that notifications are not shown to the user in a timely
manner). This commit makes the silent-push handler bypass that quirk by
querying Ping records directly across all known zones for the notified database
scope, then feeding them through the existing onPings callback so local
notifications are surfaced immediately.

The per-zone since cursor uses a forward-moving scope checkpoint when available
and falls back to the GameEntity&#39;s createdAt timestamp, so the first run after
install or reset captures the triggering ping without replaying historical ones
from before the device joined the game. To keep the eventual CKSyncEngine
catch-up from re-surfacing the same pings, presentation now dedupes by Ping
record name through a new ring buffer in NotificationState, and resetSyncState
clears both the scope and live-query checkpoints.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>290ad1a3b776b7c8255abe6e09e9a07584adf0b0</id>
<published>2026-05-11T22:23:34Z</published>
<updated>2026-05-11T22:23:34Z</updated>
<title type="text">Tweak Clue Bar layout</title>
<link rel="alternate" type="text/html" href="commit/290ad1a3b776b7c8255abe6e09e9a07584adf0b0.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 290ad1a3b776b7c8255abe6e09e9a07584adf0b0
parent e17a351feb32304960f8e47ec18d979a784c1e68
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue, 12 May 2026 07:23:34 +0900

Tweak Clue Bar layout

</content>
</entry>
<entry>
<id>e17a351feb32304960f8e47ec18d979a784c1e68</id>
<published>2026-05-10T18:55:37Z</published>
<updated>2026-05-10T18:55:37Z</updated>
<title type="text">Add Crossmate version to puzzle data structures</title>
<link rel="alternate" type="text/html" href="commit/e17a351feb32304960f8e47ec18d979a784c1e68.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e17a351feb32304960f8e47ec18d979a784c1e68
parent 7b68f66437ad9e56f3c409b83559d3211278d9c4
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 03:55:37 +0900

Add Crossmate version to puzzle data structures

This commit adds a Crossmate version number (set to 1 if absent) for
parsed puzzles so that it&#39;s possible to communicate to the system when
it needs to reparse puzzle data.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>7b68f66437ad9e56f3c409b83559d3211278d9c4</id>
<published>2026-05-10T18:30:38Z</published>
<updated>2026-05-10T18:30:38Z</updated>
<title type="text">Update .gitignore</title>
<link rel="alternate" type="text/html" href="commit/7b68f66437ad9e56f3c409b83559d3211278d9c4.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 7b68f66437ad9e56f3c409b83559d3211278d9c4
parent 1835d68e6c8bcba1e6df4f392e2a41997b2790ac
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 03:30:38 +0900

Update .gitignore

</content>
</entry>
<entry>
<id>1835d68e6c8bcba1e6df4f392e2a41997b2790ac</id>
<published>2026-05-10T16:42:54Z</published>
<updated>2026-05-10T16:42:54Z</updated>
<title type="text">Use publisher as a consistent name</title>
<link rel="alternate" type="text/html" href="commit/1835d68e6c8bcba1e6df4f392e2a41997b2790ac.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 1835d68e6c8bcba1e6df4f392e2a41997b2790ac
parent 5e688df84884a75c038e09b923b2910c210a5fbb
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 01:42:54 +0900

Use publisher as a consistent name

</content>
</entry>
<entry>
<id>5e688df84884a75c038e09b923b2910c210a5fbb</id>
<published>2026-05-10T16:18:27Z</published>
<updated>2026-05-10T16:18:27Z</updated>
<title type="text">Fix movement when deleting first letter of a word</title>
<link rel="alternate" type="text/html" href="commit/5e688df84884a75c038e09b923b2910c210a5fbb.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5e688df84884a75c038e09b923b2910c210a5fbb
parent 46a8f86ec2037ad71368553fd944841dae1c72b2
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 01:18:27 +0900

Fix movement when deleting first letter of a word

</content>
</entry>
<entry>
<id>46a8f86ec2037ad71368553fd944841dae1c72b2</id>
<published>2026-05-10T16:13:38Z</published>
<updated>2026-05-10T16:13:38Z</updated>
<title type="text">Fix movement when typing last letter in a direction</title>
<link rel="alternate" type="text/html" href="commit/46a8f86ec2037ad71368553fd944841dae1c72b2.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 46a8f86ec2037ad71368553fd944841dae1c72b2
parent e4f3eda2a2463b946a69887865b437b1c387fbdd
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 01:13:38 +0900

Fix movement when typing last letter in a direction

</content>
</entry>
<entry>
<id>e4f3eda2a2463b946a69887865b437b1c387fbdd</id>
<published>2026-05-10T15:55:02Z</published>
<updated>2026-05-10T15:55:02Z</updated>
<title type="text">Adjust touch target for previous/next buttons in Clue Bar</title>
<link rel="alternate" type="text/html" href="commit/e4f3eda2a2463b946a69887865b437b1c387fbdd.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e4f3eda2a2463b946a69887865b437b1c387fbdd
parent f09cea5083c83b81beaf30cc99f20bd1304570cb
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 00:55:02 +0900

Adjust touch target for previous/next buttons in Clue Bar

</content>
</entry>
<entry>
<id>f09cea5083c83b81beaf30cc99f20bd1304570cb</id>
<published>2026-05-10T15:25:13Z</published>
<updated>2026-05-10T15:25:13Z</updated>
<title type="text">Improve latency of pings</title>
<link rel="alternate" type="text/html" href="commit/f09cea5083c83b81beaf30cc99f20bd1304570cb.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit f09cea5083c83b81beaf30cc99f20bd1304570cb
parent bf1bf5b95390caa8dad248e27659676abca21641
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon, 11 May 2026 00:25:13 +0900

Improve latency of pings

</content>
</entry>
<entry>
<id>bf1bf5b95390caa8dad248e27659676abca21641</id>
<published>2026-05-10T14:17:48Z</published>
<updated>2026-05-10T14:17:48Z</updated>
<title type="text">Clean up completed game ping records</title>
<link rel="alternate" type="text/html" href="commit/bf1bf5b95390caa8dad248e27659676abca21641.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit bf1bf5b95390caa8dad248e27659676abca21641
parent 1fa75ef3ad4cfa2fb33e3cf48ee074b2c0ab76de
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 23:17:48 +0900

Clean up completed game ping records

Crossmate uses Ping records as a means of providing shared notification
triggers. They are used during an active cooperative puzzle but are no longer
required after a puzzle has been completed. Retaining them keeps (potentially)
a number of records around unnecessarily.

This commit causes Crossmate to delete non-win Ping records from an owned
game’s CloudKit zone after completion, while preserving win pings so the final
notification remains available. It also drops pending non-win pings for that
game before they are sent.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>1fa75ef3ad4cfa2fb33e3cf48ee074b2c0ab76de</id>
<published>2026-05-10T13:55:50Z</published>
<updated>2026-05-10T13:55:50Z</updated>
<title type="text">Tweak layout of Clue List on iOS</title>
<link rel="alternate" type="text/html" href="commit/1fa75ef3ad4cfa2fb33e3cf48ee074b2c0ab76de.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 1fa75ef3ad4cfa2fb33e3cf48ee074b2c0ab76de
parent 5ab0b2b441677c1e775db26de5e2786f4c317c8c
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 22:55:50 +0900

Tweak layout of Clue List on iOS

</content>
</entry>
<entry>
<id>5ab0b2b441677c1e775db26de5e2786f4c317c8c</id>
<published>2026-05-10T11:01:48Z</published>
<updated>2026-05-10T11:01:48Z</updated>
<title type="text">Move diagnostics menu option into Puzzle Menu</title>
<link rel="alternate" type="text/html" href="commit/5ab0b2b441677c1e775db26de5e2786f4c317c8c.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5ab0b2b441677c1e775db26de5e2786f4c317c8c
parent e220f22a61f7aff0cdf9a0ba77bb7ca2395fc7e7
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 20:01:48 +0900

Move diagnostics menu option into Puzzle Menu

</content>
</entry>
<entry>
<id>e220f22a61f7aff0cdf9a0ba77bb7ca2395fc7e7</id>
<published>2026-05-10T10:49:03Z</published>
<updated>2026-05-10T10:49:03Z</updated>
<title type="text">Make diagnostics accessible from Puzzle View</title>
<link rel="alternate" type="text/html" href="commit/e220f22a61f7aff0cdf9a0ba77bb7ca2395fc7e7.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e220f22a61f7aff0cdf9a0ba77bb7ca2395fc7e7
parent b2abc4d1c8af7198cc7850923eb871ea0c5b283f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 19:49:03 +0900

Make diagnostics accessible from Puzzle View

</content>
</entry>
<entry>
<id>b2abc4d1c8af7198cc7850923eb871ea0c5b283f</id>
<published>2026-05-10T10:23:16Z</published>
<updated>2026-05-10T10:23:16Z</updated>
<title type="text">Simplify push notification fallback</title>
<link rel="alternate" type="text/html" href="commit/b2abc4d1c8af7198cc7850923eb871ea0c5b283f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit b2abc4d1c8af7198cc7850923eb871ea0c5b283f
parent 9b285ad724c8f424dc46040ab063d4faf7e7aa21
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 19:23:16 +0900

Simplify push notification fallback

</content>
</entry>
<entry>
<id>9b285ad724c8f424dc46040ab063d4faf7e7aa21</id>
<published>2026-05-10T06:18:48Z</published>
<updated>2026-05-10T06:18:48Z</updated>
<title type="text">Limit push fallback to active game</title>
<link rel="alternate" type="text/html" href="commit/9b285ad724c8f424dc46040ab063d4faf7e7aa21.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 9b285ad724c8f424dc46040ab063d4faf7e7aa21
parent 75b0b776f45cc97eef42c9154743feb771211edc
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 15:18:48 +0900

Limit push fallback to active game

The silent-push fallback only needs live collaboration state for the currently
open puzzle. This commit replaces the broad nil-token replay with an
active-zone query that fetches the Game record and recently modified Moves and
Player records, while continuing to leave Ping records out of the live path.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>75b0b776f45cc97eef42c9154743feb771211edc</id>
<published>2026-05-10T05:51:32Z</published>
<updated>2026-05-10T05:51:32Z</updated>
<title type="text">Include Player records in fallback fetch</title>
<link rel="alternate" type="text/html" href="commit/75b0b776f45cc97eef42c9154743feb771211edc.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 75b0b776f45cc97eef42c9154743feb771211edc
parent e27c5b5001169bab53a38f70ef51215f3994f6e1
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun, 10 May 2026 14:51:32 +0900

Include Player records in fallback fetch

</content>
</entry>
<entry>
<id>e27c5b5001169bab53a38f70ef51215f3994f6e1</id>
<published>2026-05-09T05:09:34Z</published>
<updated>2026-05-09T05:09:34Z</updated>
<title type="text">Disable solved puzzle toolbar actions</title>
<link rel="alternate" type="text/html" href="commit/e27c5b5001169bab53a38f70ef51215f3994f6e1.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e27c5b5001169bab53a38f70ef51215f3994f6e1
parent aeb3e878ffca33755491e42f6fdfce6d7b5d697d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 14:09:34 +0900

Disable solved puzzle toolbar actions

Completed puzzles should leave the toolbar in a read-only state. This commit
disables the Players menu alongside the existing Entry and Hints controls, and
makes the Draft button drop its active styling when solved.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>aeb3e878ffca33755491e42f6fdfce6d7b5d697d</id>
<published>2026-05-09T03:01:30Z</published>
<updated>2026-05-09T03:01:30Z</updated>
<title type="text">Load puzzles before roster refresh</title>
<link rel="alternate" type="text/html" href="commit/aeb3e878ffca33755491e42f6fdfce6d7b5d697d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit aeb3e878ffca33755491e42f6fdfce6d7b5d697d
parent 23e50ac6bfecf87c6c59138b2d435684a9532ec1
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 12:01:30 +0900

Load puzzles before roster refresh

Opening a shared puzzle could wait on CloudKit-backed roster work before
rendering the game, leaving the destination blank for a long time. This commit
renders the puzzle as soon as local game state is loaded, moves roster and
sharing setup into cancellable follow-up work, and shows waiting player rows
until game-specific Player records arrive.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>23e50ac6bfecf87c6c59138b2d435684a9532ec1</id>
<published>2026-05-09T02:40:23Z</published>
<updated>2026-05-09T02:40:23Z</updated>
<title type="text">Retry pending move flushes until persisted</title>
<link rel="alternate" type="text/html" href="commit/23e50ac6bfecf87c6c59138b2d435684a9532ec1.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 23e50ac6bfecf87c6c59138b2d435684a9532ec1
parent d2243dc5ff197aeba21f4ac28161038a4d117307
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 11:40:23 +0900

Retry pending move flushes until persisted

Leaving a puzzle can happen before the debounce has persisted the latest edit,
and failed flushes could previously drop the in-memory buffer. This commit
flushes moves on puzzle exit and keeps pending edits queued when author
identity or Core Data persistence is unavailable, so they can be retried
instead of lost.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>d2243dc5ff197aeba21f4ac28161038a4d117307</id>
<published>2026-05-09T01:28:52Z</published>
<updated>2026-05-09T01:28:52Z</updated>
<title type="text">Fix notification replay bug</title>
<link rel="alternate" type="text/html" href="commit/d2243dc5ff197aeba21f4ac28161038a4d117307.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit d2243dc5ff197aeba21f4ac28161038a4d117307
parent 6982ae31db03498897daa4d519bdf9fb95031ea7
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 10:28:52 +0900

Fix notification replay bug

Notifications are created by Ping records. These were being refetched
during active sessions causing a flurry of notifications to be received.
These records should not be refetched at all; only Moves and Game are
required.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>6982ae31db03498897daa4d519bdf9fb95031ea7</id>
<published>2026-05-09T00:19:39Z</published>
<updated>2026-05-09T00:19:39Z</updated>
<title type="text">Preserve local Moves record at all times</title>
<link rel="alternate" type="text/html" href="commit/6982ae31db03498897daa4d519bdf9fb95031ea7.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 6982ae31db03498897daa4d519bdf9fb95031ea7
parent a80f4ff678caa09eb27351d910d759432306fe5d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 09:19:39 +0900

Preserve local Moves record at all times

Direct push fetches can re-deliver this device&#39;s older server-side Moves record
while newer local edits are still queued for upload. This commit keeps local
value state authoritative for the current author/device row while still
adopting CloudKit system fields, and continues to replace cached rows for other
devices normally.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>a80f4ff678caa09eb27351d910d759432306fe5d</id>
<published>2026-05-08T22:40:57Z</published>
<updated>2026-05-08T22:40:57Z</updated>
<title type="text">Fix Success Panel animation</title>
<link rel="alternate" type="text/html" href="commit/a80f4ff678caa09eb27351d910d759432306fe5d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a80f4ff678caa09eb27351d910d759432306fe5d
parent b812b0fc67c9e44d7027a9d8031ef7b12e6fbd3f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 07:40:57 +0900

Fix Success Panel animation

</content>
</entry>
<entry>
<id>b812b0fc67c9e44d7027a9d8031ef7b12e6fbd3f</id>
<published>2026-05-08T22:17:55Z</published>
<updated>2026-05-08T22:17:55Z</updated>
<title type="text">Improve Success Panel animation</title>
<link rel="alternate" type="text/html" href="commit/b812b0fc67c9e44d7027a9d8031ef7b12e6fbd3f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit b812b0fc67c9e44d7027a9d8031ef7b12e6fbd3f
parent 674d35c79bf3eea1500a02dbc0c9ce9270e8c4fb
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 07:17:55 +0900

Improve Success Panel animation

The Success Panel was animating from above the safe area rather than
from the true edge of the device. This commit fixes that.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>674d35c79bf3eea1500a02dbc0c9ce9270e8c4fb</id>
<published>2026-05-08T21:14:16Z</published>
<updated>2026-05-08T21:14:16Z</updated>
<title type="text">Extend divider into safe area in landscape</title>
<link rel="alternate" type="text/html" href="commit/674d35c79bf3eea1500a02dbc0c9ce9270e8c4fb.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 674d35c79bf3eea1500a02dbc0c9ce9270e8c4fb
parent d0f776b69a7f9c80c8b678b581160c2b8dda6b1c
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 06:14:16 +0900

Extend divider into safe area in landscape

</content>
</entry>
<entry>
<id>d0f776b69a7f9c80c8b678b581160c2b8dda6b1c</id>
<published>2026-05-08T15:00:45Z</published>
<updated>2026-05-08T15:00:45Z</updated>
<title type="text">Disable author tinting for local games</title>
<link rel="alternate" type="text/html" href="commit/d0f776b69a7f9c80c8b678b581160c2b8dda6b1c.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit d0f776b69a7f9c80c8b678b581160c2b8dda6b1c
parent 6547352f9bb61891576f36f9f031b71f2d99f031
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  9 May 2026 00:00:45 +0900

Disable author tinting for local games

</content>
</entry>
<entry>
<id>6547352f9bb61891576f36f9f031b71f2d99f031</id>
<published>2026-05-08T14:39:11Z</published>
<updated>2026-05-08T14:39:11Z</updated>
<title type="text">Collapse roster list in Scoreboard</title>
<link rel="alternate" type="text/html" href="commit/6547352f9bb61891576f36f9f031b71f2d99f031.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 6547352f9bb61891576f36f9f031b71f2d99f031
parent 8b6d81274d17a76339e39f874eac2c0c4741e3b1
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 23:39:11 +0900

Collapse roster list in Scoreboard

</content>
</entry>
<entry>
<id>8b6d81274d17a76339e39f874eac2c0c4741e3b1</id>
<published>2026-05-08T14:33:07Z</published>
<updated>2026-05-08T14:33:07Z</updated>
<title type="text">Make roster always present</title>
<link rel="alternate" type="text/html" href="commit/8b6d81274d17a76339e39f874eac2c0c4741e3b1.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 8b6d81274d17a76339e39f874eac2c0c4741e3b1
parent 93ebd202cf3df9b55009848c09105092944d063d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 23:33:07 +0900

Make roster always present

</content>
</entry>
<entry>
<id>93ebd202cf3df9b55009848c09105092944d063d</id>
<published>2026-05-08T13:58:01Z</published>
<updated>2026-05-08T14:01:09Z</updated>
<title type="text">Add logging for rosters</title>
<link rel="alternate" type="text/html" href="commit/93ebd202cf3df9b55009848c09105092944d063d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 93ebd202cf3df9b55009848c09105092944d063d
parent ede281d8b794776b545c43b07ab3d58e4d900561
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 22:58:01 +0900

Add logging for rosters

</content>
</entry>
<entry>
<id>ede281d8b794776b545c43b07ab3d58e4d900561</id>
<published>2026-05-08T13:38:03Z</published>
<updated>2026-05-08T13:38:03Z</updated>
<title type="text">Add alternative CloudKit path for notified changes</title>
<link rel="alternate" type="text/html" href="commit/ede281d8b794776b545c43b07ab3d58e4d900561.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit ede281d8b794776b545c43b07ab3d58e4d900561
parent 30f36a0b511561736809b18f25eb025ccd815c53
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 22:38:03 +0900

Add alternative CloudKit path for notified changes

</content>
</entry>
<entry>
<id>30f36a0b511561736809b18f25eb025ccd815c53</id>
<published>2026-05-08T13:25:14Z</published>
<updated>2026-05-08T13:25:14Z</updated>
<title type="text">Use more targeted CloudKit fetches</title>
<link rel="alternate" type="text/html" href="commit/30f36a0b511561736809b18f25eb025ccd815c53.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 30f36a0b511561736809b18f25eb025ccd815c53
parent 452f858ec29608bf522482d1eca19c694b9ca907
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 22:25:14 +0900

Use more targeted CloudKit fetches

</content>
</entry>
<entry>
<id>452f858ec29608bf522482d1eca19c694b9ca907</id>
<published>2026-05-08T13:09:50Z</published>
<updated>2026-05-08T13:09:50Z</updated>
<title type="text">Fetch current puzzle zone on push</title>
<link rel="alternate" type="text/html" href="commit/452f858ec29608bf522482d1eca19c694b9ca907.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 452f858ec29608bf522482d1eca19c694b9ca907
parent 4208cbfcd4d46501ba9fbb51cba61f04fc884cd2
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 22:09:50 +0900

Fetch current puzzle zone on push

Remote notifications were waking the receiver, but the generic CKSyncEngine
fetch could still leave the open puzzle stale until a later foreground sync or
poll.

This commit targets the push-triggered fetch when a puzzle is already open.
AppServices now derives the current game’s database scope and zone ID, then
asks SyncEngine to fetch that zone immediately using
CKSyncEngine.FetchChangesOptions. If no current puzzle can be targeted, the
existing broad fetch remains the fallback.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>4208cbfcd4d46501ba9fbb51cba61f04fc884cd2</id>
<published>2026-05-08T12:39:32Z</published>
<updated>2026-05-08T12:39:32Z</updated>
<title type="text">Create push subscriptions expressly</title>
<link rel="alternate" type="text/html" href="commit/4208cbfcd4d46501ba9fbb51cba61f04fc884cd2.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 4208cbfcd4d46501ba9fbb51cba61f04fc884cd2
parent fa22f91fc24578e27a065075b8ce09ea075c9de8
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 21:39:32 +0900

Create push subscriptions expressly

</content>
</entry>
<entry>
<id>fa22f91fc24578e27a065075b8ce09ea075c9de8</id>
<published>2026-05-08T12:29:08Z</published>
<updated>2026-05-08T12:29:08Z</updated>
<title type="text">Add more instrumentation to push notifications</title>
<link rel="alternate" type="text/html" href="commit/fa22f91fc24578e27a065075b8ce09ea075c9de8.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit fa22f91fc24578e27a065075b8ce09ea075c9de8
parent da5eacca4a39307d777fd5e29038e7dd513d9c05
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 21:29:08 +0900

Add more instrumentation to push notifications

</content>
</entry>
<entry>
<id>da5eacca4a39307d777fd5e29038e7dd513d9c05</id>
<published>2026-05-08T12:12:16Z</published>
<updated>2026-05-08T12:12:16Z</updated>
<title type="text">Set APN environment to production</title>
<link rel="alternate" type="text/html" href="commit/da5eacca4a39307d777fd5e29038e7dd513d9c05.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit da5eacca4a39307d777fd5e29038e7dd513d9c05
parent 740860632cd6af9d83571899ee8f6774b84f8741
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 21:12:16 +0900

Set APN environment to production

</content>
</entry>
<entry>
<id>740860632cd6af9d83571899ee8f6774b84f8741</id>
<published>2026-05-08T11:37:20Z</published>
<updated>2026-05-08T11:37:20Z</updated>
<title type="text">Instrument notification code path</title>
<link rel="alternate" type="text/html" href="commit/740860632cd6af9d83571899ee8f6774b84f8741.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 740860632cd6af9d83571899ee8f6774b84f8741
parent b2b5b6719e991d5532878d4936e55960b82c6d39
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 20:37:20 +0900

Instrument notification code path

</content>
</entry>
<entry>
<id>b2b5b6719e991d5532878d4936e55960b82c6d39</id>
<published>2026-05-08T07:54:34Z</published>
<updated>2026-05-08T07:54:34Z</updated>
<title type="text">Push Moves immediately after debounced flush</title>
<link rel="alternate" type="text/html" href="commit/b2b5b6719e991d5532878d4936e55960b82c6d39.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit b2b5b6719e991d5532878d4936e55960b82c6d39
parent 122cc21b2b95ef939cb18ac5fd02d37b93a574c0
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 16:54:34 +0900

Push Moves immediately after debounced flush

Cross-device sync felt sluggish after the moves-architecture rewrite in 122cc21
even though far fewer records were going over the wire. With the previous
design, every cell edit produced a distinct CKRecord.ID in
pendingRecordZoneChanges, so the framework&#39;s scheduler almost always saw fresh
work and shipped it quickly. Under the new one-record-per-device model, every
keystroke for a given (game, authorID, deviceID) targets the same CKRecord.ID.
Repeated state.add calls collapse onto the already-queued intent and
CKSyncEngine&#39;s scheduler is free to sit on the change for a while before
deciding to send.

This commit adds a direct state.add at the two call sites
(enqueueMoves and enqueuePlayerRecord) and, after enqueueMoves finishes
staging, kicks the affected engines with sendChanges() via fire-and-forget
Tasks. MovesUpdater already debounces at 1500ms, so the kick fires at most once
per typing burst per game; the framework internally serialises concurrent
sends, so back-to-back kicks are safe.  The existing
duplicateMoveEnqueueIsDeduped test continues to pass, which confirms
CKSyncEngine dedupes pendingRecordZoneChanges by record ID on its own.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>122cc21b2b95ef939cb18ac5fd02d37b93a574c0</id>
<published>2026-05-08T03:31:04Z</published>
<updated>2026-05-08T03:31:04Z</updated>
<title type="text">Rewrite sync approach</title>
<link rel="alternate" type="text/html" href="commit/122cc21b2b95ef939cb18ac5fd02d37b93a574c0.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 122cc21b2b95ef939cb18ac5fd02d37b93a574c0
parent 89bdb89447837912e1f48d8633f2977f7a4e088d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 12:31:04 +0900

Rewrite sync approach

This commit introduces a completely reworked sync approach for game
activity. The previous approach involved recording the individual moves
each player made. These were synchronised to each player and device. In
a large game, this could create numerous records. There was a plan to
rationalise these at various points (e.g. end of game) but there were
synchronisation issues with this as well as potential loss of useful
information.

The new approach is for each player and device to have its own moves
log. Moves made by that player on that device update only that log. The
log is shared between players who use a reconciliation process to
produce the overall game state. The current state of each individual log
is &#39;cached&#39; in the log so that a user can quickly reproduce the current
game state.

In conjunction with this, a new version of the database container has
been added (iCloud.net.inqk.crossmate.v3).

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>89bdb89447837912e1f48d8633f2977f7a4e088d</id>
<published>2026-05-07T22:40:02Z</published>
<updated>2026-05-07T22:40:02Z</updated>
<title type="text">Stop CKSyncEngine from looping on ZoneNotFound</title>
<link rel="alternate" type="text/html" href="commit/89bdb89447837912e1f48d8633f2977f7a4e088d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 89bdb89447837912e1f48d8633f2977f7a4e088d
parent b099dd7c8eda40e10d1f96c9624dbc72eb2e4d4d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 07:40:02 +0900

Stop CKSyncEngine from looping on ZoneNotFound

Prior to this commit, it is possible for the shared-DB engine to repeatedly
re-push player-...-_localAuthor with CKError zoneNotFound (26/2036), keeping
&#39;Last Error: Failed to send changes&#39; stuck on the diagnostics screen across
different cycles.  Because handleSentRecordZoneChanges only logged failed saves
and tried recoverServerChangedSave, the pending change stayed in CKSyncEngine&#39;s
queue and is retried on every send.

This commit teaches handleSentRecordZoneChanges to recognise per-record
.zoneNotFound failures and route them to a new applyZoneOrphaning helper:

1. Pending record-zone changes (saveRecord and deleteRecord) targeting the
   orphaned zone are removed from the engine&#39;s state so the framework stops
   retrying them.
2. In-memory pendingPings entries for the affected game are dropped so they do
   not get rebuilt against the dead zone.
3. The local GameEntity is hard-deleted on the private engine and marked
   isAccessRevoked on the shared engine, mirroring the split landed for
   fetch-side zone deletions in 04bfdfd.
4. onGameRemoved / onGameAccessRevoked callbacks fire so GameStore can clear the
   open puzzle if it was the one that just disappeared. The revoked callback is
   suppressed when the entity was already revoked, to avoid double-firing during
   steady-state pushes.

ZoneOrphaningTests covers the private hard-delete, shared revocation,
already-revoked idempotency, and unknown-zone no-op paths. applyZoneOrphaning
is internal rather than private because CKSyncEngine.Event payloads have no
public initialiser, so the test cannot drive handleSentRecordZoneChanges
end-to-end.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>b099dd7c8eda40e10d1f96c9624dbc72eb2e4d4d</id>
<published>2026-05-07T22:39:09Z</published>
<updated>2026-05-07T22:39:09Z</updated>
<title type="text">Add testing instructions to AGENTS.md</title>
<link rel="alternate" type="text/html" href="commit/b099dd7c8eda40e10d1f96c9624dbc72eb2e4d4d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit b099dd7c8eda40e10d1f96c9624dbc72eb2e4d4d
parent 04bfdfdb295bf6aa66e4af362782b2d1f2b57b2f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 07:39:09 +0900

Add testing instructions to AGENTS.md

</content>
</entry>
<entry>
<id>04bfdfdb295bf6aa66e4af362782b2d1f2b57b2f</id>
<published>2026-05-07T21:11:09Z</published>
<updated>2026-05-07T21:11:09Z</updated>
<title type="text">Apply private-DB zone deletions on receiving devices</title>
<link rel="alternate" type="text/html" href="commit/04bfdfdb295bf6aa66e4af362782b2d1f2b57b2f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 04bfdfdb295bf6aa66e4af362782b2d1f2b57b2f
parent 76a6790f7915798779e66a04c63c8a1efe195bc5
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 06:11:09 +0900

Apply private-DB zone deletions on receiving devices

Diagnostics from a two-device session showed the receiving device fetching the
zone deletions, logging &#39;private db changes [src=push]: 4 zone mods, 2 zone
deletions&#39;, and then doing nothing with them. handleFetchedDatabaseChanges
bailed out early for the private engine, so the matching GameEntity rows
lingered with no remote data and the deletion never propagated.

This commit replaces the early return with a split:

1. Private-DB zone deletions delete the local GameEntity (cascade rules handle
   child Move, Snapshot, Player, and Cell rows) and a new onGameRemoved callback
   lets GameStore clear currentEntity / currentMutator / currentGame if the open
   puzzle is the one that just disappeared.
2. Shared-DB zone deletions keep their existing access-revoked path, since &#39;the
   owner removed our access&#39; warrants a different UI affordance than &#39;the user
   deleted their own game on another device&#39;.

This commit also promotes the remaining `try? ctx.save()` calls inside the
fetched- changes handlers to logged catches. CKSyncEngine advances its change
token whenever the delegate returns from a fetched-record-zone-changes event,
regardless of whether we persisted anything; a silent save failure there means
the records are gone from the engine&#39;s &#39;to deliver&#39; set and won&#39;t come back
without a manual resetSyncState. Surfacing the failure is the only way to
notice that drift if it ever recurs.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>76a6790f7915798779e66a04c63c8a1efe195bc5</id>
<published>2026-05-07T20:31:11Z</published>
<updated>2026-05-07T20:31:11Z</updated>
<title type="text">Recover from CloudKit etag drift on Player and Move records</title>
<link rel="alternate" type="text/html" href="commit/76a6790f7915798779e66a04c63c8a1efe195bc5.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 76a6790f7915798779e66a04c63c8a1efe195bc5
parent 5aad0672b4f04e8cdd2bcb66d88baf8d4c9cb02f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 05:31:11 +0900

Recover from CloudKit etag drift on Player and Move records

Diagnostics from a shared-game session showed the owner&#39;s Player record locked
at clientEtag c5 while the server had advanced to c7, producing a stream of
OpLockFailed retries. Move records hit the same problem (&#39;record to insert
already exists&#39; against a local row whose ckSystemFields was still nil) which
enqueueUnconfirmedMoves then resurrected on every push.

The root cause is that PresencePublisher, NameBroadcaster, applyPlayerRecord
and writeBackSystemFields all run on background contexts with no merge policy.
The default NSErrorMergePolicy throws on conflict, and the `try? ctx.save()` at
the end of each block swallowed the error, so the system-fields writeback was
silently dropped whenever a concurrent write to the same row had landed in
between.

This commit sets mergePolicy = .mergeByPropertyObjectTrump on the four
sync-side background contexts so concurrent edits to the same row coexist and
writeback persists. The silent ctx.save inside handleSentRecordZoneChanges is
promoted to a logged failure so future drift is visible if it sneaks back in.

This commit also adds recoverServerChangedSave so that when CloudKit returns
serverRecordChanged with an attached server record, the local entity adopts
that record&#39;s system fields. The next retry then uses a fresh change tag while
still pushing the local values the user wanted to save. This covers both the
stale-etag update path and the already-exists duplicate-create path observed
for Move records.

Finally, a timestamp guard is added in applyPlayerRecord. Fetched server
records still always refresh ckSystemFields, but the name/selection/updatedAt
fields are only adopted when the incoming updatedAt is at least as recent as
the local one — otherwise an in-flight fetch could clobber the user&#39;s live
cursor with a stale server position on every poll.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;
Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>5aad0672b4f04e8cdd2bcb66d88baf8d4c9cb02f</id>
<published>2026-05-07T20:30:29Z</published>
<updated>2026-05-07T20:30:29Z</updated>
<title type="text">Add basic AGENTS.md file</title>
<link rel="alternate" type="text/html" href="commit/5aad0672b4f04e8cdd2bcb66d88baf8d4c9cb02f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5aad0672b4f04e8cdd2bcb66d88baf8d4c9cb02f
parent fff7c0e5a6d66d0d718dcf43476108c5798bb01d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Fri,  8 May 2026 05:30:29 +0900

Add basic AGENTS.md file

</content>
</entry>
<entry>
<id>fff7c0e5a6d66d0d718dcf43476108c5798bb01d</id>
<published>2026-05-07T10:26:47Z</published>
<updated>2026-05-07T10:26:47Z</updated>
<title type="text">Add initial 10 bundled puzzles</title>
<link rel="alternate" type="text/html" href="commit/fff7c0e5a6d66d0d718dcf43476108c5798bb01d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit fff7c0e5a6d66d0d718dcf43476108c5798bb01d
parent 86820a2aa18c3039a80113013f43a638e7c2758d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 19:26:47 +0900

Add initial 10 bundled puzzles

</content>
</entry>
<entry>
<id>86820a2aa18c3039a80113013f43a638e7c2758d</id>
<published>2026-05-07T09:49:13Z</published>
<updated>2026-05-07T09:49:13Z</updated>
<title type="text">Move debug puzzles to new top-level directory</title>
<link rel="alternate" type="text/html" href="commit/86820a2aa18c3039a80113013f43a638e7c2758d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 86820a2aa18c3039a80113013f43a638e7c2758d
parent 8062849feb3bcefef09c9dcf3122da39e22b1755
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 18:49:13 +0900

Move debug puzzles to new top-level directory

</content>
</entry>
<entry>
<id>8062849feb3bcefef09c9dcf3122da39e22b1755</id>
<published>2026-05-07T08:09:20Z</published>
<updated>2026-05-07T08:09:20Z</updated>
<title type="text">Support Across Lite puzzle import</title>
<link rel="alternate" type="text/html" href="commit/8062849feb3bcefef09c9dcf3122da39e22b1755.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 8062849feb3bcefef09c9dcf3122da39e22b1755
parent eeaa7d15015c74f63cd0fe0faf3ab030a43b6222
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 17:09:20 +0900

Support Across Lite puzzle import

This commit adds an Across Lite file format (i.e. .puz) converter that reads
the binary header, solution grid, metadata, clue table, circled-cell extension,
and rebus extensions, then emits the same .xd source format the rest of the app
already stores and syncs.

The import paths now accept both .xd and .puz files, including direct document
opens and files listed from the iCloud Drive import folder. The app&#39;s document
type declarations and file importer content types include the Across Lite UTI.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>eeaa7d15015c74f63cd0fe0faf3ab030a43b6222</id>
<published>2026-05-07T07:15:59Z</published>
<updated>2026-05-07T07:15:59Z</updated>
<title type="text">Add external sources section to Settings</title>
<link rel="alternate" type="text/html" href="commit/eeaa7d15015c74f63cd0fe0faf3ab030a43b6222.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit eeaa7d15015c74f63cd0fe0faf3ab030a43b6222
parent dce12946624fd3c3e7d41785497efe032febc1a8
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 16:15:59 +0900

Add external sources section to Settings

</content>
</entry>
<entry>
<id>dce12946624fd3c3e7d41785497efe032febc1a8</id>
<published>2026-05-07T05:36:35Z</published>
<updated>2026-05-07T05:36:35Z</updated>
<title type="text">Fetch account e-mail through GraphQL profile</title>
<link rel="alternate" type="text/html" href="commit/dce12946624fd3c3e7d41785497efe032febc1a8.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit dce12946624fd3c3e7d41785497efe032febc1a8
parent 155be03f2175684e59741415de897d959eab6e9c
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 14:36:35 +0900

Fetch account e-mail through GraphQL profile

The previous sign-in flow for the NYT tried to read the user&#39;s email from a
previously supported URL. That endpoint now returns 404 with an NYT-S-only
session, while the crossword puzzle endpoints still accept the same cookie.

This commit switches email discovery to the account page&#39;s GraphQL path.
Signed-in sessions no longer require an email value to be stored. If NYT does
not expose the email, Settings shows a generic signed-in status instead of the
misleading &quot;NYT User&quot; fallback.

NYTAuthServiceTests cover GraphQL email decoding, ignoring unrelated
email-looking text, missing-email responses and account-page GraphQL config
extraction.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>155be03f2175684e59741415de897d959eab6e9c</id>
<published>2026-05-07T04:41:43Z</published>
<updated>2026-05-07T04:41:43Z</updated>
<title type="text">Add About screen to Settings</title>
<link rel="alternate" type="text/html" href="commit/155be03f2175684e59741415de897d959eab6e9c.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 155be03f2175684e59741415de897d959eab6e9c
parent 98a6960605bc992ac070d7dae996293d0c881e83
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 13:41:43 +0900

Add About screen to Settings

</content>
</entry>
<entry>
<id>98a6960605bc992ac070d7dae996293d0c881e83</id>
<published>2026-05-06T23:21:18Z</published>
<updated>2026-05-06T23:21:18Z</updated>
<title type="text">Fix silent record drops in inbound CloudKit sync</title>
<link rel="alternate" type="text/html" href="commit/98a6960605bc992ac070d7dae996293d0c881e83.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 98a6960605bc992ac070d7dae996293d0c881e83
parent a65825bc8d781fc2ccce66b7ee93d6af18dff63a
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 08:21:18 +0900

Fix silent record drops in inbound CloudKit sync

Prior to this commit, two defects in the inbound sync path could leave a device
permanently missing records while still reporting &#39;Pending Changes: 0&#39;:

1. Resetting sync state only cleared the persisted
   CKSyncEngine.State.Serialization in Core Data; the running engines kept their
   tokens in memory, and the next stateUpdate event re-saved those tokens back
   into the cleared field. Force-quitting between reset and the next sync did
   not help because the rewrite ran on the same foreground tick.

2. The functions applyMoveRecord, applyPlayerRecord, and applySnapshotRecord
   guard-returned when the parent GameEntity was not yet in Core Data, but
   CKSyncEngine paginates the initial pull of a fresh device across multiple
   FetchedRecordZoneChanges events with no guarantee that a zone&#39;s Game record
   arrives in the first batch — any Move, Snapshot, or Player record processed
   before its Game record was silently dropped while the engine still advanced
   its server change token, leaving the missing rows invisible to subsequent
   incremental fetches.

This commit makes resetSyncState replace the in-memory privateEngine and
sharedEngine instances in addition to clearing the persisted state, so the new
engines start with no tokens, walk every zone from scratch on the next fetch,
and re-enqueue locally-unconfirmed moves via the existing
enqueueUnconfirmedMoves path. pendingPings and the loggedFirstSharedPushPayload
flag are reset in the same step so the post-reset engine state matches a cold
start.

For the apply path, a new RecordSerializer.ensureGameEntity helper fetches
GameEntity by ckRecordName and creates an unpopulated stub if none is found,
with id / zone / scope derivable from the inbound record&#39;s gameID and zoneID.
The stub uses empty title and puzzleSource so GameSummary.init? filters it out
of the library until applyGameRecord arrives with the real metadata and updates
the same row by ckRecordName. The three apply functions now route through
ensureGameEntity, eliminating the guard-return and the record.parent reference
fallback that was redundant with the parsed gameID. EnsureGameEntityTests pins
the invariant: stub creation, no duplicates on repeated calls, scope derived
from zone owner, and follow-up applyGameRecord matching the stub by
ckRecordName.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>a65825bc8d781fc2ccce66b7ee93d6af18dff63a</id>
<published>2026-05-06T21:58:18Z</published>
<updated>2026-05-06T21:58:18Z</updated>
<title type="text">Replace in-memory engines on reset sync state</title>
<link rel="alternate" type="text/html" href="commit/a65825bc8d781fc2ccce66b7ee93d6af18dff63a.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a65825bc8d781fc2ccce66b7ee93d6af18dff63a
parent e3b9b5effc7c5ab89b32ada395f0fbb2e23855ff
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 06:58:18 +0900

Replace in-memory engines on reset sync state

</content>
</entry>
<entry>
<id>e3b9b5effc7c5ab89b32ada395f0fbb2e23855ff</id>
<published>2026-05-06T19:31:14Z</published>
<updated>2026-05-06T19:31:14Z</updated>
<title type="text">Propagate ckShareRecordName to other owner-devices</title>
<link rel="alternate" type="text/html" href="commit/e3b9b5effc7c5ab89b32ada395f0fbb2e23855ff.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e3b9b5effc7c5ab89b32ada395f0fbb2e23855ff
parent 16bbcad048c21630c238862693219dc5d73f2103
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 04:31:14 +0900

Propagate ckShareRecordName to other owner-devices

Prior to this commit, ckShareRecordName was a local-only field set on the
GameEntity of the device that ran createShareLink and never serialized into the
Game CKRecord. As a result, an owner playing the same shared game on a second
device (iPad created the share, iPhone has the game synced) saw isShared
evaluate to false in GameStore since ckShareRecordName was nil locally and
databaseScope was 0 (the zone still lives in the owner&#39;s private database).
This suppressed the share badge, the PlayerRoster passed to GridView, and
therefore author tinting and the players panel.

This commit adds shareRecordName to the Game CKRecord so the marker round
trips between owner devices: RecordSerializer.populateGameRecord now writes
entity.ckShareRecordName, applyGameRecord reads it back conservatively
(missing-field reads do not clobber a locally-set value), and
ShareController.persistShareName is async so it can call
syncEngine.enqueueGame after the local save and force a Game record push.
The Game record type needs a shareRecordName String field added manually in
the iCloud Dashboard before the field can be written.

Backfilling games whose share was created before this change required CloudKit
Console edits to the owner&#39;s private database, which are not available from
non-developer Apple IDs. To work around this, a generic record editor has been
added under Settings → Debugging → CloudKit Record. It fetches by (scope, zone,
owner, record name), displays every field with its type, lets the user edit
String fields in place or add a new String field, and saves through CKDatabase.
After the save, the engine re-runs fetchChanges so any locally-tracked entity
picks up the new server change tag rather than going stale. Two new methods on
SyncEngine, fetchRecordForEdit and saveRecordForEdit, keep the CKContainer
reference inside the actor.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>16bbcad048c21630c238862693219dc5d73f2103</id>
<published>2026-05-06T18:30:37Z</published>
<updated>2026-05-06T18:30:37Z</updated>
<title type="text">Fix XD accepted-answer rebus completion</title>
<link rel="alternate" type="text/html" href="commit/16bbcad048c21630c238862693219dc5d73f2103.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 16bbcad048c21630c238862693219dc5d73f2103
parent cb8e7ee0e14c2f4cfcba8038bbc3ddd1fce7009a
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 03:30:37 +0900

Fix XD accepted-answer rebus completion

Regeneration of the project files had been overlooked in prior commits and
tests were not running. After remedying this error, certain XD-related tests
were not passing.

This commit tightens XD accepted-answer handling for rebus cells whose clue
answer is represented by a single grid cell. When a clue’s declared answer
matches that cell’s solution, its ^Accept: metadata is now applied directly to
the cell, including normalized and escaped alternatives.

Completion counting now ignores cells without known solutions, which keeps
multi-cell rebus fixtures from requiring filler cells that are not part of the
declared answer while preserving normal grid completion behaviour.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>cb8e7ee0e14c2f4cfcba8038bbc3ddd1fce7009a</id>
<published>2026-05-06T17:51:09Z</published>
<updated>2026-05-06T17:51:09Z</updated>
<title type="text">Update text in notification messages</title>
<link rel="alternate" type="text/html" href="commit/cb8e7ee0e14c2f4cfcba8038bbc3ddd1fce7009a.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit cb8e7ee0e14c2f4cfcba8038bbc3ddd1fce7009a
parent 383f9235f07e0ede550d3b9edb1d064477c38d7b
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 02:51:09 +0900

Update text in notification messages

</content>
</entry>
<entry>
<id>383f9235f07e0ede550d3b9edb1d064477c38d7b</id>
<published>2026-05-06T17:38:42Z</published>
<updated>2026-05-06T17:38:42Z</updated>
<title type="text">Tweak count on in-progress scoreboard on iPadOS</title>
<link rel="alternate" type="text/html" href="commit/383f9235f07e0ede550d3b9edb1d064477c38d7b.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 383f9235f07e0ede550d3b9edb1d064477c38d7b
parent c7e50b39faa287bd3ac144d5da5fa187fe9e6f5d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 02:38:42 +0900

Tweak count on in-progress scoreboard on iPadOS

</content>
</entry>
<entry>
<id>c7e50b39faa287bd3ac144d5da5fa187fe9e6f5d</id>
<published>2026-05-06T17:34:50Z</published>
<updated>2026-05-06T17:34:50Z</updated>
<title type="text">Improve final scoreboard display on iPadOS</title>
<link rel="alternate" type="text/html" href="commit/c7e50b39faa287bd3ac144d5da5fa187fe9e6f5d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit c7e50b39faa287bd3ac144d5da5fa187fe9e6f5d
parent b37b245a5f23ff75a392129bfa6247b0316c39c6
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 02:34:50 +0900

Improve final scoreboard display on iPadOS

</content>
</entry>
<entry>
<id>b37b245a5f23ff75a392129bfa6247b0316c39c6</id>
<published>2026-05-06T16:52:47Z</published>
<updated>2026-05-06T16:52:47Z</updated>
<title type="text">Replace clear menu with entry menu</title>
<link rel="alternate" type="text/html" href="commit/b37b245a5f23ff75a392129bfa6247b0316c39c6.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit b37b245a5f23ff75a392129bfa6247b0316c39c6
parent a294cfab29be5c3ffea530c22e05282307e971c6
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 01:52:47 +0900

Replace clear menu with entry menu

</content>
</entry>
<entry>
<id>a294cfab29be5c3ffea530c22e05282307e971c6</id>
<published>2026-05-06T16:22:48Z</published>
<updated>2026-05-06T16:22:48Z</updated>
<title type="text">Fix concurrency warning about isolation</title>
<link rel="alternate" type="text/html" href="commit/a294cfab29be5c3ffea530c22e05282307e971c6.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a294cfab29be5c3ffea530c22e05282307e971c6
parent cfdf5f4db73b4e3b33378e6466d8b4e8cce87417
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 01:22:48 +0900

Fix concurrency warning about isolation

</content>
</entry>
<entry>
<id>cfdf5f4db73b4e3b33378e6466d8b4e8cce87417</id>
<published>2026-05-06T15:51:15Z</published>
<updated>2026-05-06T15:51:15Z</updated>
<title type="text">Support XD clue metadata accepted answers</title>
<link rel="alternate" type="text/html" href="commit/cfdf5f4db73b4e3b33378e6466d8b4e8cce87417.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit cfdf5f4db73b4e3b33378e6466d8b4e8cce87417
parent d82f0f161206435992ecb542f9e378dece34d17a
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Thu,  7 May 2026 00:51:15 +0900

Support XD clue metadata accepted answers

This commit adapts the XD parser to handle clue metadata lines generically.
The ^Accept: field is treated as metadata-derived accepted answers while
leaving the canonical answer from the clue unchanged.

In conjunction with this, conversion from NYT&#39;s JSON format to Crossmate&#39;s XD
format now emits ^Accept: metadata for cells with moreAnswers, and answer
validation uses accepted answers when checking entries, completion, reveals,
and success-panel contribution counts.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>d82f0f161206435992ecb542f9e378dece34d17a</id>
<published>2026-05-06T14:03:47Z</published>
<updated>2026-05-06T14:03:47Z</updated>
<title type="text">Support additional iPad orientation</title>
<link rel="alternate" type="text/html" href="commit/d82f0f161206435992ecb542f9e378dece34d17a.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit d82f0f161206435992ecb542f9e378dece34d17a
parent 7263ae381f48c3cd1e601db57ae8a813b9f45baf
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 23:03:47 +0900

Support additional iPad orientation

</content>
</entry>
<entry>
<id>7263ae381f48c3cd1e601db57ae8a813b9f45baf</id>
<published>2026-05-06T13:56:05Z</published>
<updated>2026-05-06T13:56:05Z</updated>
<title type="text">Change swipe gesture to change cursor axis to tap gesture</title>
<link rel="alternate" type="text/html" href="commit/7263ae381f48c3cd1e601db57ae8a813b9f45baf.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 7263ae381f48c3cd1e601db57ae8a813b9f45baf
parent 7feb9d2759fe22b32fb412c106b63fdc58ed51f0
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 22:56:05 +0900

Change swipe gesture to change cursor axis to tap gesture

</content>
</entry>
<entry>
<id>7feb9d2759fe22b32fb412c106b63fdc58ed51f0</id>
<published>2026-05-06T13:17:56Z</published>
<updated>2026-05-06T13:17:56Z</updated>
<title type="text">Add iPad portrait layout</title>
<link rel="alternate" type="text/html" href="commit/7feb9d2759fe22b32fb412c106b63fdc58ed51f0.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 7feb9d2759fe22b32fb412c106b63fdc58ed51f0
parent 89c6eb043f7504a4c9f5ce372dad5e1e47771953
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 22:17:56 +0900

Add iPad portrait layout

This commit adds a dedicated portrait arrangement to the iPad layout: the grid
occupies the top three-quarters, with the scoreboard and clue list sitting
side-by-side in the bottom quarter, and the same navigation-keys keyboard used
in landscape pinned to the bottom. The clue bar is dropped because the full
clue list is always on screen.

The 3:1 vertical split between grid and bottom pane is implemented as a small
WeightedVStack Layout that proposes proportional heights to its children, since
SwiftUI does not offer a built-in ratioed split for VStack. A PadLayout enum
(.landscape, .portrait, .none) replaces the previous isLandscapePad bool, and
pad-vs-phone selection moves from a UIDevice.orientationDidChangeNotification
observer reading UIWindowScene.effectiveGeometry to an onGeometryChange on the
puzzle view.

The sidebar clue list now scrolls the selected clue to the vertical centre
when currentID changes, matching the behaviour of the sheet presentation.
currentID only changes on clue or direction transitions, so typing within
a word does not retrigger the scroll.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>89c6eb043f7504a4c9f5ce372dad5e1e47771953</id>
<published>2026-05-06T09:17:54Z</published>
<updated>2026-05-06T09:17:54Z</updated>
<title type="text">Add external keyboard input handling</title>
<link rel="alternate" type="text/html" href="commit/89c6eb043f7504a4c9f5ce372dad5e1e47771953.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 89c6eb043f7504a4c9f5ce372dad5e1e47771953
parent 8793ab064ba240a07f857b77af9730365590d944
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 18:17:54 +0900

Add external keyboard input handling

This commit adds support for hardware key presses from the puzzle view and
route them through the existing PlayerSession actions for letters, delete,
arrows, tab, space, return, escape, and command-left/right navigation.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>8793ab064ba240a07f857b77af9730365590d944</id>
<published>2026-05-06T04:23:01Z</published>
<updated>2026-05-06T04:23:01Z</updated>
<title type="text">Add initial iPad layout</title>
<link rel="alternate" type="text/html" href="commit/8793ab064ba240a07f857b77af9730365590d944.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 8793ab064ba240a07f857b77af9730365590d944
parent 5401155b14fba730c8b6216bab2190d0d951c5ab
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 13:23:01 +0900

Add initial iPad layout

This adds a basic layout the iPad. It is... (wait for it) oriented
around landscape use.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>5401155b14fba730c8b6216bab2190d0d951c5ab</id>
<published>2026-05-06T01:20:46Z</published>
<updated>2026-05-06T01:20:46Z</updated>
<title type="text">Add keyboard swipe to set entry direction</title>
<link rel="alternate" type="text/html" href="commit/5401155b14fba730c8b6216bab2190d0d951c5ab.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5401155b14fba730c8b6216bab2190d0d951c5ab
parent 5a248e38bc1e6c397a5f9a9aaa5826a127a6aeac
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 10:20:46 +0900

Add keyboard swipe to set entry direction

This commit adds a swipe gesture on the keyboard view: a predominantly
horizontal swipe sets direction to .across, a predominantly vertical swipe sets
it to .down. The gesture is attached as a simultaneousGesture on top of a
DragGesture with a 20pt minimum distance, so it coexists with the existing key
button taps without intercepting them, and a stray finger movement during a tap
will not trip it.

PlayerSession gains setDirection(_:), a sibling to toggleDirection that applies
the same guard (the new direction is only adopted if a word exists in that
direction at the current cell) and no-ops if the requested direction already
matches the current one.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>5a248e38bc1e6c397a5f9a9aaa5826a127a6aeac</id>
<published>2026-05-06T00:07:39Z</published>
<updated>2026-05-06T00:07:39Z</updated>
<title type="text">Preserve cell author on same-letter rewrites and reveal-of-correct</title>
<link rel="alternate" type="text/html" href="commit/5a248e38bc1e6c397a5f9a9aaa5826a127a6aeac.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5a248e38bc1e6c397a5f9a9aaa5826a127a6aeac
parent 6756becfdf92f1eca0a34803922693415c36e12d
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 09:07:39 +0900

Preserve cell author on same-letter rewrites and reveal-of-correct

Prior to this commit, every call to Game.setLetter unconditionally overwrote
the square&#39;s letterAuthorID with the acting user, and GameMutator.emitMove
attached the acting user&#39;s authorID to every emitted move. As a result, typing
across a crossing word that was already filled in by a collaborator
reattributed each shared square to the second typist, and tapping Reveal on a
correctly-filled square broadcast a move authored by the revealer even though
Game.revealCells already left the cell&#39;s contents untouched.

This commit makes Game.setLetter preserve letterAuthorID when the new letter
matches the existing entry (after the same case-normalisation the entry write
already does) so the mark can still toggle between pen and pencil but the
author is left alone. GameMutator.emitMove now reads the cell-effective author
from square.letterAuthorID rather than from the authorIDProvider, so any
preservation done by Game flows through to the persisted MoveEntity and
CellEntity.

To keep session-ping presence accurate, MoveBuffer.enqueue gains an optional
actingAuthorID parameter; the cell-effective authorID is still what gets
written to MoveEntity/CellEntity, but maybeFireSessionPing now fires on the
acting user.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>6756becfdf92f1eca0a34803922693415c36e12d</id>
<published>2026-05-05T22:16:58Z</published>
<updated>2026-05-05T22:16:58Z</updated>
<title type="text">Expand scope of notifications of collaborator actions</title>
<link rel="alternate" type="text/html" href="commit/6756becfdf92f1eca0a34803922693415c36e12d.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 6756becfdf92f1eca0a34803922693415c36e12d
parent 0812dac2de826334bc3c6fb64104ba42412f3b84
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 07:16:58 +0900

Expand scope of notifications of collaborator actions

Prior to this commit, the only collaborator notification was a generic
session-start ping (&#39;X has updated the puzzle Y&#39;) fired by MoveBuffer after a
30-minute idle gap, with the receiver further suppressing repeats inside a
two-hour window.

This commit broadens the system to cover four additional events (collaborator
joining a shared game, solving the puzzle, checking an answer, and revealing an
answer) and tailors the body copy of each. The session ping is retained but its
idle threshold is shortened to twenty minutes, and the receiver-side dedup
window is shortened to match so the sender&#39;s gating dominates.

The CloudKit record type is generalised: SessionPing is renamed to Ping, gains
kind and scope fields, and the record-name prefix moves from sessionping- to
ping-. The new check/reveal events are emitted from PlayerSession with a scope
of square, word, or puzzle; win is emitted on the .solved completion-state
transition (seeded at construction so opening an already-solved puzzle does not
falsely fire); and join is emitted by the joiner once share acceptance has
fetched the shared zone.

Suppression rules are split: active-puzzle suppression applies to all ping
kinds (no notifications for the puzzle the user is already in), while the dedup
window applies only to session pings. The foreground willPresent handler reads
the ping kind from userInfo so a non-session ping cannot extend the session
dedup window.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>0812dac2de826334bc3c6fb64104ba42412f3b84</id>
<published>2026-05-05T15:19:42Z</published>
<updated>2026-05-05T15:19:42Z</updated>
<title type="text">Make GameStore collaborators required at construction</title>
<link rel="alternate" type="text/html" href="commit/0812dac2de826334bc3c6fb64104ba42412f3b84.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 0812dac2de826334bc3c6fb64104ba42412f3b84
parent 24ef231551d8a3755259620f735f1e09a25065e1
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Wed,  6 May 2026 00:19:42 +0900

Make GameStore collaborators required at construction

Prior to this commit, GameStore exposed five late-wired Optional properties
(moveBuffer, authorIDProvider, onGameCreated, onGameUpdated, and onGameDeleted)
that AppServices assigned after construction. The outer Optional carried no
semantic meaning: in production the closures were always non-nil, and the
absent case existed only as an artifact of construction order.

The blocker for making them required init parameters was a cycle
between MoveBuffer and GameStore: MoveBuffer&#39;s afterFlush called
store.createSnapshotsIfNeeded(for:), so the buffer needed the store at
construction, but the store now wanted the buffer at construction too.

This commit extracts the snapshot lifecycle (createSnapshotsIfNeeded,
pruneMoves, and the pruneDurableSnapshots/usesSharedSync helpers) out of
GameStore into a new SnapshotService that only depends on
PersistenceController. MoveBuffer&#39;s afterFlush captures the service instead of
the store, the cycle disappears, and GameStore.init takes its five
collaborators as non-Optional parameters.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>24ef231551d8a3755259620f735f1e09a25065e1</id>
<published>2026-05-05T11:43:52Z</published>
<updated>2026-05-05T11:43:52Z</updated>
<title type="text">Persist local author identity across launches</title>
<link rel="alternate" type="text/html" href="commit/24ef231551d8a3755259620f735f1e09a25065e1.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 24ef231551d8a3755259620f735f1e09a25065e1
parent 36031858801573ea8a1e5a129e0b1b09db594da4
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Tue,  5 May 2026 20:43:52 +0900

Persist local author identity across launches

Prior to this commit, AuthorIdentity fetched the iCloud user record name
asynchronously at app start and held it only in memory.

As a result, tapping a shared game notification could route into
PlayerRoster.refresh() before the fetch completed, at which point the local
authorID fell back to the empty string. The pre-filtered moveAuthorIDs from
that first phase then survived into the second applyRoster call (after the
share fetch completed and currentID had populated), producing a roster with the
local user&#39;s authorID listed twice and crashing GridView&#39;s
Dictionary(uniqueKeysWithValues:).

This commit caches currentID to UserDefaults so it is non-nil on every launch
after the first successful fetch, and changes PlayerRoster to guard on a known
authorID rather than silently substituting the empty string.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>36031858801573ea8a1e5a129e0b1b09db594da4</id>
<published>2026-05-04T00:36:53Z</published>
<updated>2026-05-04T00:36:53Z</updated>
<title type="text">Ensure notifications display during backgrounding</title>
<link rel="alternate" type="text/html" href="commit/36031858801573ea8a1e5a129e0b1b09db594da4.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 36031858801573ea8a1e5a129e0b1b09db594da4
parent 358f33254ddcbe6be90a7060e0380da6967df2d8
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon,  4 May 2026 09:36:53 +0900

Ensure notifications display during backgrounding

Prior to this commit, the gating logic for notifications would prevent
those appearing for a puzzle that was open when backgrounding occurred.
This commit changes that so that notifications do display.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>358f33254ddcbe6be90a7060e0380da6967df2d8</id>
<published>2026-05-04T00:01:08Z</published>
<updated>2026-05-04T00:01:08Z</updated>
<title type="text">Prevent author tinting on empty squares</title>
<link rel="alternate" type="text/html" href="commit/358f33254ddcbe6be90a7060e0380da6967df2d8.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 358f33254ddcbe6be90a7060e0380da6967df2d8
parent 60341428a21b266c074bb5753bdc16234961387e
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon,  4 May 2026 09:01:08 +0900

Prevent author tinting on empty squares

</content>
</entry>
<entry>
<id>60341428a21b266c074bb5753bdc16234961387e</id>
<published>2026-05-03T23:54:43Z</published>
<updated>2026-05-03T23:54:43Z</updated>
<title type="text">Tweak behaviour regarding shared games</title>
<link rel="alternate" type="text/html" href="commit/60341428a21b266c074bb5753bdc16234961387e.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 60341428a21b266c074bb5753bdc16234961387e
parent 4fcc40cc30b3e6f30eef6db55fb24752c2fe5a6c
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon,  4 May 2026 08:54:43 +0900

Tweak behaviour regarding shared games

</content>
</entry>
<entry>
<id>4fcc40cc30b3e6f30eef6db55fb24752c2fe5a6c</id>
<published>2026-05-03T23:13:44Z</published>
<updated>2026-05-03T23:13:44Z</updated>
<title type="text">Support author tinting on share</title>
<link rel="alternate" type="text/html" href="commit/4fcc40cc30b3e6f30eef6db55fb24752c2fe5a6c.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 4fcc40cc30b3e6f30eef6db55fb24752c2fe5a6c
parent a516df0db69f0b9c01d5107c25f4be6d8e073405
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon,  4 May 2026 08:13:44 +0900

Support author tinting on share

When a puzzle is shared, this commit updates the currently entered
squares with an author tint.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>a516df0db69f0b9c01d5107c25f4be6d8e073405</id>
<published>2026-05-03T22:58:23Z</published>
<updated>2026-05-03T22:58:23Z</updated>
<title type="text">Update author tints dynamically</title>
<link rel="alternate" type="text/html" href="commit/a516df0db69f0b9c01d5107c25f4be6d8e073405.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a516df0db69f0b9c01d5107c25f4be6d8e073405
parent a8216ec129dd0b1f614bd165d148d595cb474432
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Mon,  4 May 2026 07:58:23 +0900

Update author tints dynamically

This commit tweaks the way that author information is assigned to
squares so that tinting can happen dynamically during gameplay.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>a8216ec129dd0b1f614bd165d148d595cb474432</id>
<published>2026-05-03T05:31:30Z</published>
<updated>2026-05-03T05:31:30Z</updated>
<title type="text">Use a faint background to differentiate authors</title>
<link rel="alternate" type="text/html" href="commit/a8216ec129dd0b1f614bd165d148d595cb474432.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit a8216ec129dd0b1f614bd165d148d595cb474432
parent 18134f4f82ee0cdbe3a1b30210e8881c3e5c9940
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 14:31:30 +0900

Use a faint background to differentiate authors

Colouring the letters was a bit garish. This commit applies a faint tint
to the squares by author.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>18134f4f82ee0cdbe3a1b30210e8881c3e5c9940</id>
<published>2026-05-03T04:20:56Z</published>
<updated>2026-05-03T04:20:56Z</updated>
<title type="text">Add colours to text in shared games</title>
<link rel="alternate" type="text/html" href="commit/18134f4f82ee0cdbe3a1b30210e8881c3e5c9940.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 18134f4f82ee0cdbe3a1b30210e8881c3e5c9940
parent 33cbb14d87ffcb3b0656b9415bb4f0c909c03715
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 13:20:56 +0900

Add colours to text in shared games

In shared games, it can be helpful to see which user entered which
letter. This commit adds this for shared games only.

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>33cbb14d87ffcb3b0656b9415bb4f0c909c03715</id>
<published>2026-05-02T16:54:48Z</published>
<updated>2026-05-02T16:54:48Z</updated>
<title type="text">Avoid using icons in puzzle view menus</title>
<link rel="alternate" type="text/html" href="commit/33cbb14d87ffcb3b0656b9415bb4f0c909c03715.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 33cbb14d87ffcb3b0656b9415bb4f0c909c03715
parent 61e5502e7f82cb7a0e83276d31572f125a11d45f
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 01:54:48 +0900

Avoid using icons in puzzle view menus

</content>
</entry>
<entry>
<id>61e5502e7f82cb7a0e83276d31572f125a11d45f</id>
<published>2026-05-02T16:43:21Z</published>
<updated>2026-05-02T16:43:21Z</updated>
<title type="text">Fix warnings about discarded results</title>
<link rel="alternate" type="text/html" href="commit/61e5502e7f82cb7a0e83276d31572f125a11d45f.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 61e5502e7f82cb7a0e83276d31572f125a11d45f
parent 5b91ff697d378262d83bee39121e8b08a468a9e0
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 01:43:21 +0900

Fix warnings about discarded results

</content>
</entry>
<entry>
<id>5b91ff697d378262d83bee39121e8b08a468a9e0</id>
<published>2026-05-02T16:42:25Z</published>
<updated>2026-05-02T16:42:25Z</updated>
<title type="text">Fix leaving of shared games</title>
<link rel="alternate" type="text/html" href="commit/5b91ff697d378262d83bee39121e8b08a468a9e0.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 5b91ff697d378262d83bee39121e8b08a468a9e0
parent df107211e64fbd73de2375f783c34e85aaaf35cc
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 01:42:25 +0900

Fix leaving of shared games

</content>
</entry>
<entry>
<id>df107211e64fbd73de2375f783c34e85aaaf35cc</id>
<published>2026-05-02T15:28:14Z</published>
<updated>2026-05-02T15:28:14Z</updated>
<title type="text">Improve consistency between game list menu and puzzle menu</title>
<link rel="alternate" type="text/html" href="commit/df107211e64fbd73de2375f783c34e85aaaf35cc.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit df107211e64fbd73de2375f783c34e85aaaf35cc
parent 43ad4137f505113dac43732872cca3ec2c100de4
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 00:28:14 +0900

Improve consistency between game list menu and puzzle menu

This commit attempts to provide consistent actions for puzzles, whether
that be for puzzles viewed in the game list or puzzles viewed in the
puzzle viewer.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>43ad4137f505113dac43732872cca3ec2c100de4</id>
<published>2026-05-02T15:06:45Z</published>
<updated>2026-05-02T15:06:45Z</updated>
<title type="text">Simplify leave/deletion options for shared games</title>
<link rel="alternate" type="text/html" href="commit/43ad4137f505113dac43732872cca3ec2c100de4.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 43ad4137f505113dac43732872cca3ec2c100de4
parent ed7c2f607c5711a56371788b90e61c6d262fbd1c
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sun,  3 May 2026 00:06:45 +0900

Simplify leave/deletion options for shared games

If a shared game has been shared by the user, the user can delete it. If
a shared game has been shared by another user, the user can leave it.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>ed7c2f607c5711a56371788b90e61c6d262fbd1c</id>
<published>2026-05-02T11:29:59Z</published>
<updated>2026-05-02T11:29:59Z</updated>
<title type="text">Unwind changes to reduce CloudKit calls</title>
<link rel="alternate" type="text/html" href="commit/ed7c2f607c5711a56371788b90e61c6d262fbd1c.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit ed7c2f607c5711a56371788b90e61c6d262fbd1c
parent fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 20:29:59 +0900

Unwind changes to reduce CloudKit calls

In a mistaken attempt to address the cause of an unresponsiveness that
was the result of unnecessary parsing, CloudKit calls were reduced. This
has complicated the synchronisation logic. This commit undoes that.

In addition, the snapshot logic can fail because Lamports are
independently assigned per player so can conflict. In shared games,
snapshotting is disabled.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18</id>
<published>2026-05-02T08:25:51Z</published>
<updated>2026-05-02T08:25:51Z</updated>
<title type="text">Remove performance logging</title>
<link rel="alternate" type="text/html" href="commit/fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit fde29e2799e42bd38804e5c99ffa4d7cfa5f6e18
parent ddb3e953d67455b295bd929595e742c18f25524b
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 17:25:51 +0900

Remove performance logging

At the risk of tempting the universe, this commit preemptively
removes the performance logging. A debugging device is now available
that makes this less necessary.

Co-Authored-By: Codex GPT 5.5 &lt;codex@openai.com&gt;

</content>
</entry>
<entry>
<id>ddb3e953d67455b295bd929595e742c18f25524b</id>
<published>2026-05-02T08:13:07Z</published>
<updated>2026-05-02T08:13:07Z</updated>
<title type="text">Fix needless game parsing on key strokes</title>
<link rel="alternate" type="text/html" href="commit/ddb3e953d67455b295bd929595e742c18f25524b.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit ddb3e953d67455b295bd929595e742c18f25524b
parent e862c908e7a41a4c730260d0855b86baa79893f9
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 17:13:07 +0900

Fix needless game parsing on key strokes

The architecture prior to this commit meant that all games were reparsed
from the XD source on every key stroke. This resulted in unacceptable
performance as the number of games increased.

This commit uses a caching approach to cache summaries of the games so
that the game list can be efficiently updated (because of the way that
SwiftUI works, the game list is updated even when the puzzle view is
what the user can see).

Co-Authored-By: Claude Opus 4.7 &lt;noreply@anthropic.com&gt;

</content>
</entry>
<entry>
<id>e862c908e7a41a4c730260d0855b86baa79893f9</id>
<published>2026-05-02T08:09:10Z</published>
<updated>2026-05-02T08:09:10Z</updated>
<title type="text">Reenable ProMotion</title>
<link rel="alternate" type="text/html" href="commit/e862c908e7a41a4c730260d0855b86baa79893f9.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit e862c908e7a41a4c730260d0855b86baa79893f9
parent 90977fba7084aaa41fd0b23d411d6a76712201e9
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 17:09:10 +0900

Reenable ProMotion

</content>
</entry>
<entry>
<id>90977fba7084aaa41fd0b23d411d6a76712201e9</id>
<published>2026-05-02T04:13:09Z</published>
<updated>2026-05-02T04:13:09Z</updated>
<title type="text">Disable ProMotion</title>
<link rel="alternate" type="text/html" href="commit/90977fba7084aaa41fd0b23d411d6a76712201e9.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 90977fba7084aaa41fd0b23d411d6a76712201e9
parent 27c0611f893c48428de21d2d81236f8c07980cbc
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 13:13:09 +0900

Disable ProMotion

</content>
</entry>
<entry>
<id>27c0611f893c48428de21d2d81236f8c07980cbc</id>
<published>2026-05-02T01:36:31Z</published>
<updated>2026-05-02T01:36:31Z</updated>
<title type="text">Allow certain logging to be disabled</title>
<link rel="alternate" type="text/html" href="commit/27c0611f893c48428de21d2d81236f8c07980cbc.html" />
<author>
<name>Michael Camilleri</name>
<email>mike@inqk.net</email>
</author>
<content type="text">commit 27c0611f893c48428de21d2d81236f8c07980cbc
parent e905838f71f1da7e48cfe954ec765b45f6b76af4
Author: Michael Camilleri &lt;mike@inqk.net&gt;
Date:   Sat,  2 May 2026 10:36:31 +0900

Allow certain logging to be disabled

</content>
</entry>
</feed>
