From 28a4d3edc4751b365772d33497701ac4e93506d5 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:24:50 +0300 Subject: [PATCH 1/6] docs: add planning bundle for docs CI drift guard Full-lane spec (design.md + plan.md) plus a decision recording why doc code-fence validation is deliberately out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../design.md | 151 ++++++++++++++ .../2026-07-05.01-docs-ci-drift-guard/plan.md | 194 ++++++++++++++++++ ...2026-07-05-no-doc-code-fence-validation.md | 58 ++++++ 3 files changed, 403 insertions(+) create mode 100644 planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md create mode 100644 planning/changes/2026-07-05.01-docs-ci-drift-guard/plan.md create mode 100644 planning/decisions/2026-07-05-no-doc-code-fence-validation.md diff --git a/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md b/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md new file mode 100644 index 0000000..b8e5535 --- /dev/null +++ b/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md @@ -0,0 +1,151 @@ +--- +summary: Make mkdocs --strict a PR gate (parallel docs job) with native link/anchor validation, pin docs deps, fix the make_outbox_table 63-byte docstring, and one-time-audit README. +--- + +# Design: Docs CI drift guard + +## Summary + +Add a PR-time guard against the class of documentation drift that a 2026-07-05 +docs audit (`docs/audit-drift-fixes`, PR #125) surfaced. Today `mkdocs build +--strict` runs **only** in `docs.yml` on push to `main` (for deploy), so a PR +that breaks an internal link or orphans a page is not caught until after merge; +broken `#anchor` fragments are not caught at all. This change makes the strict +build a parallel PR check, turns on mkdocs' native link/anchor validation, pins +the (currently unpinned) docs toolchain, fixes one source docstring that +carries the same wording bug the audit fixed in the docs, and does a one-time +correctness pass over `README.md` (which lives outside `docs_dir` and is +therefore invisible to mkdocs). + +Deliberately **excluded**: any validation of the Python inside `docs/` code +fences (syntax-parse, type-check, or execute). That was considered and rejected +for cost / false-positive reasons — recorded in +[`../../decisions/2026-07-05-no-doc-code-fence-validation.md`](../../decisions/2026-07-05-no-doc-code-fence-validation.md). + +## Motivation + +The docs audit (PR #125) found, among ~20 fixes: three broken code samples, +a migration snippet missing a load-bearing CHECK, several stale numbers, and a +set of `#anchor` links that happened to resolve. CI caught **none** of it, +because: + +- `mkdocs build --strict` — which fails on broken internal links and orphaned + pages — is not part of the PR `checks` workflow (`_checks.yml`). It runs only + in `docs.yml`, which triggers on push to `main` with `paths: docs/**`. So + link/orphan breakage fails the **deploy** after merge, not the PR. +- mkdocs does not validate `#anchor` fragments by default; the audit relied on a + throwaway script to check them. +- `docs/requirements.txt` pins nothing (`mkdocs`, `mkdocs-material`). The + audit's strict build already emitted the upstream "MkDocs 2.0 will break all + plugins/themes" warning — an unpinned major bump would break the build with no + notice. + +The cheapest durable guards are all in-tool: promote the existing strict build +to a PR gate and switch on mkdocs' own link/anchor validation. No new script to +own. + +## Non-goals + +- No validation of Python code fences in `docs/` (syntax/type/execute) — see the + decision file. This is the single biggest scope cut and the reason the change + stays small. +- No ongoing link-checking of `README.md`. Its links are predominantly stable + external badges/URLs; an external link-checker is flaky and low-value. README + is fixed once here (§4) and not added to a recurring guard. +- No change to what `docs.yml` does at deploy time; the PR job simply runs the + same command earlier. + +## Design + +### 1. Strict docs build as a parallel PR job + +Add a `docs` job to `.github/workflows/_checks.yml` (the reusable workflow that +`ci.yml` calls on every PR and push to `main`). It runs the **existing** +recipe, identical to the deploy build: + +```yaml + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 + - run: just docs-build +``` + +Parallel with `lint` and `pytest`; isolated from the lint runner; the same +`just docs-build` command PR-time and deploy-time, so they cannot diverge. + +### 2. Native link + anchor validation in mkdocs + +Add a `validation:` block to `mkdocs.yml` (supported since mkdocs 1.6; the repo +resolves 1.6.1): + +```yaml +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + anchors: warn +``` + +Under `--strict`, every `warn` is promoted to an error, so a broken `#anchor` +fragment or an unrecognized internal link now fails the build. This subsumes the +audit's throwaway anchor script with zero maintained code. + +### 3. `make_outbox_table` docstring fix (folds in #2) + +`faststream_outbox/schema.py`'s `make_outbox_table` docstring says it raises +`ValueError` when `table_name` pushes the NOTIFY channel `outbox_` +past Postgres' 63-byte limit. The guard (`validate_table_identifiers`) actually +binds on the **longest derived identifier** — an index/constraint name such as +`_pending_idx` / `_timer_id_uq`, which is longer than +the channel prefix. This is the exact wording the audit corrected in +`docs/operations/checklist.md`; fixing the docstring closes it at the source so +docs and code agree. One-line prose change; no behavior change. + +### 4. One-time README audit (folds in #3) + +`README.md` is outside `docs_dir`, so neither the strict build nor mkdocs +validation ever touches it. Do a one-time correctness/consistency pass with the +same rigor as the docs audit: verify every code sample's imports/symbols/kwargs +against source, check stated numbers/claims, and check links. Fix confident +drift in place; surface any judgment calls. Findings are recorded inline in the +implementing PR (a full `planning/audits/` report is overkill for a single +file); if the pass turns up something structural, it spawns its own change. + +### 5. Pin the docs toolchain + +`docs/requirements.txt` gets upper-bounded pins so the strict build (now a PR +gate) is reproducible and immune to a breaking major: + +``` +mkdocs>=1.6,<2 +mkdocs-material>=9,<10 +``` + +(Exact lower bound for `mkdocs-material` set to the resolved version's series at +implementation time.) + +## Testing + +- **Negative-to-positive on the guard:** on the branch, temporarily introduce a + broken `#anchor` link and a broken relative `.md` link in a docs page and + confirm `just docs-build` now **fails**; revert. Confirm a clean tree + **passes**. This proves §1+§2 actually gate. +- **README:** re-run the audit checks (imports/symbols resolve, links resolve) + after fixes; `git diff` review. +- **Docstring:** `just lint-ci` (ruff + ty) stays green; no runtime surface. +- **Planning:** `just check-planning` validates this bundle + the decision file. +- **Full CI on the PR:** lint, the new docs job, and the pytest matrix all green. + +## Risk + +- **Low — new PR gate turns red on pre-existing latent breakage.** If any + current docs link/anchor is already broken under the stricter validation, the + first run fails. Mitigation: PR #125 already left links/anchors clean; the + branch build is run before opening the PR. +- **Low — pinning drifts from upstream.** Upper bounds mean a manual bump to + adopt mkdocs-material 10 / mkdocs 2. Acceptable and intended (that is the + point of the pin); the bound documents the tested range. +- **Negligible — docstring/README are prose-only**, no behavior change. diff --git a/planning/changes/2026-07-05.01-docs-ci-drift-guard/plan.md b/planning/changes/2026-07-05.01-docs-ci-drift-guard/plan.md new file mode 100644 index 0000000..5f58c8a --- /dev/null +++ b/planning/changes/2026-07-05.01-docs-ci-drift-guard/plan.md @@ -0,0 +1,194 @@ +# docs-ci-drift-guard — 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 `mkdocs build --strict` a PR gate with native link/anchor +validation, pin the docs toolchain, fix the `make_outbox_table` 63-byte +docstring, and one-time-audit `README.md`. + +**Spec:** [`design.md`](./design.md) + +**Branch:** `chore/docs-ci-drift-guard` + +**Commit strategy:** Per-task commits; all ship in one PR alongside this bundle. + +--- + +### Task 1: Enable mkdocs link/anchor validation + +**Files:** +- Modify: `mkdocs.yml` + +Add the `validation:` block so `--strict` fails on broken internal links and +`#anchor` fragments. + +- [ ] **Step 1: Add the validation block** + + Insert a top-level `validation:` key (mkdocs 1.6+): + ```yaml + validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + anchors: warn + ``` + +- [ ] **Step 2: Prove it gates (negative test)** + + Temporarily append a broken anchor link (e.g. `[x](usage/basic.md#no-such-anchor)`) + to a docs page, run `just docs-build`, confirm it **fails** with an anchor + warning-as-error, then remove the temporary link. + +- [ ] **Step 3: Confirm clean tree passes** + + `just docs-build` → "Documentation built"; no warnings-as-errors. + +- [ ] **Step 4: Commit** + + ```bash + git add mkdocs.yml + git commit -m "docs: validate internal links and anchors under mkdocs --strict" + ``` + +--- + +### Task 2: Pin the docs toolchain + +**Files:** +- Modify: `docs/requirements.txt` + +Upper-bound the docs deps so the (now gating) strict build is reproducible. + +- [ ] **Step 1: Resolve current mkdocs-material series** + + `uvx --with-requirements docs/requirements.txt mkdocs --version` and note the + installed `mkdocs-material` major (for the lower bound). + +- [ ] **Step 2: Pin with upper bounds** + + ``` + mkdocs>=1.6,<2 + mkdocs-material>=,< + ``` + +- [ ] **Step 3: Verify build still passes on the pins** + + `just docs-build` → passes. + +- [ ] **Step 4: Commit** + + ```bash + git add docs/requirements.txt + git commit -m "docs: pin mkdocs and mkdocs-material with upper bounds" + ``` + +--- + +### Task 3: Add the strict docs build as a parallel PR job + +**Files:** +- Modify: `.github/workflows/_checks.yml` + +Add a `docs` job running `just docs-build`, parallel with `lint`/`pytest`. + +- [ ] **Step 1: Add the job** + + Mirror the `lint` job's setup steps; final step `- run: just docs-build`. + +- [ ] **Step 2: Sanity-check YAML** + + `uv run python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/_checks.yml'))"` + (or `just` if a lint target covers workflows) → no parse error. + +- [ ] **Step 3: Commit** + + ```bash + git add .github/workflows/_checks.yml + git commit -m "ci: run mkdocs --strict as a PR check" + ``` + +--- + +### Task 4: Fix the `make_outbox_table` docstring (#2) + +**Files:** +- Modify: `faststream_outbox/schema.py` + +Correct the 63-byte-guard wording: the binding identifier is the longest +derived index/constraint name, not the NOTIFY channel. + +- [ ] **Step 1: Edit the docstring** + + Reword the `Raises ValueError ...` line in `make_outbox_table` to say the + guard trips when the **longest derived identifier** (e.g. `_pending_idx`) + exceeds 63 bytes — longer than the `outbox_
` channel. + +- [ ] **Step 2: Lint stays green** + + `just lint-ci` → ruff + ty pass. + +- [ ] **Step 3: Commit** + + ```bash + git add faststream_outbox/schema.py + git commit -m "docs: correct make_outbox_table 63-byte guard docstring" + ``` + +--- + +### Task 5: One-time README audit (#3) + +**Files:** +- Modify: `README.md` (as findings require) + +Audit `README.md` against source + `docs/`; fix confident drift, surface +judgment calls. + +- [ ] **Step 1: Verify code samples** + + For every code block in `README.md`, confirm imports/symbols/kwargs resolve + against `faststream_outbox/` (grep `__init__.py` exports; check signatures). + +- [ ] **Step 2: Verify claims, numbers, links** + + Check stated defaults/numbers against source; check every link (badges, + docs-site links, GitHub URLs) resolves. + +- [ ] **Step 3: Apply fixes; surface judgment calls** + + Fix confident drift in place. Any ambiguous/design-choice item → raise to + maintainer, do not decide unilaterally. + +- [ ] **Step 4: Re-verify** + + Re-run the import/symbol/link checks on the edited README. + +- [ ] **Step 5: Commit** + + ```bash + git add README.md + git commit -m "docs: fix drift in README" + ``` + +--- + +### Task 6: Finalize the bundle and open the PR + +**Files:** +- Modify: `planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md` (finalize `summary:`) + +- [ ] **Step 1: Validate planning** + + `just check-planning` → passes (bundle + decision frontmatter, lane, spec link). + +- [ ] **Step 2: Full local gate** + + `just lint-ci` and `just docs-build` both green. + +- [ ] **Step 3: Push + open PR; watch CI** + + Push the branch, open the PR, and watch lint + the new `docs` job + the pytest + matrix to green. diff --git a/planning/decisions/2026-07-05-no-doc-code-fence-validation.md b/planning/decisions/2026-07-05-no-doc-code-fence-validation.md new file mode 100644 index 0000000..119a2e1 --- /dev/null +++ b/planning/decisions/2026-07-05-no-doc-code-fence-validation.md @@ -0,0 +1,58 @@ +--- +status: accepted +summary: Do not validate Python inside docs/ code fences (no syntax/type/execute check); rely on mkdocs --strict for links/anchors and human review for sample correctness. +supersedes: null +superseded_by: null +--- + +# No automated validation of docs code fences + +**Decision:** The docs CI guard (change `2026-07-05.01-docs-ci-drift-guard`) +will **not** parse, type-check, or execute the Python inside `docs/` code +fences. Code-sample correctness stays a human-review concern; CI guards only +links, anchors, and page structure via `mkdocs build --strict`. + +## Context + +The 2026-07-05 docs audit (PR #125) fixed three broken code samples, including +`docs/usage/testing.md` calling `pub.publish({...})` without the required +`session=` — valid syntax, a `TypeError` at runtime. When designing the CI +guard, three levels of fence validation were on the table: + +1. **Syntax-parse all fences** (AST) — cheap, but would not have caught the + `session=` bug (valid syntax) or stale numbers. +2. **Type-check curated fences** against the real API (assemble + `ty`) — would + catch the `session=` class, but requires a fragment-vs-complete convention + because most fences are intentional fragments (undefined `broker`, `...` + bodies, bare signatures) that a type-checker floods with false positives. +3. **Execute curated fences** (doctest-style) — highest fidelity, but needs a + harness, fixtures, mocks, and a Postgres service for integration samples. + +## Decision & rationale + +Rejected all three. The value/cost ratio is poor for this codebase: + +- The failure it targets is **rare** — the audit was the first docs sweep to + find broken samples, and PR #125 already fixed the backlog. +- Levels 2 and 3 both require a **fragment-vs-complete tagging convention** the + docs do not have. Retrofitting it across ~23 pages, then maintaining it, is + ongoing overhead on every future doc edit — exactly the "expensive" cost the + maintainer flagged when scoping this change. +- False positives on intentional fragments would train reviewers to ignore the + check, which is worse than no check. +- The cheap in-tool guards (`mkdocs --strict` for links/orphans + native + `validation.anchors`) cover the **structural** drift class at zero maintained + code, and human review already covers sample correctness on the rare edit. + +So the guard covers links/anchors/structure and stops there; sample correctness +is not automated. + +## Revisit trigger + +Reopen if **either**: + +- doc code samples break ≥3 times within a release cycle (the failure stops + being rare), at which point curated type-checking (level 2) earns its keep; or +- a clean fragment-vs-complete convention emerges naturally (e.g. an + `examples/` directory of runnable snippets the docs `--8<--` include), which + removes the false-positive obstacle and makes level 2 or 3 cheap to add. From c516b1eeb032715e16ac0ef12e7375072b586b9a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:25:40 +0300 Subject: [PATCH 2/6] docs: validate internal links and anchors under mkdocs --strict Co-Authored-By: Claude Opus 4.8 (1M context) --- mkdocs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 1fc3bd0..2e99dfb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,12 @@ theme: icon: material/brightness-4 name: Switch to system preference +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + anchors: warn + markdown_extensions: - admonition - toc: From 443da74bb8c4bef00afa10eda05fa7681bec1ed4 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:26:43 +0300 Subject: [PATCH 3/6] docs: pin mkdocs and mkdocs-material with upper bounds Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 9a8a4ca..cba3dda 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -mkdocs -mkdocs-material +mkdocs>=1.6,<2 +mkdocs-material>=9,<10 From fe85fcfcf3d966cb4439a13012b2be6a701494b5 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:27:34 +0300 Subject: [PATCH 4/6] ci: run mkdocs --strict as a PR check Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_checks.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index e961e7c..12a3f71 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -17,6 +17,14 @@ jobs: - run: uv python pin 3.13 - run: just install lint-ci + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: extractions/setup-just@v4 + - uses: astral-sh/setup-uv@v8.2.0 + - run: just docs-build + pytest: runs-on: ubuntu-latest strategy: From 909f2249b78cf29cb8e03eaac51dac06a6a013ba Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:30:06 +0300 Subject: [PATCH 5/6] docs: correct make_outbox_table 63-byte guard docstring The guard binds on the longest derived identifier (an index/constraint name like
_pending_idx), not the shorter NOTIFY channel outbox_
. Same wording fixed in docs/operations/checklist.md by the 2026-07-05 docs audit. Co-Authored-By: Claude Opus 4.8 (1M context) --- faststream_outbox/schema.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/faststream_outbox/schema.py b/faststream_outbox/schema.py index 3cfac7d..fc0992c 100644 --- a/faststream_outbox/schema.py +++ b/faststream_outbox/schema.py @@ -88,8 +88,12 @@ def make_outbox_table(metadata: "MetaData", table_name: str = "outbox") -> Table The user wires the returned table into their own SQLAlchemy ``MetaData`` so it is discovered by Alembic's autogenerate. They are responsible for the actual migration. - Raises ``ValueError`` if *table_name* would push the NOTIFY channel - ``outbox_`` past Postgres' 63-byte identifier limit (NAMEDATALEN-1). + Raises ``ValueError`` if *table_name* would push any derived identifier past + Postgres' 63-byte limit (NAMEDATALEN-1). The binding identifier is the longest + one derived — usually an index/constraint name (``_pending_idx``, + ``_timer_id_uq``), which is longer than the NOTIFY channel + ``outbox_``, so a name that fits the channel can still overflow an + index name. See ``validate_table_identifiers``. """ validate_table_identifiers(table_name) table = Table( From 5fdea830ee33ce1c606d4afbbb327c4b419c94c3 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sun, 5 Jul 2026 11:32:49 +0300 Subject: [PATCH 6/6] docs: finalize docs-ci-drift-guard bundle summary (README clean) Co-Authored-By: Claude Opus 4.8 (1M context) --- planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md b/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md index b8e5535..b035261 100644 --- a/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md +++ b/planning/changes/2026-07-05.01-docs-ci-drift-guard/design.md @@ -1,5 +1,5 @@ --- -summary: Make mkdocs --strict a PR gate (parallel docs job) with native link/anchor validation, pin docs deps, fix the make_outbox_table 63-byte docstring, and one-time-audit README. +summary: Made mkdocs --strict a PR gate (parallel docs job) with native link/anchor validation, pinned docs deps (<2 / <10), and fixed the make_outbox_table 63-byte docstring; README audit found no drift (clean). --- # Design: Docs CI drift guard