docs/calculator-test-patterns: 3 new patterns from π* kernel work + 4 exemplar files

Land the three test-pattern shapes the Explore-agent investigation
of fox's overnight π* kernel commits surfaced. Patterns 1-8 in
this doc were the original 9-item checklist; patterns 9-11 are
new domain-specific contract pins that the π* kernels require but
that t3_bound_calculator (the original exemplar) does not.

== New patterns added ==

**§9 Projective-contract pin (one-way canonicalizers)**

For canonicalizers whose output type ≠ input type — output is
not in the input domain by design — assert that re-applying the
function raises. Pinned in code-py-ast@v1 (Python → S-expr) and
time-series-quantized@v1 (JSON → quantized text). When the output
type DOES equal the input type (arithmetic kernel's ℚ → ℚ),
use the dual: round-trip idempotence
``canonicalize(canonicalize(x)) == canonicalize(x)``. Both pin a
contract; pick by the kernel's type signature.

**§10 Dispatch-order pin (Python type-hierarchy gotchas)**

Python's ``bool`` subclasses ``int``, so a naive ``isinstance(x,
int)`` chain never reaches a bool branch. Kernels distinguishing
``True`` from ``1`` (Python AST normalizers, etc) must check
``bool`` first. Pin the branch order so a "simplify the dispatch"
PR fires loud. From ``593550b``.

