# CLI Refactoring Plan: cli.py (7,437 lines)

**Date:** 2026-02-26
**File analyzed:** `src/gitflow_analytics/cli.py`
**Current size:** 7,437 lines
**Status:** Research only — no code changes made

---

## Executive Summary

`cli.py` is a monolithic 7,437-line file that contains at least five distinct concerns:

1. CLI plumbing (Click decorators, group routing, error formatting)
2. The entire `analyze` pipeline orchestration (4,000+ lines of business logic)
3. Report generation orchestration (report-type dispatch, ~1,000 lines)
4. Identity / alias management commands (~1,200 lines)
5. ML training commands (~400 lines)

The `analyze()` function alone spans lines 602–4,589 (approximately 3,987 lines). It is a sequential script masquerading as a function: it imports lazily, builds objects, runs pipeline stages, validates database state, and generates every report format — all in a single call stack. There is significant code duplication: the git-cloning logic appears verbatim twice (around lines 2,066–2,232 and 2,789–2,900), and `progress_callback` is defined as an inner closure three times (lines 1,686, 2,247, 4,839).

---

## Current Structure Map

| Section | Lines | Start | End | Concern |
|---|---|---|---|---|
| Imports + module-level setup | 41 | 1 | 42 | CLI |
| `RichHelpFormatter` class | 41 | 45 | 86 | CLI formatting |
| Date/timezone utilities | 111 | 88 | 199 | **Business logic** |
| `ImprovedErrorHandler` class | 87 | 201 | 288 | CLI |
| `AnalyzeAsDefaultGroup` class | 24 | 290 | 314 | CLI |
| `cli()` group + `@cli.command("analyze")` decorator | 283 | 316 | 601 | CLI |
| `analyze()` implementation – init + config + batch fetch | 2,157 | 602 | 2,759 | **Business logic + orchestration** |
| `analyze()` – traditional (non-batch) mode | 813 | 2,759 | 3,572 | **Business logic + orchestration** |
| `analyze()` – report generation loop | 1,017 | 3,572 | 4,589 | **Report orchestration** |
| `fetch` command | 330 | 4,591 | 4,921 | CLI + orchestration |
| `collect` / `classify` / `report` pipeline commands | 307 | 4,922 | 5,229 | CLI (thin wrappers) |
| `cache-stats` + `merge-identity` commands | 99 | 5,230 | 5,329 | CLI |
| `_resolve_config_path()` helper | 110 | 5,331 | 5,441 | CLI utility |
| `run` + `install` commands | 179 | 5,443 | 5,622 | CLI |
| `discover-storypoint-fields` command | 62 | 5,561 | 5,623 | CLI |
| `identities` command | 212 | 5,624 | 5,836 | Identity domain |
| `aliases` command | 337 | 5,837 | 6,174 | Identity domain |
| `list-developers` command | 61 | 6,175 | 6,236 | Identity domain |
| `create-alias-interactive` command | 276 | 6,237 | 6,513 | Identity domain |
| `alias-rename` command | 311 | 6,514 | 6,825 | Identity domain |
| `train` command | 329 | 6,826 | 7,155 | ML training domain |
| `verify-activity` command | 65 | 7,156 | 7,221 | CLI (thin wrapper) |
| `help` command | 129 | 7,222 | 7,351 | CLI |
| `training-statistics` command | 78 | 7,352 | 7,430 | ML training domain |
| `main()` | 6 | 7,431 | 7,437 | CLI |

---

## Problems Identified

### P1 — Business logic embedded in CLI layer

The following functions belong in `core/` or a dedicated service layer, not in `cli.py`:

- `get_week_start()`, `get_week_end()`, `get_monday_aligned_start()` (lines 88–168): Pure date math. Already used by `fetch` command and `analyze` in three separate locations. They should live in `utils/date_utils.py`.
- `handle_timezone_error()` (lines 171–198): Cross-cutting error utility used throughout `analyze`.
- The git clone logic (lines 2,066–2,232 and 2,789–2,900): Verbatim duplication of 150 lines each. Should be extracted to a `core/repo_cloner.py` service.
- The ML config dict assembly (lines 1,274–1,313 and 5,685–5,725 and 5,920–5,964): Identical 40-line block copy-pasted into `analyze`, `identities`, and `aliases` commands.
- `_resolve_config_path()` (lines 5,331–5,441): Config resolution with interactive prompts is a wizard utility, not business logic, but it mixes file I/O with interactive prompts and belongs in `cli_wizards/`.

