docs: add deep modules refactor plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cmoyates 2026-03-03 09:43:41 -03:30
parent 1a992a853d
commit 88a0cd9cb8
No known key found for this signature in database
GPG Key ID: F65E08480FDC22D2

View File

@ -0,0 +1,295 @@
---
title: Deep Modules Refactor
type: refactor
date: 2026-03-03
---
# Deep Modules Refactor
## Overview
Restructure corridorkey-mlx into "deep modules" with explicit public interfaces (`__all__` gateways in `__init__.py`), `_testing` subpackages for parity test access to internals, and `import-linter` enforcing cross-package boundaries.
**Guiding principle:** each subpackage exposes a simple interface via `__init__.py`. Implementation files are internal. Tests access internals through `_testing` re-export modules. Import-linter enforces boundaries between top-level packages only (within a package, free imports).
## Problem Statement
All subpackage `__init__.py` files (except `convert/`) are empty. Consumers must know internal filenames (`from corridorkey_mlx.model.corridorkey import GreenFormer`). No tooling enforces boundaries. An AI agent dropping into this codebase has no way to discover a module's API without reading every file.
## Proposed Module Interfaces
### `model/` — Deep Module
```python
# model/__init__.py
from corridorkey_mlx.model.corridorkey import GreenFormer
__all__ = ["GreenFormer"]
```
Internal: `backbone.py`, `hiera.py`, `decoder.py`, `refiner.py`, `corridorkey.py`
```python
# model/_testing.py
"""Unstable re-exports for parity tests. Not part of public API."""
from corridorkey_mlx.model.backbone import HieraBackbone
from corridorkey_mlx.model.hiera import HieraPatchEmbed
from corridorkey_mlx.model.decoder import DecoderHead, MLP
from corridorkey_mlx.model.refiner import CNNRefinerModule, RefinerBlock
```
### `inference/` — Deep Module
```python
# inference/__init__.py
from corridorkey_mlx.inference.pipeline import (
load_model,
compile_model,
infer,
infer_and_save,
)
from corridorkey_mlx.inference.tiling import tiled_inference
from corridorkey_mlx.inference.selective_refine import (
selective_refine,
SelectiveRefineConfig,
SelectiveRefineResult,
)
__all__ = [
"load_model",
"compile_model",
"infer",
"infer_and_save",
"tiled_inference",
"SelectiveRefineConfig",
"SelectiveRefineResult",
"selective_refine",
]
```
Internal: `pipeline.py`, `tiling.py`, `selective_refine.py`
```python
# inference/_testing.py
"""Unstable re-exports for unit tests. Not part of public API."""
from corridorkey_mlx.inference.tiling import (
_compute_tile_coords,
_make_blend_weights_2d,
)
from corridorkey_mlx.inference.selective_refine import (
_binary_dilation,
_build_input,
_gaussian_blur,
_gaussian_kernel_1d,
_resize_to_square,
)
```
### `io/` — Thin Module
```python
# io/__init__.py
from corridorkey_mlx.io.image import (
load_image,
load_alpha_hint,
normalize_rgb,
preprocess,
postprocess_alpha,
postprocess_foreground,
save_alpha,
save_foreground,
)
__all__ = [
"load_image",
"load_alpha_hint",
"normalize_rgb",
"preprocess",
"postprocess_alpha",
"postprocess_foreground",
"save_alpha",
"save_foreground",
]
```
### `utils/` — Shared Base Layer
```python
# utils/__init__.py
from corridorkey_mlx.utils.layout import (
nchw_to_nhwc,
nhwc_to_nchw,
conv_weight_pt_to_mlx,
nchw_to_nhwc_np,
nhwc_to_nchw_np,
)
from corridorkey_mlx.utils.profiling import (
TimingResult,
time_fn,
warmup_and_bench,
)
__all__ = [
"nchw_to_nhwc",
"nhwc_to_nchw",
"conv_weight_pt_to_mlx",
"nchw_to_nhwc_np",
"nhwc_to_nchw_np",
"TimingResult",
"time_fn",
"warmup_and_bench",
]
```
### `convert/` — Already Done
Keep as-is. Already has proper `__all__`.
### Root-level files — Stay Flat
- `engine.py` — add `__all__ = ["CorridorKeyMLXEngine"]`
- `weights.py` — add `__all__ = ["download_weights"]`
- `weights_cli.py` — add `__all__ = ["main", "build_parser"]`
- `__init__.py` — already has `__all__`
- `__main__.py` — no changes needed
## Technical Approach
### Phase 1: Add Gateways (No Breaking Changes)
1. Populate all `__init__.py` files with re-exports and `__all__`
2. Create `_testing.py` modules in `model/` and `inference/`
3. Add `__all__` to `engine.py`, `weights.py`, `weights_cli.py`
4. Run full test suite — everything must still pass (imports unchanged)
**Success criteria:** `uv run pytest` passes, no import changes yet.
### Phase 2: Migrate Imports
Update all consumers to use the gateway imports:
#### Tests
| File | Before | After |
|------|--------|-------|
| `test_decoder_parity.py` | `from corridorkey_mlx.model.decoder import DecoderHead` | `from corridorkey_mlx.model._testing import DecoderHead` |
| `test_refiner_parity.py` | `from corridorkey_mlx.model.refiner import CNNRefinerModule` | `from corridorkey_mlx.model._testing import CNNRefinerModule` |
| `test_hiera_stage_parity.py` | `from corridorkey_mlx.model.hiera import HieraBackbone` | `from corridorkey_mlx.model._testing import HieraBackbone` |
| `test_hiera_stage_shapes.py` | `from corridorkey_mlx.model.hiera import HieraBackbone, HieraPatchEmbed` | `from corridorkey_mlx.model._testing import HieraBackbone, HieraPatchEmbed` |
| `test_greenformer_forward.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `test_end_to_end_smoke.py` | `from corridorkey_mlx.io.image import ...` | `from corridorkey_mlx.io import ...` |
| `test_end_to_end_parity.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `test_compiled_vs_eager_consistency.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `test_tiling_consistency.py` | `from corridorkey_mlx.inference.tiling import _compute_tile_coords, ...` | `from corridorkey_mlx.inference._testing import _compute_tile_coords, ...` |
| `test_selective_refine.py` | `from corridorkey_mlx.inference.selective_refine import _binary_dilation, ...` | `from corridorkey_mlx.inference._testing import _binary_dilation, ...` |
| `test_engine.py` | `from corridorkey_mlx.engine import _validate_image, _validate_mask` | Keep as-is (engine is flat, linter won't enforce within-file) |
| `test_weights.py` | `from corridorkey_mlx.weights import _parse_hash_line, ...` | Keep as-is (weights is flat) |
| `test_conversion.py` | `from corridorkey_mlx.convert.converter import ...` | `from corridorkey_mlx.convert import ...` |
#### Scripts
| File | Before | After |
|------|--------|-------|
| `infer.py` | `from corridorkey_mlx.inference.pipeline import load_model, infer_and_save` | `from corridorkey_mlx.inference import load_model, infer_and_save` |
| `bench_mlx.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `bench_mlx.py` | `from corridorkey_mlx.utils.profiling import warmup_and_bench` | `from corridorkey_mlx.utils import warmup_and_bench` |
| `compare_reference.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `convert_weights.py` | `from corridorkey_mlx.convert.converter import convert_checkpoint` | `from corridorkey_mlx.convert import convert_checkpoint` |
| `experiment_selective_refine.py` | `from corridorkey_mlx.inference.pipeline import load_model` + `selective_refine.*` | `from corridorkey_mlx.inference import load_model, selective_refine, ...` |
#### Internal cross-package imports
| File | Before | After |
|------|--------|-------|
| `engine.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` (TYPE_CHECKING) | `from corridorkey_mlx.model import GreenFormer` |
| `engine.py` | `from corridorkey_mlx.inference.pipeline import load_model, compile_model` | `from corridorkey_mlx.inference import load_model, compile_model` |
| `engine.py` | `from corridorkey_mlx.io.image import preprocess, ...` | `from corridorkey_mlx.io import preprocess, ...` |
| `inference/pipeline.py` | `from corridorkey_mlx.model.corridorkey import GreenFormer` | `from corridorkey_mlx.model import GreenFormer` |
| `inference/pipeline.py` | `from corridorkey_mlx.io.image import ...` | `from corridorkey_mlx.io import ...` |
| `inference/selective_refine.py` | `from corridorkey_mlx.io.image import normalize_rgb` | `from corridorkey_mlx.io import normalize_rgb` |
**Success criteria:** `uv run pytest` passes with all new import paths.
### Phase 3: Import Linter
Add `import-linter` to enforce cross-package boundaries.
```toml
# pyproject.toml additions
[tool.importlinter]
root_packages = ["corridorkey_mlx"]
include_external_packages = false
[[tool.importlinter.contracts]]
name = "Package layering"
type = "layers"
layers = [
"corridorkey_mlx.engine",
"corridorkey_mlx.inference",
"corridorkey_mlx.model | corridorkey_mlx.io | corridorkey_mlx.convert",
"corridorkey_mlx.utils",
]
# engine → inference → model/io/convert → utils
[[tool.importlinter.contracts]]
name = "Convert independence"
type = "independence"
modules = [
"corridorkey_mlx.convert",
"corridorkey_mlx.model",
"corridorkey_mlx.io",
]
# convert, model, io should not import from each other
[[tool.importlinter.contracts]]
name = "Weights independence"
type = "independence"
modules = [
"corridorkey_mlx.weights",
"corridorkey_mlx.model",
"corridorkey_mlx.inference",
"corridorkey_mlx.io",
]
# weights module is standalone
```
Add to dev dependencies and CLAUDE.md commands:
```bash
uv run lint-imports # import boundary check
```
**Success criteria:** `uv run lint-imports` passes. All cross-package imports go through gateways.
### Phase 4: Documentation & CLAUDE.md Updates
1. Update CLAUDE.md repo layout section to document public interfaces
2. Add a "Module Boundaries" section explaining the gateway pattern
3. Add `_testing.py` convention to CLAUDE.md
4. Update commands section with `lint-imports`
## Acceptance Criteria
- [ ] Every subpackage `__init__.py` has `__all__` defining its public API
- [ ] `model/_testing.py` and `inference/_testing.py` exist with internal re-exports
- [ ] All tests/scripts use gateway imports (no direct `.corridorkey`, `.decoder`, `.pipeline` imports cross-package)
- [ ] `import-linter` passes with layering + independence contracts
- [ ] Full test suite passes (`uv run pytest`)
- [ ] Lint passes (`uv run ruff check .`)
- [ ] CLAUDE.md documents module boundaries and gateway convention
## Dependencies & Risks
**Risk: circular imports from re-exports.** Mitigated by the existing acyclic dependency graph — no cycles detected.
**Risk: import-linter false positives.** `TYPE_CHECKING` imports should be excluded. `import-linter` supports `--exclude-type-checking-imports`. Verify this works.
**Risk: breaking scripts outside the repo.** Only the root `CorridorKeyMLXEngine` export matters for external consumers. Internal restructuring doesn't affect the public API.
**Dependency:** `import-linter` package added to dev deps.
## Unresolved Questions
- `utils.layout` funcs used inside `convert/converter.py` — does `convert` importing from `utils` violate any intended boundary? (current plan: `utils` is the base layer, everyone can import it)
- Should `_testing.py` modules have their own `__all__`? (leaning yes for explicitness)
- `weights.py` has module-level constants (`DEFAULT_ASSET_NAME`, etc.) used by `test_weights.py` — add to `__all__` or keep test accessing flat file directly?