From e555c68983cb5009b16b1510e6622ec3cf6d457f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Wed, 15 Jul 2026 19:23:02 +0300 Subject: [PATCH] =?UTF-8?q?test:=20replace=20docs-slug=20census=20count=20?= =?UTF-8?q?pin=20with=20slug=E2=86=94page=20bijection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The count pin `_EXPECTED_CONCRETE_CLASS_COUNT = 22` fired on any error-class add/remove even when slug and page were supplied correctly, friction with no behavioral cause. Fold the class→page test into a single slug↔page bijection: set equality reports missing pages and orphan pages separately, preserves the narrowing-walk guard (fewer slugs than pages fails), and adds orphan-page coverage the census lacked. No magic constant to hand-bump. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../changes/2026-07-15.04-census-bijection.md | 76 +++++++++++++++++++ tests/test_docs_slug_census.py | 31 +++----- 2 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 planning/changes/2026-07-15.04-census-bijection.md diff --git a/planning/changes/2026-07-15.04-census-bijection.md b/planning/changes/2026-07-15.04-census-bijection.md new file mode 100644 index 0000000..16410fe --- /dev/null +++ b/planning/changes/2026-07-15.04-census-bijection.md @@ -0,0 +1,76 @@ +--- +summary: Replace the docs-slug census count pin with a slug↔page bijection test. +--- + +# Design: Census bijection + + + +## 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/.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. diff --git a/tests/test_docs_slug_census.py b/tests/test_docs_slug_census.py index 3043d76..6ae3d1c 100644 --- a/tests/test_docs_slug_census.py +++ b/tests/test_docs_slug_census.py @@ -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/.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/.md` — the full completeness gate. +See planning/changes/2026-07-07.06-error-docs-registry.md. """ import pathlib @@ -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, @@ -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/.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