### P2 — The `analyze()` function is a 3,987-line God Function

`analyze()` (lines 602–4,589) is a sequential script, not a function. It currently:

1. Configures logging
2. Loads and validates config
3. Handles PM integration overrides
4. Handles CI/CD config injection (including an inline class definition `CICDConfig` at line 786)
5. Authenticates GitHub
6. Resolves identity database
7. Builds `GitAnalyzer` + `IntegrationOrchestrator`
8. Discovers organization repositories
9. Checks cache completeness per-repo
10. Fetches commits per-repo (with retry/clone logic)
11. Enriches with GitHub PR data
12. Validates database state (three separate validation passes)
13. Runs batch classification
14. Loads classified commits from database
15. Runs qualitative analysis
16. Aggregates PM data
17. Generates 15+ report files
18. Displays summary statistics

Each stage should be a callable function in `core/pipeline/` or `core/orchestration/`.

### P3 — Identity/alias commands are a subsystem (1,197 lines)

`identities`, `aliases`, `list-developers`, `create-alias-interactive`, `alias-rename` are five related commands that belong in their own `cli/commands/identity.py` module.

### P4 — Duplicated code patterns

| Pattern | Occurrences | Lines |
|---|---|---|
| Logging configuration block (`if log.upper() != "NONE": ...`) | 5 | ~12 lines each |
| `progress_callback` inner closure | 3 | ~5 lines each |
| Git clone subprocess block | 2 | ~150 lines each |
| ML config dict assembly | 3 | ~40 lines each |
| `display/click.echo` dual-output pattern | ~100+ | 2 lines each |

### P5 — Inline class definition

A `CICDConfig` class is defined inline inside `analyze()` at line 786. This is a dataclass that belongs in `config/` or the CI/CD integration module.

---

## Proposed Module Structure

```
src/gitflow_analytics/
├── cli/
│   ├── __init__.py          # Re-exports cli group and main()
│   ├── app.py               # cli() group, AnalyzeAsDefaultGroup, main()
│   ├── formatting.py        # RichHelpFormatter, ImprovedErrorHandler
│   ├── options.py           # Shared Click option definitions (decorators)
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── analyze.py       # analyze_subcommand() + analyze() orchestration
│   │   ├── fetch.py         # fetch() command
│   │   ├── pipeline.py      # collect, classify, report thin wrappers
│   │   ├── cache.py         # cache-stats, merge-identity
│   │   ├── identity.py      # identities, aliases, list-developers,
│   │   │                    #   create-alias-interactive, alias-rename
│   │   ├── training.py      # train, train-stats
│   │   ├── setup.py         # run, install, discover-storypoint-fields
│   │   └── help.py          # help, verify-activity
│   └── utils.py             # _resolve_config_path(), logging_setup()
├── core/
│   ├── (existing modules)
│   ├── repo_cloner.py       # Extracted git clone logic (new)
│   └── analyze_pipeline.py  # Extracted analyze() stages (new)
└── utils/
    ├── (existing modules)
    └── date_utils.py        # get_week_start/end/monday_aligned_start (new)
```

### Compatibility shim

`src/gitflow_analytics/cli.py` becomes a 10-line shim:

```python
"""GitFlow Analytics CLI — compatibility shim."""
from gitflow_analytics.cli.app import cli, main

__all__ = ["cli", "main"]
```

The `pyproject.toml` entry point (`gitflow_analytics.cli:main`) continues to work unchanged.

---

## Detailed Module Plan

### `cli/app.py` (~80 lines)

**Contains:**
- `AnalyzeAsDefaultGroup(click.Group)` with `parse_args()`
- `@click.group(cls=AnalyzeAsDefaultGroup)` `cli()` function
- `main()` entry point
- All `cli.add_command()` registrations

**Imports from:** all `cli/commands/*.py` modules

### `cli/formatting.py` (~130 lines)

**Contains:**
- `RichHelpFormatter` class (format_command_help, format_option_help, suggest_command)
- `ImprovedErrorHandler` class (handle_command_error)

**Note:** No dependency on business logic — pure formatting.

### `cli/options.py` (~100 lines)

**Purpose:** Centralize the 20+ repeated `--config`, `--weeks`, `--log`, `--no-rich`, `--output` Click option definitions as reusable decorators.

**Pattern:**
```python
import click, functools

def config_option(required=True):
    return click.option("--config", "-c", type=click.Path(path_type=Path),
                        required=required, help="...")

def weeks_option(default=12):
    return click.option("--weeks", "-w", type=int, default=default, help="...")

def log_option():
    return click.option("--log", type=click.Choice(["none","INFO","DEBUG"]),
                        default="none", help="...")
```

