refactor-split
Split a single overlarge module into multiple focused modules — without changing behavior. Sibling to refactor-extract (function-level) and refactor-architecture (package-level). Loads when the operat
SKILL.md.claude/skills/refactor-split/SKILL.md
GitHubrefactor-split
Splits a single overlarge module into multiple focused modules. Sits between
refactor-extract (function/class-level — extract one thing within a file)
and refactor-architecture (package-level — restructure many packages).
This skill operates at the FILE level: one module becomes 2+ modules,
typically with a shared package directory.
Triggered when a single .py file accumulates multiple responsibilities and
becomes hard to navigate. AICP convention (CLAUDE.md): "Keep modules small
and focused. One responsibility per file." Splits enforce that.
Trigger phrases (when to load this skill)
Load when the conversation matches any of:
- Direct verb: operator says "this file is too big", "split aicp/core/X.py", "router.py is doing 5 things", "break this module up", "factor this into multiple files"
- Quality-audit / quality-lint finding: a single file is hot in multiple metrics (line count high + cyclomatic complexity high + responsibility unclear) — the file needs splitting, not just internal extraction
- Quality-coverage finding: coverage gaps cluster in one file because its responsibilities are too tangled to test in isolation — split first, then test
- From quality-debt: a TODO or HACK marker says "// TODO: split this file when we figure out the right boundaries"
- From refactor-architecture: package-level refactor identified that one of the destination packages should be a multi-file package, not one giant module — load this skill to split
Do NOT load when:
- Function-level extraction within ONE file is enough (load
refactor-extract) - Package-level restructuring is needed (load
refactor-architecture) - Behavior needs to change (load
feature-implement) - Lexical rename only (load
refactor-rename)
Operations
This skill has 4 named operations.
Operation 1: Identify the responsibilities to split + the destination layout
Trigger: skill loaded; oversized module identified.
Process:
- Read the source module. Capture: total line count, top-level definitions (classes, functions, constants), implicit dependencies between them.
- Identify the SEAMS — where natural responsibility boundaries lie:
- Group definitions by what data they operate on (cohesion: same data ⇒ same module)
- Group by who calls them (callers from one package suggest the definition belongs near that package)
- Group by lifecycle (initialization-only vs runtime-only vs shutdown-only suggest different modules)
- Group by conceptual layer (low-level utilities vs high-level orchestration)
- Choose the destination layout:
- In-place split:
router.pybecomesrouter/__init__.py+router/scoring.py+router/dispatch.py+router/circuit.py. The package is created at the OLD module's path. Importers see the samefrom aicp.core.router import Xif__init__.pyre-exports. - Sibling split:
router.pystays but slimmer; newrouter_scoring.pyandrouter_circuit.pysiblings created. Less disruption to imports. - Submodule split:
router.pystays as the public entrypoint; new moduleaicp/core/_router/scoring.pyetc. holds internals (underscore-prefix package signals private).
- In-place split:
- For EACH new module, name what it contains in 1 sentence:
router/scoring.py— pure scoring logic, no I/Orouter/dispatch.py— backend dispatch + failover chain traversalrouter/circuit.py— circuit breaker state machine
- Verify the split makes IMPORTS cleaner, not harder. If
router/dispatch.pyends up importingrouter/scoring.pyANDrouter/circuit.pyAND being imported byrouter/__init__.py— fine, layered. If the new modules form circular imports, the split is wrong — re-think the seams. - Document the plan in chat for operator approval.
Quality bar (Operation 1 done when):
- Source module enumerated (definitions, line counts, dependencies)
- Seams identified with reasoning (cohesion / callers / lifecycle / layer)
- Destination layout chosen (in-place / sibling / submodule)
- Each new module has a 1-sentence purpose
- No circular imports in the proposed layout
- Operator approved the plan
Operation 2: Verify test coverage exists
Trigger: Operation 1 plan approved.
Process:
Find tests that exercise the source module:
grep -rn "from aicp.core.router\|aicp.core.router\." tests/ 2>/dev/null pytest tests/ --collect-only -q 2>/dev/null | grep routerRun them with coverage:
pytest tests/test_router*.py --cov=aicp.core.router --cov-report=term-missing 2>&1 | tail -20If coverage is insufficient (lines being moved aren't exercised), STOP and load
quality-coverageorfeature-testto add coverage first. Refactoring uncovered code is silent regression risk.Capture BASELINE: test count, pass rate, coverage % per file.
Quality bar (Operation 2 done when):
- Tests exercising the source module identified
- Coverage on lines-to-move confirmed
- Baseline test count + coverage captured
Operation 3: Apply the split in 4 sub-steps
Trigger: Operation 2 baseline confirmed.
Process (4 sub-steps, each in its own commit):
Sub-step A: Create the new module(s) — empty shells
- For in-place split:
mkdir aicp/core/router/then create__init__.py(initially re-exports everything from a temporary_legacy.py) + the planned new files (initially empty). - For sibling/submodule splits: just create the new files, empty.
- The OLD module still works. New shells are dead code at this point.
- Run tests — must still pass (the change is purely structural).
- Lint passes.
- Commit:
refactor(<scope>): scaffold split of <module> into <new layout>.
Sub-step B: Move the actual code — one logical group per commit
- Pick ONE responsibility group from Operation 1 (e.g., the scoring functions). Cut them from the source module, paste into the new module.
- Update internal imports within the source module + new module. Use
from .X import Y(relative imports for intra-package). - Update the
__init__.pyto re-export from the new module so external imports keep working:from .scoring import compute_score. - Run tests — must still pass.
- Lint passes.
- Commit:
refactor(<scope>): move <group> into <new module>. - Repeat for each responsibility group. ONE commit per group, not one giant commit.
Sub-step C: Update external imports (optional, can be separate task)
- Decide: do external callers update their imports now (
from aicp.core.router.scoring import compute_scoredirectly), OR keep using the old path (from aicp.core.router import compute_score) which works via__init__.pyre-exports? - Recommendation: leave re-exports in place. Updating ALL external imports is high-churn for low value (the re-exports are stable). Update only when you're touching the caller for other reasons.
- If you DO update some imports, run the tests after each batch.
Sub-step D: Cleanup the temporary _legacy.py (if used)
- If Sub-step A used a
_legacy.pyshim, now that all responsibilities have moved, the shim is empty (or has only re-exports). Delete it. - Run tests.
- Commit:
refactor(<scope>): remove legacy shim from split.
Quality bar (Operation 3 done when):
- Each sub-step committed independently with passing tests
- Each new module has its responsibility group + only that group
-
__init__.pyre-exports the public surface (external imports unchanged unless explicitly updated) - No circular imports
- Test count unchanged from Operation 2 baseline
- Lint clean throughout
Operation 4: Verify behavior preservation + close out
Trigger: Operation 3 sub-steps complete.
Process:
- Final verification:
Full test suite:
pytest tests/ --tb=short— exit 0, count unchangedLint:
ruff check + ruff format --check— exit 0Wiki lint (if AGENTS.md or CLAUDE.md mentioned the old structure, update):
grep -rn "aicp/core/<old_module>\.py" AGENTS.md CLAUDE.md wiki/ 2>/dev/nullFor each match, update to reflect the new layout.
- Update
wiki/config/domain-profiles/backend-ai-platform-python.yamlif path patterns referenced the old module specifically (usually patterns are glob-based and unaffected). - If the split surfaced a SYSTEMIC pattern (e.g., "every backend file in
aicp/backends/is starting to look like router did — they all need internal splits"), file as a follow-up task. - No standalone wiki page needed for routine splits — commit messages capture the refactor. EXCEPTION: if the split creates a new public package (with
__init__.pyexposing API), add a brief module-level docstring in__init__.pyexplaining the package's purpose and public surface.
Quality bar (Operation 4 done when):
- Full suite + lint final-check pass
- AGENTS.md / CLAUDE.md / wiki references to old structure updated
- Domain profile path patterns confirmed still applicable
- Lesson contributed if systemic
- New package (if created) has docstring at
__init__.pytop
Gotchas (known failure modes — read before doing)
Gotcha 1: Splitting along arbitrary lines (random groupings)
The temptation: file is 1200 lines, split into 4 modules of ~300 lines each. NO — splits should follow RESPONSIBILITY seams, not line counts. A 600-line module focused on one responsibility is fine; a 200-line module mixing 3 responsibilities is bad.
Detection: did Operation 1 step 2 identify seams BY RESPONSIBILITY (cohesion / callers / lifecycle / layer)? Or did you just chunk the file?
The rule: split where responsibilities diverge, not where line counts cross thresholds. A "too long" module that has only one responsibility is a candidate for refactor-extract (extract a few helpers), not refactor-split.
Gotcha 2: Breaking external imports without re-exports (consumer breakage)
The temptation: split the module, update internal imports, ship it. External callers (other AICP modules, fleet's agent-tooling.yaml referencing skills, tests) suddenly fail with ImportError. You broke the public surface.
Detection: grep -rn "from aicp.core.<old_module> import" aicp/ tools/ tests/ ../openfleet/ 2>/dev/null — any matches?
The rule: __init__.py re-exports preserve the public surface. External imports keep working. Only update external imports as a SEPARATE pass with its own commits (Operation 3 Sub-step C).
Gotcha 3: Circular imports between split modules (dead-on-arrival)
The temptation: cut the module along seams that LOOK clean, but the actual imports form a cycle: scoring.py imports dispatch.py, dispatch.py imports circuit.py, circuit.py imports scoring.py. Python detects circular imports at import time; the package doesn't load.
Detection: did Operation 1 step 5 verify the import graph is a DAG (no cycles)?
The rule: layer the new modules. Lower-level modules (scoring is data-driven, no I/O) at the bottom; higher-level modules (dispatch orchestrates) at the top. If you find a cycle, restructure the seams (often the seam itself is wrong).
Gotcha 4: Splitting before testing (regression risk)
Same as refactor-extract Gotcha 1, applied at module scale. If the source module's lines aren't covered by tests, splitting can introduce silent regressions (different import order = different module-level state, different error wrapping, etc.).
Detection: Operation 2 confirmed coverage on lines being moved?
The rule: tests first, split second. The cost of pausing to add coverage is small; the cost of a silent split regression in production is large.
Gotcha 5: One giant commit (un-revertable)
The temptation: do all 4 sub-steps in one commit because "they're all related." But if Sub-step C broke something, you can only revert THE WHOLE thing, losing Sub-steps A and B's work.
Detection: more than 5-10 files changed in a single commit on a refactor branch?
The rule: per Operation 3 — each sub-step is independently revertable. Each ends in a buildable + testable state.
Reference exemplars
Per Extension Standards, reference exemplars are the second brain's model-builder and wiki-agent skills. For the OUTPUT artifact:
- Sibling skills: refactor-architecture (package-level — multi-package restructure) and refactor-extract (function-level — pull out one thing)
- Real example: this repo's CLAUDE.md slim (646→265 lines) was a hybrid — content extraction (move sections to AGENTS.md) + structural reduction. See git history for the multi-step pattern this skill formalizes.
Domain context
This skill operates in backend-ai-platform-python — see wiki/config/domain-profiles/backend-ai-platform-python.yaml. AICP convention (CLAUDE.md): "Keep modules small and focused. One responsibility per file." Splits enforce this. Common AICP split candidates: aicp/core/router.py (scoring + dispatch + circuit + escalation), aicp/core/controller.py (mode enforcement + dispatch + events).
Related skills
| Skill | When | Why distinct |
|---|---|---|
| refactor-extract | extract a function/class within a file | Function-level; this skill is module-level |
| refactor-architecture | restructure packages | Package-level; this skill is module-level |
| refactor-rename | lexical rename | Mechanical; this skill is structural |
| refactor-patterns | apply a design pattern | Pattern-level |
| refactor-dependencies | dep hygiene | Different scope |
| feature-implement | new behavior | Refactor preserves behavior |
| quality-audit | umbrella that surfaces split candidates | Sibling — audit identifies need; this skill executes |