Merge pull request 'Rust parity: cargo green, lane parity unfalsifiable — plus core rust build was broken (plan §6A)' (#124) from fix/rust-build-env-and-parity-report into main
This commit is contained in:
commit
9d9fa340df
4 changed files with 262 additions and 3 deletions
19
core/cli.py
19
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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -27,6 +28,7 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_cognitive_turn_pipeline.py",
|
||||
"tests/test_audit_ledger_r7.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
|
||||
|
|
|
|||
157
docs/research/rust-parity-measurement-2026-07-26.md
Normal file
157
docs/research/rust-parity-measurement-2026-07-26.md
Normal file
|
|
@ -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`.
|
||||
87
tests/test_cli_runner_contract.py
Normal file
87
tests/test_cli_runner_contract.py
Normal file
|
|
@ -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"
|
||||
)
|
||||
Loading…
Reference in a new issue