paxman 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paxman-0.1.0/.github/workflows/ci.yml +72 -0
- paxman-0.1.0/.github/workflows/opencode.yml +33 -0
- paxman-0.1.0/.github/workflows/publish.yml +88 -0
- paxman-0.1.0/.gitignore +37 -0
- paxman-0.1.0/AGENTS.md +108 -0
- paxman-0.1.0/ARCHITECTURE.md +258 -0
- paxman-0.1.0/CONTEXT.md +1013 -0
- paxman-0.1.0/CONTRIBUTING.md +156 -0
- paxman-0.1.0/HOW_TO_ADD_NEW_CAPABILITY.md +1158 -0
- paxman-0.1.0/HOW_TO_ADD_NEW_GRAMMAR.md +288 -0
- paxman-0.1.0/LICENSE.md +21 -0
- paxman-0.1.0/PKG-INFO +713 -0
- paxman-0.1.0/QUICKSTART.md +107 -0
- paxman-0.1.0/README.md +697 -0
- paxman-0.1.0/SECURITY.md +43 -0
- paxman-0.1.0/TESTING_STRATEGY.md +131 -0
- paxman-0.1.0/benchmarks/README.md +11 -0
- paxman-0.1.0/benchmarks/__init__.py +0 -0
- paxman-0.1.0/benchmarks/baseline.json +95 -0
- paxman-0.1.0/benchmarks/harness.py +97 -0
- paxman-0.1.0/benchmarks/scenarios.py +241 -0
- paxman-0.1.0/capability_homogeneity_audit.md +408 -0
- paxman-0.1.0/docs/adr/0001-clean-architecture-pipeline.md +283 -0
- paxman-0.1.0/docs/adr/0002-remove-replay-hash.md +147 -0
- paxman-0.1.0/docs/adr/0003-semantic-affinity-routing.md +260 -0
- paxman-0.1.0/docs/adr/0004-single-value-invariant.md +162 -0
- paxman-0.1.0/docs/adr/0005-si-unit-multi-solidus-and-parens.md +90 -0
- paxman-0.1.0/docs/adr/0006-si-unit-split-prefixes.md +94 -0
- paxman-0.1.0/docs/adr/0007-contract-surface-unification.md +40 -0
- paxman-0.1.0/docs/adr/0008-staged-recognition-pipeline.md +277 -0
- paxman-0.1.0/docs/development/AGENTS.md +44 -0
- paxman-0.1.0/docs/development/MILESTONE.md +74 -0
- paxman-0.1.0/docs/development/plans/2026-07-26-contract-params-hypothesis-tests-revised.md +2051 -0
- paxman-0.1.0/docs/development/plans/2026-07-26-contract-params-hypothesis-tests.md +1594 -0
- paxman-0.1.0/docs/development/plans/2026-07-26-email-capability.md +2613 -0
- paxman-0.1.0/docs/development/plans/2026-07-28-country-capability-implementation.md +3685 -0
- paxman-0.1.0/docs/development/plans/2026-08-01-phone-number-capability.md +3414 -0
- paxman-0.1.0/docs/development/plans/2026-08-02-capability-surface-homogeneity.md +220 -0
- paxman-0.1.0/docs/development/plans/2026-08-03-f1-grammar-rule-affinity.md +298 -0
- paxman-0.1.0/docs/development/plans/2026-08-03-f2-include-feature-gating.md +275 -0
- paxman-0.1.0/docs/development/plans/2026-08-03-f3-recognition-validation-boundary.md +495 -0
- paxman-0.1.0/docs/development/plans/2026-08-04-centralize-output-format.md +431 -0
- paxman-0.1.0/docs/development/plans/2026-08-04-recognition-homogeneity.md +1580 -0
- paxman-0.1.0/docs/development/plans/2026-08-05-isbn-capability.md +1310 -0
- paxman-0.1.0/docs/development/plans/2026-08-05-money-capability.md +5099 -0
- paxman-0.1.0/docs/development/plans/2026-08-06-url-capability.md +379 -0
- paxman-0.1.0/docs/development/plans/2026-08-08-currency-capability.md +1317 -0
- paxman-0.1.0/docs/development/plans/2026-08-09-si-units-capability.md +2389 -0
- paxman-0.1.0/docs/development/plans/2026-08-10-adr0002-remove-replay-hash.md +574 -0
- paxman-0.1.0/docs/development/plans/2026-08-11-adr0003-semantic-affinity-routing.md +772 -0
- paxman-0.1.0/docs/development/plans/2026-08-17-capability-scaffolder.md +450 -0
- paxman-0.1.0/docs/development/plans/2026-08-17-register-all-shipped-bootstrap.md +368 -0
- paxman-0.1.0/docs/development/plans/2026-08-17-segmentation-recipe.md +208 -0
- paxman-0.1.0/docs/development/plans/2026-08-17-stale-surface-cleanup.md +473 -0
- paxman-0.1.0/docs/development/plans/2026-08-20-staged-recognition-pipeline.md +1697 -0
- paxman-0.1.0/docs/development/plans/2026-08-21-issn-capability.md +766 -0
- paxman-0.1.0/docs/development/plans/2026-08-22-iban-capability.md +1127 -0
- paxman-0.1.0/docs/development/release/checklist.md +226 -0
- paxman-0.1.0/docs/development/reports/2026-08-07-architecture-review.md +254 -0
- paxman-0.1.0/docs/development/reports/2026-08-22-first-time-user-experience.md +312 -0
- paxman-0.1.0/docs/development/reports/architecture-review-2026-07-26.md +221 -0
- paxman-0.1.0/docs/development/reports/recognition-handling-library-research.md +538 -0
- paxman-0.1.0/docs/development/research/2026-08-05-isbn-canonicalization.md +584 -0
- paxman-0.1.0/docs/development/research/2026-08-05-money-canonicalization.md +818 -0
- paxman-0.1.0/docs/development/research/2026-08-06-url-canonicalization.md +415 -0
- paxman-0.1.0/docs/development/research/2026-08-09-si-unit-canonicalization.md +561 -0
- paxman-0.1.0/docs/development/research/2026-08-21-issn-canonicalization.md +644 -0
- paxman-0.1.0/docs/development/research/2026-08-22-iban-canonicalization.md +832 -0
- paxman-0.1.0/docs/development/specs/2026-07-28-country-capability-design.md +652 -0
- paxman-0.1.0/docs/development/specs/2026-08-03-f3-recognition-validation-boundary-design.md +118 -0
- paxman-0.1.0/docs/recipes/segmentation.md +161 -0
- paxman-0.1.0/docs/user/api-reference.md +241 -0
- paxman-0.1.0/docs/user/capabilities/country.md +132 -0
- paxman-0.1.0/docs/user/capabilities/currency.md +133 -0
- paxman-0.1.0/docs/user/capabilities/date.md +125 -0
- paxman-0.1.0/docs/user/capabilities/email.md +140 -0
- paxman-0.1.0/docs/user/capabilities/index.md +80 -0
- paxman-0.1.0/docs/user/capabilities/ip.md +105 -0
- paxman-0.1.0/docs/user/capabilities/isbn.md +114 -0
- paxman-0.1.0/docs/user/capabilities/money.md +137 -0
- paxman-0.1.0/docs/user/capabilities/phone.md +125 -0
- paxman-0.1.0/docs/user/capabilities/si-unit.md +116 -0
- paxman-0.1.0/docs/user/capabilities/url.md +115 -0
- paxman-0.1.0/docs/user/concepts/candidates-and-ambiguity.md +147 -0
- paxman-0.1.0/docs/user/concepts/capabilities.md +108 -0
- paxman-0.1.0/docs/user/concepts/contracts.md +175 -0
- paxman-0.1.0/docs/user/concepts/errors.md +126 -0
- paxman-0.1.0/docs/user/concepts/execution-result.md +149 -0
- paxman-0.1.0/docs/user/concepts/index.md +90 -0
- paxman-0.1.0/docs/user/concepts/pipeline.md +151 -0
- paxman-0.1.0/docs/user/concepts/provenance.md +79 -0
- paxman-0.1.0/docs/user/extending.md +239 -0
- paxman-0.1.0/docs/user/getting-started.md +221 -0
- paxman-0.1.0/docs/user/index.md +111 -0
- paxman-0.1.0/docs/user/migration.md +122 -0
- paxman-0.1.0/ocs/development/plans/2026-08-19-mid-term-recommendations.md +1551 -0
- paxman-0.1.0/paxman/__init__.py +16 -0
- paxman-0.1.0/paxman/__main__.py +6 -0
- paxman-0.1.0/paxman/api/__init__.py +3 -0
- paxman-0.1.0/paxman/api/bootstrap.py +76 -0
- paxman-0.1.0/paxman/api/canonicalize.py +31 -0
- paxman-0.1.0/paxman/capabilities/AGENTS.md +68 -0
- paxman-0.1.0/paxman/capabilities/Country/__init__.py +7 -0
- paxman-0.1.0/paxman/capabilities/Country/capability.py +150 -0
- paxman-0.1.0/paxman/capabilities/Country/contract.py +41 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/alpha2_recognition.py +39 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/alpha3_recognition.py +39 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/data/__init__.py +12 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/data/chinese_names.py +100 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/data/english_names.py +334 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/data/historical_names.py +57 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/data/localized_names.py +213 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/name_recognition.py +59 -0
- paxman-0.1.0/paxman/capabilities/Country/grammar/numeric_recognition.py +39 -0
- paxman-0.1.0/paxman/capabilities/Country/notation.py +68 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/cldr_localized_ed2025.py +78 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/data/cldr_ed2025.py +198 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/data/iso_3166_ed2020_part3.py +101 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/data/iso_3166_ed2024.py +1890 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/iso_3166_ed2024.py +238 -0
- paxman-0.1.0/paxman/capabilities/Country/rules/iso_3166_historical_ed2020.py +122 -0
- paxman-0.1.0/paxman/capabilities/Currency/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/Currency/capability.py +93 -0
- paxman-0.1.0/paxman/capabilities/Currency/contract.py +83 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/code_recognition.py +50 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/data/currency_symbols.py +83 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/data/currency_words.py +80 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/symbol_recognition.py +59 -0
- paxman-0.1.0/paxman/capabilities/Currency/grammar/word_recognition.py +55 -0
- paxman-0.1.0/paxman/capabilities/Currency/notation.py +35 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/cldr_currencies_ed2025.py +199 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/data/cldr_currencies.py +190 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/data/iso4217_list_one.py +204 -0
- paxman-0.1.0/paxman/capabilities/Currency/rules/iso_4217_ed2015.py +72 -0
- paxman-0.1.0/paxman/capabilities/Date/__init__.py +6 -0
- paxman-0.1.0/paxman/capabilities/Date/capability.py +103 -0
- paxman-0.1.0/paxman/capabilities/Date/contract.py +25 -0
- paxman-0.1.0/paxman/capabilities/Date/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Date/grammar/european_recognition.py +54 -0
- paxman-0.1.0/paxman/capabilities/Date/grammar/iso8601_recognition.py +41 -0
- paxman-0.1.0/paxman/capabilities/Date/grammar/slash_iso_recognition.py +48 -0
- paxman-0.1.0/paxman/capabilities/Date/grammar/us_recognition.py +52 -0
- paxman-0.1.0/paxman/capabilities/Date/notation.py +20 -0
- paxman-0.1.0/paxman/capabilities/Date/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Date/rules/en_50160_ed2010.py +76 -0
- paxman-0.1.0/paxman/capabilities/Date/rules/iso_8601_ed2019.py +57 -0
- paxman-0.1.0/paxman/capabilities/Date/rules/us_federal_rules_ed2023.py +76 -0
- paxman-0.1.0/paxman/capabilities/Email/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/Email/capability.py +64 -0
- paxman-0.1.0/paxman/capabilities/Email/contract.py +29 -0
- paxman-0.1.0/paxman/capabilities/Email/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Email/grammar/localhost_recognition.py +35 -0
- paxman-0.1.0/paxman/capabilities/Email/grammar/obfuscated_recognition.py +53 -0
- paxman-0.1.0/paxman/capabilities/Email/grammar/standard_recognition.py +34 -0
- paxman-0.1.0/paxman/capabilities/Email/notation.py +13 -0
- paxman-0.1.0/paxman/capabilities/Email/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Email/rules/rfc_5322_ed2008.py +46 -0
- paxman-0.1.0/paxman/capabilities/Email/rules/rfc_6761_ed2012.py +43 -0
- paxman-0.1.0/paxman/capabilities/IBAN/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/IBAN/capability.py +51 -0
- paxman-0.1.0/paxman/capabilities/IBAN/contract.py +18 -0
- paxman-0.1.0/paxman/capabilities/IBAN/grammar/__init__.py +5 -0
- paxman-0.1.0/paxman/capabilities/IBAN/grammar/iban_recognition.py +53 -0
- paxman-0.1.0/paxman/capabilities/IBAN/notation.py +22 -0
- paxman-0.1.0/paxman/capabilities/IBAN/rules/__init__.py +3 -0
- paxman-0.1.0/paxman/capabilities/IBAN/rules/iso_13616_1_ed2020.py +166 -0
- paxman-0.1.0/paxman/capabilities/IP/__init__.py +5 -0
- paxman-0.1.0/paxman/capabilities/IP/capability.py +56 -0
- paxman-0.1.0/paxman/capabilities/IP/contract.py +26 -0
- paxman-0.1.0/paxman/capabilities/IP/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/IP/grammar/ipv4_recognition.py +31 -0
- paxman-0.1.0/paxman/capabilities/IP/grammar/ipv6_recognition.py +70 -0
- paxman-0.1.0/paxman/capabilities/IP/notation.py +12 -0
- paxman-0.1.0/paxman/capabilities/IP/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/IP/rules/rfc_5952_ed2010.py +54 -0
- paxman-0.1.0/paxman/capabilities/IP/rules/rfc_791_ed1981.py +55 -0
- paxman-0.1.0/paxman/capabilities/ISBN/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISBN/capability.py +129 -0
- paxman-0.1.0/paxman/capabilities/ISBN/contract.py +46 -0
- paxman-0.1.0/paxman/capabilities/ISBN/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISBN/grammar/isbn10_recognition.py +42 -0
- paxman-0.1.0/paxman/capabilities/ISBN/grammar/isbn13_recognition.py +37 -0
- paxman-0.1.0/paxman/capabilities/ISBN/notation.py +16 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/data/range_message.py +2250 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/data/range_message_2026-08-05.xml +9216 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/isbn_range_message_ed2026.py +75 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/isbn_users_manual_ed2012.py +50 -0
- paxman-0.1.0/paxman/capabilities/ISBN/rules/iso_2108_ed2017.py +66 -0
- paxman-0.1.0/paxman/capabilities/ISSN/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/ISSN/capability.py +67 -0
- paxman-0.1.0/paxman/capabilities/ISSN/contract.py +16 -0
- paxman-0.1.0/paxman/capabilities/ISSN/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISSN/grammar/issn_recognition.py +34 -0
- paxman-0.1.0/paxman/capabilities/ISSN/notation.py +15 -0
- paxman-0.1.0/paxman/capabilities/ISSN/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/ISSN/rules/iso_3297_ed2022.py +47 -0
- paxman-0.1.0/paxman/capabilities/Money/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/Money/capability.py +130 -0
- paxman-0.1.0/paxman/capabilities/Money/contract.py +97 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/__init__.py +45 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/code_recognition.py +58 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/data/currency_symbols.py +83 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/data/currency_words.py +77 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/symbol_recognition.py +67 -0
- paxman-0.1.0/paxman/capabilities/Money/grammar/word_recognition.py +62 -0
- paxman-0.1.0/paxman/capabilities/Money/notation.py +47 -0
- paxman-0.1.0/paxman/capabilities/Money/parsing.py +131 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/cldr_currencies_ed2025.py +245 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/data/cldr_currencies.py +183 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/data/iso4217_list_one.py +353 -0
- paxman-0.1.0/paxman/capabilities/Money/rules/iso_4217_ed2015.py +121 -0
- paxman-0.1.0/paxman/capabilities/Phone/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/Phone/capability.py +151 -0
- paxman-0.1.0/paxman/capabilities/Phone/contract.py +92 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/_common.py +28 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/e164_recognition.py +106 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/international_00_recognition.py +56 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/national_recognition.py +59 -0
- paxman-0.1.0/paxman/capabilities/Phone/grammar/tel_uri_recognition.py +54 -0
- paxman-0.1.0/paxman/capabilities/Phone/notation.py +27 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/data/e164_country_codes.py +262 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/data/nanp_tables.py +18 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/e164_ed2010.py +138 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/nanp_ed2024.py +191 -0
- paxman-0.1.0/paxman/capabilities/Phone/rules/rfc_3966_ed2004.py +63 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/__init__.py +9 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/capability.py +113 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/contract.py +34 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/compound_recognition.py +69 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/data/compound_tokens.py +16 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/data/prefix_tokens.py +71 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/data/unit_name_tokens.py +922 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/data/unit_symbol_tokens.py +942 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/name_recognition.py +83 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/grammar/symbol_recognition.py +93 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/notation.py +46 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/bipm_si_brochure_ed2019.py +187 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/__init__.py +1 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/prefixed_unit_names.py +876 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/prefixed_units.py +878 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/si_base_units.py +8 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/si_derived_units.py +38 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/si_nonsi_units.py +38 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/si_prefixes.py +59 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/data/unit_names.py +70 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/iso_80000_ed2022.py +218 -0
- paxman-0.1.0/paxman/capabilities/SIUnit/rules/split_prefixes.py +71 -0
- paxman-0.1.0/paxman/capabilities/URL/__init__.py +7 -0
- paxman-0.1.0/paxman/capabilities/URL/capability.py +56 -0
- paxman-0.1.0/paxman/capabilities/URL/contract.py +24 -0
- paxman-0.1.0/paxman/capabilities/URL/grammar/__init__.py +0 -0
- paxman-0.1.0/paxman/capabilities/URL/grammar/absolute_uri_recognition.py +84 -0
- paxman-0.1.0/paxman/capabilities/URL/notation.py +17 -0
- paxman-0.1.0/paxman/capabilities/URL/parsing.py +837 -0
- paxman-0.1.0/paxman/capabilities/URL/rules/__init__.py +0 -0
- paxman-0.1.0/paxman/capabilities/URL/rules/data/__init__.py +0 -0
- paxman-0.1.0/paxman/capabilities/URL/rules/data/idna_uts46_mapping.py +15019 -0
- paxman-0.1.0/paxman/capabilities/URL/rules/data/idna_uts46_mapping.txt +9026 -0
- paxman-0.1.0/paxman/capabilities/URL/rules/whatwg_url_standard.py +77 -0
- paxman-0.1.0/paxman/capabilities/__init__.py +104 -0
- paxman-0.1.0/paxman/cli.py +275 -0
- paxman-0.1.0/paxman/core/AGENTS.md +41 -0
- paxman-0.1.0/paxman/core/__init__.py +69 -0
- paxman-0.1.0/paxman/core/capability.py +87 -0
- paxman-0.1.0/paxman/core/capability_contract.py +95 -0
- paxman-0.1.0/paxman/core/contract.py +124 -0
- paxman-0.1.0/paxman/core/discovery.py +83 -0
- paxman-0.1.0/paxman/core/domain.py +264 -0
- paxman-0.1.0/paxman/core/errors.py +56 -0
- paxman-0.1.0/paxman/core/extensions.py +117 -0
- paxman-0.1.0/paxman/core/grammar/__init__.py +31 -0
- paxman-0.1.0/paxman/core/grammar/boundary.py +110 -0
- paxman-0.1.0/paxman/core/grammar/composer.py +108 -0
- paxman-0.1.0/paxman/core/grammar/lexicon.py +42 -0
- paxman-0.1.0/paxman/core/grammar/pipeline.py +45 -0
- paxman-0.1.0/paxman/core/grammar/stages.py +195 -0
- paxman-0.1.0/paxman/engine/__init__.py +0 -0
- paxman-0.1.0/paxman/engine/orchestrator.py +525 -0
- paxman-0.1.0/paxman/py.typed +0 -0
- paxman-0.1.0/paxman/shared_data/README.md +10 -0
- paxman-0.1.0/paxman/shared_data/currency_snapshot.json +1609 -0
- paxman-0.1.0/pyproject.toml +129 -0
- paxman-0.1.0/tests/AGENTS.md +47 -0
- paxman-0.1.0/tests/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/country/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/country/test_capability.py +297 -0
- paxman-0.1.0/tests/capabilities/country/test_data_consistency.py +153 -0
- paxman-0.1.0/tests/capabilities/country/test_grammar.py +483 -0
- paxman-0.1.0/tests/capabilities/country/test_rules.py +654 -0
- paxman-0.1.0/tests/capabilities/currency/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/currency/test_capability.py +149 -0
- paxman-0.1.0/tests/capabilities/currency/test_contract.py +68 -0
- paxman-0.1.0/tests/capabilities/currency/test_data.py +207 -0
- paxman-0.1.0/tests/capabilities/currency/test_data_consistency.py +87 -0
- paxman-0.1.0/tests/capabilities/currency/test_grammar.py +166 -0
- paxman-0.1.0/tests/capabilities/currency/test_notation.py +43 -0
- paxman-0.1.0/tests/capabilities/currency/test_rules.py +315 -0
- paxman-0.1.0/tests/capabilities/date/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/date/test_capability.py +149 -0
- paxman-0.1.0/tests/capabilities/date/test_grammar.py +240 -0
- paxman-0.1.0/tests/capabilities/date/test_rules.py +204 -0
- paxman-0.1.0/tests/capabilities/email/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/email/test_capability.py +124 -0
- paxman-0.1.0/tests/capabilities/email/test_grammar.py +177 -0
- paxman-0.1.0/tests/capabilities/email/test_rules.py +163 -0
- paxman-0.1.0/tests/capabilities/iban/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/iban/test_capability.py +43 -0
- paxman-0.1.0/tests/capabilities/iban/test_contract.py +42 -0
- paxman-0.1.0/tests/capabilities/iban/test_grammar.py +119 -0
- paxman-0.1.0/tests/capabilities/iban/test_notation.py +32 -0
- paxman-0.1.0/tests/capabilities/iban/test_rules.py +96 -0
- paxman-0.1.0/tests/capabilities/ip/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/ip/test_capability.py +159 -0
- paxman-0.1.0/tests/capabilities/ip/test_grammar.py +137 -0
- paxman-0.1.0/tests/capabilities/ip/test_rules.py +204 -0
- paxman-0.1.0/tests/capabilities/isbn/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/isbn/test_capability.py +109 -0
- paxman-0.1.0/tests/capabilities/isbn/test_contract.py +51 -0
- paxman-0.1.0/tests/capabilities/isbn/test_data.py +85 -0
- paxman-0.1.0/tests/capabilities/isbn/test_grammar.py +196 -0
- paxman-0.1.0/tests/capabilities/isbn/test_notation.py +35 -0
- paxman-0.1.0/tests/capabilities/isbn/test_rules.py +228 -0
- paxman-0.1.0/tests/capabilities/issn/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/issn/test_capability.py +68 -0
- paxman-0.1.0/tests/capabilities/issn/test_contract.py +56 -0
- paxman-0.1.0/tests/capabilities/issn/test_grammar.py +143 -0
- paxman-0.1.0/tests/capabilities/issn/test_notation.py +43 -0
- paxman-0.1.0/tests/capabilities/issn/test_rules.py +99 -0
- paxman-0.1.0/tests/capabilities/money/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/money/test_capability.py +179 -0
- paxman-0.1.0/tests/capabilities/money/test_contract.py +107 -0
- paxman-0.1.0/tests/capabilities/money/test_data.py +214 -0
- paxman-0.1.0/tests/capabilities/money/test_data_consistency.py +69 -0
- paxman-0.1.0/tests/capabilities/money/test_grammar.py +337 -0
- paxman-0.1.0/tests/capabilities/money/test_notation.py +79 -0
- paxman-0.1.0/tests/capabilities/money/test_parsing.py +161 -0
- paxman-0.1.0/tests/capabilities/money/test_rules.py +390 -0
- paxman-0.1.0/tests/capabilities/phone/__init__.py +1 -0
- paxman-0.1.0/tests/capabilities/phone/test_capability.py +331 -0
- paxman-0.1.0/tests/capabilities/phone/test_data.py +70 -0
- paxman-0.1.0/tests/capabilities/phone/test_grammar.py +443 -0
- paxman-0.1.0/tests/capabilities/phone/test_rules.py +482 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_capability.py +240 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_contract.py +54 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_data.py +199 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_data_consistency.py +113 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_grammar.py +238 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_notation.py +31 -0
- paxman-0.1.0/tests/capabilities/si_unit/test_rules.py +305 -0
- paxman-0.1.0/tests/capabilities/url/__init__.py +0 -0
- paxman-0.1.0/tests/capabilities/url/test_capability.py +135 -0
- paxman-0.1.0/tests/capabilities/url/test_contract.py +59 -0
- paxman-0.1.0/tests/capabilities/url/test_data.py +58 -0
- paxman-0.1.0/tests/capabilities/url/test_data_consistency.py +121 -0
- paxman-0.1.0/tests/capabilities/url/test_grammar.py +106 -0
- paxman-0.1.0/tests/capabilities/url/test_notation.py +31 -0
- paxman-0.1.0/tests/capabilities/url/test_parsing.py +178 -0
- paxman-0.1.0/tests/capabilities/url/test_parsing_conformance.py +298 -0
- paxman-0.1.0/tests/capabilities/url/test_rule.py +234 -0
- paxman-0.1.0/tests/conftest.py +9 -0
- paxman-0.1.0/tests/e2e/__init__.py +0 -0
- paxman-0.1.0/tests/e2e/test_bootstrap.py +25 -0
- paxman-0.1.0/tests/e2e/test_canonicalize.py +621 -0
- paxman-0.1.0/tests/integration/__init__.py +0 -0
- paxman-0.1.0/tests/integration/test_ambiguity.py +59 -0
- paxman-0.1.0/tests/integration/test_benchmark_harness.py +40 -0
- paxman-0.1.0/tests/integration/test_country_pipeline.py +593 -0
- paxman-0.1.0/tests/integration/test_currency_pipeline.py +173 -0
- paxman-0.1.0/tests/integration/test_date_capability.py +154 -0
- paxman-0.1.0/tests/integration/test_feature_gating.py +423 -0
- paxman-0.1.0/tests/integration/test_format_value_seam.py +361 -0
- paxman-0.1.0/tests/integration/test_grammar_extensions.py +338 -0
- paxman-0.1.0/tests/integration/test_iban_capability.py +141 -0
- paxman-0.1.0/tests/integration/test_issn_capability.py +211 -0
- paxman-0.1.0/tests/integration/test_money_pipeline.py +254 -0
- paxman-0.1.0/tests/integration/test_phone_pipeline.py +259 -0
- paxman-0.1.0/tests/integration/test_pipeline.py +756 -0
- paxman-0.1.0/tests/integration/test_recognition_seam.py +432 -0
- paxman-0.1.0/tests/integration/test_si_unit_pipeline.py +233 -0
- paxman-0.1.0/tests/integration/test_single_value_invariant.py +111 -0
- paxman-0.1.0/tests/integration/test_span_exposure.py +55 -0
- paxman-0.1.0/tests/integration/test_temporal.py +61 -0
- paxman-0.1.0/tests/integration/test_url_pipeline.py +166 -0
- paxman-0.1.0/tests/property/__init__.py +0 -0
- paxman-0.1.0/tests/property/_legacy_currency_grammars.py +117 -0
- paxman-0.1.0/tests/property/_legacy_money_grammars.py +153 -0
- paxman-0.1.0/tests/property/_legacy_phone_url_grammars.py +218 -0
- paxman-0.1.0/tests/property/_legacy_remaining_grammars.py +523 -0
- paxman-0.1.0/tests/property/_legacy_siunit_grammars.py +145 -0
- paxman-0.1.0/tests/property/grammar_stage_parity.py +31 -0
- paxman-0.1.0/tests/property/test_currency_properties.py +80 -0
- paxman-0.1.0/tests/property/test_domain_properties.py +182 -0
- paxman-0.1.0/tests/property/test_format_value_properties.py +275 -0
- paxman-0.1.0/tests/property/test_grammar_properties.py +115 -0
- paxman-0.1.0/tests/property/test_grammar_stage_parity.py +952 -0
- paxman-0.1.0/tests/property/test_iban_properties.py +82 -0
- paxman-0.1.0/tests/property/test_isbn_properties.py +130 -0
- paxman-0.1.0/tests/property/test_issn_properties.py +160 -0
- paxman-0.1.0/tests/property/test_money_properties.py +135 -0
- paxman-0.1.0/tests/property/test_rule_properties.py +140 -0
- paxman-0.1.0/tests/property/test_si_unit_properties.py +119 -0
- paxman-0.1.0/tests/property/test_url_properties.py +111 -0
- paxman-0.1.0/tests/unit/__init__.py +0 -0
- paxman-0.1.0/tests/unit/test_api_coverage_fix.py +36 -0
- paxman-0.1.0/tests/unit/test_bootstrap.py +81 -0
- paxman-0.1.0/tests/unit/test_boundary_guards.py +75 -0
- paxman-0.1.0/tests/unit/test_candidate.py +114 -0
- paxman-0.1.0/tests/unit/test_capability.py +136 -0
- paxman-0.1.0/tests/unit/test_capability_contract.py +181 -0
- paxman-0.1.0/tests/unit/test_capability_exports.py +184 -0
- paxman-0.1.0/tests/unit/test_capability_lazy_import.py +91 -0
- paxman-0.1.0/tests/unit/test_capability_surface.py +567 -0
- paxman-0.1.0/tests/unit/test_contract.py +197 -0
- paxman-0.1.0/tests/unit/test_contract_surface.py +129 -0
- paxman-0.1.0/tests/unit/test_currency_data_regeneration.py +21 -0
- paxman-0.1.0/tests/unit/test_discovery.py +214 -0
- paxman-0.1.0/tests/unit/test_errors.py +58 -0
- paxman-0.1.0/tests/unit/test_extensions.py +253 -0
- paxman-0.1.0/tests/unit/test_grammar_semantic_purity.py +78 -0
- paxman-0.1.0/tests/unit/test_grammar_semantics_consistency.py +325 -0
- paxman-0.1.0/tests/unit/test_grammar_semantics_metadata.py +152 -0
- paxman-0.1.0/tests/unit/test_lexicon_alternation.py +60 -0
- paxman-0.1.0/tests/unit/test_new_capability_tool.py +272 -0
- paxman-0.1.0/tests/unit/test_package_install.py +158 -0
- paxman-0.1.0/tests/unit/test_phone_helpers.py +43 -0
- paxman-0.1.0/tests/unit/test_pipeline_stages.py +66 -0
- paxman-0.1.0/tests/unit/test_provenance.py +72 -0
- paxman-0.1.0/tests/unit/test_recognized_rep.py +148 -0
- paxman-0.1.0/tests/unit/test_removed_surfaces.py +75 -0
- paxman-0.1.0/tests/unit/test_resolution.py +29 -0
- paxman-0.1.0/tests/unit/test_rule_metadata.py +153 -0
- paxman-0.1.0/tests/unit/test_rule_output_format_purity.py +50 -0
- paxman-0.1.0/tests/unit/test_status_computation_invariant.py +99 -0
- paxman-0.1.0/tests/unit/test_version_stamp.py +41 -0
- paxman-0.1.0/tools/__init__.py +1 -0
- paxman-0.1.0/tools/canonicalize_si_unit.py +55 -0
- paxman-0.1.0/tools/generate_readme_table.py +100 -0
- paxman-0.1.0/tools/new_capability.py +773 -0
- paxman-0.1.0/tools/regenerate_currency_data.py +445 -0
- paxman-0.1.0/tools/regenerate_idna_uts46_data.py +142 -0
- paxman-0.1.0/tools/regenerate_isbn_range_data.py +106 -0
- paxman-0.1.0/tools/regenerate_si_prefix_data.py +270 -0
- paxman-0.1.0/tools/si_unit_canonicalize.py +32 -0
- paxman-0.1.0/uv.lock +527 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
types: [opened, synchronize, reopened]
|
|
7
|
+
push:
|
|
8
|
+
branches: [main, "feature/**", "refactor/**"]
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ci-${{ github.ref }}
|
|
12
|
+
cancel-in-progress: true
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
ci:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
28
|
+
with:
|
|
29
|
+
persist-credentials: false
|
|
30
|
+
|
|
31
|
+
- name: Install uv
|
|
32
|
+
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
|
|
33
|
+
|
|
34
|
+
- name: Set up Python
|
|
35
|
+
run: uv python install ${{ matrix.python-version }}
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: uv sync --all-extras --python ${{ matrix.python-version }}
|
|
39
|
+
|
|
40
|
+
- name: Ruff lint
|
|
41
|
+
run: uv run ruff check paxman/ tests/
|
|
42
|
+
|
|
43
|
+
- name: Ruff format check
|
|
44
|
+
run: uv run ruff format --check paxman/ tests/
|
|
45
|
+
|
|
46
|
+
- name: Pyright type check
|
|
47
|
+
run: uv run pyright
|
|
48
|
+
|
|
49
|
+
- name: Import lint
|
|
50
|
+
run: uv run import-linter lint
|
|
51
|
+
|
|
52
|
+
- name: Check generated Currency/Money data drift
|
|
53
|
+
run: uv run python tools/regenerate_currency_data.py --check
|
|
54
|
+
|
|
55
|
+
- name: Check generated SIUnit data drift
|
|
56
|
+
run: uv run python tools/regenerate_si_prefix_data.py --check
|
|
57
|
+
|
|
58
|
+
- name: Benchmark (informational, non-blocking)
|
|
59
|
+
continue-on-error: true
|
|
60
|
+
run: uv run python -m benchmarks.harness --iterations 50 --output /tmp/bench.json && cat /tmp/bench.json
|
|
61
|
+
|
|
62
|
+
- name: Tests with coverage
|
|
63
|
+
run: uv run pytest --cov=paxman --cov-report=term-missing --cov-report=html --tb=short -q
|
|
64
|
+
|
|
65
|
+
- name: Enforce per-package coverage
|
|
66
|
+
run: |
|
|
67
|
+
echo "Checking per-package coverage thresholds..."
|
|
68
|
+
uv run coverage report --include="paxman/core/*" --fail-under=95
|
|
69
|
+
uv run coverage report --include="paxman/capabilities/*" --fail-under=95
|
|
70
|
+
uv run coverage report --include="paxman/engine/*" --fail-under=95
|
|
71
|
+
uv run coverage report --include="paxman/api/*" --fail-under=95
|
|
72
|
+
echo "All packages meet 95% coverage threshold."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: opencode
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
opencode:
|
|
11
|
+
if: |
|
|
12
|
+
contains(github.event.comment.body, ' /oc') ||
|
|
13
|
+
startsWith(github.event.comment.body, '/oc') ||
|
|
14
|
+
contains(github.event.comment.body, ' /opencode') ||
|
|
15
|
+
startsWith(github.event.comment.body, '/opencode')
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write
|
|
19
|
+
contents: read
|
|
20
|
+
pull-requests: read
|
|
21
|
+
issues: read
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v6
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Run opencode
|
|
29
|
+
uses: anomalyco/opencode/github@latest
|
|
30
|
+
env:
|
|
31
|
+
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
|
32
|
+
with:
|
|
33
|
+
model: ${{ secrets.OPENCODE_MODEL_ID }}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: Build distributions
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
persist-credentials: false
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.11
|
|
27
|
+
|
|
28
|
+
- name: Validate version matches tag
|
|
29
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
30
|
+
run: |
|
|
31
|
+
TAG_RAW="${GITHUB_REF#refs/tags/}"
|
|
32
|
+
if [[ ! "$TAG_RAW" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
33
|
+
echo "::error::Tag $TAG_RAW does not match expected format vX.Y.Z"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
TAG="${TAG_RAW#v}"
|
|
37
|
+
VERSION="$(uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
|
|
38
|
+
echo "Tag: $TAG pyproject version: $VERSION"
|
|
39
|
+
if [ "$TAG" != "$VERSION" ]; then
|
|
40
|
+
echo "::error::Tag v$TAG does not match pyproject.toml version $VERSION"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
- name: Build sdist and wheel
|
|
45
|
+
run: uv build
|
|
46
|
+
|
|
47
|
+
- name: Twine check
|
|
48
|
+
run: uv run --with twine twine check dist/*
|
|
49
|
+
|
|
50
|
+
- name: Verify py.typed in wheel
|
|
51
|
+
run: |
|
|
52
|
+
uv run python -c "
|
|
53
|
+
import zipfile, pathlib, sys
|
|
54
|
+
whls = list(pathlib.Path('dist').glob('*.whl'))
|
|
55
|
+
assert whls, 'no wheel built'
|
|
56
|
+
for whl in whls:
|
|
57
|
+
with zipfile.ZipFile(whl) as z:
|
|
58
|
+
names = z.namelist()
|
|
59
|
+
assert any(n.endswith('paxman/py.typed') for n in names), f'py.typed missing in {whl.name}: {names[:20]}'
|
|
60
|
+
print(f'py.typed Ships in {whl.name}')
|
|
61
|
+
"
|
|
62
|
+
|
|
63
|
+
- name: Upload artifacts
|
|
64
|
+
uses: actions/upload-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/
|
|
68
|
+
|
|
69
|
+
publish:
|
|
70
|
+
name: Publish to PyPI
|
|
71
|
+
needs: build
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
74
|
+
environment: pypi
|
|
75
|
+
permissions:
|
|
76
|
+
id-token: write
|
|
77
|
+
steps:
|
|
78
|
+
- name: Download distributions
|
|
79
|
+
uses: actions/download-artifact@v4
|
|
80
|
+
with:
|
|
81
|
+
name: dist
|
|
82
|
+
path: dist/
|
|
83
|
+
|
|
84
|
+
- name: Publish to PyPI via Trusted Publishing
|
|
85
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1
|
|
86
|
+
with:
|
|
87
|
+
verbose: true
|
|
88
|
+
print-hash: true
|
paxman-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.omo
|
|
2
|
+
.worktrees/
|
|
3
|
+
# Python
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.so
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.pyright/
|
|
31
|
+
htmlcov/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
coverage.xml
|
|
35
|
+
*.cover
|
|
36
|
+
*.py,cover
|
|
37
|
+
.hypothesis/
|
paxman-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# PROJECT KNOWLEDGE BASE
|
|
2
|
+
|
|
3
|
+
**Generated:** 2026-08-22
|
|
4
|
+
**Commit:** d7737f0
|
|
5
|
+
**Branch:** chores/pre-release-housekeeping
|
|
6
|
+
|
|
7
|
+
## OVERVIEW
|
|
8
|
+
Paxman is a Python 3.11+ canonicalization library with a small CLI: takes ambiguous human input, returns what authoritative specs say it means, with full provenance. Deterministic, provenance-first. 12 capabilities (Country, Currency, Date, Email, IBAN, IP, ISBN, ISSN, Money, Phone, SI Unit, URL). Toolchain: uv + hatchling, ruff, strict pyright, import-linter, pytest at 95% coverage.
|
|
9
|
+
|
|
10
|
+
## STRUCTURE
|
|
11
|
+
```text
|
|
12
|
+
paxman/
|
|
13
|
+
├── api/ # canonicalize() + bootstrap (register_all_shipped, list_shipped_capabilities)
|
|
14
|
+
├── cli.py # CLI: `paxman` console script / `python -m paxman` (--list, --json, stdin)
|
|
15
|
+
├── __main__.py # python -m paxman entry
|
|
16
|
+
├── engine/ # run_capability() pipeline orchestrator
|
|
17
|
+
├── core/ # domain objects, Contract protocol, registry, extensions, errors (+ grammar/ shared machinery)
|
|
18
|
+
├── capabilities/ # 12 self-contained capability packages
|
|
19
|
+
├── shared_data/ # cross-capability source snapshots (currency_snapshot.json → Currency + Money data)
|
|
20
|
+
└── py.typed # PEP 561 marker
|
|
21
|
+
benchmarks/ # harness.py (CI-run), grammar_stage_parity.py, baseline.json
|
|
22
|
+
tests/ # unit / capabilities/<cap> / integration / property / e2e
|
|
23
|
+
tools/ # new_capability.py (scaffolder), generate_readme_table.py,
|
|
24
|
+
# regenerate_{isbn_range,si_prefix,idna_uts46,currency}_data.py
|
|
25
|
+
docs/ # adr/, development/, recipes/, user/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## WHERE TO LOOK
|
|
29
|
+
| Task | Location |
|
|
30
|
+
|------|----------|
|
|
31
|
+
| Trace pipeline flow | `paxman/engine/orchestrator.py` → `run_capability()` |
|
|
32
|
+
| Domain vocabulary (Rule, Provenance, Candidate…) | `paxman/core/domain.py` |
|
|
33
|
+
| Contract protocol | `paxman/core/contract.py`, `paxman/core/capability_contract.py` |
|
|
34
|
+
| Capability registration | `paxman/core/discovery.py` (explicit, never auto) |
|
|
35
|
+
| Community extensions | `paxman/core/extensions.py` → `register_grammar` / `register_rule` + `extra_grammars` on contracts |
|
|
36
|
+
| Error hierarchy | `paxman/core/errors.py` |
|
|
37
|
+
| Add a capability | `HOW_TO_ADD_NEW_CAPABILITY.md` (62KB spec — read first). Scaffold first with `tools/new_capability.py` (see HOW_TO_ADD_NEW_CAPABILITY.md Step 0); then fill in the domain. |
|
|
38
|
+
| Recognition (per cap) | `paxman/capabilities/<Name>/grammar/` |
|
|
39
|
+
| Validation (per cap) | `paxman/capabilities/<Name>/rules/` |
|
|
40
|
+
| Presentation seam | `paxman/capabilities/<Name>/capability.py` → `format_value()` |
|
|
41
|
+
| Regenerate generated data | `tools/regenerate_isbn_range_data.py` (ISBN range), `tools/regenerate_si_prefix_data.py` (SIUnit prefixed units), `tools/regenerate_idna_uts46_data.py` (URL IDNA mapping), `tools/regenerate_currency_data.py` (Currency + Money from `paxman/shared_data/currency_snapshot.json`) |
|
|
42
|
+
| CLI behavior | `paxman/cli.py` (`--list`, `--json`, stdin; contract flags are API-only) |
|
|
43
|
+
| Merge-blocking commands | `.github/workflows/ci.yml` (authoritative) |
|
|
44
|
+
|
|
45
|
+
## CODE MAP
|
|
46
|
+
| Symbol | Type | Location | Role |
|
|
47
|
+
|--------|------|----------|------|
|
|
48
|
+
| `canonicalize()` | function | `paxman/api/canonicalize.py` | Sole user entry point → `run_capability()` |
|
|
49
|
+
| `register_all_shipped()` / `list_shipped_capabilities()` | functions | `paxman/api/bootstrap.py` | One-call registration of the 12 shipped capabilities; deterministic name list |
|
|
50
|
+
| `list_registered_capabilities()` | function | `paxman/core/discovery.py` | Introspection of the live registry |
|
|
51
|
+
| `register_capability()` | function | `paxman/core/discovery.py` | Registry add; freezes on first run |
|
|
52
|
+
| `register_grammar()` / `register_rule()` | functions | `paxman/core/extensions.py` | Community extension seam (opt-in via contract `extra_grammars`) |
|
|
53
|
+
| `run_capability()` | function | `paxman/engine/orchestrator.py` | Full pipeline (recognize→validate→resolve→hash) |
|
|
54
|
+
| `ExecutionResult` | dataclass | `paxman/engine/orchestrator.py` | Return type of `canonicalize()` |
|
|
55
|
+
| `Capability` | ABC | `paxman/core/capability.py` | `get_grammars`/`get_rules`/`format_value` |
|
|
56
|
+
| `CapabilityContract` | dataclass | `paxman/core/capability_contract.py` | Frozen contract base (no slots) |
|
|
57
|
+
| `Contract` | Protocol | `paxman/core/contract.py` | Structural contract interface |
|
|
58
|
+
| `Rule` / `Grammar` | ABCs | `paxman/core/domain.py` | Validation / recognition units |
|
|
59
|
+
| `Resolution`, `Provenance`, `Candidate`, `RecognizedRep`, `VersionStamp` | dataclasses | `paxman/core/domain.py` | Pipeline value objects |
|
|
60
|
+
| `main()` | function | `paxman/cli.py` | CLI entry (`[project.scripts] paxman` + `python -m paxman`) |
|
|
61
|
+
|
|
62
|
+
## CONVENTIONS
|
|
63
|
+
- **uv only** — no Makefile/tox/nox. Every command via `uv run`.
|
|
64
|
+
- Per-capability layout: `notation.py`, `contract.py`, `capability.py`, `grammar/`, `rules/`.
|
|
65
|
+
- Rule file = ONE publication (`rfc_5322_ed2008.py`); class = one section; rule `name` = `"Section 3.4.1-addr-spec"`.
|
|
66
|
+
- Grammars recognize only: span-bearing `RecognitionMatch`, never bare notation, never validate/dedup/order.
|
|
67
|
+
- Rules never read `output_format` (CI source-scan enforced), never raise, never gate on `include_*` (declared as `requires_features`).
|
|
68
|
+
- `format_value()` is the ONLY presentation seam; `output_format` resolved in `CapabilityContract.__post_init__`.
|
|
69
|
+
- Domain objects: `@dataclass(frozen=True, slots=True)`. Contracts: `@dataclass(frozen=True)` **without** slots.
|
|
70
|
+
- Test doubles local to the test file/conftest — no shared mock libraries.
|
|
71
|
+
- Registry is module-level and freezes per pipeline run; tests use autouse `_clean_registry` fixture (integration/e2e).
|
|
72
|
+
- TDD: failing test first. No skipped tests without justification.
|
|
73
|
+
|
|
74
|
+
## ANTI-PATTERNS (THIS PROJECT)
|
|
75
|
+
- **No `# type: ignore` / `# noqa` / `# pyright: ignore` in `paxman/` source** — fix root cause or use scoped ruff `per-file-ignores` (sanctioned pattern in pyproject). Tests may use `# type: ignore[misc]` for immutability checks.
|
|
76
|
+
- Deterministic by construction: given the same input, the same contract, and the same library snapshot (fixed library version, registry contents, and rule-data tables), the pipeline always yields the same canonical output — no world-knowledge, no clock, no environment-dependent ordering, no fuzzy logic, no network inference across recognition, validation, and canonicalization.
|
|
77
|
+
- No cross-capability imports; capabilities import only from `paxman.core`; `paxman.core` imports nothing from `paxman.*`.
|
|
78
|
+
- Grammars must not map tokens to canonical values or import rule-layer data.
|
|
79
|
+
- Rules never contain the token `output_format` (code, comments, or docstrings).
|
|
80
|
+
- No `as any`, no broad exception suppression.
|
|
81
|
+
|
|
82
|
+
## COMMANDS
|
|
83
|
+
```bash
|
|
84
|
+
uv sync --all-extras # install
|
|
85
|
+
uv run ruff check paxman/ tests/ # lint
|
|
86
|
+
uv run ruff format --check paxman/ tests/ # format check
|
|
87
|
+
uv run pyright # strict typecheck
|
|
88
|
+
uv run import-linter lint # layer boundaries
|
|
89
|
+
uv run pytest # all tests
|
|
90
|
+
uv run pytest -m "unit or capability or integration or e2e" # by marker (also: property, benchmark, country, currency, isbn, issn, money, url, si_unit)
|
|
91
|
+
uv run pytest --cov=paxman --cov-report=term-missing --tb=short -q
|
|
92
|
+
uv run coverage report --include="paxman/core/*,paxman/capabilities/*,paxman/engine/*,paxman/api/*" --fail-under=95
|
|
93
|
+
uv run python tools/regenerate_isbn_range_data.py # regenerate ISBN range message module
|
|
94
|
+
uv run python tools/regenerate_si_prefix_data.py # regenerate SIUnit prefixed-unit modules
|
|
95
|
+
uv run python tools/regenerate_idna_uts46_data.py # regenerate URL IDNA UTS #46 mapping
|
|
96
|
+
uv run python tools/regenerate_currency_data.py # regenerate Currency + Money data from shared snapshot
|
|
97
|
+
uv run python -m paxman email "user@example.com" # CLI smoke test
|
|
98
|
+
```
|
|
99
|
+
Full pre-PR gate: `uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run import-linter lint && uv run pytest`
|
|
100
|
+
|
|
101
|
+
## NOTES
|
|
102
|
+
- `paxman/capabilities/__init__.py` exports all twelve shipped capabilities (Country, Currency, Date, Email, IBAN, IP, ISBN, ISSN, Money, Phone, SI Unit, URL); export completeness is enforced by `tests/unit/test_capability_exports.py`.
|
|
103
|
+
- CONTEXT.md is the domain glossary for the full shipped set (twelve capabilities). It is kept in sync with the code; when adding a capability, update its Notation/table entries there too.
|
|
104
|
+
- No `pyrightconfig.json` — pyright config is inline `[tool.pyright]` in pyproject.toml. No `.editorconfig`.
|
|
105
|
+
- Data modules live under `rules/data/` and `grammar/data/` — plain module-level tables separating data from logic. Generated modules (edit via snapshot + regenerate, never by hand): ISBN range message (`tools/regenerate_isbn_range_data.py`), URL IDNA UTS #46 mapping (`tools/regenerate_idna_uts46_data.py`), SIUnit prefixed-unit and grammar token tables (`tools/regenerate_si_prefix_data.py`), and the Currency + Money data set (`tools/regenerate_currency_data.py`, from `paxman/shared_data/currency_snapshot.json`). Unmarked data files are edited directly.
|
|
106
|
+
- Library + CLI: `[project.scripts] paxman = "paxman.cli:main"` and `python -m paxman`; CLI supports `--list`, `--json`, stdin input. Version 0.1.0.
|
|
107
|
+
- Publishing: `.github/workflows/publish.yml` uses PyPI Trusted Publishing (OIDC) with a Git-tag ↔ `pyproject.toml` version safety check; `paxman/py.typed` ships PEP 561 conformance.
|
|
108
|
+
- Coverage: global `fail_under = 95`; `paxman/cli.py` and `paxman/__main__.py` are omitted from coverage (smoke-tested via e2e).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Paxman Architecture
|
|
2
|
+
|
|
3
|
+
Paxman is a canonicalization authority resolver — a library that takes ambiguous human input and returns what authoritative specifications say that input means, with full provenance. This document describes the architectural principles, structural layers, and design decisions that shape the system.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Principles
|
|
8
|
+
|
|
9
|
+
### Determinism
|
|
10
|
+
|
|
11
|
+
Paxman never guesses. Given the same input, the same contract configuration, and the same library snapshot (fixed library version, registry contents, and rule-data tables), the pipeline always yields the same canonical output. This property holds by construction: every pipeline stage — grammar recognition, rule validation, capability formatting, candidate deduplication, and status determination — is a pure function of its inputs with no world-knowledge, no clock, no environment-dependent ordering, no fuzzy logic, and no network inference. Identical inputs therefore always produce identical outputs, enabling auditability and reproducibility. Across library snapshots, provenance or rule-routing changes may alter the resulting metadata; the determinism guarantee is scoped to a fixed snapshot.
|
|
12
|
+
|
|
13
|
+
### Provenance-First
|
|
14
|
+
|
|
15
|
+
Every canonicalized value carries full provenance — a citation of the authoritative specification, registry, or policy that validates it. Provenance is not optional metadata; it is a structural requirement. If no authority can validate a recognized input, the system reports INVALID rather than returning an unvalidated value. This ensures that users always know *why* a value is considered canonical.
|
|
16
|
+
|
|
17
|
+
### Separation of Recognition and Validation
|
|
18
|
+
|
|
19
|
+
Paxman strictly separates the act of finding values in text (recognition) from the act of determining whether those values are valid (validation). This separation is the foundation of the architecture:
|
|
20
|
+
|
|
21
|
+
- **Recognition** (syntactic): Grammars scan raw text and extract structured representations. They do not validate — they only find.
|
|
22
|
+
- **Validation** (semantic): Rules accept structured representations and determine whether authoritative specifications validate them. They produce canonical values with provenance.
|
|
23
|
+
|
|
24
|
+
This separation means that a single input can be recognized by multiple grammars and validated by multiple rules, enabling ambiguity detection when different authoritative sources disagree.
|
|
25
|
+
|
|
26
|
+
### Recognition Pipeline Contract
|
|
27
|
+
|
|
28
|
+
Every grammar implements `recognize(text) -> list[RecognitionMatch]`, where
|
|
29
|
+
`RecognitionMatch` carries the notation plus a half-open `[start, end)` span
|
|
30
|
+
and the matched `raw_text`. The grammar produces positions; the engine owns
|
|
31
|
+
all cross-match policy:
|
|
32
|
+
|
|
33
|
+
- **Containment dedup (per grammar):** a match fully contained in a longer
|
|
34
|
+
match from the SAME grammar is dropped ("longer wins"). Matches from the
|
|
35
|
+
same grammar with identical `[start, end)` spans keep the first-emitted
|
|
36
|
+
match. Dedup never runs across grammars, so two grammars agreeing on the
|
|
37
|
+
same span (e.g. US vs European date reading of `01/02/2026`) are both
|
|
38
|
+
preserved and ambiguity stays observable.
|
|
39
|
+
- **Ordering:** recognitions are emitted in the total order
|
|
40
|
+
`(start, end, active-set index, grammar name)`, i.e. document order — where the
|
|
41
|
+
active set is `contract.active_grammars` or, when the contract returns `None`,
|
|
42
|
+
every shipped grammar in `get_grammars()` order.
|
|
43
|
+
- **Candidate dedup** (`value, recognition_rule, validation_rule`) runs
|
|
44
|
+
after validation as a stability net.
|
|
45
|
+
|
|
46
|
+
Grammars perform syntax-level extraction and normalization only; rules own
|
|
47
|
+
semantic validation with provenance. This contract applies identically to
|
|
48
|
+
every capability, built-in or future.
|
|
49
|
+
|
|
50
|
+
### Capability Isolation
|
|
51
|
+
|
|
52
|
+
Each domain (Email, Date, Country, etc.) is encapsulated as a **Capability** — an independent module that defines its own intermediate representation, recognition rules, and validation rules. Capabilities cannot import from each other. The engine and core domain provide the orchestration layer; capabilities provide the domain expertise.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Structural Layers
|
|
57
|
+
|
|
58
|
+
Paxman is organized into four layers, each with a distinct responsibility. Dependencies flow inward — outer layers depend on inner layers, never the reverse.
|
|
59
|
+
|
|
60
|
+
### Core Domain
|
|
61
|
+
|
|
62
|
+
The innermost layer defines the shared vocabulary and abstract contracts that all other layers consume. It contains:
|
|
63
|
+
|
|
64
|
+
- **Abstract base classes** for Grammars (recognition) and Rules (validation)
|
|
65
|
+
- **Immutable value objects** representing provenance, candidates, recognized representations, and version stamps
|
|
66
|
+
- **Enums** for resolution status (MISSING, INVALID, SUCCESS, AMBIGUOUS) and rule strategies (REGEX, LOOKUP_TABLE, PARSER)
|
|
67
|
+
- **The Contract protocol** — a structural interface that all capability contracts must satisfy
|
|
68
|
+
- **The Capability abstract class** — a base class that all capability implementations must extend
|
|
69
|
+
- **The discovery registry** — a module-level registry that manages capability registration and lookup
|
|
70
|
+
- **Exception hierarchy** — typed errors for different failure modes
|
|
71
|
+
|
|
72
|
+
The core layer has no knowledge of specific capabilities. It defines *what* a capability is, not *how* any particular capability works.
|
|
73
|
+
|
|
74
|
+
### Capabilities
|
|
75
|
+
|
|
76
|
+
Each capability is a self-contained domain module that provides:
|
|
77
|
+
|
|
78
|
+
- **A Notation type** — a typed intermediate representation specific to the domain (e.g., email local part and domain part, date N1/N2/N3)
|
|
79
|
+
- **Grammars** — recognition rules that extract the notation from raw text
|
|
80
|
+
- **Validation Rules** — semantic rules that validate the notation against authoritative specifications
|
|
81
|
+
- **A Contract** — a user-facing configuration object that toggles grammars, excludes rules, and passes parameters
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
### Engine
|
|
86
|
+
|
|
87
|
+
The engine is the orchestration layer that coordinates the full pipeline. It:
|
|
88
|
+
|
|
89
|
+
1. Freezes the capability registry
|
|
90
|
+
2. Looks up the requested capability by name
|
|
91
|
+
3. Runs the recognition phase — iterating over active grammars to extract span-bearing recognition matches
|
|
92
|
+
4. Runs the validation phase — testing each notation against active rules, which normalize to each capability's default canonical form and never inspect `output_format`
|
|
93
|
+
5. Formats each validated value through the capability's `format_value()` seam — immediately after normalization and before deduplication and status determination
|
|
94
|
+
6. Deduplicates identical candidates
|
|
95
|
+
7. Determines the resolution status based on candidate outcomes
|
|
96
|
+
8. Assembles the final execution result
|
|
97
|
+
|
|
98
|
+
The engine is capability-agnostic. It does not know what a "grammar" or "rule" does — it only knows that grammars produce span-bearing recognition matches and rules produce candidates.
|
|
99
|
+
|
|
100
|
+
Before the recognition phase, the engine composes each capability's shipped grammars and rules with any community extensions registered for that capability (see "Community Extensions" below). Composition is guarded: duplicate names fail fast, and every rule's declared `target_semantics` must resolve within the composed set.
|
|
101
|
+
|
|
102
|
+
### Public API
|
|
103
|
+
|
|
104
|
+
The outermost layer exposes the user-facing interface. It is intentionally minimal — a single entry point that accepts input text and a contract, and returns a fully-resolved execution result with full provenance.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Key Architectural Patterns
|
|
109
|
+
|
|
110
|
+
### Protocol-Based Contracts
|
|
111
|
+
|
|
112
|
+
Contracts are defined as structural protocols (`Contract`), not inheritance-based base classes. Any class that satisfies the structural interface — providing the required attributes and methods — qualifies as a contract. This allows capability authors to design contract objects that fit their domain (using dataclasses, Pydantic models, etc.) without being constrained by a base class hierarchy. This prioritizes **user flexibility** and **decoupling**.
|
|
113
|
+
|
|
114
|
+
### ABC-Based Capabilities
|
|
115
|
+
|
|
116
|
+
In contrast to contracts, Capabilities are defined as Abstract Base Classes (`Capability`). This prioritizes **internal rigidity** and **reliability**. Since capabilities are internal components managed by the engine's registry, strict inheritance ensures they adhere to the required structure (`get_grammars()`, `get_rules()`) and prevents runtime errors during discovery.
|
|
117
|
+
|
|
118
|
+
### Capability as Factory
|
|
119
|
+
|
|
120
|
+
Capabilities do not hold state. They are factories that produce grammars and rules on demand. The engine queries a capability for its available grammars and rules, then filters them based on the contract configuration. This design keeps capabilities lightweight and makes the filtering logic centralized in the engine.
|
|
121
|
+
|
|
122
|
+
### Typed Notation
|
|
123
|
+
|
|
124
|
+
Each capability defines a frozen dataclass Notation with one `str` field per recognized component (e.g., `DateNotation` carries `N1`, `N2`, `N3`). The concrete notation type is the sole type parameter threaded end to end: `Grammar[NotationT]` recognizes it and `Rule[NotationT]` validates it, so the engine and every rule operate on the fully typed object rather than a positional `list[str]`. Rules read notation fields by name (e.g., `notation.N1`); there is no generic list alias and no conversion bridge between the typed notation and a list form.
|
|
125
|
+
|
|
126
|
+
### Contract Parameters
|
|
127
|
+
|
|
128
|
+
Contracts pass configuration parameters to validation rules, enabling rules to adapt their behavior based on user preferences.
|
|
129
|
+
|
|
130
|
+
**Base Contract Parameters:**
|
|
131
|
+
- **`output_format`**: Controls the canonical value format (e.g., `"ISO"` for `YYYY-MM-DD`, `"US"` for `MM/DD/YYYY`). `CapabilityContract.__post_init__` resolves `None`, `"default"`, and each capability's default format to a concrete string; the capability's `format_value()` seam applies the format to the rule-produced default canonical value. Validation rules never inspect `output_format` — they always normalize to the default canonical form. See "The Formatting Seam" below.
|
|
132
|
+
- **`pinned_rules`**: Pins to specific validation rules by name. When set, ONLY those rules run — `excluded_rules` is ignored. Takes precedence over `excluded_rules`.
|
|
133
|
+
- **`extra_grammars`**: Names community grammars (opt-in) to run alongside the capability's shipped active set — `contract.active_grammars`, or every shipped grammar when the contract returns `None` — in order. Unknown names are silently skipped; shipped names listed here are deduplicated. Registration happens through `paxman.register_grammar` / `paxman.register_rule` (see "Community Extensions" below).
|
|
134
|
+
|
|
135
|
+
**Date-Specific Parameters:**
|
|
136
|
+
- **`two_digit_base_year`**: Specifies the base year for interpreting two-digit years (e.g., `2000` means `"26"` becomes `2026`). Only available on Date contracts, not part of the base Contract protocol. Used by US and European grammars to resolve ambiguous year values.
|
|
137
|
+
|
|
138
|
+
These parameters are passed through the contract to rule methods (`matches()` and `normalize()`), allowing rules to be contract-aware without direct coupling to specific capabilities. `output_format` is the exception: it is a presentation parameter consumed by the capability's formatting seam, never by validation rules.
|
|
139
|
+
|
|
140
|
+
### The Formatting Seam
|
|
141
|
+
|
|
142
|
+
Validation and presentation are separated at the pipeline level. Rules own validation and default normalization only: `matches()` never consults `output_format`, and `normalize()` always returns the capability's default canonical form (e.g., `YYYY-MM-DD` for Date, E.164 `+CCNSN` for Phone, alpha-2 for Country). The engine then renders each validated value through the capability's `format_value(value, output_format, notation)` method — called immediately after `normalize()` and before candidate deduplication and status determination:
|
|
143
|
+
|
|
144
|
+
**recognition → validation → default normalization → capability formatting → candidate deduplication → status → result**
|
|
145
|
+
|
|
146
|
+
Formatting adds no provenance: `Candidate.provenance`, `recognition_rule`, and `validation_rule` are set from the rule that validated the notation, and the formatter only transforms the value. Date, Phone, and Country implement conversions; Email and IP inherit the identity implementation because they offer no alternative formats.
|
|
147
|
+
|
|
148
|
+
A CI source scan (`tests/unit/test_rule_output_format_purity.py`) rejects any `output_format` token in `paxman/capabilities/*/rules/` modules — in code, comments, or docstrings — so presentation cannot migrate back into rules. In the Country capability, localized names (e.g., `Alemania` → `DE`) are formatted through the current alpha-2 conversion tables for `alpha3`, `numeric`, and `name` while retaining Unicode/CLDR provenance; historical former codes (e.g., `SU`) pass through unchanged for those formats when no current mapping exists, retaining ISO 3166-3 provenance.
|
|
149
|
+
|
|
150
|
+
### Immutability
|
|
151
|
+
|
|
152
|
+
All domain objects are immutable. Once created, they cannot be modified. This is enforced through `@dataclass(frozen=True, slots=True)` — stdlib dataclasses that prevent attribute assignment and use efficient slot-based storage. Immutability ensures that objects can be safely shared, hashed, and used as dictionary keys without defensive copying.
|
|
153
|
+
|
|
154
|
+
### Temporal Filtering
|
|
155
|
+
|
|
156
|
+
Rules carry a publication year from their authoritative specification. When a contract specifies a year, the engine filters out rules whose publication year exceeds that year. This allows users to pin to a specific historical version of a specification, excluding rules from newer revisions.
|
|
157
|
+
|
|
158
|
+
### Determinism by Construction
|
|
159
|
+
|
|
160
|
+
Determinism is a structural property of the layered pipeline, not a post-hoc artifact:
|
|
161
|
+
|
|
162
|
+
- **Recognition layer.** Active grammars emit span-bearing `RecognitionMatch` objects from the input text, matching the `Grammar.recognize()` contract. Grammar output depends only on the input and the grammar itself.
|
|
163
|
+
- **Validation layer.** Rules accept recognized representations and produce candidates, each carrying a canonical value and provenance. A rule's output depends only on the representation and the contract.
|
|
164
|
+
- **Result layer.** The engine deduplicates identical candidates and folds the distinct candidate values into one of the resolution statuses: `SUCCESS` when all candidates agree on a single canonical value, `AMBIGUOUS` when they disagree, `INVALID` when nothing validated, and `MISSING` when no grammar recognized anything.
|
|
165
|
+
|
|
166
|
+
Every stage is a pure function of its inputs — no clocks, no randomness, no environment-dependent ordering, no world-knowledge, no fuzzy logic, no network inference — so the same input, contract configuration, and library snapshot (fixed library version, registry contents, and rule-data tables) always produce the same canonical output. The `VersionStamp` on each execution result records the library version for provenance; the determinism guarantee rests on this determinism-by-construction. Changes to provenance or rule routing across library snapshots can alter result metadata; the determinism guarantee is scoped to a fixed snapshot.
|
|
167
|
+
|
|
168
|
+
### Community Extensions
|
|
169
|
+
|
|
170
|
+
Capabilities are closed for modification but open for extension. Community contributors register additional grammars (and the rules that validate them) against an existing capability through `paxman.core.extensions` — never by editing the capability package. The registries freeze with the capability registry at the first pipeline run.
|
|
171
|
+
|
|
172
|
+
A contract opts a registered grammar in by naming it in `extra_grammars`, a base `CapabilityContract` field surfaced on every `create_contract` factory. The engine composes the shipped active set with the opted-in extras, deduplicating names while preserving order — shipped slots first, extras after (unknown extra names are silently skipped). The shipped slots are `contract.active_grammars` when the contract implements it (the gated capabilities), or every shipped grammar in `get_grammars()` order when it returns `None` (the base default). Opt-in preserves determinism: a contract that names no extras composes to exactly the shipped set, so non-opt-in behavior is identical (deterministic).
|
|
173
|
+
|
|
174
|
+
Community rules follow the same opt-in discipline: a registered rule runs only when the contract's `extra_grammars` resolve to one of its `target_semantics` ids. An un-opted community rule — even one targeting a shipped grammar's semantics — never affects results, so a default contract resolves with shipped rules only.
|
|
175
|
+
|
|
176
|
+
Composition is guarded at pipeline start: a community grammar name colliding with a shipped name raises `CapabilityError`, and an opted-in community rule whose `target_semantics` names an id no grammar claims raises `ContractError` — failing fast rather than producing a silently wrong result. Community grammars and rules are pure functions of their inputs, and the composed set is fixed once the registries freeze, so the determinism guarantees of "Determinism by Construction" extend unchanged.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Resolution Semantics
|
|
181
|
+
|
|
182
|
+
The system produces one of four resolution statuses:
|
|
183
|
+
|
|
184
|
+
| Status | Meaning |
|
|
185
|
+
|--------|---------|
|
|
186
|
+
| **MISSING** | No grammars recognized anything in the input. The input does not match any known pattern. |
|
|
187
|
+
| **INVALID** | Grammars recognized the input, but no validation rule could validate it against an authoritative specification. |
|
|
188
|
+
| **SUCCESS** | One or more rules validated the input, and all agree on the same canonical value. |
|
|
189
|
+
| **AMBIGUOUS** | Multiple rules validated the input but produced different canonical values. The system cannot determine which is correct. |
|
|
190
|
+
|
|
191
|
+
Ambiguity is detected at the value level, not the candidate level. Multiple candidates with the same canonical value still produce SUCCESS. Ambiguity requires genuinely different canonical outputs from different authoritative sources.
|
|
192
|
+
|
|
193
|
+
For multi-entity input, segmentation is caller-owned — see the [segmentation recipe](docs/recipes/segmentation.md) (ADR-0004 companion).
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Error Handling
|
|
198
|
+
|
|
199
|
+
The exception hierarchy separates different failure modes:
|
|
200
|
+
|
|
201
|
+
- **CapabilityError** — the requested capability is unknown or the registry is in an invalid state
|
|
202
|
+
- **ContractError** — the contract configuration is malformed or missing required fields
|
|
203
|
+
- **RecognitionError** — a grammar failed during recognition (e.g., malformed regex), wrapping the original exception
|
|
204
|
+
- **ValidationError** — a rule failed during validation (e.g., unexpected data), wrapping the original exception
|
|
205
|
+
|
|
206
|
+
Recognition and validation errors carry the name of the offending rule and the original exception, enabling targeted debugging without losing context.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Quality Enforcement
|
|
211
|
+
|
|
212
|
+
Paxman enforces architectural invariants through tooling:
|
|
213
|
+
|
|
214
|
+
- **Static type checking** in strict mode ensures type safety across all layers
|
|
215
|
+
- **Import boundary enforcement** prevents capability-to-capability dependencies and ensures the core layer remains independent
|
|
216
|
+
- **Linting and formatting** enforce consistent code style
|
|
217
|
+
- **Rule-purity source scan** fails CI when any validation-rule module under `paxman/capabilities/*/rules/` references `output_format`, enforcing that presentation is owned solely by `Capability.format_value()`
|
|
218
|
+
- **Property-based testing** validates domain object contracts (immutability, equality, hashability)
|
|
219
|
+
|
|
220
|
+
These tools run as part of the development workflow and block merges when invariants are violated.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Date Capability Design
|
|
225
|
+
|
|
226
|
+
The Date capability demonstrates the system's handling of ambiguous inputs through multiple grammars and validation rules.
|
|
227
|
+
|
|
228
|
+
### Grammars
|
|
229
|
+
|
|
230
|
+
Three grammars recognize date patterns with different position mappings:
|
|
231
|
+
|
|
232
|
+
| Grammar | Delimiter | N1 (first) | N2 (second) | N3 (third) | Notes |
|
|
233
|
+
|---------|-----------|------------|-------------|------------|-------|
|
|
234
|
+
| ISO | `-` | year | month | day | 4-digit year only |
|
|
235
|
+
| US | `/` | month | day | year | Supports 2-digit years |
|
|
236
|
+
| European | `/` | day | month | year | Supports 2-digit years |
|
|
237
|
+
|
|
238
|
+
European and US grammars both use `/` as delimiter. Ambiguity arises from different position mappings, not delimiters.
|
|
239
|
+
|
|
240
|
+
### Validation Rules
|
|
241
|
+
|
|
242
|
+
Three rules validate date notations against authoritative specifications:
|
|
243
|
+
|
|
244
|
+
| Rule | Standard | Canonical Output |
|
|
245
|
+
|------|----------|------------------|
|
|
246
|
+
| ISO 8601 | ISO 8601:2019 | `YYYY-MM-DD` |
|
|
247
|
+
| US federal | US government standard | `YYYY-MM-DD` |
|
|
248
|
+
| EN 50160 | European EN 50160 | `YYYY-MM-DD` |
|
|
249
|
+
|
|
250
|
+
All rules normalize to ISO 8601 format (`YYYY-MM-DD`) regardless of input grammar; the capability's formatting seam applies a requested alternative (e.g., `output_format="US"`) to that default afterward.
|
|
251
|
+
|
|
252
|
+
### Ambiguity Detection
|
|
253
|
+
|
|
254
|
+
When the same input is recognized by multiple grammars, each grammar produces notation with different position mappings. For example, `"01/02/2026"` is recognized by both US and European grammars:
|
|
255
|
+
- US grammar: N1=month=01, N2=day=02, N3=year=2026
|
|
256
|
+
- European grammar: N1=day=01, N2=month=02, N3=year=2026
|
|
257
|
+
|
|
258
|
+
Each grammar's notation flows to its corresponding validation rule. If both rules validate and produce different canonical values, the system reports AMBIGUOUS.
|