From 49d2d8a90bed3c1acfc7bf65cba3d325ac42b7c5 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 16 Jun 2026 19:00:00 +0300 Subject: [PATCH 1/4] docs(planning): spec actionable error for Alembic-blind schema drift validate_schema() detects CHECK/partial-index drift that `alembic revision --autogenerate` cannot remediate. Spec appends a docs-pointer to the RuntimeError (only when blind drift is present) plus a hand-written-migration recipe in docs/operations/alembic.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../design.md | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md diff --git a/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md new file mode 100644 index 0000000..2e5b9e5 --- /dev/null +++ b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md @@ -0,0 +1,194 @@ +--- +status: draft +date: 2026-06-16 +slug: actionable-schema-drift-error +supersedes: null +superseded_by: null +pr: null +outcome: null +--- + +# Design: Actionable error for Alembic-blind schema drift + +## Summary + +`validate_schema()` detects schema drift that `alembic revision --autogenerate` +**cannot** generate a migration for — the `_lease_ck` CHECK constraint and +the load-bearing partial-index predicates. Today the operator gets a bare +`RuntimeError` and a dead end: re-running autogenerate produces nothing. This +change appends a one-line pointer to that error — only when an Alembic-blind +drift is present — directing the operator to a new docs section that holds the +exact hand-written migration recipe. No comparator hook; no new exception type. + +## Motivation + +`make_outbox_table` declares three partial indexes (each with a load-bearing +`postgresql_where`) and a `
_lease_ck` CHECK +(`(acquired_token IS NULL) = (acquired_at IS NULL)`). On a **fresh** +`create_table`, autogenerate renders all of it. On an **incremental** migration +onto a pre-existing table, Alembic's `compare_metadata` has no check-constraint +comparator and its index comparator ignores `postgresql_where`, so a missing or +drifted CHECK — and a non-partial / wrong-predicate / non-unique index — ships +silently. `validate_schema()` backstops this with direct `pg_catalog` / +`pg_constraint` probes (`_validate_index_predicates_sync`, +`_validate_check_constraints_sync`), so the drift *is* caught at runtime. + +But the resulting error — e.g. + +> `RuntimeError: Outbox schema mismatch: missing CHECK constraint 'outbox_lease_ck' (expected 'acquired_token is null = acquired_at is null')` + +— is a dead end. The operator runs `alembic revision --autogenerate` expecting a +remediation migration and gets an empty `upgrade()`, because the same blindness +that let the drift through also prevents autogenerate from fixing it. The +detection is correct; the remediation path is missing. + +The third validation pass (the Alembic `compare_metadata` diff for +tables/columns/plain indexes) reports drift that autogenerate *can* fix, so it +needs no special handling — the operator there just re-runs autogenerate. + +## Non-goals + +- An Alembic autogenerate comparator hook ("make autogenerate emit it"): + explicitly declined — we are not registering a `comparators.dispatch_for` + extension or asking users to wire one into `env.py`. +- A structured exception type (`SchemaMismatchError` with a `.remediation` + list): out of scope; the raised type stays `RuntimeError`. +- Embedding the full copy-pasteable DDL in the error text: the error carries a + short pointer; the DDL lives in docs. +- Server-default drift: still not validated (`compare_server_default=False`, + unchanged). + +## Design + +### 1. Error composition in `validate_schema()` (`client.py`) + +`validate_schema()` runs the same three probe groups as today, but tracks +whether the **Alembic-blind** probes contributed any errors. The blind probes +are exactly `_validate_index_predicates_sync` and +`_validate_check_constraints_sync`, both run only on the outbox table +(`self._table`) — the DLQ table declares no partial-index predicates or CHECK +constraints, so it is validated solely by the Alembic diff. The Alembic +`compare_metadata` diff (`_validate_schema_sync` / `_validate_dlq_schema_sync`) +is autogenerate-fixable and does **not** set the blind flag. + +The message is built through a new **pure** helper so the append logic is +unit-testable without a live engine (the 100 % coverage gate forbids an +untested branch, and `test_unit.py` runs with no Postgres): + +```python +_SCHEMA_MISMATCH_PREFIX = "Outbox schema mismatch: " +_AUTOGEN_BLIND_HINT = ( + "These (CHECK constraints and partial-index predicates) are invisible to " + "'alembic revision --autogenerate' — hand-write the migration: " + "https://faststream-outbox.modern-python.org/operations/alembic/" + "#fixing-drift-autogenerate-cant-see" +) + + +def _compose_schema_mismatch_message(errors: list[str], *, has_blind_drift: bool) -> str: + msg = _SCHEMA_MISMATCH_PREFIX + "; ".join(errors) + if has_blind_drift: + msg += "\n\n" + _AUTOGEN_BLIND_HINT + return msg +``` + +`validate_schema()` collects the blind-probe errors separately, ORs their +presence into `has_blind_drift`, and raises +`RuntimeError(_compose_schema_mismatch_message(errors, has_blind_drift=...))`. + +The prefix `"Outbox schema mismatch: " + "; ".join(errors)` and the per-error +strings are unchanged, so every existing `pytest.raises(..., match=...)` +substring assertion keeps passing. The pointer is appended on its own line +(`\n\n`) after the joined errors. + +### 2. Docs section (`docs/operations/alembic.md`) + +New section `## Fixing drift autogenerate can't see { #fixing-drift-autogenerate-cant-see }`, +placed after "Drift detection in CI" and before "DLQ retention via partition +drop". It states the two Alembic-blind classes and why autogenerate misses them +(no check-constraint comparator; index comparator ignores `postgresql_where`), +then gives exact hand-written `op.*` recipes: + +- **Missing or drifted `lease_ck` CHECK** — drop first only if it exists but + drifted, then create: + + ```python + # only if it exists with a wrong predicate: + op.drop_constraint('outbox_lease_ck', 'outbox', type_='check') + op.create_check_constraint( + 'outbox_lease_ck', 'outbox', + '(acquired_token IS NULL) = (acquired_at IS NULL)', + ) + ``` + +- **Non-partial / wrong-predicate / non-unique index** — drop and recreate with + the load-bearing `postgresql_where` (and `unique=True` for the timer-id + index): + + ```python + op.drop_index('outbox_timer_id_uq', table_name='outbox') + op.create_index( + 'outbox_timer_id_uq', 'outbox', ['queue', 'timer_id'], + unique=True, postgresql_where=sa.text('timer_id IS NOT NULL'), + ) + # outbox_pending_idx: postgresql_where=sa.text('acquired_token IS NULL') + # outbox_lease_idx: postgresql_where=sa.text('acquired_token IS NOT NULL') + ``` + +The recipe names the three index suffixes (`_pending_idx`, `_lease_idx`, +`_timer_id_uq`) and their expected predicates, matching +`_EXPECTED_INDEX_PREDICATES` in `client.py`. + +A one-line cross-link is added from `docs/usage/schema-validation.md` to this +anchor. + +### 3. Anchor stability + +The error URL is `https://faststream-outbox.modern-python.org/operations/alembic/#fixing-drift-autogenerate-cant-see` +(site_url from `mkdocs.yml`, directory-URL form matching the existing +`#dlq-retention-via-partition-drop` anchor). The explicit `{ #... }` attr-list +anchor pins the slug so a later heading reword can't silently break the link. + +## Operations + +None. No infra, DNS, or external-account changes. + +## Testing + +- **`test_unit.py`** (no Postgres): + - `_compose_schema_mismatch_message(errors, has_blind_drift=True)` contains the + pointer URL and the `;`-joined prefix. + - `has_blind_drift=False` omits the pointer entirely. + - Prefix and `; ` join format are intact in both cases. +- **`test_integration.py`**: + - Extend an Alembic-blind case (e.g. + `test_validate_schema_fails_when_lease_check_constraint_missing`) to assert + the pointer URL is in the raised message. + - Assert an autogenerate-fixable case (e.g. + `test_validate_schema_fails_when_columns_missing`) does **not** contain the + pointer. +- **Docs**: `just docs-build` (`mkdocs build --strict`) passes — the new anchor + resolves and the cross-link from `schema-validation.md` does not 404. +- **Lint**: `just lint-ci` clean. + +## Risk + +- **Low — link rot.** If the docs site_url or page path changes, the error URL + goes stale. Mitigation: the explicit attr-list anchor + the `--strict` docs + build (which fails on a broken in-repo cross-link) catch the in-repo half; the + hostname is the one piece a strict build can't verify, and it is the published + canonical domain. +- **Low — flag plumbing.** `has_blind_drift` must be ORed from the two blind + probes specifically, not from the full error list, or the pointer would also + fire on pure column drift. Covered by the autogenerate-fixable negative test. +- **Negligible — message contract.** Appending a trailing line preserves the + prefix and per-error substrings, so existing `match=` assertions and any + operator log-greps on the prefix are unaffected. + +## On merge + +Promote into `architecture/`: the schema-validation / drift behavior is +described in `CLAUDE.md`'s "User-owned schema" section (and any +`architecture/` deep-dive that covers `validate_schema`). Add a sentence noting +that an Alembic-blind drift error now carries a remediation pointer to +`docs/operations/alembic.md#fixing-drift-autogenerate-cant-see`. \ No newline at end of file From e0a4b1db572700dd80c308667322682e7af1c2b8 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 16 Jun 2026 19:04:03 +0300 Subject: [PATCH 2/4] docs(planning): implementation plan for actionable schema-drift error Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plan.md | 407 ++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100644 planning/changes/active/2026-06-16.01-actionable-schema-drift-error/plan.md diff --git a/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/plan.md b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/plan.md new file mode 100644 index 0000000..1a6ca34 --- /dev/null +++ b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/plan.md @@ -0,0 +1,407 @@ +--- +status: draft +date: 2026-06-16 +slug: actionable-schema-drift-error +spec: actionable-schema-drift-error +pr: null +--- + +# actionable-schema-drift-error — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use +> superpowers:subagent-driven-development (recommended) or +> superpowers:executing-plans to implement this plan task-by-task. Steps +> use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `validate_schema()`'s `RuntimeError` point operators to a +hand-written-migration recipe whenever it reports drift that +`alembic revision --autogenerate` cannot remediate (the `lease_ck` CHECK and the +partial-index predicates). + +**Spec:** [`design.md`](./design.md) + +**Branch:** `fix/actionable-schema-drift-error` + +**Commit strategy:** Per-task commits. + +**Tooling notes for the executor:** +- Pure-helper unit tests run with no Postgres: `uv run pytest tests/test_unit.py -k `. +- Integration tests need Postgres via docker compose: `just test tests/test_integration.py -k `. + Pass a single `-k` keyword — `just test -k "a or b"` word-splits and fails. +- Coverage gate is `--cov-fail-under=100` on the full `just test` run; partial + runs fail it, so use `--no-cov` while iterating and rely on the final full run. +- All imports go at module top — never inline (project convention). Type every + new test parameter, including fixtures. + +--- + +### Task 1: Add the pure message-composition helper (TDD) + +**Files:** +- Modify: `faststream_outbox/client.py` +- Test: `tests/test_unit.py` + +Introduce `_compose_schema_mismatch_message` plus the `_SCHEMA_MISMATCH_PREFIX` +and `_AUTOGEN_BLIND_HINT` constants. Pure function, fully unit-testable without +Postgres. + +- [ ] **Step 1: Write the failing tests** + + Add to `tests/test_unit.py`. Extend the existing client import on line 42 + (`from faststream_outbox.client import OutboxClient, _validate_schema_sync`) to + also import the new names: + + ```python + from faststream_outbox.client import ( + OutboxClient, + _AUTOGEN_BLIND_HINT, + _SCHEMA_MISMATCH_PREFIX, + _compose_schema_mismatch_message, + _validate_schema_sync, + ) + ``` + + Then add these three tests (place them near the other client unit tests): + + ```python + def test_compose_schema_mismatch_message_appends_hint_on_blind_drift() -> None: + msg = _compose_schema_mismatch_message( + ["missing CHECK constraint 'outbox_lease_ck' (expected '...')"], + has_blind_drift=True, + ) + assert msg.startswith(_SCHEMA_MISMATCH_PREFIX) + assert _AUTOGEN_BLIND_HINT in msg + assert "#fixing-drift-autogenerate-cant-see" in msg + + + def test_compose_schema_mismatch_message_omits_hint_without_blind_drift() -> None: + msg = _compose_schema_mismatch_message( + ["table 'outbox' missing column 'headers'"], + has_blind_drift=False, + ) + assert msg == _SCHEMA_MISMATCH_PREFIX + "table 'outbox' missing column 'headers'" + assert _AUTOGEN_BLIND_HINT not in msg + + + def test_compose_schema_mismatch_message_joins_multiple_errors() -> None: + msg = _compose_schema_mismatch_message(["a", "b"], has_blind_drift=False) + assert msg == _SCHEMA_MISMATCH_PREFIX + "a; b" + ``` + +- [ ] **Step 2: Run the tests to verify they fail** + + Run: `uv run pytest tests/test_unit.py -k compose_schema_mismatch --no-cov -v` + Expected: collection/import error or FAIL — `_compose_schema_mismatch_message` + (and the two constants) do not exist yet. + +- [ ] **Step 3: Implement the helper in `client.py`** + + Add the following at module scope in `faststream_outbox/client.py`, immediately + after the `_validate_check_constraints_sync` function (after the block ending at + the current line 576), so all schema-validation machinery stays grouped: + + ```python + # The published docs anchor for hand-written migrations that fix drift + # `alembic revision --autogenerate` cannot emit (no check-constraint comparator; + # the index comparator ignores postgresql_where). Appended to the RuntimeError + # only when an Alembic-blind probe actually fired — see validate_schema(). + _SCHEMA_MISMATCH_PREFIX = "Outbox schema mismatch: " + _AUTOGEN_BLIND_HINT = ( + "These (CHECK constraints and partial-index predicates) are invisible to " + "'alembic revision --autogenerate' — hand-write the migration: " + "https://faststream-outbox.modern-python.org/operations/alembic/" + "#fixing-drift-autogenerate-cant-see" + ) + + + def _compose_schema_mismatch_message(errors: list[str], *, has_blind_drift: bool) -> str: + """Build the validate_schema RuntimeError text; append the remediation pointer for Alembic-blind drift.""" + msg = _SCHEMA_MISMATCH_PREFIX + "; ".join(errors) + if has_blind_drift: + msg += "\n\n" + _AUTOGEN_BLIND_HINT + return msg + ``` + +- [ ] **Step 4: Run the tests to verify they pass** + + Run: `uv run pytest tests/test_unit.py -k compose_schema_mismatch --no-cov -v` + Expected: 3 passed. + +- [ ] **Step 5: Commit** + + ```bash + git add faststream_outbox/client.py tests/test_unit.py + git commit -m "feat(client): add schema-mismatch message composer with autogen-blind hint + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 2: Wire `validate_schema()` to the helper and assert the pointer in integration tests + +**Files:** +- Modify: `faststream_outbox/client.py:401-425` (the `validate_schema` method) +- Test: `tests/test_integration.py` + +Track the Alembic-blind probe errors separately and raise through the new helper. +Cover the present/absent pointer behaviour against real Postgres. + +- [ ] **Step 1: Update the integration tests (failing assertions first)** + + In `tests/test_integration.py`, edit + `test_validate_schema_fails_when_lease_check_constraint_missing` (currently + lines 1919-1929) to capture the exception and assert the pointer: + + ```python + async def test_validate_schema_fails_when_lease_check_constraint_missing( + pg_engine: AsyncEngine, + outbox_table: Table, + ) -> None: + """A dropped ``
_lease_ck`` CHECK must be caught — alembic's diff can't see it (audit 2026-06-14).""" + drop_sql = f'ALTER TABLE "{outbox_table.name}" DROP CONSTRAINT "{outbox_table.name}_lease_ck"' + async with pg_engine.begin() as conn: + await conn.exec_driver_sql(drop_sql) + client = OutboxClient(pg_engine, outbox_table) + with pytest.raises(RuntimeError, match="missing CHECK constraint") as excinfo: + await client.validate_schema() + assert "operations/alembic/#fixing-drift-autogenerate-cant-see" in str(excinfo.value) + ``` + + And edit `test_validate_schema_fails_when_columns_missing` (currently lines + 454-461) to assert the pointer is **absent** for autogenerate-fixable drift: + + ```python + async def test_validate_schema_fails_when_columns_missing(pg_engine, outbox_table) -> None: + """Drop a column the package expects and verify validate_schema reports it.""" + drop_sql = f'ALTER TABLE "{outbox_table.name}" DROP COLUMN headers' + async with pg_engine.begin() as conn: + await conn.exec_driver_sql(drop_sql) + client = OutboxClient(pg_engine, outbox_table) + with pytest.raises(RuntimeError, match="missing column 'headers'") as excinfo: + await client.validate_schema() + assert "fixing-drift-autogenerate-cant-see" not in str(excinfo.value) + ``` + +- [ ] **Step 2: Run the two tests to verify the new assertions fail** + + Run: `just test tests/test_integration.py -k test_validate_schema_fails_when_lease_check_constraint_missing` + Expected: FAIL — the current message has no pointer, so the new + `assert ... in str(excinfo.value)` fails. + (The columns-missing test still passes at this point — the pointer is already + absent — but its new negative assertion guards Step 3.) + +- [ ] **Step 3: Rewire `validate_schema` in `client.py`** + + Replace the body of `validate_schema` (lines 410-425) — keep the docstring + above it unchanged. Old: + + ```python + async with self._engine.connect() as conn: + errors = await conn.run_sync(_validate_schema_sync, self._table) + # S2: alembic's autogenerate diff compares index columns + uniqueness but NOT + # the partial-index WHERE predicate, so a wrong postgresql_where slips through + # and later breaks the producer's ON CONFLICT arbiter. Probe the predicates + # directly against the live catalog. + errors.extend(await conn.run_sync(_validate_index_predicates_sync, self._table)) + # Alembic's compare_metadata has no check-constraint comparator, so a missing + # or altered
_lease_ck (the half-set-lease guard) passes the diff above + # silently. Probe pg_constraint directly, mirroring the partial-index probe. + errors.extend(await conn.run_sync(_validate_check_constraints_sync, self._table)) + if self._dlq_table is not None: + errors.extend(await conn.run_sync(_validate_dlq_schema_sync, self._dlq_table)) + if errors: + msg = "Outbox schema mismatch: " + "; ".join(errors) + raise RuntimeError(msg) + ``` + + New: + + ```python + async with self._engine.connect() as conn: + errors = await conn.run_sync(_validate_schema_sync, self._table) + # S2 / lease_ck: these two probes catch drift that `alembic revision + # --autogenerate` cannot remediate — its index comparator ignores + # postgresql_where and it has no check-constraint comparator at all. + # Collect them separately so the raised error can point operators at + # the hand-written-migration recipe (_AUTOGEN_BLIND_HINT) only when + # one of them actually fired. + blind_errors = await conn.run_sync(_validate_index_predicates_sync, self._table) + blind_errors.extend(await conn.run_sync(_validate_check_constraints_sync, self._table)) + errors.extend(blind_errors) + if self._dlq_table is not None: + errors.extend(await conn.run_sync(_validate_dlq_schema_sync, self._dlq_table)) + if errors: + raise RuntimeError( + _compose_schema_mismatch_message(errors, has_blind_drift=bool(blind_errors)), + ) + ``` + +- [ ] **Step 4: Run the affected integration tests to verify they pass** + + Run each separately (single `-k` keyword each): + - `just test tests/test_integration.py -k test_validate_schema_fails_when_lease_check_constraint_missing` + - `just test tests/test_integration.py -k test_validate_schema_fails_when_columns_missing` + - `just test tests/test_integration.py -k test_validate_schema_fails_when_lease_check_constraint_predicate_wrong` + Expected: PASS for each. The third confirms the predicate-drift path also still + raises (it now flows through the helper with `has_blind_drift=True`). + +- [ ] **Step 5: Commit** + + ```bash + git add faststream_outbox/client.py tests/test_integration.py + git commit -m "fix(client): point operators at the migration recipe on autogen-blind drift + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 3: Document the hand-written-migration recipe + +**Files:** +- Modify: `docs/operations/alembic.md` +- Modify: `docs/usage/schema-validation.md` + +Add the anchored section the error points to, and cross-link it from the schema +validation page. + +- [ ] **Step 1: Add the recipe section to `docs/operations/alembic.md`** + + Insert the following **between** the end of the "Drift detection in CI" section + (after the server-defaults paragraph that ends at line 158) and the + `## DLQ retention via partition drop { #dlq-retention-via-partition-drop }` + heading (line 160): + + ````markdown + ## Fixing drift autogenerate can't see { #fixing-drift-autogenerate-cant-see } + + Two kinds of drift that + [`validate_schema()`](../usage/schema-validation.md) reports **cannot** be + remediated by `alembic revision --autogenerate` — the same blindness that + let them drift in also stops autogenerate from emitting a fix: + + - **The `outbox_lease_ck` CHECK constraint.** Alembic's `compare_metadata` + has no check-constraint comparator, so a missing or altered CHECK never + appears in an autogenerated migration. + - **Partial-index predicates.** Alembic's index comparator ignores + `postgresql_where`, so an index that exists but was created non-partial, + with the wrong `WHERE`, or (for `outbox_timer_id_uq`) non-unique is + invisible to the diff. + + When `validate_schema()` raises for one of these, its error ends with a + pointer to this section. Re-running autogenerate produces an empty + `upgrade()` — hand-write the migration instead, then re-run + `validate_schema()` to confirm the drift is cleared. + + ### Restore the lease CHECK + + ```python + # Drop first ONLY if the constraint exists with a wrong predicate; skip the + # drop if it is absent entirely. + op.drop_constraint('outbox_lease_ck', 'outbox', type_='check') + op.create_check_constraint( + 'outbox_lease_ck', + 'outbox', + '(acquired_token IS NULL) = (acquired_at IS NULL)', + ) + ``` + + ### Restore a partial index + + Drop the drifted index and recreate it with its load-bearing predicate. The + three indexes and their expected shape: + + | Index | Columns | Unique | `postgresql_where` | + | --- | --- | --- | --- | + | `outbox_pending_idx` | `queue, next_attempt_at` | no | `acquired_token IS NULL` | + | `outbox_lease_idx` | `queue, acquired_at` | no | `acquired_token IS NOT NULL` | + | `outbox_timer_id_uq` | `queue, timer_id` | yes | `timer_id IS NOT NULL` | + + ```python + op.drop_index('outbox_timer_id_uq', table_name='outbox') + op.create_index( + 'outbox_timer_id_uq', + 'outbox', + ['queue', 'timer_id'], + unique=True, + postgresql_where=sa.text('timer_id IS NOT NULL'), + ) + ``` + + Substitute the columns / `unique` / predicate from the table above for + `outbox_pending_idx` and `outbox_lease_idx`. + ```` + +- [ ] **Step 2: Cross-link from `docs/usage/schema-validation.md`** + + Insert this paragraph immediately after the "Extras are intentionally ignored" + paragraph (after line 39, before the `!!! warning "Server defaults..."` + admonition): + + ```markdown + Some drift cannot be fixed by re-running `alembic revision --autogenerate` — a + missing/altered `outbox_lease_ck` CHECK or a drifted partial-index predicate. + For those, the `RuntimeError` ends with a pointer to + [Alembic migrations § Fixing drift autogenerate can't see](../operations/alembic.md#fixing-drift-autogenerate-cant-see), + which holds the hand-written migration recipe. + ``` + +- [ ] **Step 3: Build the docs strict to verify anchors resolve** + + Run: `just docs-build` + Expected: `mkdocs build --strict` succeeds with no warnings — the new + `#fixing-drift-autogenerate-cant-see` anchor resolves and the cross-link from + `schema-validation.md` does not 404. + +- [ ] **Step 4: Commit** + + ```bash + git add docs/operations/alembic.md docs/usage/schema-validation.md + git commit -m "docs: hand-written migration recipe for autogen-blind drift + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +### Task 4: Full verification + +**Files:** none (verification only). + +Confirm lint, the full test suite, and the 100% coverage gate all pass with the +changes in place. + +- [ ] **Step 1: Lint** + + Run: `just lint-ci` + Expected: clean — `eof-fixer`, `ruff format --check`, `ruff check`, `ty check` + all pass. (If `_compose_schema_mismatch_message` or the constants trip an unused + import in `test_unit.py`, fix the import list rather than suppressing.) + +- [ ] **Step 2: Full test suite with coverage gate** + + Run: `just test` + Expected: all tests pass and `--cov-fail-under=100` is satisfied — the helper's + three branches are covered by the Task 1 unit tests; the `has_blind_drift` True + and False paths in `validate_schema` are covered by the Task 2 integration tests. + +- [ ] **Step 3: Commit (only if Step 1/2 required fixups)** + + ```bash + git add -A + git commit -m "chore: lint/coverage fixups for autogen-blind drift hint + + Co-Authored-By: Claude Opus 4.8 (1M context) " + ``` + +--- + +## On merge + +Move this bundle to `planning/changes/archive/` with `status: shipped`, `pr:`, +and `outcome:` filled, and promote the conclusion into the affected +architecture record: note in `CLAUDE.md`'s "User-owned schema" section (and any +`architecture/` deep-dive covering `validate_schema`) that an Alembic-blind +drift error now carries a remediation pointer to +`docs/operations/alembic.md#fixing-drift-autogenerate-cant-see`. From 71dd1fe38b2216e462f8f97cd173dbb1ae452d2f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 16 Jun 2026 19:35:25 +0300 Subject: [PATCH 3/4] docs: adopt org logo, favicon, and brand palette Match modern-python.org: white wordmark logo top-left, mark favicon, forest-green Material palette. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/assets/modern-python-mark.svg | 1 + docs/assets/modern-python-white.svg | 1 + docs/css/brand.css | 17 +++++++++++++++++ mkdocs.yml | 11 +++++++---- 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 docs/assets/modern-python-mark.svg create mode 100644 docs/assets/modern-python-white.svg create mode 100644 docs/css/brand.css diff --git a/docs/assets/modern-python-mark.svg b/docs/assets/modern-python-mark.svg new file mode 100644 index 0000000..e441c2a --- /dev/null +++ b/docs/assets/modern-python-mark.svg @@ -0,0 +1 @@ + diff --git a/docs/assets/modern-python-white.svg b/docs/assets/modern-python-white.svg new file mode 100644 index 0000000..77b32d8 --- /dev/null +++ b/docs/assets/modern-python-white.svg @@ -0,0 +1 @@ + diff --git a/docs/css/brand.css b/docs/css/brand.css new file mode 100644 index 0000000..5c95b6f --- /dev/null +++ b/docs/css/brand.css @@ -0,0 +1,17 @@ +/* Brand palette — forest green sampled from the org logo (#356852) */ +:root > * { + --md-primary-fg-color: #356852; + --md-primary-fg-color--light: #4a8a6e; + --md-primary-fg-color--dark: #234738; + --md-accent-fg-color: #c98a00; +} + +/* Dark scheme keeps the same brand hue; lighter green for body links so they + stay AA-readable on the dark background. */ +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #356852; + --md-primary-fg-color--light: #4a8a6e; + --md-primary-fg-color--dark: #234738; + --md-accent-fg-color: #e0a300; + --md-typeset-a-color: #7fb79f; +} diff --git a/mkdocs.yml b/mkdocs.yml index c677854..4b3bdab 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,8 @@ nav: - Alembic migrations: operations/alembic.md theme: name: material + logo: assets/modern-python-white.svg + favicon: assets/modern-python-mark.svg features: - content.code.copy - content.code.annotate @@ -52,15 +54,15 @@ theme: palette: - media: "(prefers-color-scheme: light)" scheme: default - primary: black - accent: pink + primary: custom + accent: custom toggle: icon: material/brightness-7 name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate - primary: black - accent: pink + primary: custom + accent: custom toggle: icon: material/brightness-4 name: Switch to system preference @@ -86,6 +88,7 @@ markdown_extensions: extra_css: - css/code.css + - css/brand.css extra: social: From b3ff01111a647ffd3a74d1bed7f97e04baef0d03 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Tue, 16 Jun 2026 19:46:09 +0300 Subject: [PATCH 4/4] chore: add missing EOF newline to schema-drift design doc Pre-existing eof-fixer lint failure on main, surfaced by CI on this PR. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-16.01-actionable-schema-drift-error/design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md index 2e5b9e5..786c124 100644 --- a/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md +++ b/planning/changes/active/2026-06-16.01-actionable-schema-drift-error/design.md @@ -191,4 +191,4 @@ Promote into `architecture/`: the schema-validation / drift behavior is described in `CLAUDE.md`'s "User-owned schema" section (and any `architecture/` deep-dive that covers `validate_schema`). Add a sentence noting that an Alembic-blind drift error now carries a remediation pointer to -`docs/operations/alembic.md#fixing-drift-autogenerate-cant-see`. \ No newline at end of file +`docs/operations/alembic.md#fixing-drift-autogenerate-cant-see`.