Writing a rule pack¶
The four built-in policies cover the open, published scheme rules. Requirements that are proprietary, correspondent-specific, or jurisdiction-specific are better shipped as premium rule packs: separately installable Python distributions that register additional policies through an entry point. This page describes the contract.
The core never bundles a pack. A pack is discovered automatically once installed, contributes its own policy ids and finding codes, and is gated at load time by entitlement + integrity checks.
1. Advertise the pack via an entry point¶
Register your pack under the structured_address_fix.policies entry-point
group. The name is the policy id callers will pass to assess / remediate;
the value points at the PolicyPack object your distribution exposes:
# pyproject.toml of your pack distribution
[project]
name = "acme-saf-pack"
version = "1.0.0"
dependencies = ["structured-address-fix>=0.1"]
[project.entry-points."structured_address_fix.policies"]
acme-correspondent-2026 = "acme_saf_pack:PACK"
Poetry projects use the equivalent stanza:
[tool.poetry.plugins."structured_address_fix.policies"]
acme-correspondent-2026 = "acme_saf_pack:PACK"
If two installed packs claim the same policy id, resolution raises
PolicyConflictError (see the error taxonomy).
2. Implement the rulebook¶
A pack inspects a CanonicalAddress and returns RiskFindings. Reuse the
shared domain vocabulary so downstream code keys on findings uniformly. Custom,
pack-specific finding codes carry their own prefix (never SAF) so they can
never collide with the core codes:
# acme_saf_pack/__init__.py
from structured_address_fix.domain import (
CanonicalAddress, FindingCode, RiskFinding, Severity,
)
def check(address: CanonicalAddress, *, location: str) -> list[RiskFinding]:
"""Raise the pack's findings against a single address.
``location`` is the JSON pointer to the address's ``PstlAdr`` element, so
findings thread back to the exact node.
"""
findings: list[RiskFinding] = []
# Example: this correspondent profile insists on a post code.
if address.post_code is None:
findings.append(
RiskFinding(
code=FindingCode.MISSING_TOWN, # or a pack-specific code
severity=Severity.ERROR,
message="Correspondent profile requires a post code.",
policy_id="acme-correspondent-2026",
location=location,
rulebook_clause="ACME-MP 4.2",
rejects_payment=True,
)
)
return findings
3. Bind it into a PolicyPack¶
Expose the object named in your entry point. A pack binds its policy id to the
check callable and declares its metadata (id, version, and — for a commercial
pack — its entitlement + integrity material):
# acme_saf_pack/__init__.py (continued)
from structured_address_fix.plugins import PolicyPack
PACK = PolicyPack(
policy_id="acme-correspondent-2026",
version="1.0.0",
check=check,
)
4. Entitlement and integrity (commercial packs)¶
For a paid pack, the loader enforces two gates before a pack's rules run:
- Entitlement — the caller must be licensed for the pack. A pack the caller
is not entitled to raises
PackNotLicensedError(SAF_PACK_NOT_LICENSED). - Integrity — the pack must pass its signature / version check. A tampered
or mismatched pack raises
PackIntegrityError(SAF_PACK_INTEGRITY).
Both are subclasses of EntitlementError, so a host can catch the family and
surface a licensing prompt distinctly from an input or policy error.
5. Test it like the core¶
A pack should hold itself to the same bar as the core library: 100% line + branch coverage on its rules, a Hypothesis property test over the address shapes it cares about, and a regression test pinning each finding code's meaning (codes are a stable public API — a changed rule earns a new code, never a repurposed one).
Checklist¶
- [ ] Entry point registered under
structured_address_fix.policies - [ ]
checkreturnsRiskFindings with yourpolicy_idand alocation - [ ] Custom finding codes use a non-
SAFprefix - [ ]
PolicyPackobject exposed at the entry-point target - [ ] Entitlement + integrity material set for a commercial pack
- [ ] Tests pin every finding code and cover the classification edges