### `cli/utils.py` (~150 lines)

**Contains:**
- `_resolve_config_path(config)` (currently at line 5,331)
- `setup_logging(log_level_str)` — extracted from the duplicated 12-line block
- `create_display(no_rich, version)` — extracted from duplicated display-init pattern

### `cli/commands/analyze.py` (~500 lines)

**Contains:**
- `@cli.command("analyze")` decorator and `analyze_subcommand()` (thin Click entry point, ~100 lines)
- `analyze()` orchestrator function, refactored to call extracted pipeline functions
- Configuration setup section (~100 lines) — PM overrides, CI/CD config, GitHub auth check
- Identity resolver initialization (~50 lines)
- Pipeline dispatch: calls `run_collect()`, `run_classify()`, `run_report()` from `pipeline.py`
- Final summary display (~80 lines)

**Business logic that moves OUT of this file:**
- Git clone logic → `core/repo_cloner.py`
- Batch fetch loop → `core/analyze_pipeline.py:run_batch_fetch()`
- Batch validation → `core/analyze_pipeline.py:validate_batch_state()`
- Qualitative analysis → `core/analyze_pipeline.py:run_qualitative_analysis()`
- Report dispatch → `core/analyze_pipeline.py:run_report_generation()`

### `cli/commands/fetch.py` (~200 lines)

**Contains:** `fetch()` Click command
**Note:** Share `setup_logging()` and `create_display()` from `cli/utils.py`. Reuse `get_monday_aligned_start()` from `utils/date_utils.py`.

### `cli/commands/pipeline.py` (~120 lines)

**Contains:** `collect_command()`, `classify_command()`, `report_command()` — already thin wrappers around `pipeline.py`.

### `cli/commands/cache.py` (~100 lines)

**Contains:** `cache_stats()`, `merge_identity()`.

### `cli/commands/identity.py` (~1,200 lines)

**Contains:**
- `identities()` command
- `aliases_command()` command
- `list_developers()` command
- `create_alias_interactive()` command
- `alias_rename()` command

**Shared code to extract within this file:**
- `build_analyzer_from_config(cfg, cache)` helper — the 40-line ML config dict assembly repeated in `identities` and `aliases` commands should become one local factory function.

### `cli/commands/training.py` (~420 lines)

**Contains:** `train()`, `training_statistics()`.

### `cli/commands/setup.py` (~250 lines)

**Contains:** `run_launcher()`, `install_command()`, `discover_storypoint_fields()`.

### `cli/commands/help.py` (~200 lines)

**Contains:** `show_help()`, `verify_activity()`.

### `utils/date_utils.py` (~80 lines, new file)

**Extracts from `cli.py`:**
- `get_week_start(date)` (line 88)
- `get_week_end(date)` (line 125)
- `get_monday_aligned_start(weeks_back)` (line 146)

**Rationale:** These are pure functions used by `analyze`, `fetch`, `identities`, `train` commands, and referenced in comments elsewhere. They are date-math utilities with no CLI dependency.

### `core/repo_cloner.py` (~180 lines, new file)

**Extracts from `cli.py`:**
- The subprocess-based git clone logic with retry, timeout, authentication (lines 2,066–2,232 and 2,789–2,900)

**Interface:**
```python
def clone_repository(
    github_repo: str,
    repo_path: Path,
    token: Optional[str],
    branch: Optional[str],
    max_retries: int = 2,
    timeout_seconds: int = 300,
    progress_callback: Optional[Callable] = None,
) -> bool: ...
```

**Rationale:** The clone logic appears verbatim twice (~150 lines each), indicating it is a genuine reusable service. Moving it to `core/` lets the `fetch` command and `analyze` command share one implementation.

### `core/analyze_pipeline.py` (~600 lines, new file)

**Extracts from `analyze()`:**

```python
def run_batch_fetch(
    repos_needing_analysis, cache, cfg, orchestrator,
    start_date, end_date, weeks, force_fetch,
    display=None, progress=None,
) -> dict: ...  # Returns {total_commits, total_tickets, total_developers}

def validate_batch_state(
    cache, start_date, end_date, total_commits_from_fetch
) -> None: ...  # Raises ClickException on inconsistency

def run_qualitative_analysis(
    all_commits, enable_qualitative, qualitative_only,
    get_qualitative_config_fn, cfg, display=None,
) -> tuple[list, dict]: ...  # Returns (results, cost_stats)

def run_report_generation(
    all_commits, all_prs, developer_stats, ticket_analysis,
    cfg, output, weeks, generate_csv, anonymize,
    display=None, **kwargs,
) -> list[str]: ...  # Returns generated report filenames
```

