pi_star: time-series-quantized@v1 graduates + Substrate docs section

Two items from the menu, fanned out:

1. time-series-quantized@v1 — last meaningful π* stub graduates.
   Sample-array carrier (sensor / temporal data) joins the registry
   alongside text · claim_lattice · code · arithmetic · logic.
   Quantizes to (dt, dv) grid, sorts by timestamp, dedupes
   collisions (last wins), serializes as integer-vector text:

       dt=1;dv=0.1;n=2;t0=0:10|20

   Equivalence classes preserved: timestamp jitter < Δ_t,
   value jitter < Δ_v/2 (banker's rounding), out-of-order samples,
   different JSON presentation. Distinct: any change to dt/dv grid,
   any quantized value or timestamp difference. Projective —
   canonical text is not valid JSON, so re-canonicalization raises.

   - 13 unit tests in tests/test_pi_star.py (jitter, dedupe, sort,
     fractional dv, error paths, idempotency-projective)
   - 10 syntax + 12 semantics fixtures under bench/fixtures/5s/
     (10/10 + 12/12 pass)
   - bench-5s-time-series Makefile target
   - time_series added to PHASE_1_CARRIERS whitelist
   - tabular-pinned@v1 is now the only remaining stub

2. Substrate docs — first formal coverage of the registry, bench
   harness, and v8 ForkScore at arborist.unturf.com:

   - docs/_source/pi-star.rst: registry overview, cross-modality
     discipline (carrier + pi_star_ref), math π* highlights
     (arithmetic + logic-kernel worked examples), composition
     algebra pointer, authoring checklist (8 steps).
   - docs/_source/bench.rst: 5S/5T/5F/5R structure, sub-batteries,
     phase-1 carriers, ForkScore integration, fixture format,
     reproducibility (runtime_digest, fixture_digest).
   - docs/_source/v8-fork-score.rst: formula, default weights,
     verdict thresholds (ACCEPT/MARGINAL/REJECT), hard-regression +
     NEG_INF_REGRESSION flags, CLI usage.
   - index.rst gets a "Substrate" toctree section above the existing
     module-reference autosummary.

   Sphinx build clean (3 new pages, no new warnings).

Test suite: 1269 passed, 36 skipped.
This commit is contained in:
russell@unturf.com 2026-05-08 09:33:19 -04:00
parent e4ecf89621
commit 508a25076d
No known key found for this signature in database
10 changed files with 739 additions and 12 deletions

View file

@ -286,6 +286,12 @@ bench-5s-logic-kernel: bootstrap ## 5S logic-kernel π* (SQD §14.3; CNF canonic
bench-5s-math: bench-5s-arithmetic bench-5s-logic-kernel ## complete math π* surface (arithmetic + logic-kernel)
bench-5s-time-series: bootstrap ## 5S time-series-quantized π* (SQD §13.5; quantized integer-vector canonicalizer)
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
--fixtures bench/fixtures/5s/syntax-time-series-v1.jsonl
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
--fixtures bench/fixtures/5s/semantics-time-series-v1.jsonl
bench-5f-formulate-live: bootstrap ## 5F Formulate via live arborist.qa.parse_claims (Phase 1b.2)
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5f --sub formulate \
--fixtures bench/fixtures/5f/formulate-live-v1.jsonl

View file

@ -1,14 +1,75 @@
"""``time-series-quantized@v1`` π* (stub).
"""``time-series-quantized@v1`` π* — temporal signal canonicalizer.
Domain: ``time-series``. Planned semantics: resample at a substrate-
declared rate, quantize per public Δ_t / Δ_y, encode as committed
integer vector with header (rate, Δ, length).
Domain: ``time-series``. Implements the SQD-whitepaper-§13.5 plan
that ticket #000015 §1.7 reserved: resample at a declared rate,
quantize per declared Δ_v, encode as a committed integer vector
with header (dt, dv, count, t0).
Activates the cross-modality discipline for sensor / temporal-signal
data alongside text + claim_lattice + memory + code + arithmetic +
logic, this is the **seventh** real carrier domain in the registry.
Input format
------------
UTF-8 JSON object::
{
"dt": <number>, # time quantization step (required)
"dv": <number>, # value quantization step (required)
"samples": [[t, v], ...] # array of (timestamp, value) pairs
}
- ``dt`` and ``dv`` MUST be positive numbers (int or float).
- ``samples`` is sorted by timestamp during canonicalization;
duplicate timestamps (within Δ_t resolution) are dropped (last
value wins).
- Empty ``samples`` is allowed (canonicalizes to an empty series).
Canonical output
----------------
UTF-8 text of shape::
dt=<dt>;dv=<dv>;n=<count>;t0=<first-quantized-timestamp>:<v0>|<v1>|<v2>|...
Where each ``v_i`` is an integer ``round(value / dv)`` with
ties-to-even rounding (Python's default banker's rounding).
Equivalence classes preserved
-----------------------------
- Timestamp jitter within Δ_t resolution: same canonical.
- Value jitter within Δ_v / 2: same canonical (banker's rounding).
- Out-of-order samples: same canonical (sorted on input).
- Different presentation (e.g., trailing whitespace in JSON) but
same logical data: same canonical.
Equivalence classes kept distinct
---------------------------------
- Different ``dt`` or ``dv`` declarations (the quantization grid is
part of identity).
- Sample sequences that differ in any quantized value or timestamp.
Round-trip property
-------------------
Projective. The output is the canonical text form, not valid JSON
input re-canonicalizing the canonical bytes raises PiStarError.
Versioning
----------
``time-series-quantized@v1`` pins this format. Any change to the
serialization, rounding rule, or sort order requires a new version.
Source: ticket #000015 §1.7 reserved this stub; this commit
graduates it. With ``arithmetic@v1``, ``logic-kernel@v1``, and now
``time-series-quantized@v1``, three of the four originally-stubbed
modalities are real. ``tabular-pinned@v1`` remains the last stub.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from arborist.pi_star.protocol import PiStarError
from arborist.pi_star.registry import register
@ -19,9 +80,93 @@ class TimeSeriesQuantizedV1:
domain: str = "time-series"
def canonicalize(self, raw: bytes) -> bytes:
raise NotImplementedError(
"time-series-quantized@v1 is a stub; implementation ticket pending."
)
if not isinstance(raw, (bytes, bytearray)):
raise PiStarError(
"time-series-quantized@v1 expects bytes; got "
f"{type(raw).__name__}"
)
try:
text = raw.decode("utf-8", errors="surrogatepass")
except UnicodeDecodeError as exc: # pragma: no cover
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
try:
obj = json.loads(text)
except json.JSONDecodeError as exc:
raise PiStarError(
f"time-series-quantized@v1 input is not valid JSON: {exc}"
) from exc
if not isinstance(obj, dict):
raise PiStarError(
"time-series-quantized@v1 input must be a JSON object"
)
for required in ("dt", "dv", "samples"):
if required not in obj:
raise PiStarError(
f"time-series-quantized@v1 missing required field {required!r}"
)
dt = obj["dt"]
dv = obj["dv"]
if not isinstance(dt, (int, float)) or dt <= 0:
raise PiStarError(
f"time-series-quantized@v1 dt must be positive number; got {dt!r}"
)
if not isinstance(dv, (int, float)) or dv <= 0:
raise PiStarError(
f"time-series-quantized@v1 dv must be positive number; got {dv!r}"
)
samples = obj["samples"]
if not isinstance(samples, list):
raise PiStarError(
"time-series-quantized@v1 samples must be a JSON array"
)
# Quantize each sample to integer (t_idx, v_idx) pairs.
quantized: list[tuple[int, int]] = []
for i, pair in enumerate(samples):
if not isinstance(pair, list) or len(pair) != 2:
raise PiStarError(
f"sample[{i}] must be a [timestamp, value] pair"
)
t, v = pair
if not isinstance(t, (int, float)) or not isinstance(v, (int, float)):
raise PiStarError(
f"sample[{i}] timestamp and value must be numbers"
)
t_idx = int(round(t / dt))
v_idx = int(round(v / dv))
quantized.append((t_idx, v_idx))
# Sort by timestamp; on collision, last value wins.
quantized.sort(key=lambda p: p[0])
if not quantized:
return _serialize(dt, dv, t0=0, values=[]).encode("utf-8")
# Dedupe by timestamp keeping the last value seen.
deduped: dict[int, int] = {}
for t_idx, v_idx in quantized:
deduped[t_idx] = v_idx
sorted_indices = sorted(deduped.keys())
t0 = sorted_indices[0]
values = [deduped[i] for i in sorted_indices]
return _serialize(dt, dv, t0=t0, values=values).encode("utf-8")
def _serialize(dt, dv, *, t0: int, values: list[int]) -> str:
# Format dt and dv canonically: int → "1", float → its repr (which
# is the shortest round-trip representation).
dt_s = _num(dt)
dv_s = _num(dv)
n = len(values)
body = "|".join(str(v) for v in values)
return f"dt={dt_s};dv={dv_s};n={n};t0={t0}:{body}"
def _num(x) -> str:
if isinstance(x, int) or (isinstance(x, float) and x.is_integer()):
return str(int(x))
return repr(x)
register(TimeSeriesQuantizedV1())

View file

@ -116,6 +116,8 @@ PHASE_1_CARRIERS = frozenset({
# (SQD §14.1 + §14.3).
"arithmetic",
"logic",
# Sensor / temporal-signal carrier — time-series-quantized@v1.
"time_series",
})

View file

@ -0,0 +1,13 @@
{"_meta":{"battery":"5s","sub_battery":"semantics","version":"v1","task_count":12,"notes":"time-series-quantized@v1 equivalence tests. Jitter under Δ_t / Δ_v collapses; reordered samples collapse; different dt/dv/values do not."}}
{"id":"5s-sem-ts-001","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":0.1,\"samples\":[[0,1.0],[1,2.0]]}","input_b":"{\"dt\":1,\"dv\":0.1,\"samples\":[[0.4,1.04],[1.3,2.0]]}","expected_equivalent":true}
{"id":"5s-sem-ts-002","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1],[1,2],[2,3]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[2,3],[0,1],[1,2]]}","expected_equivalent":true}
{"id":"5s-sem-ts-003","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1]]}","input_b":"{\"dt\":2,\"dv\":1,\"samples\":[[0,1]]}","expected_equivalent":false}
{"id":"5s-sem-ts-004","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":0.1,\"samples\":[[0,1.0]]}","input_b":"{\"dt\":1,\"dv\":1.0,\"samples\":[[0,1.0]]}","expected_equivalent":false}
{"id":"5s-sem-ts-005","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[0,2]]}","expected_equivalent":false}
{"id":"5s-sem-ts-006","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1],[1,2]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1],[1,2],[2,3]]}","expected_equivalent":false}
{"id":"5s-sem-ts-007","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[]}","expected_equivalent":true}
{"id":"5s-sem-ts-008","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,5],[0.3,7]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[0,7]]}","expected_equivalent":true}
{"id":"5s-sem-ts-009","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":0.5,\"dv\":0.1,\"samples\":[[0,1.0],[0.5,1.5]]}","input_b":"{\"dt\":0.5,\"dv\":0.1,\"samples\":[[0.05,1.04],[0.55,1.46]]}","expected_equivalent":true}
{"id":"5s-sem-ts-010","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1],[1,2],[2,3]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1],[1,2],[2,4]]}","expected_equivalent":false}
{"id":"5s-sem-ts-011","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":10,\"dv\":1,\"samples\":[[0,100],[10,200]]}","input_b":"{\"dt\":10,\"dv\":1,\"samples\":[[3,100],[12,200]]}","expected_equivalent":true}
{"id":"5s-sem-ts-012","battery":"5s","sub_battery":"semantics","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input_a":"{\"dt\":1,\"dv\":1,\"samples\":[[0,5]]}","input_b":"{\"dt\":1,\"dv\":1,\"samples\":[[5,0]]}","expected_equivalent":false}

View file

@ -0,0 +1,11 @@
{"_meta":{"battery":"5s","sub_battery":"syntax","version":"v1","task_count":10,"notes":"time-series-quantized@v1 parse-pass tests. JSON time-series objects with dt/dv/samples that must canonicalize without raising."}}
{"id":"5s-syn-ts-001","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":1,\"samples\":[]}","expected":"pass"}
{"id":"5s-syn-ts-002","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":0.1,\"samples\":[[0,1.0]]}","expected":"pass"}
{"id":"5s-syn-ts-003","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":0.1,\"samples\":[[0,1.0],[1,2.0],[2,3.0]]}","expected":"pass"}
{"id":"5s-syn-ts-004","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":0.5,\"dv\":0.01,\"samples\":[[0,0.05],[0.5,0.10],[1.0,0.15]]}","expected":"pass"}
{"id":"5s-syn-ts-005","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":10,\"dv\":1,\"samples\":[[0,100],[10,200],[20,300]]}","expected":"pass"}
{"id":"5s-syn-ts-006","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":1,\"samples\":[[5,10],[3,6],[1,2],[7,14]]}","expected":"pass"}
{"id":"5s-syn-ts-007","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":0.001,\"dv\":0.001,\"samples\":[[0,0.001],[0.001,0.002]]}","expected":"pass"}
{"id":"5s-syn-ts-008","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":1,\"samples\":[[0,-5],[1,-3],[2,-1]]}","expected":"pass"}
{"id":"5s-syn-ts-009","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":1,\"samples\":[[0,0],[100,100],[200,200]]}","expected":"pass"}
{"id":"5s-syn-ts-010","battery":"5s","sub_battery":"syntax","version":"v1","carrier":"time_series","domain":"sample_array","pi_star_ref":"time-series-quantized@v1","input":"{\"dt\":1,\"dv\":1,\"samples\":[[0,1]]}","expected":"pass"}

140
docs/_source/bench.rst Normal file
View file

@ -0,0 +1,140 @@
Benchmark surface
=================
Arborist ships the complete **Dav1DPrometheus 5S/5T/5F/5R** evaluation
suite — 21 sub-batteries, ~660 deterministic fixtures — as
first-class infrastructure. Every benchmark is reproducible, no
LLM-as-judge, and many sub-batteries route through the actual
arborist surface (parser, verifier, audit chain, π* registry) rather
than synthetic gold output.
Quick reference
---------------
.. code-block:: bash
make bench-suite # complete 5S + 5T + 5F + 5R suite
make bench-5s # representational discipline
make bench-5t # temporal / cross-reasoning
make bench-5f # operational quality (Phase 1a embedded)
make bench-5f-live # 5F bridged to live arborist surfaces (Phase 1b.2)
make bench-5r # workspace operators
make bench-5s-math # arithmetic@v1 + logic-kernel@v1 fixtures
make bench-5s-code # code-py-ast@v1 fixtures
Each invocation emits a JSON :class:`bench.batteries.base.BatteryResult`
with per-task pass/fail, fixture digest, runtime digest, and
sub-battery-specific metrics.
The four batteries
------------------
5S — representation discipline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Five sub-batteries testing what the system understands at the
sign / meaning / derivation level:
- **Syntax** — does the named π* parse the input without raising?
- **Semantics** — do two surface forms canonicalize to the same
bytes when they should (and not when they shouldn't)?
- **Syllogism** — does each step in a deductive chain validly
follow under the named rule (categorical_transitivity, chain_3,
invalid_converse, missing_premise)?
- **Synthesis** — does the system assemble cited facts into a
coherent derivation supported by the fact set?
- **Semiotics** — is meaning preserved under controlled label
swaps?
5T — temporal / cross-reasoning discipline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Six sub-batteries (legacy ``transfer`` plus the canonical
Dav1DPrometheus five):
- **Transfer Learning** — does a learned pattern carry across
task / domain / carrier?
- **Triangulation** — do independent strategies (substring,
token_subset, token_overlap, entity_match) agree at threshold?
- **Truthtables** — exhaustive propositional coverage at N=2..4
variables.
- **Transitivity** — typed-relation chains under whitelist
(``implies``, ``subset_of``, ``ancestor_of``, ``before``,
``less_than``).
- **Time** — temporal-context preservation across memory_root
snapshots; integrates with #000017 surface.
5F — operational quality
~~~~~~~~~~~~~~~~~~~~~~~~
Five sub-batteries, all with **embedded** (Phase 1a synthetic) AND
**live** (Phase 1b.2, routes through real arborist surfaces) modes:
================ =============================================
Sub-battery Live surface
================ =============================================
Function ``arborist.qa.parse_claims.parse_pointer_claims``
Finetuning ``arborist.selfmodel.store_snapshot`` round-trip
Falsification ``arborist.qa.verify.verify_quotes``
Formulate ``arborist.qa.parse_claims.parse_pointer_claims``
Feedback Loop ``arborist.store.append_audit`` + ``memory.snapshot``
================ =============================================
Per-task ``detail.source`` reports ``"embedded"`` or ``"live"`` so
bench output distinguishes synthetic from production signal.
5R — workspace operators
~~~~~~~~~~~~~~~~~~~~~~~~
Five sub-batteries testing operators applied to a workspace
(SelfModel + memory_root + audit chain):
- **React** — observations integrate into downstream state.
- **Rearrange** — restructure without semantic shift.
- **Restore** — retrieve prior facts from history.
- **Replicate** — π*-determinism across N replicas.
- **Resonate** — variance-zero across N runs.
Cross-modality discipline
-------------------------
Every fixture carries:
- ``carrier`` — domain whitelist enforced by
:data:`bench.batteries.base.PHASE_1_CARRIERS`. Phase 1
domains: ``text``, ``claim_lattice``, ``memory_snapshot``,
``selfmodel_snapshot``, ``providence_record``, ``audit_event``,
``code``, ``arithmetic``, ``logic``, ``time_series``.
- ``domain`` — sub-domain qualifier (e.g.,
``rational``, ``propositional``, ``python_ast``).
- ``pi_star_ref`` — registry key naming the canonicalizer.
- ``loss_report_refs`` — optional projection-loss links.
- ``modality_notes`` — scope note.
Unsupported carriers fail explicitly with
``reason="unsupported_carrier"`` — never silently accepted. Hidden-
channel work is defensive only (detection / flagging, never
generation).
ForkScore consumes battery output
---------------------------------
The v8 ForkScore (see :doc:`v8-fork-score`) reads BatteryResult JSON
from a parent and child organism, computes a weighted scalar
verdict with ACCEPT / MARGINAL / REJECT classes:
.. code-block:: bash
make bench-suite # generates parent.json
# ... apply changes ...
make bench-suite # generates child.json
arborist v8 score --parent parent.json --child child.json
Authoring new fixtures
----------------------
See :file:`docs/spec-methodology.md` (per-author checklist) and the
existing fixture files under :file:`bench/fixtures/`. New
sub-batteries follow the protocol in :mod:`bench.batteries.base`
``Battery.run(fixtures_path) → BatteryResult``, deterministic, no
LLM-as-judge, carrier metadata mandatory.

View file

@ -13,6 +13,14 @@ Contents:
concepts
cookbook
.. toctree::
:maxdepth: 2
:caption: Substrate
pi-star
bench
v8-fork-score
.. toctree::
:maxdepth: 3
:caption: API Modules

134
docs/_source/pi-star.rst Normal file
View file

@ -0,0 +1,134 @@
π* 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.
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.

View file

@ -0,0 +1,134 @@
v8 ForkScore
============
The Merkle-AGI v8 selection protocol's **scoring half**. Pure
function over a (parent, child) bench-result pair → a single
scalar with ACCEPT / MARGINAL / REJECT verdict. Phase 1a of
ticket ``#000012``.
The complementary canonicalization half (validator state machine,
acceptance protocol, slashing, fork-choice rule) is reserved for
the v8 paper itself; Phase 1a ships only the function ForkScore
without the consensus surface around it.
Formula
-------
.. code-block:: text
ForkScore =
α · Δ5S
+ β · Δ5T
+ γ · Δ5F (incl. efficiency-aware bonus)
+ δ · SelfModelCalibrationGain
+ ε · AuditCompleteness
+ ζ · ValidatorDiversity
- η · RegressionPenalty
- θ · CapitalCostPenalty
- ι · SecurityRiskPenalty (reserved; Phase 1a = 0)
- κ · ComplexityPenalty (reserved; Phase 1a = 0)
- λ · MemoryInvalidationPenalty
Each term consumes the metrics every 5S/5T/5F sub-battery emits in
its :class:`BatteryResult.metrics` dict. The Δ-rate per battery is
the mean of per-sub-battery rate deltas (child — parent).
Δ5F additionally consumes the inf-aware efficiency aggregations
landed under the 2026-05-08 ``fbd99a8`` review:
``adaptation_efficiency_mean_finite``,
``adaptation_efficiency_infinite_count``,
``adaptation_efficiency_neg_infinite_count``,
``feedback_efficiency_mean_finite``,
``feedback_efficiency_infinite_count``.
Verdict thresholds
------------------
========================== ================== ============
Score / flags Verdict CLI exit
========================== ================== ============
``score >= SIGNAL_FLOOR`` **ACCEPT** ``0``
``[0, SIGNAL_FLOOR)`` **MARGINAL** ``0``
``score < 0`` **REJECT** ``1``
hard-regression flag **REJECT** ``1``
``NEG_INF_REGRESSION`` flag **REJECT** ``1``
========================== ================== ============
``SIGNAL_FLOOR`` defaults to ``0.05`` (5pp; matches
:file:`docs/bench-maxing.md`'s noise floor).
Hard-regression flag fires if any single sub-battery rate drops by
``HARD_REGRESSION_FLOOR`` (default 5pp), regardless of net score.
``NEG_INF_REGRESSION`` flag fires when
``*_efficiency_neg_infinite_count`` increases parent → child:
*free regression* is unsafe regardless of other gains.
Default weights
---------------
.. code-block:: python
WeightSet(
alpha=1.0, # Δ5S
beta=1.0, # Δ5T
gamma=1.0, # Δ5F
delta=0.5, # SelfModelCalibrationGain
epsilon=0.3, # AuditCompleteness
zeta=0.0, # ValidatorDiversity (off in single-validator)
eta=2.0, # RegressionPenalty (heavy by design)
theta=0.5, # CapitalCostPenalty
iota=0.0, # SecurityRiskPenalty (reserved)
kappa=0.0, # ComplexityPenalty (reserved)
lambda_=0.5, # MemoryInvalidationPenalty
)
Override via JSON file passed to ``--weights``:
.. code-block:: json
{
"alpha": 1.5,
"eta": 5.0,
"lambda": 1.0
}
The JSON key ``"lambda"`` round-trips into ``WeightSet.lambda_``
because ``lambda`` is a Python reserved word.
CLI
---
.. code-block:: bash
arborist v8 score \
--parent parent-bench.json \
--child child-bench.json \
[--weights weights.json] \
[--capital-delta N] \
[--memory-invalidation-count N] \
[--audit-completeness 0..1] \
[--selfmodel-calibration-gain N]
``parent-bench.json`` and ``child-bench.json`` are the JSON output
of ``bench.batteries.runner --all``.
Output: :class:`arborist.v8.fork_score.ScoredFork` with
``score``, ``verdict``, per-term ``breakdown``, ``flags`` list,
and the active ``weights`` echoed back.
What's NOT in Phase 1a
----------------------
- Validator state machine (bonding / signing / slashing).
- Acceptance protocol (proposal / quorum / finalization).
- Challenge protocol (audit-replay disagreement).
- Fork-choice rule (which of two competing finalizations wins).
- Mesh wire format extensions for validator gossip.
- Stake mechanics + economic incentives.
- Cross-validator ZK proof exchange.
These are commissioned by the v8 paper itself
(``docs/merkle-agi-v8-consensus.rst``, still open under #000012).
Phase 1a's scoring function is the substrate the paper consumes;
landing it now lets the paper cite measured values instead of
stipulated ones.

View file

@ -127,11 +127,9 @@ def test_claim_lattice_empty_input_returns_empty_array():
@pytest.mark.parametrize(
"key",
[
# code-py-ast@v1 graduated to real implementation — see
# test_code_py_ast_*.
# logic-kernel@v1 graduated to real CNF canonicalizer — see
# test_logic_kernel_*.
"time-series-quantized@v1",
# code-py-ast@v1, logic-kernel@v1, arithmetic@v1, and
# time-series-quantized@v1 all graduated to real
# implementations. Only tabular-pinned@v1 remains a stub.
"tabular-pinned@v1",
],
)
@ -141,6 +139,142 @@ def test_stubs_raise_not_implemented(key):
pi_star.canonicalize(b"anything")
# --- time-series-quantized@v1 (graduated from stub) ------------------
def test_time_series_canonicalizes_basic_series():
import json as _json
pi_star = get("time-series-quantized@v1")
src = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0], [1, 2.5]]}).encode()
out = pi_star.canonicalize(src)
assert out == b"dt=1;dv=0.1;n=2;t0=0:10|25"
def test_time_series_collapses_jitter_within_resolution():
"""Timestamps off by < dt/2 quantize to the same grid index."""
import json as _json
pi_star = get("time-series-quantized@v1")
a = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0], [1, 2.0]]}).encode()
b = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0.4, 1.04], [1.3, 2.0]]}).encode()
assert pi_star.canonicalize(a) == pi_star.canonicalize(b)
def test_time_series_sorts_out_of_order_samples():
import json as _json
pi_star = get("time-series-quantized@v1")
sorted_input = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1], [1, 2], [2, 3]]}).encode()
shuffled = _json.dumps({"dt": 1, "dv": 1, "samples": [[2, 3], [0, 1], [1, 2]]}).encode()
assert pi_star.canonicalize(sorted_input) == pi_star.canonicalize(shuffled)
def test_time_series_distinguishes_different_dt():
import json as _json
pi_star = get("time-series-quantized@v1")
a = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1]]}).encode()
b = _json.dumps({"dt": 2, "dv": 1, "samples": [[0, 1]]}).encode()
assert pi_star.canonicalize(a) != pi_star.canonicalize(b)
def test_time_series_distinguishes_different_dv():
import json as _json
pi_star = get("time-series-quantized@v1")
a = _json.dumps({"dt": 1, "dv": 0.1, "samples": [[0, 1.0]]}).encode()
b = _json.dumps({"dt": 1, "dv": 1.0, "samples": [[0, 1.0]]}).encode()
assert pi_star.canonicalize(a) != pi_star.canonicalize(b)
def test_time_series_distinguishes_different_values():
import json as _json
pi_star = get("time-series-quantized@v1")
a = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1]]}).encode()
b = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 2]]}).encode()
assert pi_star.canonicalize(a) != pi_star.canonicalize(b)
def test_time_series_dedupes_collisions_keeping_last():
"""Two samples at the same quantized timestamp: last wins."""
import json as _json
pi_star = get("time-series-quantized@v1")
src = _json.dumps({"dt": 1, "dv": 1, "samples": [[0, 1], [0.3, 5]]}).encode()
out = pi_star.canonicalize(src)
# Both timestamps quantize to t_idx=0; second sample (5) wins.
assert out == b"dt=1;dv=1;n=1;t0=0:5"
def test_time_series_handles_empty_samples():
import json as _json
pi_star = get("time-series-quantized@v1")
out = pi_star.canonicalize(_json.dumps({"dt": 1, "dv": 1, "samples": []}).encode())
assert out == b"dt=1;dv=1;n=0;t0=0:"
def test_time_series_rejects_non_json():
from arborist.pi_star.protocol import PiStarError
pi_star = get("time-series-quantized@v1")
with pytest.raises(PiStarError, match="not valid JSON"):
pi_star.canonicalize(b"not json")
def test_time_series_rejects_missing_fields():
import json as _json
from arborist.pi_star.protocol import PiStarError
pi_star = get("time-series-quantized@v1")
with pytest.raises(PiStarError, match="missing required field"):
pi_star.canonicalize(_json.dumps({"dt": 1, "samples": []}).encode())
def test_time_series_rejects_zero_dt():
import json as _json
from arborist.pi_star.protocol import PiStarError
pi_star = get("time-series-quantized@v1")
with pytest.raises(PiStarError, match="dt must be positive"):
pi_star.canonicalize(_json.dumps({"dt": 0, "dv": 1, "samples": []}).encode())
def test_time_series_rejects_negative_dv():
import json as _json
from arborist.pi_star.protocol import PiStarError
pi_star = get("time-series-quantized@v1")
with pytest.raises(PiStarError, match="dv must be positive"):
pi_star.canonicalize(_json.dumps({"dt": 1, "dv": -1, "samples": []}).encode())
def test_time_series_rejects_non_pair_samples():
import json as _json
from arborist.pi_star.protocol import PiStarError
pi_star = get("time-series-quantized@v1")
with pytest.raises(PiStarError, match="\\[timestamp, value\\] pair"):
pi_star.canonicalize(_json.dumps({"dt": 1, "dv": 1, "samples": [[1, 2, 3]]}).encode())
def test_time_series_deterministic_across_repeated_calls():
import json as _json
pi_star = get("time-series-quantized@v1")
src = _json.dumps({"dt": 0.5, "dv": 0.01, "samples": [[0, 0.05], [0.5, 0.10], [1.0, 0.15]]}).encode()
a = pi_star.canonicalize(src)
b = pi_star.canonicalize(src)
c = pi_star.canonicalize(src)
assert a == b == c
# --- code-py-ast@v1 (graduated from stub) ----------------------------