# Facade Implementation — Reference

Detailed reference for `Git::Repository::*` facade modules and methods. This file
is loaded by subagents during the [Facade Implementation](SKILL.md) workflow.

## Contents

- [Contents](#contents)
- [Files to generate](#files-to-generate)
- [Topic module selection](#topic-module-selection)
  - [Existing modules](#existing-modules)
  - [Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
    - [Choosing a module for a new facade method](#choosing-a-module-for-a-new-facade-method)
  - [Naming a new topic module](#naming-a-new-topic-module)
- [Designing a facade method](#designing-a-facade-method)
  - [Choosing the return type](#choosing-the-return-type)
  - [Choosing the method signature](#choosing-the-method-signature)
  - [One-line delegator](#one-line-delegator)
  - [Orchestration sequence](#orchestration-sequence)
  - [Sequencing multiple commands](#sequencing-multiple-commands)
- [Topic module skeleton](#topic-module-skeleton)
- [The five facade responsibilities checklist](#the-five-facade-responsibilities-checklist)
- [Argument pre-processing patterns](#argument-pre-processing-patterns)
  - [Path normalization](#path-normalization)
  - [Option whitelisting (preventing API expansion)](#option-whitelisting-preventing-api-expansion)
  - [Deprecation handling](#deprecation-handling)
  - [Defaults and policy options](#defaults-and-policy-options)
- [Internal helpers and encapsulation](#internal-helpers-and-encapsulation)
  - [The rule](#the-rule)
  - [The pattern](#the-pattern)
  - [Why this works](#why-this-works)
  - [Naming rules](#naming-rules)
  - [Growth path](#growth-path)
  - [Decision 1 — Where does the helper live?](#decision-1--where-does-the-helper-live)
  - [Decision 2 — How is state passed to the helper?](#decision-2--how-is-state-passed-to-the-helper)
  - [Why not `ActiveSupport::Concern`?](#why-not-activesupportconcern)
- [Parser vs. raw stdout](#parser-vs-raw-stdout)
- [Result-class factory methods](#result-class-factory-methods)
- [Common failures](#common-failures)
  - [One-line delegation when orchestration is needed](#one-line-delegation-when-orchestration-is-needed)
  - [Leaking command-class types into the public API](#leaking-command-class-types-into-the-public-api)
  - [Exposing command-DSL-shaped argv in the facade signature](#exposing-command-dsl-shaped-argv-in-the-facade-signature)
  - [Changing the legacy return type or signature on extraction](#changing-the-legacy-return-type-or-signature-on-extraction)
  - [Bypassing `@execution_context`](#bypassing-execution_context)
  - [Placing an overridable policy default after caller options](#placing-an-overridable-policy-default-after-caller-options)
  - [Skipping option whitelisting on opaque opts hashes](#skipping-option-whitelisting-on-opaque-opts-hashes)
  - [Mixing facade and command responsibilities](#mixing-facade-and-command-responsibilities)
  - [Adding a topic module whose methods fit an existing one](#adding-a-topic-module-whose-methods-fit-an-existing-one)

## Files to generate

For a facade method on `Git::Repository::<Topic>`:

- `lib/git/repository/<topic>.rb` — the topic module (created on first method,
  extended for subsequent methods)
- `spec/unit/git/repository/<topic>_spec.rb` — unit tests
- `spec/integration/git/repository/<topic>_spec.rb` — integration tests
  (omit for true one-line delegators that add no orchestration)

When the topic module is new, also update:

- `lib/git/repository.rb` — add `require 'git/repository/<topic>'` and `include
  Git::Repository::<Topic>` in alphabetical order with the existing entries.

## Topic module selection

### Existing modules

List `lib/git/repository/` to see all current topic modules. Add to one of those
modules whenever the new method fits the topic. Do not create a new module when
an existing one would do.

### Decision rules for adding a new module

Create a new topic module only when **both** of the following are true:

1. The topic is recognizable to a reader familiar with git — preferably matching
   one of the categories at <https://git-scm.com/docs> (Working tree, Branching,
   History, Sharing, Patching, Inspection, Configuration, Plumbing).
2. The methods would be awkward to place in any existing module without diluting
   that module's topic.

There is no fixed method-count requirement. A module that starts with one or two
methods is fine when the topic is genuinely distinct; the question is always
*fit*, not count. Default to extending an existing module whenever the new method
plausibly fits there.

#### Choosing a module for a new facade method

When placing a single facade method without a planned batch:

1. Before deciding placement, scan existing `Git::Repository::*` modules for
   sibling methods on the same git topic (e.g. when adding a method related to
   branches, check `Git::Repository::Branching` for existing siblings).
2. If those siblings form a coherent topic that does not fit any existing module,
   create the new module so subsequent additions have a home.
3. Otherwise place the method in the closest existing module. Revisit module
   organization later if a distinct topic emerges; promote the cluster to its own
   module in a single `refactor(repository):` commit at that point.

For a one-off method that does not fit any existing module and does not justify a
new module yet, place it in the closest existing module and revisit the
organization when more methods join it.

### Naming a new topic module

Topic modules follow a **two-tier** convention (documented in
[redesign/3_architecture_implementation.md §Facade module naming convention](../../../redesign/3_architecture_implementation.md#facade-module-naming-convention)):

- **Gerund** (`verb-ing`) when a single action word clearly names the whole module:
  `Staging`, `Committing`, `Branching`, `Merging`, `Logging`, `Diffing`, `Stashing`.
- **Noun + `Operations`** when the module groups a mixed bag of methods by git
  concept rather than a single action: `RemoteOperations`, `ObjectOperations`,
  `StatusOperations`, `WorktreeOperations`.

Additional rules:

- Use PascalCase; the file name is the snake_case equivalent (`Branching` →
  `branching.rb`).
- Do **not** use plain nouns that clash with existing public or domain-object class names such
  as `Branch`, `Diff`, `Log`, `Object`, `Remote`, `Status`, `Worktree`, etc. Those names
  belong to existing domain/query/value objects, not topic modules.
- Do not use the `*Info` or `*Result` suffix — reserved for parsed result structs.

## Designing a facade method

Decide the return type and the signature first (together they are the public
contract), then choose the body shape — one-line delegator for the simplest
cases, orchestration sequence otherwise.

### Choosing the return type

Apply these rules in order:

1. **Changing an existing `Git::Repository` method?** Preserve the existing return
   type exactly — same shape, same nil/empty semantics. Backward compatibility for
   callers of `Git::Repository` is the public contract; capture the existing return
   in the Step 2 plan.
2. **No legacy method (greenfield facade method)?** Choose the return type from
   the public-API perspective, in this order of preference:
   1. A **domain object** (`Git::BranchInfo`, `Git::DiffResult`, …) when the
      output has structure callers will inspect.
   2. A **primitive** (`String` of chomped stdout, `Boolean`, `Integer`) when
      the output is a single value.
   3. `nil` or `self` when the method is called for its side effects.
   4. **`Git::CommandLineResult`** only when the topic module explicitly
      documents that as its contract (rare — reserved for low-level escape
      hatches). Do not return `CommandLineResult` by default just because the
      command returns it.
3. **Never return** a type from `Git::Commands::*` (e.g.
   `Git::Commands::Foo::Bar::SomeResult`). Command-internal types are not part
   of the facade's public API.

### Choosing the method signature

The Ruby signature is part of the public contract — just as binding as the
return type. Apply these rules in order:

1. **Changing an existing `Git::Repository` method?** Preserve the existing
   signature exactly: same positional arguments in the same order, same defaults,
   same `opts = {}` vs. `**options` shape, same nil/sentinel semantics. Capture the
   existing signature in the Step 2 plan and diff against it after implementation.
2. **No legacy method (greenfield facade method)?** Design the signature from
   the public-API perspective:
   1. **Positional arguments** for the natural domain identifiers the method
      operates on (paths, refs, names, messages). At most two or three.
   2. **Keyword arguments with `**options`** for option hashes. Prefer
      `**options` over `opts = {}` in greenfield code — it surfaces unknown
      keys at the call site and reads better with the [whitelist
      pattern](#option-whitelisting-preventing-api-expansion). When the body
      forwards the options unchanged (the common case), use the **anonymous**
      keyword splat (`**`) so RuboCop's `Style/ArgumentsForwarding` cop is
      satisfied; name the splat (`**options`) only when the body must inspect
      or mutate the hash before forwarding (e.g. merging a positional
      argument into it, or applying a deprecation rewrite).
   3. **Named keyword arguments** (`force: false`, `all: true`) for a small,
      fixed set of flags that are part of the documented API and unlikely to
      grow. Switch to `**options` once the set exceeds ~3 keys.
3. **Validate cross-argument constraints in the facade**, before calling the
   command. Raise `ArgumentError` with a message that names the offending
   arguments — for example, `pull` raises when `branch` is given without
   `remote`. The command class stays neutral about Ruby-level argument
   relationships.
4. **Never expose** command-DSL-shaped arguments (`*argv`, raw `Hash` of CLI
   flags) on the facade. The facade's job is to translate Ruby idioms into
   command calls; passing through opaque argv defeats the layer.

Mechanical patterns for shaping the inputs (path coercion, option whitelisting,
deprecation handling) live in [Argument pre-processing
patterns](#argument-pre-processing-patterns).

### One-line delegator

When the facade method takes no options hash, does no pre-processing, and only
a trivial post-processing step (such as `.stdout.chomp`), it is a single-line
delegation. For example, a hypothetical `Git::Repository::Inspection#current_branch`
preserving the `String` return type contract:

```ruby
# Return the name of the currently checked-out branch
#
# @example Get the current branch name
#   repo.current_branch #=> "main"
#
# @return [String] the current branch name
#
# @raise [Git::FailedError] when `git rev-parse` exits with a non-zero status
#
def current_branch
  Git::Commands::RevParse.new(@execution_context).call('--abbrev-ref', 'HEAD').stdout.chomp
end
```

Use the one-line form only when **all** of the following hold:

- The Ruby signature exactly matches the command's `#call` signature (with at most
  trivial coercion like `Array(paths)` or `*[remote, branch].compact`).
- The method takes no options hash. Any facade method that accepts options must
  whitelist them — see [Option whitelisting](#option-whitelisting-preventing-api-expansion) —
  which makes it at minimum a two-line orchestration.
- The post-processing is at most a single chained call (`.stdout`,
  `.stdout.chomp`, etc.) that produces the documented return type (see
  [Choosing the return type](#choosing-the-return-type) above).

If the documented return type requires parsing, multiple commands, validation,
deprecation, option whitelisting, or any conditional logic, the facade needs an
orchestration sequence — not a one-line delegator.

### Orchestration sequence

When the facade method needs pre-processing, multiple commands, parsing, or result
assembly, expand the body into explicit phases:

```ruby
def branches_all
  result = Git::Commands::Branch::List.new(@execution_context).call(
    all: true,
    format: Git::Parsers::Branch::FORMAT_STRING
  )
  Git::Parsers::Branch.parse_list(result.stdout)
end

def commit(message, opts = {})
  SharedPrivate.assert_valid_opts!(COMMIT_ALLOWED_OPTS, **opts)
  opts = opts.merge(message: message) if message
  opts = deprecate_commit_no_gpg_sign_option(opts)
  Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
end
```

Three phases — keep them in this order:

1. **Pre-process** — validate, whitelist, normalize, deprecate, default.
2. **Call** — invoke one or more `Git::Commands::*` instances, each via
   `@execution_context`.
3. **Assemble** — pass stdout/stderr/status through a parser or result-class
   factory method, or return the raw `CommandLineResult` value the topic module
   documents.

### Sequencing multiple commands

When a facade method orchestrates more than one command, sequence the calls
explicitly with intermediate results in local variables:

```ruby
def branch_status(name)
  upstream_result = Git::Commands::RevParse.new(@execution_context).call("#{name}@{upstream}")
  ahead_behind = Git::Commands::RevList.new(@execution_context).call(
    "#{name}...#{upstream_result.stdout.chomp}",
    left_right: true,
    count: true
  )
  Git::BranchStatus.from_rev_list_output(name, ahead_behind.stdout)
end
```

Do not build a generic dispatcher or a "run everything in parallel" abstraction.
Explicit sequential calls are the documented pattern.

## Topic module skeleton

The full file layout for a topic module under `lib/git/repository/`:

```ruby
# frozen_string_literal: true

require 'git/commands/<command_a>'
require 'git/commands/<command_b>'
# require 'git/parsers/<parser>' — when the module uses a parser

module Git
  class Repository
    # Short summary of the topic and the facade methods it provides
    #
    # Included by {Git::Repository}.
    #
    # @api public
    #
    module Topic
      # YARD docs per facade-yard-documentation skill
      def method_a(...)
        # body
      end

      # YARD docs per facade-yard-documentation skill
      def method_b(...)
        # body
      end
    end
  end
end
```

Then wire into `lib/git/repository.rb`:

```ruby
require 'git/repository/topic'
# ...

class Repository
  include Git::Repository::Topic
  # ...
end
```

## The five facade responsibilities checklist

From [redesign/2_architecture_redesign.md §2.1](../../../redesign/2_architecture_redesign.md).
For each facade method, confirm whether each responsibility applies and is handled:

- [ ] **Manage execution context** — calls `Git::Commands::*.new(@execution_context)`,
  never builds CLI argv directly and never bypasses the execution context.
- [ ] **Pre-process arguments** — applies path expansion, Ruby-idiomatic defaults,
  option whitelisting, deprecations.
- [ ] **Collect data** — gathers any additional information needed before or after
  command execution to build the response (e.g., reading config, listing refs).
  Most facade methods do not need this; flag explicitly when present.
- [ ] **Call commands** — invokes one or more `Git::Commands::*` classes; multiple
  calls are sequenced explicitly with intermediate results held in local variables.
- [ ] **Build rich response objects** — passes stdout through a `Git::Parsers::*`
  class or a result-class factory method to produce the documented return type.
  Returning the raw `CommandLineResult` is acceptable only when that is the
  documented public contract for the topic module.

## Argument pre-processing patterns

### Path normalization

Accept `String` or `Array<String>` for path arguments and splat into the command:

```ruby
def add(paths = '.', **)
  SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
  Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
end
```

For path arguments that must be absolute or relative to the worktree root, expand
with `File.expand_path` against `@execution_context.git_work_dir`.

### Option whitelisting (preventing API expansion)

When the facade method accepts an options hash (positional `opts = {}` *or*
keyword `**options`) and forwards it to a command, the underlying command class
typically exposes many more options than the public facade contract. Without
filtering, callers could pass options that happen to match command DSL names but
were never part of the facade's public API — silently expanding the contract.

Use a per-method whitelist constant + `SharedPrivate.assert_valid_opts!`:

```ruby
PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
private_constant :PULL_ALLOWED_OPTS

def pull(remote = nil, branch = nil, **)
  raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?

  SharedPrivate.assert_valid_opts!(PULL_ALLOWED_OPTS, **)
  positional_args = [remote, branch].compact
  Git::Commands::Pull.new(@execution_context)
                     .call(*positional_args, no_edit: true, **)
                     .stdout
end
```

The helper's signature is `assert_valid_opts!(allowed, **opts)` — the allowed
set comes first as a positional argument so callers can re-forward the
anonymous splat (`**`) into both the assertion and the command call. Name the
splat (`**options`) only when the body must inspect or mutate the options
hash before forwarding it (see the [`commit` example](#orchestration-sequence)
above for that case).

Rules:

- Name the constant `<METHOD>_ALLOWED_OPTS` and mark it `private_constant`. It
  is implementation detail, not part of the public API.
- Place the constant immediately before the method definition.
- The whitelist must match the `@option` tags in the YARD doc exactly. Reviewers
  should verify the two lists are equal in both directions.
- `SharedPrivate.assert_valid_opts!` raises
  `ArgumentError: Unknown options: <key>` for any unrecognized key. Document this
  with `@raise [ArgumentError]` on the facade method.
- Every facade method that accepts an options hash **must** have a unit test
  that passes an unknown key and expects `ArgumentError`. That test — not a
  defensive `slice` at the call site — is what guarantees the whitelist stays
  load-bearing under future refactors. Forward `**options` directly after the
  assertion; do not also `slice` it (the assertion already proves every key is
  allowed, and a second mechanism invites cargo-culting and confusion about
  which one enforces the contract).

Even when the facade uses `**options` keyword forwarding, whitelist explicitly.
Relying on the command's own `ArgumentError` couples the facade contract to the
command's argument DSL, which is exactly what this layer exists to prevent.

### Deprecation handling

Handle deprecated option keys explicitly in the facade — never let deprecation
shims leak into the command class. Pattern:

```ruby
def commit(message, opts = {})
  opts = opts.merge(message: message) if message
  opts = deprecate_commit_no_gpg_sign_option(opts)
  opts = deprecate_commit_add_all_option(opts)
  Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
end

private

def deprecate_commit_no_gpg_sign_option(opts)
  return opts unless opts.key?(:no_gpg_sign)

  Git::Deprecation.warn(
    "Git::Repository#commit's :no_gpg_sign option is deprecated. " \
    'Use gpg_sign: false instead.'
  )
  opts.dup.tap do |o|
    o[:gpg_sign] = false unless o.key?(:gpg_sign)
    o.delete(:no_gpg_sign)
  end
end
```

### Defaults and policy options

The facade is where **policy defaults** are applied — options that support
non-interactive execution, control output format for parsing, or set safe
command-level defaults. The command class stays neutral; the facade makes the
defaults explicit.

There are two categories of policy defaults:

**Fixed policy defaults** are set unconditionally and are NOT included in the
method's `ALLOWED_OPTS` constant. `assert_valid_opts!` rejects any
caller-supplied value for these keys before it reaches the command call,
enforcing the policy. They are not part of the facade's public API and must
not be documented as `@option` tags.

| Policy option | Why facade sets it |
| --- | --- |
| `no_edit: true` | Subprocesses cannot launch `$EDITOR` |
| `no_progress: true` | Progress output goes to stderr and pollutes parsing |
| `no_color: true` | ANSI escapes interfere with parsers |
| `format: Git::Parsers::Foo::FORMAT_STRING` | Facade wants a parseable format |

**Overridable policy defaults** are included in `ALLOWED_OPTS`. The facade
sets a sensible default but callers may override it. Place these **before**
the caller's `**opts` in the command call so the caller's value wins on key
collision, and document them as `@option` tags since they are part of the
public API:

```ruby
# :verbose is in ALLOWED_OPTS — caller can pass verbose: true to override
Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
```

## Internal helpers and encapsulation

Topic modules under `lib/git/repository/` often share helper logic — option
validation, path normalization, deprecation warnings, error wrapping. These
helpers must be reachable from any topic module without leaking onto the public
`Git::Repository` API surface.

### The rule

**Do not put shared helpers as private methods on `Git::Repository`** (directly
or via `include`). `include` copies private instance methods onto the host class,
so any caller with a `Git::Repository` instance can `repo.send(:helper, ...)`.
This:

1. Re-creates the god-class problem `Git::Lib` had — the reason the redesign introduced topic modules.
2. Couples every topic module silently to ambient mixin state.
3. Is not actually private and not `@api`-marked, so YARD/tooling cannot enforce it.

### The pattern

Put shared helpers in a sibling **internal module** under `lib/git/repository/`
that is **not** `include`d into `Git::Repository`. Use `module_function` so
methods are called as singleton methods from the topic modules:

```ruby
# lib/git/repository/shared_private.rb
module Git
  class Repository
    # Namespace for internal helpers shared across facade topic modules
    #
    # @api private
    #
    module SharedPrivate
      module_function

      def assert_valid_opts!(allowed, **options)
        unknown = options.keys - allowed
        return if unknown.empty?

        raise ArgumentError, "Unknown options: #{unknown.join(', ')}"
      end
    end

    private_constant :SharedPrivate
  end
end
```

Call sites use the short unqualified form (since the constant is private,
fully-qualified external references are not possible):

```ruby
# lib/git/repository/staging.rb
def add(paths = '.', **)
  SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
  Git::Commands::Add.new(@execution_context)
                    .call(*Array(paths), **)
                    .stdout
end
```

### Why this works

- **No mixin pollution.** `Git::Repository` instances do not gain
  `assert_valid_opts!` as a method. The helper is namespaced and private-by-API.
- **Explicit dependency.** A reader of `staging.rb` sees exactly where the helper
  comes from. No magic mixin chain.
- **Stateless by contract.** Without `include`, helpers cannot access
  `@execution_context` or other instance state — they must take everything as
  arguments. This keeps them pure and trivially unit-testable.
- **Truly private constant.** `private_constant :SharedPrivate` causes
  fully-qualified external references (`Git::Repository::SharedPrivate`) to
  raise a `NameError` at runtime. Callers inside the `Git::Repository` class
  body (i.e. the topic modules) use the short `SharedPrivate.foo(...)` form
  and are unaffected.

### Naming rules

**Topic modules** (those `include`d in `Git::Repository`) follow the two-tier
naming convention in [Naming a new topic module](#naming-a-new-topic-module):
gerund for single-action modules, `Noun + Operations` for mixed-bag modules.

**Internal helper modules** (those **not** `include`d) use descriptive nouns,
never generic role-suffixes like `*Helpers`, `*Utils`, or `*Support`. The
`*Operations` suffix is a topic-module convention — it is not a generic role
suffix and is not prohibited here:

| Module                                             | Distinguished by                                   |
| -------------------------------------------------- | -------------------------------------------------- |
| `Git::Repository::Staging`                         | `include`d, `@api public` (gerund topic module)    |
| `Git::Repository::Branching`                       | `include`d, `@api public` (gerund topic module)    |
| `Git::Repository::RemoteOperations`                | `include`d, `@api public` (`*Operations` topic module) |
| `Git::Repository::SharedPrivate`                   | not `include`d, `@api private`, `private_constant` |
| `Git::Repository::SharedPrivate::OptionValidation` | nested under `SharedPrivate`, `@api private`       |

Reasons:

- The location (`lib/git/repository/`) plus the `@api` tag and absence of an
  `include` line in `lib/git/repository.rb` already convey API status. A
  `*Helpers` suffix is redundant signage.
- Symmetry with topic modules keeps the directory listing readable.
- "Helpers" invites junk-drawer dumping; responsibility-named modules invite
  cohesion. Ruby stdlib follows the same convention (`URI::DEFAULT_PARSER`,
  `ActiveSupport::Inflector`, not `*Helpers`).

### Growth path

**Every time a new method is added to `SharedPrivate`**, count the total methods
and look for sub-themes. If **either** trigger fires, extract before committing
the new method:

1. `SharedPrivate` would exceed ~5 methods after the addition.
2. Clear sub-themes are visible (validation vs. normalization vs. error wrapping).

Then extract responsibility-named submodules **nested under `SharedPrivate`**:

```text
Git::Repository::SharedPrivate                       # catch-all (initial)
        ↓ grows / develops sub-themes
Git::Repository::SharedPrivate::OptionValidation     # extracted by responsibility
Git::Repository::SharedPrivate::PathNormalization
Git::Repository::SharedPrivate                       # remaining miscellany (or deleted)
```

Submodules live in `lib/git/repository/shared_private/option_validation.rb`.
They do not need their own `private_constant` since the parent is already
private. The extraction is mechanical — call sites change from
`SharedPrivate.foo(...)` to `SharedPrivate::OptionValidation.foo(...)`.

### Decision 1 — Where does the helper live?

Choose placement by working through these questions in order:

1. **Is it used by only one topic module?** → `module Private` nested inside
   that topic module.
2. **Is it used by two or more modules, and does an existing `SharedPrivate::*`
   submodule cover this concern?** → Add the method to that submodule.
3. **Is it used by two or more modules, and no fitting submodule exists?** →
   Add it directly to `SharedPrivate`, then apply the growth-path check (see
   [Growth path](#growth-path)) to decide whether a new submodule is now
   warranted.

| Condition | Placement |
| --- | --- |
| Used by one topic module only | `module Private` nested inside that topic module |
| Shared; fitting `SharedPrivate::*` submodule exists | That submodule (e.g. `SharedPrivate::OptionValidation`) |
| Shared; no fitting submodule exists | `SharedPrivate` directly |

**Nested `module Private`** — for helpers local to one topic module.
Nest it inside the topic module, mark it `@api private`, and call its methods
as `Private.foo(...)`:

```ruby
module Git
  class Repository
    module Branching
      # ... public facade methods ...

      # Helpers private to the `Branching` topic module
      #
      # @api private
      module Private
        module_function

        # Translates checkout options into git command arguments
        #
        # @param branch [String, nil] the target branch name
        #
        # @param options [Hash] caller-supplied checkout options
        #
        # @return [Array] a two-element tuple of translated arguments
        #
        # @api private
        def translate_checkout_opts(branch, options)
          # ...
        end
      end
      private_constant :Private
    end
  end
end
```

**Existing `SharedPrivate::*` submodule** — check
`lib/git/repository/shared_private/` for a file whose name matches the
concern (e.g. `option_validation.rb`). If one exists, add the method there.

**`SharedPrivate` directly** — when no fitting submodule exists yet.
See [The pattern](#the-pattern) for the full skeleton. Call sites use
`SharedPrivate.foo(...)`. After adding, re-run the growth-path check.

### Decision 2 — How is state passed to the helper?

`module_function` helpers (whether in `module Private` or `SharedPrivate`) are
stateless by design — they cannot access `@execution_context` or any other
instance state. This is intentional: stateless helpers are trivially
unit-testable and have no hidden dependencies.

When a helper needs state, pass it explicitly rather than making the helper
stateful:

1. **Pass state as an argument** — the preferred approach. Add the execution
   context (or whatever state is needed) as a positional argument:

   ```ruby
   module Private
     module_function

     def build_result(execution_context, name)
       Git::Commands::Foo.new(execution_context).call(name)
     end
   end
   ```

2. **Extract a PORO** — when the helper has enough state and behavior to
   justify its own object. Place it under `lib/git/repository/`, mark it
   `@api private`, and do not `include` it. In `lib/git/repository/commit_operation.rb`:

   ```ruby
   # Callable helper for executing a git commit
   #
   # @api private
   class Git::Repository::CommitOperation
     def initialize(execution_context)
       @execution_context = execution_context
     end

     def call(...)
       # ...
     end
   end
   ```

Avoid: inline private instance methods directly on the topic module (i.e.,
`def` after `private` in the module body without a `Private` namespace). This
pollutes the `Git::Repository` instance namespace with methods reachable via
`repo.send(:helper, ...)`, which is the exact problem these patterns exist to
prevent.

### Why not `ActiveSupport::Concern`?

`Concern` does not solve the include-time leak: a `Concern` `include`d into a
class still copies its private instance methods onto that class. Rails tolerates
the leak via underscore prefixes and `:nodoc:`, or extracts service objects.
This project takes the stricter approach (sibling module + `module_function`)
without depending on `activesupport`.

## Parser vs. raw stdout

| Situation | Use |
| --- | --- |
| The facade returns a `String` of git's stdout (chomped or as-is) | `.stdout` |
| The facade returns a structured object built from line-by-line parsing | A `Git::Parsers::*` class |
| The facade returns a single bool/int derived from output | Inline transformation in the facade |
| The facade returns a `Git::CommandLineResult` | Return the raw result |

If the parsing logic exceeds ~5 lines, extract it into a `Git::Parsers::*` class
and call it from the facade. The facade method remains an orchestration sequence,
not a parser.

## Result-class factory methods

When the facade returns a domain object (e.g. `BranchInfo`, `BranchDeleteResult`,
`DiffResult`), use a factory method on the result class rather than constructing
it inline:

```ruby
def branch_delete(name, force: false)
  result = Git::Commands::Branch::Delete.new(@execution_context).call(name, force: force)
  Git::BranchDeleteResult.from(name: name, command_result: result)
end
```

This keeps result-object construction in one place per type and makes parsers
reusable across facade methods.

## Common failures

### One-line delegation when orchestration is needed

If the facade method discards information the caller documented as part of the
return type (e.g. returns `result.stdout` when the caller expects a parsed Hash),
the one-line form is wrong. Expand to the orchestration sequence and call the
appropriate parser.

### Leaking command-class types into the public API

The public return type should never be `Git::Commands::Foo::Bar::SomeResult` or
any other type from `Git::Commands::*`. Returning `Git::CommandLineResult` is
acceptable when the topic module documents that as its contract, but is not the
default — see [Choosing the return type](#choosing-the-return-type). Returning
domain objects (`Git::BranchInfo`, `Git::DiffResult`, etc.) is preferred for
methods that produce structured data.

### Exposing command-DSL-shaped argv in the facade signature

The facade signature is a Ruby API, not a transcription of the git CLI. Accepting
a free-form `*args` that is forwarded straight to the command, or naming keyword
arguments after CLI flags (e.g. `no_ff:`, `set_upstream_to:`) instead of
Ruby-idiomatic names, leaks the command DSL into the public contract. Define the
signature from the caller's perspective; translate to command DSL inside the
body.

### Changing the legacy return type or signature on extraction

When adding or changing a `Git::Repository` facade method, returning a
different type, accepting a different positional/keyword shape, or changing
nil-handling silently breaks every caller. Capture the existing contract in the
Step 2 plan and diff before/after — see
[Choosing the return type](#choosing-the-return-type) rule 1 and
[Choosing the method signature](#choosing-the-method-signature) rule 1.

### Bypassing `@execution_context`

Constructing a command with anything other than `@execution_context` (e.g.
`Git::Commands::Add.new(self)` from inside a facade method) is wrong. The facade
holds a `Git::ExecutionContext::Repository`; commands must always be constructed
with it.

### Placing an overridable policy default after caller options

This anti-pattern applies to **overridable** policy defaults (those in `ALLOWED_OPTS`).
Placing the default after `**opts` silently overwrites the caller's explicit value:

```ruby
# ❌ Wrong — caller's :verbose is silently discarded
Git::Commands::Log.new(@execution_context).call(*args, **opts, verbose: false)

# ✅ Correct — caller's :verbose wins because opts is splatted last
Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
```

Place overridable policy defaults before the caller's `**opts` so the caller's
value wins on key collision. This does not apply to fixed policy options (not in
`ALLOWED_OPTS`): `assert_valid_opts!` prevents those keys from reaching the
command call at all.

### Skipping option whitelisting on opaque opts hashes

If the facade accepts an options hash (positional `opts = {}` *or* keyword
`**options`), it must call `SharedPrivate.assert_valid_opts!` against a
`private_constant`-marked `<METHOD>_ALLOWED_OPTS` constant. Without it,
callers can silently pass any key the command DSL happens to accept, which is
API expansion that the facade did not commit to.

### Mixing facade and command responsibilities

The facade does not build CLI argv. The command does not pre-process Ruby
arguments or parse output. If a facade method calls `command(...)` directly
(rather than `Git::Commands::*.new(...).call(...)`) it is bypassing the command
layer; refactor by introducing or extending the appropriate command class first.

### Adding a topic module whose methods fit an existing one

A topic module is not justified when its method(s) would fit naturally in an
existing module. Before creating a new module, scan existing modules for a
plausible home. The absence of a method-count threshold is not an invitation
to fragment the API — place the method in the closest existing module unless
the topic is genuinely distinct and the method would be awkward there. See
[Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
for the full two-criteria test.
