arborist/docs/pi-star-composition.md
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

6.1 KiB

π* composition algebra

Reference doc for ticket #000015. Defines when two canonical projections compose to a valid canonical projection, and how arborist's runtime registers compositions as first-class π*'s.

1. Why composition matters

A canonical projection π*: D → C collapses domain elements that should be treated as "equivalent" by some downstream verifier into a single canonical-bytes representative. arborist's QA layer relies on at least two π*'s already:

  • wikitext-base@v1 — wikitext bytes → canonical prose bytes (collapses templates, links, formatting).
  • claim-lattice@v1 — text bytes → canonical-JSON list of parsed claims (collapses surface variation in claim phrasing to its parsed structure).

A real query path runs these in series: raw wikitext → prose → parsed claim lattice. That sequence is itself a π*. Without first- class composition, every consumer re-derives the chain ad-hoc; the identity of the chain is implicit; cache-key hygiene drifts.

2. The composition rule

Given two registered π*'s:

  • π*_inner with domain D_in and codomain C_in
  • π*_outer with domain D_out and codomain C_out

Their composition π*_outer ∘ π*_inner (apply inner first, then outer) is a valid canonical projection if and only if:

  1. Type-compatible. C_in ⊆ D_out. The bytes the inner projection emits must be valid input to the outer.
  2. Determinism preserved. Both factors are deterministic functions ⇒ the chain is a deterministic function. (arborist demands this of every π*; composition inherits it for free.)
  3. Equivalence-class-preserving. If π*_inner(x) = π*_inner(y) then (π*_outer ∘ π*_inner)(x) = (π*_outer ∘ π*_inner)(y). Trivially true under function composition.

Type-compatibility is the only practical check the runtime can enforce automatically. compose(inner_key, outer_key) does NOT verify domain/codomain matching today (most domains are textual bytes; the inner's canonical bytes are usually valid input to the outer). Authors are responsible for declaring the domain match in the composition manifest doc when adding a new pair.

3. Lossy compositions

If (π*_outer ∘ π*_inner)(x) does not round-trip through the chain's codomain — i.e., (π*_outer ∘ π*_inner)(out)out under repeated application — the composition is projective rather than invertible.

The text-then-claim-lattice composition is projective: applying claim-lattice to its own output (a JSON-encoded list of parsed claims) does not return the same list. That's expected. The composition is still deterministic and equivalence-class- preserving; idempotency on the chain's codomain is a stronger property only some compositions need.

4. Composition as first-class π*

Each named composition registers itself in the global π* registry under a derived key:

"<inner-name>-then-<outer-name>@<version>"

Default for compose("wikitext-base@v1", "claim-lattice@v1") is "wikitext-base-then-claim-lattice@v1". This means:

  • Downstream callers look up the composition by name like any other π*.
  • Cache keys folded against governance_policy_hash see one stable identity for the chain, not a tuple of (inner_key, outer_key).
  • Replacing the inner π* with a new version is a NEW composition key — old records still reference the old chain by content.

5. Composition manifest hash

For governance hash inclusion, compositions also expose a content-addressed fingerprint:

canonical_composition_id("wikitext-base@v1", "claim-lattice@v1")

This is SHA-256(canonical_json({"chain": [inner_key, outer_key]})). Order matters: compose(a, b)compose(b, a) and their fingerprints differ. Folding the fingerprint into a policy hash pins which composition is live for a given run.

6. Cross-domain anchor projections (future)

Genuinely cross-domain claims — "this paragraph describes that function" — need an anchor projection that takes both sides' canonical forms plus a relation kind:

anchor = SHA-256(
    canon_a_id || canon_b_id || relation_kind || relation_payload_canonical
)

Anchor projections are the cross-domain analog of single-domain π*. Designing them is out of scope for ticket #000015's first landing; the algebra above generalizes. Open work:

  • Define a controlled vocabulary for relation_kind (committed in a sibling registry).
  • Define which operator policies require anchor commitments folded into governance_policy_hash vs sibling fields.
  • Worked example showing text-anchored-to-code as the first cross-domain composition in the registry.

7. Authoring checklist (new π* or composition)

Before landing a new π* or composition, verify:

  • Round-trip stability test: canonicalize(canonicalize(x)) == canonicalize(x) on a representative sample. (Note: only idempotent π*'s satisfy this; lossy compositions document the projective relationship explicitly.)
  • Determinism test: same input + same registry → byte-identical output across runs.
  • Domain declaration: name, version, domain set on the class.
  • Registry-entry test: arborist.pi_star.list_keys() includes the new key after package import.
  • Composition-fingerprint test (compositions only): the canonical_composition_id is stable across runs.
  • Spec-methodology checklist (ticket #000019 / docs/spec- methodology.md) reviewed.

8. Worked example: text → claim-lattice

from arborist.pi_star import compose, get

# Build the composition (auto-registers).
chain = compose("wikitext-base@v1", "claim-lattice@v1")
# Registry now holds "wikitext-base-then-claim-lattice@v1".

# Apply it.
raw = b"- The thing happened. [E1]"
canonical_bytes = chain.canonicalize(raw)
# bytes are JSON-encoded list of one ParsedClaim with pointer_ids=["E1"].

# Same result via sequential composition (sanity check).
inner = get("wikitext-base@v1")
outer = get("claim-lattice@v1")
manual = outer.canonicalize(inner.canonicalize(raw))
assert canonical_bytes == manual

This is the test fixture exercised in tests/test_pi_star.py ::test_compose_text_then_claim_lattice.