# mgf-django v0.1.0 — Maiden voyage cutover guide

> **Audience:** consumer agents (and humans relaying for them)
> migrating from `mgf-common[django]` (v0.30 and earlier) to
> `mgf-django` v0.1.0 + `mgf-common` v0.31.0.
> **What this is:** the canonical migration document for the
> federation extraction. The mgf-common-side guide is at
> [`mgf-common/docs/cutover/v0.31.0.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/cutover/v0.31.0.md).

This is the **fourth and final framework-adapter sibling extraction**
under the federation split plan recorded in
[`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/release/federation_roadmap.md).
mgf-fastapi shipped first at v0.1.0 / mgf-common v0.28; mgf-http
shipped at v0.1.0 / mgf-common v0.29; mgf-sqlalchemy + mgf-alembic +
mgf-fastapi v0.2 paired at mgf-common v0.30; mgf-django ships now
at v0.1.0 paired with mgf-common v0.31. After this release the
v0.32+ stabilization window opens and mgf-common settles into its
final pre-v1.0 shape: pure-Python infrastructure + standards
source-of-truth.

---

## At-a-glance

| Was (≤ mgf-common v0.30) | Is (mgf-common v0.31 + mgf-django v0.1) |
|---|---|
| `pip install 'mgf-common[django]'` | `pip install mgf-django` |
| `INSTALLED_APPS = [..., "mgf.common.django"]` | `INSTALLED_APPS = [..., "mgf.django"]` |
| `MIDDLEWARE = ["mgf.common.django.MgfRequestIdMiddleware", ...]` | `MIDDLEWARE = ["mgf.django.MgfRequestIdMiddleware", ...]` |
| `MIDDLEWARE = ["mgf.common.django.MgfContextMiddleware", ...]` | `MIDDLEWARE = ["mgf.django.MgfContextMiddleware", ...]` |
| `LOGGING = {"formatters": {"json": {"()": "mgf.common.django.JsonFormatter"}}}` | `LOGGING = {"formatters": {"json": {"()": "mgf.django.JsonFormatter"}}}` |
| `from mgf.common.django import …` | `from mgf.django import …` |
| `apps.get_app_config("mgf_common")` ⚠️ | `apps.get_app_config("mgf_django")` (label renamed) |

**One BREAKING runtime change beyond the import-rename:** the
Django app **label** changes from `"mgf_common"` to `"mgf_django"`
to match the new module path. See [The label rename](#the-label-rename).

**No code body changes** otherwise. The five public surfaces
(`MgfCommonConfig`, `MgfRequestIdMiddleware`, `MgfContextMiddleware`,
`JsonFormatter`, `DjangoSettingsBridge`) are byte-identical to their
mgf-common v0.30.0 originals modulo import paths and the label.

---

## What landed in mgf-django v0.1.0

| Submodule | What |
|---|---|
| `mgf.django` | All five public names: `MgfCommonConfig`, `MgfRequestIdMiddleware`, `MgfContextMiddleware`, `JsonFormatter`, `DjangoSettingsBridge`. |

Plus a top-level `apps.py` (Django's `INSTALLED_APPS` discovery
walks for it) re-exporting `MgfCommonConfig` from the private
`_apps.py` impl per the closed-box convention.

---

## The label rename

Django app labels can't contain dots, so when the module lived
under `mgf.common.django` it picked `"mgf_common"` as a stable
label that wouldn't collide with consumer app labels. The label
choice was independent of the module path; renaming the module
to `mgf.django` was a natural moment to also align the label to
`"mgf_django"`.

The label is visible in three places consumers might have
hardcoded:

1. **`apps.get_app_config(label)` calls** — most common.
2. **Migration filenames** — Django writes
   `{app_label}_{NNNN}_{description}.py`. Existing migrations stay
   on disk with their `mgf_common_*` filenames; the migrations
   table records the old labels too. New migrations from this
   AppConfig won't be written here (the AppConfig has no
   migration-producing models), so practically there's nothing to
   rename.
3. **Admin URLs** — `/admin/{app_label}/{model}/...`. The
   AppConfig has no admin-registered models, so this is a no-op.

For 99% of consumers the migration is a one-shot `apps.get_app_config`
sed (if any).

