summaryrefslogtreecommitdiff
path: root/ROADMAP.txt
diff options
context:
space:
mode:
Diffstat (limited to 'ROADMAP.txt')
-rw-r--r--ROADMAP.txt176
1 files changed, 116 insertions, 60 deletions
diff --git a/ROADMAP.txt b/ROADMAP.txt
index cf608b5..4352b9e 100644
--- a/ROADMAP.txt
+++ b/ROADMAP.txt
@@ -27,6 +27,15 @@ Things the schema does not tell you, each of which has already cost real time.
`git clone --depth 1 https://git.sr.ht/~sircmpwn/<service>.sr.ht` and look at
`api/graph/schema.graphqls`. Regenerating the dumps with a full introspection
query would remove the trap.
+- **`MailingList.subscription` does not report your subscription.** The field
+ exists and is typed `MailingListSubscription`, but it returns null even
+ immediately after a `mailingListSubscribe` that hands you back a real
+ subscription id — verified live against `~hutch`, for both owned and
+ non-owned lists. Do not gate subscribe-state on it. The authoritative source
+ is membership in the `subscriptions` query (correct: true after subscribe,
+ false after unsubscribe); the mutations take `listID: Int!`, read from
+ `list(rid:){ id }`. Cost the v3.11.0 subscribe toggle a full afternoon of the
+ "looks right, isn't" variety.
## Phase 0: Unblock CI — done (v3.5.0)
@@ -79,13 +88,14 @@ were never called. Each removes a "why can't I do this here?" moment.
- ~~`updatePreferences`~~ (todo.sr.ht and lists.sr.ht) — `notifySelf` and
`copySelf`, surfaced as an Email section in Settings.
-`mailingListSubscribe` is deliberately not wired up. `MailingList` has no
-`subscription` field, unlike `Ticket` and `Tracker`, so per-list state is only
-knowable from the `subscriptions` query — which by definition lists what the
-user is already subscribed to. Subscribing needs a list the user is *not*
-subscribed to, and sr.ht exposes no discovery API to find one (see
-[SCOPE.txt](SCOPE.txt) on hub.sr.ht). Revisit if hub.sr.ht ever gains an API, or
-alongside Phase 2, which surfaces lists through patchsets.
+`mailingListSubscribe` was left unwired here on the view that per-list state was
+only knowable from the `subscriptions` query, and subscribing needs a list you
+are *not* subscribed to. **Shipped in v3.11.0** once live testing clarified two
+things: a specific list is reachable without a discovery API (Lookup, a project's
+lists, patchsets), and the `subscriptions` query *is* the reliable state source —
+membership in it answers "am I subscribed to this rid?". `MailingList.subscription`
+looked like a shortcut but is a trap (see API traps); it is not used. See
+"mailing list subscribe" below.
### Refactors folded in
@@ -143,17 +153,20 @@ so "breaking change" does not apply. These buckets track *user-visible scale*.
| --- | --- | --- |
| v3.8.1 | SonarCloud triage; housekeeping | No behaviour change at all |
| v3.8.2 | Home system status moved to a title-bar status badge | Small UI relocation, no new surface |
-| v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map |
-| v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds |
-| v3.11.0 | Accessibility | Independent, device-verified |
+| v3.9.0 | ~~hub.sr.ht project writes + discovery (#12–#15); multi-language highlighting (#16); App Intents expansion (#17); man-page catalog sync (#7); checklist / recent-activity / pull-to-refresh fixes (#18, #11, #9)~~ | Shipped — the cut this session |
+| v3.10.0 | ~~git.sr.ht deploy keys~~ (shipped); ~~"What's cooking" ingest + doc truth-up~~ (done) | Ships one feature, corrects the map |
+| v3.11.0 | ~~Mailing list subscribe/unsubscribe toggle~~ (shipped) | Ingest-surfaced; state via the `subscriptions` query (the `subscription` field is a trap) |
+| v3.12.0 | Accessibility | Independent, device-verified |
| v4.0.0 | Localization *with* translations | The only true re-presentation |
-| — | Swift 6 language mode; cache reads | Internal; ride along, no tag |
+| — | ~~Swift 6 language mode~~ (done); cache reads | Internal; ride along, no tag |
-Ordering is by dependency, not size. v3.9.0 leads because it is the only item
-that corrects the others' inputs: the ingest's real output is a `SCOPE.txt` that
-is true, and v3.10.0 rests entirely on one unverified sentence in a blog post.
-Do not commit v3.10.0's number until the SDL has been read — the bucket may turn
-out to be empty, which is the point of sequencing it second.
+3.9.0 was cut this session, bundling the hub.sr.ht writes with the other
+features listed. That reorders the original plan: the "What's cooking" ingest
+and deploy keys — once slated for 3.9.0 — move to 3.10.0, and the hub.sr.ht
+writes that were provisionally 3.10.0 landed early, because the SDL, once
+actually read, turned out to have the mutations (it was not the empty bucket the
+sequencing had guarded against). The ingest still leads 3.10.0: its real output
+is a `SCOPE.txt` that is true.
`KeychainHelper` is deliberately unbucketed; see the SonarCloud hotspots below.
@@ -274,7 +287,7 @@ exception earns the release its own line: the `forceRefresh` fix changes what
pull-to-refresh does, so it needs a manual pass on a device before v3.8.1 ships,
not just a green suite.
-### Ingest "What's cooking on SourceHut?" — v3.9.0
+### Ingest "What's cooking on SourceHut?" — v3.10.0
sr.ht posts a quarterly update to `~sircmpwn/sr.ht-announce`, mirrored at
<https://sourcehut.org/blog/>. Nothing in Hutch tracks it, so the API grows and
@@ -290,33 +303,66 @@ and it has been discouraging work that is in fact already done.
flags two openings:
- **hub.sr.ht gained a writable GraphQL API** for managing projects and project
- resources. Hutch's projects are read-only, and `SCOPE.txt` still rules out
- discovery on the grounds that hub has no public API. Both claims need
- rechecking — this may also unblock `mailingListSubscribe`, which Phase 1 left
- out for exactly that reason.
-- **git.sr.ht deploy keys are complete** (`createDeployKey` / `deleteDeployKey`
- are in the SDL). Hutch never calls them.
+ resources. ~~Rechecked and shipped~~: project create/edit, resource
+ link/unlink, and public discovery landed (#12–#15) — see "hub.sr.ht writes"
+ below. `SCOPE.txt`'s "hub has no public API / no discovery" claim has since
+ been corrected.
+- ~~git.sr.ht deploy keys are complete~~ — **shipped** (v3.10.0).
+ `createDeployKey` / `deleteDeployKey` (and `Repository.deployKeys`) are wired
+ into the repository actions menu, owner-only, alongside ACLs.
Start from Q1 2026 forward — that is roughly when the current `Docs/API` dumps
were captured.
-Research does not ship, so v3.9.0 pairs the ingest with **deploy keys** — the one
-self-contained feature it has already surfaced and that the SDL confirms exists.
-That gives the release something a user can see. Everything else the ingest turns
-up gets filed, not built, and hub.sr.ht gets its own bucket below.
-
-### hub.sr.ht writes — v3.10.0, provisional
-
-Everything here rests on a single sentence in the Q2 2026 post: that hub.sr.ht
-gained a writable GraphQL API. If true, three things unblock at once —
-project writes (Hutch's projects are read-only), discovery (which `SCOPE.txt`
-rules out on the grounds hub has no public API), and `mailingListSubscribe`,
-which Phase 1 declined for exactly that reason.
-
-All three live or die on the same unverified claim, which is why this is
-sequenced after the ingest rather than planned now. Read
-`api/graph/schema.graphqls` in `hub.sr.ht` before committing the version number.
-The bucket may be empty.
+Deploy keys — the one self-contained feature the ingest had already surfaced and
+that the SDL confirmed — shipped in v3.10.0.
+
+**Ingest run (2026-08, Q1–Q2 posts + live schema introspection with a test
+token):** everything else the posts flagged is already in Hutch — RIDs (used
+throughout), pronouns and avatars (queried and displayed, avatar upload/delete
+in Settings), hub project writes and discovery (shipped), deploy keys (shipped).
+Planned-but-not-yet-shipped upstream, so nothing to build: anonymous API access
+and "standardized / connections-spec" GraphQL (Q2 named both as future work).
+The one *new* opening the introspection turned up is below.
+
+### Mailing list subscribe — done (v3.11.0)
+
+A subscribe / unsubscribe toggle now sits in the mailing-list detail toolbar
+(`MailingListDetailView`, which backs both Lookup results and
+`ProjectMailingListView`). It is hidden for lists you own and while state is
+unknown.
+
+Live testing rewrote the plan. `MailingList.subscription` looked like the state
+source but is a trap — it returns null even right after a successful
+`mailingListSubscribe` that hands back a subscription id (see API traps). So
+state comes from membership in the `subscriptions` query, which *is* reliable,
+and the numeric `listID` the mutations require comes from `list(rid:){ id }`.
+The mutations themselves (`mailingListSubscribe` / `mailingListUnsubscribe`,
+`listID: Int!`) work as expected. Phase 1's "no discovery API" worry was moot:
+a specific list is reachable via Lookup, a project's lists, or patchsets, and
+that is all subscribing needs.
+
+### hub.sr.ht writes — projects and discovery done
+
+Reading `api/graph/schema.graphqls` in `hub.sr.ht` settled the Q2 2026 claim:
+the master schema does expose the project write API — `createProject`,
+`updateProject`, `deleteProject`, the `link*` / `unlink*` resource mutations,
+and a public `projects` discovery query. Two of the three items this bucket
+tracked shipped against it:
+
+- ~~Project writes~~ — create (#13), edit (#14), and manage linked
+ repositories, trackers, and mailing lists (#15).
+- ~~Discovery~~ — a browsable directory of public projects (#12).
+
+Built against the master SDL; live deployment on `sr.ht/query` could not be
+confirmed without a token (introspection there is auth-gated), so the mutations
+degrade to a visible error rather than a crash if a field is not yet deployed.
+Verify on a signed-in device.
+
+`mailingListSubscribe` is now buildable: the ingest found `MailingList` gained a
+`subscription` field, so per-list state is readable and the subscribe/unsubscribe
+toggle can reflect it — see the "mailing list subscribe" bucket below. `SCOPE.txt`
+has since had its "hub has no public API / no discovery" claim corrected.
### App Intent gaps — unscheduled
@@ -326,31 +372,41 @@ search/persistence layer — so neither is schedulable until that lands. (These
were the two `swift:S1135` TODOs; promoted here so the code carries a reference
rather than a bare `TODO`.)
-- **Global content search.** `SearchHutchIntent` accepts a query but routes to
- the Lookup screen — sourcehut entity resolution — because Hutch has no
- full-text search across tickets, repos, and lists. Its own description says
- "Opens Hutch lookup with a search query." When a real search exists, repoint
- the `.search` route in `SearchHutchIntent.route`.
+- **Global content search.** `SearchHutchIntent` accepts a query — and now a
+ search *type* (#17) — but still routes to the Lookup screen, sourcehut entity
+ resolution, because Hutch has no full-text search across tickets, repos, and
+ lists. When a real search exists, repoint the `.search` route in
+ `SearchHutchIntent.route`. (#17 also completed Check Status / Check Builds
+ dialogs and added the Clear Recent Activity and Unpin Resource mutating
+ intents; those were shipped, not gaps.)
- **`OpenSavedSearchIntent`.** Saved searches are per-tracker only
(`TicketSavedFilterStore`, `ScopedSearchHistoryStore`); there is no global
saved-search store for an intent to open. Add the intent once global
saved-search persistence exists.
-### Swift 6 language mode — no release of its own
-
-The project builds in Swift 5 language mode with
-`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`. Moving to Swift 6 is blocked on
-concurrency diagnostics that are warnings today and errors there:
-
-- `APICacheTests` and `BundleUserAgentTests` call main-actor-isolated
- initialisers and properties from nonisolated contexts, and `await` a few
- expressions without marking them. Roughly 20 warnings, all in tests.
-- Response types are implicitly `@MainActor` under the default isolation, so
- their `Decodable` conformances are too. Decoding one from a nonisolated
- context — an `async let` over a raw `client.execute`, say — warns now and
- fails then. The pattern that avoids it is `async let` over `@MainActor`
- methods, as in `HomeViewModel.loadDashboard` and
- `NotificationPreferencesViewModel.load`.
+### Swift 6 language mode — done
+
+`SWIFT_VERSION` is now `6.0` (keeping `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`).
+The migration was mostly mechanical, in a few buckets:
+
+- **Models are `nonisolated`.** Under MainActor-default, every value-type model
+ was implicitly `@MainActor`; the pure data types in `Models/` (and utility
+ extensions like `Date.relativeDescription`, `DateFormatter+SRHT`, `SRHTWebURL`)
+ are now `nonisolated`, so the nonisolated networking layer can use them.
+- **App Intents statics.** `AppIntent`/`AppEntity`/`AppEnum` `static var`s were
+ "global shared mutable state"; the stored ones are now `static let`.
+- **Sendable dictionaries.** `nil as String? as Any` in `[String: any Sendable]`
+ GraphQL variable dicts became `nil as String? as any Sendable`.
+- **`UserDefaults`** gets a retroactive `@unchecked Sendable` (documented
+ thread-safe) since it threads through account sessions and stores.
+- **WidgetKit** completion handlers are rebound `nonisolated(unsafe)` to cross
+ into their `Task {}`; the `@Observable` `TipStoreViewModel` task handle is
+ `@ObservationIgnored nonisolated(unsafe)` for its nonisolated `deinit`.
+- **Tests** run on `@MainActor` (they exercise MainActor app code), with a few
+ constant fixtures marked `nonisolated` for use inside `@Sendable` stub
+ closures.
+
+Builds and the full suite are clean in Swift 6 mode with no behaviour change.
### Cache reads that bypass the client — no release of its own