**Rationale:** Each of these represents a discrete pipeline stage. Extracting them eliminates the 4,000-line God Function and makes individual stages unit-testable.

---

## Business Logic That Should Move to Core Layers

| Code Location | Lines | Destination |
|---|---|---|
| `get_week_start/end/monday_aligned_start` | 88–168 | `utils/date_utils.py` |
| `handle_timezone_error()` | 171–198 | `utils/error_utils.py` or `core/` |
| Git clone subprocess logic | 2,066–2,232, 2,789–2,900 | `core/repo_cloner.py` |
| Batch fetch loop (step 1) | 1,535–1,890 | `core/analyze_pipeline.py` |
| Cache validation logic | 1,902–2,530 | `core/analyze_pipeline.py` |
| Batch classification dispatch | 2,562–2,755 | `core/analyze_pipeline.py` |
| Qualitative analysis dispatch | 3,200–3,565 | `core/analyze_pipeline.py` |
| Report generation loop | 3,608–4,560 | `core/analyze_pipeline.py` |
| ML config dict assembly | 1,274–1,313 (×3) | `config/builder.py` or per-command factory |
| Inline `CICDConfig` class | 786–791 | `config/cicd.py` or `integrations/cicd/` |

---

## Estimated Target Line Counts

| File | Estimated Lines | Notes |
|---|---|---|
| `cli.py` (compatibility shim) | 10 | Import re-export only |
| `cli/app.py` | 80 | Group + main |
| `cli/formatting.py` | 130 | Pure formatting classes |
| `cli/options.py` | 100 | Shared Click decorators |
| `cli/utils.py` | 150 | setup_logging, _resolve_config_path, create_display |
| `cli/commands/analyze.py` | 500 | Thin orchestrator; delegates to core |
| `cli/commands/fetch.py` | 200 | Standalone fetch |
| `cli/commands/pipeline.py` | 120 | collect/classify/report wrappers |
| `cli/commands/cache.py` | 100 | cache-stats, merge-identity |
| `cli/commands/identity.py` | 1,200 | All 5 identity/alias commands |
| `cli/commands/training.py` | 420 | train, train-stats |
| `cli/commands/setup.py` | 250 | run, install, discover-storypoint-fields |
| `cli/commands/help.py` | 200 | help, verify-activity |
| `utils/date_utils.py` | 80 | Week boundary functions |
| `core/repo_cloner.py` | 180 | Git clone with retry |
| `core/analyze_pipeline.py` | 600 | Extracted pipeline stages |
| **Total** | **~4,320** | **~42% reduction from 7,437** |

The reduction comes primarily from eliminating the two verbatim clone duplications (~300 lines saved) and consolidating the duplicated logging-setup and ML-config blocks (~200 lines saved). The rest of the reduction is structural reorganization without code deletion.

---

## Shared Code to Extract

### 1. Logging setup (5 occurrences, ~12 lines each)

Extract to `cli/utils.py::setup_logging(log_level_str: str) -> None`.

### 2. Display initialization (3 occurrences, ~6 lines each)

Extract to `cli/utils.py::create_display(no_rich: bool, version: str) -> Optional[ProgressDisplay]`.

### 3. ML config dict assembly (3 occurrences, ~40 lines each)

Extract to a factory function. Two options:
- Option A: `cli/commands/_shared.py::build_ml_config(cfg) -> Optional[dict]` — simple, keeps it CLI-adjacent
- Option B: `config/builder.py::build_ml_config(cfg) -> Optional[dict]` — cleaner separation

### 4. Git clone logic (2 occurrences, ~150 lines each)

Extract to `core/repo_cloner.py::clone_repository(...)` as described above.

### 5. Week-aligned date calculation (4 occurrences, ~8 lines each)

Move to `utils/date_utils.py`. The pattern:
```python
current_week_start = get_week_start(current_time)
last_complete_week_start = current_week_start - timedelta(weeks=1)
start_date = last_complete_week_start - timedelta(weeks=weeks - 1)
end_date = get_week_end(last_complete_week_start + timedelta(days=6))
```
appears in `analyze`, `fetch`, `identities`, and `train` commands. Extract to `calculate_analysis_period(weeks: int) -> tuple[datetime, datetime]`.

---

## Priority Order for Refactoring