### Mechanical rewrite

```bash
# At your repo root:
git ls-files '*.py' | xargs sed -i \
    -e 's|from mgf\.common\.django\b|from mgf.django|g' \
    -e 's|import mgf\.common\.django\b|import mgf.django|g' \
    -e 's|mgf\.common\.django\.|mgf.django.|g' \
    -e 's|"mgf\.common\.django"|"mgf.django"|g' \
    -e "s|'mgf\\.common\\.django'|'mgf.django'|g" \
    -e 's|get_app_config("mgf_common")|get_app_config("mgf_django")|g' \
    -e "s|get_app_config('mgf_common')|get_app_config('mgf_django')|g"

# Then audit:
git grep -n 'mgf\.common\.django\|mgf_common'
# Manually inspect remaining matches — `mgf_common` may legitimately
# appear in unrelated contexts (env var names like
# MGF_COMMON_APP_NAME stay with that name regardless).
```

The sed pair handles ~all the call sites. The audit catches the
rare consumer who used `mgf_common` for something unrelated to the
app label (e.g., a project-internal namespace).

---

## Migration steps

### 1. Update your `pyproject.toml`

```toml
# Before
dependencies = [
    "mgf-common[django]>=0.30,<0.31",
]

# After
dependencies = [
    "mgf-common>=0.31,<0.32",
    "mgf-django>=0.1,<0.2",
]
```

If you used a pinned `requirements/*.lock` shape (pip-tools or
similar):

```bash
make lock      # or whatever your project's lock-regen target is
```

### 2. Rewrite imports + config strings

