# ADR-GRPC-003: Transport, business and stream evidence are three axes, and none implies another - Status: accepted - Date: 2026-08-30 - Scope: `:grpc:grpc-core-api`, `:grpc:grpc-policy`, `:grpc:grpc-testkit` ## Context A failed RPC produces a status code, and a status code is not an answer to the question the caller actually has. `DEADLINE_EXCEEDED` on a mutation does not say whether the mutation happened; `UNAVAILABLE` after the request was sent does not say the server never saw it; response headers arriving does not say a transaction committed. Every one of those is a place where a plausible inference produces a duplicate write or a lost one, and none of them is visible in a test that only exercises the happy path. ## Decision Model what happened as three independent axes, and refuse the inferences between them. `GrpcTransportEvidence` records what the client observed on the wire, and distinguishes `NOT_SENT` — the client watched its own send fail — from `UNOBSERVED`, which is every other case where nothing is known. `GrpcBusinessEvidence` records what the application confirmed, with `COMMIT_UNKNOWN` as a real state rather than a placeholder. `GrpcStreamEvidence` is a sealed hierarchy whose non-empty cases all carry a position, because "partial" without a last sequence can be neither resumed nor reconciled. `GrpcExecutionEvidence` holds all three and rejects combinations nobody could have observed: a unary call with stream evidence, or a request the client watched fail to send that nonetheless carries business evidence. Promoting response headers to a confirmed commit is possible only by editing `withResponseHeadersSeen`, which is one method rather than a plausible line in an interceptor. `GrpcCompletionOutcome.forMutation` derives what a caller may conclude, and defaults `DEADLINE_EXCEEDED` and post-send `UNAVAILABLE` on a mutation to `COMPLETION_UNKNOWN`. The same types are used by the failure model and by the observation convention, so an incident has one account of a call rather than two. ## Consequences **A whole class of retry bug becomes unrepresentable.** `GrpcRetryEligibility` reads all three axes plus the idempotency profile; a caller cannot reach "retry" from a status alone because the status alone is not an input. **The fault lane has something to check.** `GrpcTransportEvidenceClassifier` turns a client's observations into evidence and refuses to infer `NOT_SENT` from an unobserved state — and the lane exercises it against a real connection dropped mid-call, not against a mock. **Callers must handle a third outcome.** `COMPLETION_UNKNOWN` is not a failure and not a success, and a caller that treats it as either is wrong. `GrpcOperationStatusQuery` and `GrpcCompletionReconciler` exist so that resolving it is a supported path rather than an exercise for the caller.