### Priority 1 — Zero-risk extractions (no behavior change, pure moves)

These changes extract code to new modules with no logic modification. Each can be done and tested in isolation.

1. **Create `utils/date_utils.py`** — Move `get_week_start`, `get_week_end`, `get_monday_aligned_start`, and add `calculate_analysis_period()`. Update imports in `cli.py`. Existing tests cover this indirectly.

2. **Create `cli/formatting.py`** — Move `RichHelpFormatter` and `ImprovedErrorHandler`. No functional change.

3. **Create `cli/utils.py`** — Extract `setup_logging()`, `create_display()`, `_resolve_config_path()` to shared utilities. Replace the 5 duplicated logging blocks with `setup_logging(log)` calls.

4. **Create `cli/options.py`** — Factor out shared Click option decorators. No runtime behavior change.

### Priority 2 — Deduplication (removes duplicated code, requires testing)

5. **Create `core/repo_cloner.py`** — Extract git clone logic. Replace both occurrences in `analyze()`. Write unit tests. This eliminates ~300 duplicate lines and fixes any future bugs in one place.

6. **Deduplicate ML config dict assembly** — Create `build_ml_config(cfg)` and `build_llm_config(cfg)` factory functions. Replace the 3 copies in `analyze`, `identities`, `aliases` commands.

### Priority 3 — Command split (structural reorganization)

7. **Create `cli/commands/identity.py`** — Move all 5 identity/alias commands. This is a large but clean extraction since these commands share no runtime state with `analyze`.

8. **Create `cli/commands/training.py`** — Move `train` and `training_statistics`. Small and low-risk.

9. **Create `cli/commands/pipeline.py`** — Move `collect_command`, `classify_command`, `report_command`. These are already thin wrappers and easy to extract.

10. **Create `cli/commands/setup.py`** and **`cli/commands/cache.py`** and **`cli/commands/help.py`** — Move remaining simple commands.

### Priority 4 — Core pipeline extraction (highest complexity, highest value)

11. **Create `core/analyze_pipeline.py`** — Extract the 4 major pipeline stage functions from `analyze()`. This is the most impactful refactoring but also the most complex due to the deeply nested state sharing (`display`, `progress`, `cache`, `cfg`, etc.). Requires careful interface design.

12. **Slim down `cli/commands/analyze.py`** — After extracting pipeline stages, `analyze()` should become an orchestration function that assembles inputs and calls the pipeline functions in sequence.

13. **Create `cli/app.py`** — Move `cli()` group, `AnalyzeAsDefaultGroup`, and `main()`. Replace `cli.py` with the 10-line compatibility shim.

### Priority 5 — Inline class cleanup

14. **Move `CICDConfig` class** — The inline class defined at line 786 inside `analyze()` should become a proper dataclass in `config/cicd.py` or handled by the existing config system.

---

## Risk Assessment

| Risk | Severity | Mitigation |
|---|---|---|
| Breaking lazy imports in `analyze()` | Medium | Keep lazy imports inside the pipeline stage functions that need them |
| Circular imports between `cli/` and `core/` | Low | `core/` never imports from `cli/` — one-way dependency |
| State sharing between pipeline stages | High | Design `core/analyze_pipeline.py` functions to take explicit arguments, not closures |
| Breaking `pyproject.toml` entry points | Low | Keep `cli.py` compatibility shim pointing to `cli.app:main` |
| Inner `progress_callback` closures capturing outer state | Medium | Pass `display` as explicit argument to extracted functions |
| Test coverage for current `analyze()` | Unknown | Run existing test suite after each priority phase |

---

## Notes on Current Test Files

From the git status, test files exist for:
- `tests/test_cli.py` — likely tests Click command invocation
- `tests/core/test_identity.py`, `tests/utils/test_commit_utils.py` — unit tests for core modules

No specific tests for the `analyze()` pipeline stages were observed. The refactoring should add unit tests for each extracted pipeline function as part of Priority 4.

---

## Summary

The core recommendation is to treat `cli.py` as three distinct problems:

1. **CLI wiring** (group, routing, formatting, options) → `cli/` package (~560 lines total)
2. **Command implementations** → `cli/commands/` package (~2,990 lines total)
3. **Business logic accidentally in CLI** → `core/` and `utils/` modules (~860 lines total)

The `analyze()` God Function should be dissolved into discrete pipeline stage functions with explicit interfaces. This alone eliminates the most significant maintainability risk: any future bug in data fetching, validation, or report generation currently requires navigating a 4,000-line function to find the relevant 50-line section.
