# Changelog

All notable changes to `mgf-common` are documented here.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
the project follows [SemVer](https://semver.org/spec/v2.0.0.html). Per
the 0.x window ([SemVer 0.x](https://semver.org/#spec-item-4) and rule
**AP-03**), MINOR releases MAY break the public API. Pin tightly:

```toml
mgf-common = ">=0.X.0,<0.Y"   # one minor at a time
```

Each release section cross-references the [`FEEDBACK.md`](FEEDBACK.md)
entry and (where applicable) the consumer-side migration recipe. The
release-engineering discipline that produces this changelog is in
[`docs/standards/RELEASING.md`](docs/standards/RELEASING.md).

---

## [Unreleased]

Nothing yet.

---

## [0.24.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.24.0/> · Tag: `v0.24.0`

> **Cutover guide:** [`docs/cutover/v0.24.0.md`](docs/cutover/v0.24.0.md)
> — codify the four-layer test pyramid as a standard so other
> projects adopt the same shape. **Documentation-only release;
> zero Python-surface change.**

### Added

- **Three new TS-* rules in `docs/standards/TESTING.md`:**
  - **TS-19** — test layer markers MUST be registered in
    `pyproject.toml::[tool.pytest.ini_options].markers`. Combined
    with `--strict-markers` (TS-05) this prevents silent typos.
  - **TS-20** — tests MUST be partitioned across
    `tests/unit/` (mirrors `src/`), `tests/integration/`
    (cross-module), `tests/e2e/` (full bootstrap-to-output),
    `tests/smoke/` (post-install sanity).
  - **TS-21** — every published library MUST have a smoke
    layer in `tests/smoke/` running in <100 ms total. CLI/desktop
    apps without a wheel surface MAY skip.
- **`docs/standards/TESTING.md` §13 — the operational layer-marker
  pattern.** New section codifying the four-layer pyramid as a
  standard: directory shape, marker registration, runtime
  budgets, the smoke-layer minimum-viable shape, the e2e
  bootstrap-fixture pattern, the integration cross-module
  pattern, and a decision tree for "which layer for a new
  test?". Section §13.10 includes mgf-common's own per-layer
  counts as a reference.
- **`docs/internal/TEST_COVERAGE.md`** — reference document
  showing what mgf-common tests at each layer plus per-module
  coverage breakdown plus coverage gaps as a roadmap. Consumer
  projects can copy the shape for their own
  `docs/internal/TEST_COVERAGE.md`.
- **Section renumbering in `TESTING.md`:** new §13 "Layer
  markers" pushes the original §13–§17 to §14–§18 (Anti-patterns,
  Self-review, Mechanical enforcement, AS-IS, Cross-references).

### Engineering

- v0.24.0 is the **second docs-only release** for `mgf-common`
  (after v0.20.0). The pattern: when a feature shape stabilises
  and proves itself across multiple polish rounds (here:
  v0.21 → v0.22 → v0.23 for `mgf.common.process`), the next
  step is **codifying the pattern as a standard so other
  projects adopt the same shape**. This is the load-bearing
  part of the "cornerstone library" framing — the standards
  drive consistency across the ecosystem, not just the wheel.

---

## [0.23.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.23.0/> · Tag: `v0.23.0`

> **Cutover guide:** [`docs/cutover/v0.23.0.md`](docs/cutover/v0.23.0.md)
> — second `mgf.common.process` polish round. Quality + comprehensive
> test layer expansion. **No behaviour breaks** for v0.22.0 consumers;
> existing `run()` / `Service` / readiness predicates / Process*
> exceptions all unchanged.

### Added

- **Public `redactor` property on `JsonFormatter` and `TextFormatter`.**
  Replaces the fragile private-attribute introspection that
  `mgf.common.process._streamer.get_active_redactor` was doing
  (the `formatter._redactor` lookup). The private attribute stays
  as a back-compat alias; new callers should use the public property.
- **`Service.__repr__`** for debugging. Shape:
  `Service(name='<n>', command=[...], status=<state>, pid=<pid|->)`
  where status is `not-started` / `running` / `exited(<rc>)`.
- **Best-effort OTel span wrapping** for `run()` and
  `Service.start()`. When `mgf.common.bootstrap()` is active AND
  the OpenTelemetry SDK is installed, each launch opens an
  `operation_span("process.run.<name>")` / `operation_span(
  "service.start.<name>")` with `command_id` as a span attribute.
  Failed launches (`ProcessTimeout` / `ServiceNotReady`) are
  recorded on the span. **Gated on bootstrap state** — when no
  `AppContext` is active, the call short-circuits to
  `contextlib.nullcontext()`, avoiding the `UnconfiguredWarning`
  the unguarded `operation_span` would emit (since it reads
  `app_name()` to name the tracer). Test suites with
  `filterwarnings = error` continue to work.
- **Tuple commands accepted** — `run(("echo", "hi"))` works
  alongside `run(["echo", "hi"])`. UX paper-cut for tests/scripts
  that build command tuples. Internal validation (`_validate_command`)
  normalizes to `list[str]` so the downstream `Popen` always sees
  a list and the `ProcessResult.command` field is always a list.
- **Pytest layer markers** (`smoke`, `e2e`, `integration`)
  registered in `pyproject.toml`. Run a layer in isolation with
  `pytest -m smoke` etc.

### Test coverage expansion

- **Smoke tier** (`tests/smoke/`) — 7 tests, < 100 ms total. Minimal
  post-install sanity (imports, exception MRO, `run()` happy path,
  Service no-launch-on-init, tuple acceptance, shell-string
  rejection).
- **End-to-end tier** (`tests/e2e/`) — 8 tests:
  - **Redaction E2E** (5 tests) — bootstrap + `run()` + child
    emitting a Bearer token → assert scrubbed in BOTH captured
    `stdout` and the streamed log record. Includes `redact=False`
    opt-out, `redact=True` requires-bootstrap raise, no-bootstrap
    silent-no-redaction.
  - **Correlation E2E** (3 tests) — `command_id` consistency
    across captured records, distinct ids across concurrent
    `run()` calls in threads, Service lifecycle records share
    the same id with the streamer's records.
- **Integration tier** (`tests/integration/process/`) — 5 tests:
  - **Multi-service via `ExitStack`** (2 tests) — declared-order
    start, LIFO teardown, no-leak when the dependent's start
    fails.
  - **Concurrent `run()`** (3 tests) — 16 parallel calls produce
    16 distinct `command_id`s, captured stdout is per-call
    isolated, log records don't cross processes.
- **Unit tier additions** (`tests/unit/process/test_polish_v023.py`) —
  9 tests: restart-after-failure pattern, OTel span gating
  (no `UnconfiguredWarning` without bootstrap; nullcontext path
  vs real-span path), public-redactor property on both
  formatters, `Service.__repr__` shape across lifecycle states.

**75 → 104 tests in the process subtree (+29)**. Full repo:
1416 → 1445 (+29). Zero regressions.

### Changed (internal — no surface impact)

- **`get_active_redactor()`** prefers the new public `redactor`
  property; falls back to `_redactor` for older formatters
  (back-compat).
- **Extracted `_validate_command(command)`** helper from
  duplicated validation code in `run()` and `Service.__init__`.
  Single source of truth for "list-or-tuple of strings, non-empty,
  no shell strings."

### Engineering

- **Stability tier:** every name in `mgf.common.process` stays
  at `experimental` per AP-11. Promotion still requires one full
  MINOR cycle without a 🔴 Blocker filed.
- **Test pyramid is now well-formed:** unit (84 process module
  tests) → integration (5) → e2e (8) → smoke (7). Each layer
  catches a different class of failure; the layer markers
  encourage the right CI shape (smoke pre-flight, full suite on
  PR, e2e on nightly).

---

## [0.22.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.22.0/> · Tag: `v0.22.0`

> **Cutover guide:** [`docs/cutover/v0.22.0.md`](docs/cutover/v0.22.0.md)
> — `mgf.common.process` polish round. Reliability fixes + simplicity
> + one small additive method. **No behaviour breaks** for v0.21.0
> consumers; existing `run()` / `Service` / readiness predicates
> continue to work unchanged.

### Fixed

- **`Service.start()` early-exit detection.** Pre-fix: a child that
  crashed during startup (typo in command, missing dependency,
  segfault) caused `start()` to burn the full readiness `timeout_s`
  before raising a useless "probe X did not resolve" error. Post-fix:
  `start()` polls `proc.poll()` alongside the readiness probe and
  raises `ServiceNotReady` immediately when the child has exited
  before becoming ready. The returncode + stderr tail land in
  `last_error` for diagnosis. **Resolution time: from full timeout
  (e.g., 30s) to ~250ms** for the failure case.
- **Thread-safe `LogLineReady` snapshot.** Pre-fix: under high-volume
  child stdout (≥ thousands of lines per second), `LogLineReady`
  iterating its capture deque could race the streamer thread's
  `append`, raising `RuntimeError: deque mutated during iteration`.
  Post-fix: snapshots use `list(deque)` — atomic against the
  appending thread under CPython's GIL. Pinned by a 2000-line +
  1ms-poll-interval stress test.
- **Setup-failure rollback.** Pre-fix: any failure between `Popen`
  and the readiness-resolved log line could leak the launched
  process. Post-fix: `Service.start()` wraps streamer init +
  readiness wait in a `try/except BaseException` that calls the
  full teardown path before re-raising. The "no leaked process on
  failure" contract now covers every code path, including
  `KeyboardInterrupt`.

### Added

- **`Service.wait(timeout=None) -> int | None`** — block until the
  child exits naturally OR `timeout` elapses. Returns the
  `returncode` on natural exit, `None` on timeout (process still
  running). The escape hatch from `with Service(...)` for "start
  this service, let it run until it crashes or completes naturally"
  — without `wait()`, the only escape was `stop()`, which kills
  the child even if it was about to exit cleanly.

### Changed (internal — no surface impact)

- **Extracted `_streamer.resolve_redactor()` and `make_streamers()`
  helpers.** Deduplicates the parallel logic between
  `mgf.common.process.run()` and `Service.start()`. Both consumers
  now resolve redaction + create streamers through one call site
  each. ~30 LOC removed.
- **`_LineSnapshot` class → closure.** The single-method
  callable wrapper became a closure (3 lines vs 25). Behaviour
  identical; mutation surface smaller.
- **Tightened `get_active_redactor()`.** Single pass over
  `logging.root.handlers`; removed the try/except chain around the
  `_GLOBAL_CONTEXT` import path that was hiding logic — formatter
  introspection IS the discovery path; the AppContext walk added
  no value.
- **Replaced bespoke `_suppress_close_errors` class with stdlib
  `contextlib.suppress(Exception)`.** Three lines deleted; behaviour
  identical.
- **Dropped unused `Service._recent_lines` deque** (leftover from
  an earlier design that pre-dated the `_StreamCapture.snapshot`
  approach).

### Engineering

- **+11 new tests in `tests/unit/process/test_polish.py`** covering:
  early-exit detection (3 tests — fast resolution, stderr-tail
  diagnostics, normal-timeout still works), thread-race resilience
  under load (1 stress test), no-leak rollback regression
  (1 test), `Service.wait()` (3 tests — natural exit, timeout,
  before-start), shared helper sanity (3 tests). All v0.21.0
  tests continue to pass unchanged.
- **64 → 75 process tests; 1405 → 1416 total. Zero regressions.**
- **Stability tier:** every name in `mgf.common.process` stays at
  `experimental` per AP-11. The polish round does not promote;
  promotion still requires one full MINOR cycle without a 🔴
  Blocker filed.

---

## [0.21.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.21.0/> · Tag: `v0.21.0`

> **Cutover guide:** [`docs/cutover/v0.21.0.md`](docs/cutover/v0.21.0.md)
> — process management. New top-level surface
> `mgf.common.process` for unified subprocess + service-lifecycle
> orchestration in tools, scripts, and tests.

### Added

- **`mgf.common.process` package** (10 names; all `experimental`
  per AP-11) — unified subprocess + service-lifecycle orchestration:
  - **`run(command, *, ...)` → `ProcessResult`** — one-shot
    subprocess wrapper. List-only command (eliminates command-
    injection at the API surface), structured result with
    `command_id` correlation, timeout-with-grace SIGTERM→SIGKILL
    escalation, redaction-aware line-by-line log streaming
    through `mgf.process.<name>` (stdout=INFO, stderr=WARNING),
    POSIX process-group cascade by default. Default `check=True`
    raises typed errors on non-zero / signal exit.
  - **`Service(command, *, ready, ...)`** — long-lived process
    with a readiness predicate + clean shutdown. Context-manager
    protocol; `start()` blocks until ready or raises
    `ServiceNotReady`. **No leaked process on failure** — service
    is fully torn down before the readiness exception is raised.
    Idempotent `start()` / `stop()`.
  - **Readiness predicates** — `PortReady(host, port)`,
    `HttpReady(url, expected_status=200)`, `LogLineReady(pattern,
    stream)`, `CallableReady(predicate)`, `AllOf([...])`,
    `AnyOf([...])`. `ReadinessProbe` Protocol for custom
    predicates. `LogLineReady` peeks into `Service`'s captured
    line history (last 1000 per stream).
  - **POSIX-first.** `process_group=True` (default) launches in a
    new session group via `start_new_session=True`; SIGTERM
    cascades via `os.killpg`. Windows is best-effort — silent
    downgrade to per-PID kill; signal-cascade semantics differ.
- **`mgf.common.exceptions` Process-launch category** — 5 new
  names, all subclasses of `OperationError` so existing
  EH-04-shaped CLI top-level handlers catch them unchanged:
  - `ProcessError` (base) — carries `.command` + `.command_id`.
  - `ProcessTimeout` — carries `.timeout`.
  - `ProcessNonZeroExit` — carries `.returncode` + `.stderr_tail`
    (last 10 lines of stderr for diagnostics).
  - `ProcessSignaled` — POSIX-only; carries `.signal`.
  - `ServiceNotReady` — carries `.probe_description`,
    `.timeout`, `.last_error`.

### Changed

- **`mgf.common.exceptions.__all__`** grew from 31 → 36 names.
  Additive — every existing import keeps working.

### Documentation

- **`docs/public/recipes/process-management.md`** (~370 lines) —
  comprehensive recipe covering `run()` patterns (timeout,
  redaction, log correlation, env_extra, capture-output),
  `Service` patterns (Postgres-for-test with composite
  PortReady+LogLineReady, Uvicorn smoke, multi-service via
  `ExitStack`, custom readiness via `CallableReady`,
  composite-`AnyOf` for platform-dependent ready signals),
  failure modes, testing notes, cross-references, and
  out-of-scope reminders.
- **`PUBLIC_API.md`** — new `mgf.common.process` section
  (10 names) + 5 new ProcessError leaves in the exceptions
  table. Total surface: 178 → 193 names.

### Engineering

- **65 new tests** in `tests/unit/process/` covering:
  - `test_errors.py` — taxonomy + attribute pinning.
  - `test_run.py` — basic run, error paths, log streaming
    (per-line records, level mapping, command_id/pid extras),
    name option, duration accuracy.
  - `test_readiness.py` — every predicate (PortReady,
    HttpReady, LogLineReady, CallableReady, AllOf, AnyOf) +
    composite short-circuit + `wait_for_ready` semantics.
  - `test_service.py` — context-manager + explicit start/stop
    + idempotency + readiness-failure with no-leak guarantee
    + LogLineReady stdout/stderr matching + composite probes
    + property accessors. Uses Python one-liner subprocesses as
    "services" — zero external dependencies.
- **Stability tier:** every name ships `experimental` per AP-11.
  Promotion to `stable` after one full MINOR cycle without a 🔴
  Blocker filed against any name (rule AP-11). Two consumers
  exercising real workloads is the practical bar.
- **Out of scope (deliberate):** restart-on-crash supervision
  (use systemd / supervisord), backoff machinery, multi-service
  declarative orchestration (compose `Service` context managers
  via `ExitStack`), shell-string commands (lists only —
  command-injection-safe by construction), async API
  (`mgf.common.process._aio` deferred to future when a consumer
  needs it).

---

## [0.20.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.20.0/> · Tag: `v0.20.0`

> **Cutover guide:** [`docs/cutover/v0.20.0.md`](docs/cutover/v0.20.0.md)
> — documentation polish release. Three closures, all docs-only:
> framework-owned-logging recipe, `docs/standards/web/` subtree
> opening, and the documented-non-adoption principle.

**Code change:** none. `__version__` bumps from 0.19.0 → 0.20.0
to ship the new docs through the wheel's `_standards/` data
(per `[tool.hatch.build.targets.wheel.force-include]`). Standards
docs reach consumers via `mgf-common standards <topic>` so the
new paths need to land in a real release.

### Documentation

- **`docs/public/recipes/framework-owned-logging.md`** (closes
  [PAPER-25](FEEDBACK.md)) — promotes the
  `bootstrap(setup_logging=False)` pattern from v0.19.0's
  cutover guide to a permanent recipe with Django + FastAPI +
  Flask sections, a what-`setup_logging=False`-preserves table,
  and an explicit "what you give up" list. Cutover guides are
  time-bounded; framework-owned logging is the durable shape
  every web/API consumer needs on day one.
- **`docs/standards/web/README.md`** (closes
  [ASPIRE-03](FEEDBACK.md), Approach A — incremental) — opens
  the web-domain narrative subtree as a navigation page. Lists
  the cross-cutting recipes (currently scattered across
  cutover guides + `docs/public/recipes/`) and gives a
  read-in-order narrative for a new web consumer. Per-recipe
  promotion (cloud-native logging, framework config, http
  exceptions, bootstrap, stub-gaps, adoption-walk) trickles in
  over subsequent releases — each as one focused PR.
- **`docs/standards/principles/non-adoption-is-valid.md`**
  (closes [ASPIRE-04](FEEDBACK.md), "Magogi-foundation
  principle") — codifies "documented non-adoption is a
  first-class outcome of standards conformance." The framing
  behind every shipped carve-out (LOG-01, CONFIG-01,
  BOOTSTRAP-01); naming it gives newer contributors cover to
  decline a primitive when adoption would harm their context,
  AND nudges them to file the rationale upstream so the
  carve-out can be generalised. Three components: conscious
  decline, documented rationale, feedback if generalizable.
  Plus three worked examples (inthewords' LOG-01 / BOOTSTRAP-01
  pre-v0.19.0; PlasmaMapper's "small consumer" scope
  discipline).
- **`docs/standards/README.md`** — Conformance section grew a
  "Documented non-adoption preserves conformance" paragraph
  pointing at the new principle doc; "Per-kind subtrees"
  pointer to `web/README.md` added.

### Engineering

- The "incremental Approach A" structuring for ASPIRE-03 means
  v0.20.0 ships the front door; per-recipe promotion lands as
  small PRs across subsequent releases, tracked in ASPIRE-03's
  Retrospective field. This is the first release that uses an
  explicit promotion-roadmap checklist as a release artifact.
- v0.20.0 is the first **docs-only** release for `mgf-common`.
  It tests the release pipeline's tolerance for zero
  Python-surface-change releases.

---

## [0.19.0] — 2026-05-07

PyPI: <https://pypi.org/project/mgf-common/0.19.0/> · Tag: `v0.19.0`

> **Cutover guide:** [`docs/cutover/v0.19.0.md`](docs/cutover/v0.19.0.md)
> — composition + framework-native batch. Closes the v0.18.0
> HTTP-01 keystone (PAPER-24) and the Django adoption blocker
> (BOOTSTRAP-01).

### Fixed

- **`ExceptionTranslationMiddleware` now reads `HttpError.http_status`**
  (closes [PAPER-24](FEEDBACK.md)) — the v0.18.0 architectural
  batch shipped HTTP-01 (`HttpError` + 9 status-bearing concretes,
  each with `http_status: int` class attribute) AND the FastAPI
  middleware, but they didn't compose: a consumer rooting a domain
  exception in `HttpNotFound` still had to register every leaf in
  `status_map` for the middleware to find the 404. Resolution
  order is now: `status_map` (non-`AppError` ancestors) →
  class-declared `http_status` → `status_map[AppError]` catch-all
  → 500. Backward compatible — explicit `status_map` entries still
  win as the override seam. Regression introduced in v0.18.0;
  unblocks PlasmaMapper's `TenantNotFoundError(HttpNotFound)`
  rebase.

### Added

- **`bootstrap(setup_logging=True)` keyword-only flag** (closes
  [BOOTSTRAP-01](FEEDBACK.md) — Django adoption blocker, part 2)
  — when `False`, the StreamHandler + RotatingFileHandler install
  + per-component `level_overrides` are skipped. Identity, OTel,
  excepthooks, AppContext registration, and the bootstrap-log
  buffer replay still happen. For consumers whose framework owns
  the logging stack: Django (`LOGGING` dictConfig), FastAPI/
  uvicorn, Flask (`app.logger`), Pyramid. Default `True`
  preserves v0.18 behaviour for every existing consumer.
- **`mgf.common.alembic.configure_env` accepts `include_object=`
  + `include_schemas=`** (closes [PAPER-22](FEEDBACK.md)) —
  pass-through to `alembic.context.configure(...)`. Unblocks
  PostGIS / TimescaleDB / pgvector consumers who need to filter
  extension-owned tables (`spatial_ref_sys`,
  `_timescaledb_internal`, …) from `alembic revision
  --autogenerate`. Defaults `None` / `False` match alembic's own
  defaults — no behaviour change for current users.

### Changed

- **`settings_config()` defaults `dotenv_filtering="match_prefix"`**
  (closes [BOOTSTRAP-01](FEEDBACK.md) — Django adoption blocker,
  part 1) — `BaseAppSettings` subclasses now silently ignore
  `.env` keys that don't match their `env_prefix`, instead of
  routing them to `extra="forbid"` and raising `ValidationError`.
  This lets a `.env` be shared between mgf-common (`MGF_*`) and
  the consumer's framework (Django `SECRET_KEY`/`DB_*`,
  SQLAlchemy `DATABASE_URL`, Celery `CELERY_BROKER_URL`, …).
  **Behaviour change for consumers with `extra="allow"`** that
  were relying on the legacy permissive dotenv source to surface
  unprefixed keys onto the model — pass `dotenv_filtering=None`
  in your `model_config` override to restore the old behaviour.
  Every other `extra` mode is unaffected (`forbid` was raising
  before, `ignore` was already silent).

### Documentation

- **Editable-install caveat for `pytest_plugin`** (closes
  [PAPER-23](FEEDBACK.md)) — note added to
  `mgf.common.pytest_plugin` docstring + `docs/public/recipes/
  testing.md` explaining that `uv sync` against a sibling editable
  source target does not write the dist-info `entry_points.txt`,
  so the `pytest11` auto-registration is silently skipped. Opt
  in via `pytest_plugins = ["mgf.common.pytest_plugin"]` in
  consumer `tests/conftest.py`. PyPI-installed consumers are
  unaffected — the entry point fires during `pip install` /
  `uv pip install`.

### Engineering

- 4 new entries triaged + closed in one batch (PAPER-22, PAPER-23,
  PAPER-24, BOOTSTRAP-01); two from PlasmaMapper, one composite
  from inthewords. v0.19.0 is the first release that closed a
  same-release-introduced regression (PAPER-24 was a v0.18.0
  miss).

---

## [0.18.0] — 2026-05-06

PyPI: <https://pypi.org/project/mgf-common/0.18.0/> · Tag: `v0.18.0`

> **Cutover guide:** [`docs/cutover/v0.18.0.md`](docs/cutover/v0.18.0.md)
> — first release using the cutover-guide pattern (REL-09
> delivery mechanism). Read it for migration instructions per
> closed entry.

### Added

- **`HttpError` category in `mgf.common.exceptions`** (closes
  [HTTP-01](FEEDBACK.md)) — eighth category in the typed-
  exception taxonomy. `HttpError` base + `HttpClientError`
  (4xx) + `HttpServerError` (5xx) + nine concrete leaves
  (`HttpBadRequest` 400, `HttpUnauthorized` 401, `HttpForbidden`
  403, `HttpNotFound` 404, `HttpConflict` 409, `HttpGone` 410,
  `HttpUnprocessable` 422, `HttpInternalServerError` 500,
  `HttpServiceUnavailable` 503). Each carries `http_status: int`
  as a class attribute. 12 new names in
  `mgf.common.exceptions.__all__`, all stability tier
  `experimental`. Distinct from `ResourceNotFoundError`
  (provider-domain) by deliberate non-inheritance.
- **Framework-native config carve-out in `CONFIGURATION.md`
  §0a** (closes [CONFIG-01](FEEDBACK.md)) — explicit
  recognition that Django (and Rails-equivalent) consumers
  can't simultaneously be CF-* compliant and use
  `from django.conf import settings`. CF-01 / CF-04 / CF-08 /
  CF-09 declared N/A under framework-native; CF-02 / CF-05 /
  CF-10 / CF-12 still apply with framework-native mechanics.
  Worked example for `inthewords` (Django) included.
- **Per-rule applicability column on standards docs** (closes
  [LOG-02](FEEDBACK.md), [CONFIG-01](FEEDBACK.md)) — `Applies
  to` column on every LG-* and CF-* rule with values from
  `{all, gui-app, service, fastapi, django, cli, library,
  consumer-owned-config}`. Per-kind compliance matrix added at
  the top of `LOGGING.md`'s Rules box. `mgf-common catch-up`
  and future audit tooling filter by detected kind so
  non-applicable rules don't surface as violations.
- **Cloud-native logging recipe** (closes
  [LOG-01](FEEDBACK.md)) — new doc at
  `docs/public/recipes/cloud-native-logging.md` walks the
  stdout-only `MgfSettings` shape, the JSON-formatter choice,
  the deployment-time check ("the platform IS my rotating
  file sink"), and the `inthewords` worked example
  (Daphne → systemd journal → Promtail → Grafana Cloud).
- **Cutover-guide pattern** (maintainer-initiated process
  improvement) — new directory `docs/cutover/` with one
  file per release that closes ≥1 FEEDBACK entry. The first
  cutover guide is `docs/cutover/v0.18.0.md` (this release's
  artifact). `RELEASING.md` §7.0 + REL-09 updated to make
  cutover guides the canonical migration source; per-PAPER
  notification paragraphs become pointers into them.

### Changed

- **LG-06 wording** (closes [LOG-01](FEEDBACK.md)) — revised
  to parameterise on deployment topology. Three first-class
  branches: rotating local file (desktop / single-server),
  process stdout/stderr (cloud-native / container), or remote
  sink (OTLP / syslog / HTTP webhook). "Console only, no
  aggregation" is non-compliant in production regardless of
  branch. The desktop branch's prescription is unchanged
  (no regression for VManager-shape consumers).
- **`STRATEGIC_REVIEW.md` Priority 1 marked ✅ ACHIEVED**
  (closes [ASPIRE-02](FEEDBACK.md)) — `inthewords` and
  `PlasmaMapper` arrived as the second + third independent
  consumers. PAPER-06 (reference-consumer restructure)
  unblocks for follow-up. PAPER-10 (entry-point auto-discovery)
  receives "non-pain" evidence: `inthewords` reports they
  don't need entry-point discovery; PAPER-10 stays DEFERRED.

### Fixed

- **FastAPI kind template's quick-start no longer imports
  from a private module** (closes [PAPER-21](FEEDBACK.md)) —
  one-line fix: `from mgf.common._lifecycle import bootstrap`
  → `from mgf.common import bootstrap`. The same audit that
  flags private imports on consumer src/ now also lints every
  rendered kind-template (cli / fastapi / django / library /
  service) via the new `TestQuickStartBlocksNoClosedBoxLeaks`
  regression guard. Audit-and-template asymmetry can't recur.

### Migration

See [`docs/cutover/v0.18.0.md`](docs/cutover/v0.18.0.md) — one
§ per closed entry with concrete steps. Headlines:

- **Pin bump:** `mgf-common = ">=0.18,<0.19"`.
- **HTTP consumers:** lift exception hierarchies onto the new
  `HttpError` category (Approach A). Existing project-level
  `AppError` shims keep working unchanged; the migration is
  structural and lands when you're ready.
- **Django consumers:** re-read `CONFIGURATION.md` §0a; declare
  CF-* applicability per the worked example.
- **Cloud-native consumers:** read the new
  `cloud-native-logging.md` recipe.
- **FastAPI consumers scaffolded against 0.17.2:** re-run
  `mgf-common catch-up --apply` to refresh the auto-block; if
  you copy-pasted the v0.17.2 quick-start, replace the
  private-module import.

---

## [0.17.2] — 2026-05-05

PyPI: <https://pypi.org/project/mgf-common/0.17.2/> · Tag: `v0.17.2`

### Added

- **`mgf-common catch-up` kind detection** (closes
  [PAPER-13](FEEDBACK.md) + [PAPER-17](FEEDBACK.md)).
  `_resolve_project_identity` now infers `--kind` heuristically
  when not supplied:
  - **With `pyproject.toml`:** parse `[project] dependencies`
    + `[project.scripts]`. `fastapi` → `fastapi`; `django` →
    `django`; `flask` → `service` (closest existing template
    kind); non-empty scripts without framework → `cli`;
    otherwise → `library`.
  - **Without `pyproject.toml`:** `manage.py` at root → `django`
    (Django's canonical layout marker). Other no-pyproject
    shapes fall through to `library`.
  - Default still `library` when no marker fires.
- **`mgf-common catch-up --apply` bootstraps `docs/CLAUDE.md`
  BEGIN/END markers** when the file exists but lacks them
  (closes [PAPER-16](FEEDBACK.md)). `_apply_bootstrap_claude_md_markers`
  reads the existing CLAUDE.md, backs it up to
  `.bak.<timestamp>`, writes a new file with the auto-block
  at the top + the original content verbatim below. The
  destructive `init-project --force` workaround is no longer
  the documented path; `catch-up --apply` is non-destructive.

### Changed

- **`mgf-common catch-up` pin audit emits a tailored info-level
  message for pip-tools layouts** (partial close of
  [PAPER-15](FEEDBACK.md)). When `pyproject.toml` is absent
  but `requirements/*.{txt,lock}` or a top-level
  `requirements.txt` is present, the audit acknowledges the
  layout and recommends the minimal-pyproject.toml workaround
  inline. Full pip-tools-manifest support (reading the pin
  from `requirements/base.txt`, four-gate audit from standalone
  config files) is deferred to a follow-up PAPER.

### Migration

None — every change is additive to existing CLI behaviour. No
public-Python-surface change. Re-run `mgf-common catch-up`
after upgrading to see the kind detection in action; existing
`--kind`-on-the-CLI invocations still take precedence over the
heuristic.

---

## [0.17.1] — 2026-05-05

PyPI: <https://pypi.org/project/mgf-common/0.17.1/> · Tag: `v0.17.1`

### Fixed

- **`mgf-common catch-up` v0.1-pattern audit no longer walks
  `.venv/`** (closes [PAPER-14](FEEDBACK.md)). The audit's
  `_count_matches` walk now applies the same skip-dirs filter
  (`tests/`, `.venv/`, `__pycache__/`, `.pytest_cache/`,
  `.mypy_cache/`, `.ruff_cache/`, etc.) that the closed-box-leak
  audit already used. Without the filter, audits on Django-shape
  projects (no `src/`) walked into `.venv/` and counted
  mgf-common's own deprecated shim definitions as user-source
  v0.1 call sites — dozens of false positives.
- **`mgf-common doctor` first-line banner reflects the imported
  package's `__version__`** (closes [PAPER-18](FEEDBACK.md)),
  not `importlib.metadata.version()`. The metadata path goes
  stale during editable-install dev workflows; reading
  `__version__` from the imported package always matches the
  live source tree per AP-13's sync rule.
- **`mgf-common <subcommand>` no longer emits
  `UnconfiguredWarning`** (closes [PAPER-19](FEEDBACK.md)). The
  CLI's `main()` calls `_set_identity("mgf-common-cli",
  __version__)` before dispatch, so subcommands that read
  identity (`explain`, `doctor`, future settings-aware
  surfaces) get the right values without firing the
  warning chain or printing `vunknown` placeholders. Lighter
  than the filing's suggested `bootstrap_for_test` because
  diagnostic CLIs MUST stay filesystem-side-effect-free.
- **`mgf-common catch-up --apply` no longer rewrites the
  pyproject pin against an editable install** (closes
  [PAPER-20](FEEDBACK.md)). Editable installs (e.g., `pip
  install -e ../mgf_common`, `[tool.uv.sources]` overrides) can
  run AHEAD of the latest PyPI-published version; rewriting a
  consumer's pin to the editable `__version__` produced an
  unsatisfiable pin on plain-PyPI resolvers. The audit row
  still surfaces staleness; the consumer bumps the pin by hand
  against `pip index versions mgf-common`.

### Migration

None — every change is a fix-in-place to existing CLI
behaviour. Re-run `mgf-common catch-up` after upgrading; you
may see fewer false positives on the v0.1-pattern row, a
correct version string on `doctor`'s banner, and clean stderr
on `explain`. Editable-install consumers see the audit warn
without auto-rewriting the pin.

---

## [0.17.0] — 2026-05-05

PyPI: <https://pypi.org/project/mgf-common/0.17.0/> · Tag: `v0.17.0`

### Added

- **`mgf-common catch-up` closed-box-leak audit** — a new audit
  row that AST-walks the consumer's `src/` and flags imports of
  `mgf-common`'s private surface. Closes [PAPER-08](FEEDBACK.md).

  Three leak shapes detected:

  1. `from mgf.common._x import …`     — module itself private
  2. `from mgf.common.X import _y`     — public module, private name
  3. `import mgf.common._x`            — direct private-module import

  The consumer's `tests/` directory is exempt (legitimate
  test-helper imports inside `_internal_test_helpers/` style
  directories aren't violations). Caches and vendored
  directories (`.venv/`, `__pycache__/`, `.pytest_cache/`,
  `.mypy_cache/`, `.ruff_cache/`, `node_modules/`, `dist/`,
  `build/`, `.tox/`) are skipped.

  Known private→public migrations are named inline in the
  warning so the audit doesn't just say "stop" — it says "do
  this instead." Today's mapping:

  | Private | Public (since) |
  |---|---|
  | `_default_log_path` | `mgf.common.observability.default_log_path` (v0.13.0) |
  | `_crash_dir` | `mgf.common.observability.default_crash_dir` (v0.13.0) |

  Extend `_PRIVATE_TO_PUBLIC` in `_catch_up.py` whenever a
  future PAPER-07-shaped private→public promotion happens.

### Migration

Consumers running `mgf-common catch-up` after upgrading will
see one new audit row. If you have any closed-box leaks in
your `src/` (you might — VManager had one until v0.13.0),
they surface now with the file:line and the suggested public
replacement. Like the existing `v0.1-pattern usage` row, the
fix is structural — `catch-up --apply` does NOT auto-rewrite
import statements; the consumer migrates by hand.

---

## [0.16.0] — 2026-05-02

PyPI: <https://pypi.org/project/mgf-common/0.16.0/> · Tag: `v0.16.0`

### Changed

- **Renamed** `docs/standards/SECURITY.md` →
  `docs/standards/SECURITY_STANDARD.md` to remove the
  duplicate-name collision with the repository-root
  `SECURITY.md` (vulnerability-reporting policy). The two files
  serve genuinely different purposes — SC-12 mandates the root
  policy file; the engineering standard now has a name that
  reflects what it is. The wheel's bundled-data path changes
  in lockstep: `mgf/common/_standards/SECURITY.md` →
  `mgf/common/_standards/SECURITY_STANDARD.md`.

### Migration

The CLI alias `security` keeps working unchanged — typing
`mgf-common standards security` still resolves to the engineering
standard. Both lower- (`security`) and upper-case (`SECURITY`)
inputs route through the alias map to the new canonical filename.

If your tooling parses `mgf-common standards --paths security`
output, the printed path now ends in `SECURITY_STANDARD.md`
rather than `SECURITY.md`. Update any consumer scripts that
hard-coded the old filename.

```bash
# Before — outputs .../SECURITY.md
$ mgf-common standards --paths security

# After (0.16+) — outputs .../SECURITY_STANDARD.md
$ mgf-common standards --paths security
```

The `mgf-common standards security` (no `--paths`) print-content
behaviour is unchanged.

---

## [0.15.0] — 2026-05-02

PyPI: <https://pypi.org/project/mgf-common/0.15.0/> · Tag: `v0.15.0` · SHA256 (wheel): `ad14d4ed17ca…`

### Added

- **`mgf.common.exceptions.AppConfigErrorKind`** — `StrEnum`
  discriminator for `AppConfigError` failure modes. Members:
  `FILE_NOT_FOUND`, `IO_READ_FAILED`, `JSON_PARSE_FAILURE`,
  `YAML_PARSE_FAILURE`, `NON_DICT_ROOT`, `MISSING_VERSION`,
  `UNSUPPORTED_VERSION`, `MIGRATE_FAILED`, `SCHEMA_VALIDATION`.
  String values are stable contract — usable as a machine-readable
  kind in serialised crash reports.
- **`AppConfigError.kind: AppConfigErrorKind | None`** — class-level
  attribute (default `None`). Every load-path raise in
  `mgf.common.versioned._store` now sets `kind`. Consumers stop
  string-matching on private message markers and discriminate via
  `match e.kind:`. Closes [PAPER-11](FEEDBACK.md).

### Migration

For consumers translating `AppConfigError` to project-flavoured
wording — replace string-match discriminators with
pattern-matching on `kind`:

```python
# Before — fragile to upstream rewording
except AppConfigError as e:
    raw = str(e).lower()
    if "is malformed (json)" in raw:
        raise OperationError("... is unreadable") from e
    elif "has version=" in raw or "is missing a typed" in raw:
        raise OperationError("... has unsupported schema_version") from e
    else:
        raise OperationError(f"... is malformed: {e}") from e

# After — typed, exhaustive, stable
except AppConfigError as e:
    match e.kind:
        case AppConfigErrorKind.JSON_PARSE_FAILURE | AppConfigErrorKind.YAML_PARSE_FAILURE:
            raise OperationError("... is unreadable") from e
        case AppConfigErrorKind.UNSUPPORTED_VERSION | AppConfigErrorKind.MISSING_VERSION:
            raise OperationError("... has unsupported schema_version") from e
        case _:
            raise OperationError(f"... is malformed: {e}") from e
```

Backward-compatible: every existing `except AppConfigError:` clause
keeps catching unchanged. Save-path raises don't yet opt in — file a
`PAPER-NN` if you hit pain there.

---

## [0.14.0] — 2026-05-02

PyPI: <https://pypi.org/project/mgf-common/0.14.0/> · Tag: `v0.14.0` · SHA256 (wheel): `5c172712f454…`

### Added

- **`VersionedFileStore[T](..., tolerate_missing_version=False)`** —
  new constructor flag. Default `False` keeps today's
  strict-by-default contract: missing `version:` field raises
  `AppConfigError`. `True` opts in to a grace mode for consumers
  with **pre-versioning legacy files**: a missing field logs one
  `WARNING` via the `mgf.common.versioned` logger and is treated as
  `current_version` (the migrate callback is NOT called — there is
  no version to migrate from). The next `save()` stamps the file,
  so the warning fires at most once per file. Files where the field
  is *present but not an int* still raise (corruption signal, not
  a legacy file). Closes [PAPER-09](FEEDBACK.md).

### Migration

Consumers with bespoke "missing means current" handling for
pre-versioning YAML/JSON satellite files can replace it with the
opt-in flag:

```python
store = VersionedFileStore(
    path,
    schema=ProfileFile,
    current_version=2,
    migrate=migrate,
    tolerate_missing_version=True,   # missing → v2 with WARNING
)
```

The grace-mode warning from `mgf.common.versioned` replaces any
hand-rolled warning the consumer was emitting.

---

## [0.13.0] — 2026-05-02

PyPI: <https://pypi.org/project/mgf-common/0.13.0/> · Tag: `v0.13.0` · SHA256 (wheel): `02d8ed8bc4b1…`

### Added

- **`mgf.common.observability.default_log_path`** — public helper
  that resolves to `<state>/<app>/logs/<app>.log`, where
  `<state>` honours `XDG_STATE_HOME` and falls back to
  `~/.local/state`. This is where `setup_logging` writes when
  called without an explicit `log_file=`. Surface in your UI for
  "where do my logs live?" diagnostics.
- **`mgf.common.observability.default_crash_dir`** — public helper
  resolving to `<state>/<app>/crashes/`. Where `report_now` writes
  serialised crash reports.
- **`mgf.common.observability.default_state_dir`** — public helper
  resolving to `<state>/<app>/`, the parent of both. The directory
  is NOT created — callers `mkdir` themselves if needed.

All three are lazy (resolve at call time, rule **CF-04**); per-test
isolation when monkeypatching `HOME` / `XDG_STATE_HOME` keeps
working. Closes [PAPER-07](FEEDBACK.md).

### Deprecated

- **`mgf.common.observability.logging_setup._default_log_path`** —
  was a closed-box leak (consumers had no public alternative). Now
  a deprecated shim that delegates to `default_log_path` and emits
  `MgfDeprecationWarning`. Removed in a future release.
- **`mgf.common.observability.crash_reporter._crash_dir`** — same
  shape; deprecated shim delegating to `default_crash_dir`.

### Migration

```python
# Before — reaching into the private surface (closed-box leak)
from mgf.common.observability.logging_setup import _default_log_path
from mgf.common.observability.crash_reporter import _crash_dir

# After — public API
from mgf.common.observability import default_log_path, default_crash_dir
```

The underscored names continue to work for one cycle but emit
`MgfDeprecationWarning` per call.

---

## Pre-`0.13.0`

Earlier releases (`0.1.0`, `0.3.0`, `0.4.0`, `0.4.1`, `0.4.2`,
`0.11.1`, `0.11.3`, `0.12.0`) were cut before this CHANGELOG was
introduced. The audit trail is in `git log` and the
[PyPI release pages](https://pypi.org/project/mgf-common/#history).

Notable ones (most relevant for consumers upgrading):

- **`0.12.0`** (2026-05-01) — final release before the CHANGELOG.
  Public surface that ships in this release matches the wheel on
  PyPI (`pip show mgf-common` against an installed `0.12.0`); note
  that **`mgf.common.versioned`** was added to master *after* the
  `0.12.0` PyPI cut — consumers reading `PUBLIC_API.md` from a
  master checkout may see entries that 0.12.0 doesn't ship. (This
  asymmetry is the root cause of [PAPER-12](FEEDBACK.md); the
  `PUBLIC_API.md` banner and the doc at
  [`docs/standards/RELEASING.md`](docs/standards/RELEASING.md)
  prevent its recurrence going forward.)

[Unreleased]: https://codeberg.org/magogi-admin/mgf_common/compare/v0.18.0...HEAD
[0.18.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.18.0
[0.17.2]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.17.2
[0.17.1]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.17.1
[0.17.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.17.0
[0.16.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.16.0
[0.15.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.15.0
[0.14.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.14.0
[0.13.0]: https://codeberg.org/magogi-admin/mgf_common/releases/tag/v0.13.0
