Skip to content

0005 — Split large command modules by responsibility

  • Status: Accepted
  • Deciders: rhiza-tools maintainers

Context

Command modules naturally mix three concerns: the Typer command surface and orchestration, version/bump logic, and git plumbing. Left together they grow past the size at which a file is easy to reason about (the original bump.py crossed this line in #207).

Decision

When a command module grows large, extract a cohesive responsibility into a sibling module and re-export the moved names from the original module so the public import surface — and existing tests — are unchanged. The established pattern:

Command Surface / orchestration Extracted module(s)
bump bump.py bump_versioning.py, bump_engine.py (the adapter, ADR-0001)
release release.py release_versioning.py (bump-type resolution)
rollback rollback.py rollback_git.py (git tag/commit plumbing)

Because the helpers are re-exported, callers and tests continue to import release.<helper> / rollback.<helper>. When a moved function looks up a dependency in its new module's namespace, the corresponding test patch target moves with it (e.g. release_versioning.bump_command, rollback_git.run_git_command).

The test_module_size gate enforces the ceiling that triggers this; after the #223 split the largest command module is ~700 lines and the gate is set to 750.

Amendment — sibling suffixes became subpackages

The flat suffix files described above were later folded into per-command subpackages: bump_versioning.pybump/versioning.py, release_git.pyrelease/git.py, and so on, with the orchestration module becoming the package's __init__.py. The convention is otherwise unchanged — the __init__.py re-exports the extracted helpers, dependencies still point downward only, and patch targets follow the dependency into the new module namespace (now commands.release.versioning.bump_command, commands.rollback.git.run_git_command). See the Architecture guide for the current layout; tests/ mirrors it under tests/commands/<command>/.

Consequences

  • Each module has a single clear responsibility and stays navigable.
  • The re-export convention keeps refactors non-breaking for importers.
  • Splitting interacts with the test suite's module-namespace patching: moving code requires moving the patch target to where the dependency now resolves.