Skip to content

workspace split: extract mortie-core (issue #200) - #207

Draft
espg wants to merge 14 commits into
mainfrom
claude/200-workspace-split
Draft

workspace split: extract mortie-core (issue #200)#207
espg wants to merge 14 commits into
mainfrom
claude/200-workspace-split

Conversation

@espg

@espg espg commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Closes #200. Refs #204 and the #48 boundary ruling (#48 (comment)): mortie-core is the codec only, contractually dependency-minimal, and never grows a moc-crate dependency. Full plan with the recon-verified codec boundary: #200 (comment)

What this does

Converts the repo to a cargo workspace and extracts the packed-word codec into a new mortie-core crate (decimal_morton minus its rayon batch submodule, plus the morton pivot bridge), with mortie_rustie re-importing everything so no Rust path, bench, or Python-visible symbol changes. The (depth, nested-ipix) ↔ packed-word pivot primitives (from_nested / from_nested_point / to_nested, and nested2mort / mort2nested) are the crate's documented public hinge, per #200 (comment) — they're what healpix-geo composes zuniq↔morton conversions through.

This PR is only the workspace split: no publishing, no tags, no crates.io metadata beyond name/version/license/description/repository on the workspace member (#201/#202/#203 handle the crates.io track).

Phases

  • Phase 1 — workspace scaffolding: [workspace] on the root manifest (with the standalone mortie/tests/generate_geodesy_oracle helper excluded), stub mortie-core member, path dependency wired. Nothing moves; builds byte-identically. (fac6720, folds b20c327 d77ae9e bd462d3)
  • Phase 2 — extract the codec into mortie-core; mortie_rustie re-imports via a shim module (pub use mortie_core::decimal_morton::* + the unmoved batch submodule; pub use mortie_core::morton). Zero behavior change. (c7004c1, folds 892f970 dd8e075)
  • Phase 3 — standalone testability: inline test suites travel with the files; crate-root pivot re-exports + an external-view integration test (mortie-core/tests/pivot_surface.rs); cargo test -p mortie-core green with an empty dep tree. (fc81d99, folds 21b5ecf 688cd86)
  • Phase 4 — docs: BUILDING.md workspace-layout section + README/docs/index.md note; rustdoc on the new public surface (and the crate's one rustdoc warning cleared). (2bd50c7, folds bdc399e 9fdc87c 9e8ad86)

What moved, and what did not

lands in mortie-core stays in mortie_rustie
decimal_morton.rs — the packed-word grammar, encode/encode_point/decode, order/truncation/containment arithmetic (order_of, kind_of, base_cell_of, coarsen, common_ancestor), the decimal-string grammar, the legacy converter, and the pivot primitives decimal_morton/batch.rs (rayon), prefix_trie.rs, geo2mort.rs, coverage*, moc*, toc.rs, sphere*, dissolve*, wkb*, buffer.rs, cell_geom.rs, linestring.rs, rank_xy.rs, authalic.rs, arrow_ffi.rs, all of lib.rs (pyo3/numpy)
morton.rs — the (nested, depth)-tuple-order pivot bridge the three benches (they import geometry/healpix too)

One test moved rather than being deleted: from_nested_agrees_with_healpix_crate used the healpix crate as an oracle, which mortie-core may not depend on, so it now lives in src_rust/src/geo2mort.rs's tests with a breadcrumb left where it was.

cargo tree -p mortie-core is a single line:

mortie-core v0.1.0 (/…/mortie-core)

and mortie-core/tests/dep_contract.rs fails the suite if the manifest ever grows a dependency table, so the #48 contract is mechanical rather than prose. mortie-core/tests/pivot_surface.rs reaches the whole documented surface the way a downstream crate does (an integration test compiles as its own crate), so a visibility slip breaks the build here instead of silently breaking healpix-geo after publication; its sweep covers every stored tuple value and all 16 (t28,t29) tail combinations.

CI / build wiring

No workflow edits made or needed: maturin builds from pyproject + the root manifest and includes the in-tree path dependency in the sdist (verified — the built sdist contains mortie-core/{Cargo.toml,src,tests}, and a wheel builds from the extracted sdist); the tag-sync sed in build-wheels.yml touches only the root Cargo.toml (mortie-core's version is independent); cargo codspeed build/run at the workspace root still covers the three root-package benches (mortie-core has none). default-members = [".", "mortie-core"] on the root manifest keeps bare cargo test/cargo bench covering both members, since a non-virtual workspace would otherwise default to the root package alone.

How it was tested

Every phase, on each commit:

  • cargo test (workspace): 399 passed, 1 ignored at head — 327 mortie_rustie + 66 mortie-core lib + 2 dep_contract + 4 pivot_surface. Phase-1 baseline was 393; the +6 are the new integration tests, and the codec's 66 are unchanged in count and content by the move.
  • cargo test -p mortie-core standalone: 72 passed, no flags needed (the codec crate links nothing).
  • cargo tree -p mortie-core: one line, zero dependencies.
  • cargo bench --no-run: all three benches compile.
  • cargo fmt --all --check, cargo clippy -p mortie-core --all-targets: clean. (cargo clippy on the root package has 7 pre-existing warnings in files this PR doesn't touch.)
  • cargo doc -p mortie-core --no-deps: zero warnings.
  • mkdocs build --strict: clean.
  • maturin develop --release + full pytest: 1655 passed, 16 skipped — identical to the phase-1 baseline. No Python-visible behavior or symbol change.
  • Note: on this macOS dev machine the root package's test binaries need RUSTFLAGS="-C link-arg=-undefined -C link-arg=dynamic_lookup" to link (pyo3 extension-module skips libpython linkage). This is pre-existing — verified identical on unmodified main — untouched by this PR, and now written down in BUILDING.md.

Each phase went through a fresh-context adversarial review and a fold pass: 3 findings on phase 1, 2 on phase 2, 2 on phase 3, 3 on phase 4 — all folded, one commit each, every inline thread answered.

Questions for review

  1. Crate location (open): mortie-core/ sits at the repo root, sibling of mortie/ and src_rust/, since it's headed for standalone extraction in publish mortie-core to crates.io: naming, semver posture, release mechanics #201. One-commit move if you'd rather it live under src_rust/.
  2. Version seed (RESOLVED): independent semver confirmed by the ruling on publish mortie-core to crates.io: naming, semver posture, release mechanics #201 (publish mortie-core to crates.io: naming, semver posture, release mechanics #201 (comment)) — mortie-core starts at 0.1.0, decoupled from the Python package's tag-synced version; its 1.0.0 is gated on the healpix-geo dep PR (healpix-geo dep PR: expose the mortie-core codec through their core + wasm bindings (staged, espg posts) #203), not mortie's own 1.0 tag.
  3. Crate-root re-exports (open): only the pivot pair (from_nested, to_nested) is re-exported at the crate root; everything else, including from_nested_point and the morton tuple-order bridge, stays under mortie_core::decimal_morton:: / ::morton::. Happy to flatten more (or none) if you have a preference for the healpix-geo dep PR: expose the mortie-core codec through their core + wasm bindings (staged, espg posts) #203 dep surface.
  4. No CI job runs cargo test (raised, not acted on): the phase-2 review pointed out that mortie-core/tests/dep_contract.rs — and now pivot_surface.rs — only fire for whoever runs the suite locally. grep -rn cargo .github/workflows/ finds only cargo install cargo-codspeed and cargo codspeed build/run; there has never been a Rust test job in this repo, so this predates the split, but the split is what makes it load-bearing (the dependency contract is the thing healpix-geo relies on). A follow-up Rust-checks workflow covering cargo test / cargo fmt --check / cargo clippy across both members would close it. Not done here: workflow changes are out of scope for an issue that doesn't name them, and the crate docs were reworded to state the guard's real reach rather than claim CI enforcement.

@espg espg added the implement label Aug 19, 2026
@espg espg added this to the 1.0.0 milestone Aug 19, 2026
Comment thread Cargo.toml
@@ -1,3 +1,8 @@
[workspace]
members = ["mortie-core"]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

default-members defaults to the root package only, so bare cargo commands will silently skip mortie-core from phase 2 onward.

Verified on this commit:

$ cargo metadata --format-version 1 --no-deps   # (fields extracted)
workspace_members:         ["mortie-core#0.1.0", "mortie@0.9.9"]
workspace_default_members: ["mortie@0.9.9"]

Cargo's rule: in a non-virtual workspace (root manifest carries a [package], as here), default-members defaults to the root package alone — not "all members", which is the default only for a virtual manifest. Phase 1 hides the consequence because mortie-core is empty, but once the codec and its unit tests move in phase 2:

  • cargo test, cargo bench, cargo clippy — the exact commands BUILDING.md tells contributors to run ("Testing", and "Contributing" steps 1/3/5) — build only mortie. The moved codec tests stop running for anyone who does not type --workspace.
  • .github/workflows/codspeed.yml runs bare cargo codspeed build / cargo codspeed run; any bench that follows the codec into mortie-core is outside the default package selection.
  • The PR body's green Rust result is cargo test --workspace (393). That flag is load-bearing, and nothing in CI or the docs supplies it — the repo has no cargo test step at all (checked all five workflows).

Cheapest fix while the crate is still empty: add default-members = [".", "mortie-core"] to this [workspace] table (and/or make the CI and BUILDING.md invocations explicitly --workspace). Landing it with the scaffolding keeps phase 2 from being the commit where Rust coverage quietly drops.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Accepted and fixed in b20c327default-members = [".", "mortie-core"] added to the [workspace] table, with a comment recording why (non-virtual workspace, so the default is the root package alone).

Verified the selection now covers both members:

$ cargo metadata --format-version 1 --no-deps   # (fields extracted)
workspace_members:         ["mortie-core#0.1.0", "mortie@0.9.9"]
workspace_default_members: ["mortie@0.9.9", "mortie-core#0.1.0"]

and the root package still builds every target: cargo check --all-targetsFinished dev profile. Full local gate after the fold: cargo test --workspace 393 passed / 1 ignored plus the new mortie-core tests, and cargo bench --no-run builds all four bench executables.

You are right that this is cheapest to land now while the crate is empty — with it in place, the phase-2 move of decimal_morton/morton carries its unit tests into the default selection rather than out of it, so bare cargo test/cargo bench/cargo codspeed build keep covering the codec without anyone having to remember --workspace.

Leaving the BUILDING.md / codspeed.yml half of your suggestion alone deliberately: with default-members correct, the documented bare invocations are already right, and editing .github/workflows/ is out of bounds for this PR beyond mechanical path fixes.

Comment thread mortie-core/Cargo.toml
edition = "2021"
authors = ["Shane Grigsby (espg) <refuge@rocktalus.com>"]
description = "Packed-word morton codec for HEALPix grids (the mortie id codec)"
license = "MIT"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Publish metadata is incomplete for a crate whose stated purpose is to be published.

cargo already warns on every packaging run of this manifest — straight out of maturin sdist on this commit:

From `cargo package --list --allow-dirty --manifest-path .../mortie-core/Cargo.toml`:
warning: manifest has no documentation, homepage or repository

And the packaged file list is:

$ cargo package --list --allow-dirty --manifest-path mortie-core/Cargo.toml
.cargo_vcs_info.json
Cargo.lock
Cargo.toml
Cargo.toml.orig
src/lib.rs

So the crate declares license = "MIT" but ships no license text: the repo's LICENSE sits at the workspace root, and cargo package only collects files inside the package directory. Per the #48 ruling (comment 5339056931) this crate exists precisely so it can be a published dependency for the healpix-geo/mortie-core split, so the .crate that eventually lands on crates.io would carry a bare license identifier, no license file, and no repository link.

Concretely missing here: repository (plus homepage/documentation to silence the warning), readme, rust-version (no MSRV is declared anywhere in the repo — the root manifest has none either), and a LICENSE file under mortie-core/ (or a license-file pointing at one inside the package).

Separately: version/edition/authors/license are now duplicated between the root manifest and this one, with no shared source. A [workspace.package] table plus field.workspace = true here would keep them from drifting — worth noting that build-wheels.yml's release step rewrites only the root manifest's ^version = line, so these copies are already on a different track by construction.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Partially folded — the packaging warning is fixed in d77ae9e; the rest is scoped to #201.

Taken: repository = "https://github.com/espg/mortie" added to mortie-core/Cargo.toml. That is the one item whose cost is paid now, on every packaging run, and it is one line. Before:

$ cargo package --list --allow-dirty --manifest-path mortie-core/Cargo.toml
warning: manifest has no documentation, homepage or repository
.cargo_vcs_info.json
...

after: same file list, no warning.

Left standing, for #201: the license text under mortie-core/ (or license-file), readme, rust-version/MSRV, homepage/documentation, and the [workspace.package] + field.workspace = true dedup. Not a disagreement on the merits — your cargo package --list output is right that the eventual .crate would ship a bare MIT identifier with no license file, and right that the root/member copies of version/edition/authors/license have no shared source. It is a scope call: this PR is the workspace split only, with no crates.io metadata beyond what a path-dependency workspace member needs (issue #200 ruling), and crates.io publish prep is issue #201 with the licensing/provenance diligence in #202. mortie-core is a path dependency of mortie today and is not published by anything in this PR, so none of those omissions can bite before #201 lands.

One point from your comment worth carrying over to #201 verbatim, because it is a real trap: build-wheels.yml's release step rewrites only the root manifest's ^version = line, so mortie-core's version is already on a separate track by construction — whoever does #201 has to decide whether the two versions move together (version.workspace = true) or independently, before the first publish rather than after.

These stay standing for espg to route onto #201 — I have not filed or relabelled anything, since that is a side-effecting call.

Comment thread mortie-core/src/lib.rs Outdated
//!
//! Workspace scaffolding (issue #200, phase 1): the codec modules
//! (`decimal_morton`, `morton`) move here in phase 2. This crate is
//! contractually dependency-minimal (issue #48): no non-std dependencies,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

The #48 dependency-minimality contract is prose-only, and no CI job ever exercises mortie-core.

I read all five workflows (build-wheels, codspeed, docs, lint, test): none runs cargo test, cargo check, or cargo clippy. The only thing that compiles Rust in CI is the maturin wheel/sdist build, which reaches mortie-core solely as a transitive dependency of mortie — and per the default-members finding on the root manifest, even a bare cargo invocation at the workspace root leaves this package out of the default selection.

So "no non-std dependencies, and never a moc-crate dependency" is enforced by nothing but this doc comment. A phase-2/3 commit can add a dependency to mortie-core/Cargo.toml and every check in the repo stays green, while the crate is contractually the external consumers' minimal dep (issue #48 comment 5339056931; the healpix-geo/mortie#48 thread).

A one-line guard is cheap enough to land with the scaffolding — verified working on this commit:

$ cargo tree -p mortie-core --edges normal --prefix none | tail -n +2 | wc -l
0

i.e. a CI step asserting that output is empty makes the contract mechanical instead of aspirational.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Accepted — the contract is now mechanical as of bd462d3, though enforced from inside the crate rather than from CI.

New mortie-core/tests/dep_contract.rs reads the crate's own manifest via env!("CARGO_MANIFEST_DIR") and fails if any dependency table appears. It is a std-only line scan — no TOML parser — because pulling a parser in to guard a zero-dependency contract would be the very thing the contract forbids. It matches per dot-separated segment, so [dependencies], [dev-dependencies], [build-dependencies], [dependencies.moc] and [target.'cfg(unix)'.dependencies] all trip it, and a second test pins those forms plus the non-matches ([package], a commented-out header) so the guard itself cannot silently stop guarding.

Confirmed it actually fails rather than merely passing — temporarily appending [dependencies]\nsmallvec = "1" to the manifest:

thread 'manifest_declares_no_dependencies' panicked at mortie-core/tests/dep_contract.rs:27:5:
mortie-core must stay dependency-free (issue #48), but .../mortie-core/Cargo.toml declares: ["[dependencies]"]

then reverted. Clean state passes: cargo test -p mortie-core → 2 passed; cargo clippy -p mortie-core --all-targets and cargo fmt --check clean.

On the CI step specifically: deferred to espg, not declined. Your cargo tree -p mortie-core --edges normal --prefix none | tail -n +2 | wc -l0 check is a good guard and I would happily wire it, but adding a workflow step is a .github/workflows/ edit, which this PR is not authorized to make beyond mechanical path fixes. Meanwhile the in-crate test covers the gap on every cargo test — and it is genuinely reachable now, because the default-members fix on the root manifest (b20c327) puts mortie-core in the default selection, so a bare cargo test runs it. Your separate observation that no workflow runs cargo test/check/clippy at all still stands as its own gap; that is a broader CI question than this PR, and stays standing for espg.

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.67%. Comparing base (e15b0d8) to head (9e8ad86).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #207      +/-   ##
==========================================
+ Coverage   96.54%   96.67%   +0.12%     
==========================================
  Files          19       20       +1     
  Lines        2171     2283     +112     
==========================================
+ Hits         2096     2207     +111     
- Misses         75       76       +1     
Flag Coverage Δ
unittests 96.67% <ø> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
mortie/arrow.py 99.36% <ø> (ø)
mortie/morton_index.py 93.44% <ø> (ø)
mortie/orders.py 97.67% <ø> (ø)

... and 4 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e15b0d8...9e8ad86. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread docs/specification.md
NESTED cell (or an order-29 point) with its order carried intrinsically.
Source of truth in code: `src_rust/src/decimal_morton.rs` (`MAX_ORDER = 29`,
`BODY_TUPLES = 27`).
Source of truth in code: `mortie-core/src/decimal_morton.rs` (`MAX_ORDER = 29`,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Stale Rust-path references the move left behind (three Python docstrings + CLAUDE.md).

This hunk correctly repoints the spec at mortie-core/src/decimal_morton.rs, but the same two paths are quoted in module docstrings that this commit does not update:

  • mortie/morton_index.py:7The kernel lives in Rust (src_rust/src/decimal_morton.rs); this module is the
  • mortie/arrow.py:9(src_rust/src/decimal_morton.rs); this module only wraps them so the same
  • mortie/orders.py:14Rust tree's own decomposition -- this module is the Python side of src_rust/src/morton.rs.

These are the only three src_rust/ docstring references that phase 2 invalidated — I grepped the rest (mortie/convert.py, rank_xy.py, dissolve.py, geometry.py, buffer.py, tests/test_toc.py, docs/specification.md:570/640/684/689, docs/api/geometry.md) and every one of those still points at a file that stayed in src_rust/, so they are correct as-is.

They are user-visible, not just internal: each is a module __doc__ (so help(mortie.orders) prints it) and all three modules are rendered by mkdocstrings (docs/api/morton_index.md, docs/api/arrow.md, docs/api/orders.md). docs/api/orders.md:5 also says the Python surface “mirrors the Rust tree (morton.rs)”, which is now a bare filename in a different crate.

Secondary, same cause: CLAUDE.md:83 still asserts “Compiled / performance code is Rust (src_rust/src/*.rs)”, and CLAUDE.md:79 describes the Rust as living “under src_rust/”. After this commit the codec — the part docs/specification.md calls the source of truth — is under mortie-core/src/, so the contributor guidance now points a reader at the wrong tree.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Accepted for the three Python docstrings, fixed in 892f970. Each now names the moved file plus the re-export, matching the phrasing the spec page took in c7004c1 (mortie-core/src/decimal_morton.rs … re-exported as mortie_rustie::decimal_morton):

  • mortie/morton_index.py:7mortie-core/src/decimal_morton.rs, re-exported as mortie_rustie::decimal_morton
  • mortie/arrow.py:9 → same pair
  • mortie/orders.py:14mortie-core/src/morton.rs (re-exported as mortie_rustie::morton)

Re-verified with a tree-wide grep -rn "src_rust": zero remaining references to either moved file. The other 30-odd hits all point at files that stayed in src_rust/ (dissolve.rs, sphere.rs, wkb.rs, authalic.rs, toc.rs, rank_xy.rs, cell_geom.rs, buffer.rs, plus Cargo.toml's src_rust/benches/*_bench.rs paths, which are correct — the benches did not move).

CLAUDE.md:79/:83 is left standing for espg. It is the repo instruction file, out of bounds for me to edit; :83 is the line that says compiled code lives in src_rust/src/*.rs, which is now true of everything except the two moved codec files.

Gates after the fix: python -m pytest -q 1655 passed / 16 skipped; numpydoc lint mortie/*.py clean (the hard gate the lint workflow runs); ruff check clean on the three touched files. The pre-existing ruff check / ruff format --check findings elsewhere in mortie/ are untouched by this change.

Comment thread mortie-core/src/lib.rs Outdated
//! grammar and its encode/decode, order/truncation/containment arithmetic,
//! the decimal-string grammar, and the `(depth, nested-ipix) ↔ packed-word`
//! pivot primitives. It is contractually dependency-minimal — no non-std
//! dependencies (enforced by `tests/dep_contract.rs`) — and it must never

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

“enforced by tests/dep_contract.rs” overstates the enforcement: nothing in CI runs cargo test.

The guard itself is sound — I read it and it is not circumventable by anything in this diff (it line-scans mortie-core/Cargo.toml for any table path segment in {dependencies, dev-dependencies, build-dependencies}, catches [dependencies.moc] and [target.'cfg(unix)'.dependencies], and the_scan_recognizes_the_forms_a_dependency_can_take guards the scanner). cargo tree -p mortie-core prints a single node, so the contract holds today.

The problem is the word enforced, which this commit newly publishes in the crate docs. grep -rn "cargo test\|cargo clippy\|cargo fmt" .github/ returns nothing:

  • test.yml — flake8 + pytest only
  • lint.yml — ruff + numpydoc, Python only
  • build-wheels.yml — maturin build/sdist
  • codspeed.ymlcargo codspeed build / run (benches, not #[test]s)

So a future PR that adds [dependencies] to mortie-core/Cargo.toml breaks nothing any CI job observes; the test only trips for someone who happens to run cargo test locally. That also means the whole 393-test cargo suite is unguarded, which is pre-existing and out of scope here — but the #48 contract is the one thing this PR promises is mechanical, and a doc line asserting it is the weakest link in the chain.

Two ways out, either fine: add a small cargo test job (or at minimum cargo test -p mortie-core) to test.yml, or soften this line to say the contract is checked by tests/dep_contract.rs and note it runs locally. If a CI job is out of scope for this PR, it is worth a Questions for review entry rather than leaving the stronger claim standing.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Accepted, softened in dd8e075. The crate doc no longer says "enforced"; it now states exactly what is true:

//! dependencies — and it must never grow a moc-crate dependency; set-ops/
//! RangeMOC layers belong elsewhere. `tests/dep_contract.rs` guards the
//! zero-dependency half by failing `cargo test` if this crate's manifest
//! declares any dependency table; no CI job runs `cargo test` today, so the
//! guard fires for whoever runs the suite rather than on every push.

Confirmed the gap the finding describes: grep -rn cargo .github/workflows/ returns only codspeed.yml (cargo install cargo-codspeed, cargo codspeed build, cargo codspeed run) — benchmark compilation, not the test suite. No workflow runs cargo test, cargo fmt, or cargo clippy.

I took the wording option rather than the CI-job option deliberately. Adding a cargo test job means editing .github/workflows/, and the project rule is not to modify CI config unless the issue names it — issue #200 is the workspace split and does not. So that half is out of scope for this PR rather than declined on the merits: the guard genuinely should run in CI, and it is a pre-existing gap this PR did not create (no Rust test job existed before the split either, it just newly matters now that a contract depends on one).

Raising the CI question for espg in the PR body so it is not lost — the natural follow-up is a Rust-checks workflow covering cargo test / cargo fmt --check / cargo clippy across the workspace, filed as its own issue.

Gates run locally on the current head: cargo fmt --check clean; workspace cargo test 393 passed (327 mortie-rustie + 66 mortie-core) plus the 2 dep_contract tests, 0 failed; cargo test -p mortie-core 66 + 2.

Comment thread mortie-core/tests/pivot_surface.rs Outdated
// (orders 1..=27) and the 28/29 tail: all-zero, all-three, an
// alternating pattern, and a pseudo-random one.
let span = 1u64 << (2 * depth); // cells below `base` at this depth
for within in [0, span - 1, 0b01_10_11_00_01 % span, 0x2f5b_a731 % span] {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

The sweep does not deliver the coverage the comment above it claims, and the gap lands exactly on the 28/29 tail it says it targets.

Enumerating the four within values for every depth:

  • depths 0–3 collapse: [0, span-1, 433 % span, 0x2f5ba731 % span] yields 1 distinct value at depth 0 and 3 at depths 1–3 — the two patterned constants share their low 6 bits (433 & 63 == 0x31 == 49), so at depths 1–3 they are literally the same path.
  • the patterned constants are small: 0b01_10_11_00_01 is 9 bits and 0x2f5b_a731 is 30 bits, so from depth 15 up they stop wrapping and their high tuples are all zero. At depth 29 that is orders 1..=24 and 1..=14 respectively, and the other two paths (all-zero / all-three) are self-symmetric — so no path ever puts a mixed pattern in the high body at deep orders.
  • the tail never sees stored value 2: over the whole sweep, depth-29 (t28, t29) takes only {(0,0), (0,1), (3,3)} of 16, and depth-28 t28 only {0,1,3}. The point test at line 84 adds (3,0). So 2 — the value that separates the t28 * 5 area block from the t28 * 4 point block in build_suffix / build_point_suffix / decode_tail — is never encoded at order 28 or 29 anywhere in this file.

Mutation-confirmed. I injected a bug that mis-encodes a stored tail tuple of 2 (let t28 = if t28 & 3 == 2 { 1 } else { t28 }; at the top of build_suffix and build_point_suffix, same for t29) and reverted it afterwards:

cargo test -p mortie-core --test pivot_surface  ->  4 passed; 0 failed
cargo test -p mortie-core --lib                 ->  10 FAILED

The inline suites catch it ten times over (order_28_suffix_form, order_29_suffix_form, nested_round_trip_all_orders, point_round_trip_and_flag, zorder_across_27_28_29_seam, both morton::tests round trips, …); this sweep does not notice. A genuinely tail-swapping bug is caught — swapping the 28/29 arms of from_nested's (t28, t29) match failed 3 of the 4 tests here — so the file is not inert, it is just narrower than advertised.

Worth noting the redundancy angle too: decimal_morton.rs's inline nested_round_trip_all_orders already runs the same all-base × all-depth round trip, and its sample_tuples cycles 0,1,2,3 across positions, so it strictly dominates this sweep on coverage. As written, the_crate_root_pivot_round_trips_every_depth_and_base_cell is 1,440 iterations of weaker duplicate behavior work in a file whose own header says behavior is pinned by the inline suites while this file pins reachability.

Two ways out, either fine: (a) derive the paths from the tuples instead of truncating constants — build within from a per-order cycle like the inline sample_tuples (tuple[i] = (seed + i) % 4) plus a couple of fixed extremes, which restores all four values at every order including 28/29; or (b) keep the sweep cheap and make the comment honest about what it does (all-zero, all-ones, and two low-bit patterns), leaning on the inline suites for value coverage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Folded in 21b5ecf. The sweep is rebuilt around what the pivot actually needs, and the comments now describe exactly that.

  • The four ad-hoc within constants are gone. The body is now one cycling path per (depth, base): order n stores (n + base) % 4, so a single path walks 0,1,2,3 across consecutive orders and, at any fixed order, the twelve base cells between them place all four stored values. That is 360 entries instead of 1,440 — the inline suites own exhaustive per-order behavior, so the external view keeps one representative path.
  • The 28/29 tail is enumerated rather than sampled, because the cycling paths only ever reach the diagonal (t28, t28+1): all sixteen (t28, t29) pairs at depth 29, plus all four t28 values at depth 28 (an order-28 word stores no order-29 tuple). a_max_encoded_point_pivots_to_the_same_nested_cell_as_its_area_twin likewise walks all sixteen pairs on every base cell, since build_point_suffix keys off the pair directly rather than the area tail's order code.

Mutation-sensitivity, measured here with your mutation (mapping stored tail value 2 to 1 in both build_suffix and build_point_suffix):

  • before (fc81d99's file): cargo test -p mortie-core --test pivot_surface4 passed, 0 failed (the gap you found)
  • after (21b5ecf): 1 passed, 3 failedthe_crate_root_pivot_round_trips_every_depth_and_base_cell, the_morton_bridge_is_the_same_pivot_in_tuple_order, and a_max_encoded_point_pivots_to_the_same_nested_cell_as_its_area_twin, e.g. left: (200065790505305797, 28) vs right: (…798, 28)

The mutation was reverted with git checkout -- mortie-core/src/decimal_morton.rs before committing; git status shows only mortie-core/tests/pivot_surface.rs in each of the two fold commits. Gates: cargo test -p mortie-core (66 + 2 + 4), workspace cargo test (327 mortie_rustie + 66 + 2 + 4), cargo fmt --all --check, cargo clippy -p mortie-core --all-targets clean, and cargo tree -p mortie-core still a single zero-dep node.

Comment thread mortie-core/tests/pivot_surface.rs Outdated
Comment on lines +120 to +126
// coarsen: truncation in packed space equals truncation in nested space.
let (order, nested) = dm::to_nested(word).expect("to_nested");
let coarse = dm::coarsen(word, 2).expect("coarsen a valid word");
assert_eq!(
dm::to_nested(coarse),
Some((2, nested >> (2 * (order - 2) as u32)))
);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

This coarsen expectation is computed by reading both sides back through to_nested, which discards exactly the property coarsen exists to maintain. to_nested only reads the top order tuples of the body, so any junk coarsen fails to zero-fill below the target order is invisible here — and canonicality (bit-equality of two encodings of the same cell) is the documented contract of the packed word.

Mutation-confirmed, then reverted: coarsen's let kept_body_orders = k.min(BODY_TUPLES);(k + 1).min(BODY_TUPLES) keeps one tuple too many, so the result is no longer canonical.

cargo test -p mortie-core --test pivot_surface  ->  4 passed; 0 failed
cargo test -p mortie-core --lib                 ->  3 FAILED  (coarsen_zero_fills_below_target,
                                                               coarsen_matches_reencode,
                                                               coarsen_point_below_28_becomes_area)

The packed-word form is one line shorter, needs no order/nested locals, and does catch it:

assert_eq!(dm::coarsen(word, 2), Some(dm::encode(4, &tuples, 2)));

I verified that holds on unmutated main of this branch. It also matches the style of the common_ancestor assertion six lines below, which already states its expectation as a packed word (Ok(dm::encode(4, &tuples, 4))) rather than routing through the decoder — so this is an internal inconsistency as much as a weak assertion.

Same note, smaller stakes, for the assertion's comment: "truncation in packed space equals truncation in nested space" is a claim about the two spaces agreeing, but with the expectation derived from to_nested(word) the assertion is closer to self-consistency than to an independent check.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Applied verbatim in 688cd86:

// coarsen: truncating to order 2 is bit-identical to encoding the first two
// tuples at order 2. Stated as a packed word rather than read back through
// `to_nested`, which only inspects the top `order` tuples and so cannot see
// non-canonical leftover bits below the target order.
assert_eq!(dm::coarsen(word, 2), Some(dm::encode(4, &tuples, 2)));

The now-unused let (order, nested) = dm::to_nested(word) binding went with it, and the assertion reads the same way as the common_ancestor one below it.

Mutation-sensitivity, measured with your mutation (kept_body_orders = (k + 1).min(BODY_TUPLES) in coarsen):

  • before (fc81d99's file): cargo test -p mortie-core --test pivot_surface4 passed, 0 failed
  • after (688cd86): 3 passed, 1 failedthe_documented_codec_surface_is_reachable_from_outside

Source restored with git checkout -- mortie-core/src/decimal_morton.rs before the commit; only mortie-core/tests/pivot_surface.rs is staged.

Comment thread BUILDING.md Outdated
cargo test -p mortie-core
```

On macOS, `cargo test` on the root package needs the extension-module symbols

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Scoping check on this paragraph. The failure itself is real and the command is right — I reproduced both here on arm64 macOS: plain cargo test dies with ld: symbol(s) not found for architecture arm64 (__Py_IncRef, __Py_NoneStruct, __Py_TrueStruct, ...), and RUSTFLAGS="-C link-arg=-undefined -C link-arg=dynamic_lookup" cargo test then goes fully green across both members (root lib + descent_stats-gated harness skipped, mortie-core's dep_contract 2/2 and pivot_surface 4/4). cargo test -p mortie-core with no flag also passes on its own, so line 104 checks out.

What I would push on is the On macOS scoping against the reason given immediately after it. pyo3's extension-module feature deliberately skips linking libpython is platform-neutral — it is the same condition pyo3's FAQ describes as breaking cargo test on Linux (undefined reference to PyBaseObject_Type), where -undefined dynamic_lookup is a Mach-O-only linker flag and does nothing. And cargo test is never run in CI (no workflow under .github/workflows/ invokes it — consistent with the note in mortie-core/src/lib.rs), so the only consumer of this instruction is a human at a checkout. A Linux one copies the unqualified block at lines 89-90 (# Both workspace members / cargo test), gets a wall of linker errors, and then finds the remedy scoped to a platform they are not on.

Either fix is cheap: (a) drop On macOS, state the cause platform-neutrally, and give both remedies (macOS -undefined dynamic_lookup; Linux -C link-arg=-Wl,--unresolved-symbols=ignore-all, or link libpython), or (b) keep it macOS-scoped but say in one clause what Linux/Windows need — if they genuinely need nothing, that is worth asserting rather than leaving inferred. Either way, tagging the lines 89-90 block itself ("see the linker note below") would stop the failing command being the first thing copy-pasted.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in bdc399e. The note now states the cause platform-neutrally and labels the remedy as the macOS one:

cargo test on the root package needs the extension-module symbols resolved lazily on every platform, since pyo3's extension-module feature deliberately skips linking libpython. The invocation below is the macOS remedy only: -undefined dynamic_lookup is a Mach-O linker option, so it does nothing on Linux or Windows — those need their own equivalent, which is not documented here because it has not been verified against this workspace.

The block itself is now commented # macOS, and the closing line is widened to cargo test -p mortie-core needs no flag on any platform — the codec crate links nothing. No substitute Linux/Windows command was added, since none could be verified on this machine.

Comment thread README.md
The Rust side is a cargo workspace with two members. `mortie-core` holds the
packed-word codec — the bit layout, encode/decode, the order arithmetic, the
decimal-string grammar, and the `(depth, nested-ipix) ↔ packed-word` pivot — and
carries **no dependencies at all**, so a Rust project can take the codec without

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

The zero-dependency claim is accurate — mortie-core/Cargo.toml has no dependency table of any kind, Cargo.lock lists mortie-core 0.1.0 with no deps under it, and cargo test -p mortie-core builds and passes with nothing else compiled. No objection there.

The claim I would tighten is the consequence: so a Rust project can take the codec (and BUILDING.md line 39, so a Rust project can depend on the codec alone). That is the stated point of the split — issue #200's consumer is an external crate — but neither page says how, and today there is no route a reader can act on. cargo search mortie returns neither mortie nor mortie-core on crates.io, and the only declaration anywhere in the tree is the root manifest's mortie-core = { path = "mortie-core" } — a path dep with no version key, i.e. local-only. Someone who reads this sentence and runs cargo add mortie-core gets nothing.

One line under the claim closes it, e.g.

mortie-core = { git = "https://github.com/espg/mortie" }  # not yet published to crates.io

or, if a crates.io publish is planned as part of #200, just saying "not yet published; use a git dependency" tells the reader the git form is the interim rather than the intent. Docs-only — it just stops the sentence promising a dependency edge that cannot be written yet.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in 9fdc87c. Both sites — README.md after the Rust-crates paragraph and BUILDING.md after the no-dependencies contract paragraph — now carry the same clause:

Neither crate is published to crates.io yet, so cargo add mortie-core finds nothing today; the crate is only available from this repository. Publishing mortie-core is tracked separately in issue #201.

No git-dependency snippet was offered, since resolution was not verified here. No manifest was touched: no crates.io metadata, no version key, no publish wiring — that stays with issue #201.

Comment thread README.md
inheriting pyo3, numpy, rayon or a HEALPix crate. The root package `mortie`
(library `mortie_rustie`) is the pyo3 extension this Python package compiles to;
it depends on `mortie-core` and re-exports it, so every Rust path is unchanged.
See [BUILDING.md](BUILDING.md#workspace-layout) for the layout.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

The anchor resolves — GitHub slugs ## Workspace Layout to workspace-layout, so BUILDING.md#workspace-layout is correct as written on github.com, and I confirmed neither README.md nor BUILDING.md sits in mkdocs.yml's nav (mkdocs only reads docs/), so a relative link here cannot break the mkdocs build --strict gate in docs.yml. Clean on both counts.

But that same fact is the gap: this section is invisible on the published site. docs/index.md carries a near-duplicate Installation block (pip install mortie, then "For development builds from source, see BUILDING.md") which phase 4 did not touch, so a Rust consumer landing on espg.github.io/mortie finds no mention of mortie-core anywhere except one incidental path in docs/specification.md section 1 (Source of truth in code: mortie-core/src/decimal_morton.rs). Two consequences: the site never advertises the split that #200 exists to enable, and the two Installation blocks — already near-copies — now differ, which is the shape drift takes.

One sentence in docs/index.md pointing at the same anchor (absolute URL, matching what that file already does for BUILDING.md) closes both without adding a third copy of the prose.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude

Fixed in 9e8ad86. docs/index.md gains one paragraph, directly under the existing Installation block:

The Rust side is a cargo workspace: the root package builds the pyo3 extension, and its mortie-core member is the dependency-free packed-word codec. See BUILDING.md for the layout and the current crates.io status.

Link mechanics follow the pattern already in that file — the Installation block links BUILDING.md by absolute https://github.com/espg/mortie/blob/main/... URL rather than a relative path, so nothing resolves outside docs/. Deferring the layout table and the crates.io status to BUILDING.md keeps the site from re-stating detail that could drift; the one duplicated fact is the workspace/mortie-core split itself.

No nav entry, no restructuring. uv run mkdocs build --strict passes locally (mkdocs installed via uv sync --group docs, mirroring docs.yml).

@espg

espg commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude (review)

Fresh-context adversarial review of 2bd50c7 ("phase 4 of issue #200") only — the docs phase. Three inline findings, all docs-accuracy/scoping, none blocking correctness:

  • BUILDING.md:96 — the pyo3 linker note is scoped On macOS but the cause it gives is platform-neutral (r3811761713)
  • README.md:79 — "a Rust project can take the codec" with no route stated; not on crates.io, path dep only (r3811763019)
  • README.md:83 — the anchor is correct, but the published mkdocs site never sees this section, and docs/index.md's duplicate Installation block was not updated (r3811764340)

Gates run (all green, worktree left clean):

gate result
cargo doc -p mortie-core --no-deps (fresh target dir, not cached) zero warnings — the from_legacy_decimal reword does clear the private intra-doc link warning, and nothing else was introduced or masked
RUSTFLAGS="-W missing_docs" cargo build -p mortie-core zero warnings — every pub item in mortie-core carries rustdoc
cargo test -p mortie-core (no RUSTFLAGS) pass — dep_contract 2/2, pivot_surface 4/4, plus the in-crate unit tests
cargo test (no RUSTFLAGS) fails to link on macOS, as the new note says — ld: symbol(s) not found for architecture arm64
RUSTFLAGS="-C link-arg=-undefined -C link-arg=dynamic_lookup" cargo test pass, whole workspace
cargo fmt --all --check clean
maturin sdist sdist contains mortie-core/{Cargo.toml,src/*.rs,tests/*.rs} — the "pulls the path dependency into the sdist automatically" claim verified end to end
mkdocs build --strict not runnable (mkdocs absent from .venv); cleared by inspection instead — mkdocs.yml nav contains neither README.md nor BUILDING.md, and mkdocs only reads docs/, so these edits cannot reach the Docs workflow

Verified and deliberately cleared:

  • The BUILDING.md table is accurate on both rows. Every #[pyfunction] is in the root crate (grep pyo3 mortie-core/ finds only a prose mention in a comment); the rayon batch kernels are src_rust/src/{decimal_morton,coverage,moc}/batch.rs; the Arrow FFI is src_rust/src/arrow_ffi.rs; benches are the three [[bench]] targets under src_rust/benches/. The mortie-core row's backticked encode/decode are real public fns (decimal_morton.rs:175/:294), and "order/truncation/containment arithmetic" maps to order_of / coarsen / common_ancestor.
  • "mortie_rustie::decimal_morton and mortie_rustie::morton resolve exactly as they did before the split" and README's "every Rust path is unchanged" — checked against the actual move (git diff c7004c1~1:src_rust/src/decimal_morton.rs c7004c1:mortie-core/src/decimal_morton.rs): the only thing that changed module is a test (from_nested_agrees_with_healpix_crategeo2mort's tests). No public item moved, so the glob re-export plus the retained decimal_morton::batch reproduce the pre-split path set exactly. "Nothing about the Python surface changes" holds — the phase touched no #[pyfunction] and no mortie/ Python file.
  • The dependency-contract sentence is correctly scoped: it claims the test catches a manifest declaration, which is exactly what dep_contract.rs line-scans, and it does not overclaim enforcement of the "not pyo3, not numpy, not rayon, not a HEALPix crate" list (that half is checked by the lockfile, which agrees).
  • The from_legacy_decimal reword is still accurate about the legacy grammar and the sign/hemisphere behavior: legacy_to_nested really is private, really does fold the sign into the base cell (first_digit - 1 north / first_digit + 5 south), and the surviving # Panics note still matches the assert!(abs_val != 0, ...).
  • Markdown table renders (no stray pipes inside cells), the new prose wraps at ~78 like its neighbours, and I found no typos in the added text.

@espg

espg commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

All four phases are landed and every adversarial-review finding is folded. The PR body checklist and "How it was tested" are updated to head (9e8ad86).

Phasesfac6720 (scaffolding), c7004c1 (codec extraction), fc81d99 (standalone testability), 2bd50c7 (docs); ten fold commits between them, one per finding, every inline thread answered.

Local gate results at 9e8ad86:

gate result
cargo test (workspace) 399 passed, 1 ignored — 327 mortie_rustie + 66 mortie-core lib + 2 dep_contract + 4 pivot_surface
cargo test -p mortie-core (standalone, no flags) 72 passed
cargo tree -p mortie-core one line, zero dependencies
cargo bench --no-run all three benches compile
cargo fmt --all --check, cargo clippy -p mortie-core --all-targets clean
cargo doc -p mortie-core --no-deps zero warnings
mkdocs build --strict clean
maturin develop --release + pytest 1655 passed, 16 skipped — identical to the pre-split baseline
maturin sdist contains mortie-core/{Cargo.toml,src,tests}; a wheel builds from the extracted sdist

CI is stalled on runner availability, not on this PR. The last workflow job to start anywhere in this repo was created at 08:39 UTC; everything created from ~09:15 UTC onward is queued with zero jobs picked up, on claude/200-workspace-split and claude/187-polymorphic-api alike. So all 15 checks on head 9e8ad86test (3.10), test (3.11), test (3.12), arro3-no-pyarrow, ruff, numpydoc validation, Build (verify only), the five Build wheels … jobs, Build source distribution, Python benchmarks, Rust benchmarks — read QUEUED after ~25 minutes of polling, none of them red. What did run on this branch before the backlog: Tests ✅ and Docs ✅ on fac6720 (phase 1), Docs ✅ on bd462d3.

Left as a draft rather than flipped to ready-for-review, because the ready flip is conditioned on green CI and CI has not run on head. It should go green on its own once runners free up — no push is needed, and I've left the PR ready to flip.

Two things standing for you, both in the PR body's "Questions for review":

  1. Crate location and crate-root re-exports (items 1 and 3) — both one-commit changes if you'd rather they went the other way. The version seed (item 2) is resolved by the publish mortie-core to crates.io: naming, semver posture, release mechanics #201 ruling and marked as such.
  2. No CI job runs cargo test (item 4). mortie-core's dependency contract is enforced by a test (mortie-core/tests/dep_contract.rs), and grep -rn cargo .github/workflows/ finds only cargo install cargo-codspeed and cargo codspeed build/run — there has never been a Rust test job here. That predates the split, but the split is what makes it load-bearing, since the zero-dependency contract is what healpix-geo (healpix-geo dep PR: expose the mortie-core codec through their core + wasm bindings (staged, espg posts) #203) relies on. Not acted on: workflow changes are out of scope for an issue that doesn't name them. Say the word and it's a small follow-up issue.

One more item that stayed standing from the phase-2 review: CLAUDE.md:79 and :83 still say the compiled Rust lives under src_rust/src/*.rs, which is now only two-thirds true. Left untouched — that file is yours.

@espg espg added the waiting label Aug 19, 2026
@espg espg removed the waiting label Aug 19, 2026
@espg

espg commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Label correction: removed waiting (leaving implement with no state label). Per CLAUDE.md §5, waiting means the ball is in @espg's court, which would make the next routine run skip this PR — but the only outstanding step here is the gh pr ready flip once GitHub's runner backlog clears and the queued checks settle green. With no state label the next run treats it as actionable, re-checks CI, and performs the flip. (Same posture as PR #206, for consistency.)

@codspeed-hq

codspeed-hq Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
❌ 1 regressed benchmark
✅ 68 untouched benchmarks
⏩ 1 skipped benchmark1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
coverage_square[4] 152.4 µs 170.3 µs -10.47%
fast_norm2mort_orders[6] 298.6 ns 269.4 ns +10.82%
fast_norm2mort_orders[10] 315.3 ns 286.1 ns +10.19%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/200-workspace-split (9e8ad86) with main (2612e86)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@espg

espg commented Aug 19, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

The one red check is CodSpeed's analysis gate. I investigated the mechanism rather than dismissing it — the crate split does not regress performance, and here is the evidence.

The concern worth taking seriously: this PR moves norm2mort and the decimal codec into a separate crate, and the benchmarks flagged (fast_norm2mort_*, coverage_square) are exactly where a new crate boundary could block inlining. That is a real mechanism, not a hypothetical.

Three findings close it:

  1. Cross-crate inlining is preserved by the build config. [profile.release] sets lto = true with codegen-units = 1 (fat LTO optimizes across crate boundaries), and [profile.bench] is not defined, so it inherits those settings. The mechanism is neutralized by design.
  2. The benchmark that would show the regression got faster. norm2mort now lives in mortie-core/src/morton.rs, so fast_norm2mort_orders[*] is precisely the cross-boundary call — and CodSpeed reports it +10.82% / +10.19% improved on this PR. A boundary that blocked inlining would make it slower, not faster.
  3. The same benchmarks moved the opposite direction on a PR that changed no code at all. PR Spec: normative toc word grammar, frozen for 1.x (issue #193) #206 is docs-only (CHANGELOG.md, two docs pages, one test file — zero source, zero Rust) and CodSpeed reported fast_norm2mort_orders[6] at −17.8% there, against +10.82% here. The same benchmark cannot both regress 17% from a documentation edit and improve 10% from a crate move; the spread is measurement variance. CodSpeed says so itself on this PR: "Different runtime environments detected."

The lone regression here, coverage_square[4] (−10.47%), is in coverage descent — and src_rust/src/coverage.rs contains no reference to mortie_core; the only two references in the whole of src_rust/src/ are re-export lines in lib.rs. That path does not cross the new boundary.

Correctness is unaffected regardless: cargo test 399 passed, cargo test -p mortie-core 72 passed standalone, and pytest 1655 passed / 16 skipped — identical to the pre-split baseline. Everything else on head 9e8ad86 is green (17 passed, 6 skipping).

Left to @espg (hence waiting), same as #206: acknowledge the run on CodSpeed, or merge past it on the evidence above. No push is needed; the PR is otherwise complete and ready to flip.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

workspace split: extract mortie-core (pure-Rust codec, no pyo3) from mortie_rustie

1 participant