ADR-0002 — A hand-rolled Exec[F] instead of cats-effect or ZIO

Context

The public API is Future-based (ADR-0005). Internally, the cross-cutting logic — retry, pagination, error mapping — needs to be written once and tested synchronously, because Future-based tests are where flakiness comes from and because mutation testing over asynchronous code is slow and unreliable.

That argues for abstracting the core over some F[_]. The usual way to get that is cats-effect (Sync/MonadError) or ZIO.

Decision

Define a minimal capability typeclass in modules/core:

trait Exec[F[_]]:
  def pure[A](value: A): F[A]
  def raise[A](error: CodebergError): F[A]
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
  def attempt[A](fa: F[A]): F[Either[CodebergError, A]]
  def suspend[A](thunk: () => F[A]): F[A]

with two instances: Either[CodebergError, *] for tests, Future for the published client. Roughly forty lines.

Consequences

Good:

Bad:

Rejected alternatives

Alternative Why rejected
cats-effect Sync Large transitive dependency imposed on every consumer of the library.
ZIO Same, plus it would push the public API toward ZIO and away from ADR-0005.
No abstraction — write everything directly against Future Retry and pagination could then only be tested asynchronously; exactly the flakiness we are avoiding.