From 808f3d32c4f51b7bf261f58685a6358fab09f48b Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 26 Jul 2026 11:50:32 -0700 Subject: [PATCH] fix(cli): core rust build was broken; report Rust parity as unfalsifiable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan §6A, report-only per DIVISION-OF-WORK §4. Both halves ran. The cargo half is real evidence; the LANE half is vacuous, and saying so is most of the point. cargo test (PYO3_PYTHON=/opt/homebrew/bin/python3.12, --offline): 43 passed, 0 failed across 7 suites including test_crdt_hash_parity.rs. The plan's read was right — cargo 1.97.1 and the registry cache are present locally and the cloud session was blocked only by sandbox network policy. Lane parity: 11/11 pinned SHAs match under both backends. That number means nothing. Sabotaging EVERY core_rs callable to raise and re-running under CORE_BACKEND=rust still gives `lanes: 11/11 match pinned SHAs`, exit 0. Three sufficient reasons: - The hot path bypasses the dispatch. algebra.cga.cga_inner is NOT algebra.backend.cga_inner; 69 call sites import the hot functions from algebra.cga/algebra.cl41 directly vs 24 through algebra.backend. The ~34k cga_inner->geometric_product calls/turn that plan §6C calls ~73% of turn time never reach the dispatch. - The dtype gates reject the runtime workload. versor_condition IS imported from algebra.backend by pipeline.py:24, and still falls through: the Rust arm requires f32 and the wave-field workload is f64. - Every dispatch arm ends in `except (..., Exception): pass`, so a broken, garbage-returning, or absent kernel falls back to Python SILENTLY. That is what makes the first two unfalsifiable by lane hashes. Instrumented under CORE_BACKEND=rust: zero Rust calls in the whole deductive_logic lane (pure ROBDD, no CGA), and exactly ONE (versor_apply_with_closure_f64) in a full pipeline turn. CORE_BACKEND=rust is today a one-call switch. So §6B ("if parity holds, consider Rust default") rests on a measurement that cannot fail, and §6C is the real blocker and is upstream of the backend. NOT acted on — §6.4 says a parity/determinism finding is escalated, not fixed. Recorded three follow-ups in dependency order: make the dispatch fail loudly, add a parity test that fails when the Rust path is not TAKEN, only then route the hot path. The one thing fixed here is a plain defect blocking the measurement. `core rust build` — the activation path print_rust_status() itself recommends — raised `TypeError: _run() got an unexpected keyword argument 'env'` on every invocation. core/cli.py::_run is injected into cli_rust.cmd_rust_build, which passes env=rust_build_env() to carry PYO3_USE_ABI3_FORWARD_COMPATIBILITY, and _run lacked the keyword the CommandRunner Protocol declares. One keyword, plus tests/test_cli_runner_contract.py deriving the expected keywords FROM the Protocol so the two cannot drift apart again. Verified end-to-end: `core rust build` now builds and installs. [Verification]: in-worktree on canonical CPython 3.12.13 with `uv sync --locked`: smoke 575 (569 + 6), deductive 291 (unchanged). New tests fail under mutation (revert the env keyword -> 3 red). Registered in `fast` AND `smoke`. Installing core_rs does not activate it (_ALLOW_RUST requires CORE_BACKEND), verified: extension present + no env var -> "rust backend: inactive", so no default behaviour moves. --- core/cli.py | 19 ++- core/cli_test.py | 2 + .../rust-parity-measurement-2026-07-26.md | 157 ++++++++++++++++++ tests/test_cli_runner_contract.py | 87 ++++++++++ 4 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 docs/research/rust-parity-measurement-2026-07-26.md create mode 100644 tests/test_cli_runner_contract.py diff --git a/core/cli.py b/core/cli.py index f8cc6cea..5e13342a 100644 --- a/core/cli.py +++ b/core/cli.py @@ -33,9 +33,22 @@ from core.cli_test import TEST_SUITES as _TEST_SUITES # noqa: E402 -def _run(*args: str, check: bool = False, cwd: Path | None = None) -> int: - """Run a child command and return its exit code.""" - completed = subprocess.run(args, check=check, text=True, cwd=cwd) +def _run( + *args: str, + check: bool = False, + cwd: Path | None = None, + env: dict[str, str] | None = None, +) -> int: + """Run a child command and return its exit code. + + ``env`` is part of the ``CommandRunner`` contract that ``core/cli_rust.py`` + and ``core/cli_test.py`` are written against; omitting it here made + ``core rust build`` raise ``TypeError`` unconditionally, because + ``cli_rust.cmd_rust_build`` passes ``env=rust_build_env()`` to carry + ``PYO3_USE_ABI3_FORWARD_COMPATIBILITY``. The injected runner has to accept + everything the injection sites pass. + """ + completed = subprocess.run(args, check=check, text=True, cwd=cwd, env=env) return int(completed.returncode) diff --git a/core/cli_test.py b/core/cli_test.py index ab018585..943dd94a 100644 --- a/core/cli_test.py +++ b/core/cli_test.py @@ -13,6 +13,7 @@ from typing import Protocol TEST_SUITES: dict[str, tuple[str, ...]] = { "fast": ( "tests/test_cli_test_suites.py", + "tests/test_cli_runner_contract.py", "tests/test_runtime_config.py", "tests/test_core_semantic_seed_pack.py", "tests/test_intent_proposition_graph.py", @@ -26,6 +27,7 @@ TEST_SUITES: dict[str, tuple[str, ...]] = { "tests/test_runtime_config.py", "tests/test_cognitive_turn_pipeline.py", "tests/test_architectural_invariants.py", + "tests/test_cli_runner_contract.py", # Audio sensorium lane — part of the smoke.yml PR gate (compiler, # CRDT merge, eval gates, pack manifest, mount, teachers; ~3s). # Listed explicitly so the local-first pre-push gate (AGENTS.md diff --git a/docs/research/rust-parity-measurement-2026-07-26.md b/docs/research/rust-parity-measurement-2026-07-26.md new file mode 100644 index 00000000..071864cf --- /dev/null +++ b/docs/research/rust-parity-measurement-2026-07-26.md @@ -0,0 +1,157 @@ +# Rust backend parity — measured, and why the instructed method cannot fail + +**Date:** 2026-07-26 · **Arc:** curriculum-license-loop-2026-07 · **Unit:** plan §6A +**Instruction:** "`PYO3_PYTHON=/opt/homebrew/bin/python3.12`, `cargo test` in +`core-rs/`, then rerun the hash-pinned lanes under `CORE_BACKEND=rust`. **Report the +measurement; do not act on it.**" (`DIVISION-OF-WORK.md` §4) + +Both halves ran. The first is a real signal. **The second is vacuous, and this note +exists mostly to say so** — run as instructed it produces "11/11 lanes match, parity +holds", which is true, and which would be read as evidence about the Rust kernels +when it is not evidence about them at all. + +--- + +## 1. `cargo test` — 43 passed, 0 failed + +``` +PYO3_PYTHON=/opt/homebrew/bin/python3.12 cargo test --offline +``` + +| suite | tests | +|---|---:| +| unit (`src/`) | 7 | +| `test_arena.rs` | 10 | +| `test_cga.rs` | 5 | +| `test_cl41.rs` | 7 | +| `test_crdt_hash_parity.rs` | 6 | +| `test_vault.rs` | 4 | +| `test_versor.rs` | 4 | +| **total** | **43 passed / 0 failed** | + +The plan's read was correct: cargo 1.97.1 and the registry cache are present +locally, and the cloud session was blocked only by sandbox network policy. +`--offline` needed no downloads. This half is genuine evidence — in particular +`test_crdt_hash_parity.rs` includes `parity_permutation_invariant_matches_python` +and `parity_signed_zero_distinct`. + +## 2. Lane parity — 11/11 both ways, and the number means nothing + +``` +uv run python scripts/verify_lane_shas.py # lanes: 11/11 match +CORE_BACKEND=rust uv run python scripts/verify_lane_shas.py # lanes: 11/11 match +``` + +Identical, byte for byte, every lane. Then the check that matters: + +> **Sabotaged every `core_rs` callable to raise, re-ran under `CORE_BACKEND=rust`: +> `lanes: 11/11 match pinned SHAs`, exit 0.** + +A measurement that passes with the entire Rust backend replaced by exceptions is +not measuring the Rust backend. Three independent reasons, each sufficient: + +**(a) The hot path bypasses the dispatch.** `algebra/backend.py` is the only place +that consults `CORE_BACKEND`, and `algebra.cga.cga_inner is not +algebra.backend.cga_inner`. Counting call sites for the hot functions: + +| import route | call sites | +|---|---:| +| `from algebra.cga import …` / `from algebra.cl41 import …` (**bypasses** dispatch) | **69** | +| `from algebra.backend import …` (uses dispatch) | 24 | + +So the ~34k `cga_inner` → `geometric_product` calls per turn that plan §6C +identifies as ~73% of turn time do not reach the dispatch at all. + +**(b) The dtype gates reject the runtime workload.** `versor_condition`, +`cga_inner` and the f32 arm of `geometric_product` require +`_is_f32_workload(...)`. `core/cognition/pipeline.py:24` imports +`versor_condition` from `algebra.backend` — the dispatch IS consulted there — and +still falls through, because the wave-field workload is f64. + +**(c) Every dispatch arm swallows every exception.** Each Rust branch is wrapped in +`except (AttributeError, TypeError, ValueError, Exception): pass`. A Rust kernel +that raises, returns garbage of the wrong type, or is missing entirely produces a +**silent** fallback to Python. That is what makes (a) and (b) unfalsifiable by lane +hashes: Python answers, the hash matches, the lane is green. + +Instrumented call counts under `CORE_BACKEND=rust`, wrapping every `core_rs` +callable: + +| workload | Rust kernel calls | +|---|---| +| `deductive_logic_v1` lane (whole lane) | **none** — the lane is pure ROBDD, no CGA | +| one full `CognitiveTurnPipeline.run` turn | **1** (`versor_apply_with_closure_f64`) | + +One call per turn. `CORE_BACKEND=rust` is, today, a one-call switch. + +## 3. What this means for the plan + +- **§6A is answered narrowly.** `cargo test` is green, and the one dispatched f64 + path a turn actually takes (`versor_apply_with_closure_f64`) is bit-identical — + lane hashes are unchanged with it live. Nothing broader was established. +- **§6B ("if parity holds: consider Rust default") rests on a measurement that + cannot fail.** Flipping the default today would change one call per turn and gain + approximately nothing, because the hot path does not route through the dispatch. + The premise needs to be re-earned before the decision is worth making. +- **§6C is the actual blocker, and it is upstream of the backend.** Accelerating + `cga_inner`/`geometric_product` requires routing their 69 call sites through + `algebra.backend` and giving the f64 workload a bit-identical f64 kernel. That is + a real change to a determinism-critical path, and it is exactly the kind of change + the lane pins exist to gate — but only once the pins can actually see it. + +**Not acted on** — the instruction is report-only, and §6.4 is explicit that a +parity or determinism finding is escalated rather than fixed. Three things a future +unit should consider, in dependency order: + +1. **Make the dispatch fail loudly.** Replace the blanket `except … Exception: pass` + with a narrow catch plus a one-time warning, or a strict mode that raises. While + fallback is silent, no parity measurement over lanes can be trusted, and this + note's conclusion would have to be re-derived by hand every time. +2. **A parity test that fails when the Rust path is not taken.** Assert a nonzero + Rust call count under `CORE_BACKEND=rust`, so "parity holds" cannot be satisfied + by never invoking Rust. Without it, §6A's exit criterion is unfalsifiable. +3. Only then: route the hot path through the dispatch (§6C). + +## 4. One defect found and fixed here + +`core rust build` — the documented activation path, named by +`print_rust_status()`'s own `activation: run \`core rust build\`` hint — raised +`TypeError: _run() got an unexpected keyword argument 'env'` on **every** +invocation. `core/cli.py::_run` is injected into `cli_rust.cmd_rust_build`, which +passes `env=rust_build_env()` to carry +`PYO3_USE_ABI3_FORWARD_COMPATIBILITY` (PyO3 0.21 supports Python through 3.12). +`_run` had no `env` parameter, while the `CommandRunner` Protocol both call sites +are typed against does. + +Fixed (one keyword) and pinned by `tests/test_cli_runner_contract.py`, which +derives the expected keywords **from the Protocol** rather than from a hand-written +list, so the two cannot drift apart again. The build had to be driven with +`python -m maturin develop --release` directly to take the measurements above. + +## 5. Reproduction + +```bash +# 1. Rust unit + parity suites +cd core-rs && PYO3_PYTHON=/opt/homebrew/bin/python3.12 cargo test --offline + +# 2. install the extension (needs the §4 fix, or call maturin directly) +PYO3_PYTHON=/opt/homebrew/bin/python3.12 uv run core rust build + +# 3. activation is opt-in: importable is NOT active +uv run python -c "from core.cli_rust import print_rust_status; print_rust_status()" +# -> rust backend : inactive +CORE_BACKEND=rust uv run python -c "from core.cli_rust import print_rust_status; print_rust_status()" +# -> rust backend : active + +# 4. lane hashes, both backends +uv run python scripts/verify_lane_shas.py +CORE_BACKEND=rust uv run python scripts/verify_lane_shas.py + +# 5. the check that shows 4 proves nothing: sabotage every kernel, rerun 4. +# Still 11/11. +``` + +Installing `core_rs` does **not** activate it — `_ALLOW_RUST` requires +`CORE_BACKEND` in `{rust, core_rs, rs}`, so a stray local build cannot silently +change default behaviour. Verified: with the extension installed and no env var, +`rust backend : inactive`. diff --git a/tests/test_cli_runner_contract.py b/tests/test_cli_runner_contract.py new file mode 100644 index 00000000..a8e74f9f --- /dev/null +++ b/tests/test_cli_runner_contract.py @@ -0,0 +1,87 @@ +"""The injected `CommandRunner` must accept everything its call sites pass. + +`core/cli.py::_run` is injected into `cli_rust.cmd_rust_build` and +`cli_test`'s runner. Both are typed against a `CommandRunner` Protocol carrying +`check`, `cwd` and `env`. `_run` was missing `env`, so `core rust build` raised +`TypeError: _run() got an unexpected keyword argument 'env'` on every +invocation — the documented Rust activation path had never worked. + +Nothing caught it because the runner is only exercised through subprocess-y +commands that tests avoid calling for real. So the pin is on the SIGNATURE, +checked against the Protocol rather than against a hand-written list, so a +future keyword added to one side cannot drift from the other. +""" + +from __future__ import annotations + +import inspect + +import pytest + +from core.cli import _run +from core.cli_test import CommandRunner, run_command + + +def _keywords(fn: object) -> set[str]: + sig = inspect.signature(fn) # type: ignore[arg-type] + return { + name + for name, p in sig.parameters.items() + if p.kind in (p.KEYWORD_ONLY, p.POSITIONAL_OR_KEYWORD) + and name not in ("args", "self") + } + + +def test_injected_runner_satisfies_the_command_runner_protocol() -> None: + """Derived from the Protocol, so it cannot fall behind it.""" + protocol_kwargs = _keywords(CommandRunner.__call__) + assert protocol_kwargs, "CommandRunner.__call__ has no keywords — protocol changed?" + missing = protocol_kwargs - _keywords(_run) + assert not missing, ( + f"core.cli._run is missing {sorted(missing)} — it is injected into " + "cli_rust.cmd_rust_build, which passes env=rust_build_env(). A runner " + "that cannot accept what its call sites pass raises TypeError at runtime." + ) + + +def test_default_runner_and_injected_runner_agree() -> None: + """Both runners are used interchangeably; their keywords must match.""" + assert _keywords(_run) == _keywords(run_command) + + +@pytest.mark.parametrize("keyword", ["check", "cwd", "env"]) +def test_runner_accepts_each_documented_keyword(keyword: str) -> None: + assert keyword in _keywords(_run) + + +def test_rust_build_passes_env_so_the_runner_must_accept_it() -> None: + """Pins the actual call site that broke, not just the signature. + + Runs `cmd_rust_build` with a recording fake runner: it must reach the + maturin invocation and carry an env, rather than raising TypeError. + """ + import argparse + + from core import cli_rust + + seen: list[dict[str, object]] = [] + + def fake_run(*args: str, check: bool = False, cwd: object = None, + env: dict[str, str] | None = None) -> int: + seen.append({"args": args, "env": env}) + return 0 + + args = argparse.Namespace(skip_auditwheel=False) + rc = cli_rust.cmd_rust_build( + args, run=fake_run, fail=lambda m, code=2: (_ for _ in ()).throw(SystemExit(code)), + python_executable="python3", + ) + assert rc == 0 + maturin = [c for c in seen if "maturin" in c["args"]] + assert maturin, f"maturin was never invoked; calls={seen}" + env = maturin[-1]["env"] + assert isinstance(env, dict) + assert env.get("PYO3_USE_ABI3_FORWARD_COMPATIBILITY") == "1", ( + "the env is the whole reason the keyword exists — PyO3 0.21 supports " + "Python through 3.12 and needs the forward-compatibility escape hatch" + )