Commit f9e5b19cff

f9e5b19cff51ad5292cbad507bc00a5c68e27a96

parent: 4e80a4e282

Unsigned

cmc <hello@cleberg.net> · 2026-07-16 04:36 UTC

docs: record the API traps and Swift 6 blockers

Adds a SourceHut API traps section for the two things that cost this work real
time and that the schema will not tell you:

- Thread.updated is the root email's insert time, not thread activity. It never
  advances on a reply, while the schema describes threads as ordered "most
  recently bumped". Every inbox surface trusted it.
- The schema dumps in Docs/API omit inputFields and enumValues, so they answer
  "what shape is this mutation's input" and "what does this enum accept" with an
  empty array rather than an error. Both questions have to go to the real SDL.

Phase 3 picks up the Swift 6 language mode blockers and the three view models
still reading client.responseCache directly, which was previously buried as a
footnote inside completed Phase 1.

Also corrects the Phase 0 record. It credited d5ae787 with fixing an
Identifiable collision between same-subject threads. There is no such collision
to fix: deduplicateThreads merges those threads before anything renders, and the
test that motivated the change built its summaries by hand and skipped that
step. The change stands on clarity; the claimed bug was not real.

Layout: unified · split

ROADMAP.md +58 −8
@@ -6,6 +6,28 @@ call sites in the Swift source.
66
77See [SCOPE.md](SCOPE.md) for features that are intentionally out of scope.
88
9## SourceHut API traps
10
11Things the schema does not tell you, each of which has already cost real time.
12
13- **`Thread.updated` is not the thread's activity.** It is the root email's
14 insert time and never advances when a reply arrives, despite the name and
15 despite the schema describing `MailingList.threads` as ordered "most recently
16 bumped". sr.ht returns `updated` seven seconds after `root.date` on a thread
17 carrying four replies. Anything built on it silently treats thread creation as
18 activity. Use `MailingList.emails`, which is reverse-chronological arrival
19 data — see `MailingListActivity`. Prefer `Email.received` over `Email.date`:
20 `received` is server-side and non-null, `date` comes from the sender's header
21 and is neither.
22- **The schema dumps in `Docs/API` are partial.** They were captured with an
23 introspection query that omits `inputFields` and `enumValues`, so they cannot
24 answer what a mutation's input looks like or what an enum accepts — both come
25 back as empty arrays rather than as an error. For input shapes and enum cases,
26 read the real SDL instead:
27 `git clone --depth 1 https://git.sr.ht/~sircmpwn/<service>.sr.ht` and look at
28 `api/graph/schema.graphqls`. Regenerating the dumps with a full introspection
29 query would remove the trap.
30
931## Phase 0: Unblock CI — done (v3.5.0)
1032
1133Nothing downstream is trustworthy until the build badge means something.
@@ -29,12 +51,19 @@ been running only on demand in Xcode, and ten had rotted:
2951 `request.httpBody` read inside a `URLProtocol` (always nil; the body lives on
3052 `httpBodyStream`), an incident fixture contradicting its own RSS input, and an
3153 image assertion that treated the correct `&amp;` attribute encoding as a bug.
32- Four were real bugs the suite had been right about all along: repository
54- Three were real bugs the suite had been right about all along: repository
3355 descriptions could not be cleared (a nil subscript assignment drops the key
3456 instead of sending JSON null), `serviceNotProvisioned` was unreachable behind
35 a broader `no such` match, code spans rendered their contents as live markup,
36 and inbox threads keyed `id` on a subject-derived grouping key so two threads
37 sharing a subject on one list collided under `Identifiable`.
57 a broader `no such` match, and code spans rendered their contents as live
58 markup.
59- One was neither. `keepsDistinctThreadsDistinctByRootMessageID` asserted that
60 two same-subject threads get distinct `id`s, and `eff81f3` obliged by keying
61 `id` on the root Message-ID. The commit message claims this fixed an
62 `Identifiable` collision; it did not, because `deduplicateThreads` merges
63 same-subject threads into one summary before anything renders, so the
64 collision is unreachable. The test constructed summaries by hand and skipped
65 that step. The change is harmless and separating identity from grouping reads
66 better, but the stated reason was wrong.
3867
3968## Phase 1: Close the write gaps — done (v3.6.0)
4069
@@ -69,10 +98,8 @@ alongside Phase 2, which surfaces lists through patchsets.
6998 TTL-aware path — so both were removed rather than merged. `responseCache`
7099 remains as the in-memory layer behind `cachedPayload`.
71100
72Known follow-up: `BuildListViewModel`, `RepositoryListViewModel`, and
73`PasteService` still read `client.responseCache` directly, falling back across
74two different cache keys. That predates `APICacheKeys` and should be folded into
75`cachedPayload`.
101Known follow-up: three view models still read `client.responseCache` directly.
102Tracked under Phase 3.
76103
77104## Phase 2: Patchsets — done (v3.7.0)
78105
@@ -121,6 +148,29 @@ GraphQL mutation. Treat that boundary as explicit rather than half-building it.
121148 `deleteMailingList`).
122149- `events` feed (todo.sr.ht) and `archiveMessage` (lists.sr.ht).
123150
151### Swift 6 language mode
152
153The project builds in Swift 5 language mode with
154`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on
155concurrency diagnostics that are warnings today and errors there:
156
157- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated
158 initialisers and properties from nonisolated contexts, and `await` a few
159 expressions without marking them. Roughly 20 warnings, all in tests.
160- Response types are implicitly `@MainActor` under the default isolation, so
161 their `Decodable` conformances are too. Decoding one from a nonisolated
162 context — an `async let` over a raw `client.execute`, say — warns now and
163 fails then. The pattern that avoids it is `async let` over `@MainActor`
164 methods, as in `HomeViewModel.loadDashboard` and
165 `NotificationPreferencesViewModel.load`.
166
167### Cache reads that bypass the client
168
169`BuildListViewModel`, `RepositoryListViewModel`, and `PasteService` still read
170`client.responseCache` directly, each falling back across two different cache
171keys. That predates `APICacheKeys` and should be folded into `cachedPayload`,
172which already consults the persistent cache before the memory layer.
173
124174## Housekeeping
125175
126176- `Hutch/Hutch/App/AccountSession.swift` sits in a stray nested directory;