Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b8e8145
docs(plans): add mid-term recommendations implementation plan (ocs/de…
azaharizaman Aug 19, 2026
802ddf6
docs(adr): add ADR-0007 contract surface unification (Item 5)
azaharizaman Aug 19, 2026
59c018a
test(core): add failing contract surface unification tests (Item 5 RED)
azaharizaman Aug 19, 2026
68049f7
refactor(core): unify contract surface — CapabilityContract single so…
azaharizaman Aug 19, 2026
450a07a
docs(core): polish contract exports and HOWTO for ADR-0007 (Item 5)
azaharizaman Aug 19, 2026
ca2e5f8
feat(shared-data): scaffold shared CLDR/ISO4217 snapshot for Currency…
azaharizaman Aug 19, 2026
26b10f1
feat(tools): add regenerate_currency_data with --check (Item 6)
azaharizaman Aug 19, 2026
7a7533f
feat(currency): generate Currency tables from shared snapshot (Item 6)
azaharizaman Aug 19, 2026
9132b2b
feat(money): generate Money tables from shared snapshot (Item 6)
azaharizaman Aug 19, 2026
2db9143
ci: add drift checks for Currency/Money shared snapshot (Item 6)
azaharizaman Aug 19, 2026
b330953
feat(benchmarks): add minimal harness skeleton (Item 7)
azaharizaman Aug 19, 2026
3a69fd8
feat(benchmarks): add 10 deterministic scenarios, one per capability …
azaharizaman Aug 19, 2026
3cbbc0b
ci(benchmarks): add baseline and informational bench job (Item 7)
azaharizaman Aug 19, 2026
0556ccc
docs(benchmarks): add harness README (Item 7)
azaharizaman Aug 19, 2026
6386030
test(capabilities): add failing PEP 562 lazy-import tests (Item 8 RED)
azaharizaman Aug 19, 2026
cd6d0cf
perf(capabilities): adopt PEP 562 lazy exports for W4 (Item 8)
azaharizaman Aug 19, 2026
0f69fa8
fix(tools): support PEP 562 lazy exports in new_capability scaffolder…
azaharizaman Aug 19, 2026
ac003ed
fix(engine): fail-fast ContractError for missing extra_grammars (ADR-…
azaharizaman Aug 19, 2026
36502c4
fix: address PR review comments — validate harness, tighten contract …
azaharizaman Aug 20, 2026
8e974df
fix: address PR review — behavioral extra_grammars coverage, harness …
azaharizaman Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ jobs:
- name: Import lint
run: uv run import-linter lint

- name: Check generated Currency/Money data drift
run: uv run python tools/regenerate_currency_data.py --check

- name: Check generated SIUnit data drift
run: uv run python tools/regenerate_si_prefix_data.py --check

- name: Benchmark (informational, non-blocking)
continue-on-error: true
run: uv run python -m benchmarks.harness --iterations 50 --output /tmp/bench.json && cat /tmp/bench.json

- name: Tests with coverage
run: uv run pytest --cov=paxman --cov-report=term-missing --cov-report=html --tb=short -q

Expand Down
2 changes: 2 additions & 0 deletions HOW_TO_ADD_NEW_CAPABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ Every contract and rule in the codebase follows the same structural rules. The `

**1. Every contract MUST inherit `CapabilityContract`**

Subclass `CapabilityContract` (never `Contract` directly). `Contract` is engine-internal since ADR-0007.

Import it from `paxman.core.contract` (defined in `paxman/core/capability_contract.py`). Your contract is a frozen dataclass subclass:

```python
Expand Down
11 changes: 11 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Benchmarks (Item 7, W5)

One scenario per capability, deterministic per (input, contract, library snapshot).

Run:
uv run python -m benchmarks.harness --iterations 200
uv run python -m benchmarks.harness --output bench.json
uv run python -m benchmarks.harness --update-baseline # refresh committed baseline.json

CI runs 50 iterations informational (non-blocking). Baseline is tracked but not gated.
Add a new capability: add one entry to `benchmarks/scenarios.py` with `register` + `contract_factory`.
Empty file added benchmarks/__init__.py
Empty file.
95 changes: 95 additions & 0 deletions benchmarks/baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"scenarios": [
{
"capability": "country",
"iterations": 100,
"mean_ms": 0.27881579706445336,
"p50_ms": 0.20324101205915213,
"p95_ms": 0.31342904549092054,
"min_ms": 0.15853706281632185,
"max_ms": 4.520129994489253
},
{
"capability": "currency",
"iterations": 100,
"mean_ms": 0.1070823660120368,
"p50_ms": 0.10521896183490753,
"p95_ms": 0.12719491496682167,
"min_ms": 0.090339919552207,
"max_ms": 0.15212292782962322
},
{
"capability": "date",
"iterations": 100,
"mean_ms": 0.11676306487061083,
"p50_ms": 0.0926790526136756,
"p95_ms": 0.17091899644583464,
"min_ms": 0.0784930307418108,
"max_ms": 0.26655092369765043
},
{
"capability": "email",
"iterations": 100,
"mean_ms": 0.09656466194428504,
"p50_ms": 0.09762297850102186,
"p95_ms": 0.11605804320424795,
"min_ms": 0.060654012486338615,
"max_ms": 0.17720204778015614
},
{
"capability": "ip",
"iterations": 100,
"mean_ms": 0.13068879838101566,
"p50_ms": 0.14457793440669775,
"p95_ms": 0.17728202510625124,
"min_ms": 0.08774595335125923,
"max_ms": 0.27406506706029177
},
{
"capability": "isbn",
"iterations": 100,
"mean_ms": 0.13500795583240688,
"p50_ms": 0.13043393846601248,
"p95_ms": 0.18328602891415358,
"min_ms": 0.0971390400081873,
"max_ms": 0.20166800823062658
},
{
"capability": "money",
"iterations": 100,
"mean_ms": 0.20503543317317963,
"p50_ms": 0.20394206512719393,
"p95_ms": 0.246229930780828,
"min_ms": 0.11839496437460184,
"max_ms": 0.5321740172803402
},
{
"capability": "phone",
"iterations": 100,
"mean_ms": 0.18060301197692752,
"p50_ms": 0.18282991368323565,
"p95_ms": 0.21669501438736916,
"min_ms": 0.10399601887911558,
"max_ms": 0.46293798368424177
},
{
"capability": "si_unit",
"iterations": 100,
"mean_ms": 0.12988552218303084,
"p50_ms": 0.12476707343012094,
"p95_ms": 0.15376601368188858,
"min_ms": 0.11326710227876902,
"max_ms": 0.2440139651298523
},
{
"capability": "url",
"iterations": 100,
"mean_ms": 0.23713358445093036,
"p50_ms": 0.2435100032016635,
"p95_ms": 0.27114199474453926,
"min_ms": 0.14298106543719769,
"max_ms": 0.47093897592276335
}
],
"iterations": 100
}
97 changes: 97 additions & 0 deletions benchmarks/harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Minimal benchmark harness — one scenario per capability (W5, Item 7).

No external deps. Deterministic per contract+input+library snapshot.
Measures wall-clock via time.perf_counter, reports mean/p50/p95 per scenario.
Used in CI as non-blocking signal and locally via --update-baseline.

Usage:
uv run python -m benchmarks.harness --iterations 200
uv run python -m benchmarks.harness --output /tmp/bench.json
uv run python -m benchmarks.harness --update-baseline
"""

from __future__ import annotations

import argparse
import json
import statistics
import sys
import time
from pathlib import Path

from benchmarks.scenarios import SCENARIOS

ROOT = Path(__file__).resolve().parents[1]
BASELINE = ROOT / "benchmarks" / "baseline.json"


def run_once(
capability: str, text: str, iterations: int = 100
) -> dict[str, float | str | int]:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
from paxman import canonicalize
from paxman.core.discovery import reset_registry

if iterations <= 0:
raise ValueError(f"iterations must be a positive integer, got {iterations!r}")

# Import capability lazily to respect PEP 562 (Item 8)
# — harness must not hide import cost
# Scenarios carry a factory: lambda: (contract, text)
scenario = next(s for s in SCENARIOS if s["capability"] == capability)
durations: list[float] = []
for _ in range(iterations):
reset_registry()
# Register only the needed capability for this scenario
# (isolates import cost if lazy)
scenario["register"]()
contract = scenario["contract_factory"]()
start = time.perf_counter()
canonicalize(text, contract)
durations.append((time.perf_counter() - start) * 1000)
# reset_registry cleans for next iteration
reset_registry()
durations.sort()
p50 = durations[len(durations) // 2]
p95 = durations[int(len(durations) * 0.95)]
return {
"capability": capability,
"iterations": iterations,
"mean_ms": statistics.mean(durations),
"p50_ms": p50,
"p95_ms": p95,
"min_ms": min(durations),
"max_ms": max(durations),
}


def main(argv: list[str] | None = None) -> None:
parser = argparse.ArgumentParser(description="Paxman benchmark harness (Item 7)")
parser.add_argument("--iterations", type=int, default=100)
parser.add_argument("--output", type=str, default=None)
parser.add_argument("--update-baseline", action="store_true")
args = parser.parse_args(argv)

results: list[dict[str, float | str | int]] = []
for scenario in SCENARIOS:
results.append(
run_once(
scenario["capability"], scenario["text"], iterations=args.iterations
)
)

payload: dict[str, object] = {"scenarios": results, "iterations": args.iterations}
if args.output:
Path(args.output).write_text(
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
)
print(f"wrote {args.output}")
elif args.update_baseline:
BASELINE.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"wrote {BASELINE}")
else:
json.dump(payload, sys.stdout, indent=2)
sys.stdout.write("\n")


if __name__ == "__main__":
main()
Loading
Loading