Contract-failure vocabulary — v1
This document specifies the closed set of canonical refusals the neuroim public API raises when a spatial, indexing, or provenance contract is violated. It is a public contract: tests, downstream tools, and third-party backends may match on the message substrings and exception classes defined here.
The spec is intentionally narrow. It covers the contract failures the library guarantees — the ones a wrong-but-plausible silent return would otherwise mask. Ordinary bugs (KeyError, NumPy ValueError from a bad reshape, etc.) are out of scope.
Layering model
Spatial-mismatch refusals are produced by a three-level stack. Each level prepends a prefix and re-raises; the leaf message is always present in the final string, which is the property that makes it greppable across every operation.
NeuroSpace.compatible_with leaf: "NeuroSpace mismatch in {spatial dim|affine}: left=…, right=…"
│ (raises ValueError)
▼
verify.assert_same_space wrap: "assert_same_space: spatial contract mismatch — {leaf}"
│ (re-raises ValueError)
▼
operations.concat wrap: "spatial contract mismatch: NeuroVecs must have same
spatial dimensions and affine; {assert_same_space msg}"
A conforming reader that wants “did a spatial contract fail?” matches the substring spatial contract mismatch or the leaf NeuroSpace mismatch in. A reader that wants “which operation raised?” matches the outermost prefix.
The vocabulary
All strings below are verbatim in the current source. {…} marks a runtime-interpolated field; everything outside {…} is literal.
Spatial-frame refusals
| ID | Exception | Message | Raised by |
|---|---|---|---|
SPACE-DIM |
ValueError |
NeuroSpace mismatch in spatial dim: left={tuple}, right={tuple} |
NeuroSpace.compatible_with |
SPACE-AFFINE |
ValueError |
NeuroSpace mismatch in affine: left={list}, right={list} |
NeuroSpace.compatible_with |
SPACE-ASSERT |
ValueError |
assert_same_space: spatial contract mismatch — {SPACE-DIM\|SPACE-AFFINE} |
verify.assert_same_space (structured-space path) |
SPACE-HASH |
ValueError |
assert_same_space: input_space_hash mismatch + a.input_space_hash = {repr} + b.input_space_hash = {repr} |
verify.assert_same_space (receipt-only path) |
MASK-HASH |
ValueError |
assert_same_mask: mask_hash mismatch + a.mask_hash = {repr} + b.mask_hash = {repr} |
verify.assert_same_mask |
CONCAT-SPACE |
ValueError |
spatial contract mismatch: NeuroVecs must have same spatial dimensions and affine; {SPACE-ASSERT} |
operations.concat |
SPACE-ASSERT is the message reached by series_roi, searchlight_apply, temporal_snr(mask=…), and every other operation that takes a spatial mask=/roi argument: they all route through verify.assert_same_space on the structured-space path.
Off-grid refusals
| ID | Exception | Message | Raised by |
|---|---|---|---|
IDX-OOB |
IndexError |
{coordinate\|index} out of bounds for spatial shape {tuple}: {preview}{… (+N more)} |
series_at, series_at_coords, series_at_indices |
WORLD-OOB |
ValueError |
world coord {tuple} mm maps to voxel {tuple} which is outside the image grid of shape {tuple}. |
series_at_world, series_roi_world |
IDX-OOB and WORLD-OOB are deliberately different exception types. The voxel/index path is reporting an indexing error (IndexError); the world-coordinate path has already performed the affine inversion and is reporting a domain error (ValueError). The out_of_bounds="zero" opt-in suppresses both in favour of zero-fill; the default is "raise".
Backend refusals
| ID | Exception | Message | Raised by |
|---|---|---|---|
FB-DRIFT |
ValueError |
Volume {idx} has inconsistent affine/space: {underlying} |
FileBackedNeuroVec (per-volume load) |
Immutability refusals
| ID | Exception | Message | Raised by |
|---|---|---|---|
FROZEN-SPACE |
AttributeError |
NeuroSpace is immutable |
NeuroSpace.__setattr__ after construction |
Construction refusals
| ID | Exception | Message | Raised by |
|---|---|---|---|
DIM-POS |
ValueError |
All dimensions must be positive |
NeuroSpace.__init__ |
SPACING-POS |
ValueError |
Spacing values must be positive |
NeuroSpace.__init__ |
TRANS-3D |
ValueError |
trans must be 4x4 matrix for {ndim}D space, got {shape} |
NeuroSpace.__init__ (ndim ≤ 3) |
TRANS-ND |
ValueError |
trans must be {n}x{n} matrix for {ndim}D space, got {shape} |
NeuroSpace.__init__ (ndim > 3) |
TRANS-SINGULAR |
ValueError |
Transformation matrix must be invertible |
NeuroSpace.__init__ |
OOB-MODE |
ValueError |
out_of_bounds must be 'raise' or 'zero' |
NeuroVec.series_at* argument validation |
Conformance
A third-party VoxelSeriesStore backend (see the voxel-series-store protocol) is conforming only if, on the canonical bad input for each applicable row, it raises the listed exception class and its message contains the listed literal substring (with {…} fields free to vary).
The conformance test for this spec is tests/test_contract_failure_vocabulary.py (added with this spec): for every operation in the tables above it constructs the canonical bad input and asserts pytest.raises(<class>, match=<literal>). Any change that alters a message’s literal portion is a spec-breaking change and bumps this document to v2.
Why this spec exists
“It raises on a bad mask” is only a guarantee if which error, with what message, is pinned. Without that, a refactor can silently downgrade a ValueError to a False return or change the message so that downstream match= assertions and log scrapers stop firing — and the mission claim (“silent space/orientation/mask mismatches are caught at the contract layer”) quietly stops being testable. This vocabulary is what makes the refusal a contract instead of a habit.
See the Contract failures concept page for the narrative version, and examples/scenarios/ for the runnable scenarios each refusal was filed against.