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.
162 lines
6.1 KiB
Markdown
162 lines
6.1 KiB
Markdown
# π* 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:
|
|
|
|
```text
|
|
"<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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```text
|
|
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
|
|
|
|
```python
|
|
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`.
|