feat(utils): settle the hazma._core dispatch and error contract - #62
Merged
LoganAMorrison merged 2 commits intoAug 12, 2026
Merged
Conversation
Task 3.5 of the cython-to-rust project, and the one that closes Phase 03.
The reference's premise — "every public function follows one shape;
implement once as a helper" — is false. Classifying all 43 surviving
top-level `def`s from source and measuring a representative of each
against a 24-input matrix found four shapes, two of which disagree with
each other: the 15 hasattr-dispatching spectra entry points raise
AssertionError on a 0-d array and accept a list, while the 18 cross
sections accept a 0-d array (`.item()`) and reject a list with
AttributeError. Transcribing either one would have broken the other's
callers.
`rust/src/dispatch.rs` is therefore three helpers over one
classification: `map_unary` (33 of the 35 dispatching entry points),
`map_flavors` (the 2 neutrino ones — 3-tuple for a scalar, (3, N) for a
grid, kernel called once per energy), and `require_vector`
(`partial_widths`; rank and dtype only, never length). Kernels stay
PyO3-free and pass their quantity wording in.
The rule that decides every divergence: each exception the Cython raises
explicitly keeps its type, and only its `assert`s change type (rules.md
rule 9 — today they vanish under `python -O`). Three widenings ride
along, none of which can break a call that works today: a 0-d array
takes the scalar path, a list or tuple is accepted, and the dtype
message names the dtype (the Cython has no single string to match — the
spectra say `expected 'double'`, the mediator modules `expected
'float64_t'`, for the same rejection).
Two traps found by measurement rather than reasoning. A 0-d array's
`__float__` forwards to its element and `np.str_` subclasses `str`, so a
draft that accepted a 0-d array by attempting `extract::<f64>` returned
a number for `dnde_photon("15.0", 200.0)`; the guard is a dtype-`kind`
check. And the mutation campaign (14 mutations, 13 caught) refuted a
claim in the implementation's own comment — an argument-ordering swap
described as load-bearing turned out to be unobservable — which was
corrected rather than dropped.
No public value changes: the only diff under `hazma/` is `_core.pyi`,
comments throughout, and the parity corpus ran in bit-equality mode.
What the task does settle is five user-visible exception changes that
land with Phases 04-06, logged in the working memory's numerical record
for the Phase 07 CHANGELOG.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #62 review round 1. The stale-state sweep had two disjoint populations and only one was swept: `Task 3.5 decides` — the *pointer* text — went across every durable doc, while the **statements of the pre-decision contract** carry no such token and were left asserting the old rules as current fact. Corrected, verified against `rust/src/dispatch.rs` rather than read: - `learnings/phase-02-rust-scaffold.md` §2 said a 0-d array must be `float64`, that anything else raises `ValueError`, that a 0-d array still enforces dtype, and that `map_unary` was the sole helper. All four moved in this PR. Rewritten with the settled contract, each bullet annotated with what Phase 02 read and what Task 3.5 changed, under a pointer to the Phase 03 learnings. - `task-notes/README.md`'s Task 2.3 finding carried the same 0-d claim. Struck through and replaced. Re-sweeping on the *behavior* words rather than the task id (`still enforces dtype`, `anything else → ValueError`, `single implementation`) found five more live occurrences across three further files — `task-notes/phase-02/{README.md,task-2.1,task-2.3}` — each now carrying a dated supersession note. Two of them had predicted this ("a Task 3.5 decision that changes any of them turns a named test red"), so closing their own loop was one line each. Recorded in `docs/agents/lessons.md` as `[settling-a-deferral-has-two-sweeps]`: the pointers and the statements share no token, so one pattern cannot find both. Docs only — `git diff HEAD -- rust/ test/ hazma/` is empty, so the code half of this PR is byte-identical to the reviewed 7f9f90f and no numerical re-measurement applies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LoganAMorrison
deleted the
claude/cython-to-rust/task-3.5-dispatch-and-error-layer
branch
August 12, 2026 01:55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
will use, as three helpers over one classification in
rust/src/dispatch.rs:map_unary(33 of the 35 dispatching entrypoints),
map_flavors(the 2 neutrino ones — a 3-tuple for a scalar, a(3, N)array for a grid, kernel called once per energy), andrequire_vector(partial_widths— rank and dtype only, never length).Kernels stay PyO3-free and pass their quantity wording in.
one shape; implement once as a helper" — classifying all 43 surviving
top-level
defs from source and measuring a representative of eachagainst a 24-input matrix found four shapes, two of which disagree
with each other: the 15
hasattr-dispatching spectra entry points raiseAssertionErroron a 0-d array and accept a list, while the 18 crosssections accept a 0-d array (
.item()) and reject a list withAttributeError. A port that transcribed either would have broken theother's callers. The reference and the phase file are patched.
raises explicitly keeps its type; only its
asserts change type(
rules.mdrule 9 — today they vanish underpython -O). Threewidenings ride along, none of which can break a call that works today —
a 0-d array takes the scalar path (what the 18 cross sections already
do), a list or tuple is accepted (what the 17
hasattr-dispatchingentry points already do), and the dtype message names the dtype, because
the Cython has no single string to match (
expected 'double'in thespectra,
expected 'float64_t'in the mediator modules, for the samerejection).
git diff origin/master -- hazmais onefile,
hazma/_core.pyi, and every line of the hunk is comment text; theparity corpus ran in bit-equality mode (
rtol = 0across all 41consumed entry points, 179,695 pinned values). What the task does
settle is five user-visible exception changes that land with Phases
04–06 — the two widenings above, the
AssertionError→ValueErrortightening (message byte-identical), the reworded dtype error, and
hazma/spectra/_neutrino/_muon.pyx:205's copy-pasted "Photon energies"becoming "Neutrino energies". All five are logged in the working
memory's Numerical impact so far so the Phase 07 CHANGELOG picks them
up.
__float__forwards to its element andnp.str_subclassesstr, so adraft that accepted a 0-d array by attempting
extract::<f64>returneda number for
dnde_photon("15.0", 200.0); the guard is a dtype-kindcheck. And the mutation campaign refuted a claim in the implementation's
own comment — an argument-ordering swap described as load-bearing turned
out to be unobservable — which was corrected rather than dropped.
status: Complete,PLAN.mdand working-memory rows updated. Acrossall five Phase 03 tasks the only change under
hazma/is thenon-executable
_core.pyi, comments only.Project
projects/cython-to-rust/— Task 3.5: Dispatch and error layer (the lasttask of Phase 03, Numerics foundation).
See
projects/cython-to-rust/task-notes/phase-03/task-3.5-dispatch.mdforimplementation detail, decisions, the mutation table, and verification;
projects/cython-to-rust/learnings/phase-03-numerics-foundation.mdforthe phase's durable record.
Test plan
scripts/agents/preflight.sh --paths "test/test_core_dispatch.py test/parity/cases.py test/parity/test_parity.py" --md "<10 changed docs>"→ RESULT: PASS (every gate PASS, no WARN):Bare
pytest -q→1378 passed, 13 skipped in 564.55s. +64 onTask 3.4's 1314, which is exactly
test/test_core_dispatch.pygrowingfrom 54 tests to 118 — no other module's count moved. The skip count
is unchanged at 13, which is what proves the parity corpus ran in
bit-equality mode.
pytest test/test_core_dispatch.py -q→118 passed in 1.58s(population derived by
--collect-only -q, 11 classes). Coverageincludes the scalar path, the 0-d decision across five numeric dtypes,
the 1-D array path, sequences, every error path and its exact message,
the flavor shape (including the transpose-invisible length-3 case),
require_vector, five declared divergences asserted against the liveCython, and a class that extracts the error strings from the
.pyxsources and renders each through the port.
cargo test --manifest-path rust/Cargo.toml --no-default-features→
69 passed(2 new).Corpus mode checked directly rather than inferred:
Mutation campaign: 14 mutations against
rust/src/dispatch.rsand
rust/src/kernels.rs, applied one at a time from a green baseline,reverted after each, baseline re-asserted at the end (both runs
118 passed). 13 caught. The one survivor is analysed in the tasknote and produced a source correction rather than being dropped. Full
table in the task note's
## Verification.Numerical impact: none —
git diff origin/master -- hazmaishazma/_core.pyionly,14 insertions(+), 8 deletions(-), comment textthroughout. No grid sweep is reported because the parity corpus is a
stricter grid than any ad-hoc one and nothing under
hazma/imports thecode this PR adds.
🤖 Generated with Claude Code