diff options
| author | Christian Cleberg <[email protected]> | 2026-08-02 15:29:33 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-08-02 15:35:23 -0500 |
| commit | fbcb4113f110a7db9cf21d18510cb59c2e90ba23 (patch) | |
| tree | 5a4afda3920f88f61698329c89771bda75fb8ced /ROADMAP.md | |
| parent | 04fae8d7fb3726b4d1b4c2576ece04766d2afe74 (diff) | |
| download | hutch-main.tar.gz hutch-main.tar.bz2 hutch-main.zip | |
Diffstat (limited to 'ROADMAP.md')
| -rw-r--r-- | ROADMAP.md | 370 |
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 `&` 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. |
