gem-contribute 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gem_release.yml +1 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- data/.github/workflows/ci.yml +26 -0
- data/.github/workflows/pr-template-check.yml +100 -0
- data/CHANGELOG.md +41 -0
- data/CLAUDE.md +1 -1
- data/CODE_OF_CONDUCT.md +86 -0
- data/CONTRIBUTING.md +12 -13
- data/README.md +21 -8
- data/docs/OPEN_QUESTIONS.md +167 -0
- data/docs/ROADMAP.md +266 -0
- data/docs/adr/0006-standalone-gem-not-plugin.md +1 -1
- data/docs/adr/0008-rooibos-tui-framework.md +3 -3
- data/docs/adr/0010-charm-ruby-tui-framework.md +84 -0
- data/docs/adr/0011-host-adapter-owns-host-verbs.md +58 -0
- data/docs/adr/0012-output-free-service-objects-three-interface-architecture.md +79 -0
- data/docs/adr/0013-revert-to-rooibos.md +71 -0
- data/docs/adr/0014-ship-bundler-and-rubygems-plugins.md +75 -0
- data/docs/adr/README.md +7 -2
- data/docs/design-interface-layer.md +295 -0
- data/docs/design.md +31 -8
- data/docs/ideas.md +1 -0
- data/docs/index.md +2 -2
- data/docs/prep-plan.md +6 -6
- data/docs/talk/README.md +45 -0
- data/docs/talk/index.html +4165 -0
- data/docs/talk/lightning.md +425 -0
- data/docs/talk/lightning.pdf +0 -0
- data/lib/gem_contribute/cli/auth.rb +22 -44
- data/lib/gem_contribute/cli/config.rb +32 -16
- data/lib/gem_contribute/cli/fix.rb +122 -0
- data/lib/gem_contribute/cli/fork.rb +145 -0
- data/lib/gem_contribute/cli/init.rb +78 -0
- data/lib/gem_contribute/cli/issue_announcer.rb +42 -0
- data/lib/gem_contribute/cli/issues.rb +37 -44
- data/lib/gem_contribute/cli/platform_tools.rb +33 -0
- data/lib/gem_contribute/cli/post_clone_hooks.rb +50 -0
- data/lib/gem_contribute/cli/rate_limit_footer.rb +34 -0
- data/lib/gem_contribute/cli/scan.rb +20 -15
- data/lib/gem_contribute/cli/submit.rb +60 -64
- data/lib/gem_contribute/cli/workflow.rb +63 -0
- data/lib/gem_contribute/cli.rb +11 -14
- data/lib/gem_contribute/config.rb +28 -4
- data/lib/gem_contribute/git.rb +49 -0
- data/lib/gem_contribute/host_adapter.rb +52 -5
- data/lib/gem_contribute/host_adapters/github_adapter.rb +126 -37
- data/lib/gem_contribute/operations/announce.rb +52 -0
- data/lib/gem_contribute/operations/branch.rb +35 -0
- data/lib/gem_contribute/operations/clone.rb +41 -0
- data/lib/gem_contribute/operations/fix_pipeline.rb +70 -0
- data/lib/gem_contribute/operations/fork.rb +35 -0
- data/lib/gem_contribute/output/null.rb +20 -0
- data/lib/gem_contribute/output/standard.rb +71 -0
- data/lib/gem_contribute/version.rb +1 -1
- data/lib/gem_contribute.rb +10 -18
- metadata +120 -3
- data/lib/gem_contribute/cli/fork_clone_branch.rb +0 -197
data/docs/ROADMAP.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Roadmap to v1
|
|
2
|
+
|
|
3
|
+
**Goal:** v1 of `gem-contribute` ships a single gem with three entry points against `github.com`:
|
|
4
|
+
|
|
5
|
+
1. **Standalone CLI** — `gem-contribute <verb>`. Bare invocation (no subcommand) launches the **Rooibos TUI** (project list → issue list → issue detail → CONTRIBUTING viewer + auth overlay).
|
|
6
|
+
2. **Bundler plugin** — `bundle contribute [verb]`. CLI-only. Bare invocation runs a default summary verb (TBD: `scan` or `list all`).
|
|
7
|
+
3. **RubyGems plugin** — `gem contribute [verb]`. CLI-only. Same shape as Bundler plugin.
|
|
8
|
+
|
|
9
|
+
All three share the same output-free service layer (per [ADR-0012](adr/0012-output-free-service-objects-three-interface-architecture.md)). All three are tested. v1 has a real release on rubygems.org, a CHANGELOG, and CI.
|
|
10
|
+
|
|
11
|
+
This document is the plan. Decisions still in flight live in [`OPEN_QUESTIONS.md`](OPEN_QUESTIONS.md) and get resolved one at a time.
|
|
12
|
+
|
|
13
|
+
> 🌱 marks a [good first issue](https://github.com/cdhagmann/gem-contribute/labels/good%20first%20issue) — small, self-contained scope, friendly for someone new to the codebase.
|
|
14
|
+
|
|
15
|
+
## Decision history (the short version)
|
|
16
|
+
|
|
17
|
+
- **Workshop is over** (2026-05-02). Decisions made primarily for workshop scope are reversed.
|
|
18
|
+
- **TUI framework: Rooibos** (per [ADR-0013](adr/0013-revert-to-rooibos.md), supersedes ADR-0010). Bubbletea-ruby was a workshop-driven choice; Rooibos enables the post-v1 world map view (issue #5) and matches the project's verbs better.
|
|
19
|
+
- **Three entry points, one gem** (per [ADR-0014](adr/0014-ship-bundler-and-rubygems-plugins.md), amends ADR-0006 and ADR-0012). Standalone binary + Bundler plugin + RubyGems plugin all live in the `gem-contribute` gem.
|
|
20
|
+
- **GitHub-only at v1.0.** GitLab/Codeberg adapters are v1.x territory. ADR-0011's architecture is the bet that pays off there.
|
|
21
|
+
- **Service layer is output-free** (per ADR-0012). dry-monads `Result` returns; dry-operation pipelines; no `stdout:` in operations.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What's already done
|
|
26
|
+
|
|
27
|
+
- **Data layer.** `LockfileParser`, `Resolver`, `GitHubAdapter`, `Auth`, `TokenStore`, `Cache`, `Operations::Fork`, `Operations::Clone`.
|
|
28
|
+
- **CLI verbs.** `init`, `scan`, `issues`, `config`, `auth`, `fork`, `fix`, `submit`.
|
|
29
|
+
- **HostAdapter cleanup.** ADR-0011 work landed: adapter owns host verbs, Operations layer composes them, CLI verbs compose Operations.
|
|
30
|
+
- **ADR-0012 service layer (Phase 1).** dry-monads `Result`, dry-operation pipelines, dry-initializer initializers, output-free `Operations::*`. Merged via [PR #48](https://github.com/cdhagmann/gem-contribute/pull/48) on 2026-05-04.
|
|
31
|
+
- **Basic CI.** rubocop + rspec on push/PR landed via [PR #21](https://github.com/cdhagmann/gem-contribute/pull/21) (closes [#7](https://github.com/cdhagmann/gem-contribute/issues/7)). Plugin-install smoke and gated integration tests still pending under [#43](https://github.com/cdhagmann/gem-contribute/issues/43).
|
|
32
|
+
|
|
33
|
+
## What hasn't started
|
|
34
|
+
|
|
35
|
+
- TUI
|
|
36
|
+
- Bundler plugin
|
|
37
|
+
- RubyGems plugin
|
|
38
|
+
- Remaining release infrastructure (MAINTAINER doc, release workflow, README rewrite)
|
|
39
|
+
|
|
40
|
+
## In flight
|
|
41
|
+
|
|
42
|
+
- **ADR-0012 Phase 2 (CLI output pipeline)** — `Output::Standard`/`Output::Null`, `tty-spinner`, `tty-prompt`. Open in [PR #51](https://github.com/cdhagmann/gem-contribute/pull/51).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Phase 0 — Reset workshop-era decisions (DONE)
|
|
47
|
+
|
|
48
|
+
Two new ADRs landed:
|
|
49
|
+
|
|
50
|
+
- [ADR-0013](adr/0013-revert-to-rooibos.md) — Rooibos as the TUI framework, superseding ADR-0010.
|
|
51
|
+
- [ADR-0014](adr/0014-ship-bundler-and-rubygems-plugins.md) — Bundler + RubyGems plugins ship at v1, single gem.
|
|
52
|
+
|
|
53
|
+
ADR header sweep done in commit `00f5a4c`. Doc sweeps:
|
|
54
|
+
- [x] 🌱 [#23](https://github.com/cdhagmann/gem-contribute/issues/23) — Sweep `docs/design.md` for residual bubbletea references (no-op; doc was already clean)
|
|
55
|
+
- [x] 🌱 [#24](https://github.com/cdhagmann/gem-contribute/issues/24) — Sweep `docs/design-interface-layer.md` for "bubbletea" → "Rooibos" and update gem-plugin section
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Phase 1 — Service layer (ADR-0012 Phase 1) (DONE)
|
|
60
|
+
|
|
61
|
+
Made every operation output-free and Result-returning. This is what lets the TUI and the two plugins reuse the same code paths the standalone CLI uses. Merged via [PR #48](https://github.com/cdhagmann/gem-contribute/pull/48) on 2026-05-04.
|
|
62
|
+
|
|
63
|
+
**Steps:**
|
|
64
|
+
|
|
65
|
+
1. Add `dry-monads`, `dry-operation`, `dry-initializer` to gemspec.
|
|
66
|
+
2. Remove `stdout:` from `Operations::Fork` and `Operations::Clone`.
|
|
67
|
+
3. Define `Operations::Clone::Result = Data.define(:path, :reused)` (currently returns a bare path).
|
|
68
|
+
4. Convert both operations to return `Success(Result)` / `Failure(reason)`.
|
|
69
|
+
5. Convert `Workflow#build_adapter` to return `Success(adapter)` / `Failure(:unauthenticated)`.
|
|
70
|
+
6. Extract `Operations::Branch` (from inline branch logic in `CLI::Fix`) and `Operations::Announce` (from `CLI::IssueAnnouncer`).
|
|
71
|
+
7. Build `Operations::FixPipeline` using `dry-operation` to compose Fork → Clone → Branch → Announce.
|
|
72
|
+
8. Replace verbose initializers in `CLI::Fork`/`CLI::Fix` with `dry-initializer` (kills the rubocop suppressions).
|
|
73
|
+
9. Update CLI verbs to pattern-match on `Result`. Retire `Workflow#with_workflow_rescues`.
|
|
74
|
+
|
|
75
|
+
**Acceptance:**
|
|
76
|
+
- [x] No `Operations::*` class accepts `stdout:` or `stderr:`
|
|
77
|
+
- [x] All operations return `Success` / `Failure`
|
|
78
|
+
- [x] `FixPipeline` exists; `CLI::Fix#execute` calls it instead of wiring steps inline
|
|
79
|
+
- [x] All existing tests pass (no behaviour change visible to users)
|
|
80
|
+
- [x] No new rubocop suppressions
|
|
81
|
+
|
|
82
|
+
**Issues:**
|
|
83
|
+
- [x] [#25](https://github.com/cdhagmann/gem-contribute/issues/25) — Adopt dry-rb suite; convert `Operations::Fork`/`Clone` to `Result`
|
|
84
|
+
- [x] [#26](https://github.com/cdhagmann/gem-contribute/issues/26) — `Workflow#build_adapter` returns `Result`; retire `with_workflow_rescues`
|
|
85
|
+
- [x] [#27](https://github.com/cdhagmann/gem-contribute/issues/27) — Extract `Operations::Branch` and `Operations::Announce`; build `Operations::FixPipeline`
|
|
86
|
+
- [x] [#28](https://github.com/cdhagmann/gem-contribute/issues/28) — Migrate `CLI::Fork`/`CLI::Fix` initializers to dry-initializer
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Phase 2 — CLI pipeline (ADR-0012 Phase 2) (in flight)
|
|
91
|
+
|
|
92
|
+
Open in [PR #51](https://github.com/cdhagmann/gem-contribute/pull/51). Move CLI verbs to a semantic output abstraction so the look-and-feel can evolve independently of the service layer.
|
|
93
|
+
|
|
94
|
+
**Steps:**
|
|
95
|
+
|
|
96
|
+
1. Introduce `Output::Standard` (wraps stdout/stderr; `#info`, `#warn`, `#error`, `#progress`).
|
|
97
|
+
2. Introduce `Output::Null` (for tests).
|
|
98
|
+
3. Replace raw `@stdout` / `@stderr` in every CLI verb with `@output`.
|
|
99
|
+
4. Add `tty-spinner` for `Output::Standard#progress`.
|
|
100
|
+
5. Replace `CLI::Init`'s `stdout.print` + `@gets` with `tty-prompt`.
|
|
101
|
+
|
|
102
|
+
**Acceptance:**
|
|
103
|
+
- [ ] No raw `puts` to `@stdout`/`@stderr` in `lib/gem_contribute/cli/`
|
|
104
|
+
- [ ] Long operations show a spinner in TTY contexts and a plain line in non-TTY contexts
|
|
105
|
+
- [ ] `Init`'s test suite no longer mocks `gets`
|
|
106
|
+
- [ ] User-visible CLI output unchanged for non-interactive flows; spinners appear in interactive ones
|
|
107
|
+
|
|
108
|
+
**Issues:**
|
|
109
|
+
- [ ] [#29](https://github.com/cdhagmann/gem-contribute/issues/29) — `Output::Standard` and `Output::Null`; migrate CLI verbs off raw stdout/stderr
|
|
110
|
+
- [ ] [#30](https://github.com/cdhagmann/gem-contribute/issues/30) — `tty-spinner`-backed `#progress`
|
|
111
|
+
- [ ] [#31](https://github.com/cdhagmann/gem-contribute/issues/31) — `CLI::Init` uses `tty-prompt`
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Phase 3 — Rooibos TUI
|
|
116
|
+
|
|
117
|
+
**Umbrella issue:** [#2 — Implement Rooibos TUI on top of the v0.1 CLI](https://github.com/cdhagmann/gem-contribute/issues/2). The major work. Per design.md and ADR-0013.
|
|
118
|
+
|
|
119
|
+
**Pre-work (Q7 verification):**
|
|
120
|
+
- [ ] Confirm Rooibos's current published version on rubygems.org
|
|
121
|
+
- [ ] Verify `Command.http`, `Command.system`, `Command.wait`, `Command.cancel` still exist in 0.7.x
|
|
122
|
+
- [ ] Verify Rooibos snapshot test helpers still ship
|
|
123
|
+
- [ ] Pin `rooibos` and `ratatui_ruby` in gemspec
|
|
124
|
+
|
|
125
|
+
**Fragments:**
|
|
126
|
+
|
|
127
|
+
- `ProjectList` — gems from the lockfile with issue counts (lazy-loaded via `Command.http`)
|
|
128
|
+
- `IssueList` — open issues for the selected project, labels rendered verbatim (ADR-0005)
|
|
129
|
+
- `IssueDetail` — body, labels, action keys (`f` fix, `c` open CONTRIBUTING, `o` open in browser)
|
|
130
|
+
- `ContributingViewer` — rendered markdown (ADR-0007); also surfaces the upstream PR template per [#13](https://github.com/cdhagmann/gem-contribute/issues/13)
|
|
131
|
+
- `AuthOverlay` — device-flow prompt that fires on `:auth_required`
|
|
132
|
+
|
|
133
|
+
(World map fragment is post-v1; framework choice locks in now per ADR-0013.)
|
|
134
|
+
|
|
135
|
+
**Wiring:**
|
|
136
|
+
- [ ] `gem-contribute` (no args, with a `Gemfile.lock` in cwd) launches the TUI. This is the entry-point change in `cli.rb`.
|
|
137
|
+
- [ ] `gem-contribute` (no args, no `Gemfile.lock`) prints a clear "no Gemfile.lock found" message and the USAGE.
|
|
138
|
+
|
|
139
|
+
**Key contracts:**
|
|
140
|
+
- All async work goes through Rooibos Commands (no `Thread.new`, no `Async`)
|
|
141
|
+
- `Update` is a pure function tested as such (per fragment)
|
|
142
|
+
- Service-layer calls happen inside Commands and return `Result` types (Phase 1's contract)
|
|
143
|
+
- Command result messages are pattern-matched in `Update` to `Success(...)` / `Failure(...)` shapes
|
|
144
|
+
|
|
145
|
+
**Acceptance:**
|
|
146
|
+
- [ ] All five fragments render and route as designed
|
|
147
|
+
- [ ] `Update` test for every fragment, covering each key handler and each command-result branch
|
|
148
|
+
- [ ] At least one snapshot test for the main flow (project list → issue list → issue detail → fix)
|
|
149
|
+
- [ ] At least one snapshot test for the auth overlay firing mid-flow
|
|
150
|
+
- [ ] `q` quits, `Ctrl+C` quits, `?` shows help overlay
|
|
151
|
+
- [ ] Status bar shows rate limit remaining
|
|
152
|
+
|
|
153
|
+
**Issues (under umbrella [#2](https://github.com/cdhagmann/gem-contribute/issues/2)):**
|
|
154
|
+
- [ ] [#32](https://github.com/cdhagmann/gem-contribute/issues/32) — `ProjectList` fragment with lazy-loaded issue counts
|
|
155
|
+
- [ ] [#33](https://github.com/cdhagmann/gem-contribute/issues/33) — `IssueList` fragment
|
|
156
|
+
- [ ] [#34](https://github.com/cdhagmann/gem-contribute/issues/34) — `IssueDetail` fragment with action keys (f / c / o)
|
|
157
|
+
- [ ] [#35](https://github.com/cdhagmann/gem-contribute/issues/35) — `ContributingViewer` fragment (may absorb [#13](https://github.com/cdhagmann/gem-contribute/issues/13))
|
|
158
|
+
- [ ] [#36](https://github.com/cdhagmann/gem-contribute/issues/36) — `AuthOverlay` fragment for device-flow prompt
|
|
159
|
+
- [ ] [#37](https://github.com/cdhagmann/gem-contribute/issues/37) — No-arg `gem-contribute` launches the TUI
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Phase 4 — Bundler plugin (`bundle contribute`)
|
|
164
|
+
|
|
165
|
+
A `plugins.rb` entry point at the root of the gem registers a Bundler plugin command per Bundler convention. Delegates to the same dispatch table the standalone CLI uses.
|
|
166
|
+
|
|
167
|
+
**Constraints:**
|
|
168
|
+
- Plugin entry point MUST NOT require Rooibos or `ratatui_ruby` (per ADR-0014). TUI loading is gated to the standalone binary.
|
|
169
|
+
- Bare `bundle contribute` runs the default verb (TBD per OPEN_QUESTIONS Q3a: `scan` or `list all`).
|
|
170
|
+
- `bundle contribute <verb>` mirrors `gem-contribute <verb>`.
|
|
171
|
+
|
|
172
|
+
**Acceptance:**
|
|
173
|
+
- [ ] `bundle plugin install gem-contribute` works against the local gem
|
|
174
|
+
- [ ] `bundle contribute` produces the default summary
|
|
175
|
+
- [ ] `bundle contribute fix sidekiq/123` runs the fix verb
|
|
176
|
+
- [ ] Smoke test verifies plugin registration without booting the TUI
|
|
177
|
+
|
|
178
|
+
**Issue:** [#38](https://github.com/cdhagmann/gem-contribute/issues/38) — Bundler plugin: `bundle contribute` entry point
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Phase 5 — RubyGems plugin (`gem contribute`)
|
|
183
|
+
|
|
184
|
+
A `rubygems_plugin.rb` entry point registers a `Gem::Command` subclass per RubyGems convention. Same dispatch table.
|
|
185
|
+
|
|
186
|
+
**Constraints:**
|
|
187
|
+
- Same TUI-load gating as Phase 4.
|
|
188
|
+
- Same default-verb behavior as Phase 4.
|
|
189
|
+
|
|
190
|
+
**Acceptance:**
|
|
191
|
+
- [ ] `gem install gem-contribute` registers the `Gem::Command`
|
|
192
|
+
- [ ] `gem contribute --help` lists the same verbs as `gem-contribute --help`
|
|
193
|
+
- [ ] `gem contribute fix sidekiq/123` runs the fix verb
|
|
194
|
+
- [ ] Smoke test verifies plugin registration without booting the TUI
|
|
195
|
+
|
|
196
|
+
**Issue:** [#39](https://github.com/cdhagmann/gem-contribute/issues/39) — RubyGems plugin: `gem contribute` Gem::Command
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Phase 6 — Polish, release infrastructure, v1.0
|
|
201
|
+
|
|
202
|
+
Everything required to call it 1.0 and not 0.x.
|
|
203
|
+
|
|
204
|
+
**Pre-existing user-facing issues that fold into this phase:**
|
|
205
|
+
- [ ] 🌱 [#1 — Add `preferred_labels` config so non-canonical good-first-issue labels are caught](https://github.com/cdhagmann/gem-contribute/issues/1)
|
|
206
|
+
- [ ] 🌱 [#3 — Add `gem-contribute open <gem>` to open the repo in the browser](https://github.com/cdhagmann/gem-contribute/issues/3)
|
|
207
|
+
- [ ] 🌱 [#9 — Add `--label LABEL` flag to scan and issues for one-off overrides](https://github.com/cdhagmann/gem-contribute/issues/9) (related to #1)
|
|
208
|
+
- [ ] 🌱 [#10 — Friendlier message when `fix` runs against a repo you own](https://github.com/cdhagmann/gem-contribute/issues/10)
|
|
209
|
+
|
|
210
|
+
**Release infrastructure:**
|
|
211
|
+
- [ ] 🌱 [#40](https://github.com/cdhagmann/gem-contribute/issues/40) — Add CHANGELOG.md
|
|
212
|
+
- [ ] 🌱 [#41](https://github.com/cdhagmann/gem-contribute/issues/41) — Add CONTRIBUTING.md
|
|
213
|
+
- [ ] [#42](https://github.com/cdhagmann/gem-contribute/issues/42) — Add MAINTAINER.md (release process, OAuth App, plugin verification)
|
|
214
|
+
- [ ] OAuth App: stay on personal-account App for v1.0 (per Q13); migrate when rate limits bite
|
|
215
|
+
- [ ] [#43](https://github.com/cdhagmann/gem-contribute/issues/43) — CI workflow (`.github/workflows/ci.yml`): rubocop + rspec; gated integration tests; plugin install smoke (basic rubocop + rspec already landed via [PR #21](https://github.com/cdhagmann/gem-contribute/pull/21) / [#7](https://github.com/cdhagmann/gem-contribute/issues/7); gated integration tests and plugin smoke still pending)
|
|
216
|
+
- [ ] [#44](https://github.com/cdhagmann/gem-contribute/issues/44) — Release workflow (`.github/workflows/release.yml`) with **Trusted Publishing (OIDC)**
|
|
217
|
+
- [ ] 🌱 [#45](https://github.com/cdhagmann/gem-contribute/issues/45) — Archive workshop docs to `docs/archive/`
|
|
218
|
+
- [ ] [#46](https://github.com/cdhagmann/gem-contribute/issues/46) — README rewrite for v1 audience
|
|
219
|
+
- [ ] Verify `bundle plugin install` and `gem install` from a clean machine (covered by CI smoke test in #43)
|
|
220
|
+
- [ ] [#47](https://github.com/cdhagmann/gem-contribute/issues/47) — Meta-PR: use `gem-contribute` against a real downstream
|
|
221
|
+
- [ ] Tag `v1.0.0`, push to rubygems
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Sequencing logic
|
|
226
|
+
|
|
227
|
+
- **Phase 0 → 1 → 2 are strictly ordered.** Each unblocks the next.
|
|
228
|
+
- **Phase 3, 4, 5 can in principle parallelize** once Phases 1–2 land. In practice Phase 3 (TUI) is the biggest piece and probably ships first; the plugin shims (Phases 4–5) are small once the dispatch table is the single source of truth.
|
|
229
|
+
- **Phase 6 happens after** all of 3/4/5 work end-to-end.
|
|
230
|
+
|
|
231
|
+
If we're behind: Phase 3 is the load-bearing one for "v1 worth releasing." Phases 4–5 can slip to v1.1 if needed (they unlock the better-discoverability story but don't change capability). Phase 6 cannot slip.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Out of scope at v1.0
|
|
236
|
+
|
|
237
|
+
(Confirmed via OPEN_QUESTIONS Q10.)
|
|
238
|
+
|
|
239
|
+
- Multi-host adapters — v1.x. GitLab tracked at [#8](https://github.com/cdhagmann/gem-contribute/issues/8); Codeberg/sourcehut not yet ticketed.
|
|
240
|
+
- gem.coop-exclusive gems — v1.x. Mirrored gems work today; dedicated-namespace gems need a Resolver fallback to the gem.coop API. Tracked at [#50](https://github.com/cdhagmann/gem-contribute/issues/50).
|
|
241
|
+
- World-map TUI fragment — post-v1, awaits adoption. Tracked indirectly via [#5](https://github.com/cdhagmann/gem-contribute/issues/5)'s acceptance criteria (which also owns the `KICKED_THE_TIRES.yml` data source). [needs dedicated issue when ready to build]
|
|
242
|
+
- Private repos / `repo` OAuth scope — post-v1, no issue
|
|
243
|
+
- PR creation from inside the TUI — design choice, browser-based stays (ADR-0011)
|
|
244
|
+
- AI-anything (ADR-0007)
|
|
245
|
+
- Label normalization (ADR-0005)
|
|
246
|
+
- CONTRIBUTING parsing (ADR-0007)
|
|
247
|
+
|
|
248
|
+
🌱 [#5](https://github.com/cdhagmann/gem-contribute/issues/5) itself stays open indefinitely as a sandbox for new contributors to practice the `fix` → `submit` loop.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Issue tracking
|
|
253
|
+
|
|
254
|
+
All roadmap work is tracked on the issue tracker. Filter by `phase:N` label to see a phase's work, e.g. [phase:1](https://github.com/cdhagmann/gem-contribute/labels/phase%3A1).
|
|
255
|
+
|
|
256
|
+
| Phase | Issue range | Notes |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| 0 | #23, #24 | Two doc sweeps |
|
|
259
|
+
| 1 | #25–#28 | Service layer (ADR-0012) |
|
|
260
|
+
| 2 | #29–#31 | CLI output pipeline |
|
|
261
|
+
| 3 | umbrella #2 + sub-issues #32–#37, plus #13 | Five fragments + launch wiring |
|
|
262
|
+
| 4 | #38 | Bundler plugin |
|
|
263
|
+
| 5 | #39 | RubyGems plugin |
|
|
264
|
+
| 6 | #40–#47, plus #1, #3, #9, #10 | Release infra + pre-existing polish |
|
|
265
|
+
|
|
266
|
+
Out-of-scope items don't get phase labels. [#5](https://github.com/cdhagmann/gem-contribute/issues/5) (sandbox) and [#8](https://github.com/cdhagmann/gem-contribute/issues/8) (GitLab adapter, v1.x) live without phase labels.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR 0006: Standalone gem, not a Bundler plugin
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** Partially superseded — the standalone-gem decision stands; the no-Bundler-plugin decision is **reversed by [ADR-0014](0014-ship-bundler-and-rubygems-plugins.md)**. Earlier amendment by [ADR-0012](0012-output-free-service-objects-three-interface-architecture.md) (which added a RubyGems plugin) is now itself superseded by ADR-0014's single-gem packaging.
|
|
4
4
|
**Date:** 2026-04-27
|
|
5
5
|
|
|
6
6
|
## Context
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ADR 0008: Use Rooibos for the TUI layer
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** Superseded by [ADR-0010](0010-charm-ruby-tui-framework.md). **Substance subsequently restored by [ADR-0013](0013-revert-to-rooibos.md)**, which superseded ADR-0010 — Rooibos is once again the TUI framework, with new framing. Read this ADR for the original technical reasoning; read ADR-0013 for the current decision header.
|
|
4
4
|
**Date:** 2026-04-27
|
|
5
5
|
**Supersedes parts of:** the original "TUI built directly on `ratatui_ruby`" approach implied by ADR-0001 and the `docs/design.md` v1.
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
`gem-contribute` is a TUI that needs to:
|
|
10
10
|
|
|
11
11
|
1. Make multiple HTTP calls (RubyGems API, GitHub API, GitHub OAuth device flow polling) without freezing the UI.
|
|
12
|
-
2. Shell out to `git` for
|
|
12
|
+
2. Shell out to `git` for the `fix` flow, also without freezing the UI.
|
|
13
13
|
3. Compose four primary views (project list → issues → issue detail → CONTRIBUTING) with the auth-prompt flow able to interrupt any of them.
|
|
14
14
|
4. Be testable enough that the gem can be maintained past the conference without breaking on every PR.
|
|
15
15
|
|
|
@@ -41,7 +41,7 @@ Use Rooibos as the TUI framework. `ratatui_ruby` remains a transitive dependency
|
|
|
41
41
|
|
|
42
42
|
## Consequences
|
|
43
43
|
|
|
44
|
-
**On the design doc:** the "Modules" section needs revision. Views become Rooibos fragments, not bare classes. The `Worker` module disappears —
|
|
44
|
+
**On the design doc:** the "Modules" section needs revision. Views become Rooibos fragments, not bare classes. The `Worker` module disappears — `fix` is a sequence of Commands emitted from `Update`. The architecture diagram becomes MVU-shaped. Testing strategy shifts from "test the boundaries, skip the TUI" to "test the Update functions everywhere."
|
|
45
45
|
|
|
46
46
|
**On dependencies:** add `rooibos` to the gemspec. Pin to `~> 0.7.0` for v0.1 (allows patch updates within 0.7, blocks 0.8+ until we audit). Bump deliberately, with an ADR if the bump requires meaningful changes.
|
|
47
47
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# ADR 0010: Use Charm-Ruby (bubbletea + lipgloss) for the TUI layer
|
|
2
|
+
|
|
3
|
+
**Status:** **Superseded by [ADR-0013](0013-revert-to-rooibos.md)** (2026-05-03). Bubbletea-ruby was a workshop-driven choice; once the workshop concluded, ADR-0013 restored Rooibos as the TUI framework.
|
|
4
|
+
**Date:** 2026-05-02
|
|
5
|
+
**Supersedes:** [ADR-0008](0008-rooibos-tui-framework.md) — but see ADR-0013, which reverses this supersession.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
[ADR-0008](0008-rooibos-tui-framework.md) chose Rooibos (on `ratatui_ruby`) as the TUI framework. That decision was made on 2026-04-27, *before* `bubbletea-ruby` (Marco Roth's Ruby bindings to Charm's Bubble Tea) existed. Bubbletea-ruby was first published on 2025-12-26 and is now at 0.1.4 (March 2026); the companion styling library, `lipgloss-ruby`, is at 0.2.2.
|
|
10
|
+
|
|
11
|
+
Stage 3 (the TUI work, [issue #2](https://github.com/cdhagmann/gem-contribute/issues/2)) has not started, so the cost of changing this decision is at its lifetime minimum.
|
|
12
|
+
|
|
13
|
+
## Decision
|
|
14
|
+
|
|
15
|
+
Use **bubbletea-ruby** as the TUI framework, with **lipgloss-ruby** for styling. This replaces Rooibos and removes `ratatui_ruby` from the dependency tree.
|
|
16
|
+
|
|
17
|
+
## Reasoning
|
|
18
|
+
|
|
19
|
+
**Idiomatic Ruby surface.** Bubbletea-ruby's API is plain Ruby — `class Foo; include Bubbletea::Model; def init; def update(msg); def view; end`. Rooibos uses lambda-as-constants (`Init = ->`, `View = ->`), which ADR-0008 itself called out as a real onboarding cost for Rails developers. For Blue Ridge Ruby 2026, lower onboarding cost matters.
|
|
20
|
+
|
|
21
|
+
**Battle-tested core.** The Go bubbletea library is the dominant TUI framework in the Go ecosystem — used by `gh`, `glow`, the Charm CLI suite, and dozens of other production tools. Bubbletea-ruby is a young binding (0.1.x), but the rendering and event-loop semantics it wraps are mature in a way that no pure-Ruby alternative is.
|
|
22
|
+
|
|
23
|
+
**Workshop transferability.** "You're learning the Ruby flavor of Bubble Tea" is a stronger pitch than "you're learning Rooibos." MVU is the transferable mental model; Bubble Tea is the largest MVU-TUI ecosystem to walk into afterward.
|
|
24
|
+
|
|
25
|
+
**Install friction is acceptable on workshop hardware.** Both frameworks ship precompiled binaries via the standard Ruby gem-platform mechanism. On a typical Mac/Linux workshop laptop, `gem install` pulls a binary; no Rust or Go toolchain on the user's machine. Verified on rubygems.org:
|
|
26
|
+
|
|
27
|
+
| Platform | ratatui_ruby (1.5.0) | bubbletea (0.1.4) / lipgloss (0.2.2) |
|
|
28
|
+
|----------------------------|-----------------------|---------------------------------------|
|
|
29
|
+
| macOS arm64 | precompiled | precompiled |
|
|
30
|
+
| macOS x86_64 | source build | precompiled |
|
|
31
|
+
| Linux x86_64 gnu | precompiled | precompiled |
|
|
32
|
+
| Linux x86_64 musl | source build | precompiled |
|
|
33
|
+
| Linux arm64 (gnu/musl) | source build | precompiled |
|
|
34
|
+
| Windows | precompiled | source build |
|
|
35
|
+
|
|
36
|
+
Bubbletea wins broadly on Mac+Linux; ratatui_ruby wins on Windows. For a regional Ruby conference, that asymmetry favors bubbletea.
|
|
37
|
+
|
|
38
|
+
**Companion ecosystem.** Lipgloss-ruby provides idiomatic styling; the Charm-Ruby umbrella (charm-ruby.dev) signals an ongoing port effort, not a one-off binding.
|
|
39
|
+
|
|
40
|
+
## Tradeoffs accepted
|
|
41
|
+
|
|
42
|
+
**Project-shaped Command primitives are gone.** Rooibos provides `Command.system`, `Command.http`, `Command.wait`, `Command.cancel` as first-class primitives matching this project's exact verbs. Bubbletea-ruby follows the Go idiom: a Command is a closure that returns a message. We write thin helpers — `http_command(url, envelope:)`, `system_command(argv, envelope:)`, `wait_command(seconds, envelope:)` — once, and use them throughout. This is the only meaningful piece of Rooibos value we reproduce ourselves.
|
|
43
|
+
|
|
44
|
+
**Test helpers unverified.** Rooibos shipped pure-function-Update testing + snapshot helpers + headless terminal style assertions. Bubbletea-ruby's testing story is not yet documented in its README. Pre-merge of Stage 3, verify what bubbletea-ruby ships and either use it, port `teatest` patterns from Go's bubbletea, or build a minimal snapshot harness. Acceptable risk because pure-function `update` is testable on its own.
|
|
45
|
+
|
|
46
|
+
**Younger Ruby surface.** Bubbletea-ruby is at 0.1.4; Rooibos was at 0.7/0.8. Both are pre-1.0 with API-change risk; bubbletea-ruby has had less time to settle. Mitigation: pin bubbletea-ruby and lipgloss to known-good versions; bump deliberately with verification.
|
|
47
|
+
|
|
48
|
+
**Two native gems vs one.** Bubbletea-ruby and lipgloss-ruby are both Go-built native gems; Rooibos was pure Ruby on top of one native gem (`ratatui_ruby`). The user-visible difference is marginal — both `bundle install` to a precompiled binary on common platforms.
|
|
49
|
+
|
|
50
|
+
**Maintainer alignment.** Rooibos and `ratatui_ruby` were both Kerrick Long: one mind, one ecosystem. Charm-Ruby splits maintainership across charmbracelet (Go upstream) and Marco Roth (Ruby bindings, plus many other projects). When Go upstream bumps an API, Marco's lag determines our exposure. Mitigation: pin to a known-good version; bump deliberately.
|
|
51
|
+
|
|
52
|
+
**Windows attendees compile.** No precompiled bubbletea binary for Windows. Document the source-build path in `docs/workshop.md` for any Windows attendee.
|
|
53
|
+
|
|
54
|
+
## Alternatives considered
|
|
55
|
+
|
|
56
|
+
- **Stay on Rooibos.** ADR-0008's reasoning is no longer current. The "lambda-as-constant style is unfamiliar" cost it identified is removed by switching, and the platform matrix favors bubbletea on the workshop's expected hardware.
|
|
57
|
+
- **Bare bubbletea (no lipgloss).** Bubbletea handles the rendering loop; lipgloss handles styling. Skipping lipgloss means hand-building style helpers. Not worth it — lipgloss is small, well-shaped, and idiomatic to use alongside bubbletea.
|
|
58
|
+
- **Wait for bubbletea-ruby 1.0.** Same logic as ADR-0008 declining to wait for Rooibos 1.0: the architectural fit is good, the change cost rises every week we delay, and pinning is sufficient mitigation.
|
|
59
|
+
|
|
60
|
+
## Consequences
|
|
61
|
+
|
|
62
|
+
**On dependencies:** remove `rooibos` from the gemspec. Add `bubbletea` (`~> 0.1.4`) and `lipgloss` (`~> 0.2.2`). Drop the `ratatui_ruby` Rust-toolchain warning from `docs/workshop.md`.
|
|
63
|
+
|
|
64
|
+
**On `docs/design.md`:** the TUI-layer section needs reworking — fragments become bubbletea models composed by routing, lambda-as-constant examples are replaced with idiomatic class-with-mixin examples, the Command list (`Command.http`, etc.) is replaced with the wrapper helpers defined above.
|
|
65
|
+
|
|
66
|
+
**On `CLAUDE.md`:** the working-agreement bullet "Async work is always a Rooibos Command" becomes "Async work is always a bubbletea Command." The package-pinning note about Rooibos is replaced with bubbletea/lipgloss pins.
|
|
67
|
+
|
|
68
|
+
**On ADR-0008:** marked Superseded by ADR-0010.
|
|
69
|
+
|
|
70
|
+
**On [issue #2](https://github.com/cdhagmann/gem-contribute/issues/2):** rewritten to point at bubbletea-ruby + lipgloss-ruby instead of Rooibos.
|
|
71
|
+
|
|
72
|
+
**On the workshop:** attendees still learn MVU, but the framing is "Charm-style TUI in Ruby" — the same pattern as `gh`, `glow`, and the Charm CLIs they may already know.
|
|
73
|
+
|
|
74
|
+
**On testing:** before Stage 3 lands, verify bubbletea-ruby's test helpers. If absent, port `teatest` patterns or build a minimal snapshot harness. Pure-function `update` testability is independent of the framework choice.
|
|
75
|
+
|
|
76
|
+
**On the maintainer relationship:** reach out to Marco Roth before the workshop to mention "we're building a workshop project on bubbletea-ruby for Blue Ridge Ruby 2026" — same etiquette ADR-0008 prescribed for Kerrick Long.
|
|
77
|
+
|
|
78
|
+
## What this *doesn't* change
|
|
79
|
+
|
|
80
|
+
- ADR-0001 (just-in-time auth). MVU shape preserved; framework change.
|
|
81
|
+
- ADR-0002, ADR-0003, ADR-0004 (data layer). Outside the TUI.
|
|
82
|
+
- ADR-0005, ADR-0007 (render verbatim, no parsing). Display contract; framework choice doesn't affect it.
|
|
83
|
+
- ADR-0006 (standalone gem, not Bundler plugin). Packaging concern; orthogonal.
|
|
84
|
+
- ADR-0009 (top-level namespace). Orthogonal.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# ADR 0011: HostAdapter owns host verbs; Operations compose them; CLI verbs compose Operations
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-05-02
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
`CLI::Fork` (and, by delegation, `CLI::Fix`) currently fuses four concerns into one class:
|
|
9
|
+
|
|
10
|
+
1. CLI shell — argv parsing, usage errors, summary, post-clone hooks.
|
|
11
|
+
2. Host-API ceremony — call `adapter.fork`, poll `adapter.fork_ready?` in a 12×5s loop, branch on `adapter.already_forked?`.
|
|
12
|
+
3. VCS work on the local filesystem — `git clone` into `<root>/<owner>/<repo>`, reuse if `.git` exists, add an `upstream` remote.
|
|
13
|
+
4. Host-specific URL templating — `https://github.com/<owner>/<repo>.git` is hardcoded at [fork.rb:51](../../lib/gem_contribute/cli/fork.rb).
|
|
14
|
+
|
|
15
|
+
ADR-0001 (and the design doc) already commit to GitLab and Codeberg adapters as a near-term goal. The hardcoded `github.com` literal and the GitHub-shaped readiness loop both block that. A previous refactor (commit 3d53ffc, "dissolve ForkClone into Fork") pulled in the opposite direction: it merged the bootstrap primitive into the CLI verb. That made the multi-host port harder, not easier.
|
|
16
|
+
|
|
17
|
+
Two reasonable shapes for splitting:
|
|
18
|
+
|
|
19
|
+
1. **Wider adapter, thicker CLI verbs.** Push everything host-specific into `HostAdapter` (`fork`, `comment`, `pull_request_url`, `clone_url`); CLI verbs compose adapter calls and `Git` calls directly.
|
|
20
|
+
2. **Adapter + Operations layer.** Same wider adapter, plus a thin layer of host-agnostic primitives (`Operations::Fork`, `Operations::Clone`) that compose `HostAdapter` and `Git`. CLI verbs compose Operations.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Adopt shape 2: a three-layer split.
|
|
25
|
+
|
|
26
|
+
- **`HostAdapter`** owns *every* host-API verb. Concrete methods: `fork`, `comment`, `pull_request_url`, `clone_url`, plus the existing reads (`issues`, `issue`, `issue_comments`, `community_profile`, `file_contents`, `search_issues`) and identity (`viewer_login`).
|
|
27
|
+
- **`Operations::Fork`** and **`Operations::Clone`** are the bootstrap primitives. They depend on a `HostAdapter` and (for Clone) a `Git`. They produce the local clone path the CLI verbs need.
|
|
28
|
+
- **`CLI::Fork`** and **`CLI::Fix`** parse argv, resolve a `Project`, compose the Operations primitives, print summaries, and run post-clone hooks. Nothing else.
|
|
29
|
+
|
|
30
|
+
Three sub-decisions that shape the adapter's surface:
|
|
31
|
+
|
|
32
|
+
- **`fork(project)` is idempotent and blocks until ready.** The 12×5s polling loop moves *into* the GitHub adapter. Callers ask for "fork this and give me a working clone URL"; the adapter decides whether that needs a poll, a single request, or something else. `fork_ready?` and `already_forked?` become private details.
|
|
33
|
+
- **PR creation stays browser-based; the adapter exposes `pull_request_url(...)`.** Today `submit` deliberately opens a pre-filled compare page so the user reviews PR text before submitting. We keep that UX. The adapter's job is to construct the host-correct compare URL; GitLab returns a `merge_requests/new` URL, GitHub returns a `compare` URL, etc.
|
|
34
|
+
- **`clone_url(project)` replaces the hardcoded `https://github.com/...` literal.** The Operations layer asks the adapter; the adapter knows its own host.
|
|
35
|
+
|
|
36
|
+
## Reasoning
|
|
37
|
+
|
|
38
|
+
The data layer / TUI layer split in [`docs/design.md`](../design.md) is built around adapters being swappable. That bet pays off only if "swap in a GitLab adapter" really is a weekend project. Today it isn't — a GitLab port would have to fork (no pun intended) the GitHub-shaped readiness loop, the hardcoded clone URL, and the same-shape compare URL out of `CLI::Fork` and `CLI::Submit`. With this split, a GitLab port is: implement `HostAdapter#fork` (likely no poll), `#clone_url` (return `https://gitlab.com/...`), `#pull_request_url` (return the GitLab MR URL form), `#comment`. Operations and CLI don't change.
|
|
39
|
+
|
|
40
|
+
The Operations layer (rather than a fatter CLI) earns its keep because two things in the bootstrap aren't host-API and aren't raw git either: the *clone-or-reuse* policy (skip clone if `.git` exists) and the *upstream remote* policy (always add `upstream` pointing at the canonical repo's `clone_url`). Those are gem-contribute conventions, not git primitives, and they're shared between `fix` and `fork`. Putting them in their own class makes them testable in isolation and keeps the CLI verbs honestly thin.
|
|
41
|
+
|
|
42
|
+
This direction supersedes the merge in 3d53ffc. That commit was right that `ForkClone` and `Fork` had drifted into near-duplicates; it was wrong that the resolution was to dissolve the primitive into the verb. The correct resolution was to *re-extract* the primitive at a sharper boundary — which is what this ADR does.
|
|
43
|
+
|
|
44
|
+
## Alternatives considered
|
|
45
|
+
|
|
46
|
+
- **Wider adapter, no Operations layer (shape 1 above).** Simpler — one fewer namespace. Rejected because the clone-or-reuse and upstream-remote policies don't belong on `HostAdapter` (they're not host-API) or on `Git` (they're gem-contribute policy on top of git). Without an Operations layer they leak into CLI verbs, where they get duplicated between `fork` and `fix`.
|
|
47
|
+
- **Move PR creation to API-based (`adapter.create_pull_request`).** Rejected — the deliberate UX in [`submit.rb`](../../lib/gem_contribute/cli/submit.rb) is that the user reviews PR text in the browser before submitting. That's a v1 product decision, not an artifact of laziness.
|
|
48
|
+
- **Keep `fork_ready?` / `already_forked?` on the public adapter interface.** Rejected as the default. They're GitHub-shaped (the 202-then-poll dance is a GitHub artifact). Hiding them behind an idempotent, blocking `fork` lets each host implement readiness however it actually works. We can re-expose them later if a real caller needs them.
|
|
49
|
+
|
|
50
|
+
## Consequences
|
|
51
|
+
|
|
52
|
+
- `HostAdapter` grows: `fork` semantics tighten (idempotent, blocking), `comment` replaces `comment_on_issue`, new methods `pull_request_url` and `clone_url`. `fork_ready?` and `already_forked?` come off the public interface.
|
|
53
|
+
- New namespace `GemContribute::Operations::` housing `Fork` and `Clone`.
|
|
54
|
+
- `CLI::Fork` and `CLI::Fix` shrink. The `ensure_fork` / `wait_until_ready` / `clone_into_root` privates move out.
|
|
55
|
+
- The hardcoded `github.com` URL literal at [fork.rb:51](../../lib/gem_contribute/cli/fork.rb) goes away.
|
|
56
|
+
- [`docs/design.md`](../design.md) needs the `HostAdapter` signature snippet (currently shows the old shape) and a paragraph on the Operations layer.
|
|
57
|
+
- Tests: `Operations::Fork` and `Operations::Clone` get unit specs; the existing `CLI::Fork` and `CLI::Fix` specs shrink (less to assert in the verb itself, more in the primitives).
|
|
58
|
+
- The "no orchestrator class" rule in CLAUDE.md is unchanged: Operations primitives are not orchestrators. They're single-step compositions of an adapter call and (sometimes) a git call. The TUI's `fix` flow remains a state machine in `Update`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ADR 0012: Output-free service objects, dry-monads Result contract, three-interface architecture
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted — partially amended by [ADR-0014](0014-ship-bundler-and-rubygems-plugins.md). The output-free service-layer / dry-monads / dry-operation contract stands. Packaging is changed: ADR-0014 collapses the planned separate `rubygems-contribute` gem into a single `gem-contribute` gem with three entry points, and adds a Bundler plugin alongside.
|
|
4
|
+
**Date:** 2026-05-03
|
|
5
|
+
**Amends:** [ADR-0006](0006-standalone-gem-not-plugin.md) — adds a RubyGems plugin as a third interface; the Bundler plugin decision was unchanged at the time of this ADR (subsequently reversed by ADR-0014).
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`Operations::Fork` and `Operations::Clone` accept `stdout:` and print progress lines as side effects during `call`. This couples service objects to a specific output model and breaks down across the three interfaces now in scope:
|
|
10
|
+
|
|
11
|
+
- **CLI** (`gem-contribute`): stdout/stderr strings, synchronous.
|
|
12
|
+
- **TUI** (bubbletea, ADR-0010): no output stream — results travel as messages delivered by Commands to `Update`, which renders model state.
|
|
13
|
+
- **gem plugin** (`gem contribute`): a RubyGems plugin (not a Bundler plugin — see ADR-0006) that reuses the CLI pipeline but is a distinct interface layer.
|
|
14
|
+
|
|
15
|
+
Injecting an `Output::Tui` into service objects would require it to dispatch callbacks back into bubbletea's async event loop from a synchronous call site inside a Command. That is complex, depends on unverified bubbletea-ruby API surface (ADR-0010 flagged this explicitly), and routes information through a mechanism it doesn't belong in.
|
|
16
|
+
|
|
17
|
+
Separately, several places in the codebase signal failure by returning `nil` and printing to `stderr` as a side effect (notably `Workflow#build_adapter`). This mixes concerns: the service layer decides what to say, and callers are left checking `nil` without knowing why.
|
|
18
|
+
|
|
19
|
+
Workshop constraints (Blue Ridge Ruby 2026) that previously argued against dry-rb ecosystem dependencies no longer apply — the workshop concluded 2026-05-02.
|
|
20
|
+
|
|
21
|
+
## Decision
|
|
22
|
+
|
|
23
|
+
1. **Service objects are output-free.** `Operations::*` and all data-layer classes accept no `stdout:` or `stderr:` parameter and produce no I/O side effects.
|
|
24
|
+
|
|
25
|
+
2. **Service objects return `dry-monads` `Result` types.** `Success(value)` on the happy path, `Failure(reason)` for expected error conditions. Typed exceptions (`AuthRequired`, `AdapterError`) are no longer used as cross-layer control flow; they may still be raised and rescued within a single layer.
|
|
26
|
+
|
|
27
|
+
3. **Multi-step pipelines use `dry-operation`.** The fork → clone → branch → announce sequence in `fix` is expressed as a `dry-operation` pipeline: each step receives and enriches a shared input, and failure at any step short-circuits the chain.
|
|
28
|
+
|
|
29
|
+
4. **Three interface layers share service objects and own their own output.**
|
|
30
|
+
- CLI: prints around service calls using `Output::Standard` (wraps `stdout`/`stderr`; exposes `#info`, `#warn`, `#error`).
|
|
31
|
+
- TUI: wraps service calls in Commands; `Update` renders the returned `Success`/`Failure` as model state.
|
|
32
|
+
- gem plugin: reuses the CLI pipeline; does not launch the TUI.
|
|
33
|
+
|
|
34
|
+
5. **`Output::Standard` and `Output::Null` live in the interface layer only.** Service objects never see them.
|
|
35
|
+
|
|
36
|
+
## Reasoning
|
|
37
|
+
|
|
38
|
+
**Output-free is the only shape that works across all three interfaces.** The TUI has no output stream — injecting one requires plumbing that doesn't exist yet (bubbletea-ruby's callback story is unverified). Output-free service objects with Result return types leave each interface free to handle output in the way natural to it.
|
|
39
|
+
|
|
40
|
+
**`dry-monads` Result over exceptions for cross-layer signaling.** Auth failure and adapter errors are expected outcomes, not exceptional conditions — the adapter will regularly encounter them on the happy path (rate limits, unauthenticated users, forks that already exist). `Failure(:unauthenticated)` makes the call site enumerate every outcome explicitly rather than knowing which exceptions to rescue. Ruby 3.2 `case/in` pattern matching on `Success`/`Failure` is readable and idiomatic.
|
|
41
|
+
|
|
42
|
+
**`dry-operation` over manual step composition.** The fix pipeline currently threads state through sequential calls with early returns on `nil`. `dry-operation` names each step, makes its `Success`/`Failure` contract explicit, and allows testing each step in isolation. Adding `dry-operation` pulls in `dry-monads` transitively, so one gemspec entry covers both.
|
|
43
|
+
|
|
44
|
+
**RubyGems plugin is a distinct interface, not covered by ADR-0006.** ADR-0006 rejected the *Bundler* plugin pattern (`bundle contribute`). A RubyGems plugin (`gem contribute`) registers a `Gem::Command` in a gem named `rubygems-contribute` — a different mechanism, not considered in ADR-0006. The three-interface architecture creates a natural home for it: it reuses the CLI pipeline without touching the TUI.
|
|
45
|
+
|
|
46
|
+
## Alternatives considered
|
|
47
|
+
|
|
48
|
+
- **`Output` abstraction injected into service objects** (`Output::Standard`, `Output::Tui`). Rejected: `Output::Tui` requires a dispatch callback into bubbletea's event loop from a synchronous call site inside an async Command. Complex, unverified, routes output through the wrong abstraction.
|
|
49
|
+
|
|
50
|
+
- **Keep `stdout:` injection; pass a null output to the TUI.** Rejected: silently drops progress information that the TUI should surface as model state. Information is lost rather than translated.
|
|
51
|
+
|
|
52
|
+
- **`dry-transaction` instead of `dry-operation`.** Rejected: `dry-transaction` is deprecated by the dry-rb team. `dry-operation` is the current recommendation with the same step-composition semantics.
|
|
53
|
+
|
|
54
|
+
- **Native Ruby only (`Data.define` results + typed exceptions + `case/in`).** Valid; Ruby 3.2 has most of the surface. Rejected now that workshop constraints are lifted: `dry-monads` provides a richer failure vocabulary, Do notation reduces boilerplate in multi-step callers, and `dry-operation` formalizes pipeline shape more explicitly than manual early-returns.
|
|
55
|
+
|
|
56
|
+
## Consequences
|
|
57
|
+
|
|
58
|
+
**On `Operations::Fork` and `Operations::Clone`:** remove `stdout:`. Both return `Success(Result)` or `Failure(reason)`. `Operations::Clone::Result` gains a `reused:` field (mirroring `Operations::Fork::Result`) so CLI callers can print the appropriate message without asking the operation what happened.
|
|
59
|
+
|
|
60
|
+
**On `Workflow#build_adapter`:** remove `nil`-returning and stderr side effect. Return `Success(adapter)` or `Failure(:unauthenticated)`. Callers pattern-match.
|
|
61
|
+
|
|
62
|
+
**On `CLI::Fork#execute` and `CLI::Fix#execute`:** print progress and results around service calls using `Output::Standard` rather than raw `@stdout`/`@stderr`.
|
|
63
|
+
|
|
64
|
+
**On `CLI::Fix`:** the fork → clone → branch → announce sequence becomes a `dry-operation` pipeline.
|
|
65
|
+
|
|
66
|
+
**On dependencies:** add `dry-operation` to the gemspec (verify current version on rubygems.org before pinning; `~> 0.1` at time of writing). `dry-monads` is pulled in transitively but may be listed explicitly for clarity.
|
|
67
|
+
|
|
68
|
+
**On the gem plugin:** a future `rubygems-contribute` gem registers a `Gem::Command` and delegates to the CLI pipeline. This ADR establishes its architectural home; a separate ADR is not required for its internal structure.
|
|
69
|
+
|
|
70
|
+
**On ADR-0006:** status updated to note that the Bundler plugin decision is unchanged, but a RubyGems plugin interface is now explicitly in scope under ADR-0012.
|
|
71
|
+
|
|
72
|
+
**On `CLAUDE.md`:** the working-agreement bullet "Async work is always a bubbletea Command" gains a companion: "Service objects return `dry-monads` `Result` types and produce no output."
|
|
73
|
+
|
|
74
|
+
## What this doesn't change
|
|
75
|
+
|
|
76
|
+
- ADR-0001 (just-in-time auth). The auth flow's shape is unchanged; the signaling mechanism moves from exception to `Failure`.
|
|
77
|
+
- ADR-0002 through ADR-0005 (data layer). Parsers and resolvers are already output-free.
|
|
78
|
+
- ADR-0010 (bubbletea + lipgloss). Framework choice unchanged; this ADR defines the contract those Commands return.
|
|
79
|
+
- ADR-0011 (HostAdapter owns host verbs). The adapter interface is unchanged; its error conditions now propagate as `Failure` rather than raised exceptions at the cross-layer boundary.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# ADR 0013: Revert TUI framework to Rooibos
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-05-03
|
|
5
|
+
**Supersedes:** [ADR-0010](0010-charm-ruby-tui-framework.md)
|
|
6
|
+
**Restores substance of:** [ADR-0008](0008-rooibos-tui-framework.md) (with a new framing)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
[ADR-0010](0010-charm-ruby-tui-framework.md) (2026-05-02) chose bubbletea-ruby + lipgloss-ruby over Rooibos. Two reasons drove that decision:
|
|
11
|
+
|
|
12
|
+
1. **Workshop onboarding cost.** Rooibos's lambda-as-constant style (`Init = ->`, `View = ->`) was identified in ADR-0008 itself as unfamiliar to Rails developers. ADR-0010 cited this as a real cost for Blue Ridge Ruby 2026 attendees.
|
|
13
|
+
2. **Idiomatic Ruby surface.** Bubbletea-ruby's class/mixin shape (`include Bubbletea::Model`) was framed as a more natural Ruby surface for the workshop audience.
|
|
14
|
+
|
|
15
|
+
The Blue Ridge Ruby workshop concluded 2026-05-02. ADR-0010's primary justification is no longer load-bearing. At the same time, a new feature has entered the v1 roadmap: a near-easter-egg "world map" view ([issue #5](https://github.com/cdhagmann/gem-contribute/issues/5)) showing the locations of users who've kicked the tires on the tool. The feature itself ships post-v1 (until adoption is large enough for the data to be interesting), but the framework choice locks in now.
|
|
16
|
+
|
|
17
|
+
Stage 3 (the TUI work) has not started. The cost of changing the framework decision is at its lifetime minimum.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
Use **Rooibos** as the TUI framework. `ratatui_ruby` is the rendering layer Rooibos sits on top of. This is the framework choice originally made in ADR-0008.
|
|
22
|
+
|
|
23
|
+
## Reasoning
|
|
24
|
+
|
|
25
|
+
**The workshop-onboarding argument is gone.** The single biggest reason ADR-0010 gave for switching no longer applies. The lambda-as-constant style is a mild stylistic cost for any Ruby developer; it's not a structural barrier. Maintainers and contributors going forward will be self-selected developers who chose to work on this codebase, not workshop attendees parachuted in for a weekend.
|
|
26
|
+
|
|
27
|
+
**Preserving the world-map view as a future option.** Rooibos sits on `ratatui_ruby`, which exposes the full Ratatui widget surface (canvas, custom blocks, programmable rendering). Bubbletea-ruby is a binding to a different rendering model with a narrower extension story. We don't want to bet against a feature we already know we want to build.
|
|
28
|
+
|
|
29
|
+
**ADR-0008's original technical reasoning still stands.**
|
|
30
|
+
|
|
31
|
+
- `Command.http`, `Command.system`, `Command.wait`, `Command.cancel` map exactly to the project's verbs (HTTP to GitHub, shelling out to `git`, polling the device-flow endpoint). Bubbletea-ruby followed the Go idiom of "a Command is a closure that returns a message" and required us to write our own thin helpers for these.
|
|
32
|
+
- Rooibos shipped pure-function `Update` testing, snapshot helpers, and a headless terminal style-assertion harness at 0.7. ADR-0010 explicitly flagged bubbletea-ruby's testing story as unverified and a risk to mitigate post-decision.
|
|
33
|
+
- Rooibos's Router DSL maps to the four-fragment design directly. Bubbletea-ruby required us to build the routing.
|
|
34
|
+
|
|
35
|
+
**Same maintainer as `ratatui_ruby`.** Reduces the chance of cross-library impedance mismatch — the same property ADR-0008 cited.
|
|
36
|
+
|
|
37
|
+
## Tradeoffs accepted
|
|
38
|
+
|
|
39
|
+
- **Pre-1.0 framework risk** (same as ADR-0008). Rooibos at 0.7 has "APIs may change before 1.0." Mitigation: pin to a known-good version, bump deliberately, ADR if a bump requires meaningful changes.
|
|
40
|
+
- **Rust toolchain on source-build platforms.** `ratatui_ruby` is precompiled for the common platforms but builds from source on Linux musl, Linux arm64, and macOS x86_64. Document in `MAINTAINER.md` and the README.
|
|
41
|
+
- **Lambda-as-constant style** stays, but is now a maintainer/contributor concern rather than a workshop-attendee concern. Acceptable.
|
|
42
|
+
|
|
43
|
+
## Alternatives considered
|
|
44
|
+
|
|
45
|
+
- **Stay on bubbletea-ruby (ADR-0010).** Rejected: the workshop justification has expired, the Command primitives don't match the project's verbs without writing our own helpers, and the world-map view is a non-trivial extension on top of the bubbletea rendering model.
|
|
46
|
+
- **Wait for Rooibos 1.0.** Same logic ADR-0008 used to reject this: 1.0 timeline unknown, the architectural fit is too good to defer, pinning is sufficient mitigation.
|
|
47
|
+
|
|
48
|
+
## Consequences
|
|
49
|
+
|
|
50
|
+
**On dependencies:** add `rooibos` (`~> 0.7.0`) and `ratatui_ruby` to the gemspec. Remove `bubbletea` and `lipgloss` (which were never actually added — ADR-0010 was a paper decision, no code shipped against it).
|
|
51
|
+
|
|
52
|
+
**On `docs/design.md`:** Already framed in Rooibos terms (the bubbletea revision in ADR-0010 was never propagated through). Minor cleanup needed where doc text mentions bubbletea.
|
|
53
|
+
|
|
54
|
+
**On `docs/design-interface-layer.md`:** The TUI pipeline section refers to "bubbletea Command" and "bubbletea-ruby" — replace with Rooibos terminology. The Result-pattern-matching contract is unchanged (per ADR-0012, which is unaffected).
|
|
55
|
+
|
|
56
|
+
**On `CLAUDE.md`:** Already says Rooibos. No change needed.
|
|
57
|
+
|
|
58
|
+
**On the workshop docs:** Workshop is over; archive `docs/workshop.md` and any workshop-specific framing.
|
|
59
|
+
|
|
60
|
+
**On ADR-0010:** marked Superseded by ADR-0013.
|
|
61
|
+
|
|
62
|
+
**On ADR-0008:** Already superseded by ADR-0010; that link stands. The fact that ADR-0013 restores ADR-0008's *substance* is captured in this ADR's header rather than reopening the older one.
|
|
63
|
+
|
|
64
|
+
## What this *doesn't* change
|
|
65
|
+
|
|
66
|
+
- ADR-0001 (just-in-time auth). Same MVU-shaped flow.
|
|
67
|
+
- ADR-0002, ADR-0003, ADR-0004, ADR-0005, ADR-0007, ADR-0009 (data layer / display rules). Outside the TUI.
|
|
68
|
+
- ADR-0006 (standalone gem). Packaging concern; orthogonal.
|
|
69
|
+
- ADR-0011 (HostAdapter owns host verbs). Service layer; orthogonal.
|
|
70
|
+
- ADR-0012 (output-free service objects, dry-monads). The contract is framework-independent — Rooibos has the same "Commands return messages, not stdout" property bubbletea did, so the substance carries over unchanged.
|
|
71
|
+
- ADR-0014 (Bundler + RubyGems plugins as v1 interfaces). Orthogonal — plugins are CLI-only.
|