summaryrefslogtreecommitdiff
path: root/ROADMAP.md
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-08-02 15:29:33 -0500
committerChristian Cleberg <[email protected]>2026-08-02 15:35:23 -0500
commitfbcb4113f110a7db9cf21d18510cb59c2e90ba23 (patch)
tree5a4afda3920f88f61698329c89771bda75fb8ced /ROADMAP.md
parent04fae8d7fb3726b4d1b4c2576ece04766d2afe74 (diff)
downloadhutch-fbcb4113f110a7db9cf21d18510cb59c2e90ba23.tar.gz
hutch-fbcb4113f110a7db9cf21d18510cb59c2e90ba23.tar.bz2
hutch-fbcb4113f110a7db9cf21d18510cb59c2e90ba23.zip
convert readme to nfo; convert docs to txt; relicense to 0bsdHEADmain
Diffstat (limited to 'ROADMAP.md')
-rw-r--r--ROADMAP.md370
1 files changed, 0 insertions, 370 deletions
diff --git a/ROADMAP.md b/ROADMAP.md
deleted file mode 100644
index fcec5df..0000000
--- a/ROADMAP.md
+++ /dev/null
@@ -1,370 +0,0 @@
-# Roadmap
-
-Planned work for Hutch, ordered by dependency. Feature gaps below were
-identified by diffing the GraphQL schema dumps in `Docs/API` against actual
-call sites in the Swift source.
-
-See [SCOPE.md](SCOPE.md) for features that are intentionally out of scope.
-
-## SourceHut API traps
-
-Things the schema does not tell you, each of which has already cost real time.
-
-- **`Thread.updated` is not the thread's activity.** It is the root email's
- insert time and never advances when a reply arrives, despite the name and
- despite the schema describing `MailingList.threads` as ordered "most recently
- bumped". sr.ht returns `updated` seven seconds after `root.date` on a thread
- carrying four replies. Anything built on it silently treats thread creation as
- activity. Use `MailingList.emails`, which is reverse-chronological arrival
- data — see `MailingListActivity`. Prefer `Email.received` over `Email.date`:
- `received` is server-side and non-null, `date` comes from the sender's header
- and is neither.
-- **The schema dumps in `Docs/API` are partial.** They were captured with an
- introspection query that omits `inputFields` and `enumValues`, so they cannot
- answer what a mutation's input looks like or what an enum accepts — both come
- back as empty arrays rather than as an error. For input shapes and enum cases,
- read the real SDL instead:
- `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.
-
-## Phase 0: Unblock CI — done (v3.5.0)
-
-Nothing downstream is trustworthy until the build badge means something.
-
-- ~~Fix `repo-structure-check` in `builds/swift-ci.yml`~~. It asserted
- `test -d "website"`, but `website/` was removed in `24c8bc6` (2026-04-10), so
- the check had failed since then.
-- ~~Add a macOS CI job that runs `xcodebuild test`~~. builds.sr.ht has no macOS
- image and its maintainer has ruled them out, so `xcodebuild` cannot run there.
- The test plan now runs on the GitHub mirror via `.github/workflows/test.yml`;
- builds.sr.ht keeps secret scanning and structure checks.
-
-Turning the gate on first required making the suite green. All 214 tests had
-been running only on demand in Xcode, and ten had rotted:
-
-- The `Hutch` scheme referenced `container:HutchTests` without the
- `.xctestplan` extension, so `xcodebuild test -scheme Hutch` — the path the
- README sends contributors down — could not run at all.
-- Five were test-side rot: uppercase GraphQL enum rawValues asserted as
- lowercase, an ordering expectation predating `sortBuildItemsForTriage`,
- `request.httpBody` read inside a `URLProtocol` (always nil; the body lives on
- `httpBodyStream`), an incident fixture contradicting its own RSS input, and an
- image assertion that treated the correct `&amp;` attribute encoding as a bug.
-- Three were real bugs the suite had been right about all along: repository
- descriptions could not be cleared (a nil subscript assignment drops the key
- instead of sending JSON null), `serviceNotProvisioned` was unreachable behind
- a broader `no such` match, and code spans rendered their contents as live
- markup.
-- One was neither. `keepsDistinctThreadsDistinctByRootMessageID` asserted that
- two same-subject threads get distinct `id`s, and `eff81f3` obliged by keying
- `id` on the root Message-ID. The commit message claims this fixed an
- `Identifiable` collision; it did not, because `deduplicateThreads` merges
- same-subject threads into one summary before anything renders, so the
- collision is unreachable. The test constructed summaries by hand and skipped
- that step. The change is harmless and separating identity from grouping reads
- better, but the stated reason was wrong.
-
-## Phase 1: Close the write gaps — done (v3.6.0)
-
-Small, independently shippable mutations that already existed in the API but
-were never called. Each removes a "why can't I do this here?" moment.
-
-- ~~`updateTicket`~~ — edit a ticket's subject and body after creation.
-- ~~`deleteTicket`~~ — delete a ticket, behind a confirmation.
-- ~~`ticketSubscribe` / `ticketUnsubscribe`, `trackerSubscribe` /
- `trackerUnsubscribe`~~ — `Ticket.subscription` and `Tracker.subscription` are
- null when not subscribed, so both toggles reflect real server state.
-- ~~`mailingListUnsubscribe`~~ — see the caveat below.
-- ~~`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.md](SCOPE.md) on hub.sr.ht). Revisit if hub.sr.ht ever gains an API, or
-alongside Phase 2, which surfaces lists through patchsets.
-
-### Refactors folded in
-
-- ~~Collapse `SRHTClient`'s duplicated request paths~~. Extracted
- `makeAuthorizedRequest`, `send`, and `encodedGraphQLBody`; `executeMultipart`
- became the single-file case of `executeMultipartFiles`. The `#if DEBUG`
- logging block went from five copies to one. 938 lines to 569.
-- ~~Unify the two `executeCached` overloads~~. The memory-only overload and
- `executeAndCache` turned out to be dead — all 38 call sites already used the
- TTL-aware path — so both were removed rather than merged. `responseCache`
- remains as the in-memory layer behind `cachedPayload`.
-
-Known follow-up: three view models still read `client.responseCache` directly.
-Tracked under Phase 3.
-
-## Phase 2: Patchsets — done (v3.7.0)
-
-The flagship gap. Sending and reviewing patches over email is the SourceHut
-contribution model, and Hutch had no reference to `patchset` anywhere.
-
-Scoped as review-and-triage, not submission:
-
-- ~~Patchset list per mailing list~~ — see the caveat below.
-- ~~Patchset detail~~: cover letter, per-patch diffs (via the existing
- `DiffView`), checks, and the version / superseded-by chain.
-- ~~Status transitions via `updatePatchset`~~.
-
-Two schema facts shaped the result, and are worth knowing before extending this:
-
-- **`MailingList` has no `patchsets` field.** A list's patchsets cannot be
- queried directly; they are reachable only through thread roots. The existing
- threads query now also selects `root.patchset`, so the Patches tab costs no
- extra request — but it also means patchsets cannot be filtered by status
- server-side, and only patchsets whose thread appears in the current page are
- listed.
-- **`Patch` carries no diff.** It has only `index`, `count`, `version`,
- `prefix`, `subject`, and `trailers`. The diff exists solely inside the email
- body, so it is recovered with `InboxThreadUtilities.segmentMessageBody` — the
- same splitter the inbox thread view uses.
-
-Patch *submission* remains out of reach: it is a `git send-email` flow, not a
-GraphQL mutation. Treat that boundary as explicit rather than half-building it.
-
-## Phase 3: Polish and reach
-
-Unlike Phases 1 and 2, this is not one shippable thing. It is several, and they
-are sized very differently — measure before committing to one.
-
-### Release plan
-
-Hutch is an app with a `MARKETING_VERSION`, not a library with an API contract,
-so "breaking change" does not apply. These buckets track *user-visible scale*.
-
-| Version | Contents | Why here |
-| --- | --- | --- |
-| 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 |
-| v4.0.0 | Localization *with* translations | The only true re-presentation |
-| — | Swift 6 language mode; 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.md` 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.
-
-`KeychainHelper` is deliberately unbucketed; see the SonarCloud hotspots below.
-
-### API features — done (v3.8.0)
-
-- ~~`uploadArtifact` / `deleteArtifact`~~ — artifacts were read-only.
-- ~~`auditLog` (meta.sr.ht)~~ — surfaced under the tokens in Profile.
-- ~~Mailing list creation and settings~~ (`createMailingList`,
- `updateMailingList`, `deleteMailingList`).
-
-Three of the six planned. The other three did not survive contact:
-
-- `archiveMessage` is `@internal` and inaccessible.
-- The `events` feed was built, then removed: todo.sr.ht's root `events` resolver
- joins `event.participant_id` against `participant.user_id`, which are
- different id spaces, so it returns an empty list for everyone. See
- [SCOPE.md](SCOPE.md).
-- Webhook management, `shareSecret`, and build groups are reachable but declined
- on judgement — see [SCOPE.md](SCOPE.md) for the reasoning, so they do not get
- re-proposed.
-
-### Localization — v4.0.0, and only with translations
-
-The project sets `LOCALIZATION_PREFERS_STRING_CATALOGS = YES` but ships no
-string catalog, so every user-facing string is hardcoded English. Roughly 634
-literals: 239 `Text(`, 150 `Label(`, 117 `Button(`, 77 `Section(`, 51
-`navigationTitle(`.
-
-Worth knowing before starting: a catalog containing only English changes nothing
-for users until translations exist. It is groundwork, and it is the largest diff
-in the roadmap — it touches nearly every view, with the regression risk that
-implies.
-
-That combination is why this is bucketed at v4.0.0 *bundled with at least one
-real translation*, rather than shipped alone. An English-only catalog would earn
-the major number on regression risk while delivering nothing — the wrong trade.
-Hold the catalog until a translation lands. If it ever ships unbundled, it is
-groundwork and belongs in a quiet minor, not a 4.0.
-
-### Accessibility — v3.11.0
-
-Labels and hints appear in 17 of 89 view files. Mechanical and low-risk, but it
-cannot be verified from a build — it needs VoiceOver driven on a device.
-Independent of every other bucket, so it can move if a device pass is convenient.
-
-### SonarCloud backlog — done in code (v3.8.1)
-
-The live count is **53 issues / 10 rules**, not the 51 / 5 an earlier pass
-recorded — a reminder that this section rots like everything else, so query the
-API before budgeting. **0 bugs, 0 vulnerabilities**; everything is a code smell
-or hotspot. What the code side of v3.8.1 actually did:
-
-Fixed (`e93972f`):
-
-- **`swift:S1871`** — `RootView` had byte-identical `.home` / `.recentActivity`
- deep-link cases. Merged; recent activity is a *section* of Home, not a screen,
- so both correctly land on the Home tab.
-- **3× `swift:S1186` (empty closure/function, CRITICAL)** — two are
- `Button("Cancel", role: .cancel) {}` (dialog dismissal needs no body); the
- third is an empty `URLProtocol.stopLoading()` override in a test. All three now
- carry a nested comment. Note the earlier claim that "all three are Cancel
- buttons" was wrong — only two are.
-- **`swift:S108`** — the expected-miss `catch` in `APICacheTests` is commented.
-- **`swift:S1172`** — the unused `url` in `mimeType(for:)` is now `_`.
-- **2× `javascript:S4624`** — the nested template literal in the deep-link
- builders (`background.js`, `content.js`) is extracted to a `pathSegment` var.
-
-Fixed as a real bug instead (`65412ee`), not silenced:
-
-- **2× `swift:S1172` on `forceRefresh`** — `HomeViewModel.loadProjects` and
- `loadSystemStatusSnapshot` took the flag and dropped it, so dashboard
- pull-to-refresh returned cached projects and status. This is the trap named at
- the top of this file. `ProjectsListView` carried the same defect via its own
- `.refreshable`. Both fixed at the root in `ProjectService.fetchProjects`.
-
-Won't Fix, with reasons (resolve in SonarCloud's web UI, not in code):
-
-- **35× `swift:S1075` (hardcoded URI)** — 28 in `SourceHutWebDeepLinkMapperTests`,
- the rest in `HutchDeepLinkURLs`. A deep-link mapper's tests exist to assert
- literal URLs, and a one-forge client has fixed endpoints. "Fixing" them makes
- the code worse.
-- **`swift:S107`** — `executeCached` has 8 params across **38 call sites**. A
- param object would rewrite the hottest networking method for no behaviour or
- correctness gain against an arbitrary 7-param line. Not worth the regression
- surface.
-- **`swift:S1481`** — `ArtifactsView`'s `@Bindable var vm` is flagged unused, but
- `$vm.error` is used at line 134; Sonar's Swift analyzer misses the projected
- value. False positive — removing it breaks the build.
-- **`javascript:S7785`** — prefers top-level `await` for `injectBannerIfEnabled()`,
- but `content.js` is a classic content script, not a module. Top-level `await`
- would be a syntax error. Not applicable.
-- **5× `swift:S1135`** — TODO comments (INFO). The two in `HutchIntents` named
- real gaps and are now promoted to "App Intent gaps" below, with the inline
- `TODO`s replaced by plain references — so those two clear. The remaining three
- (`DeepLink`, `NotificationPreferencesViewModel` ×2) stay until addressed.
-
-The 3 hotspots are the part actually worth thought:
-
-- `KeychainHelper:33` and `:80` (**HIGH**) — the token is stored
- `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` with no
- `SecAccessControl`, so it does not require biometric or passcode
- authentication to read. That is a genuine product decision — should a stolen,
- unlocked phone hand over a sr.ht token? — not a lint nit. **Unbucketed on
- purpose:** adding `SecAccessControl` changes what a user must do to read their
- own token, so it needs a decision first. If the answer is yes, it is a minor
- bump of its own — a visible auth change should not hide inside a feature
- release.
-- `ReadmeView:1922` (**LOW**) — unrestricted WebView navigation. Probably a false
- positive: `isAllowedReadmeNavigationURL` enforces a scheme allowlist. Verify,
- then annotate.
-
-Query it with:
-`https://sonarcloud.io/api/issues/search?componentKeys=krazywarez_hutch&resolved=false`
-
-This was scoped as a patch on the assumption nothing executes differently — and
-that mostly held: the cosmetic fixes are comments, a merge, and a rename. The one
-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
-
-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
-this repo's assumptions quietly rot. Read each quarter's post, diff it against
-`Docs/API`, `SCOPE.md`, and the call sites, and file what changed.
-
-That this is worth doing is already proven: **`SCOPE.md` claims pronouns are
-"not in GraphQL schema", while `AppState` queries `pronouns` and
-`UserProfileView` displays them.** sr.ht shipped it, the doc never caught up,
-and it has been discouraging work that is in fact already done.
-
-[Q2 2026](https://sourcehut.org/blog/2026-05-28-whats-cooking-q2-2026/) alone
-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.md` 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.
-
-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.md`
-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.
-
-### App Intent gaps — unscheduled
-
-Two App Intents in `HutchIntents.swift` are placeholders for features Hutch does
-not have yet. Both are gated on the same missing capability — a global
-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`.
-- **`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`.
-
-### Cache reads that bypass the client — no release of its own
-
-`BuildListViewModel`, `RepositoryListViewModel`, and `PasteService` still read
-`client.responseCache` directly, each falling back across two different cache
-keys. That predates `APICacheKeys` and should be folded into `cachedPayload`,
-which already consults the persistent cache before the memory layer.
-
-Like Swift 6 above, this is internal and rides along with whatever release
-already touches that area. Neither justifies a tag.
-
-## Housekeeping
-
-- ~~`Hutch/Hutch/App/AccountSession.swift` sits in a stray nested directory;
- `Hutch/HutchTests/` is empty.~~ Done (v3.8.1, `9834b78`). Moved beside the rest
- of `App/`; both stray dirs removed. No pbxproj change — the target is a
- synchronized root group, so the file compiled by path all along.