arborist/tests/test_pi_star.py
russell@unturf.com 40d106fb2f
pi_star: land ticket #000015 (π* domain library + composition algebra)
New arborist.pi_star/ namespace centralizes canonical projections
under a name@version registry. Two existing canonicalizers re-homed
as registered π*'s:

- wikitext-base@v1 wraps arborist.wikitext.to_base
- claim-lattice@v1 wraps arborist.qa.parse_claims.parse_pointer_claims

Four stubs registered for follow-up modality tickets:
code-py-ast@v1, logic-kernel@v1, time-series-quantized@v1,
tabular-pinned@v1 — each raises NotImplementedError with a pointer
to ticket #000015.

Composition algebra in compose.py: PiStarComposition exposes
outer ∘ inner as a first-class π* with its own registry key
(default "<inner-name>-then-<outer-name>@v1"). canonical_composition_id
returns a SHA-256 fingerprint suitable for governance hash inclusion.
Order-sensitive: a∘b ≠ b∘a → different fingerprints.

Documentation: docs/pi-star-composition.md covers the rule (type-
compatible, deterministic, equivalence-class preserving), lossy vs
invertible compositions, worked text→claim-lattice example,
cross-domain anchor projections (future), authoring checklist.

Re-home is non-breaking: arborist.wikitext.to_base remains importable.
Tests: tests/test_pi_star.py (19 cases). Full suite: 1059 passed,
36 skipped.
2026-05-07 16:51:33 -04:00

196 lines
5.5 KiB
Python

"""π* domain library tests (ticket #000015).
Covers:
- Registry contains the expected built-in π*'s after import
- Round-trip determinism for active π*'s
- Stubs raise NotImplementedError
- Composition algebra: chain produces same output as sequential calls
- Composition manifest fingerprint stability
- Existing arborist.wikitext.to_base behavior preserved
"""
from __future__ import annotations
import pytest
from arborist.pi_star import (
REGISTRY,
canonical_composition_id,
compose,
domains,
get,
list_keys,
)
from arborist.pi_star.protocol import (
PiStarError,
assert_round_trip,
equivalence_class_id,
)
# --- registry --------------------------------------------------------
def test_registry_contains_expected_keys():
keys = list_keys()
assert "wikitext-base@v1" in keys
assert "claim-lattice@v1" in keys
assert "code-py-ast@v1" in keys
assert "logic-kernel@v1" in keys
assert "time-series-quantized@v1" in keys
assert "tabular-pinned@v1" in keys
def test_domains_groups_by_domain():
d = domains()
assert "code-py-ast@v1" in d.get("code", [])
assert "logic-kernel@v1" in d.get("logic", [])
assert "time-series-quantized@v1" in d.get("time-series", [])
def test_get_unknown_raises():
with pytest.raises(KeyError):
get("nonexistent@v999")
# --- wikitext-base@v1 -----------------------------------------------
def test_wikitext_base_round_trip():
pi_star = get("wikitext-base@v1")
raw = b"Plain ''italic'' [[link|word]] text."
once = pi_star.canonicalize(raw)
twice = pi_star.canonicalize(once)
assert once == twice
assert_round_trip(pi_star, raw)
def test_wikitext_base_rejects_non_bytes():
pi_star = get("wikitext-base@v1")
with pytest.raises(PiStarError):
pi_star.canonicalize("not bytes") # type: ignore[arg-type]
def test_wikitext_base_equivalence_class():
pi_star = get("wikitext-base@v1")
raw = b"Plain text."
eid = equivalence_class_id(pi_star, raw)
assert len(eid) == 64 # SHA-256 hex
def test_wikitext_base_matches_legacy_to_base():
"""Re-home preserves identical output to the legacy
``arborist.wikitext.to_base`` API."""
from arborist.wikitext import to_base
pi_star = get("wikitext-base@v1")
raw_text = "Some [[link|text]] with ''italics''."
legacy = to_base(raw_text).encode("utf-8")
via_pi_star = pi_star.canonicalize(raw_text.encode("utf-8"))
assert legacy == via_pi_star
# --- claim-lattice@v1 ------------------------------------------------
def test_claim_lattice_round_trip_on_lattice_text():
"""Canonicalize is idempotent: canonicalize(canonicalize(x)) ==
canonicalize(x). Note: the canonical form is a JSON list, so the
second canonicalization re-parses the JSON as text and produces
a different list — these projections are NOT idempotent on raw
text. We only assert determinism under the same input."""
pi_star = get("claim-lattice@v1")
sample = (
b"- A claim. [E1]\n"
b"- Another claim. [E2]\n"
)
once = pi_star.canonicalize(sample)
twice = pi_star.canonicalize(sample)
assert once == twice
def test_claim_lattice_rejects_non_bytes():
pi_star = get("claim-lattice@v1")
with pytest.raises(PiStarError):
pi_star.canonicalize(123) # type: ignore[arg-type]
def test_claim_lattice_empty_input_returns_empty_array():
pi_star = get("claim-lattice@v1")
out = pi_star.canonicalize(b"")
assert out == b"[]"
# --- stubs ---------------------------------------------------------
@pytest.mark.parametrize(
"key",
[
"code-py-ast@v1",
"logic-kernel@v1",
"time-series-quantized@v1",
"tabular-pinned@v1",
],
)
def test_stubs_raise_not_implemented(key):
pi_star = get(key)
with pytest.raises(NotImplementedError):
pi_star.canonicalize(b"anything")
# --- composition --------------------------------------------------
def test_compose_text_then_claim_lattice():
"""Chaining wikitext-base@v1 then claim-lattice@v1 produces the
same bytes as applying them sequentially."""
composition = compose(
"wikitext-base@v1",
"claim-lattice@v1",
register_in_registry=False,
)
raw = b"- A simple claim. [E1]"
via_composition = composition.canonicalize(raw)
inner = get("wikitext-base@v1")
outer = get("claim-lattice@v1")
sequential = outer.canonicalize(inner.canonicalize(raw))
assert via_composition == sequential
def test_compose_registers_into_registry():
"""compose(register_in_registry=True) puts the composition in REGISTRY."""
pre = list_keys()
composition = compose(
"wikitext-base@v1",
"claim-lattice@v1",
name="wikitext-then-claims-test",
version="vt",
)
post = list_keys()
assert "wikitext-then-claims-test@vt" in post
assert len(post) == len(pre) + 1
assert composition.domain == "text-or-wikitext"
def test_compose_unknown_inner_rejected():
with pytest.raises(KeyError):
compose("nope@v1", "claim-lattice@v1")
def test_compose_unknown_outer_rejected():
with pytest.raises(KeyError):
compose("wikitext-base@v1", "nope@v1")
def test_canonical_composition_id_stable():
a = canonical_composition_id("wikitext-base@v1", "claim-lattice@v1")
b = canonical_composition_id("wikitext-base@v1", "claim-lattice@v1")
assert a == b
# Order matters: a∘b ≠ b∘a.
swapped = canonical_composition_id(
"claim-lattice@v1", "wikitext-base@v1"
)
assert a != swapped