rust-error-handling
Use when designing, implementing, or reviewing OMMX Rust SDK fallible APIs, public error or signal types, anyhow error chains, `ommx::Result`, PyO3 `OmmxPyResult` conversions, Python exception mappings, or binding-local `map_err` and `?` propagation.
- License 许可
- Apache-2.0
- 所属仓库
- Jij-Inc/ommx
- 最近更新
- 2026-11-12
- Stars
- ★ 55
- Forks
- 3
01 前言 Front Matter
- name
- rust-error-handling
- description
- Use when designing, implementing, or reviewing OMMX Rust SDK fallible APIs, public error or signal types, anyhow error chains, `ommx::Result`, PyO3 `OmmxPyResult` conversions, Python exception mappings, or binding-local `map_err` and `?` propagation.
02 资源文件 1
- 参考文档agents/openai.yaml258 B
03 SKILL.md 正文
Rust Error Handling
Use .agents/skills/domain-responsibility-review/SKILL.md first to establish
the domain owner. Read rust/ommx/doc/tutorial/error_handling.md before changing
or reviewing a public Rust error surface. Treat every public signal and Python
exception classification as an SDK contract.
Mandatory Decision Order
- Name the failing domain operation and its owner.
- State the caller's concrete recovery action: what will the caller inspect, change, retry, skip, or select differently?
- Verify that recovery is an intentional, stable contract of that public operation, not merely a hypothetical reaction available to one internal or application-specific consumer.
- Choose the narrowest error surface that supports that contract.
If no stable, intentional recovery contract can be named, do not add a public signal merely to improve diagnostics or select a Python exception class. If an operation is already documented as producing an existing signal for that condition, treat an untyped path as a contract bug rather than inventing a new parallel signal.
| Condition | Error surface |
|---|---|
| No stable caller recovery path | ommx::Result<T> with an ordinary ommx::Error |
| One recoverable condition among broader failures | Public OMMX-owned signal preserved in the ommx::Error chain |
| A narrow API has one signal failure that callers commonly match directly | A typed Result<T, Signal> may be justified |
| Python owns the failure semantics | Explicit PyErr classification at the binding site |
Rust SDK Policy
- Keep
ommx::Result<T>as the default public fallible return type. Do not replace it with a closed SDK-wide error enum. - Curate public signal types for stable programmatic recovery. Preserve them in
the
anyhow::Errorchain so callers can useisordowncast_ref. - Keep each signal with the narrowest domain owner that has enough information to define it. Reuse an existing owner signal before introducing another type.
- Do not create an operation-wide umbrella type that combines failures with different owners or recovery actions. A domain-owned enum remains appropriate when its variants form one stable matching contract.
- Do not expose
prost,serde_json, OCI, registry-client, or other dependency errors as the OMMX consumer contract. Normalize them at the OMMX boundary only when callers need a stable OMMX-owned signal. - Preserve sources and diagnostic context while their recovery semantics remain valid at the outer owner boundary. Do not replace such a signal with a rendered string or duplicate its message in a wrapper.
- Review nested signals independently from their outer owner. If the inner signal still truthfully describes the cause and the outer owner has its own stable recovery contract, preserve the inner source beneath that outer OMMX-owned signal and map the outer signal first. Rust callers may then intentionally downcast to either layer while Python follows outer ownership.
- Do not leave an inner signal discoverable when its recovery contract becomes
false or misleading after an ownership transition, such as caller-input
validation reused to validate internally derived state. Convert that cause
to non-signal diagnostic context, then emit an outer OMMX-owned signal if one
is justified or an ordinary
ommx::Errorotherwise. Retain the relevant cause and values as structured tracing fields and regression-test the outer mapping or fallback semantics. - Expose the payload a caller needs for recovery through public fields or
accessors. Document its meaning and use
#[non_exhaustive]when callers should inspect but not exhaustively construct the signal. - Keep failures without a stable recovery path as ordinary
ommx::Errorvalues, with structured tracing at the fail site.
For every new public signal, also use
.agents/skills/rust-module-boundary/SKILL.md to justify its module owner,
visibility, payload, and public API commitment.
PyO3 Boundary Policy
Classify ownership before choosing a Python exception.
Rust SDK-owned failures
- Return
OmmxPyResult<T>from exposed bindings that can propagate Rust-owned failures. - Declare each OMMX-owned signal mapping once in the shared mapping table in
python/ommx/src/error.rs. Generate both the directFrom<Signal>conversion and fallback dispatch for a signal already erased intoommx::Error. - Use normal
?propagation at binding call sites. Do not add per-callmap_errclosures for Rust SDK-owned failures. - Preserve outer domain ownership when an error chain contains nested signals; keep fallback classification order explicit where it affects the contract.
- Map unclassified Rust SDK failures to
RuntimeError. - Do not enable or depend on PyO3's blanket
anyhowconversion, add a generic conversion that absorbs arbitrary errors, or classify by rendered message.
Python-owned failures
Keep Python semantics explicit at the binding boundary. Do not force these through the Rust SDK signal mapper:
- Python argument extraction or protocol violations (
TypeError) - Python integer conversion overflow (
OverflowError) - Mapping-like lookup behavior owned by the Python API (
KeyError) - Python object or JSON input validation (
ValueError) - Exceptions from imports, callbacks, adapters, pandas, codecs, or other Python
libraries; preserve the original
PyErr
Malformed payloads received by the private cross-extension bridge are
binding-internal Rust failures and fall back to RuntimeError; they are not
Python call-signature TypeError cases unless an original PyErr exists.
The existence of a local map_err is not itself a defect. First determine
whether Rust SDK or Python owns the failure.
Review Workflow
- Inventory the changed failure paths.
- List every public Rust entry point and exposed PyO3 method affected.
- Search sibling paths for the same failure so equivalent operations do not acquire different contracts.
- Classify every failure.
- Name its owner and caller recovery action.
- Search for an existing signal before adding one.
- Separate caller input, persisted corruption, dependency failure, and internal invariant failure.
- Verify propagation.
- Confirm the signal survives direct typed conversion and prior erasure into
ommx::Error. - Confirm nested mapped signals do not override a more specific outer owner; declaration order is part of the Python contract.
- Confirm Python-owned
PyErrvalues pass through unchanged. - Confirm ordinary SDK failures reach the
RuntimeErrorfallback.
- Confirm the signal survives direct typed conversion and prior erasure into
- Verify mutation and payload behavior.
- Validate all inputs before mutation or prove rollback/atomicity.
- Test every recovery-relevant payload field, including value direction and the complete set of missing or unknown IDs.
- Verify user-facing consistency.
- Test equivalent failures across entry points against the same Python exception class.
- Update Rustdoc, Python docstrings, migration notes, release notes, stubs, and API metadata when the public contract changes.
Evidence and Commands
Require both enforcing code and a regression test for every claimed contract.
The shared mapping mechanism must have unit coverage for direct and erased
conversion, ordinary fallback, and unchanged PyErr pass-through. A macro does
not need the same mechanism test repeated for every entry, but every newly
mapped signal needs a focused Python integration test through its real public
operation. Add both direct and erased tests for a particular signal only when
it can reach the boundary through distinct conversion paths not already proved
by the shared mechanism. When mapped outer and inner signals can coexist in one
chain, test that declaration order selects the outer Python contract. When an
ownership transition intentionally removes an obsolete inner signal, test both
that Rust can no longer downcast to it and that Python selects the intended
outer or fallback exception.
Use focused commands that match the touched surface:
rg -n "OmmxPyResult|define_ommx_error_mappings|map_err|new_err|downcast_ref" \
rust/ommx/src python/ommx/src
cargo test -p ommx affected_module --lib
env PYO3_PYTHON="$PWD/.venv/bin/python3" cargo test -p _ommx_rust error::tests --lib
uv run --no-sync pytest -q python/ommx/tests/test_affected_behavior.py
cargo tree -p _ommx_rust -e features -i pyo3
Run task python:sync first when the project .venv is absent.
Run task python:stubgen after a public PyO3 signature or docstring change.
Run cargo fmt --all -- --check and git diff --check before handoff.
Checklist
- What will a caller do differently after identifying this failure, and is that recovery an intentional contract of this operation?
- Which domain owns the signal and its payload?
- Can an existing signal express the same recovery contract?
- Does the public API remain on
ommx::Resultunless a narrow typed result is justified? - Does the error chain preserve only signals whose recovery semantics remain valid at the outer owner, plus the useful source and diagnostic context?
- Are dependency types and rendered messages absent from the public contract?
- Do direct and erased Rust errors share one declarative Python mapping?
- Are Python-owned exceptions kept outside the SDK mapper?
- Are all recovery payload fields, fallback behavior, and mutation atomicity protected by tests?
04 同仓库更多 Skills
ommx
Placeholder for verifying OMMX skill distribution. OMMX usage guidance will be provided separately.
release-note
Append release note entries for the current PR to both English and Japanese release notes
sphinx-docs-review
Review OMMX documentation publication correctness: Sphinx/docutils build and rendering failures, unresolved references, generated API wiring, and consistency of PyO3 docstrings, exports, stubs, and API pages. Use for those technical concerns, not for Book prose structure, reader prerequisites, or learning-path design alone.
rust-module-boundary
Use when writing or reviewing Rust module structure, item visibility, public API exposure, or pub/pub(crate)/pub(super) choices in OMMX.
release-note
Append release note entries for the current PR to the relevant OMMX Python SDK and/or Rust SDK release notes
python-sdk-review
Use when reviewing OMMX Python SDK changes, PyO3 bindings under python/ommx/src, public Python API shape, pyo3-stub-gen output, generated stubs, Python-side wrappers, or tests for Python SDK behavior.