From 508a25076d4247106aba840e63cb96a3c7a68dbb Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 8 May 2026 09:33:19 -0400 Subject: [PATCH] pi_star: time-series-quantized@v1 graduates + Substrate docs section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 6 + arborist/pi_star/time_series.py | 159 +++++++++++++++++- bench/batteries/base.py | 2 + .../5s/semantics-time-series-v1.jsonl | 13 ++ bench/fixtures/5s/syntax-time-series-v1.jsonl | 11 ++ docs/_source/bench.rst | 140 +++++++++++++++ docs/_source/index.rst | 8 + docs/_source/pi-star.rst | 134 +++++++++++++++ docs/_source/v8-fork-score.rst | 134 +++++++++++++++ tests/test_pi_star.py | 144 +++++++++++++++- 10 files changed, 739 insertions(+), 12 deletions(-) create mode 100644 bench/fixtures/5s/semantics-time-series-v1.jsonl create mode 100644 bench/fixtures/5s/syntax-time-series-v1.jsonl create mode 100644 docs/_source/bench.rst create mode 100644 docs/_source/pi-star.rst create mode 100644 docs/_source/v8-fork-score.rst diff --git a/Makefile b/Makefile index 7401190..e1cb860 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/arborist/pi_star/time_series.py b/arborist/pi_star/time_series.py index bf7f001..46993f3 100644 --- a/arborist/pi_star/time_series.py +++ b/arborist/pi_star/time_series.py @@ -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": , # time quantization step (required) + "dv": , # 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=
;dv=;n=;t0=:|||... + +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()) diff --git a/bench/batteries/base.py b/bench/batteries/base.py index cd6f2ea..baaffb9 100644 --- a/bench/batteries/base.py +++ b/bench/batteries/base.py @@ -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", }) diff --git a/bench/fixtures/5s/semantics-time-series-v1.jsonl b/bench/fixtures/5s/semantics-time-series-v1.jsonl new file mode 100644 index 0000000..03b5bea --- /dev/null +++ b/bench/fixtures/5s/semantics-time-series-v1.jsonl @@ -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} diff --git a/bench/fixtures/5s/syntax-time-series-v1.jsonl b/bench/fixtures/5s/syntax-time-series-v1.jsonl new file mode 100644 index 0000000..01ae986 --- /dev/null +++ b/bench/fixtures/5s/syntax-time-series-v1.jsonl @@ -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"} diff --git a/docs/_source/bench.rst b/docs/_source/bench.rst new file mode 100644 index 0000000..517d219 --- /dev/null +++ b/docs/_source/bench.rst @@ -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. diff --git a/docs/_source/index.rst b/docs/_source/index.rst index 5799e30..2f487e8 100644 --- a/docs/_source/index.rst +++ b/docs/_source/index.rst @@ -13,6 +13,14 @@ Contents: concepts cookbook +.. toctree:: + :maxdepth: 2 + :caption: Substrate + + pi-star + bench + v8-fork-score + .. toctree:: :maxdepth: 3 :caption: API Modules diff --git a/docs/_source/pi-star.rst b/docs/_source/pi-star.rst new file mode 100644 index 0000000..80c6a76 --- /dev/null +++ b/docs/_source/pi-star.rst @@ -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/.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}--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-`` Makefile target. + +See :file:`docs/spec-methodology.md` for the full discipline. diff --git a/docs/_source/v8-fork-score.rst b/docs/_source/v8-fork-score.rst new file mode 100644 index 0000000..e34b1db --- /dev/null +++ b/docs/_source/v8-fork-score.rst @@ -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. diff --git a/tests/test_pi_star.py b/tests/test_pi_star.py index 3705643..fbbd839 100644 --- a/tests/test_pi_star.py +++ b/tests/test_pi_star.py @@ -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) ----------------------------