Skip to content

feat(utils): settle the hazma._core dispatch and error contract - #62

Merged
LoganAMorrison merged 2 commits into
masterfrom
claude/cython-to-rust/task-3.5-dispatch-and-error-layer
Aug 12, 2026
Merged

feat(utils): settle the hazma._core dispatch and error contract#62
LoganAMorrison merged 2 commits into
masterfrom
claude/cython-to-rust/task-3.5-dispatch-and-error-layer

Conversation

@LoganAMorrison

Copy link
Copy Markdown
Owner

Summary

  • Settles the argument-and-error contract every Phase 04–06 entry point
    will use
    , as three helpers over one classification in
    rust/src/dispatch.rs: map_unary (33 of the 35 dispatching entry
    points), map_flavors (the 2 neutrino ones — a 3-tuple for a scalar, a
    (3, N) array 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 reference's premise was false. "Every public function follows
    one shape; implement once as a helper" — classifying all 43 surviving
    top-level defs 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. A port that transcribed either would have broken the
    other's callers. The reference and the phase file are patched.
  • The rule that decides every divergence: each exception the Cython
    raises explicitly keeps its type; only its asserts 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 (what the 18 cross sections already
    do), a list or tuple is accepted (what the 17 hasattr-dispatching
    entry points already do), and the dtype message names the dtype, because
    the Cython has no single string to match (expected 'double' in the
    spectra, expected 'float64_t' in the mediator modules, for the same
    rejection).
  • No public value changes. git diff origin/master -- hazma is one
    file, hazma/_core.pyi, and every line of the hunk is comment text; the
    parity corpus ran in bit-equality mode (rtol = 0 across all 41
    consumed 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 AssertionErrorValueError
    tightening (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.
  • Two traps found by measurement, not 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 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.
  • Closes Phase 03. Learnings synthesized, phase frontmatter
    status: Complete, PLAN.md and working-memory rows updated. Across
    all five Phase 03 tasks the only change under hazma/ is the
    non-executable _core.pyi, comments only.

Project

projects/cython-to-rust/ — Task 3.5: Dispatch and error layer (the last
task of Phase 03, Numerics foundation).

See projects/cython-to-rust/task-notes/phase-03/task-3.5-dispatch.md for
implementation detail, decisions, the mutation table, and verification;
projects/cython-to-rust/learnings/phase-03-numerics-foundation.md for
the 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):

    PASS   black --check           test/test_core_dispatch.py test/parity/cases.py test/parity/test_parity.py
    PASS   isort --check-only      test/test_core_dispatch.py test/parity/cases.py test/parity/test_parity.py
    PASS   ruff check              test/test_core_dispatch.py test/parity/cases.py test/parity/test_parity.py
    PASS   cargo fmt --check       rust/
    PASS   cargo clippy            rust/
    PASS   cargo test              rust/
    PASS   pytest                  1378 passed, 13 skipped, 5 warnings in 559.15s (0:09:19)
    PASS   import hazma            version 2.1.0
    PASS   markdownlint            <10 files>
    PASS   forbidden tokens        none added
    
  • Bare pytest -q1378 passed, 13 skipped in 564.55s. +64 on
    Task 3.4's 1314, which is exactly test/test_core_dispatch.py growing
    from 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 -q118 passed in 1.58s
    (population derived by --collect-only -q, 11 classes). Coverage
    includes 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 live
    Cython
    , and a class that extracts the error strings from the .pyx
    sources 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:

    Provenance(exact=True, detail='')
    served kernels: []
    
  • Mutation campaign: 14 mutations against rust/src/dispatch.rs
    and 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 task
    note and produced a source correction rather than being dropped. Full
    table in the task note's ## Verification.

  • Numerical impact: nonegit diff origin/master -- hazma is
    hazma/_core.pyi only, 14 insertions(+), 8 deletions(-), comment text
    throughout. No grid sweep is reported because the parity corpus is a
    stricter grid than any ad-hoc one and nothing under hazma/ imports the
    code this PR adds.

🤖 Generated with Claude Code

LoganAMorrison and others added 2 commits August 11, 2026 17:22
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
LoganAMorrison merged commit 0a32de9 into master Aug 12, 2026
8 checks passed
@LoganAMorrison
LoganAMorrison deleted the claude/cython-to-rust/task-3.5-dispatch-and-error-layer branch August 12, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant