`make query Q="0.1 + 0.2"` used to return `no_sources` because
arithmetic-shaped input has no FTS5 hits in any text shard. Two
surfaces close that gap.
**`arborist canon <key> "<input>"`** — direct π* call, no shards,
no LLM, no audit chain. Pure projection:
$ arborist canon arithmetic@v1 "0.1 + 0.2" → 3/10
$ arborist canon logic-kernel@v1 "A IMPL B" → (NOT A OR B)
$ arborist canon --list → registry contents
$ arborist canon --json arithmetic@v1 "0.1+0.2" → SHA-256 envelope
**Math/logic preflight in `arborist query`** — pure-arithmetic and
pure-propositional questions short-circuit RAG and answer through
arithmetic@v1 / logic-kernel@v1 directly. Synthetic
`audit_mode=CANONICAL_PROJECTION`, renders as
`CANONICAL · via <pi_star_ref>`:
$ arborist query "0.1 + 0.2"
0.1 + 0.2
CANONICAL · via arithmetic@v1 0.0s (projected)
3/10
$ arborist query "(NOT B) IMPL (NOT A)"
(NOT B) IMPL (NOT A)
CANONICAL · via logic-kernel@v1 0.0s (projected)
(NOT A OR B)
Sniff is conservative: pure-arithmetic shape (digits + ops, no
letters) or pure-propositional shape (uppercase atoms + reserved
keywords only). Natural-language wrapping ("what is 0.1+0.2?")
falls through to RAG. PiStarError on a shape match also falls
through — preflight is best-effort, never blocking.
Disable per-call: `--no-canonical-preflight` flag,
`policy["canonical_projection_preflight"]=False`.
No schema changes: CANONICAL_PROJECTION is a render-layer audit_mode
token. No providence_cache writes, no audit_events, no
governance_policy_hash bump. The canonical bytes ARE the answer;
SHA-256 of the bytes is the equivalence-class identity (already
committed via the π* registry).
Side housekeeping: arborist/pi_star/__init__.py docstring caught up
with reality — six concrete π*'s ship today, only tabular-pinned@v1
remains as a stub.
31 new tests (preflight sniff + dispatch, query short-circuit,
contrapositive equivalence-class collapse, CLI subcommand exit codes
and JSON envelope, --no-canonical-preflight policy gate). Full
suite: 1300 passed, 36 skipped.
198 lines
7.3 KiB
ReStructuredText
198 lines
7.3 KiB
ReStructuredText
π* domain library
|
||
=================
|
||
|
||
Arborist's canonical-projection registry — the substrate that
|
||
implements SQD whitepaper §3's invariant projection
|
||
``π*: Σ* → 𝓘 ∪ {⊥}``. Every modality the bench surface or audit
|
||
chain touches has (or reserves) a registered π* that maps surface
|
||
bytes to canonical bytes; the SHA-256 of the canonical bytes is
|
||
the equivalence-class identity.
|
||
|
||
Registry overview
|
||
-----------------
|
||
|
||
Lookup is by ``name@version`` key. Six concrete π*'s + one stub
|
||
ship today:
|
||
|
||
========================== =========== ===========================================================
|
||
Key Domain Status
|
||
========================== =========== ===========================================================
|
||
``wikitext-base@v1`` text Wikitext → plain prose (Phase 1a)
|
||
``claim-lattice@v1`` text Claim lines → JSON parsed-claim list (Phase 1a)
|
||
``code-py-ast@v1`` code Python source → canonical AST S-expression
|
||
``arithmetic@v1`` arithmetic Expression → exact rational ``num/den`` (SQD §14.1)
|
||
``logic-kernel@v1`` logic Boolean expression → canonical CNF (SQD §14.3)
|
||
``time-series-quantized@v1`` time-series JSON sample array → quantized integer vector
|
||
``tabular-pinned@v1`` tabular reserved (stub)
|
||
========================== =========== ===========================================================
|
||
|
||
Cross-modality discipline
|
||
-------------------------
|
||
|
||
Every bench fixture and audit-bound canonicalization names its
|
||
:class:`PiStar` via:
|
||
|
||
- ``carrier`` — a Phase-1 whitelist enforced by
|
||
:data:`bench.batteries.base.PHASE_1_CARRIERS`
|
||
- ``pi_star_ref`` — the registry key
|
||
|
||
Unsupported carriers fail or skip explicitly with
|
||
``reason="unsupported_carrier"`` — never silently accepted.
|
||
Hidden-channel work is defensive only (detection / flagging,
|
||
never generation or concealment).
|
||
|
||
The discipline gives every benchmark, every audit row, and every
|
||
selection score a stable answer to "what canonicalizer was used"
|
||
that survives schema migrations and is reproducible from the
|
||
canonical bytes alone.
|
||
|
||
Math π*'s — SQD §14
|
||
-------------------
|
||
|
||
Two of the most recently graduated π*'s implement the SQD
|
||
whitepaper's math substrate:
|
||
|
||
**arithmetic@v1** — closed-form rational arithmetic. Solves the
|
||
canonical SQD test exactly:
|
||
|
||
.. code-block:: python
|
||
|
||
from arborist.pi_star import get
|
||
ps = get("arithmetic@v1")
|
||
|
||
ps.canonicalize(b"0.1+0.2") # → b"3/10"
|
||
ps.canonicalize(b"0.3") # → b"3/10"
|
||
ps.canonicalize(b"1+2") # → b"3/1"
|
||
ps.canonicalize(b"6/4") # → b"3/2" (lowest terms)
|
||
|
||
No floating-point drift — ``Decimal(str(0.1))`` gives exact
|
||
``1/10``, then ``fractions.Fraction`` arithmetic stays in ℚ.
|
||
Identifiers, function calls, division by zero, and non-integer
|
||
exponents raise :class:`PiStarError`.
|
||
|
||
**logic-kernel@v1** — propositional Boolean expression →
|
||
Conjunctive Normal Form (CNF):
|
||
|
||
.. code-block:: python
|
||
|
||
ps = get("logic-kernel@v1")
|
||
|
||
ps.canonicalize(b"A AND B") # → b"A AND B"
|
||
ps.canonicalize(b"B AND A") # → b"A AND B" (commutativity)
|
||
ps.canonicalize(b"A IMPL B") # → b"(NOT A OR B)"
|
||
ps.canonicalize(b"(NOT B) IMPL (NOT A)") # → b"(NOT A OR B)" (contrapositive)
|
||
ps.canonicalize(b"NOT NOT A") # → b"A" (double negation)
|
||
ps.canonicalize(b"A OR NOT A") # → b"TRUE" (tautology)
|
||
|
||
Atom cap: 8 (CNF expansion is exponential; cap keeps
|
||
canonicalization deterministic in bounded time).
|
||
|
||
Equivalences preserved:
|
||
commutativity, associativity, IMPL/IFF/XOR rewrites, De Morgan,
|
||
double negation, distribution, idempotence, tautology collapse,
|
||
contrapositive.
|
||
|
||
CLI surfaces
|
||
------------
|
||
|
||
Two ways to reach a registered π* from the shell — direct or
|
||
auto-routed.
|
||
|
||
**Direct call** — ``arborist canon <key> "<input>"`` runs the named
|
||
π* against the given input and prints the canonical bytes. No
|
||
shards, no LLM, no audit chain. Pure projection.
|
||
|
||
.. code-block:: console
|
||
|
||
$ arborist canon arithmetic@v1 "0.1 + 0.2"
|
||
3/10
|
||
$ arborist canon logic-kernel@v1 "(NOT B) IMPL (NOT A)"
|
||
(NOT A OR B)
|
||
$ arborist canon time-series-quantized@v1 \
|
||
'{"dt":1,"dv":0.1,"samples":[[0,1.0],[1,2.0]]}'
|
||
dt=1;dv=0.1;n=2;t0=0:10|20
|
||
|
||
# registry contents:
|
||
$ arborist canon --list
|
||
arithmetic@v1 arithmetic
|
||
claim-lattice@v1 text
|
||
code-py-ast@v1 code
|
||
logic-kernel@v1 logic
|
||
tabular-pinned@v1 tabular
|
||
time-series-quantized@v1 time-series
|
||
wikitext-base@v1 text-or-wikitext
|
||
|
||
# JSON envelope w/ canonical-bytes SHA-256:
|
||
$ arborist canon --json arithmetic@v1 "0.1 + 0.2"
|
||
{"pi_star_ref":"arithmetic@v1","input":"0.1 + 0.2",
|
||
"canonical":"3/10","canonical_sha256":"088ae5cc..."}
|
||
|
||
**Auto-routed via** ``arborist query`` — pure-arithmetic and
|
||
pure-propositional questions short-circuit the RAG path and answer
|
||
directly through ``arithmetic@v1`` / ``logic-kernel@v1``. The result
|
||
gets a synthetic ``audit_mode = CANONICAL_PROJECTION`` and renders as
|
||
``CANONICAL · via <pi_star_ref>``:
|
||
|
||
.. code-block:: console
|
||
|
||
$ arborist query "0.1 + 0.2"
|
||
0.1 + 0.2
|
||
CANONICAL · via arithmetic@v1 0.0s (projected)
|
||
|
||
3/10
|
||
|
||
$ arborist query "A IMPL B"
|
||
A IMPL B
|
||
CANONICAL · via logic-kernel@v1 0.0s (projected)
|
||
|
||
(NOT A OR B)
|
||
|
||
The sniff is conservative — pure-arithmetic shape (digits + ops, no
|
||
letters) or pure-propositional shape (uppercase atoms + reserved
|
||
keywords only). Anything natural-language ("what is 0.1 + 0.2?")
|
||
falls through to the existing RAG path. Pass
|
||
``--no-canonical-preflight`` to force the RAG path even on
|
||
matching input — useful when bench-comparing the canonical answer
|
||
against the LLM's reply. Disable per-call via
|
||
``policy["canonical_projection_preflight"] = False``.
|
||
|
||
Composition algebra
|
||
-------------------
|
||
|
||
Two π*'s can be composed into a third via
|
||
:func:`arborist.pi_star.compose`:
|
||
|
||
.. code-block:: python
|
||
|
||
from arborist.pi_star import compose
|
||
|
||
chain = compose("wikitext-base@v1", "claim-lattice@v1")
|
||
# Auto-registers as "wikitext-base-then-claim-lattice@v1"
|
||
chain.canonicalize(b"- The release date was July 3, 1985. [E1]")
|
||
|
||
Each composition is itself a registered π* with its own key. See
|
||
:file:`docs/pi-star-composition.md` for the algebra (type
|
||
compatibility, determinism preservation, equivalence-class
|
||
preservation, projective vs invertible compositions).
|
||
|
||
Authoring a new π*
|
||
------------------
|
||
|
||
1. Add ``arborist/pi_star/<name>.py`` with a frozen dataclass
|
||
declaring ``name``, ``version``, ``domain``, and a
|
||
``canonicalize(self, raw: bytes) -> bytes`` method.
|
||
2. Call :func:`arborist.pi_star.register` at module import time.
|
||
3. Import the new module in ``arborist/pi_star/__init__.py`` so
|
||
the registration fires at package load.
|
||
4. Write tests covering: idempotency on the canonical form (or
|
||
document projective behavior), equivalence classes preserved,
|
||
equivalence classes kept distinct, error paths (bad input
|
||
rejected explicitly).
|
||
5. Add bench fixtures under ``bench/fixtures/5s/{syntax,semantics}-<name>-v1.jsonl``
|
||
exercising the new carrier through the existing 5S Syntax and
|
||
Semantics runners.
|
||
6. Add the carrier name to
|
||
:data:`bench.batteries.base.PHASE_1_CARRIERS`.
|
||
7. Add ``make bench-5s-<name>`` Makefile target.
|
||
|
||
See :file:`docs/spec-methodology.md` for the full discipline.
|