Run the sed batch from [The label rename](#the-label-rename) above.
This handles ~99% of the migration mechanically.

For non-import contexts (docstrings, log messages, comments):

| Search | Replace |
|---|---|
| `mgf.common.django` | `mgf.django` |
| `"mgf_common"` (Django label) | `"mgf_django"` |

### 3. Update `settings.py`

Triple-check that:

- `INSTALLED_APPS` lists `"mgf.django"` (not `"mgf.common.django"`).
- `MIDDLEWARE` lists the two middlewares under their new
  `"mgf.django.MgfRequestIdMiddleware"` /
  `"mgf.django.MgfContextMiddleware"` paths.
- `LOGGING` (if you use `mgf.django.JsonFormatter`) references the
  new path: `{"()": "mgf.django.JsonFormatter"}`.
- `MGF_COMMON_APP_NAME` and `MGF_COMMON_APP_VERSION` (if set) keep
  those names — Django settings keys aren't affected by the
  rename.

### 4. Re-run the four gates

```bash
uv sync       # or your project's equivalent
uv run ruff check
uv run mypy --strict src/
uv run pytest
python manage.py check     # Django's own consistency check
```

If your test suite was green against mgf-common v0.30, it should
be green against mgf-common v0.31 + mgf-django v0.1 immediately
after the import rewrite + label-rename audit.

### 5. Re-run `mgf-common catch-up`

```bash
mgf-common catch-up
```

The audit's pin-checker recognises `mgf-django` as an expected
sibling dep. The closed-box-leak checker also understands
`mgf.django` as a sibling import path.

---

## What changed internally

- **Same code body.** The five public surfaces move byte-identical
  modulo import paths. No behaviour change for consumers.
- **App label rename.** `MgfCommonConfig.label` was `"mgf_common"`
  in mgf-common ≤ v0.30; it's `"mgf_django"` in mgf-django v0.1+.
- **PEP 420 namespace package.** `mgf-django` ships `src/mgf/`
  without a top-level `__init__.py`, matching the federation
  pattern. `mgf.django`, `mgf.fastapi`, `mgf.fastapi.db`,
  `mgf.http`, `mgf.sqlalchemy`, `mgf.alembic`, `mgf.common`
  coexist as siblings in the same Python process.
- **`mgf.common._request_id` cross-sibling import preserved.**
  Both Django and FastAPI middlewares share the same contextvar
  for end-to-end request-id correlation. This is a deliberate
  exception to the closed-box rule, mirrored in
  `mgf.fastapi._request_id`.

---

## Why this split happened

Per the federation roadmap. The reasoning is shared across all
sibling extractions, but for `mgf-django` specifically:

- The `django` runtime dep is heavy; non-Django consumers (FastAPI
  services, CLI tools, data pipelines) shouldn't pay the install
  cost. The `[django]` extra already enforced that, but the
  sibling boundary is a cleaner barrier.
- The `mgf.common.django` adapter was the LAST framework-coupled
  module in mgf-common. Extracting it leaves mgf-common as
  pure-Python infrastructure + standards source-of-truth — the
  v1.0 target architecture.
- Future Django-shaped concretes (e.g., Channels-aware async
  middleware, DRF-specific helpers) land here, not in mgf-common
  — keeping mgf-common as the federation leaf with hierarchy
  roots only.

This is the **fourth and final** of four planned extractions:

| mgf-common release | Extracts | Status |
|---|---|---|
| v0.28 | mgf-fastapi | shipped |
| v0.29 | mgf-http | shipped |
| v0.30 | mgf-sqlalchemy + mgf-alembic + mgf-fastapi v0.2 | shipped |
| v0.31 (this one) | mgf-django | this release |
| v0.32+ | stabilization | future |
| v1.0 | lock the federated surface | future |

The full plan is in
[`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/release/federation_roadmap.md).

---

## Per-consumer migration notes

### inthewords (Django web)

**Live config not affected.** inthewords doesn't import from
`mgf.common.django` in `config/settings/base.py` — it uses its own
`apps.core.middleware.RequestLoggingMiddleware` and
`apps.core.middleware.AppErrorMiddleware`. The pin bump to
`mgf-common>=0.31,<0.32` is enough; no `INSTALLED_APPS` /
`MIDDLEWARE` / `LOGGING` rewrites needed.

**Doc cosmetics only.** `docs/CLAUDE.md` references the canonical
mgf.common.django pattern (as an example for future adoption); those
references are updated to the new sibling path at the v0.31 cutover.

### PlasmaMapper (FastAPI)

**Not affected.** PlasmaMapper is a FastAPI consumer; no Django
surface in use.

### vm-vmanager-app (Qt desktop)

**Not affected.** No Django surface in use.

### mgf-apiprobe

**Not affected.** No Django surface in use.

---

## What's deliberately NOT in v0.1.0

The first mgf-django release is **byte-identical** to mgf-common
v0.30.0's `mgf.common.django` surface (modulo the import path and
label rename). New surface waits for v0.2.0+:

- **Async-mode middleware.** Both `MgfRequestIdMiddleware` and
  `MgfContextMiddleware` are sync-mode-only today. Django wraps
  sync middleware in async views automatically; the
  synchronization-context-switch cost is usually negligible.
  Adding `async_capable = True` + `async __call__` is a follow-up
  paper if a consumer hits real performance pressure.
- **DRF-specific helpers.** inthewords uses Django REST Framework
  but builds its own DRF integration. If a second DRF consumer
  emerges, lift their pattern into a typed helper here.
- **Channels integration.** ASGI Django via Channels has its own
  middleware shape; out of scope for v0.1.
- **Email-via-OTel / structured-error-page integrations.**
  Long-tail Django ergonomics that aren't yet a generalisable
  pattern.

---

## Engineering note

mgf-django v0.1.0 carries 18 unit tests parity-preserved from
mgf-common v0.30.0's `tests/unit/django/`:

- 3 tests for `MgfCommonConfig` (label rename pinned, ready
  idempotency, cleanup safety)
- 4 tests for `JsonFormatter` (no-arg construction, JSON shape,
  redaction, Django logging integration)
- 6 tests for the two middlewares (request-id generation,
  forwarding, case-insensitive, contextvar lifecycle, context
  attachment, pre-bootstrap fallback)
- 5 tests for `DjangoSettingsBridge` (live read, secret-key
  scrubbing, mgf settings exposure, BaseAppSettings inheritance,
  direct construction)

Coverage threshold ≥80% matches mgf-common's standard. The PEP
420 namespace package (no top-level `mgf/__init__.py` in any
sibling repo) means `mgf.django`, `mgf.fastapi`, `mgf.http`,
`mgf.sqlalchemy`, `mgf.alembic`, and `mgf.common` coexist
seamlessly in the same Python process.
