Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions planning/changes/2026-07-15.04-census-bijection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
summary: Replace the docs-slug census count pin with a slug↔page bijection test.
---

# Design: Census bijection

<!-- A grounded cleanup noticed while reviewing the error-docs census. Test-only. -->

## Summary

In `tests/test_docs_slug_census.py`, drop the hand-maintained
`_EXPECTED_CONCRETE_CLASS_COUNT = 22` pin and fold the class→page test into a
single slug↔page **bijection** test.

A second candidate — "test import fragility" — was investigated and **dropped**;
see Non-goals.

## Motivation

The census already has two real completeness gates: every concrete
`ModernDIError` subclass has a unique, non-`None` `docs_slug`, and every slug
has a `docs/troubleshooting/<slug>.md` page. The count pin adds a third
assertion that fires on *any* class add/remove even when slug and page are
supplied correctly — friction on valid changes with no behavioral cause. Its
one genuine value is catching a *narrowing walk* (a filter regression that
finds fewer classes, letting the skipped ones escape the slug/page gates); the
existing non-empty check only catches the walk finding *zero*.

## Design

The troubleshooting directory is a clean bijection today: 22 pages, one per
slug, no index or extra files. Replace the pin and the old class→page test with
one set-equality test that reports both directions for readable failures:

```python
def test_slugs_and_troubleshooting_pages_are_in_bijection() -> None:
slugs = {cls.docs_slug for cls in _concrete_error_classes() if cls.docs_slug}
pages = {p.stem for p in _TROUBLESHOOTING_DIR.glob("*.md")}
missing_pages = sorted(slugs - pages) # slug with no page; also fires if the walk narrows
orphan_pages = sorted(pages - slugs) # page with no matching error class (new coverage)
assert not missing_pages, f"docs_slug values with no matching page: {missing_pages}"
assert not orphan_pages, f"troubleshooting pages with no matching error class: {orphan_pages}"
```

- Delete `_EXPECTED_CONCRETE_CLASS_COUNT`, `test_census_pins_the_concrete_class_count`,
and `test_every_docs_slug_has_a_troubleshooting_page` (subsumed).
- Keep `test_every_concrete_error_has_a_unique_docs_slug` (uniqueness, non-`None`,
non-empty) and `test_base_classes_have_no_docs_slug` untouched.
- Update the module docstring to describe the bijection, dropping the "22" reference.

Net: magic constant gone, no hand-bump on valid add/remove, narrowing-walk guard
preserved (fewer slugs than pages → mismatch), orphan-page coverage added.

## Non-goals

- **Glossary backlinks dropped from this change.** Adding reverse
capability→glossary links was considered alongside this and cut; this change
is test-only.
- **"Test import fragility" dropped.** The flagged tests use `from modern_di
import suggester` (and explicit `from modern_di.dependency_graph import ...`);
Python's from-import falls back to loading the submodule, so these resolve
reliably regardless of import order or `__all__`. Verified: `from modern_di
import types_parser` works in a fresh interpreter though `__init__` never
touches it. No latent break — nothing to fix.

## Testing

`just test tests/test_docs_slug_census.py` passes (four tests → three: the two
kept plus the new bijection). `just test-ci` stays green at 100% coverage.

## Risk

Low. Test-only, no behavior change, no `architecture/` promotion. The bijection
is enforceable now because the directory is an exact 22↔22 match; if a
legitimately non-class troubleshooting page (an index/overview) is ever added,
the orphan-page assertion must be updated to exclude it.
31 changes: 10 additions & 21 deletions tests/test_docs_slug_census.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Census of docs_slug coverage across modern_di.exceptions (ERR-4/DOC-6).

Every concrete `ModernDIError` subclass sets a unique `docs_slug`, and every slug has a
matching page under `docs/troubleshooting/<slug>.md` — the full completeness gate. See
planning/changes/2026-07-07.06-error-docs-registry.md.
Every concrete `ModernDIError` subclass sets a unique `docs_slug`, and the slugs stand in a
bijection with the pages under `docs/troubleshooting/<slug>.md` — the full completeness gate.
See planning/changes/2026-07-07.06-error-docs-registry.md.
"""

import pathlib
Expand All @@ -13,10 +13,6 @@
_REPO_ROOT = pathlib.Path(__file__).parent.parent
_TROUBLESHOOTING_DIR = _REPO_ROOT / "docs" / "troubleshooting"

# The 5 pre-existing troubleshooting pages, the 16 added by the error-docs-registry change,
# and GroupScopeConflictError (API-4).
_EXPECTED_CONCRETE_CLASS_COUNT = 22

_BASE_CLASSES = frozenset(
{
exceptions.ModernDIError,
Expand Down Expand Up @@ -47,23 +43,16 @@ def test_every_concrete_error_has_a_unique_docs_slug() -> None:
assert len(slugs) == len(set(slugs)), "docs_slug values must be unique across all concrete error classes"


def test_every_docs_slug_has_a_troubleshooting_page() -> None:
missing = sorted(
cls.docs_slug
for cls in _concrete_error_classes()
if cls.docs_slug and not (_TROUBLESHOOTING_DIR / f"{cls.docs_slug}.md").exists()
)
assert not missing, f"docs_slug values with no matching docs/troubleshooting/<slug>.md page: {missing}"
def test_slugs_and_troubleshooting_pages_are_in_bijection() -> None:
slugs = {cls.docs_slug for cls in _concrete_error_classes() if cls.docs_slug}
pages = {p.stem for p in _TROUBLESHOOTING_DIR.glob("*.md")}
missing_pages = sorted(slugs - pages) # slug with no page; also fires if the walk narrows
orphan_pages = sorted(pages - slugs) # page with no matching error class
assert not missing_pages, f"docs_slug values with no matching page: {missing_pages}"
assert not orphan_pages, f"troubleshooting pages with no matching error class: {orphan_pages}"


def test_base_classes_have_no_docs_slug() -> None:
# Base classes are never raised directly and never get a troubleshooting page.
for base in _BASE_CLASSES:
assert base.docs_slug is None


def test_census_pins_the_concrete_class_count() -> None:
# Guards against a class silently falling out of (or into) the walk above — if this count
# changes, a class was added/removed and the slug table (and, eventually, its page) must
# follow in the same change.
assert len(_concrete_error_classes()) == _EXPECTED_CONCRETE_CLASS_COUNT