From 9ac1eee1913efade5f7aae3c4a478b93f3f91945 Mon Sep 17 00:00:00 2001 From: Azahari Zaman Date: Wed, 19 Aug 2026 14:24:50 +0800 Subject: [PATCH 1/4] feat(api): add register_all_shipped bootstrap helper --- paxman/__init__.py | 2 + paxman/api/bootstrap.py | 62 ++++++++++++++++++++++ tests/integration/test_bootstrap.py | 20 ++++++++ tests/unit/test_bootstrap.py | 79 +++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 paxman/api/bootstrap.py create mode 100644 tests/integration/test_bootstrap.py create mode 100644 tests/unit/test_bootstrap.py diff --git a/paxman/__init__.py b/paxman/__init__.py index cc35bb43..ff6e01dd 100644 --- a/paxman/__init__.py +++ b/paxman/__init__.py @@ -1,3 +1,4 @@ +from paxman.api.bootstrap import register_all_shipped from paxman.api.canonicalize import canonicalize from paxman.core.discovery import register_capability from paxman.core.errors import CapabilityError @@ -7,6 +8,7 @@ "CapabilityError", "canonicalize", "register_capability", + "register_all_shipped", "register_grammar", "register_rule", ] diff --git a/paxman/api/bootstrap.py b/paxman/api/bootstrap.py new file mode 100644 index 00000000..02d15dca --- /dev/null +++ b/paxman/api/bootstrap.py @@ -0,0 +1,62 @@ +"""Sanctioned bootstrap: register every shipped capability in one call.""" + +from __future__ import annotations + +from typing import Any + +from paxman.capabilities import ( + IP, + ISBN, + URL, + Country, + Currency, + Date, + Email, + Money, + Phone, + SIUnit, +) +from paxman.core.capability import Capability +from paxman.core.discovery import get_capability, register_capability +from paxman.core.errors import CapabilityError + +# Fixed, documented order (alphabetical by capability registry name) — +# bootstrap is deterministic. D2: literal tuple, no dynamic enumeration. +_SHIPPED: tuple[type[Capability[Any]], ...] = ( + Country, + Currency, + Date, + Email, + IP, + ISBN, + Money, + Phone, + SIUnit, + URL, +) + + +def register_all_shipped() -> tuple[str, ...]: + """Register every shipped capability not already registered. + + Idempotent by name: a capability already registered (including a + caller-registered subclass) is skipped, never overridden. Does not + freeze the registry — freezing still happens on the first + ``canonicalize()`` call. Raises ``CapabilityError`` if the registry is + already frozen and anything remains to register. + + Threading contract: complete registration — single-calls or this + helper — from a single thread before the first ``canonicalize()`` + call; post-freeze reads are safe from any thread. + + Returns: + Names newly registered, in call order. + """ + registered: list[str] = [] + for cls in _SHIPPED: + try: + get_capability(cls.name) + except CapabilityError: + register_capability(cls()) + registered.append(cls.name) + return tuple(registered) diff --git a/tests/integration/test_bootstrap.py b/tests/integration/test_bootstrap.py new file mode 100644 index 00000000..3de604b7 --- /dev/null +++ b/tests/integration/test_bootstrap.py @@ -0,0 +1,20 @@ +import pytest + +import paxman +from paxman.capabilities import Email +from paxman.core.discovery import reset_registry +from paxman.core.domain import Resolution + + +@pytest.mark.integration +def test_bootstrap_then_canonicalize_round_trip() -> None: + """register_all_shipped() is a complete bootstrap: pipeline resolves.""" + reset_registry() + try: + paxman.register_all_shipped() + contract = Email.create_contract() + result = paxman.canonicalize("user@Example.COM", contract) + assert result.status is Resolution.SUCCESS + assert result.canonicalized_value == "user@example.com" + finally: + reset_registry() diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py new file mode 100644 index 00000000..752c44cb --- /dev/null +++ b/tests/unit/test_bootstrap.py @@ -0,0 +1,79 @@ +"""Unit tests for the register_all_shipped bootstrap helper.""" + +from __future__ import annotations + +from collections.abc import Iterator + +import pytest + +import paxman +from paxman.capabilities import Email +from paxman.core.discovery import ( + get_capability, + is_registry_frozen, + register_capability, + reset_registry, +) + + +@pytest.fixture +def _clean_registry() -> Iterator[None]: + reset_registry() + yield + reset_registry() + + +@pytest.mark.unit +def test_registers_all_ten_shipped(_clean_registry) -> None: + names = paxman.register_all_shipped() + expected = ( + "country", + "currency", + "date", + "email", + "ip", + "isbn", + "money", + "phone", + "si_unit", + "url", + ) + assert names == expected + for name in expected: + assert get_capability(name).name == name + + +@pytest.mark.unit +def test_idempotent_second_call_registers_nothing(_clean_registry) -> None: + paxman.register_all_shipped() + assert paxman.register_all_shipped() == () + + +@pytest.mark.unit +def test_preserves_caller_registration(_clean_registry) -> None: + mine = Email() + register_capability(mine) + names = paxman.register_all_shipped() + assert "email" not in names + assert len(names) == 9 + assert get_capability("email") is mine + + +@pytest.mark.unit +def test_does_not_freeze_registry(_clean_registry) -> None: + paxman.register_all_shipped() + assert is_registry_frozen() is False + + +@pytest.mark.unit +def test_raises_after_freeze(_clean_registry) -> None: + """Natural freeze: one canonicalize() call freezes the registry; a later + bootstrap with anything left to register must surface the error.""" + register_capability(Email()) + contract = Email.create_contract() + paxman.canonicalize("user@example.com", contract) # freezes naturally + assert is_registry_frozen() is True + # "url" was never registered, so the helper still has work to do — it + # must raise (via register_capability's frozen check), never swallow. + with pytest.raises(paxman.CapabilityError): + paxman.register_all_shipped() From ad2e271d4908139e2a5b813cf4f6db44032a5413 Mon Sep 17 00:00:00 2001 From: Azahari Zaman Date: Wed, 19 Aug 2026 14:27:45 +0800 Subject: [PATCH 2/4] docs: document register_all_shipped and the registration threading contract --- ARCHITECTURE.md | 2 +- QUICKSTART.md | 8 +++++--- README.md | 8 +++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3584a697..96784c49 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -80,7 +80,7 @@ Each capability is a self-contained domain module that provides: - **Validation Rules** — semantic rules that validate the notation against authoritative specifications - **A Contract** — a user-facing configuration object that toggles grammars, excludes rules, and passes parameters -Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes at the start of each pipeline run, ensuring that the set of available capabilities is stable during execution. +Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes at the start of each pipeline run, ensuring that the set of available capabilities is stable during execution. The sanctioned bulk form is `paxman.register_all_shipped()`, which registers all ten shipped capabilities in fixed alphabetical order; registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call, after which reads are safe from any thread. ### Engine diff --git a/QUICKSTART.md b/QUICKSTART.md index 5cedebf2..28aaa268 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -18,10 +18,8 @@ pip install paxman import paxman from paxman.capabilities.Email.capability import EmailCapability from paxman.core.domain import Resolution -from paxman.core.discovery import register_capability -# Register the Email capability (once, before first use) -register_capability(EmailCapability()) +paxman.register_all_shipped() # once, before first use # Create a contract and canonicalize contract = EmailCapability.create_contract() @@ -32,6 +30,10 @@ if result.status == Resolution.SUCCESS: print(result.canonicalized_value) # "user@example.com" ``` +To register only what you need, call `register_capability(EmailCapability())` per capability. + +Registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call; post-freeze reads are safe from any thread. + That's it. Paxman recognized the email in your text, validated it against RFC 5322, and returned the lowercase canonical form. --- diff --git a/README.md b/README.md index 13251871..f414c5da 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,9 @@ pip install paxman ```python import paxman from paxman.capabilities import Email -from paxman.core.discovery import register_capability from paxman.core.domain import Resolution -# Register the Email capability (once, before first use) -register_capability(Email()) +paxman.register_all_shipped() # once, before first use # Create a contract and canonicalize contract = Email.create_contract() @@ -34,6 +32,10 @@ if result.status == Resolution.SUCCESS: print(result.canonicalized_value) # "user@example.com" ``` +To register only what you need, call `register_capability(Email())` per capability. + +**Registration and threading:** Registration — single (`register_capability`) or bootstrap (`paxman.register_all_shipped()`) — must complete from a single thread before the first `canonicalize()` call; the registry then freezes and reads are safe from any thread; registering later raises `CapabilityError`. + --- ## What Happens From 52aa974b833e5ab68b66f69e4121b70c5b95f7ff Mon Sep 17 00:00:00 2001 From: Azahari Zaman Date: Wed, 19 Aug 2026 14:41:50 +0800 Subject: [PATCH 3/4] fix(docs): qualify alternative register_capability as paxman.register_capability Addresses thermo F1: bare register_capability in alternative sentence caused NameError; now shows paxman.register_capability per correct import path. F2/F3/F4/F5 declined with rationale in notepad. --- QUICKSTART.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QUICKSTART.md b/QUICKSTART.md index 28aaa268..8f98b057 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -30,7 +30,7 @@ if result.status == Resolution.SUCCESS: print(result.canonicalized_value) # "user@example.com" ``` -To register only what you need, call `register_capability(EmailCapability())` per capability. +To register only what you need, call `paxman.register_capability(EmailCapability())` per capability. Registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call; post-freeze reads are safe from any thread. diff --git a/README.md b/README.md index f414c5da..04eaceab 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ if result.status == Resolution.SUCCESS: print(result.canonicalized_value) # "user@example.com" ``` -To register only what you need, call `register_capability(Email())` per capability. +To register only what you need, call `paxman.register_capability(Email())` per capability. **Registration and threading:** Registration — single (`register_capability`) or bootstrap (`paxman.register_all_shipped()`) — must complete from a single thread before the first `canonicalize()` call; the registry then freezes and reads are safe from any thread; registering later raises `CapabilityError`. From 8e2d99577019978b1d92d60e86f9ad97d6c744c3 Mon Sep 17 00:00:00 2001 From: Azahari Zaman Date: Wed, 19 Aug 2026 15:31:18 +0800 Subject: [PATCH 4/4] fix: address review comments on ARCHITECTURE freeze wording and bootstrap e2e fixture - ARCHITECTURE.md: describe registry as freezing on first canonicalize() and remaining frozen for later runs, not at start of each pipeline run (line 83) - tests/e2e/test_bootstrap.py: move round-trip test from integration to e2e, apply e2e marker, replace manual reset_registry try/finally with autouse _clean_registry fixture; preserve bootstrap/canonicalize assertions Fixes still-valid findings; other review data skipped. --- ARCHITECTURE.md | 2 +- tests/e2e/test_bootstrap.py | 25 +++++++++++++++++++++++++ tests/integration/test_bootstrap.py | 20 -------------------- 3 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 tests/e2e/test_bootstrap.py delete mode 100644 tests/integration/test_bootstrap.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 96784c49..9e454244 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -80,7 +80,7 @@ Each capability is a self-contained domain module that provides: - **Validation Rules** — semantic rules that validate the notation against authoritative specifications - **A Contract** — a user-facing configuration object that toggles grammars, excludes rules, and passes parameters -Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes at the start of each pipeline run, ensuring that the set of available capabilities is stable during execution. The sanctioned bulk form is `paxman.register_all_shipped()`, which registers all ten shipped capabilities in fixed alphabetical order; registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call, after which reads are safe from any thread. +Capabilities are registered with the discovery registry before the first canonicalization call. The registry freezes on the first `canonicalize()` call and remains frozen for later runs, ensuring that the set of available capabilities is stable during execution. The sanctioned bulk form is `paxman.register_all_shipped()`, which registers all ten shipped capabilities in fixed alphabetical order; registration — single or bootstrap — must complete from a single thread before the first `canonicalize()` call, after which reads are safe from any thread. ### Engine diff --git a/tests/e2e/test_bootstrap.py b/tests/e2e/test_bootstrap.py new file mode 100644 index 00000000..01a2e1f4 --- /dev/null +++ b/tests/e2e/test_bootstrap.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import pytest + +import paxman +from paxman.capabilities import Email +from paxman.core.discovery import reset_registry +from paxman.core.domain import Resolution + + +@pytest.fixture(autouse=True) +def _clean_registry(): + reset_registry() + yield + reset_registry() + + +@pytest.mark.e2e +def test_bootstrap_then_canonicalize_round_trip() -> None: + """register_all_shipped() is a complete bootstrap: pipeline resolves.""" + paxman.register_all_shipped() + contract = Email.create_contract() + result = paxman.canonicalize("user@Example.COM", contract) + assert result.status is Resolution.SUCCESS + assert result.canonicalized_value == "user@example.com" diff --git a/tests/integration/test_bootstrap.py b/tests/integration/test_bootstrap.py deleted file mode 100644 index 3de604b7..00000000 --- a/tests/integration/test_bootstrap.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -import paxman -from paxman.capabilities import Email -from paxman.core.discovery import reset_registry -from paxman.core.domain import Resolution - - -@pytest.mark.integration -def test_bootstrap_then_canonicalize_round_trip() -> None: - """register_all_shipped() is a complete bootstrap: pipeline resolves.""" - reset_registry() - try: - paxman.register_all_shipped() - contract = Email.create_contract() - result = paxman.canonicalize("user@Example.COM", contract) - assert result.status is Resolution.SUCCESS - assert result.canonicalized_value == "user@example.com" - finally: - reset_registry()