diff --git a/.agents/skills/polylith/README.md b/.agents/skills/polylith/README.md index 438db3da..98de999d 100644 --- a/.agents/skills/polylith/README.md +++ b/.agents/skills/polylith/README.md @@ -3,7 +3,14 @@ > **Note for contributors:** this README is a **human reference**. The agent loads each `*/SKILL.md` independently via the skill loader; this file is **not** auto-loaded with any skill. Anything an agent must know to act has to live in the relevant `SKILL.md` itself, not here. -## Available Skills +## Skill loading model + +Two kinds of skill live under this directory; the distinction matters when picking an entry point: + +- **Atomic skills (`polylith-*`).** Each maps to one `poly` CLI command (or one focused concept). Safe to load in isolation; individually composable. These cover everyday Polylith workflows — creating bricks, syncing, checking, inspecting, and so on. +- **Orchestrated skill set (`migrate-project/migrate-*`).** A multi-phase workflow with shared state (`migration//state.md`) and a git safety net. **Never load an individual `migrate-*` skill directly** — always load `migrate-orchestrator` and let it drive the phases in order. See [`migrate-project/README.md`](./migrate-project/README.md). This is an advanced, explicit-opt-in workflow used for migrating a **non-Polylith** project into a Polylith workspace, **not** part of daily Polylith use. + +## Available Skills (daily Polylith workflows) | Skill | Command | Purpose | |---------------------------|--------------------|----------------------------------------------------------------------------------------------------------| @@ -15,8 +22,13 @@ | [Sync](./polylith-sync/SKILL.md) | `poly sync` | Update each project's brick list to match actual imports. | | [Workspace Inspection](./polylith-workspace-inspection/SKILL.md) | `poly info` | Show brick × project usage (which projects use which bricks). | | [Dependency Visualization](./polylith-dependency-visualization/SKILL.md) | `poly deps` | Show brick × brick dependencies and interface compliance. | +| [Dependency Management](./polylith-dependency-management/SKILL.md) | — | Add or manage third-party libraries for a brick or project. | | [Testing](./polylith-testing/SKILL.md) | `poly test diff` | List bricks/projects affected by **test-code** changes since a tag. | | [Diff](./polylith-diff/SKILL.md) | `poly diff` | List bricks whose **implementation** changed since a tag. | | [Check](./polylith-check/SKILL.md) | `poly check` | Validate the workspace (CI gate; exits 1 on failure). | | [Libs](./polylith-libs/SKILL.md) | `poly libs` | Inspect third-party libraries per project. | -| [Concepts](./polylith-concepts/SKILL.md) | — | Provides foundational knowledge about Polylith architecture and terminology. | +| [Concepts](./polylith-concepts/SKILL.md) | — | Foundational knowledge about Polylith architecture and terminology. | + +## Advanced workflow + +For migrating an existing **non-Polylith** Python project into a Polylith workspace, see [`migrate-project/README.md`](./migrate-project/README.md). This is a destructive, multi-phase, explicit-opt-in workflow — start with the `migrate-orchestrator` skill, not any individual `migrate-*` sub-skill. diff --git a/.agents/skills/polylith/migrate-project/README.md b/.agents/skills/polylith/migrate-project/README.md new file mode 100644 index 00000000..3192b08c --- /dev/null +++ b/.agents/skills/polylith/migrate-project/README.md @@ -0,0 +1,111 @@ +# Project Migration Skills + +> **Note for contributors:** this README is a **human reference** and a skill **index**. Agents load each `migrate-*/SKILL.md` independently via the skill loader; this README is **not** auto-loaded with any skill. Anything an agent must know to act has to live in the relevant `SKILL.md` itself, not here. + +This directory contains skills for migrating **non-Polylith Python projects** into a Polylith workspace. They cooperate via two artifacts under `migration//`: + +- `state.md` — a flat `KEY=value` file. Canonical schema lives in [`migrate-discover/SKILL.md`](./migrate-discover/SKILL.md). +- `manifest.md` — a human-readable structural inventory of the source project. + +--- + +## ⚠️ When to use these skills + +**Only** when: +1. A human has **explicitly instructed** to migrate a specific project (e.g., "migrate `projects/my-app` to Polylith"). +2. The target project lives under `projects//` of this Polylith workspace. +3. The goal is to refactor the project into Polylith bricks (bases and components). + +**Do not use** for: +- Automated or unattended migrations. +- Projects that are already structured as Polylith bricks. +- Daily Polylith development tasks — for those, see the sibling `polylith-*` skills. + +--- + +## How to invoke + +Load the orchestrator and let it drive the rest: + +``` +Load the `migrate-orchestrator` skill and migrate `projects/`. +``` + +The orchestrator will: +1. Ask the user for explicit confirmation. +2. Establish a git safety net (dedicated branch + per-phase commits). +3. Load and execute each phase skill in order, verifying after each. + +> **Why every sub-skill says "do not load directly".** Each `migrate-` skill's `description:` starts with a redirect to `migrate-orchestrator`. This is intentional: it makes the orchestrator the only valid entry point regardless of which sub-skill the agent's fuzzy-match initially favours. The sub-skills depend on state (`migration//state.md`) and a git safety net that only the orchestrator sets up. + +--- + +## Downstream installation + +When this skill set is installed into another Polylith workspace (e.g. via a skills package), the in-skill `[ENTRY POINT]` / `[Internal sub-skill of migrate-orchestrator …]` markers in each `description:` are the primary routing signal — they ship with the package. + +For an extra-strong signal, downstream consumers should add the following snippet to their own repo-level `AGENTS.md` (or equivalent agent-routing file). It is **not** required — the in-skill markers are usually sufficient — but it removes any ambiguity for agents that read `AGENTS.md` before scanning skill descriptions. + +```markdown +## Polylith migration instructions + +When the user asks to migrate a non-Polylith Python project to Polylith +(e.g. "migrate `projects/` to Polylith"), load the +`migrate-orchestrator` skill first and let it drive the workflow. + +Never load `migrate-discover`, `migrate-extract-to-base`, or any other +`migrate-*` sub-skill directly — they are phases the orchestrator +invokes in order, with per-phase verification and git checkpoints +between them. +``` + +--- + +## Workflow at a glance + +| # | Phase | Skill | Depends on | +|---|----------------------------------------|------------------------------------------------|---------------------------------------------------------------------| +| — | Orchestration | [`migrate-orchestrator`](./migrate-orchestrator/SKILL.md) | — | +| 1 | Discover | [`migrate-discover`](./migrate-discover/SKILL.md) | — | +| 2 | Extract to base | [`migrate-extract-to-base`](./migrate-extract-to-base/SKILL.md) | `migrate-discover` | +| 3 | Prepare project | [`migrate-prepare-project`](./migrate-prepare-project/SKILL.md) | `migrate-extract-to-base` | +| 4 | Isolate base and big component | [`migrate-isolate-base-and-big-component`](./migrate-isolate-base-and-big-component/SKILL.md) | `migrate-prepare-project` | +| 5 | Split big component | [`migrate-split-big-component`](./migrate-split-big-component/SKILL.md) | `migrate-isolate-base-and-big-component` | +| 6 | Extract standalone modules | [`migrate-extract-standalone-modules`](./migrate-extract-standalone-modules/SKILL.md) | `migrate-split-big-component` | +| 7 | Isolate shared and project logic | [`migrate-isolate-shared-and-project-logic`](./migrate-isolate-shared-and-project-logic/SKILL.md) | `migrate-extract-standalone-modules`, `migrate-split-big-component` | +| 8 | Distribute wiring | [`migrate-distribute-wiring`](./migrate-distribute-wiring/SKILL.md) | `migrate-isolate-shared-and-project-logic` | +| 9 | Split component internals | [`migrate-split-component-internals`](./migrate-split-component-internals/SKILL.md) | `migrate-distribute-wiring` | +|10 | Refactor tests | [`migrate-refactor-tests`](./migrate-refactor-tests/SKILL.md) | `migrate-split-component-internals` | +|11 | Definition of done | [`migrate-definition-of-done`](./migrate-definition-of-done/SKILL.md) | `migrate-refactor-tests` | + +### Optional skills (triggered during `migrate-discover`) + +| Skill | Purpose | Trigger / dependency | +|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [`migrate-convert-linter`](./migrate-convert-linter/SKILL.md) | Align the project's linter/formatter with the **workspace's** configured tool. | `CONVERT_LINTER=yes` in `state.md`. Runs between phase 1 and phase 2. | +| [`migrate-convert-type-checker`](./migrate-convert-type-checker/SKILL.md) | Align the project's type checker with the **workspace's** configured tool. | `CONVERT_TYPE_CHECKER=yes` in `state.md`. Runs between phase 1 and phase 2. | +| [`migrate-convert-package-manager`](./migrate-convert-package-manager/SKILL.md) | Convert the project's `pyproject.toml` to uv workspaces. **Opinionated about uv** — only run when the workspace itself uses uv. | `CONVERT_PACKAGE_MANAGER=yes` in `state.md`. Runs between phase 1 and phase 2. | +| [`migrate-dedupe`](./migrate-dedupe/SKILL.md) | Identify and apply controlled deduplication discovered during refactoring. | User approval. Runs after phase 5 or phase 6. | + +--- + +## Scope of the four "splitting" skills + +These skills overlap in vocabulary but address different scopes. Use this matrix to decide which one applies: + +| Skill | Scope | Trigger | +|---------------------------------------------|------------------------------------------------|---------------------------------------------------------------------------| +| `migrate-split-big-component` | Within one project; component → multiple components. | The temporary big component from phase 4 is too large. | +| `migrate-extract-standalone-modules` | Within one project; pulls foundational modules out of the residual. | Residual still contains `consts.py`/`exceptions.py`/`models.py`. | +| `migrate-split-component-internals` | Within one already-extracted shared component; `core.py` → multiple files. | A component's `core.py` mixes multiple domains internally. | +| `migrate-isolate-shared-and-project-logic` | Cross-project; separate shared vs project-specific in components used by ≥ 2 projects. | Migrating a 2nd+ project that overlaps with an already-extracted one. | + +> 💡 In a fresh migration of a single project, you usually run `migrate-split-big-component` → `migrate-extract-standalone-modules` → `migrate-split-component-internals`, and skip `migrate-isolate-shared-and-project-logic` until a second project is migrated. + +--- + +## Files in this directory + +- `README.md` — this file (human reference + index). +- `migrate-orchestrator/SKILL.md` — entry point; defines the phase order and the git safety net. +- `migrate-/SKILL.md` — one per phase listed above. diff --git a/.agents/skills/polylith/migrate-project/migrate-convert-linter/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-convert-linter/SKILL.md new file mode 100644 index 00000000..a990059e --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-convert-linter/SKILL.md @@ -0,0 +1,85 @@ +--- +name: migrate-convert-linter +description: "[Internal sub-skill of `migrate-orchestrator` (optional, runs only when opted in during phase 1). Do not load directly — load `migrate-orchestrator` first.] Align the project's linter and formatter with the **workspace's** configured tool (whatever it is — ruff, black+isort+flake8, pylint, etc.). Removes project-specific config and consolidates rules into the workspace root." +--- + +# Skill: migrate-convert-linter + +## Goal +Align the project with the **workspace's** linter and formatter. This skill is **not** opinionated about ruff — it reads the workspace's configured tool from the root `pyproject.toml` and aligns the project to that. + +## When to Skip +Skip this step if the project's `LINTER` and `FORMATTER` in `migration//state.md` already match the workspace's tools. + +## Inputs +From `migration//state.md`: +- `PROJECT_DIR` +- `LINTER`, `FORMATTER` +- `RUN_TEST_CMD` (optional: `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`) + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 0. Identify the workspace's tool +Open the **workspace root** `pyproject.toml` and identify the configured linter and formatter using the same detection table as `migrate-discover` (`[tool.ruff]` → ruff, `[tool.black]` → black, `[tool.flake8]` or `.flake8` → flake8, etc.). Record what you found — every step below refers to "the workspace's linter/formatter" and means **this** tool, not necessarily ruff. + +If the workspace has no linter configured at all, stop and ask the user how to proceed (introduce ruff? skip linting alignment? abort the conversion?). + +### 1. Remove Project-Specific Configs and Dependencies +- Remove the following sections from the project's `pyproject.toml`: + - `[tool.black]`, `[tool.isort]`, `[tool.flake8]`, `[tool.pylint.*]`, `[tool.autopep8]`, `[tool.pycodestyle]` +- Remove standalone config files: `.flake8`, `.pylintrc`, `.isort.cfg`, and lint sections in `setup.cfg` or `tox.ini`. +- Remove old linter/formatter dependencies from the project's `pyproject.toml`: + - `flake8`, `flake8-*` plugins, `pylint`, `pylint-*` plugins, `black`, `isort`, `autopep8`, `pyflakes`, `pycodestyle`, `bandit` + +### 2. Assess Workspace Linting Config +- Review the workspace root's linting and formatting configuration. +- Identify any project-specific rules or ignores that differ from the workspace's standards. + +### 3. Merge Project-Specific Rules +- If the project has unique linting rules or ignores, merge them into the workspace root's linting configuration (e.g., `[tool.ruff]`). +- For conflicts (e.g., stricter rules in the project), ask the user to provide guidance on whether to: + - Adopt the project's rules in the workspace. + - Suppress the project's rules in favor of the workspace's. + - Defer the decision and document the conflict in `migration//state.md`. + +### 4. Run Workspace Linting and Formatting Tools +- Run the workspace's linting tool to assess violations: + - **Few new violations**: Fix them now. + - **Many new violations**: Ask the user whether to fix, suppress, or defer. +- Run the workspace's formatting tool to reformat the code. + +### 5. Update `state.md` +- Set `LINTER` and `FORMATTER` to the workspace's tool(s). +- Update `RUN_LINT_CMD` to use the workspace's linting and formatting commands. + +## Verify +- The workspace's linting tool passes (or remaining violations are user-approved). +- The workspace's formatting tool passes. +- `RUN_TEST_CMD` succeeds. +- If set, `RUN_TYPECHECK_CMD` succeeds. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| The workspace's linter surfaces hundreds of violations the project's old linter didn't catch | Stricter ruleset; not a "fix everything now" situation. | Ask the user: fix now, suppress via `[tool.]` ignores in the project subsection, or defer in a follow-up branch. Do **not** auto-fix silently — record the decision in `state.md`. | +| Project-specific lint rules are stricter than the workspace's standard | Project had higher discipline; lowering it would regress quality. | Add the project's rule as a per-path override in the workspace's lint config (most linters support per-directory rule overrides). Do not weaken the rule globally. | +| Old linter config file (`.flake8`, `.pylintrc`, etc.) lingers and confuses developers' local editors | Step 1 missed a config file. | Delete it; mention in the commit message so reviewers update their editor configs. | + +## Done When +- No project-specific linter/formatter config files or dependencies remain. +- Project-specific linting rules are merged into the workspace root's configuration. +- `LINTER` and `FORMATTER` in `migration//state.md` match the workspace's tools. +- Tests pass via the workspace's tooling. + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase optional — convert-linter" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-convert-package-manager/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-convert-package-manager/SKILL.md new file mode 100644 index 00000000..3abe79e2 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-convert-package-manager/SKILL.md @@ -0,0 +1,104 @@ +--- +name: migrate-convert-package-manager +description: "[Internal sub-skill of `migrate-orchestrator` (optional, runs only when opted in during phase 1). Do not load directly — load `migrate-orchestrator` first.] Convert the project's `pyproject.toml` to PEP 621/uv-workspaces format and register it as a uv-workspace member. **Opinionated about uv** — only applies when the workspace itself uses uv. Skip otherwise." +--- + +# Skill: migrate-convert-package-manager + +> ⚠ **Opinionation gate.** This skill is **explicitly opinionated about uv**. It does not generalize to Poetry, PDM, or Hatch workspaces. If your Polylith workspace uses one of those, **do not run this skill** — align the project to the workspace's manager via a manual step instead, and leave `CONVERT_PACKAGE_MANAGER=no` in `state.md`. + +## Goal +Convert the project's `pyproject.toml` to PEP 621/uv format and register it as a uv-workspace member. This makes the project share the workspace's single lock file and virtual environment, preventing version skew. + +## When to Skip +Skip this skill if **any** of the following holds: +- `PACKAGE_MANAGER=uv` already in `migration//state.md` (nothing to convert). +- The workspace root does **not** use uv (this skill does not apply — see the opinionation gate above). +- `CONVERT_PACKAGE_MANAGER=no` in `state.md` (user opted out during `migrate-discover`). + +### Verify the workspace uses uv before proceeding +Open the **workspace root** `pyproject.toml` and look for `[tool.uv.workspace]` and/or a sibling `uv.lock`. If neither is present, **stop**: this skill does not apply. + +## Inputs +From `migration//state.md`: +- `PROJECT_DIR` +- `PACKAGE_MANAGER` +- `RUN_TEST_CMD` (optional: `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`) + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Ask for User Approval +- Ask the user if they want to convert the project's `pyproject.toml` to PEP 621/uv format. +- Record their choice in `state.md`: + ```text + CONVERT_PACKAGE_MANAGER= + ``` + +### 2. Rewrite `pyproject.toml` to PEP 621/uv Format +- Move `[tool.poetry.dependencies]` to `[project] dependencies`. Keep only **runtime** (non-dev, non-test) dependencies in the project, listed **without version constraints**. +- Remove Poetry-specific sections: `[tool.poetry]`, `[tool.poetry.group.*]`, `[[tool.poetry.source]]`, and `[build-system]` with `poetry-core`. +- Add a `[build-system]` with `hatchling` (or the workspace's build backend). +- Preserve `[tool.*]` sections for other tools (e.g., pytest, ruff, mypy). +- Add `[tool.uv]` only if project-level uv configuration is needed. + +### 3. Register as a Workspace Member +- Add the project path to the workspace root `pyproject.toml` under `[tool.uv.workspace] members`. + Example: + ```toml + members = ["projects/example-service-b"] + ``` + +### 4. Consolidate Dependencies +- Add all **third-party runtime dependencies with version constraints** to the workspace root `pyproject.toml` `[project] dependencies`. +- Move all dev/test/tooling dependencies to the workspace root `[dependency-groups]` (e.g., `dev = [...]`, `test = [...]`). +- Ensure the project's `pyproject.toml` lists runtime dependencies **without version numbers**. + +### 5. Lock and Sync +- Run `uv lock` from the workspace root to regenerate `uv.lock` with the new member. +- Run `uv sync` to install all dependencies into the shared `.venv`. +- Resolve any version conflicts that arise during `uv lock`. + +### 6. Delete Old Lock Files +- Remove `poetry.lock`, `Pipfile.lock`, and generated `requirements*.txt` from the project directory. + +### 7. Update Verification Commands +- Replace any `poetry run`, `pipenv run`, or bare commands with `uv run` equivalents in `migration//state.md`. + Example: + ```text + RUN_TEST_CMD=uv run pytest + RUN_LINT_CMD=uv run ruff check + ``` +- Update `PACKAGE_MANAGER=uv` in `state.md`. + +## Verify +- Run the updated `RUN_TEST_CMD` and confirm the same pass/fail counts as before the conversion. +- If set, run `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD`. +- Ensure `uv lock` and `uv sync` succeed from the workspace root. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| Workspace root does not use uv (no `[tool.uv.workspace]`, no `uv.lock`) | This skill does not apply — the opinionation gate at the top of this file rules it out. | Stop. Set `CONVERT_PACKAGE_MANAGER=no` in `state.md`. Align the project to the workspace's actual manager (Poetry/PDM/Hatch) via a manual step instead. | +| `uv lock` fails with "no version satisfies …" after adding the project as a workspace member | Project's old version constraints conflict with the workspace root's pins. | Relax the workspace root's range, or, if the project legitimately needs a different version, pin it explicitly in the project's `pyproject.toml`. As a last resort, exclude the project from the workspace and use a separate environment. | +| Project depends on a private/internal package that the workspace root doesn't know about | Private index or path-dependency not declared at the root. | Add the dependency (and its source — `[tool.uv.sources]` or `[[tool.uv.index]]`) to the workspace root `pyproject.toml`. | + +## Done When +- The project is listed as a workspace member in the root `pyproject.toml`. +- `uv lock` and `uv sync` succeed from the workspace root. +- Old lock files (`poetry.lock`, `Pipfile.lock`, generated `requirements*.txt`) are deleted from the project directory. +- `PACKAGE_MANAGER=uv` is recorded in `migration//state.md`. +- Verification commands in `state.md` use `uv run`. +- Tests pass via `uv run`. + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase optional — convert-package-manager" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-convert-type-checker/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-convert-type-checker/SKILL.md new file mode 100644 index 00000000..2a0dbcac --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-convert-type-checker/SKILL.md @@ -0,0 +1,83 @@ +--- +name: migrate-convert-type-checker +description: "[Internal sub-skill of `migrate-orchestrator` (optional, runs only when opted in during phase 1). Do not load directly — load `migrate-orchestrator` first.] Align the project's type checker with the **workspace's** configured tool (whatever it is — mypy, pyright, ty, etc.). Removes project-specific config and consolidates settings into the workspace root." +--- + +# Skill: migrate-convert-type-checker + +## Goal +Align the project with the **workspace's** type checker. This skill is **not** opinionated about ty — it reads the workspace's configured tool from the root `pyproject.toml` and aligns the project to that. + +## When to Skip +Skip this step if the project's `TYPE_CHECKER` in `migration//state.md` already matches the workspace's type-checking tool. + +## Inputs +From `migration//state.md`: +- `PROJECT_DIR` +- `TYPE_CHECKER` +- `RUN_TEST_CMD` (optional: `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`) + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 0. Identify the workspace's tool +Open the **workspace root** `pyproject.toml` and identify the configured type checker using the same detection table as `migrate-discover` (`[tool.mypy]` or `mypy.ini` → mypy, `[tool.pyright]` or `pyrightconfig.json` → pyright, `[tool.ty]` → ty). Record what you found — every step below refers to "the workspace's type checker" and means **this** tool, not necessarily ty. + +If the workspace has no type checker configured at all, stop and ask the user how to proceed (introduce one? skip type-check alignment? abort the conversion?). + +### 1. Remove Old Configs and Dependencies +- Remove the following sections from `pyproject.toml`: + - `[tool.mypy]`, `[[tool.mypy.overrides]]`, `[mypy-*]`, `[tool.pyright]`, `[tool.pytype]` +- Remove standalone config files: `mypy.ini`, `.mypy.ini`, `pyrightconfig.json`. +- Remove old type checker dependencies from `pyproject.toml`: + - `mypy`, `mypy-extensions`, `types-*` stub packages, `pyright`, `pytype`, `sqlalchemy-stubs`, `django-stubs`. + +### 2. Clean Up Type Ignore Comments +- Remove `# type: ignore[]` comments that reference mypy-specific error codes. +- Leave comments that suppress real issues and document them in `state.md`. + +### 3. Adopt Workspace Type-Checking Config +- The workspace root `pyproject.toml` may define the type-checking configuration. The project inherits this config. +- Add project-specific overrides if needed. + +### 4. Run the Workspace's Type-Checking Tool +- Run the workspace's type-checking tool to assess errors: + - **Same or fewer errors**: No action needed. + - **New errors**: Ask the user whether to fix, suppress, or adjust the config. + - **Missing stub errors**: Silence with per-module ignores in the workspace's type-checking config. + +### 5. Update `state.md` +- Set `TYPE_CHECKER` to the workspace's type-checking tool. +- Update `RUN_TYPECHECK_CMD` to use the workspace's type-checking command. + +## Verify +- The workspace's type-checking tool runs cleanly (or remaining errors are user-approved). +- `RUN_TEST_CMD` succeeds. +- If set, `RUN_LINT_CMD` succeeds. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| The workspace's checker surfaces type errors the old checker didn't | Stricter type rules expose real bugs that were always there. | Fix the bugs now where cheap (small handful). For systemic gaps, suppress per-module in the workspace's checker config and document the debt in `state.md`. Do **not** blanket-ignore at file scope. | +| Stub packages (`types-*`, `django-stubs`, `sqlalchemy-stubs`) were required by the old checker but the new one bundles its own | Dependency carryover. | Remove the stub packages from `pyproject.toml`. Re-run the workspace's checker to confirm it picks up its own stubs. | +| `# type: ignore[]` comments still reference the old checker's error codes | Step 2 missed some. | Strip the `[]` bracket. If the suppression is still needed, leave a bare `# type: ignore` **with a comment explaining why**; otherwise remove the suppression entirely. | + +## Done When +- No old type checker config files remain. +- Old type checker dependencies are removed from `pyproject.toml`. +- Stale `# type: ignore` comments referencing tool-specific codes are removed (or documented if intentionally kept). +- `TYPE_CHECKER` in `migration//state.md` matches the workspace's type-checking tool. +- The workspace's type-checking tool runs cleanly or known issues are documented in `state.md`. +- Tests pass via the workspace's tooling. + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase optional — convert-type-checker" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-dedupe/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-dedupe/SKILL.md new file mode 100644 index 00000000..7ff83aa8 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-dedupe/SKILL.md @@ -0,0 +1,118 @@ +--- +name: migrate-dedupe +description: "[Internal sub-skill of `migrate-orchestrator` (optional, runs only when opted in during phase 1). Do not load directly — load `migrate-orchestrator` first.] Identify and execute controlled deduplication of code during migration (if the user opts in)." +--- + +# Skill: migrate-dedupe + +> 📐 **Scope vs sibling skills.** This skill is **opportunistic deduplication** that may be triggered any time during refactoring when duplication candidates surface. It is **not** the canonical place for the structural decompositions: +> - For "split this big component into smaller ones", use `migrate-split-big-component` (it already includes a dedup-analysis subsection — usually sufficient on a first migration). +> - For "this component's `core.py` mixes domains", use `migrate-split-component-internals`. +> - For "two projects have overlapping code, split shared from project-specific", use `migrate-isolate-shared-and-project-logic`. +> +> Use `migrate-dedupe` when none of the above fits cleanly — e.g., duplication discovered across already-extracted components that don't map to a structural split. + +## Goal +Identify duplication candidates during the migration process and execute controlled deduplication for user-approved candidates. + +## When to Use +- After splitting the big component or extracting standalone modules. +- When potential duplication between components is suspected. + +## Classification + +Use this table when deciding whether a candidate is a real duplicate: + +| Class | Definition | Action | +|-------|------------|--------| +| **Identical** | Same logic, same control flow, only trivial differences (variable names, formatting, ordering of independent statements). | Extract into a shared component. Both call sites import from it. | +| **Similar** | Same purpose, slightly different behaviour (e.g., different default arguments, project-specific fields on an otherwise shared model). | Extract a **parameterized** shared component. Project-specific behaviour passes in as arguments or subclass hooks. Avoid forcing a one-size-fits-all signature. | +| **Coincidental** | Looks similar (same function name, same shape) but serves unrelated purposes. | Leave alone. Sharing here would couple two domains that should evolve independently. | + +## When to parameterize vs. keep separate + +- **Parameterize** when the core logic is identical and only data/config differs. +- **Keep separate** when control flow or structure diverges (different frameworks, different patterns) — forcing a shared abstraction here creates a brittle "shared core" that grows project-specific flags over time. +- **Extract shared base + per-project wrappers** when there's a significant shared core but non-trivial project-specific logic around it. + +### Worked example — logging + +Two projects each had their own `init_logging`. The core (structlog setup, base log levels, JSON formatter) was identical; the differences were: + +- Project A added loggers for `httpx`, `backoff`. +- Project B added a logger for `confluent_kafka_helpers`. + +The shared component exposed an `init(config, *, extra_loggers=None, cache_logger_on_first_use=False)` function. Each project's base calls `init` with its own `extra_loggers` dict. No coincidental coupling, no version skew, and adding a third project requires only its own dict — not a change to the shared component. + +## Shared-component naming + +When creating a shared component to deduplicate code, name it after the **domain or capability** it represents, never after how it's used. Good: `logging`, `kafka_client`, `merchant_serializer`. Bad: `shared_utils`, `common`, `helpers`, `misc`. Generic-named bricks attract more code over time and become the next thing that needs decomposing. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- Verification commands (`RUN_TEST_CMD`, `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`). + +From `migration//manifest.md`: +- Module map of components. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Identify Duplication Candidates +- Use `directory_tree` and `grep` to scan for overlapping logic between components. +- Classify candidates by type: + - **Identical**: Code that is exactly the same. + - **Similar**: Code that serves the same purpose but with minor differences. + - **Coincidental**: Code that looks similar but serves unrelated purposes. + +### 2. Present Candidates to the User +- Provide a list of duplication candidates, including: + - Component names. + - File paths. + - Type of duplication (identical, similar, coincidental). + - Risk assessment (low, medium, high). +- Ask the user to approve or reject each candidate for deduplication. + +### 3. Execute Deduplication for Approved Candidates +- For each approved candidate: + - **Identical Code**: Extract the shared logic into a new component and update imports. + - **Similar Code**: Refactor to use shared logic or parameterize differences. + - **Coincidental Code**: Leave as-is. +- Update `pyproject.toml` to include any new components. +- Run `POLY_CMD_PREFIX sync` to synchronize the workspace. + +### 4. Verify Changes +- Run `RUN_TEST_CMD` to ensure no regressions. +- Run `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` if set. +- Run `POLY_CMD_PREFIX check` to validate the workspace structure. + +## Verify +- All tests pass (`RUN_TEST_CMD`). +- Linting and type-checking pass (if set). +- The workspace structure is valid (`POLY_CMD_PREFIX check`). + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| Two pieces of code look identical but operate on different domains (e.g., both are `validate(...)` but one is for users, the other for transactions) | Coincidental similarity, not real duplication. | Classify as "coincidental"; leave both in place. Resist the urge to share. | +| The candidate shared component would pull in framework-specific dependencies (e.g., a "logging" shared brick that needs both `confluent_kafka_helpers` and `httpx`) | Wrong shared abstraction — you're sharing the union of two project surfaces. | Revert and parameterize instead: keep the shared core minimal and pass project-specific values as arguments. See the "Pattern: Parameterize the shared component" guidance in `migrate-split-big-component`. | +| Tests break after deduplication because `mock.patch("")` no longer hits anything | Patch strings reference the pre-dedup module path. | Update patch strings to the new shared module path. Validate by deliberately breaking the patched function and confirming the test fails. | + +## Done When +- Duplication candidates are identified and presented to the user. +- User-approved candidates are deduplicated. +- All tests and checks pass. +- The workspace structure is valid. + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase optional — dedupe" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-definition-of-done/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-definition-of-done/SKILL.md new file mode 100644 index 00000000..6991340b --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-definition-of-done/SKILL.md @@ -0,0 +1,107 @@ +--- +name: migrate-definition-of-done +description: "[Internal sub-skill of `migrate-orchestrator` (phase 11 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Define the criteria for completing the migration process." +--- + +# Skill: migrate-definition-of-done + +## Done When + +### Structure +- Temporary migration base is gone or thin. +- Bases contain only entrypoints/wiring. +- All non-entrypoint code lives in components. + +### Source Project +- `projects//` contains only: + - Packaging config (`pyproject.toml`). + - Runner scripts and task runners (`Makefile`, `Justfile`). + - Project-specific config (e.g., `alembic.ini`). +- Project `pyproject.toml` references all required bricks. +- Brick names are meaningful and non-generic. +- Base names are project-prefixed to avoid collisions. + +### Tests +- Tests are moved from `projects//tests/` to workspace level. +- Unit tests are organized according to the Polylith theme in use: + - **`loose` theme:** unit tests live under `test/bases///` and `test/components///`. + - **`tdd` theme:** unit tests live under `bases//test///` and `components//test///`. +- Integration tests live in a shared location (e.g., `test/integration/`). +- Shared fixtures live in `test//conftest.py` or `test/conftest.py`. +- `RUN_TEST_CMD` points to the test root **and collects the same number of tests as the pre-migration baseline**. + +### Infrastructure +- Infrastructure folders are moved to `infra///`. + +### Interfaces +- Each component defines its public API via `__init__.py`. +- Bricks import each other via those APIs. + +### Linting and Type-Checking +- Linting and formatting use the workspace's configured tool(s). +- Type-checking uses the workspace's configured tool(s) (if applicable). +- `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` pass. + +### Dependencies +- Workspace root `pyproject.toml` contains all third-party dependencies with version constraints. +- Project `pyproject.toml` lists runtime dependencies without version numbers. + +### Cleanup +- Migration artifacts (`migration//state.md`, `migration//manifest.md`, and any `migration/shims.md`) are either removed or kept under `migration//` for reference (user's choice). +- The migration branch (`GIT_BRANCH` from `state.md`) is ready to be merged or rebased into the main branch. Per-phase commits remain available for review/bisect. + +## End-to-end checks + +These checks go beyond per-phase verification and exercise the migrated project as a whole. **All three must pass.** + +### 1. Entrypoint smoke test + +For **each base** in the project, smoke-test its entrypoint: + +- **HTTP API base** → start the server and curl a health endpoint (or any GET that doesn't require auth). Verify it returns 200. +- **CLI base** → run ` --help` and a no-op subcommand. Verify exit code 0. +- **Worker/consumer base** → start the process and observe one cycle of its main loop in the logs. Stop it cleanly. +- **Lambda / Cloud Function base** → invoke the handler with a representative event payload (mocked or recorded). Verify it returns without exception. + +If any base fails to start, the migration is **not done** — return to `migrate-distribute-wiring` and check entrypoint wiring. + +### 2. Baseline test count restored + +Compare collected test count to the baseline recorded by `migrate-discover`: + +```bash + +``` + +The count must **equal** the baseline. A lower count means `pytest` discovery is misconfigured (see `migrate-refactor-tests` failure modes). A higher count means tests were inadvertently duplicated during the move. + +### 3. No undocumented shims remain + +Inspect `migration//shims.md` (if it exists): + +- **Empty or absent** → ✅ proceed. +- **Lists active shims** → either remove them now (rewrite imports to the new namespace and delete the shim modules) or schedule their removal as a follow-up PR and reference that PR in the migration branch description. Do **not** merge with undocumented shims. + +## 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 info` to inspect the workspace and confirm all projects and bricks are correctly registered. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| `RUN_TEST_CMD` passes but collected test count is lower than the baseline from `migrate-discover` | `pytest` discovery is misconfigured after the test reorganisation. | Update `[tool.pytest.ini_options].testpaths` (or pass paths explicitly in `RUN_TEST_CMD`). Run `pytest --collect-only` and diff against baseline collection. See `migrate-refactor-tests` for details. | +| `poly check` is green and tests pass, but the application doesn't start | Entrypoint wiring regression — a base imports something that no longer exists at the expected path, but no test exercises the boot path. | Smoke-test each base's entrypoint manually (run the FastAPI server, invoke the CLI, send a test event to the consumer). Revisit `migrate-distribute-wiring`. | +| `migration/shims.md` still lists active shims | Shims from `migrate-extract-to-base` (namespace change) were never removed. | Either rewrite imports to the new namespace and delete the shims, or schedule shim removal as a follow-up PR and note it in the migration branch description. Do not merge with undocumented shims. | + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase 11 — definition-of-done" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-discover/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-discover/SKILL.md new file mode 100644 index 00000000..a64c627b --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-discover/SKILL.md @@ -0,0 +1,226 @@ +--- +name: migrate-discover +description: "[Internal sub-skill of `migrate-orchestrator` (phase 1 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Create `migration//state.md` and `migration//manifest.md` by inspecting the existing project under `projects//`." +--- + +# Skill: migrate-discover + +## Goal +Inspect the project and create two artifacts that drive every subsequent migration phase: +- `migration//state.md` — a flat `KEY=value` file (see schema below). +- `migration//manifest.md` — a human-readable structural inventory. + +> 💡 `` is the project subfolder name (e.g., `api` for `projects/api/`). Use that exact string everywhere — paths, filenames, branch names. + +## Canonical `state.md` schema + +All later skills read `state.md` as a flat `KEY=value` file. Use **exactly** this format — no markdown tables, no fenced TOML, no inline comments. One key per line. + +```ini +# migration//state.md +PROJECT_DIR=projects/ +ORIG_TOP_NS= +TARGET_TOP_NS= +INITIAL_BASE_NAME= +ALIAS= +GROUP= + +PACKAGE_MANAGER= +LINTER= +FORMATTER= +TYPE_CHECKER= +POLY_CMD_PREFIX= + +CONVERT_LINTER= +CONVERT_TYPE_CHECKER= +CONVERT_PACKAGE_MANAGER= + +RUN_TEST_CMD= +RUN_LINT_CMD= +RUN_TYPECHECK_CMD= + +GIT_BRANCH= +GIT_BASE_SHA= +``` + +### Field reference + +| Key | Description | Source | +|-----|-------------|--------| +| `PROJECT_DIR` | Project subfolder path. | The orchestrator's ``. | +| `ORIG_TOP_NS` | Current top-level Python package name. | First non-`tests` directory under `projects//src/` or `projects//`. | +| `TARGET_TOP_NS` | Desired Polylith namespace. | `workspace.toml` `[tool.polylith].namespace`, or `ORIG_TOP_NS` if no workspace exists yet. | +| `INITIAL_BASE_NAME` | Name of the **single temporary base** used to hold all code during early phases. Becomes the **default base name** in `migrate-isolate-base-and-big-component`. Final migrations usually contain *several* bases — this is just the starting one. | Derived from `[project.name]`, confirmed by user. | +| `ALIAS` | Short alias shown in `poly info` / `poly deps` tables. Optional. | Derived, confirmed by user. | +| `GROUP` | Polylith project group. Optional. | Asked from user. | +| `PACKAGE_MANAGER` / `LINTER` / `FORMATTER` / `TYPE_CHECKER` | Detected tooling. | Detection table below. | +| `POLY_CMD_PREFIX` | Prefix every `poly …` command in later skills uses. | Derived from `PACKAGE_MANAGER`. | +| `CONVERT_*` | Whether the user opted in to a tooling conversion. | Asked from user. | +| `RUN_TEST_CMD` etc. | The exact shell commands the migration verifies against after each phase. | Derived from project config, confirmed by user if ambiguous. | +| `GIT_BRANCH` / `GIT_BASE_SHA` | Set by the orchestrator's Phase 0; recorded here so later skills know where to roll back to. | Orchestrator. | + +### Deriving `INITIAL_BASE_NAME` and `ALIAS` from `[project.name]` + +| `[project.name]` | `INITIAL_BASE_NAME` (snake) | `ALIAS` (kebab) | +|-------------------------|-----------------------------|-----------------| +| `example-service-a` | `example_a` | `svc-a` | +| `order-management-api` | `order_management` | `order-mgmt` | +| `payment-worker` | `payment` | `payment` | + +### Validation rules + +When any later phase loads `state.md`, validate before proceeding: + +1. **File exists** at `migration//state.md`. +2. **Format**: every line is one of: blank, `# comment`, or `KEY=value`. No markdown tables, no fenced TOML, no inline comments after a value. +3. **Schema coverage**: every key from the schema above is present. A value may be empty (for optional keys), but the key line must exist. +4. **Enumerations**: `PACKAGE_MANAGER`, `LINTER`, `FORMATTER`, `TYPE_CHECKER`, and the three `CONVERT_*` flags use only the documented values. +5. **Required non-empty**: `PROJECT_DIR`, `ORIG_TOP_NS`, `TARGET_TOP_NS`, `INITIAL_BASE_NAME`, `PACKAGE_MANAGER`, `POLY_CMD_PREFIX`, `RUN_TEST_CMD`, `GIT_BRANCH`, `GIT_BASE_SHA` must all be non-empty. +6. **Consistency**: `POLY_CMD_PREFIX` matches `PACKAGE_MANAGER` per the mapping table. + +If validation fails, abort the phase, surface the offending line(s) to the user, and ask them to fix `state.md` before retrying. Never silently coerce values. + +## Steps + +> Run these in order. Every step writes to `state.md` or `manifest.md`. Do not skip the confirmation gates (steps 4 and 7). + +### 1. Record project metadata +Read `projects//pyproject.toml` (or `setup.cfg`/`setup.py`) and fill in `PROJECT_DIR`, `ORIG_TOP_NS`, `TARGET_TOP_NS`, and the **derived** `INITIAL_BASE_NAME`, `ALIAS` per the tables above. + +### 2. Detect tooling +Scan project config files and fill in `PACKAGE_MANAGER`, `LINTER`, `FORMATTER`, `TYPE_CHECKER`: + +| Tool | Detection criteria | +|------|--------------------| +| **Package Manager** | | +| Poetry | `poetry.lock` or `[tool.poetry]` in `pyproject.toml` | +| Pipenv | `Pipfile` or `Pipfile.lock` | +| Pip | `requirements.txt` (no lock file) | +| UV | `uv.lock` or `[tool.uv]` in `pyproject.toml` | +| Setuptools | `setup.py` or `setup.cfg` only | +| **Linter** | | +| Flake8 | `setup.cfg`, `tox.ini` `[flake8]` section, or `.flake8` | +| Pylint | `[tool.pylint]` or `.pylintrc` | +| Ruff | `[tool.ruff]` | +| **Formatter** | | +| Black | `[tool.black]` | +| Isort | `[tool.isort]` | +| Ruff | `[tool.ruff.format]` | +| **Type Checker** | | +| Mypy | `mypy.ini`, `.mypy.ini`, or `[tool.mypy]` | +| Pyright | `[tool.pyright]` or `pyrightconfig.json` | +| Ty | `[tool.ty]` | + +### 3. Derive `POLY_CMD_PREFIX` +Map `PACKAGE_MANAGER` to the command prefix: + +| `PACKAGE_MANAGER` | `POLY_CMD_PREFIX` | +|-------------------|-------------------| +| `poetry` | `poetry poly` | +| `pipenv` | `pipenv run poly` | +| `pdm` | `pdm run poly` | +| `hatch` | `hatch run poly` | +| `uv` | `uv run poly` | +| `pip` / `setuptools` / activated venv | `poly` | + +### 4. Discover verification commands +Inspect `Makefile`, `Justfile`, `tox.ini`, `pyproject.toml` `[tool.pytest.ini_options]`, and CI config (`.github/workflows/*.yml`, `.circleci/config.yml`, etc.) to identify the project's existing commands. Fill `RUN_TEST_CMD`, and `RUN_LINT_CMD` / `RUN_TYPECHECK_CMD` when present. If a command can't be found, leave the value empty. + +### 5. Determine tooling-conversion eligibility +Read the **workspace root** `pyproject.toml` to determine the workspace's standard linter, formatter, type checker, and package manager. + +- If the project's `LINTER`/`FORMATTER` already matches the workspace's → set `CONVERT_LINTER=no` (skip). +- If the project's `TYPE_CHECKER` already matches the workspace's → set `CONVERT_TYPE_CHECKER=no` (skip). +- If the project's `PACKAGE_MANAGER` is already `uv` **and** the workspace uses uv → set `CONVERT_PACKAGE_MANAGER=no` (skip). +- If the workspace does **not** use uv, `migrate-convert-package-manager` does not apply at all — set `CONVERT_PACKAGE_MANAGER=no` and skip the question below. + +### 6. Create `manifest.md` + +Write `migration//manifest.md` using **exactly** this template — fixed headings, fixed shapes. Later phases parse this file by heading. + +`````markdown +# migration//manifest.md + +## Directory tree + +/`, fenced as a code block> + +## Module map + +| Path | Role | +|------|------| +| `` | | + +## Entrypoints + +- ``: + +## Tests + +- Root: `` (relative to project) +- File count: +- Fixture files: ``, ``, … + +## Infrastructure + +- ``: +````` + +> ⚠ Keep the five `##` headings literal. Downstream phases reference them by name; renaming a heading silently breaks input discovery. + +### 7. Present derived values, then confirm with the user +**Do not proceed past this step without explicit user confirmation.** Present the derived state in one block: + +``` +Derived from projects//: + INITIAL_BASE_NAME = ← initial base name; you will likely add more bases later + ALIAS = ← short alias for poly info/deps tables (optional) + GROUP = ← project group (optional) + +Detected tooling: + PACKAGE_MANAGER = + LINTER = + FORMATTER = + TYPE_CHECKER = + +Verification commands: + RUN_TEST_CMD = + RUN_LINT_CMD = + RUN_TYPECHECK_CMD = + +Optional conversions you can opt into: + - Convert linter/formatter to match workspace standard? (default: no) + - Convert type checker to match workspace standard? (default: no) + - Convert package manager to uv (workspace-uv only)? (default: no) + +Confirm the values above, or correct any of them. +``` + +Wait for the user's response. Update `state.md` with corrections and the `CONVERT_*` answers. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| Derived `INITIAL_BASE_NAME` collides with an existing brick under `bases//` or `components//` | Two projects derive the same base name from a generic `[project.name]`. | Append a project-specific suffix (e.g., `payment_api` instead of `payment`) and re-confirm with the user. Check before writing `state.md`. | +| Project has no detectable test command | No `pytest` / `make test` / CI config that reveals a runnable test command. | Ask the user explicitly. If none exists, set `RUN_TEST_CMD=` empty and record that **every later phase loses its primary safety check** — flag the heightened risk and require manual smoke-testing at the entrypoint level. | +| Multiple linters or formatters are configured simultaneously (e.g., black + ruff format both active) | Project history accumulated tools without a cleanup. | Record both in `state.md` (comma-separated values are acceptable in this one case), and flag for resolution during `migrate-convert-linter`. Do **not** silently pick one. | + +## Done When +The following artifacts and conditions all hold: + +- [ ] `migration//state.md` exists and contains **every** key in the schema (empty values where N/A, but no missing keys). +- [ ] `migration//manifest.md` exists with all five sections (directory tree, module map, entrypoints, tests, infrastructure). +- [ ] The user has explicitly confirmed `INITIAL_BASE_NAME`, `ALIAS`, `GROUP`, and the three `CONVERT_*` flags. +- [ ] `RUN_TEST_CMD` is set and **runs successfully on the project's current code** (the migration's baseline pass-rate). +- [ ] `GIT_BRANCH` and `GIT_BASE_SHA` are populated (from orchestrator Phase 0). + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase 1 — discover" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-distribute-wiring/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-distribute-wiring/SKILL.md new file mode 100644 index 00000000..25ead820 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-distribute-wiring/SKILL.md @@ -0,0 +1,79 @@ +--- +name: migrate-distribute-wiring +description: "[Internal sub-skill of `migrate-orchestrator` (phase 8 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Distribute app-wiring code from the residual component into the appropriate bases and shared components." +--- + +# Skill: migrate-distribute-wiring + +## Goal +Distribute app-wiring code from the residual component into the appropriate bases and shared components. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `INITIAL_BASE_NAME` +- Verification commands. +- Base names. + +From `migration//manifest.md`: +- Current module map, including what remains in the residual component. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Read the Residual Component +- List every public function remaining in the residual module. +- Confirm it contains only app-wiring code. + +### 2. Identify the Split +- Trace callers of each function (use `grep`). +- Group callers by base or runner script. +- Map each function to its natural base: + +| Function Pattern | Belongs In | Rationale | +|------------------|------------|-----------| +| `init_consumer`, `close_consumer` | Handler/consumer base | Kafka consumer lifecycle is handler-specific | +| `init_job` | Jobs base | Job bootstrap is jobs-specific | +| `init_api` / app factory | API base | HTTP server setup is API-specific | + +### 3. Extract Shared Helpers +- Identify shared init functions (e.g., `init_logging`, `init_db`). +- Move shared helpers to a `bootstrap` component if needed. + +### 4. Move Composite Functions +- Move composite functions to their respective bases. +- Update runner scripts to import from the base directly. + +### 5. Update Tests +- Update integration tests to monkeypatch the correct module. + +### 6. Clean Up +- Delete the residual component directory. +- Update `pyproject.toml` to remove the residual brick. + +## 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 | +|---------|--------------|-------------| +| Two bases now import the same `init_` function and the residual still exists | Step 6 wasn't reached — the function moved into one base but the residual reference wasn't deleted. | Confirm the residual is gone; if `init_` is genuinely shared by 2+ bases, it belongs in a shared `bootstrap` component (step 3), not in either base. | +| `mock.patch("..init_api")` fails after the move | The test references the old residual path; the function now lives in a base. | Update mock patch strings to the new location, typically `..`. | +| `poly check` says the residual brick is still declared but its files are gone | `pyproject.toml` `[tool.polylith.bricks]` still lists the deleted brick. | Remove the line from `[tool.polylith.bricks]` and re-run `POLY_CMD_PREFIX sync --quiet`. | +| Application starts but a runtime feature is missing (e.g., logging, DB) | A shared init helper was moved into one base only. The other base never calls it. | Promote the helper to a shared `bootstrap` component and call it from every base's startup path. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 8 — distribute-wiring" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-extract-standalone-modules/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-extract-standalone-modules/SKILL.md new file mode 100644 index 00000000..bbb39bad --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-extract-standalone-modules/SKILL.md @@ -0,0 +1,65 @@ +--- +name: migrate-extract-standalone-modules +description: "[Internal sub-skill of `migrate-orchestrator` (phase 6 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Extract foundational modules (e.g., `consts.py`, `exceptions.py`, or similar) from the residual component into standalone components." +--- + +# Skill: migrate-extract-standalone-modules + +## Goal +Extract **foundational modules** (e.g., `consts.py`, `exceptions.py`, `models.py`) from the residual component into standalone components. This skill is for zero-dependency or low-dependency modules that serve as building blocks for other components. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `INITIAL_BASE_NAME` +- Verification commands. + +From `migration//manifest.md`: +- Current module map, including what remains in the residual component. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Analyze the Residual Component +- Use `directory_tree` and `grep` to list modules remaining in the residual component. +- Classify each module: + - **Zero internal deps**: Modules with only stdlib/third-party imports (e.g., `exceptions.py`, `consts.py`). + - **Low internal deps**: Modules that depend on already extracted or zero-dep modules (e.g., `models.py`). + - **App-wiring**: Modules that compose infrastructure setup. These stay in the residual. + +### 2. Extract Modules in Dependency Order +- Extract zero-dep modules first, followed by modules that depend on them. + +### 3. For Each Extraction +1. Check for naming collisions. +2. Create the component directory with `__init__.py` and `core.py`. +3. Update imports in all consumers. +4. Add the new brick to `pyproject.toml`. +5. Run verification. +6. Delete the original module from the residual component. + +## 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 | +|---------|--------------|-------------| +| Module classified as zero-dep actually pulls in a transitive runtime dependency (e.g., reads an env var via the residual's config module) | The classification missed an indirect import. | Reclassify as low-dep, extract the config module first, then retry. | +| Two consumers now import the same constant via different paths (e.g., `from .consts` and `from ..consts`) | The original module wasn't deleted from the residual after extraction. | Delete the original from the residual (step 3.6), pick one canonical import path, and update every caller. Verify with `grep -r '..consts'`. | +| Extracting `exceptions.py` causes `except` clauses elsewhere to stop catching what they used to | Exception classes are identity-based: two definitions are two different classes. | Ensure the new standalone module is the **only** definition. Delete the residual copy and update every `raise`/`except` site. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 6 — extract-standalone-modules" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-extract-to-base/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-extract-to-base/SKILL.md new file mode 100644 index 00000000..1e2a1acf --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-extract-to-base/SKILL.md @@ -0,0 +1,84 @@ +--- +name: migrate-extract-to-base +description: "[Internal sub-skill of `migrate-orchestrator` (phase 2 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Extract all application code from `projects//` into a temporary migration base." +--- + +# Skill: migrate-extract-to-base + +## Goal +Extract all application code from `projects//` into a temporary migration base (`bases///`). + +## Inputs +From `migration//state.md`: +- `PROJECT_DIR` +- `ORIG_TOP_NS` +- `TARGET_TOP_NS` (default: `ORIG_TOP_NS`) +- `INITIAL_BASE_NAME` +- `RUN_TEST_CMD` (optional: `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`) + +From `migration//manifest.md`: +- Directory tree and module map. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Create the Base Directory +- Create `bases///`. + +### 2. Move Application Code +- Move application packages/modules from `projects//` to the base: + - For `src/` layout: Move `projects//src//` under the base. + - For flat layout: Move `projects///` under the base. +- Leave non-code files (Dockerfiles, k8s manifests, deploy scripts, `pyproject.toml`) in `projects//`. + +### 3. Update `pyproject.toml` +- Add the base to `[tool.polylith.bricks]`: + ```toml + [tool.polylith.bricks] + "../../bases//" = "/" + ``` + +### 4. Fix Imports +- Update imports minimally to ensure tests and linting pass. + +### 5. Update `manifest.md` +- Reflect the new structure in `migration//manifest.md`. + +### 6. Handle Namespace Changes +If `TARGET_TOP_NS != ORIG_TOP_NS`, choose one of the following options: + +| Option | Description | Risk | +|--------|-------------|------| +| **Compatibility Shim** | Keep `ORIG_TOP_NS` as a shim that re-exports from `TARGET_TOP_NS`. | Lower | +| **Rewrite Imports** | Rewrite all imports to the new namespace in one go. | Higher | + +### 7. Use Shims if Needed +- If imports break, add temporary shims to re-export names from the new brick API. +- Track shims in `migration/shims.md`. + +## Verify +- `RUN_TEST_CMD` succeeds with the same pass/fail counts as the pre-migration baseline recorded in `migrate-discover`. +- 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 | +|---------|--------------|-------------| +| `ModuleNotFoundError: No module named '.'` after move | `TARGET_TOP_NS != ORIG_TOP_NS` and imports weren't rewritten or shimmed. | Either rewrite all imports (step 6 option B) or add a shim under `ORIG_TOP_NS` that re-exports from `TARGET_TOP_NS` (step 6 option A). Record the choice in `migration/shims.md`. | +| `RUN_TEST_CMD` collects 0 tests after the move | Test files moved but `pytest` rootdir / `testpaths` still points at the old `projects//tests` location. | Update `pyproject.toml` `[tool.pytest.ini_options].testpaths` or pass explicit dirs in `RUN_TEST_CMD`. (Note: `migrate-prepare-project` moves tests properly — if extract-to-base touched tests at all, consider undoing that part.) | +| `poly check` reports "brick imports another brick that is not in `[tool.polylith.bricks]`" | Step 3 only added the base; imports inside the base may pull in components not yet listed. | Run `POLY_CMD_PREFIX sync --quiet` to populate the rest, then re-run `check`. | +| Editable install / build fails (`error: package directory '' does not exist`) | The base move broke the previous `[tool.setuptools]` or `[tool.hatch.build]` `packages` setting in `projects//pyproject.toml`. | Update `packages` to point at the new `` namespace, or remove the explicit `packages` setting and let Polylith's build hook handle it. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 2 — extract-to-base" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-isolate-base-and-big-component/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-isolate-base-and-big-component/SKILL.md new file mode 100644 index 00000000..033b4a27 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-isolate-base-and-big-component/SKILL.md @@ -0,0 +1,85 @@ +--- +name: migrate-isolate-base-and-big-component +description: "[Internal sub-skill of `migrate-orchestrator` (phase 4 of 11). Do not load directly — load `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." +--- + +# Skill: 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. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `INITIAL_BASE_NAME` +- Verification commands. + +From `migration//manifest.md`: +- Entrypoints list. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Create the Big Component +- Create `components///`. + +### 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///__init__.py`. + +### 4. Update Bases +- Update bases to import only from component APIs: + ```python + from . import ... + ``` + +### 5. Update `pyproject.toml` +- Add the new component to `[tool.polylith.bricks]`: + ```toml + [tool.polylith.bricks] + "../../bases//" = "/" + "../../components//" = "/" + ``` + +### 6. Update `manifest.md` +- Reflect the new structure in `migration//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. | +| `ImportError: cannot import name '' from '.'` | The big component's `__init__.py` doesn't re-export ``. Code that previously reached into submodules now needs the public API. | Add `from .. import ` 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 ...`) so the import graph doesn't reach the component. | Update base imports to `from . 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///`. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 4 — isolate-base-and-big-component" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-isolate-shared-and-project-logic/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-isolate-shared-and-project-logic/SKILL.md new file mode 100644 index 00000000..728e17d2 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-isolate-shared-and-project-logic/SKILL.md @@ -0,0 +1,93 @@ +--- +name: migrate-isolate-shared-and-project-logic +description: "[Internal sub-skill of `migrate-orchestrator` (phase 7 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Identify and isolate shared and project-specific logic in monolithic components (e.g., `models`, `schemas`, or similar)." +--- + +# Skill: migrate-isolate-shared-and-project-logic + +> 📐 **Scope vs sibling skills.** This skill is **cross-project**: it separates code used by **two or more projects** (shared) from code used by **a single project** (project-specific), potentially creating new shared *and* project-specific components. Don't confuse with: +> - `migrate-split-big-component` — within one project; splits one component into multiple components. +> - `migrate-split-component-internals` — within one component; splits one `core.py` into multiple files. +> - `migrate-dedupe` — opportunistic deduplication, broader than just shared-vs-project-specific. +> +> 💡 **When to skip this skill.** On the **first** project migration there is usually no second project to compare against. Skip this skill until at least one prior project has been migrated. The orchestrator still calls it after `migrate-extract-standalone-modules` — that's the right place when overlap exists. + +## Goal +Identify and isolate shared and project-specific logic in monolithic components (e.g., `models`, `schemas`). Extract shared logic into reusable components and isolate project-specific logic into project-specific components. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `INITIAL_BASE_NAME` +- Verification commands (`RUN_TEST_CMD`, `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD`). + +From `migration//manifest.md`: +- Module map of components. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Identify Monolithic Components +- Scan the workspace for monolithic components (e.g., `models`, `schemas`). +- Focus on components with large `core.py` files or mixed domain logic. + +### 2. Analyze Usage +- Use `grep` to trace imports of each definition in the component. +- Classify definitions as: + - **Shared**: Used by multiple projects. + - **Project-Specific**: Used by only one or a few projects. + - **Similar**: Definitions that could reuse shared logic (e.g., similar models). + +### 3. Extract Shared Logic +- Create a new shared component (e.g., `models_shared`, `schemas_shared`). +- Move shared definitions into the new component. +- Update imports in all projects to reference the shared component. + +### 4. Isolate Project-Specific Logic +- Create project-specific components (e.g., `models_project_a`, `schemas_project_b`). +- Move project-specific definitions into the appropriate component. +- Update imports in the relevant projects. + +### 5. Refactor Similar Models +- For similar models, extract shared logic into the shared component. +- Update the project-specific models to reuse the shared logic. + +### 6. Update `pyproject.toml` +- Add the new shared and project-specific components to the workspace's `pyproject.toml`. +- Update the project's `pyproject.toml` to reference the new components. + +### 7. Verify Changes +- Run `RUN_TEST_CMD` to ensure no regressions. +- Run `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` if set. +- Run `POLY_CMD_PREFIX check` to validate the workspace structure. + +## Verify +- All tests pass (`RUN_TEST_CMD`). +- Linting and type-checking pass (if set). +- The workspace structure is valid (`POLY_CMD_PREFIX check`). + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| Only one project exists in the workspace, so there's nothing to compare against | Running this skill on the **first** project migration. | Skip the skill entirely. Record `migrate-isolate-shared-and-project-logic: skipped (single-project workspace)` in `migration//state.md` and proceed to `migrate-distribute-wiring`. Revisit when a second project is migrated. | +| A definition looks shared but is actually used by one project via two different bases inside that project | Bases within one project both use it — still single-project usage. | Leave it project-specific. Cross-project sharing requires consumers in **different** projects under `projects/`. | +| Two projects each have a "same" class with subtle field differences (extra fields, different defaults) | Silent merging would change behaviour. | Do **not** merge silently. Either create a shared base class + project-specific subclasses, or keep the implementations separate and accept the duplication. Confirm with the user. | + +## Done When +- Shared logic is extracted into reusable components. +- Project-specific logic is isolated into project-specific components. +- Similar models reuse shared logic where possible. +- All tests and checks pass. +- The workspace structure is valid. + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase 7 — isolate-shared-and-project-logic" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/.agents/skills/polylith/migrate-project/migrate-orchestrator/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-orchestrator/SKILL.md new file mode 100644 index 00000000..d7cc4e2d --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-orchestrator/SKILL.md @@ -0,0 +1,114 @@ +--- +name: migrate-orchestrator +description: "[ENTRY POINT] Load this skill first when the user asks to migrate a non-Polylith Python project to Polylith (e.g. \"migrate `projects/` to Polylith\"). Drives all 11 migration phases plus optional tooling conversions; do not load any other `migrate-*` skill directly — they are sub-skills this orchestrator invokes." +--- + +# Skill: migrate-orchestrator + +> 🧭 **You are in the right place.** This is the **entry point** for migrating a non-Polylith Python project into a Polylith workspace. If you arrived here from a fuzzy match on a sub-skill name (e.g., `migrate-discover`, `migrate-extract-to-base`), **stay here** — those sub-skills depend on state and a git safety net that only this orchestrator sets up. Loading them in isolation is undefined behaviour. Execute the phases below in order. + +## Goal +Define and execute the workflow for migrating a non-Polylith Python project to a Polylith workspace. +**This skill must be explicitly invoked by a human with the project name/path.** + +## Usage +To migrate a project, load the `migrate-orchestrator` skill and provide the project name (the subdirectory under `projects/`): + +``` +Load the `migrate-orchestrator` skill and migrate `projects/`. +``` + +> 💡 **How sub-skills are loaded.** Each phase points to another skill named `migrate-` (e.g., `migrate-discover`, `migrate-extract-to-base`). Load each via your skill loader before executing the phase. Do not interleave phases — finish and verify one before starting the next. + +## Pre-flight + +### 0. User Confirmation +Ask the user to confirm the project path and migration intent before doing anything else: + +``` +You are about to migrate `projects/` to Polylith. This will refactor +the project into bases and components and move files. Proceed? (yes/no) +``` + +If the user declines, abort: +``` +Migration aborted by user. +``` + +### Phase 0. Safety Net (git checkpoint) +Migration is destructive — files move, directories are deleted, `pyproject.toml`s are rewritten. **Before loading `migrate-discover`, establish rollback points:** + +1. Confirm the working tree is clean: + ```bash + git status + ``` + If there are uncommitted changes, ask the user to commit/stash before proceeding. Do not start a migration on top of a dirty tree. +2. Create a dedicated migration branch: + ```bash + git checkout -b migrate/ + ``` +3. After each completed phase, commit per that phase's `## Commit` section. The commit message follows the pattern `migrate(): phase ` so phases can be located in `git log` later. + ```bash + git add -A && git commit -m "migrate(): phase " + ``` + This gives the user (and the agent) a discrete, named rollback point per phase. If a later phase fails verification, the agent can `git reset --hard HEAD~1` to back out exactly one phase without losing earlier progress. +4. Record the branch name and starting commit SHA in `migration//state.md` (the `migrate-discover` skill defines that file). + +> ⚠ Never `git reset --hard` past the start of the migration branch without explicit user approval — the user's pre-migration work lives there. + +## Workflow + +Execute the phases in this order. **Verify each phase's `Verify` section succeeds before starting the next.** Commit between phases (see Phase 0 step 3). + +| # | Phase | Skill | Depends on | +|---|----------------------------------------|------------------------------------------------|-----------------------------------------------------| +| 1 | Discover | `migrate-discover` | — | +| 2 | Extract to base | `migrate-extract-to-base` | `migrate-discover` | +| 3 | Prepare project | `migrate-prepare-project` | `migrate-extract-to-base` | +| 4 | Isolate base and big component | `migrate-isolate-base-and-big-component` | `migrate-prepare-project` | +| 5 | Split big component | `migrate-split-big-component` | `migrate-isolate-base-and-big-component` | +| 6 | Extract standalone modules | `migrate-extract-standalone-modules` | `migrate-split-big-component` | +| 7 | Isolate shared and project logic | `migrate-isolate-shared-and-project-logic` | `migrate-extract-standalone-modules`, `migrate-split-big-component` | +| 8 | Distribute wiring | `migrate-distribute-wiring` | `migrate-isolate-shared-and-project-logic` | +| 9 | Split component internals | `migrate-split-component-internals` | `migrate-distribute-wiring` | +| 10| Refactor tests | `migrate-refactor-tests` | `migrate-split-component-internals` | +| 11| Definition of done | `migrate-definition-of-done` | `migrate-refactor-tests` | + +## Optional Skills + +These are not part of the linear flow above. They are triggered when the user opts in during `migrate-discover` (or, for `migrate-dedupe`, when duplication candidates surface). When triggered, **insert them at the indicated point** in the flow. + +| Skill | When to run | Trigger | +|------------------------------------|--------------------------------------------------------------------|-------------------------------------------------| +| `migrate-convert-linter` | After phase 1 (`migrate-discover`), before phase 2. | User opts in during `migrate-discover`. | +| `migrate-convert-type-checker` | After phase 1 (`migrate-discover`), before phase 2. | User opts in during `migrate-discover`. | +| `migrate-convert-package-manager` | After phase 1 (`migrate-discover`), before phase 2. | User opts in during `migrate-discover` **AND** the workspace itself uses uv. The skill is opinionated about uv — see its header for the gating rule. | +| `migrate-dedupe` | After phase 5 (`migrate-split-big-component`) or phase 6 (`migrate-extract-standalone-modules`). | Duplication candidates surfaced and user approves. | + +> ⚠ `migrate-convert-package-manager` only converts **to uv**. If the workspace uses Poetry, PDM, or Hatch as its standard, **skip this skill entirely** — the project should be aligned to the workspace's manager via a manual step instead. + +### Ordering when multiple converters are opted in + +When the user opts into more than one of the optional `migrate-convert-*` skills during `migrate-discover`, run them in **this order** between phase 1 and phase 2: + +1. `migrate-convert-package-manager` — runs first because it rewrites `pyproject.toml` wholesale; subsequent skills must operate on the final layout. +2. `migrate-convert-linter` — runs second so workspace-level lint config consolidation happens against the final `pyproject.toml`. +3. `migrate-convert-type-checker` — runs last; type-checker config is the most localized of the three. + +`migrate-dedupe` is triggered later (after phase 5 or phase 6 surfaces duplication candidates) and has **no ordering dependency** with the converters. + +Commit between each optional skill the same way the main phases commit (see each skill's `## Commit` section). + +## Execution checklist + +For each phase: +1. **Validate `state.md`** against the rules in `migrate-discover` (`### Validation rules`). Abort the phase if validation fails. +2. Load the skill (`migrate-`). +3. Execute its `Steps` in order. +4. Run its `Verify` section. **If verification fails, do not commit and do not proceed.** Either fix the issue, or `git reset --hard` to back out the phase and consult the user. +5. On success, commit per the phase's `## Commit` section. + +## Validation +- No circular dependencies exist in the phase graph above (verified by the dependency table). +- Every skill referenced above exists as `migrate-/SKILL.md` under `.agents/skills/polylith/migrate-project/`. +- Each phase's `Verify` block uses the commands recorded in `migration//state.md` (`RUN_TEST_CMD`, optionally `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD`, plus `POLY_CMD_PREFIX check`). diff --git a/.agents/skills/polylith/migrate-project/migrate-prepare-project/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-prepare-project/SKILL.md new file mode 100644 index 00000000..1e1c9cc4 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-prepare-project/SKILL.md @@ -0,0 +1,84 @@ +--- +name: migrate-prepare-project +description: "[Internal sub-skill of `migrate-orchestrator` (phase 3 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Clean up the project subfolder and consolidate dependencies after extracting application code into bases." +--- + +# Skill: migrate-prepare-project + +## Goal +Clean up the project subfolder (`projects//`) and consolidate dependencies. After this step, the project subfolder should contain only: +- Project `pyproject.toml` (with brick references). +- Task runners (`Makefile`, `Justfile`). +- Project-specific config (e.g., `alembic.ini`). + +## Inputs +From `migration//state.md`: +- `PROJECT_DIR` +- `TARGET_TOP_NS` +- `ALIAS` (optional: `GROUP`) +- Verification commands. + +From `migration//manifest.md`: +- Infra files list. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Verify Project Subfolder +- Run `directory_tree` on `projects//` to confirm only infra and config files remain. + +### 2. Update `pyproject.toml` +- Add brick references to `[tool.polylith.bricks]`: + ```toml + [tool.polylith.bricks] + "../../bases//" = "/" + ``` +- Register the project alias and group in `workspace.toml` (if provided): + ```toml + [tool.polylith.projects.alias] + = "" + + [tool.polylith.projects.groups] + = [""] + ``` + +### 3. Move Tests to Workspace Level +- Move `tests/` to `test//`. +- Update `RUN_TEST_CMD` in `migration//state.md` to point to the new location. +- Update imports and mock patch strings in test files if paths changed. + +### 4. Move Infrastructure Folders +- Move infra folders (e.g., `helm/`, `k8s/`, `kustomize`, `alembic/`) to `infra///`. + +### 5. Consolidate Dependencies +- Move third-party dependencies with version constraints to the workspace root `pyproject.toml`. +- Move dev/test/tooling dependencies to the workspace root. +- List runtime dependencies without version numbers in the project `pyproject.toml`. + +## Verify +- `RUN_TEST_CMD` succeeds against the **new** test location. +- If set, `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` succeed. +- Run `POLY_CMD_PREFIX check` to validate the workspace structure. +- Run `POLY_CMD_PREFIX info` to inspect the workspace and confirm the project is correctly registered. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| `RUN_TEST_CMD` collects 0 tests after step 3 | The command in `state.md` still references the old `projects//tests` path. | Update `RUN_TEST_CMD` to the new `test//` location. Also check `[tool.pytest.ini_options].testpaths` / `rootdir` / `conftest.py` discovery. | +| Tests fail with `ModuleNotFoundError` on internal imports | Tests use `from tests.fixtures import …` and the `tests` package name changed. | Update test imports to the new test root path. If many tests reference the old name, consider keeping `tests` as the leaf directory and only renaming the parent. | +| `mock.patch("")` fails with `AttributeError` | Mock patch strings reference moved modules. | Update patch strings to the new module paths. Use `grep -r 'patch("' test/` to find them all. | +| Infra folder move breaks deploy scripts that hardcoded paths (e.g., `helm//values.yaml`) | Deploy scripts haven't been updated to the new `infra///` location. | Either update the scripts, or symlink the new location from the old one as a transitional step (record the symlink in `migration//state.md`). | +| `poly check` complains about brick references after dependency consolidation | A runtime dependency was moved to the workspace root but the project's `pyproject.toml` doesn't declare it. | Add the dependency name (no version) back to the project's `[project.dependencies]`. The version stays only at the workspace root. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 3 — prepare-project" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-refactor-tests/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-refactor-tests/SKILL.md new file mode 100644 index 00000000..8de3efec --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-refactor-tests/SKILL.md @@ -0,0 +1,91 @@ +--- +name: migrate-refactor-tests +description: "[Internal sub-skill of `migrate-orchestrator` (phase 10 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Restructure unit tests to align with the workspace's Polylith theme." +--- + +# Skill: migrate-refactor-tests + +## Goal +Restructure unit tests so they live next to the brick they test, following the workspace's Polylith theme (`loose` or `tdd`). + +## Scope +- **Unit tests only.** Integration tests typically stay in a shared location (e.g., `test/integration/` or `test//integration/`). Do not redistribute them across bricks. +- **Structure only.** Reorganize test files and update imports/mock patch strings. Do not rewrite test logic or fixtures unless an import/path change makes it strictly necessary. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `RUN_TEST_CMD` (will be updated by this skill) +- `RUN_LINT_CMD`, `RUN_TYPECHECK_CMD` (optional) + +From `migration//manifest.md`: +- List of all bricks (bases and components) with their module maps. + +From `workspace.toml`: +- `[tool.polylith.structure].theme` (`loose` or `tdd`). + +From `test/`: +- Current test directory structure. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Target layout + +| Theme | Test path for a base | Test path for a component | +|----------|------------------------|-----------------------------| +| `loose` | `test/bases///test_*.py` | `test/components///test_*.py` | +| `tdd` | `bases//test///test_*.py` | `components//test///test_*.py` | + +## Steps + +### 1. Classify each unit test file +For each `test_*.py` under the current test root: +1. Read its `import` statements and `mock.patch("...")` strings. +2. Identify the *primary* brick under test — usually the one most imported or the one mock-patched. Tie-break by file name (`test_user_handler.py` → ``). +3. Record the classification in a table you keep in scratch (do **not** commit a separate file for this — it's transient): + +| Test file | Primary brick | Target path | +|-----------|---------------|-------------| + +If a test exercises 2+ bricks at integration level, classify it as integration and move it to `test/integration/` instead. + +### 2. Move test files +- Create the target directories per the theme matrix above. +- Move each `test_*.py` to its brick's test directory. +- For each `conftest.py`: + - **Brick-scoped fixtures** (referenced only by tests under one brick) → move to that brick's test directory. + - **Workspace-shared fixtures** (cross-brick) → keep one `test//conftest.py` or `test/conftest.py`. + +### 3. Update imports and mock patch strings +- Update any `from tests.` imports that survived to the new layout. +- Update `mock.patch("")` strings. Brick reshuffling earlier in the migration may have changed where the patched symbol now lives — use `grep` to locate the target symbol's new path and align the patch string. Patching a wrong path silently succeeds and the test will pass for the wrong reason — verify by intentionally breaking the target function and checking that the test fails. + +### 4. Update `RUN_TEST_CMD` +- Set `RUN_TEST_CMD` in `state.md` to a command that collects from the **new** test root (typically ` pytest test/` for `loose`, or per-brick collection for `tdd`). +- Verify the test count after the move equals the baseline recorded in `migrate-discover`. **A drop in collected tests means files were lost or pytest discovery is misconfigured.** + +## Verify +- `RUN_TEST_CMD` succeeds and **collects the same number of tests** as the baseline from `migrate-discover`. +- If set, `RUN_LINT_CMD` succeeds. +- `POLY_CMD_PREFIX check` is still green. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| `pytest` collects fewer tests than before | The old test root is no longer on `pytest`'s path; or duplicate `conftest.py` files silently shadow each other. | Update `[tool.pytest.ini_options].testpaths` (or pass paths explicitly in `RUN_TEST_CMD`). Run `pytest --collect-only` and diff against baseline collection. | +| `fixture '' not found` | The fixture was in a `conftest.py` you moved into a brick's test dir; tests in another brick can no longer see it. | Either move the fixture up to a higher-scope `conftest.py`, or duplicate it (only if cheap). Don't import fixtures across `conftest.py` files. | +| Tests pass but assert nothing useful (`mock.patch` no longer hits anything) | Patch string still points at the pre-migration module path. | Re-derive the patch path: `...`. Validate by deliberately breaking the patched function and confirming the test fails. | +| `ImportError: cannot import name 'fixture_'` from `conftest.py` | A `conftest.py` `import`s a moved test helper module that didn't follow it. | Move the helper next to the new `conftest.py`, or import it from its new brick path. | +| Tests for moved code suddenly find themselves under a brick name that doesn't match their content | Misclassification in step 1. | Re-read the test's imports — the brick most imported is the one that owns the test. Move and update. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 10 — refactor-tests" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-split-big-component/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-split-big-component/SKILL.md new file mode 100644 index 00000000..eaaba46d --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-split-big-component/SKILL.md @@ -0,0 +1,193 @@ +--- +name: migrate-split-big-component +description: "[Internal sub-skill of `migrate-orchestrator` (phase 5 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Split the big component (`components///`) into multiple focused components." +--- + +# Skill: migrate-split-big-component + +> 📐 **Scope vs sibling skills.** This skill operates **within one project** and turns *one component into many components*. Don't confuse with: +> - `migrate-extract-standalone-modules` — same scope (one project) but pulls *foundational modules* (`consts.py`, `exceptions.py`) out of the residual big component into their own standalone components. +> - `migrate-split-component-internals` — operates **inside one already-extracted component**, splitting its `core.py` into multiple files. No new components are created. +> - `migrate-isolate-shared-and-project-logic` — **cross-project** scope; identifies shared-vs-project-specific logic when migrating a 2nd+ project. +> - `migrate-dedupe` — opportunistic deduplication; this skill includes a dedup-analysis subsection so for a single project you usually don't need `migrate-dedupe` separately. + +## Goal +Split the big component (`components///`) into multiple focused components to improve maintainability, clarity, and reusability. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- `INITIAL_BASE_NAME` +- Verification commands. + +From `migration//manifest.md`: +- Module map of the big component. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### Phase 1: Plan the Split +1. **Review the Big Component**: Use `directory_tree` and `grep` to analyze the big component's structure and identify natural slices. +2. **Define Component Names**: Name components after the **domain or functionality** they represent (e.g., `domain_a_serializer`, `data_transformations`). Avoid generic names like `utils` or `helpers`. +3. **Create a Split Plan**: Record the following in `migration//split_plan.md`: + - Brick name for each new component. + - Files/modules to move into each component. + - Public API (key functions/classes to export). + - Bricks that will import from the new component. + +**When NOT to Extract**: +- The module is tightly coupled to other modules in the component. +- The module is very small (< 20 lines) and extraction adds more indirection than value. + +**Extraction Order**: +- Extract modules with **zero internal dependencies** first (e.g., `exceptions.py`, `consts.py`). +- Extract modules that depend on already-extracted modules next (e.g., `models.py` that imports `exceptions` and `consts`). + +### Examples of Component Naming +Name components after the **domain or functionality** they represent: + +| Original module name | Content (after inspection) | Component name | +|----------------------|----------------------------|----------------| +| `serializers.py` | Serializes data for ERP | `domain_a_serializer` | +| `transformations/` | Maps data between formats | `data_transformations` | +| `parsers.py` | Parses event payloads | `event_parser` | +| `validators.py` | Validates records | `record_validator` | + +#### Avoiding circular imports when extracting modules + +Extracting a module into a separate component can create circular imports if the new component imports from the parent component and the parent still imports from the new component. This commonly happens when a component's `__init__.py` eagerly imports from many submodules. + +**Diagnosis:** The cycle typically looks like: + +``` +new_component.core → parent.__init__ → parent.submodule → new_component +``` + +Python triggers `parent.__init__` whenever any submodule of `parent` is imported (e.g. `from parent.consts import X` loads `parent/__init__.py` first). + +**Resolution strategies (in order of preference):** + +1. **Extract the circular part into its own component.** A circular dependency often signals that the code involved is isolated enough to be its own component. Extract the module that causes the cycle into a standalone component — this breaks the cycle structurally. A component doesn't have to be a "feature"; it can be a utility, a data definition, a pure technical module, or a single ORM model. If the code has a clear responsibility and can be imported without pulling in the rest of the parent, it belongs in its own brick. + + ``` + # Before (cycle): new_component → parent.consts → parent.__init__ → parent.handlers → new_component + # After (no cycle): new_component → consts_component (standalone, no __init__ chain) + ``` + +2. **Trim `__init__.py` exports.** Remove the problematic import from the parent's `__init__.py` and have callers import the submodule directly. This makes the dependency graph explicit and often eliminates the cycle without creating a new brick. + + ```python + # Before: parent/__init__.py imports everything eagerly + from parent.command_handler import CommandHandler # triggers handler → new_component cycle + + # After: remove from __init__.py, callers import directly + from parent.command_handler import CommandHandler # in the base that needs it + ``` + +3. **Standalone component instead of submodule.** If the new component would be a submodule of an existing package (e.g. `myns.models.example_transaction`), importing it triggers the parent package's `__init__.py` and all its eager imports. Make it a standalone component at the namespace level instead (e.g. `myns.example_transaction`). + +4. **Deferred import (last resort).** Move the import inside the function that uses it. This works but hides the dependency and makes the code harder to reason about. Prefer strategies 1–3 first. + +**Pre-flight check:** Before extracting a module, trace the import chain: +1. The new component imports `parent.submodule_X` → Python loads `parent.__init__` +2. Does `parent.__init__` (directly or transitively) import from the new component? +3. If yes → apply one of the strategies above (extract, trim, or restructure) before proceeding. + +#### Refactoring shared infrastructure components + +When a second project needs a component that already exists (e.g. `myns.logging`, `myns.kafka`), compare the implementations closely. Common refactoring patterns: + +**Pattern: Parameterize the shared component.** When two implementations are 80%+ identical with project-specific extras, refactor the shared component to accept optional parameters rather than duplicating code. + +Example — logging with project-specific loggers: +```python +# Shared component: myns.logging +def init(config, *, extra_loggers=None, cache_logger_on_first_use=False): + loggers = {**_BASE_LOGGERS} + loggers.update(_verbosity_overrides(config.LOG_VERBOSITY_LEVEL)) + if extra_loggers: + loggers.update(extra_loggers) + ... + +# Project A base: +init(config, extra_loggers={"httpx": {...}, "backoff": {...}}, + cache_logger_on_first_use=config.LOG_CACHE_LOGGER_ON_FIRST_USE) + +# Project B base: +init(config, extra_loggers={"confluent_kafka_helpers": {...}}) +``` + +**When to parameterize vs. keep separate:** +- **Parameterize** when the core logic is identical and only data/config differs. +- **Keep separate** when the control flow or structure diverges (different frameworks, different patterns). +- **Extract shared base + project-specific wrappers** when there's a significant shared core but non-trivial project-specific logic around it. + +#### Splitting Component Internals + +Components with generic names like `models`, `schemas`, `exceptions`, or `consts` may start with a single `core.py` file. As the workspace grows, these components can accumulate code from different domains. Splitting `core.py` into multiple domain-focused modules **inside the component** can improve maintainability and clarity. + +**When to Split:** +- If the `core.py` file contains definitions from multiple distinct domains (e.g., `domain_a` and `domain_b`). +- If the file contains helper/utility functions alongside class definitions. +- If preparing for a second project migration that will contribute to the same component. + +**Approach:** +- Group definitions by the domain concept they serve. +- Name each module after the domain or functionality it represents (e.g., `domain_a.py`, `domain_b.py`). +- Ensure the public API remains unchanged to avoid breaking existing imports. + +#### Cross-component duplication analysis + +After drafting the split plan (but before executing any moves), analyze the planned components — and any _already existing_ components in the workspace — for duplication: + +1. **Identify overlap:** For each planned component, check whether an existing component already contains similar logic. Look for: + - Functions/classes with the same or very similar names. + - Modules that operate on the same domain concept (e.g., two different `domain_a_serializer` implementations). + - Copy-pasted utility functions (string helpers, date formatting, retry wrappers, etc.). + +2. **Classify the overlap:** + - **Identical or near-identical:** the code does the same thing with trivial differences (variable names, formatting). → Extract to a shared component. + - **Same purpose, different behavior:** the code solves the same problem but with project-specific logic (e.g., different serialization schemas). → Keep separate, but extract any genuinely shared helpers. + - **Coincidental similarity:** the code looks similar but serves unrelated purposes. → Leave separate. + +3. **Propose shared extractions:** When genuinely duplicated code is found, add a step to the split plan: + - Create a new shared component (or extend an existing one) containing the common logic. + - Have both the existing and the new component depend on the shared one. + - Record this in `/split_plan.md` with a rationale. + +4. **Always confirm with the user** before creating shared components — "is this code genuinely shareable?" is a judgment call that depends on how the projects will evolve. + +### Phase 2: Execute the Split +For each planned component in `split_plan.md`: +1. **Create the Component**: Create the component directory with `__init__.py`. +2. **Move Files/Modules**: Move the relevant files/modules into the new component. +3. **Define the Public API**: Update `__init__.py` to re-export the public API. +4. **Update Callers**: Update all imports to reference the new component. +5. **Update `pyproject.toml`**: Add the new brick to the project's `[tool.polylith.bricks]`. +6. **Run Verification**: Ensure tests, linting, and type-checking pass. + +## 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 | +|---------|--------------|-------------| +| New component is named `utils`, `helpers`, `common`, or `misc` | Naming taken from old module names instead of the domain the code serves. | Rename to a domain-specific name (see the "Examples of Component Naming" table). Generic-named bricks attract more code and become the next big component. | +| Extracted component imports back into the residual via the residual's `__init__.py` | Circular import — see the "Avoiding circular imports" subsection above. | Apply strategies 1–3 from that subsection (extract the cyclic part, trim `__init__.py` exports, or restructure to standalone). Strategy 4 (deferred import) only as last resort. | +| `poly check` flags the newly extracted component as not used by any project | The project's base still imports from the residual path (`..`) instead of the new component. | Update the base's imports to the new component's public API, then `POLY_CMD_PREFIX sync --quiet` and re-run check. | +| 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: + +```bash +git add -A && git commit -m "migrate(): phase 5 — split-big-component" +``` + +Substitute ``, ``, and `` 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. diff --git a/.agents/skills/polylith/migrate-project/migrate-split-component-internals/SKILL.md b/.agents/skills/polylith/migrate-project/migrate-split-component-internals/SKILL.md new file mode 100644 index 00000000..df694344 --- /dev/null +++ b/.agents/skills/polylith/migrate-project/migrate-split-component-internals/SKILL.md @@ -0,0 +1,70 @@ +--- +name: migrate-split-component-internals +description: "[Internal sub-skill of `migrate-orchestrator` (phase 9 of 11). Do not load directly — load `migrate-orchestrator` first, which drives all phases.] Split monolithic `core.py` files in shared components into domain-focused modules." +--- + +# Skill: migrate-split-component-internals + +> 📐 **Scope vs sibling skills.** This skill operates **inside one already-extracted component**, splitting its `core.py` into multiple sibling Python files. **No new components are created** — only the internal file layout changes. Don't confuse with: +> - `migrate-split-big-component` — splits **one component into multiple components**. Use that when the unit being broken up is a component, not a file. +> - `migrate-isolate-shared-and-project-logic` — separates shared vs project-specific code **across components and projects**; may create new components. +> - `migrate-extract-standalone-modules` — pulls foundational modules out of the residual big component into new standalone components. + +## Goal +Split monolithic `core.py` files in **shared components** (e.g., `models_shared`, `schemas_shared`, or similar) into domain-focused modules. This skill ensures that shared components remain well-organized and maintainable. + +## Inputs +From `migration//state.md`: +- `TARGET_TOP_NS` +- Verification commands. + +From `migration//manifest.md`: +- Current component list and structure. + +> All inputs from `state.md` are assumed to satisfy the validation rules in `migrate-discover` (`### Validation rules`). Validate before proceeding. + +## Steps + +### 1. Identify Candidates +- Scan components for large `core.py` files. +- A component is a candidate if: + - The file contains definitions from multiple domains. + - The file contains helper/utility functions alongside class definitions. + - The file exceeds a reasonable size threshold. + +### 2. Group Definitions by Domain +- Group definitions by the domain concept they serve. +- Example domains: ORM models, schema/dataclass clusters, helper functions. + +### 3. Create New Modules +- Create domain-focused modules (e.g., `merchant.py`, `transaction.py`). +- Move relevant definitions from `core.py` to the new modules. + +### 4. Update `__init__.py` +- Re-export public names from the new modules in `__init__.py`. + +### 5. Handle `core.py` +- Delete `core.py` if all definitions have been moved. +- Keep `core.py` if it serves as a composition point. + +## Verify +- `RUN_TEST_CMD` succeeds. +- If set, `RUN_LINT_CMD` and `RUN_TYPECHECK_CMD` succeed. + +## Common failure modes + +| Symptom | Likely cause | Remediation | +|---------|--------------|-------------| +| `core.py` is small (<100 lines) but mixes two domains | The component itself is too small to warrant an internal split. | Leave it. Splitting adds indirection without benefit at this size; revisit when the file grows. | +| After splitting, an import like `from . import ` fails | `__init__.py` was not updated to re-export the symbol from its new module. | Add `from .. import ` to `__init__.py`. The component's public API must remain stable across the split. | +| New files inside the component now circularly import each other (e.g., `models/user.py` ↔ `models/transaction.py`) | Domain split was too aggressive; the two files genuinely share a concept. | Extract the shared concept into a third file (e.g., `models/_base.py`) and have both depend on it. | + +## Commit + +After verification passes, commit this phase to the migration branch: + +```bash +git add -A && git commit -m "migrate(): phase 9 — split-component-internals" +``` + +Substitute ``, ``, and `` 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. \ No newline at end of file diff --git a/projects/polylith_cli/pyproject.toml b/projects/polylith_cli/pyproject.toml index 464b1bf7..05af6b8b 100644 --- a/projects/polylith_cli/pyproject.toml +++ b/projects/polylith_cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polylith-cli" -version = "1.47.2" +version = "1.48.0" description = "Python tooling support for the Polylith Architecture" authors = ['David Vujic'] homepage = "https://davidvujic.github.io/python-polylith-docs/"