Descriptor golden regenerated; contract_stability::descriptor_matches_committed_golden passes.
Helper (nuncio_proto::errors) — status()/status_with_metadata() build a tonic::Status carrying a prost-encoded ErrorInfo in the grpc-status-details-bin trailer (via Status::with_details), with the canonical code from ErrorReason::code(). error_info()/error_reason() decode it client-side (from Status::details()).
Representative site conversions in `nunciod/src/grpc.rs**: account not-found (RemoveAccount, TestAccountConnection), message not-found (GetMessage, MarkRead), event not-found (GetEvent), contact not-found (GetContact), rule not-found (UpdateRule), NSQL validation (CreateRule/UpdateRule/PreviewRule + account validate), calendar not-configured (Calendar.Sync). Canonical codes are unchanged, so existing code()/message() assertions still hold. The exhaustive ~90-site sweep is left to WS-A7.
E2E testcrates/nunciod/tests/typed_errors_e2e_test.rs — boots the real daemon server and, over authenticated gRPC, decodes the reason + metadata from returned Status details for account-not-found, message-not-found, not-configured, and validation-failed.
cargo test-all — all pass, 0 ignored, 0 failed (incl. nuncio-proto 27 tests with contract_stability + errors::tests, and the new typed_errors_e2e_test)
Notes / follow-ups
ALREADY_EXISTS is defined in the taxonomy but not yet wired to a call site (no natural already-exists path today — save_account upserts); reserved for WS-A7.
Contacts sync's "no CardDAV configured" gap still returns Internal (unchanged) rather than NOT_CONFIGURED; left for the WS-A7 correctness pass to avoid scope creep.
Closes #273 (WS-A1) — the first story in the serial proto queue. Establishes the typed-error pattern for the whole `nuncio.v1` contract + CLI.
## What this adds
- **Proto (`nuncio.v1`)** — a frozen, machine-readable error taxonomy:
- `enum ErrorReason` (`ERROR_REASON_UNSPECIFIED=0`, plus `VALIDATION_FAILED`, `ACCOUNT/RULE/MESSAGE/EVENT/CONTACT_NOT_FOUND`, `ALREADY_EXISTS`, `NOT_CONFIGURED`, `BACKEND_UNREACHABLE`, `AUTH_REQUIRED`, `UNSUPPORTED`, `INTERNAL`). Append-only.
- `message ErrorInfo { ErrorReason reason=1; map<string,string> metadata=2; string message=3; }`.
- Descriptor golden regenerated; `contract_stability::descriptor_matches_committed_golden` passes.
- **Helper (`nuncio_proto::errors`)** — `status()`/`status_with_metadata()` build a `tonic::Status` carrying a prost-encoded `ErrorInfo` in the `grpc-status-details-bin` trailer (via `Status::with_details`), with the canonical code from `ErrorReason::code()`. `error_info()`/`error_reason()` decode it client-side (from `Status::details()`).
- **Reason → canonical code**: `VALIDATION_FAILED→InvalidArgument`, `*_NOT_FOUND→NotFound`, `ALREADY_EXISTS→AlreadyExists`, `NOT_CONFIGURED→FailedPrecondition`, `BACKEND_UNREACHABLE→Unavailable`, `AUTH_REQUIRED→Unauthenticated`, `UNSUPPORTED→Unimplemented`, `INTERNAL`/`UNSPECIFIED→Internal`.
- **Representative site conversions** in `nunciod/src/grpc.rs**: account not-found (RemoveAccount, TestAccountConnection), message not-found (GetMessage, MarkRead), event not-found (GetEvent), contact not-found (GetContact), rule not-found (UpdateRule), NSQL validation (CreateRule/UpdateRule/PreviewRule + account validate), calendar not-configured (Calendar.Sync). Canonical codes are unchanged, so existing code()/message() assertions still hold. The exhaustive ~90-site sweep is left to WS-A7.
- **E2E test** `crates/nunciod/tests/typed_errors_e2e_test.rs` — boots the real daemon server and, over authenticated gRPC, decodes the reason + metadata from returned `Status` details for account-not-found, message-not-found, not-configured, and validation-failed.
## Gate (3 commands, no `cargo verify`)
- `cargo fmt --all -- --check` — clean
- `cargo check-all` (clippy `--all-targets --workspace -D warnings`) — clean
- `cargo test-all` — all pass, **0 ignored, 0 failed** (incl. `nuncio-proto` 27 tests with `contract_stability` + `errors::tests`, and the new `typed_errors_e2e_test`)
## Notes / follow-ups
- `ALREADY_EXISTS` is defined in the taxonomy but not yet wired to a call site (no natural already-exists path today — `save_account` upserts); reserved for WS-A7.
- Contacts sync's "no CardDAV configured" gap still returns `Internal` (unchanged) rather than `NOT_CONFIGURED`; left for the WS-A7 correctness pass to avoid scope creep.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #273 (WS-A1) — the first story in the serial proto queue. Establishes the typed-error pattern for the whole
nuncio.v1contract + CLI.What this adds
nuncio.v1) — a frozen, machine-readable error taxonomy:enum ErrorReason(ERROR_REASON_UNSPECIFIED=0, plusVALIDATION_FAILED,ACCOUNT/RULE/MESSAGE/EVENT/CONTACT_NOT_FOUND,ALREADY_EXISTS,NOT_CONFIGURED,BACKEND_UNREACHABLE,AUTH_REQUIRED,UNSUPPORTED,INTERNAL). Append-only.message ErrorInfo { ErrorReason reason=1; map<string,string> metadata=2; string message=3; }.contract_stability::descriptor_matches_committed_goldenpasses.nuncio_proto::errors) —status()/status_with_metadata()build atonic::Statuscarrying a prost-encodedErrorInfoin thegrpc-status-details-bintrailer (viaStatus::with_details), with the canonical code fromErrorReason::code().error_info()/error_reason()decode it client-side (fromStatus::details()).VALIDATION_FAILED→InvalidArgument,*_NOT_FOUND→NotFound,ALREADY_EXISTS→AlreadyExists,NOT_CONFIGURED→FailedPrecondition,BACKEND_UNREACHABLE→Unavailable,AUTH_REQUIRED→Unauthenticated,UNSUPPORTED→Unimplemented,INTERNAL/UNSPECIFIED→Internal.crates/nunciod/tests/typed_errors_e2e_test.rs— boots the real daemon server and, over authenticated gRPC, decodes the reason + metadata from returnedStatusdetails for account-not-found, message-not-found, not-configured, and validation-failed.Gate (3 commands, no
cargo verify)cargo fmt --all -- --check— cleancargo check-all(clippy--all-targets --workspace -D warnings) — cleancargo test-all— all pass, 0 ignored, 0 failed (incl.nuncio-proto27 tests withcontract_stability+errors::tests, and the newtyped_errors_e2e_test)Notes / follow-ups
ALREADY_EXISTSis defined in the taxonomy but not yet wired to a call site (no natural already-exists path today —save_accountupserts); reserved for WS-A7.Internal(unchanged) rather thanNOT_CONFIGURED; left for the WS-A7 correctness pass to avoid scope creep.🤖 Generated with Claude Code