Skill: polylith-migrate-isolate-base-and-big-component
Goal
Shrink the temporary migration base into thin base(s) + one big component:
- Bases: Contain only entrypoints/wiring (e.g., FastAPI endpoints, CLI wiring, consumer wiring).
- Big Component: Contains all other code.
⚠ Base vs component naming (read first). A base and a component cannot share
the same brick name — both would resolve to the Python package <TARGET_TOP_NS>.<name>
and collide. So the big component takes INITIAL_BASE_NAME
(<TARGET_TOP_NS>.<INITIAL_BASE_NAME>) and each base is renamed to a distinct,
entrypoint-specific name (the <base> used in step 5 — e.g. <INITIAL_BASE_NAME>_api,
<INITIAL_BASE_NAME>_handler). This is why the temporary base must be renamed when
the component claims its name.
Multiple entrypoints → multiple thin bases. A real service often has several
deployable entrypoints (an HTTP API + one or more queue consumers + scheduled jobs).
Create one thin base per entrypoint (e.g. <x>_api, <x>_handler, <x>_jobs),
each exposing only its run/wiring function and importing logic from components. Do
not force them into a single base. (Shared startup wiring — init_db,
init_logging, … — goes into a small shared component, not duplicated per base; see
polylith-migrate-distribute-wiring.)
Inputs
From migration/<PROJECT>/state.md:
TARGET_TOP_NS
INITIAL_BASE_NAME
- Verification commands.
From migration/<PROJECT>/manifest.md:
All inputs from state.md are assumed to satisfy the validation rules in polylith-migrate-discover (### Validation rules). Validate before proceeding.
Steps
1. Create the Big Component
- Create
components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/.
- ⚠ If the temporary base currently occupies
bases/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/, rename the base(s) first — they must not keep the same name as the big component (see the naming callout above). Create one renamed thin base per entrypoint (<base> in step 5), and update the project's run scripts / Dockerfile to point at the new base name(s).
2. Move Non-Entrypoint Code
- Move non-entrypoint code from the base(s) to the big component.
3. Define Public API
- Define a minimal public API in
components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/__init__.py.
4. Update Bases
5. Update pyproject.toml
6. Update manifest.md
- Reflect the new structure in
migration/<PROJECT>/manifest.md.
7. FastAPI Guidance
| Stays in Base |
Moves to Big Component |
app = FastAPI(...) |
Domain/business logic |
| Middleware, router registration |
Persistence/repositories |
| Route handlers (endpoints) |
External integrations |
| Startup/shutdown/lifespan wiring |
Reusable parsing/validation |
Verify
RUN_TEST_CMD succeeds.
- If set,
RUN_LINT_CMD and RUN_TYPECHECK_CMD succeed.
- Run
POLY_CMD_PREFIX check to validate the workspace structure.
- Run
POLY_CMD_PREFIX sync to synchronize the [tool.polylith.bricks] table with actual imports.
Common failure modes
| Symptom |
Likely cause |
Remediation |
| Circular import: base imports from component, component imports back from base |
Some "non-entrypoint" code was moved but still references base-only helpers (e.g. the FastAPI app instance). |
Move the helper down into the component, or invert the dependency by passing the needed value as a function argument. The base's app instance must never be imported by a component. |
poly check / import error: base and big component both named <INITIAL_BASE_NAME> |
The base wasn't renamed when the component claimed INITIAL_BASE_NAME — two bricks now map to <TARGET_TOP_NS>.<INITIAL_BASE_NAME>. |
Rename the base to an entrypoint-specific name (<INITIAL_BASE_NAME>_api, _handler, …), update [tool.polylith.bricks] and the run scripts, then POLY_CMD_PREFIX sync. |
ImportError: cannot import name '<x>' from '<TARGET_TOP_NS>.<INITIAL_BASE_NAME>' |
The big component's __init__.py doesn't re-export <x>. Code that previously reached into submodules now needs the public API. |
Add from <TARGET_TOP_NS>.<INITIAL_BASE_NAME>.<submodule> import <x> to the component's __init__.py, or have the caller import the submodule directly (and accept the brick-interface violation that poly deps --interface will flag). |
| Base file becomes near-empty after the split |
Good — that's the goal. But check: is there any wiring left, or did you accidentally move the entrypoint itself? |
If the entrypoint (e.g., app = FastAPI(...)) ended up in the component, move it back to the base. The base must own the entrypoint object. |
poly check flags the component as not used by any project |
The base's imports go to the wrong namespace (e.g., from <ORIG_TOP_NS>...) so the import graph doesn't reach the component. |
Update base imports to from <TARGET_TOP_NS>.<INITIAL_BASE_NAME> import … and re-run POLY_CMD_PREFIX sync. |
| Tests for moved code now fail to find fixtures |
conftest.py was left in the base or moved to the wrong scope. |
Move test fixtures alongside the code they cover; usually that's under test/components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/. |
| Verification fails and you can't quickly diagnose |
Phase commit not yet made. |
git reset --hard HEAD to roll back to the previous phase's commit and consult the user. |
Commit
After verification passes, commit this phase to the migration branch:
git add -A && git commit -m "migrate(<PROJECT>): phase <N> — isolate-base-and-big-component"
Substitute <PROJECT>, <N>, and <phase-name> from state.md and the orchestrator's phase table. Do not proceed to the next phase without a clean commit — the per-phase commit is the rollback point for the next phase's failure-mode tables.
1---2name: polylith-migrate-isolate-base-and-big-component3description: [Internal sub-skill of `polylith-migrate-orchestrator`. Do not load directly — load `polylith-migrate-orchestrator` first, which drives all phases.] Shrink the temporary migration base into thin base(s) + one big component. Bases contain only entrypoints/wiring, while the big component contains everything else.4---56# Skill: polylith-migrate-isolate-base-and-big-component78## Goal9Shrink the temporary migration base into thin base(s) + one big component:10- **Bases**: Contain only entrypoints/wiring (e.g., FastAPI endpoints, CLI wiring, consumer wiring).11- **Big Component**: Contains all other code.1213> ⚠ **Base vs component naming (read first).** A base and a component **cannot share14> the same brick name** — both would resolve to the Python package `<TARGET_TOP_NS>.<name>`15> and collide. So the **big component takes `INITIAL_BASE_NAME`**16> (`<TARGET_TOP_NS>.<INITIAL_BASE_NAME>`) and each **base is renamed** to a distinct,17> entrypoint-specific name (the `<base>` used in step 5 — e.g. `<INITIAL_BASE_NAME>_api`,18> `<INITIAL_BASE_NAME>_handler`). This is *why* the temporary base must be renamed when19> the component claims its name.20>21> **Multiple entrypoints → multiple thin bases.** A real service often has several22> deployable entrypoints (an HTTP API + one or more queue consumers + scheduled jobs).23> Create **one thin base per entrypoint** (e.g. `<x>_api`, `<x>_handler`, `<x>_jobs`),24> each exposing only its run/wiring function and importing logic from components. Do25> **not** force them into a single base. (Shared startup wiring — `init_db`,26> `init_logging`, … — goes into a small shared component, not duplicated per base; see27> `polylith-migrate-distribute-wiring`.)2829## Inputs30From `migration/<PROJECT>/state.md`:31- `TARGET_TOP_NS`32- `INITIAL_BASE_NAME`33- Verification commands.3435From `migration/<PROJECT>/manifest.md`:36- Entrypoints list.3738> All inputs from `state.md` are assumed to satisfy the validation rules in `polylith-migrate-discover` (`### Validation rules`). Validate before proceeding.3940## Steps4142### 1. Create the Big Component43- Create `components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/`.44- ⚠ If the temporary base currently occupies `bases/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/`, **rename the base(s) first** — they must not keep the same name as the big component (see the naming callout above). Create one renamed thin base per entrypoint (`<base>` in step 5), and update the project's run scripts / Dockerfile to point at the new base name(s).4546### 2. Move Non-Entrypoint Code47- Move non-entrypoint code from the base(s) to the big component.4849### 3. Define Public API50- Define a minimal public API in `components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/__init__.py`.5152### 4. Update Bases53- Update bases to import only from component APIs:54 ```python55 from <TARGET_TOP_NS>.<INITIAL_BASE_NAME> import ...56 ```5758### 5. Update `pyproject.toml`59- Add the new component to `[tool.polylith.bricks]`:60 ```toml61 [tool.polylith.bricks]62 "../../bases/<TARGET_TOP_NS>/<base>" = "<TARGET_TOP_NS>/<base>"63 "../../components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>" = "<TARGET_TOP_NS>/<INITIAL_BASE_NAME>"64 ```6566### 6. Update `manifest.md`67- Reflect the new structure in `migration/<PROJECT>/manifest.md`.6869### 7. FastAPI Guidance70| Stays in Base | Moves to Big Component |71|---------------|------------------------|72| `app = FastAPI(...)` | Domain/business logic |73| Middleware, router registration | Persistence/repositories |74| Route handlers (endpoints) | External integrations |75| Startup/shutdown/lifespan wiring | Reusable parsing/validation |7677## Verify78- `RUN_TEST_CMD` succeeds.79- If set, `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` succeed.80- Run `POLY_CMD_PREFIX check` to validate the workspace structure.81- Run `POLY_CMD_PREFIX sync` to synchronize the `[tool.polylith.bricks]` table with actual imports.8283## Common failure modes8485| Symptom | Likely cause | Remediation |86|---------|--------------|-------------|87| Circular import: base imports from component, component imports back from base | Some "non-entrypoint" code was moved but still references base-only helpers (e.g. the FastAPI `app` instance). | Move the helper down into the component, or invert the dependency by passing the needed value as a function argument. The base's `app` instance must **never** be imported by a component. |88| `poly check` / import error: base and big component both named `<INITIAL_BASE_NAME>` | The base wasn't renamed when the component claimed `INITIAL_BASE_NAME` — two bricks now map to `<TARGET_TOP_NS>.<INITIAL_BASE_NAME>`. | Rename the base to an entrypoint-specific name (`<INITIAL_BASE_NAME>_api`, `_handler`, …), update `[tool.polylith.bricks]` and the run scripts, then `POLY_CMD_PREFIX sync`. |89| `ImportError: cannot import name '<x>' from '<TARGET_TOP_NS>.<INITIAL_BASE_NAME>'` | The big component's `__init__.py` doesn't re-export `<x>`. Code that previously reached into submodules now needs the public API. | Add `from <TARGET_TOP_NS>.<INITIAL_BASE_NAME>.<submodule> import <x>` to the component's `__init__.py`, or have the caller import the submodule directly (and accept the brick-interface violation that `poly deps --interface` will flag). |90| Base file becomes near-empty after the split | Good — that's the goal. But check: is there *any* wiring left, or did you accidentally move the entrypoint itself? | If the entrypoint (e.g., `app = FastAPI(...)`) ended up in the component, move it back to the base. The base must own the entrypoint object. |91| `poly check` flags the component as not used by any project | The base's imports go to the wrong namespace (e.g., `from <ORIG_TOP_NS>...`) so the import graph doesn't reach the component. | Update base imports to `from <TARGET_TOP_NS>.<INITIAL_BASE_NAME> import …` and re-run `POLY_CMD_PREFIX sync`. |92| Tests for moved code now fail to find fixtures | `conftest.py` was left in the base or moved to the wrong scope. | Move test fixtures alongside the code they cover; usually that's under `test/components/<TARGET_TOP_NS>/<INITIAL_BASE_NAME>/`. |93| Verification fails and you can't quickly diagnose | Phase commit not yet made. | `git reset --hard HEAD` to roll back to the previous phase's commit and consult the user. |9495## Commit9697After verification passes, commit this phase to the migration branch:9899```bash100git add -A && git commit -m "migrate(<PROJECT>): phase <N> — isolate-base-and-big-component"101```102103Substitute `<PROJECT>`, `<N>`, and `<phase-name>` from `state.md` and the orchestrator's phase table. Do not proceed to the next phase without a clean commit — the per-phase commit is the rollback point for the next phase's failure-mode tables.