Phase 1: ADR-0012 service layer (#25, #26, #27, #28)#48
Merged
Conversation
…Result
Phase 1 of ADR-0012's three-interface architecture: the service layer
becomes output-free and Result-returning so the CLI, future TUI, and
future plugin entry points can share the same code paths.
Adds three runtime deps (`dry-monads`, `dry-operation`, `dry-initializer`,
all pinned `~> latest`); subsequent commits in this branch use the latter
two. This commit only exercises `dry-monads`.
`Operations::Fork` and `Operations::Clone` lose their `stdout:` parameter
and `@stdout.puts` calls, and now return:
Success(Operations::Fork::Result.new(...)) # or
Success(Operations::Clone::Result.new(path:, reused:))
…on the happy path, and one of:
Failure(:unauthenticated)
Failure([:adapter_error, message])
…on expected failures. Adapter exceptions (`AuthRequired`, `AdapterError`)
are caught at the operation boundary and translated to tagged Failures —
they no longer propagate across the service-layer/CLI seam.
`Operations::Clone::Result` is a new `Data.define(:path, :reused)`. The
old `target` path return becomes `result.path`; the `reused` flag mirrors
`Operations::Fork::Result#reused` so callers can render the right
"reusing existing X" line without checking the filesystem themselves.
The progress messages those operations used to print ("Forking
sidekiq/sidekiq...", "Cloning into ...") move to `CLI::Fork#bootstrap`,
which is the seam both `CLI::Fork#execute` and `CLI::Fix#execute`
compose. `bootstrap` itself now returns `Success([path, fork_info])` or
propagates Operations' Failure; both verbs pattern-match the Result and
either continue or render the failure with an appropriate exit code.
`with_workflow_rescues` is intentionally still in place — it now only
catches AdapterErrors from non-Operation paths (`@git.checkout_branch`,
etc.). The next commit (#26) retires it.
Specs: Operations specs assert directly on `Result` values (no stdout
captures). CLI specs stub Operations to return `Success(...)` /
`Failure(...)` and add coverage for the failure paths.
Bundled: two trivial rubocop autocorrects from `bcf273d` that surfaced
when running `bin/rubocop` on the changed files (`Style/Lambda` in
cli.rb, `Layout/EmptyLinesAroundClassBody` in submit.rb).
Closes #25.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…w_rescues Continues Phase 1 of ADR-0012. With Operations now Result-returning (previous commit), the only remaining nil-and-print pattern was `Workflow#build_adapter`, and the only remaining typed-exception rescue shell was `Workflow#with_workflow_rescues`. Both go. `build_adapter` now returns `Success(adapter)` or `Failure(:unauthenticated)`. `CLI::Fix#run` and `CLI::Fork#run` pattern-match on the Result and print the auth-login hint themselves on the Failure branch. `with_workflow_rescues` is removed entirely. The two verbs that used it (Fix, Fork) handle each layer's failures explicitly: - build_adapter Failure(:unauthenticated) → auth-login hint, exit 1 - bootstrap Failure(:adapter_error, msg) → "<verb> failed: <msg>", exit 1 `CLI::Fix#execute` keeps a small targeted `rescue GemContribute::AdapterError` on the outer def, scoped to the post-bootstrap git work that still raises (notably `@git.checkout_branch`). The next commit (#27) folds that into `Operations::Branch` (Result-returning) and the rescue can go away. `CLI::Fork#execute` doesn't need a rescue — every call site after bootstrap is already Result-aware. No test changes: the existing "auth-login hint" specs exercise the verb's full flow and assert on the rendered message, so they pass unchanged through the new Result wiring. Closes #26. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Per ADR-0012 Phase 1, the `fix` flow becomes a four-step dry-operation
pipeline that the CLI verb (and a future TUI Command) call as a single
Result-returning service.
New operations:
* `Operations::Branch` — wraps `git checkout -b gem-contribute/issue-<N>`.
Returns `Success(Result(name:))` or `Failure([:adapter_error, msg])`.
* `Operations::Announce` — posts (or skips) the "working on this"
comment. Output-free; gating (`--no-comment`, config, viewer-self)
is pushed in via the `allow:` argument so the operation itself is
a pure decide/post/skip. Returns `Success(:posted | :skipped)` or
`Failure([:announce_failed, msg])` (informational, not fatal).
* `Operations::FixPipeline` — composes Fork → Clone → Branch → Announce
via `Dry::Operation`. The first three are `step`s (short-circuit on
Failure); Announce is called outside `step` so its Failure does not
propagate as a pipeline-level failure (the fix itself succeeded).
Adds the viewer-self gate (`fork.viewer != project.owner`) on top
of the caller's `allow_announce:` boolean.
CLI/library changes:
* `CLI::Fix` swaps its `fork:` (CLI::Fork instance) dependency for
`pipeline:` (FixPipeline). The verb is now a thin Result-pattern-
matching shell: build the adapter, compute `allow_announce`, call
the pipeline, render the summary and announce outcome. The
targeted `rescue GemContribute::AdapterError` introduced in #26
is gone — the branch step now returns Failure too.
* `CLI::IssueAnnouncer` keeps `fetch_claim_index` (used by `scan` and
`issues` to flag claimed issues) and exposes `MARKER` as an alias
for `Operations::Announce::WORKING_MARKER` so the search query
stays in sync with the marker the operation posts.
Specs: three new operation specs cover Branch, Announce, FixPipeline
(11 new examples). `cli/fix_spec.rb` swaps `fork: fork_cli` stubbing
for `pipeline: pipeline` stubbing and adds explicit coverage for the
allow_announce-gating cases (--no-comment, config off, per-repo
override) and the announce-outcome rendering (Success(:posted),
Success(:skipped), Failure([:announce_failed, ...])).
Closes #27.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The two `# rubocop:disable Metrics/ParameterLists` comments were
load-bearing only because each verb's initializer juggled 9-10 keyword
args plus the `value || Default.new` fallback dance. `dry-initializer`'s
`option :name, default: -> { ... }` syntax expresses the same thing
declaratively, evaluates defaults in declaration order (so a later
option's default can reference an earlier option), and produces the
same external interface.
Notable trickle-through:
* `option :post_clone_hooks` references `stdout` / `stderr`
* `option :pipeline` (Fix) and `option :clone_op` (Fork) reference `git`
Both work because of the in-declaration-order default semantics.
Both rubocop disable directives are gone. No behaviour change; existing
specs pass without modification (the keyword-arg constructor surface is
identical).
Closes #28.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
cdhagmann
pushed a commit
that referenced
this pull request
May 4, 2026
ROADMAP: mark Phase 1 (ADR-0012 service layer) DONE — that work merged via #48 on 2026-05-04 but the doc still listed it as not started. Annotate Phase 2 as in flight with the PR link, and note that basic CI already landed via #21 even though the broader #43 ticket remains open. CHANGELOG: backfill the [Unreleased] entry for Phase 1 internal architecture (was missing entirely), and add user-visible Phase 2 entries for the spinner-in-TTY behavior and the tty-prompt-driven init flow that this PR introduces. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-0012 Phase 1 — the output-free service layer that ROADMAP Phase 1 depends on. One commit per issue, four issues:
1df0bf8Operations::Fork/ClonereturnResulta7ea469Workflow#build_adapterreturnsResult; retirewith_workflow_rescuesd9c670aOperations::Branch+Announce; buildOperations::FixPipelineacc6fa8CLI::Fork/CLI::Fixinitializers todry-initializerNet effect: every
Operations::*class is output-free and returnsSuccess(...)/Failure(...). Thefixflow is composed byOperations::FixPipeline(aDry::Operationsubclass). The CLI verbs are thin Result-pattern-matching shells.What changed
New deps (
gem-contribute.gemspec):dry-monads ~> 1.10,dry-operation ~> 1.1,dry-initializer ~> 3.2.Operations layer:
Operations::Fork,Operations::Clonelosestdout:; returnSuccess(Result)/Failure(reason).Operations::Clone::Result = Data.define(:path, :reused)(was a bare path string).Operations::Branch—git checkout -b gem-contribute/issue-<N>, returnsResult.Operations::Announce— posts/skips the "working on this" comment with internal gating (allow:); returnsSuccess(:posted | :skipped)orFailure([:announce_failed, msg]).Operations::FixPipeline—dry-operationcomposing Fork → Clone → Branch → Announce. Announce is called outsidestepbecause its Failure is informational.CLI verbs:
CLI::Fixswaps itsfork: CLI::Forkdependency forpipeline: Operations::FixPipeline.CLI::Forkkeeps itsbootstrapshape but pattern-matches the newResultreturn.dry-initializerdeclarations — the# rubocop:disable Metrics/ParameterListscomments are gone.Workflow mixin:
build_adapterreturnsSuccess(adapter)/Failure(:unauthenticated)(was nil + stderr).with_workflow_rescuesremoved entirely.Other:
CLI::IssueAnnouncerkeepsfetch_claim_index(used byscanandissues) and exposesMARKERas an alias forOperations::Announce::WORKING_MARKER.bcf273d(Style/Lambda incli.rb, Layout/EmptyLinesAroundClassBody insubmit.rb) bundled into commit 1.Test plan
bin/rspec— 217 examples, 0 failures (was 198 on main; +19 new specs acrossOperations::Branch,Announce,FixPipeline, plus failure-path coverage incli/fork_specandcli/fix_spec)bin/rubocop— cleanfixflow against a real issue once merged (e.g.gem-contribute fix gem-contribute/5)Notes for review
dry-monadspattern matching:Failure([:tag, payload])is the constructor;Failure(:tag, payload)is the pattern.Failure#deconstructusesArray()semantics, which unwraps arrays one level. Trip-wire I hit during commit 1.Dry::Operationsubclassing (not include): in 1.x,Dry::Operationis a class. Body returns a raw value, framework wraps in Success.FixPipeline#callis to call Announce outsidestep.🤖 Generated with Claude Code