**§11 Tie-breaking-rule pin (banker's rounding)**

Python's ``round()`` uses ties-to-even (PEP 3141): 0.5→0, 1.5→2,
2.5→2. Naive switch to ``math.floor(x + 0.5)`` (round-half-up)
produces 0.5→1, 1.5→2, 2.5→3 — different output for tie inputs
without breaking non-tie tests. From ``2585d3c``.

Each pattern has worked-example pseudocode + cross-reference to
the actual test file in fox's commit. The pattern numbers extend
the existing 1-8 sequence; renumbering would have invalidated
prior references.

== Checklist updates ==

Items 10/11/12 added (conditional — only when the kernel's shape
exposes the corresponding surface). Many calculator modules
(t3 bound, anchor PRG) need only items 1-9.

== Exemplar files reorganized ==

Replaced the single-exemplar reference (t3_bound_calculator only)
with a 5-file table cross-referencing the 9-12 checklist items
each exemplar covers:

  test_t3_bound_calculator.py    items 1-9 (53 tests)
  test_pi_star_arithmetic.py     items 1-6 + 9 + idempotence (56)
  test_pi_star_logic.py          items 1-6 + 9 + 11 (53)
  test_pi_star_code.py           items 1-6 + 9 + 10 + 11 (32)
  test_pi_star_time_series.py    items 1-6 + 9 + 10 + 12 (35)

All five test counts AUTOCOUNT-tagged so future drift fires the
regression test landed in ``fc5ba50`` / ``03c0f6a``. Total tagged
claims now 49 (was 44; +5).

== Source ==

Patterns surfaced from the Explore-agent investigation of fox's
overnight 2026-05-10 commits (``6c9bc04`` arithmetic, ``e7bef5f``
logic, ``593550b`` code, ``2585d3c`` time-series — 176 KATs total
across 4 π* canonical-projection kernels). The agent walked each
commit, noted the test patterns that didn't appear in the
original 9-item checklist, and reported the pattern shapes back.
This commit promotes those findings from session memory to
architecture-reference docs.

Verification:

  $ pytest tests/test_doc_counts.py -v
  3 passed in 3.39s
This commit is contained in:
russell@unturf.com 2026-05-10 16:33:19 -04:00
parent a786d6d206
commit f5dbfabed5
No known key found for this signature in database

View file

@ -243,6 +243,106 @@ asserts ``asdict(report)`` is JSON-serializable. Catches
non-stdlib types leaking into the dataclass that would break
downstream consumers.
### 9. Projective-contract pin (one-way canonicalizers)
For canonicalizers whose output type ≠ input type — output is
**not** in the input domain by design — assert that re-applying
the function raises. Catches a future "make it round-trippable"
PR that silently breaks the projection.
**Why**: the contract is "one-way projection," not "fixed-point
normalization." If the output were re-canonicalizable, downstream
code might come to depend on idempotence; the test pins the
boundary.
**Examples** —
- ``arborist/pi_star/code_py_ast.py`` (commit ``593550b``):
Python source → S-expression. Re-canonicalizing the
S-expression text raises ``PiStarError`` because it isn't
valid Python source.
- ``arborist/pi_star/time_series_quantized.py`` (commit
``2585d3c``): JSON ``{dt, dv, samples}`` → quantized text
``dt=...;dv=...;n=...;t0=...:v0|v1|...``. The quantized text
isn't JSON; re-canonicalizing raises.
**Pattern code**::
def test_output_is_projective_not_invertible():
canonical = canonicalize(input_value)
with pytest.raises(PiStarError):
canonicalize(canonical)
When the output type **does** equal the input type (e.g. the
arithmetic kernel's rationals), use the dual pattern —
**round-trip idempotence**: ``canonicalize(canonicalize(x)) ==
canonicalize(x)``. Both patterns pin a contract; pick whichever
matches the kernel's type signature.
### 10. Dispatch-order pin (Python type-hierarchy gotchas)
For functions branching on ``isinstance`` over numeric types,
pin the branch order. ``bool`` subclasses ``int`` in Python, so
this naive chain never reaches the bool branch::
if isinstance(x, int): # True is also an int
...
elif isinstance(x, bool): # unreachable
...
A kernel that distinguishes ``True`` from ``1`` (e.g. a Python
AST normalizer) must check ``bool`` first. Pin the branch order
in a test so a future "simplify the dispatch" PR fires loud.
**Why**: subtle Python type-hierarchy gotcha that no other test
shape catches. Determinism + KAT tests pass either way at the
boundary case ``True``; only an explicit dispatch-order pin
exposes the regression.
**Example** — ``arborist/pi_star/code_py_ast.py`` (commit
``593550b``)::
def test_bool_dispatched_before_int():
assert canonicalize("True") != canonicalize("1")
# Both are int-ish; bool-first dispatch keeps them distinct.
**How to apply**: any kernel that branches on ``isinstance`` for
numeric types should have a paired test pinning the branch order.
Same applies to ``Decimal`` vs ``float``, ``bytes`` vs ``bytearray``,
``int`` vs ``numpy.int64`` — anywhere Python's MRO crosses a
distinguishing boundary.
### 11. Tie-breaking-rule pin (ties-to-even / banker's rounding)
For functions using ``round()``, pin the tie-breaking direction.
Python's ``round()`` uses **banker's rounding** by default
(ties-to-even, PEP 3141): 0.5→0, 1.5→2, 2.5→2. A naive switch to
``math.floor(x + 0.5)`` ("round half up": 0.5→1, 1.5→2, 2.5→3)
silently changes output for tie inputs without breaking
non-tie tests.
**Why**: many programmers expect round-half-up; a contributor
unfamiliar with PEP 3141 will refactor to ``floor(x + 0.5)``
"to be explicit" and silently corrupt quantization. Pin the rule
with the actual tie inputs.
**Example** — ``arborist/pi_star/time_series_quantized.py``
(commit ``2585d3c``)::
@pytest.mark.parametrize("v_in,v_out", [
(0.5, 0), # ties-to-even: 0 is even
(1.5, 2), # ties-to-even: 2 is even
(2.5, 2), # ties-to-even: 2 is even (NOT 3)
(3.5, 4), # ties-to-even: 4 is even
])
def test_quantization_uses_bankers_rounding(v_in, v_out):
assert quantize_value(v_in, dv=1) == v_out
**How to apply**: any kernel using ``round()`` or any quantization
that sits on a discrete grid should have a parametrized test
covering at least 4 ties (mix of round-up-to-even and
round-down-to-even cases).
---
## Checklist for new calculator-style code
@ -263,10 +363,42 @@ When opening a new ``bench/scripts/<X>.py`` or substrate primitive:
8. [ ] Doc-parity test if the spec doc has worked-example numbers
9. [ ] Module-export shape test (asdict round-trip,
JSON-serializability, version token presence)
10. [ ] Projective-contract pin if output type ≠ input type
(re-canonicalizing the canonical output raises) — OR
round-trip idempotence pin if output type = input type
11. [ ] Dispatch-order pin if the kernel branches on
``isinstance`` for numeric types (bool-before-int, etc)
12. [ ] Tie-breaking-rule pin if the kernel uses ``round()`` or
sits on a discrete quantization grid
The <!--AUTOCOUNT:tests:tests/test_t3_bound_calculator.py-->53<!--/AUTOCOUNT--> tests fox shipped for ``t3_bound_calculator`` are the
exemplar; new modules should aim for similar coverage density on
their own surface.
Items 10/11/12 are conditional — only apply when the kernel's
shape exposes the corresponding surface. Many calculator modules
(e.g. T3 bound, anchor PRG) need only items 1-9.
Exemplar test files:
- ``tests/test_t3_bound_calculator.py`` —
<!--AUTOCOUNT:tests:tests/test_t3_bound_calculator.py-->53<!--/AUTOCOUNT--> tests covering
items 1-9 for the T3 closed-form bound.
- ``tests/test_pi_star_arithmetic.py`` —
<!--AUTOCOUNT:tests:tests/test_pi_star_arithmetic.py-->56<!--/AUTOCOUNT--> tests covering items
1-6 + 9 + round-trip idempotence (output type = input type =
rationals). From ``6c9bc04`` 2026-05-10.
- ``tests/test_pi_star_logic.py`` —
<!--AUTOCOUNT:tests:tests/test_pi_star_logic.py-->53<!--/AUTOCOUNT--> tests covering items
1-6 + 9 + 11 (tie-breaking on multi-tautology equivalence-class
collapse). From ``e7bef5f`` 2026-05-10.
- ``tests/test_pi_star_code.py`` —
<!--AUTOCOUNT:tests:tests/test_pi_star_code.py-->32<!--/AUTOCOUNT--> tests covering items
1-6 + 9 + 10 (projective: Python → S-expr) + 11 (dispatch:
bool-before-int). From ``593550b`` 2026-05-10.
- ``tests/test_pi_star_time_series.py`` —
<!--AUTOCOUNT:tests:tests/test_pi_star_time_series.py-->35<!--/AUTOCOUNT--> tests covering
items 1-6 + 9 + 10 (projective: JSON → quantized text) + 12
(banker's rounding pin). From ``2585d3c`` 2026-05-10.
New modules should aim for similar coverage density on their
own surface.
---