errcode

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package errcode provides structured error codes for the GoCell framework. All errors exposed across package boundaries must use this package instead of bare errors.New. Error codes follow the ERR_{MODULE}_{REASON} convention.

Three-channel runtime data isolation

Error separates runtime data into three channels with distinct visibility:

  • Message: a compile-time const literal describing the failure shape. Visible to clients in HTTP responses for both 4xx and 5xx.
  • Details ([]PublicDetail via WithDetails(PublicString(k, v) | PublicInt(k, n) | …)): typed runtime fields visible to clients on 4xx responses; stripped from 5xx by Error.MarshalJSON.
  • InternalDetails ([]InternalDetail via WithInternal(InternalAttr(k, v))): server-side runtime context that never appears in any HTTP response, only in slog records and traces.

PublicDetail and InternalDetail are sealed newtypes (unexported fields) so outside-package struct-literal construction of either type is a Go compile error. PublicDetail's value field is additionally typed as the sealed publicValue marker interface so callers can only construct it via the typed PublicString / PublicInt / PublicBool / PublicDuration / PublicTime constructors — wire-unsafe types (channels, functions, NaN/Inf floats, maps, structs, pointers) are inexpressible at compile time. InternalAttr keeps an untyped any value because the entire channel is server-only.

The const-literal restriction on New/Wrap message is enforced statically by archtest MESSAGE-CONST-LITERAL-01 outside this package; InternalAttr is exempt and may carry fmt.Sprintf-formatted strings.

Assertion ctor for production panics

Programmer-error / unreachable-path panics across kernel/, runtime/, adapters/ and cells/ use Assertion(format, args...) so the recovery middleware can surface a stable 500 + ErrInternal + CategoryInfra response. See .claude/rules/gocell/error-handling.md §5 for the A/B/C panic classification and the C-class re-throw exemption list.

Prefix ownership registry

Every production errcode.Code must belong to a registered prefix namespace. The registry maps ERR_ prefix strings to owning Go module paths and enforces that no two modules claim the same prefix (fail-fast panic on conflict).

Two entry shapes are supported:

  • Namespace entry ("ERR_AUTH_", trailing underscore): claims all codes whose string starts with the prefix.
  • Whole-code entry ("ERR_INTERNAL", no trailing underscore): claims exactly that one code string, used for single-concept generic codes where a namespace prefix would over-reach (e.g. ERR_NOT_FOUND must not imply ownership of a hypothetical ERR_NOTICE_ namespace).

GoCell platform prefixes (65 entries: 52 namespace + 13 whole-code) are self-registered in init() under owner "github.com/ghbvf/gocell". External cell modules register their own prefixes from their own init() by calling errcode.RegisterPrefix(prefix, owner). OwnerOfCode uses longest-prefix matching so finer-grained entries take precedence over broader namespaces.

The closed-set invariant is enforced at CI time by archtest ERRCODE-PREFIX-OWNERSHIP-01: every errcode.New / errcode.Wrap callsite and every exported Code sentinel in production code must have a registered prefix entry. Adding a new prefix requires regenerating pkg/errcode/testdata/prefix_set.golden (ERRCODE_PREFIX_GOLDEN_UPDATE=1).

See also:

  • Issue #1091
  • ADR: docs/architecture/202606031200-1091-adr-errcode-prefix-ownership-registry.md
  • Rules: .claude/rules/gocell/error-handling.md §"错误码前缀所有权 (#1091)"

Package errcode provides structured error codes for the GoCell framework. All errors exposed across package boundaries must use this package instead of bare errors.New.

Package errcode — TRANSIENT-NET-HELPER-FORM-01: see archtest godoc.

IsTransientNet is the single-source predicate that classifies an error chain as "transient because it carries a net.Error". Adapter classifiers (S3 / Redis / Vault isTransient*Error) delegate their net.Error branch to this helper so the transient set stays symmetric across adapters and any future widening / narrowing happens in one place.

Form (locked by archtest TRANSIENT-NET-HELPER-FORM-01):

  • *url.Error is unwrapped first: Op=="parse" → permanent (operator misconfig); other Ops recurse on the wrapped cause.
  • Otherwise `var n net.Error; return errors.As(err, &n)`.

`*url.Error` itself implements `net.Error` via embedded Timeout() / Temporary() forwarders, so a bare `errors.As(err, &netErr)` would silently classify URI parse failures as recoverable. RabbitMQ's `classifyStructuredDialError` has the same precedent in adapter-local code (`*url.Error` tried before `net.Error`).

Symmetry rationale: any error chain that surfaces a net.Error implementation (transport timeout, *net.OpError dial refused / connection reset, *net.DNSError) represents a transport-level failure where the next retry may reach a healthier endpoint. Truly unknown errors (JSON decode, SDK internal bugs) do not implement net.Error and fall through to false (fail-closed-on-unknown — consumer Requeues, retry-then-budget-DLX path never loses an event).

Adapter inventory routed through this helper (ADR docs/architecture/202605161800-adr-adapter-error-classification.md §Adapter transient inventory):

  • adapters/s3.isTransientS3Error
  • adapters/redis.isTransientRedisError
  • adapters/vault.isTransientVaultError

Postgres isRetryablePGError keeps Timeout() filter (pgconn.SafeToRetry already handles dial-refused), and rabbitmq classifyStructuredDialError uses Timeout() to discriminate code (both branches transient) — both allowlisted in ADAPTER-NET-TRANSIENT-FUNNEL-01.

ref: aws/aws-sdk-go-v2 aws/retry/retryable_error.go (url.Error unwrap pattern) ref: hashicorp/go-retryablehttp client.go baseRetryPolicy (url.Error permanent shortlist) ref: redis/go-redis error.go (net.Error fan-in)

Index

Constants

View Source
const StatusClientClosedRequest = 499

StatusClientClosedRequest is nginx's non-standard 499 status code returned when the client closes the connection before the server finishes responding.

ref: nginx ngx_http_request.h — NGX_HTTP_CLIENT_CLOSED_REQUEST 499

Variables

This section is empty.

Functions

func IsDomainNotFound

func IsDomainNotFound(err error, codes ...Code) bool

IsDomainNotFound reports whether err is a domain-layer not-found condition whose code is in the caller-supplied whitelist. Both conditions must hold:

  1. err must be an *Error with Category == CategoryDomain
  2. err.Code must appear in codes

This two-gated check prevents infra errors from ever matching, regardless of which code they carry — the dual-channel invariant from k8s IsNotFound.

Callers pass Code constants directly; no string(...) conversion is needed.

func IsExpected4xx

func IsExpected4xx(err error) bool

IsExpected4xx reports whether err maps to an HTTP 400-499 response code. These are expected client-side / business rejection conditions that callers should log at Warn level; true infrastructure failures should be Error.

Returns false for nil and for unclassified / plain errors (which are infra).

func IsInfraError

func IsInfraError(err error) bool

IsInfraError reports whether err represents an infrastructure failure.

Fail-closed semantics: any error that is not definitively classified as a domain / validation / auth error is treated as infra. This prevents infra outages from silently propagating into domain-not-found branches.

Returns false only for nil. Returns true for:

  • context.Canceled / context.DeadlineExceeded
  • *Error with Category == CategoryInfra or CategoryUnspecified
  • any unrecognized plain error (fail-closed)

func IsTransient

func IsTransient(err error) bool

IsTransient reports whether err is a transient (retry-safe) condition that an EventBus handler should Requeue rather than Reject:

if errcode.IsTransient(err) {
    return outbox.Requeue(err)
}
return outbox.Reject(outbox.NewPermanentError(err))

Classification is two-tier and the outer tier is authoritative:

  1. If the chain contains any *errcode.Error, the OUTERMOST one's WrapInfra transient marker is the final answer — return ec.transient and stop. A re-wrap via Wrap/New is a deliberate RE-CLASSIFICATION: the outer layer owns the disposition decision, so an inner WrapInfra marker does NOT leak through an outer permanent re-wrap (e.g. configcore cryptoOpError re-wraps a vault-transient cause as a config-layer KindInternal error and is routed via IsInfraError, not IsTransient). Raw recognizers (tier 2) are NOT consulted once the error is classified — a classified-permanent error stays permanent even if some inner cause looks network-ish. This is the open-source consensus (gRPC/AWS SDK v2/ Kratos: the boundary classifier's decision wins; re-wrap = re-classify) and keeps the WrapInfra funnel single-sourced.
  2. Only when the chain contains NO *errcode.Error (a raw, un-classified low-level error) are these recognized as transient: - context.DeadlineExceeded (a deadline may not recur on retry); context.Canceled is NOT transient (the caller gave up); - net.Error with Timeout()==true (modern replacement for the deprecated net.Error.Temporary(), golang/go #45729); - interface{ RetryableError() bool } == true (pgconn.SafeToRetry / aws-sdk-go-v2 RetryableError idiom for raw SDK errors).

errors.As binds the outermost *Error (fmt.Errorf("…: %w", WrapInfra(…)) still yields the WrapInfra *Error as the first — and only — *Error, so fmt-wrapping is transparent; only an outer errcode.Wrap/New re-classifies).

Returns false for nil.

func IsTransientNet

func IsTransientNet(err error) bool

IsTransientNet reports whether err contains a net.Error in its chain that represents a transport-level transient failure.

Returns true for: any net.Error (timeout class, *net.OpError dial refused / connection reset, *net.DNSError).

Returns false for: nil; errors that do not implement net.Error; and *url.Error{Op:"parse"} (URL parsing is an operator-side misconfiguration, not a transient transport failure). For other *url.Error Ops (HTTP client I/O), recurses on the wrapped cause so a `url.Error` wrapping a real `*net.OpError` correctly classifies as transient.

For classified *errcode.Error values, use errcode.IsTransient — that predicate keys on the WrapInfra transient marker and handles both classified and unclassified errors. IsTransientNet is the adapter-side helper for the net.Error fan-in branch only.

func OperatorString

func OperatorString(err error) string

OperatorString renders err for local CLI and CI output. It is still safe for secrets in errcode internals, but includes sourceCode/status on 5xx errcodes so operators can route failures without guessing from a generic 500 label.

func OwnerOfCode

func OwnerOfCode(code Code) (string, bool)

OwnerOfCode returns the owner of code by longest-matching-key.

A registered key matches per entryClaimsCode: a namespace entry (trailing "_") matches by prefix, a whole-code entry (no trailing "_") matches only by exact equality. Among all matching keys the longest one wins (so "ERR_AUTH_FORBIDDEN" matches "ERR_AUTH_" when both "ERR_AUTH_" and "ERR_" are registered, preferring the more specific entry).

Returns ("", false) when no registered entry covers code.

func RegisterPrefix

func RegisterPrefix(prefix, owner string)

RegisterPrefix registers a code-prefix or whole-code string as owned by owner.

It is intended to be called from package init() functions before any test or service logic runs. Calling it after init (e.g. from a request handler) is unsupported and may cause lock contention.

prefix must start with "ERR_" (fail-fast otherwise). A trailing underscore denotes a namespace claim ("ERR_AUTH_" owns all ERR_AUTH_* codes); a missing underscore denotes a whole-code claim ("ERR_INTERNAL" owns only that code).

Registering the same (prefix, owner) pair more than once is idempotent. Registering the same prefix with a different owner panics with an *Error whose message names both owners and the conflicting prefix. Registering a DIFFERENT prefix whose claimed code-set overlaps an existing entry owned by a different owner (one prefix contains the other) also panics — cross-module namespace claims must be disjoint. Overlap within a single owner is allowed.

The panic format args intentionally carry runtime strings (prefix + module path). This is an init-stage programmer-error panic (see panic taxonomy in error-handling.md); these values are non-sensitive identifiers (module path / prefix), not PII, and the panic surfaces only in startup crash logs. This usage is consistent with the MESSAGE-CONST-LITERAL-01 allowlist for errcode.Assertion — Assertion explicitly permits runtime context in its message for programmer-error panics.

All panics use the panicregister.Approved funnel (PANIC-REGISTERED-01).

func RenderPublic

func RenderPublic(err error) string

RenderPublic renders err for user-facing public output. It preserves the public Code, Message, and 4xx Details, but never uses InternalDetails or Cause from *Error because those may carry runtime diagnostics.

Distinct namespace from the typed PublicDetail constructor PublicString (see details.go): one renders an error chain to text, the other constructs a sealed key/value pair for WithDetails.

Types

type Category

type Category int

Category classifies the origin of an error for log-level routing and dual-channel triage (infra vs domain). The zero value CategoryUnspecified is treated as infra (fail-closed) by all classifiers.

ref: k8s apimachinery pkg/api/errors — IsNotFound dual-channel pattern (infra errors must never map to domain not-found).

const (
	// CategoryUnspecified is the zero value. Classifiers treat it as infra
	// (fail-closed) to prevent leaking infra faults into domain branches.
	CategoryUnspecified Category = iota

	// CategoryDomain signals a well-known business-layer condition
	// (resource not found, conflict, validation failure).
	CategoryDomain

	// CategoryInfra signals an infrastructure failure (DB down, network
	// timeout, bad connection). Must never be mapped to domain not-found.
	CategoryInfra

	// CategoryValidation signals a caller input validation failure (400-class).
	CategoryValidation

	// CategoryAuth signals an authentication / authorisation failure (401/403).
	CategoryAuth
)

type Code

type Code string

Code is a typed error code string.

const (
	ErrMetadataInvalid  Code = "ERR_METADATA_INVALID"
	ErrMetadataNotFound Code = "ERR_METADATA_NOT_FOUND"
	ErrCellNotFound     Code = "ERR_CELL_NOT_FOUND"
	ErrSliceNotFound    Code = "ERR_SLICE_NOT_FOUND"
	ErrContractNotFound Code = "ERR_CONTRACT_NOT_FOUND"
	ErrAssemblyNotFound Code = "ERR_ASSEMBLY_NOT_FOUND"
	ErrLifecycleInvalid Code = "ERR_LIFECYCLE_INVALID"
	ErrDependencyCycle  Code = "ERR_DEPENDENCY_CYCLE"
	ErrValidationFailed Code = "ERR_VALIDATION_FAILED"
	// ErrConflict signals that an operation was rejected because the target
	// resource already exists or is in a conflicting state. Maps to HTTP 409.
	// Used by scaffold/codegen paths to signal file-already-exists conflicts.
	ErrConflict Code = "ERR_CONFLICT"
	// ErrVersionConflict signals a compare-and-swap (optimistic lock) version
	// mismatch — caller's expectedVersion does not match the current row
	// version. Maps to KindConflict / HTTP 409. Used by runtime/state/cas and
	// every cell repo that implements CAS writes (UpdatePassword,
	// UpdateForRollback, etc).
	ErrVersionConflict    Code = "ERR_VERSION_CONFLICT"
	ErrReferenceBroken    Code = "ERR_REFERENCE_BROKEN"
	ErrInternal           Code = "ERR_INTERNAL"
	ErrServiceUnavailable Code = "ERR_SERVICE_UNAVAILABLE"
	ErrAuthUnauthorized   Code = "ERR_AUTH_UNAUTHORIZED"
	ErrAuthForbidden      Code = "ERR_AUTH_FORBIDDEN"
	ErrRateLimited        Code = "ERR_RATE_LIMITED"
	ErrCSRFOriginDenied   Code = "ERR_CSRF_ORIGIN_DENIED"
	ErrBodyTooLarge       Code = "ERR_BODY_TOO_LARGE"
	ErrJourneyNotFound    Code = "ERR_JOURNEY_NOT_FOUND"
	ErrTestExecution      Code = "ERR_TEST_EXECUTION"
	ErrCheckRefInvalid    Code = "ERR_CHECKREF_INVALID"
	ErrZeroTestMatch      Code = "ERR_ZERO_TEST_MATCH"
	ErrBusClosed          Code = "ERR_BUS_CLOSED"
	ErrCellMissingOutbox  Code = "ERR_CELL_MISSING_OUTBOX"
	ErrCellMissingCodec   Code = "ERR_CELL_MISSING_CODEC"
	// ErrCellMissingTokenIssuer signals that a Cell was started without a token
	// issuer dependency that it requires.
	ErrCellMissingTokenIssuer Code = "ERR_CELL_MISSING_TOKEN_ISSUER"
	// ErrCellMissingBootstrapStore signals that a Cell was started in durable
	// mode without the bootstrap-chain ledger store it requires to persist
	// bootstrap auth-fail events. Surfaced fail-fast at cell.Init() so a
	// misconfigured production assembly fails at startup rather than deferring
	// the failure to the first consumed event (Reject → DLX).
	ErrCellMissingBootstrapStore Code = "ERR_CELL_MISSING_BOOTSTRAP_STORE"
	ErrCellInvalidConfig         Code = "ERR_CELL_INVALID_CONFIG"
	// ErrCellPlatformUnsupported signals that a Cell option requested capability
	// that is not implemented on the current GOOS — distinct from
	// ErrCellInvalidConfig (configuration mistake) so operators can route
	// "this build was deployed to the wrong platform" failures separately from
	// "the deployment YAML is wrong". Surfaced fail-fast at cell.Init() time so
	// the failure is visible at process startup rather than during phase3b
	// LifecycleHook execution.
	ErrCellPlatformUnsupported Code = "ERR_CELL_PLATFORM_UNSUPPORTED"
	ErrSessionNotFound         Code = "ERR_SESSION_NOT_FOUND"
	ErrSessionConflict         Code = "ERR_SESSION_CONFLICT"
	ErrOrderNotFound           Code = "ERR_ORDER_NOT_FOUND"
	ErrDeviceNotFound          Code = "ERR_DEVICE_NOT_FOUND"
	ErrCommandNotFound         Code = "ERR_COMMAND_NOT_FOUND"
	ErrAdapterPGNoTx           Code = "ERR_ADAPTER_PG_NO_TX"
	// ErrPGSchemaShape signals that a value read from a PostgreSQL column does
	// not conform to the expected schema shape — e.g., an enum column returned a
	// value not in the application's known set. Usable from both adapters/postgres
	// and cell-layer repositories without creating a cross-layer import. Distinct
	// from ErrAdapterPGSchemaShape (adapters/postgres package-level alias) so
	// cells/ can reference this shared code without depending on adapters/.
	ErrPGSchemaShape  Code = "ERR_PG_SCHEMA_SHAPE"
	ErrAuthKeyInvalid Code = "ERR_AUTH_KEY_INVALID"
	// ErrAuthVerifierConfig signals a JWT verifier construction error — e.g.
	// required configuration (WithExpectedAudiences) was not provided.
	// Distinct from ErrAuthKeyInvalid (key material) so operators can route
	// verifier misconfiguration separately from cryptographic key failures.
	ErrAuthVerifierConfig Code = "ERR_AUTH_VERIFIER_CONFIG"
	// ErrAuthTokenInvalid signals that a JWT or service token failed
	// cryptographic or structural validation.
	ErrAuthTokenInvalid Code = "ERR_AUTH_TOKEN_INVALID"
	// ErrAuthTokenExpired signals that a JWT or service token has passed its
	// expiry time.
	ErrAuthTokenExpired Code = "ERR_AUTH_TOKEN_EXPIRED"
	// ErrAuthInvalidTokenIntent signals that a JWT's token_use claim (and/or
	// its JOSE typ header) does not match the expected intent for the current
	// request scope — e.g., a refresh token presented at a business endpoint,
	// or an access token presented at /auth/refresh. Middleware and slice
	// layers map this to a generic ERR_AUTH_UNAUTHORIZED / ERR_AUTH_REFRESH_FAILED
	// response to prevent token-type enumeration; the specific code is only
	// visible in logs.
	//
	// ref: RFC 8725 §2.8 / §3.11 (JWT token confusion threat model)
	// ref: AWS Cognito token_use claim, Keycloak typ header constants
	ErrAuthInvalidTokenIntent Code = "ERR_AUTH_INVALID_TOKEN_INTENT"

	// Access-core cell error codes.
	ErrAuthUserNotFound   Code = "ERR_AUTH_USER_NOT_FOUND"
	ErrAuthUserDuplicate  Code = "ERR_AUTH_USER_DUPLICATE"
	ErrAuthRoleNotFound   Code = "ERR_AUTH_ROLE_NOT_FOUND"
	ErrAuthRoleDuplicate  Code = "ERR_AUTH_ROLE_DUPLICATE"
	ErrAuthPolicyNotFound Code = "ERR_AUTH_POLICY_NOT_FOUND"
	ErrAuthInvalidInput   Code = "ERR_AUTH_INVALID_INPUT"
	// ErrAuthUserNotActive signals the user's status is not 'active' (i.e.,
	// locked or suspended). Authentication surfaces (login / refresh /
	// validate) reject any non-active user fail-closed: a suspended user
	// must not obtain a fresh session, refresh existing tokens, or have
	// their existing session continue to validate. Replaces the
	// pre-S4.0-narrow ErrAuthUserLocked which only covered the locked
	// state and left suspended as a fail-open hole.
	ErrAuthUserNotActive        Code = "ERR_AUTH_USER_NOT_ACTIVE"
	ErrAuthSessionInvalidInput  Code = "ERR_AUTH_SESSION_INVALID_INPUT"
	ErrAuthIdentityInvalidInput Code = "ERR_AUTH_IDENTITY_INVALID_INPUT"
	ErrAuthLoginInvalidInput    Code = "ERR_AUTH_LOGIN_INVALID_INPUT"
	ErrAuthLoginFailed          Code = "ERR_AUTH_LOGIN_FAILED"
	// ErrAuthOldPasswordIncorrect signals the supplied old password did not
	// match the stored credential during a ChangePassword flow. Distinct from
	// ErrAuthLoginFailed: login paths collapse missing-user / wrong-password /
	// inactive-account into a single opaque 401 to prevent enumeration of
	// account state; the ChangePassword caller is an already-authenticated
	// subject mutating their own credential, so the precise reason
	// ("old password incorrect") is safe to surface and useful for UX. Both
	// codes map to KindUnauthenticated → HTTP 401.
	ErrAuthOldPasswordIncorrect Code = "ERR_AUTH_OLD_PASSWORD_INCORRECT"
	ErrAuthLogoutInvalidInput   Code = "ERR_AUTH_LOGOUT_INVALID_INPUT"
	ErrAuthLogoutUnavailable    Code = "ERR_AUTH_LOGOUT_UNAVAILABLE"
	ErrAuthRefreshInvalidInput  Code = "ERR_AUTH_REFRESH_INVALID_INPUT"
	ErrAuthRefreshFailed        Code = "ERR_AUTH_REFRESH_FAILED"
	ErrAuthRefreshUnavailable   Code = "ERR_AUTH_REFRESH_UNAVAILABLE"
	// ErrAuthServiceUnavailable signals that the authentication service is
	// temporarily unavailable — e.g. the JWT key provider is sealed, the
	// idempotency store is unreachable, or a dependency required to verify
	// credentials cannot be reached. Maps to HTTP 503 Service Unavailable.
	// Distinct from ErrAuthUnauthorized (401) so operators can route transient
	// infrastructure outages separately from invalid-credential rejections.
	ErrAuthServiceUnavailable Code = "ERR_AUTH_SERVICE_UNAVAILABLE"
	ErrAuthInvalidToken       Code = "ERR_AUTH_INVALID_TOKEN"
	ErrAuthRBACInvalidInput   Code = "ERR_AUTH_RBAC_INVALID_INPUT"
	ErrAuthKeyMissing         Code = "ERR_AUTH_KEY_MISSING"
	ErrAuthSelfDelete         Code = "ERR_AUTH_SELF_DELETE"
	// ErrAuthRoleFetchFailed signals that role-name resolution at the time of
	// session-token issuance failed due to an infrastructure fault (RoleRepository
	// unavailable, query error, etc.). Session minting is fail-closed: callers
	// must abort login / refresh / token re-issuance rather than sign a token
	// carrying empty roles, which would look like a successful authentication
	// but silently strip every RBAC capability. Maps to HTTP 500.
	ErrAuthRoleFetchFailed Code = "ERR_AUTH_ROLE_FETCH_FAILED"
	// ErrAuthPasswordResetRequired signals that the authenticated subject must
	// change their password before accessing business endpoints. The middleware
	// enforces this when the JWT claim password_reset_required is true.
	// Only the exempt endpoints (POST /api/v1/access/users/{id}/password and
	// DELETE /api/v1/access/sessions/{id}) bypass this check.
	ErrAuthPasswordResetRequired Code = "ERR_AUTH_PASSWORD_RESET_REQUIRED"
	// ErrAuthBootstrapFailed signals that HTTP Basic Auth on the first-admin
	// setup endpoint failed. All authentication failure modes (missing header,
	// wrong username, wrong password) share this single code to prevent
	// field-level oracle attacks — attackers cannot distinguish "wrong username"
	// from "wrong password" via the error code or message.
	//
	// Maps to HTTP 401 Unauthorized. Credentials are checked via
	// subtle.ConstantTimeCompare to prevent timing side-channels.
	//
	// ref: Go stdlib crypto/subtle.ConstantTimeCompare
	// ref: keycloak/keycloak KC_BOOTSTRAP_ADMIN_USERNAME/PASSWORD env model
	ErrAuthBootstrapFailed Code = "ERR_AUTH_BOOTSTRAP_FAILED"
	// ErrSetupAlreadyInitialized signals that the interactive first-run admin
	// endpoint (POST /api/v1/access/setup/admin) was invoked after the system
	// already has at least one admin. The caller should authenticate via
	// /api/v1/access/sessions/login instead. Maps to HTTP 410 Gone: the endpoint
	// is permanently retired for the lifetime of the deployment (one-shot
	// lifecycle), not just temporarily conflicting.
	ErrSetupAlreadyInitialized Code = "ERR_SETUP_ALREADY_INITIALIZED"
	// ErrAuthLastAdminProtected signals that an operation would remove the last
	// remaining admin from the system. The accesscore "at least one admin"
	// invariant (ADR `docs/architecture/202605101400-adr-admin-invariant.md`)
	// rejects DeleteUser / Lock / RevokeRole when the target is the sole admin
	// holder. Maps to HTTP 403 Forbidden — the request is structurally valid
	// but policy-blocked. The DB-level last_admin_protected trigger
	// (migrations/019_roles.sql) is the SQL safety net behind the service-
	// level LastAdminGuard (cells/accesscore/internal/domain/admin.go).
	ErrAuthLastAdminProtected Code = "ERR_AUTH_LAST_ADMIN_PROTECTED"

	// Config-core cell error codes.
	ErrConfigDuplicate           Code = "ERR_CONFIG_DUPLICATE"
	ErrConfigInvalidInput        Code = "ERR_CONFIG_INVALID_INPUT"
	ErrConfigPublishInvalidInput Code = "ERR_CONFIG_PUBLISH_INVALID_INPUT"
	ErrConfigRepoNotFound        Code = "ERR_CONFIG_REPO_NOT_FOUND"
	ErrConfigRepoDuplicate       Code = "ERR_CONFIG_REPO_DUPLICATE"
	ErrConfigRepoQuery           Code = "ERR_CONFIG_REPO_QUERY"
	ErrFlagNotFound              Code = "ERR_FLAG_NOT_FOUND"
	ErrFlagDuplicate             Code = "ERR_FLAG_DUPLICATE"
	ErrFlagInvalidInput          Code = "ERR_FLAG_INVALID_INPUT"
	ErrFlagRepoQuery             Code = "ERR_FLAG_REPO_QUERY"

	// Audit-core cell error codes.
	ErrAuditRepoNotFound Code = "ERR_AUDIT_REPO_NOT_FOUND"
	ErrAuditRepoQuery    Code = "ERR_AUDIT_REPO_QUERY"
	ErrArchiveUpload     Code = "ERR_ARCHIVE_UPLOAD"
	ErrArchiveMarshal    Code = "ERR_ARCHIVE_MARSHAL"
	ErrNotImplemented    Code = "ERR_NOT_IMPLEMENTED"

	// Audit ledger (runtime/audit/ledger) error codes.
	// ErrAuditLedgerNotFound signals that a requested audit entry (by SeqNo)
	// does not exist in the ledger store.
	ErrAuditLedgerNotFound Code = "ERR_AUDIT_LEDGER_NOT_FOUND"
	// ErrAuditLedgerAlreadyExists signals that an Append was rejected because
	// the content fingerprint (HMAC of eventID+eventType+actorID+timestamp+payload)
	// already exists in the idempotency set. The caller should treat the entry
	// as already committed and not retry.
	ErrAuditLedgerAlreadyExists Code = "ERR_AUDIT_LEDGER_ALREADY_EXISTS"
	// ErrAuditChainBroken signals that hash chain integrity verification failed
	// during startup (RestartRecoveryStrictTailVerify mode). The process cannot
	// accept new entries without first resolving the tampered or corrupted chain.
	// Maps to HTTP 500 (infrastructure fault). Operator action required.
	ErrAuditChainBroken Code = "ERR_AUDIT_CHAIN_BROKEN"

	// Pagination / validation error codes.
	ErrCursorInvalid     Code = "ERR_CURSOR_INVALID"
	ErrPageSizeExceeded  Code = "ERR_PAGE_SIZE_EXCEEDED"
	ErrInvalidTimeFormat Code = "ERR_INVALID_TIME_FORMAT"
	// ErrValidationInvalidUUID signals that a UUID-typed input (currently used
	// for HTTP path parameters declared with `format: uuid` in contract.yaml)
	// failed to parse. Maps to HTTP 400 to distinguish "malformed identifier
	// shape" from "identifier valid but resource not found" (404). Issued at
	// handler edge by httputil.ParseUUIDPathParam.
	ErrValidationInvalidUUID Code = "ERR_VALIDATION_INVALID_UUID"

	// Resilience middleware error codes.
	ErrCircuitOpen Code = "ERR_CIRCUIT_OPEN"

	// Outbox relay health error codes.
	// ErrRelayBudgetExhausted signals that an outbox relay operation (poll /
	// reclaim / cleanup) has exceeded its consecutive-failure threshold, tripping
	// the failure budget and marking /readyz unhealthy.
	ErrRelayBudgetExhausted Code = "ERR_RELAY_BUDGET_EXHAUSTED"

	// Observability configuration error.
	// Raised by kernel / runtime observability constructors when a
	// required dependency (Provider, cellID) is missing or malformed.
	// Semantically an initialisation error — distinct from
	// ErrValidationFailed (user-input validation) so operators can route
	// the two through different dashboards.
	ErrObservabilityConfigInvalid Code = "ERR_OBSERVABILITY_CONFIG_INVALID"

	// WebSocket runtime error codes.
	ErrWSConnNotFound   Code = "ERR_WS_CONN_NOT_FOUND"
	ErrWSAlreadyStarted Code = "ERR_WS_ALREADY_STARTED"
	ErrWSAlreadyStopped Code = "ERR_WS_ALREADY_STOPPED"
	ErrWSHubStopping    Code = "ERR_WS_HUB_STOPPING"
	ErrWSHubNotRunning  Code = "ERR_WS_HUB_NOT_RUNNING"
	ErrWSMaxConns       Code = "ERR_WS_MAX_CONNS"
	// ErrWebsocketOriginsMissing signals that an UpgradeHandler was constructed
	// with an empty AllowedOrigins list. The handler rejects construction
	// fail-fast rather than silently accepting connections from any origin.
	// Operators must supply at least one explicit origin host pattern.
	//
	// Example:
	//
	//	handler, err := adapterws.UpgradeHandler(hub, adapterws.UpgradeConfig{
	//	    AllowedOrigins: []string{"https://example.com"},
	//	})
	//
	// ref: docs/plans/202604270020-1-2-ci-3-claude-ship-reactive-bachman.md PR-MODE-1
	ErrWebsocketOriginsMissing Code = "ERR_WEBSOCKET_ORIGINS_MISSING"
	// ErrWebsocketOriginsInvalid signals that AllowedOrigins contains a pattern
	// that would disable browser Origin protection, such as the full wildcard
	// "*".
	ErrWebsocketOriginsInvalid Code = "ERR_WEBSOCKET_ORIGINS_INVALID"
	// ErrWebsocketHubMissing signals that UpgradeHandler was constructed with
	// a nil *rtws.Hub. Composition-time fail-fast — letting nil through would
	// defer the failure until the first HTTP request, violating the
	// error-first construction contract (PR-MODE-6.1).
	//
	// Example:
	//
	//	handler, err := adapterws.UpgradeHandler(nil, adapterws.UpgradeConfig{
	//	    AllowedOrigins: []string{"https://example.com"},
	//	})
	//	var ec *errcode.Error
	//	if errors.As(err, &ec) && ec.Code == errcode.ErrWebsocketHubMissing { /* misconfig */ }
	ErrWebsocketHubMissing Code = "ERR_WEBSOCKET_HUB_MISSING"
	// ErrWebsocketAuthenticatorMissing signals that an UpgradeHandler was
	// constructed without an Authenticator. The error is returned by the
	// error-form constructor and panicked by the static-wiring twin.
	//
	// SEC-FAIL-CLOSED (PR-V1-SEC-WS-AUTH-ACL): nil Authenticator must surface
	// at composition root, not at the first HTTP request.
	ErrWebsocketAuthenticatorMissing Code = "ERR_WEBSOCKET_AUTHENTICATOR_MISSING"
	// ErrWebsocketBroadcastFilterMissing signals that Hub.BroadcastFilter was
	// called with a nil filter function. fail-closed: full-broadcast must be
	// expressed explicitly via `func(Conn) bool { return true }`.
	ErrWebsocketBroadcastFilterMissing Code = "ERR_WEBSOCKET_BROADCAST_FILTER_MISSING"
	// ErrWebsocketBroadcastSubjectMissing signals that Hub.BroadcastToSubject
	// was called with an empty subject string.
	ErrWebsocketBroadcastSubjectMissing Code = "ERR_WEBSOCKET_BROADCAST_SUBJECT_MISSING"
	// ErrWebsocketUpgradeUnauthenticated signals that the UpgradeHandler
	// rejected an incoming HTTP request because the Authenticator returned
	// either absent (no credential) or a present-but-invalid credential. The
	// HTTP response status is 401 and the body is plain text; this code is
	// only used in server-side slog records.
	ErrWebsocketUpgradeUnauthenticated Code = "ERR_WEBSOCKET_UPGRADE_UNAUTHENTICATED"
	// ErrWebsocketSlowClient signals that the Hub evicted a connection because
	// its send buffer filled (gorilla/websocket select-default-drop pattern).
	// Emitted from BroadcastFilter / BroadcastToSubject / Send.
	ErrWebsocketSlowClient Code = "ERR_WEBSOCKET_SLOW_CLIENT"

	// Outbox envelope error codes.
	// ErrEnvelopeSchema signals that an inbound wire message does not conform
	// to the expected envelope schema — unknown schemaVersion, missing required
	// fields, or corrupt JSON. Consumers must Reject (not retry) on this error.
	ErrEnvelopeSchema Code = "ERR_ENVELOPE_SCHEMA"

	// Bootstrap lifecycle error codes.
	// ErrBootstrapLifecycle signals that a lifecycle operation was called in an
	// invalid state — e.g. Append or Start called after the lifecycle has already
	// started. Distinct from ErrLifecycleInvalid (metadata validation) so
	// operators can route runtime lifecycle faults separately.
	ErrBootstrapLifecycle Code = "ERR_BOOTSTRAP_LIFECYCLE"

	// ErrBootstrapDoubleManaged signals that the same resource (e.g. a relay)
	// was registered via two paths that both result in lifecycle management
	// (e.g. WithRelay and WithManagedResource on the same object). This would
	// cause Close() to be called twice during shutdown. Detected at phase0
	// before any side effects start.
	ErrBootstrapDoubleManaged Code = "ERR_BOOTSTRAP_DOUBLE_MANAGED"

	// Refresh token store error code (runtime/auth/refresh).
	//
	// ErrRefreshTokenRejected is the single public sentinel emitted by
	// refresh.Store implementations for every unhappy path (malformed token,
	// unknown selector, verifier mismatch, expired, revoked, reused beyond
	// grace). Internal reasons are observed through the structured slog field
	// `reason` rather than the error shape, eliminating enumeration and timing
	// side-channels. CategoryAuth — OAuth2 RFC 6749 §10.4 attack signal when
	// reuse detected; handler maps to HTTP 401 regardless of internal reason.
	ErrRefreshTokenRejected Code = "ERR_REFRESH_TOKEN_REJECTED"

	// ErrRefreshTokenReused is a distinct sentinel emitted when a refresh token
	// that has already been consumed (rotated_at IS NOT NULL) is re-presented
	// beyond the grace window. It is separate from ErrRefreshTokenRejected so
	// the sessionrefresh service (Batch 3) can trigger cascade revoke and epoch
	// bump specifically on confirmed reuse attacks, while other reject causes
	// (malformed, expired, revoked) remain plain HTTP 401 with no side-effects.
	// CategoryAuth — maps to HTTP 401; does NOT wrap ErrRefreshTokenRejected.
	ErrRefreshTokenReused Code = "ERR_REFRESH_TOKEN_REUSED"

	// KeyProvider error codes.
	// ErrKeyProviderKeyNotFound signals that the requested key ID is not
	// present in the provider's keyring — e.g. a historical key that has been
	// purged. Callers must not fall back to plaintext; surface as a config error.
	// Permanent error — EventBus handlers should return DispositionReject.
	// Maps to Vault HTTP 404 (key or mount not found).
	ErrKeyProviderKeyNotFound Code = "ERR_KEY_PROVIDER_KEY_NOT_FOUND"
	// ErrKeyProviderAuthFailed signals that the Vault token has been revoked,
	// has insufficient permissions, or has expired (Vault HTTP 403 Forbidden).
	// Distinct from ErrKeyProviderKeyNotFound (404 — key absent) so operators
	// can route permission/token failures separately from missing-key failures.
	//
	// Use when:
	//   - Vault returns HTTP 403 on any transit read/encrypt/decrypt path.
	//   - Token revoked (revoke-accessor) or token lacks required capabilities.
	//   - Permission denied on transit/keys/{name} or transit/encrypt|decrypt.
	//
	// Permanent error — EventBus handlers should return DispositionReject.
	// Operators must rotate the Vault token (not retry the operation).
	ErrKeyProviderAuthFailed Code = "ERR_KEY_PROVIDER_AUTH_FAILED"
	// ErrKeyProviderEncryptFailed signals a KMS encrypt-side operation failure
	// (Vault Transit encrypt API error, malformed response, etc.). Distinct from
	// ErrKeyProviderDecryptFailed so callers and log aggregators can route
	// encrypt-side failures (usually transient / retriable) separately from
	// decrypt-side failures (usually permanent / data integrity signal).
	// Permanent error — EventBus handlers should return DispositionReject.
	ErrKeyProviderEncryptFailed Code = "ERR_KEY_PROVIDER_ENCRYPT_FAILED"
	// ErrKeyProviderDecryptFailed signals an AES-GCM authentication failure,
	// wrong key, or malformed ciphertext. Fail-closed: callers must surface
	// this as an error and never return raw ciphertext or empty string.
	// Permanent error — EventBus handlers should return DispositionReject.
	ErrKeyProviderDecryptFailed Code = "ERR_KEY_PROVIDER_DECRYPT_FAILED"
	// ErrKeyProviderRotateFailed signals a key-rotation operation failure
	// (Vault rotate API returned an error, new key version could not be read
	// back, malformed response). Distinct from ErrKeyProviderKeyNotFound so
	// rotation-path retries and alerting do not confuse "key absent" with
	// "rotation API unreachable".
	// Permanent error — EventBus handlers should return DispositionReject.
	ErrKeyProviderRotateFailed Code = "ERR_KEY_PROVIDER_ROTATE_FAILED"
	// ErrKeyProviderTransient signals a transient KeyProvider failure that is
	// safe to retry after back-off. Maps to Vault HTTP responses indicating
	// temporary unavailability:
	//
	//   - 503 Service Unavailable (sealed, standby, maintenance)
	//   - 429 Too Many Requests (rate-limited)
	//   - 408 Request Timeout / network timeout
	//
	// Contrast with ErrKeyProviderEncryptFailed / ErrKeyProviderDecryptFailed /
	// ErrKeyProviderKeyNotFound / ErrKeyProviderRotateFailed, which signal
	// permanent conditions (400 Bad Request, 403 Forbidden, 404 Not Found).
	//
	// EventBus Disposition routing is driven by the WrapInfra transient
	// marker, NOT by this code string (post-206). An error carries transient
	// semantics only when constructed via WrapInfra (which sets the private
	// Error.transient marker that IsTransient keys on):
	//
	//   - WrapInfra(ErrKeyProviderTransient, …) → IsTransient true → Requeue
	//   - New/Wrap(KindUnavailable, ErrKeyProviderTransient, …) → IsTransient
	//     FALSE (no marker) → not transient. Do not hand-build this code and
	//     expect Requeue; route through WrapInfra (vault.classifyVaultError).
	//
	// Use IsTransient(err) to check the full error chain — never compare
	// against this code string for disposition.
	//
	// ref: aws/aws-encryption-sdk-python src/aws_encryption_sdk/exceptions.py
	// (GenerateKeyError / DecryptKeyError transient vs permanent split).
	ErrKeyProviderTransient Code = "ERR_KEY_PROVIDER_TRANSIENT"
	// ErrConfigDecryptFailed signals that a sensitive config value could not be
	// decrypted at the repository boundary. Maps to HTTP 500 (internal error).
	// Symmetric with ErrConfigEncryptFailed for the encrypt boundary.
	ErrConfigDecryptFailed Code = "ERR_CONFIG_DECRYPT_FAILED"
	// ErrConfigEncryptFailed signals that a sensitive config value could not be
	// encrypted at the repository boundary (Encrypt/EncryptVersion). Maps to
	// HTTP 500 (internal error). Symmetric with ErrConfigDecryptFailed so
	// alerting systems can filter all crypto failures via a single code prefix
	// and distinguish them from generic ErrConfigRepoQuery DB failures.
	ErrConfigEncryptFailed Code = "ERR_CONFIG_ENCRYPT_FAILED"
	// ErrConfigKeyMissing signals that a required encryption key (e.g. GOCELL_CONFIGCORE_MASTER_KEY
	// or vault token) is absent at startup. Triggers fail-fast in postgres mode.
	ErrConfigKeyMissing Code = "ERR_CONFIG_KEY_MISSING"
	// ErrVaultAuthFailed signals a Vault auth method failure: missing or malformed
	// credentials, unknown VAULT_AUTH_METHOD, AppRole / Kubernetes Login returned
	// error, static token rejected by real-mode guard, or re-authentication loop
	// exhausted (ctx canceled).
	//
	// Distinct from ErrKeyProviderAuthFailed, which signals runtime Vault 403 on
	// transit encrypt/decrypt paths (an in-flight operation failure). This code
	// is used exclusively at bootstrap and background re-auth loop boundaries.
	//
	// Permanent at boot: operators must fix configuration before restart.
	// During re-auth loop: ctx cancellation is the only exit condition; this
	// code is returned when ctx is canceled while retrying.
	//
	// Category: default CategoryInfra (consistent with ErrVault* / ErrKeyProvider* siblings).
	ErrVaultAuthFailed Code = "ERR_VAULT_AUTH_FAILED"

	// Control-plane startup configuration errors (cmd/corebundle).
	//
	// ErrControlplaneServiceSecretMissing signals that the InternalHMACRing field
	// of composition.SharedDeps is nil, so the /internal/v1/* service-token guard
	// cannot be constructed. Produced by cmd/corebundle.buildInternalHMACRing and
	// runtime/composition.SharedDeps.validateInternalListenerGuard; fails the
	// binary at startup before any listener binds. Never reaches the HTTP layer in
	// practice.
	//
	// Distinct from ErrValidationFailed (user-input validation) so operators
	// can grep startup logs specifically for control-plane misconfigurations.
	ErrControlplaneServiceSecretMissing Code = "ERR_CONTROLPLANE_SERVICE_SECRET_MISSING"

	// ErrControlplaneNonceStoreMissing signals that the control-plane
	// service-token guard was constructed without a replay-safe NonceStore
	// (either nil or a NoopNonceStore sentinel) while adapter mode is "real".
	// Produced by runtime/composition.SharedDeps.validateProductionNonceStore;
	// fails the binary at startup. Operators must inject an InMemoryNonceStore
	// (single pod) or a shared store (multi-pod) before restart.
	//
	// This is the closure of the P1 replay window: a captured valid service
	// token must not be replayable within its 5-minute validity window.
	ErrControlplaneNonceStoreMissing Code = "ERR_CONTROLPLANE_NONCE_STORE_MISSING"

	// ErrControlplaneClaimerNotDistributed signals that a real multi-pod
	// corebundle deployment is using a process-local outbox idempotency
	// Claimer. Operators must configure Redis so consumers coordinate
	// idempotency across pods before restart.
	ErrControlplaneClaimerNotDistributed Code = "ERR_CONTROLPLANE_CLAIMER_NOT_DISTRIBUTED"

	// ErrAuthReplayDetected distinguishes a service-token replay signal from
	// generic authentication failures (invalid MAC, expired token, missing
	// header). Machine-side consumers (monitoring, alerting, SDKs) can match
	// this code exclusively to trigger replay-specific escalation without
	// parsing the human-readable message.
	//
	// Maps to HTTP 401 Unauthorized — the client must not retry with the same
	// token; a new token with a fresh nonce is required.
	ErrAuthReplayDetected Code = "ERR_AUTH_REPLAY_DETECTED"

	// ErrNonceStoreFull signals that the in-memory nonce store has reached its
	// maximum entry capacity and could not reclaim any expired entries. The
	// request is rejected to prevent unbounded memory growth; the condition is
	// transient (existing nonces will expire at the next TTL boundary).
	//
	// Maps to HTTP 503 Service Unavailable — callers should retry after a brief
	// back-off. Distinct from ErrAuthReplayDetected (security signal) so
	// operators can route capacity alerts separately from security alerts.
	ErrNonceStoreFull Code = "ERR_AUTH_NONCE_STORE_FULL"

	// ErrReadyzVerboseDenied signals that /readyz?verbose was requested but
	// the supplied X-Readyz-Token header did not match the configured verbose
	// token (or no token was configured while verbose is still being
	// requested). Maps to HTTP 401 Unauthorized.
	//
	// Introduced by PR-A35: prior behavior silently downgraded mismatched
	// requests to a plain 200 (without the verbose body), masking
	// misconfiguration. The strict 401 makes configuration errors observable
	// to operators without affecting the bare /readyz endpoint used by
	// Kubernetes readinessProbes (which must not pass ?verbose).
	ErrReadyzVerboseDenied Code = "ERR_READYZ_VERBOSE_DENIED"

	// ErrControlplaneVerboseTokenMissing signals that
	// GOCELL_READYZ_VERBOSE_TOKEN was not set at startup in a mode that
	// requires the token to be explicitly configured. Starting with PR-A35
	// this invariant holds in all adapter modes (not just "real"). Operators
	// who genuinely do not want the verbose endpoint exposed at all should
	// explicitly acknowledge that via the WithReadyzVerboseDisabled option
	// instead of relying on an absent token to silently disable gating.
	ErrControlplaneVerboseTokenMissing Code = "ERR_CONTROLPLANE_VERBOSE_TOKEN_MISSING"

	// ErrControlplaneVerboseTokenSample signals that
	// GOCELL_READYZ_VERBOSE_TOKEN is set to the literal placeholder shipped
	// in .env.example. A production deploy that copies the sample env and
	// rotates only the other secrets would otherwise pass startup with a
	// publicly-known token; rejecting the literal value at startup forces
	// operators to mint a real high-entropy secret. Distinct from
	// ErrControlplaneVerboseTokenMissing so dashboards and runbooks can
	// distinguish "forgot to configure" from "configured with the sample".
	ErrControlplaneVerboseTokenSample Code = "ERR_CONTROLPLANE_VERBOSE_TOKEN_SAMPLE"

	// ErrAdapterEndpointNotTLS signals that a remote adapter endpoint (Redis,
	// Vault, S3, etc.) was configured with a non-TLS scheme and is not a loopback
	// address. Adapters call secutil.ValidateTLSEndpoint at construction time and
	// fail-closed with this code so that plaintext connections to production
	// infrastructure are rejected at startup rather than at first use.
	//
	// Loopback addresses (127.0.0.1, ::1, localhost) are exempt to allow
	// testcontainer / dev-CI workflows without TLS termination.
	//
	// ref: docs/plans/202604270020-1-2-ci-3-claude-ship-reactive-bachman.md PR-MODE-1
	ErrAdapterEndpointNotTLS Code = "ERR_ADAPTER_ENDPOINT_NOT_TLS"

	// ErrDistlockTimeout is returned by Locker.Acquire when the requested key is
	// already held by another holder and the lock cannot be granted immediately.
	// Maps to HTTP 409 Conflict at the API boundary.
	// runtime/distlock aliases this as ErrLockTimeout for ergonomic local use.
	ErrDistlockTimeout Code = "ERR_DISTLOCK_TIMEOUT"

	// ErrClientCanceled signals that the request was canceled by the client
	// before the server finished processing — typically context.Canceled
	// surfaced from a downstream IO operation. Maps to HTTP 499 (nginx
	// "Client Closed Request"); operators should treat as a client-direction
	// signal, not a server fault, so it never pollutes 5xx error-rate SLOs.
	//
	// IO-boundary helpers should wrap context.Canceled errors with this
	// code so the HTTP layer routes the response to 499 + slog.Warn via the
	// 4xx response writer path.
	//
	// Distinct from ErrServerTimeout (504): this is "the client gave up",
	// the latter is "the server's own deadline fired". Splitting the two
	// codes lets dashboards / SDK retry policies / circuit breakers react
	// differently — 499 is benign noise, 504 is a real timeout to alert on.
	//
	// ref: nginx ngx_http_special_response.c — 499 emitted on client disconnect
	// ref: OTel semantic conventions http-spans.md — 4xx server spans Unset;
	//      intentional cancellation should not set error.type
	ErrClientCanceled Code = "ERR_CLIENT_CANCELED"

	// ErrServerTimeout signals that the request exceeded a server-side or
	// upstream-inherited deadline — typically context.DeadlineExceeded
	// surfaced from a downstream IO operation. Maps to HTTP 504 (Gateway
	// Timeout); operators should treat as a real server-direction failure
	// and route through the standard 5xx error path (slog.Error + 5xx error
	// rate / SLO bucket).
	//
	// IO-boundary helpers should wrap context.DeadlineExceeded errors with
	// this code (NOT ErrClientCanceled) so the HTTP layer surfaces the
	// timeout via 504 instead of conflating it with client disconnect.
	//
	// Distinct from ErrClientCanceled (499): this is "the server's deadline
	// fired", the latter is "the client gave up". Aligns with NGINX (499 vs
	// 504), Kratos transport/http/status (Canceled→499, DeadlineExceeded→504),
	// and standard ingress / load-balancer expectations for retry semantics.
	//
	// CategoryInfra so IsInfraError predicates (health bucket, retry
	// classifiers) treat timeouts as real infrastructure faults.
	//
	// ref: RFC 9110 §15.6.5 — 504 Gateway Timeout
	// ref: kratos transport/http/status — Canceled→499, DeadlineExceeded→504
	ErrServerTimeout Code = "ERR_SERVER_TIMEOUT"

	// ErrListenerAuthChainMissing signals that a bootstrap listener was declared
	// with a nil authChain. Bootstrap phase0 fail-fasts with this code so that
	// listeners without explicit authentication intent are rejected at startup
	// rather than silently accepting all requests. Operators must pass an
	// explicit authChain — use []auth.ListenerAuth{auth.AuthNone{}} for
	// listeners that genuinely require no authentication (e.g. HealthListener
	// behind a Kubernetes probe path).
	//
	// ref: docs/plans/202604270020-1-2-ci-3-claude-ship-reactive-bachman.md PR-MODE-1
	ErrListenerAuthChainMissing Code = "ERR_LISTENER_AUTH_CHAIN_MISSING"

	// ErrGRPCServerMissing signals that a bootstrap gRPC listener was declared
	// via WithGRPCListener with a nil (bare or typed) GRPCServer. Bootstrap
	// phase0 fail-fasts with this code, mirroring WithManagedResource's nil
	// guard. The composition root owns gRPC auth wiring: the interceptor chain
	// is built via runtime/grpc/interceptor.NewUnaryChain (which always wires
	// UnaryAuth and panics on a nil verifier), so the fail-closed auth gate lives
	// at chain construction; this sentinel guards the bootstrap wiring seam.
	//
	// ref: docs/plans/specs/202605262300-048-grpc-adapter/plan.md PR 5 (RL-14)
	ErrGRPCServerMissing Code = "ERR_GRPC_SERVER_MISSING"

	// ErrReadyzVerboseUnconfigured signals that /readyz?verbose was requested
	// but neither a verbose token nor the explicit disabled flag has been
	// configured. This fail-closed default forces operators to make an explicit
	// choice — configure a token (WithReadyzVerboseToken) or disable the verbose
	// endpoint (WithReadyzVerboseDisabled) — rather than leaking internal health
	// details to unauthenticated callers by default.
	//
	// Maps to HTTP 401 Unauthorized at the health handler layer.
	//
	// ref: docs/plans/202604270020-1-2-ci-3-claude-ship-reactive-bachman.md PR-MODE-1
	ErrReadyzVerboseUnconfigured Code = "ERR_READYZ_VERBOSE_UNCONFIGURED"

	// Idempotency error codes (kernel/idempotency).
	//
	// ErrIdempotencyLeaseExpired signals that the processing lease is no longer
	// held — either it expired naturally or another consumer claimed it.
	// Callers MUST stop business logic and proceed to Release. Maps to HTTP 409.
	ErrIdempotencyLeaseExpired Code = "ERR_IDEMPOTENCY_LEASE_EXPIRED"
	// ErrIdempotencyNoClaimLease signals that Receipt methods were called for a
	// Claim result that did not acquire a processing lease. Maps to HTTP 409.
	ErrIdempotencyNoClaimLease Code = "ERR_IDEMPOTENCY_NO_CLAIM_LEASE"
	// ErrIdempotencyInProgress signals that another request is currently
	// processing the same idempotency key (ClaimBusy). The client should retry
	// after the in-flight request completes. Maps to HTTP 409.
	ErrIdempotencyInProgress Code = "ERR_IDEMPOTENCY_IN_PROGRESS"
	// ErrIdempotencyKeyReused signals that the same Idempotency-Key was sent with
	// a different request body (fingerprint mismatch). Per IETF idempotency-key
	// draft §2.7, reusing a key with a different payload is a client error that
	// maps to HTTP 422 (KindUnprocessable) — the request is syntactically valid
	// but semantically unprocessable against the response already committed for
	// the original payload.
	ErrIdempotencyKeyReused Code = "ERR_IDEMPOTENCY_KEY_REUSED"

	// Metrics error codes (kernel/observability/metrics).
	//
	// ErrMetricsLabelMismatch signals that the supplied Labels do not exactly
	// cover the registered LabelNames. Maps to HTTP 400 (caller/programmer error).
	ErrMetricsLabelMismatch Code = "ERR_METRICS_LABEL_MISMATCH"
	// ErrMetricsLabelValueIllegal signals that a label value contains a separator
	// reserved by the OTel-provider cache key. Maps to HTTP 400.
	ErrMetricsLabelValueIllegal Code = "ERR_METRICS_LABEL_VALUE_ILLEGAL"

	// Outbox error codes (kernel/outbox).
	//
	// ErrOutboxDegraded signals that the fail-open drop ratio has exceeded the
	// configured threshold. The /readyz aggregator maps this to HTTP 200 +
	// status="degraded" rather than 503, but the code itself maps to 503 for
	// direct HTTP boundary use. Maps to HTTP 503 Service Unavailable.
	ErrOutboxDegraded Code = "ERR_OUTBOX_DEGRADED"

	// Worker error codes (kernel/worker).
	//
	// ErrWorkerExitedEarly signals that a Worker.Start returned nil while the
	// group context was still live — an abnormal silent exit modeled as an error
	// so WorkerGroup can propagate the failure. Maps to HTTP 500.
	ErrWorkerExitedEarly Code = "ERR_WORKER_EXITED_EARLY"

	// SecureCookie error codes (pkg/securecookie).
	//
	// ErrSecureCookieHashKeyTooShort signals that hashKey is shorter than the
	// minimum required length (32 bytes). Maps to HTTP 400.
	ErrSecureCookieHashKeyTooShort Code = "ERR_SECURECOOKIE_HASH_KEY_TOO_SHORT"
	// ErrSecureCookieInvalidBlockKey signals that blockKey is not nil and not
	// one of the valid AES key sizes (16, 24, or 32 bytes). Maps to HTTP 400.
	ErrSecureCookieInvalidBlockKey Code = "ERR_SECURECOOKIE_INVALID_BLOCK_KEY"
	// ErrSecureCookieEncodingTooShort signals that the encoded cookie value is
	// shorter than the minimum required length. Maps to HTTP 400.
	ErrSecureCookieEncodingTooShort Code = "ERR_SECURECOOKIE_ENCODING_TOO_SHORT"
	// ErrSecureCookieHMACInvalid signals that HMAC verification failed —
	// the cookie has been tampered with or forged. Maps to HTTP 400.
	ErrSecureCookieHMACInvalid Code = "ERR_SECURECOOKIE_HMAC_INVALID"
	// ErrSecureCookieExpired signals that the cookie has exceeded its configured
	// max-age. Maps to HTTP 400.
	ErrSecureCookieExpired Code = "ERR_SECURECOOKIE_EXPIRED"
	// ErrSecureCookieDecryptFailed signals that AES-GCM decryption failed —
	// wrong key or corrupt ciphertext. Maps to HTTP 400.
	ErrSecureCookieDecryptFailed Code = "ERR_SECURECOOKIE_DECRYPT_FAILED"

	// Auth nonce error codes (runtime/auth).
	//
	// ErrAuthNonceReused signals that a nonce has already been consumed within
	// its TTL window — a replay attack or duplicate request. Maps to HTTP 401.
	ErrAuthNonceReused Code = "ERR_AUTH_NONCE_REUSED"

	// Distlock Lock.Cause() sentinel codes (runtime/distlock).
	//
	// ErrDistlockLockLost signals that the manager failed to renew the lock or
	// the backend reports ownership has been taken by another holder. Returned
	// by Lock.Cause() after the manager calls Lock.markCause(ErrLockLost).
	// Maps to HTTP 409 (Conflict) if it ever surfaces to a caller.
	ErrDistlockLockLost Code = "ERR_DISTLOCK_LOCK_LOST"
	// ErrDistlockLockReleased signals that Lock.Release() was called by the
	// application (normal end-of-critical-section). Returned by Lock.Cause()
	// after the manager calls Lock.markCause(ErrLockReleased). NOT intended to
	// surface at HTTP boundaries; the runtime/distlock package tags it
	// KindInternal so an accidental leak produces a fail-closed 500 rather
	// than a misleading 409.
	ErrDistlockLockReleased Code = "ERR_DISTLOCK_LOCK_RELEASED"
	// ErrDistlockLockOrphaned signals that Lock.Orphan() was called by the
	// application: renewal has stopped and the backend key is NOT deleted —
	// it expires on its lease TTL (~1×TTL from the last successful renewal,
	// best-effort; see runtime/distlock.Lock.Orphan), handing the lock to a
	// competitor without a Release round-trip. Returned by
	// Lock.Cause() after the manager calls Lock.markCause(ErrLockOrphaned).
	// KindInternal matches ErrDistlockLockReleased: an orphaned lock surfacing
	// to an HTTP handler is a server-side programming bug (caller chose
	// bounded-TTL handoff deliberately); fail-closed 500 is preferable to a
	// misleading 409 (which implies external conflict).
	// ref: etcd-io/etcd client/v3/concurrency/session.go Session.Orphan
	ErrDistlockLockOrphaned Code = "ERR_DISTLOCK_LOCK_ORPHANED"

	// MetricsSchema error codes (tools/metricschema).
	//
	// ErrMetricsSchemaUnresolved signals that a concrete metric registration
	// has an unresolved identity field (name, label, namespace, bucket).
	// Maps to HTTP 500.
	ErrMetricsSchemaUnresolved Code = "ERR_METRICS_SCHEMA_UNRESOLVED"

	// Saga journal error codes (kernel/saga/journal).
	//
	// ErrSagaNotFound signals that the addressed saga instance was never
	// enqueued. Returned by Load and Append on unknown instance IDs; the
	// silent (false,nil) shape on Heartbeat/MarkTerminal is deliberate
	// (callers cannot distinguish a stale lease from a missing instance).
	// Maps to HTTP 404.
	ErrSagaNotFound Code = "ERR_SAGA_NOT_FOUND"
	// ErrSagaStaleLease signals that a lease-fenced Append presented a leaseID
	// that no longer owns the instance (lost / expired / superseded). A zombie
	// leader must not corrupt the event stream, so Append surfaces this rather
	// than dropping the write silently. Maps to HTTP 409.
	ErrSagaStaleLease Code = "ERR_SAGA_STALE_LEASE"
	// ErrSagaDuplicateInstance signals that Enqueue was called for an instance
	// ID that already exists. Maps to HTTP 409. Distinct from generic
	// ErrConflict so operator dashboards can route saga producer-side races
	// separately from cell-wide conflict signals.
	ErrSagaDuplicateInstance Code = "ERR_SAGA_DUPLICATE_INSTANCE"
	// ErrProjectionNotFound signals that the projection rebuild control-plane
	// endpoint (POST /admin/v1/projection/<cell>/<name>/rebuild) was given a
	// <cell>/<name> path that resolves to no registered projection Coordinator.
	// Constructed with KindNotFound → HTTP 404.
	ErrProjectionNotFound Code = "ERR_PROJECTION_NOT_FOUND"

	// ErrWebhookInvalidSignature signals HMAC signature verification failed —
	// the recomputed digest does not match any presented signature header.
	// Constructed with KindUnauthenticated → HTTP 401.
	ErrWebhookInvalidSignature Code = "ERR_WEBHOOK_INVALID_SIGNATURE"
	// ErrWebhookTimestampExpired signals the signed timestamp falls outside the
	// allowed replay window (default ±5min). Constructed with
	// KindUnauthenticated → HTTP 401.
	ErrWebhookTimestampExpired Code = "ERR_WEBHOOK_TIMESTAMP_EXPIRED"
	// ErrWebhookDuplicateDelivery signals a delivery whose idempotency claim did
	// not acquire a fresh lease. The PR-3 receiver constructs it on the
	// ClaimBusy branch — another worker currently holds the lease for the same
	// (sourceID, deliveryID) — and maps it to HTTP 409 (KindConflict) so the
	// sender retries. The already-completed replay (ClaimDone) is a success and
	// returns 200 without this sentinel. First constructed in PR-3 (receiver
	// runtime).
	ErrWebhookDuplicateDelivery Code = "ERR_WEBHOOK_DUPLICATE_DELIVERY"
	// ErrWebhookInvalidHeader signals a required signature header is missing or
	// malformed (bad scheme, unparseable timestamp). Constructed with
	// KindInvalid → HTTP 400.
	ErrWebhookInvalidHeader Code = "ERR_WEBHOOK_INVALID_HEADER"
	// ErrWebhookAlgorithmUnsupported signals an unrecognized / disallowed signing
	// algorithm (only HMAC-SHA256 is legal). Constructed with KindInvalid →
	// HTTP 400.
	ErrWebhookAlgorithmUnsupported Code = "ERR_WEBHOOK_ALGORITHM_UNSUPPORTED"
	// ErrWebhookSourceNotFound signals the source ID has no registered secret.
	// KindNotFound → HTTP 404 is ONLY for management/config lookups
	// (list/get source). On the inbound receive/verify path this sentinel MUST
	// NOT be used — the receiver must return ErrWebhookInvalidSignature
	// (KindUnauthenticated → 401) for any source-lookup failure, so the response
	// is byte-identical to a bad-signature failure and cannot be used to
	// enumerate which source IDs exist (via either HTTP status or wire error
	// code). First constructed in PR-3 (receiver runtime).
	ErrWebhookSourceNotFound Code = "ERR_WEBHOOK_SOURCE_NOT_FOUND"
	// ErrWebhookSSRFBlocked signals an outbound dispatch target resolved to a
	// blocked address (SSRF defense). Constructed with KindPermissionDenied →
	// HTTP 403. First constructed in PR-4 (SSRF guard).
	ErrWebhookSSRFBlocked Code = "ERR_WEBHOOK_SSRF_BLOCKED"
	// ErrWebhookDeliveryFailed signals a transient delivery failure (connection
	// refused, 5xx from target). Constructed with KindUnavailable → HTTP 503.
	// First constructed in PR-5 (dispatcher).
	ErrWebhookDeliveryFailed Code = "ERR_WEBHOOK_DELIVERY_FAILED"
	// ErrWebhookPermanentFailure signals a non-retryable delivery failure
	// (bad delivery id / signing failure / a selector that signals no
	// subscription is configured). Constructed with the Kind appropriate to the
	// failure — e.g. KindInvalid (HTTP 400). Non-2xx delivery RESPONSES are not
	// permanent (standard-webhooks aligned: every non-2xx is retried). First
	// constructed in PR-5 (dispatcher).
	ErrWebhookPermanentFailure Code = "ERR_WEBHOOK_PERMANENT_FAILURE"
	// ErrWebhookBodyTooLarge signals the inbound webhook body exceeded the size
	// limit. Constructed with KindPayloadTooLarge → HTTP 413. First constructed
	// in PR-3 (receiver runtime).
	ErrWebhookBodyTooLarge Code = "ERR_WEBHOOK_BODY_TOO_LARGE"
	// ErrWebhookConfigInvalid signals invalid webhook configuration (bad source
	// ID / delivery ID / empty secret). Constructed with KindInvalid → HTTP 400.
	ErrWebhookConfigInvalid Code = "ERR_WEBHOOK_CONFIG_INVALID"

	// Reconcile leader-election / epoch-fencing codes (KERNEL-RECONCILE-01 PR-A6).
	//
	// ErrReconcileLeaseLost signals that a LeaderElector.RenewLease found the
	// lease no longer owned by this holder (expired / taken over by a follower).
	// The reconcile Loop cancels its lease-scoped ctx on this sentinel to
	// interrupt the in-flight Reconcile. Constructed with KindConflict → HTTP 409
	// (mirrors ErrDistlockLockLost / ErrSagaStaleLease). It is a control-plane
	// signal that normally never surfaces at an HTTP boundary.
	ErrReconcileLeaseLost Code = "ERR_RECONCILE_LEASE_LOST"
	// ErrFencedWriteStale signals that a FencedRepository.ApplyFenced rejected a
	// write because the presented epoch is older than the highest epoch the
	// resource row has already seen (monotonic-epoch fencing CAS, Kleppmann DDIA
	// §8.4 — NOT the outbox UUID identity-fencing). A zombie leader's late write
	// must be structurally rejected, not silently applied. Constructed with
	// KindConflict → HTTP 409 (mirrors ErrSagaStaleLease).
	ErrFencedWriteStale Code = "ERR_FENCED_WRITE_STALE"
	// ErrFencedWriterUnbound signals that a FencedWriter with no bound repository
	// was written through (programmer error: the writer was zero-valued rather
	// than minted by the Loop from a live lease). Constructed with KindInternal →
	// HTTP 500 (server-side bug, not an external conflict).
	ErrFencedWriterUnbound Code = "ERR_FENCED_WRITER_UNBOUND"
	// ErrReconcileLeaseHeld signals that AcquireLease lost the race: another holder
	// owns a live lease for this reconcilerID. It is the EXPECTED steady-state
	// signal a follower sees on every poll, so the Loop logs it at Debug (not Warn).
	// Constructed with KindConflict → HTTP 409 (control-plane signal; normally
	// never surfaces at an HTTP boundary).
	ErrReconcileLeaseHeld Code = "ERR_RECONCILE_LEASE_HELD"
)

Sentinel error codes used throughout the GoCell framework.

func PublicCodeForStatus

func PublicCodeForStatus(status int) Code

PublicCodeForStatus returns the wire-safe error code for an HTTP status. Used by framework-owned raw responses that start from a status before constructing an Error.

Only KindUnavailable (503) and KindDeadlineExceeded (504) have dedicated wire codes; every other 5xx (500/501/502/507/...) collapses to ErrInternal to match Kind.PublicCode(), which Error.MarshalJSON applies during 5xx projection. Adding a new dedicated 5xx code requires extending Kind.PublicCode() in tandem so the status→code and Kind→code mappings stay aligned.

type Error

type Error struct {
	Kind            Kind
	Code            Code
	Message         string
	InternalDetails []InternalDetail
	Details         []PublicDetail
	Cause           error
	Category        Category
	// contains filtered or unexported fields
}

Error is a structured error that carries a machine-readable Code, a Kind-derived HTTP status, a human-readable Message, optional Details, and an optional wrapped Cause.

InternalDetails holds diagnostic key/value attributes that must never be exposed to API consumers. When non-empty, Error() formats them inline (for logs/traces); HTTP response writers use Message (safe for clients) and never serialize InternalDetails.

Category classifies the error origin for infra/domain triage. The zero value CategoryUnspecified is treated as infra (fail-closed).

func Assertion

func Assertion(format string, args ...any) *Error

Assertion constructs an *Error tagged as a programmer-error / impossible path. It is the canonical replacement for `panic(fmt.Sprintf("...", err))` patterns across production code: callers panic with the returned *Error so the kernel recovery middleware can surface a 500 with category=infra and a stable ErrInternal code, while the formatted text is preserved for logs and traces via Error.Error().

Behavior:

  • Kind = KindInternal (HTTP 500)
  • Code = ErrInternal (single sentinel for unrecoverable assertions)
  • Category = CategoryInfra (treated as infrastructure for IsInfraError)
  • Message = fmt.Sprintf(format, args...) — the formatted assertion text

The formatted Message intentionally carries runtime data because Assertion indicates an impossible state that has already been reached: there is no safe way to render the failure without it. The kernel recovery layer maps this to ErrInternal before the response leaves the process, so end users receive only the public ErrInternal code.

func New

func New(kind Kind, code Code, message string, opts ...Option) *Error

New creates an *Error with an explicit transport kind.

The message argument must be a compile-time const literal — runtime information belongs in WithDetails (typed, client-visible) or WithInternal (server-side only). archtest MESSAGE-CONST-LITERAL-01 statically enforces this rule outside the errcode package.

func Wrap

func Wrap(kind Kind, code Code, message string, cause error, opts ...Option) *Error

Wrap creates an *Error with an explicit transport kind and wrapped cause.

The same const-literal restriction documented on New applies to message.

func WrapInfra

func WrapInfra(code Code, message string, cause error, opts ...Option) *Error

WrapInfra is the single typed funnel for producing a transient (retry-safe) infrastructure error. It is the ONLY constructor that sets the private Error.transient marker — adapter classifiers (classifyPGError / classifyPGConnectError / classifyRedisError / classifyS3Error / classifyConnectError) route their transient branch through it so that IsTransient can recognize the result.

Behavior:

  • Kind = KindUnavailable (HTTP 503 "retry after a brief delay")
  • Category = CategoryInfra
  • Code = the caller's operation code (e.g. ERR_ADAPTER_PG_QUERY) — there is no parallel ErrAdapter*Transient code set; transient-vs-permanent is the single Kind+marker axis, queryable in metrics by Kind.
  • the private transient marker is set true

The const-literal restriction documented on New applies to message.

Funnel double-lock (per .claude/rules/gocell/ai-robust.md "Funnel 双向锁"):

  • upstream Hard: a transient adapter error is producible only via this function; the marker field is unexported so no package outside errcode can set it.
  • downstream Hard: IsTransient's *Error positive branch keys only on the marker, never on a code string.

archtest ADAPTER-ERROR-CLASSIFICATION-TRANSIENT-01 enforces both sides.

ref: jackc/pgx pgconn SafeToRetry; aws/aws-sdk-go-v2 aws/retry RetryableError.

func (*Error) Error

func (e *Error) Error() string

Error returns a formatted string representation for logging/diagnostics. When InternalDetails is non-empty its key=value pairs are appended, because Error() is consumed by logs and traces — not by API clients. Format: "[CODE] msg" or "[CODE] msg: cause" when a Cause is present.

func (*Error) FindAttr

func (e *Error) FindAttr(key string) (PublicDetail, bool)

FindAttr returns the first detail entry whose key matches the given key, or the zero PublicDetail and false when no such entry exists. Callers access the value via PublicDetail.Value(), which is typed any.

Used by ctxcancel.ReasonFromDetails and similar helpers that need to read back a single typed detail without exposing the raw slice across package boundaries.

func (*Error) MarshalHTTPEnvelope

func (e *Error) MarshalHTTPEnvelope(requestID string) ([]byte, error)

MarshalHTTPEnvelope renders the canonical v1 wire envelope {"error":{...}} in a single marshal pass, injecting requestID into the inner error object (omitted when empty). It is the choke point all HTTP error responses funnel through via pkg/httputil.writeErrorBody.

Because the result is built directly from PublicProjection — with no intermediate map[string]any — int64 detail values retain full precision (json.Marshal encodes int64 exactly; a map[string]any round-trip would coerce them to float64 and truncate beyond 2^53). PublicProjection performs the 5xx detail-strip + public-code normalization, and the narrowed httpErrorObject makes the operator-only SourceCode/Status fields type-level unrepresentable on the wire, so the body matches the v1 schema's additionalProperties:false error object exactly.

Caller contract: this is a framework-layer primitive — business handlers must emit errors via httputil.WriteError / WriteErrorWithStatus, which call this and add the fail-closed sentinel fallback. requestID is trusted as opaque correlation metadata sourced from ctxkeys.RequestIDFrom (framework RequestID middleware, UUID-shaped); it is written to the wire verbatim, so callers must not pass user-controlled input. The receiver may be nil (PublicProjection returns a valid ErrInternal 500 envelope). The output carries no trailing newline (unlike json.Encoder.Encode); joined error chains are not expanded — use the package-level PublicProjection(error) for those.

func (*Error) MarshalJSON

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON renders e in the wire form expected by contracts/shared/errors/error-response-v1.schema.json:

{"code":"ERR_X","message":"...","details":[{"key":"k","value":v}, ...]}

Server-side errors (Kind.IsClient() == false, i.e. HTTP 5xx) emit an empty details array regardless of attached entries — this is the single source-of-truth strip rule for runtime context that must never reach a client. InternalDetails and Cause are never marshaled because they may contain sensitive runtime data.

Wire safety is enforced upstream by the sealed PublicDetail newtype and its sealed publicValue marker interface: callers can only construct PublicDetail via the typed PublicString / PublicInt / PublicBool / PublicDuration / PublicTime constructors (see details.go), so the wire-unsafe kinds the prior MustValidateDetailsKinds validator rejected (channels, functions, NaN/Inf floats, maps, structs, pointers) are a Go compile error rather than a runtime panic.

func (*Error) OperatorProjection

func (e *Error) OperatorProjection() PublicError

OperatorProjection returns the CLI/CI structured projection for e. It keeps the public code/message/details contract, and for 5xx errors adds routeable source metadata that is safe for local operators.

func (*Error) OperatorString

func (e *Error) OperatorString() string

OperatorString renders e with the CLI/CI operator surface.

func (*Error) PublicCode

func (e *Error) PublicCode() Code

PublicCode returns the wire-safe code for e.

func (*Error) PublicProjection

func (e *Error) PublicProjection() PublicError

PublicProjection returns the HTTP/public structured projection for e.

func (*Error) PublicString

func (e *Error) PublicString() string

PublicString renders e with the same public surface used by HTTP clients.

func (*Error) Status

func (e *Error) Status() int

Status returns the HTTP status derived from e.Kind.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying Cause, enabling errors.Is / errors.As chains.

type InternalDetail

type InternalDetail struct {
	// contains filtered or unexported fields
}

InternalDetail is the sealed server-only diagnostic key/value attribute carried by errcode.Error.InternalDetails. Never marshaled to wire; only surfaces in server-side slog records (httputil log4xx / log5xx) and in Error.Error() string formatting. Construction is exclusive to InternalAttr — sealed-construction Hard pattern via unexported fields.

Unlike PublicDetail, the value field is typed any: the entire channel is server-only, so wire safety does not constrain accepted types and runtime data (fmt.Sprintf output, identifiers, stack summaries) may flow through. The wire-safety enforcement applies only to PublicDetail per ADR docs/architecture/202605051730-adr-errcode-message-pii-safety.md §Amendment.

func InternalAttr

func InternalAttr(key string, value any) InternalDetail

InternalAttr constructs an InternalDetail with the given key and value. Runtime data (fmt.Sprintf output, identifiers, stack summaries) may flow through InternalDetail since the entire channel is server-only.

Free-form single-string convention: when a callsite carries a single diagnostic message with no semantic key, use "_" as the key (InternalAttr("_", "scan error key=abc")). Error.Error() renders a single "_"-keyed entry as the bare value ("[CODE] scan error key=abc") rather than "[CODE] _=scan error key=abc", preserving the pre-#1035 WithInternal(string) output format. When mixing "_" with semantically-keyed entries, the "_=value" prefix appears in Error() output — prefer all-semantic keys in new code (e.g. InternalAttr("op", op), InternalAttr("query", q)) for clean slog records.

func (InternalDetail) AsSlogAttr

func (d InternalDetail) AsSlogAttr() slog.Attr

AsSlogAttr converts the internal detail to a slog.Attr for structured server-side logging. The HTTP error-logging middleware iterates InternalDetails and writes each AsSlogAttr() output into the slog record verbatim.

For string values, AsSlogAttr returns slog.String (KindString) so that pkg/redaction.RedactSlogAttr's free-form RedactString scan can mask embedded key=value secrets. Non-string values pass through as slog.Any (KindAny); the caller is responsible for not embedding sensitive content in custom Stringer / LogValuer carriers since RedactSlogAttr's KindAny path is passthrough by design.

func (InternalDetail) Key

func (d InternalDetail) Key() string

Key returns the internal detail key.

func (InternalDetail) Value

func (d InternalDetail) Value() any

Value returns the internal detail value.

type Kind

type Kind int

Kind is the transport-facing class of an Error.

Unlike Code, Kind is intentionally small and framework-owned. Subsystems own their Code values locally; HTTP response status is derived only from Kind.

const (
	// KindInternal is the zero value so partially constructed errors fail closed
	// as 500 instead of accidentally becoming a client-visible status.
	KindInternal Kind = iota
	KindInvalid
	KindUnauthenticated
	KindPermissionDenied
	KindNotFound
	KindConflict
	// KindUnprocessable maps to HTTP 422 (Unprocessable Content): the request is
	// syntactically valid but semantically unprocessable in the current resource
	// state — e.g. an Idempotency-Key reused with a different request body (per
	// IETF idempotency-key draft §2.7). Placed next to KindConflict for 4xx
	// grouping; the iota value is not load-bearing (Kind is never serialized and
	// has no golden — only the Kind→status mapping matters).
	KindUnprocessable
	KindGone
	KindPayloadTooLarge
	KindRateLimited
	KindClientClosed
	KindDeadlineExceeded
	KindUnavailable
	KindNotImplemented
)

func (Kind) IsClient

func (k Kind) IsClient() bool

IsClient reports whether k maps to an HTTP 4xx response.

func (Kind) PublicCode

func (k Kind) PublicCode() Code

PublicCode returns the wire-safe status-level code for k.

func (Kind) Status

func (k Kind) Status() int

Status returns the HTTP status associated with k.

type Option

type Option func(*Error)

Option customizes an Error at construction time.

func WithCategory

func WithCategory(category Category) Option

WithCategory sets the origin category used by classifiers. Constructors default to CategoryUnspecified, which IsInfraError treats as infra.

func WithDetails

func WithDetails(details ...PublicDetail) Option

WithDetails attaches structured, client-visible details to the error. The framework's HTTP response writer renders 4xx errors with the detail list as a JSON array of {"key","value"} objects, and strips the list from 5xx errors so server-side runtime context never leaks to clients.

Construction goes exclusively through the typed PublicString / PublicInt / PublicBool / PublicDuration / PublicTime constructors (see details.go). PublicDetail.value is a sealed marker interface, so wire-unsafe types (channels, functions, NaN/Inf floats, maps, structs, pointers) are a Go compile error — the prior DETAILS-SLOG-ATTR-01 archtest and the MustValidateDetailsKinds runtime allowlist are both superseded by the type-system invariant.

Multiple WithDetails calls accumulate; entries append in call order.

Example:

errcode.New(KindNotFound, ErrCellNotFound, "cell not found",
    errcode.WithDetails(errcode.PublicString("cellId", id)))

func WithInternal

func WithInternal(details ...InternalDetail) Option

WithInternal attaches structured server-only diagnostic key/value pairs. InternalDetails never appear in wire responses (4xx or 5xx); they surface in server-side slog records via the HTTP error-logging middleware and in Error.Error() string formatting. Runtime data (fmt.Sprintf output, IDs, stack summaries) may flow through this channel.

Multiple WithInternal calls accumulate; entries append in call order.

Example:

errcode.New(KindInternal, ErrInternal, "config load failed",
    errcode.WithInternal(errcode.InternalAttr("path", path)))

type PrefixOwner

type PrefixOwner struct {
	Prefix string
	Owner  string
}

PrefixOwner is a snapshot entry from the prefix ownership registry. Prefix is the registered prefix string (e.g. "ERR_AUTH_" or "ERR_INTERNAL"); Owner is the module path that owns that namespace (e.g. "github.com/ghbvf/gocell").

func RegisteredPrefixes

func RegisteredPrefixes() []PrefixOwner

RegisteredPrefixes returns a sorted snapshot of all registered (Prefix, Owner) pairs. The slice is a copy — callers may modify it freely.

type PublicDetail

type PublicDetail struct {
	// contains filtered or unexported fields
}

PublicDetail is the sealed wire-safe key/value attribute carried by errcode.Error.Details. Both fields are unexported so outside-package struct-literal construction is inexpressible in Go's type system, and the value field is typed publicValue (sealed marker interface) so the constructor surface enumerates every wire-safe scalar exactly once.

Wire schema is {"key": string, "value": <scalar>}; the concrete scalar types (string / int64 / bool / nanosecond int64 for Duration / RFC3339Nano string for Time) match contracts/shared/errors/error-response-v1.schema.json which constrains value to {"string","number","boolean"}. Adding a new scalar kind requires (a) a new publicValue impl and constructor here, (b) the wire schema update, and (c) DETAILS-SEALED-FIELD-FROZEN-01 archtest update.

Known bypass surface: errcode.Error.Details is exported as []PublicDetail. External code can overwrite or append the slice with values constructed via the typed Public* constructors. The wire-side 5xx Details-strip invariant (Error.MarshalJSON → project() → []PublicDetail{}) and the projection-side zero-value filter are the defenses for leakage/schema drift regardless of append source; the sealed-construction Hard claim is "no outside code can construct a non-zero PublicDetail without going through the typed constructors", not "no outside code can mutate Error.Details". Callers should still route input through WithDetails(...PublicDetail).

func PublicBool

func PublicBool(key string, value bool) PublicDetail

PublicBool constructs a bool-valued PublicDetail.

func PublicDuration

func PublicDuration(key string, value time.Duration) PublicDetail

PublicDuration constructs a Duration-valued PublicDetail. Serializes as integer nanoseconds on the wire (matching slog.DurationValue's encoding of the channel pre-PR #1035). Operators surfacing duration via the wire should format to human-readable string at the call site and use PublicString instead — JSON consumers cannot tell nanoseconds from a generic count without out-of-band context.

func PublicInt

func PublicInt[T PublicInteger](key string, value T) PublicDetail

PublicInt constructs an integer-valued PublicDetail. Accepts any signed integer kind via the PublicInteger type constraint.

func PublicString

func PublicString(key, value string) PublicDetail

PublicString constructs a string-valued PublicDetail. The most common kind — every identifier, enum value, validation reason, error label.

func PublicTime

func PublicTime(key string, value time.Time) PublicDetail

PublicTime constructs a time.Time-valued PublicDetail. Serializes as an RFC3339Nano-formatted string (encoding/json's default time.Time format).

func (PublicDetail) AsSlogAttr

func (d PublicDetail) AsSlogAttr() slog.Attr

AsSlogAttr converts the public detail to a slog.Attr for structured logging. Used by HTTP error-logging middleware (pkg/httputil log4xx / log5xx) that fans Details into slog.Record alongside other attributes; not part of the wire serialization path.

The typed value wrappers route each scalar kind to the matching slog.Value constructor (StringValue / Int64Value / BoolValue / DurationValue / TimeValue). String values surface as slog.KindString so pkg/redaction.RedactSlogAttr's free-form RedactString scan can mask embedded key=value secrets.

func (PublicDetail) Key

func (d PublicDetail) Key() string

Key returns the public detail key.

func (PublicDetail) MarshalJSON

func (d PublicDetail) MarshalJSON() ([]byte, error)

MarshalJSON emits the canonical wire shape {"key":..., "value":...} for a single PublicDetail entry. Unexported fields are otherwise skipped by encoding/json. Wire-unsafe values (NaN/Inf, channels, etc.) cannot reach this point — the typed constructor surface excludes them at compile time.

func (PublicDetail) Value

func (d PublicDetail) Value() any

Value returns the underlying scalar as untyped any for read-side helpers (FindAttr, ctxcancel.ReasonFromDetails). The dynamic type is always one of: string, int64, bool, time.Duration, time.Time, or nil (zero PublicDetail). Mutations through the returned value cannot reach the PublicDetail because every concrete value wrapper stores by value.

type PublicError

type PublicError struct {
	Code    Code           `json:"code"`
	Message string         `json:"message"`
	Details []PublicDetail `json:"details"`

	// Operator-only fields. They are omitted from HTTP/public projections but
	// let local CLI and CI output preserve a routeable source code for 5xx
	// failures without exposing InternalDetails, Cause, or server-side Details.
	SourceCode Code `json:"sourceCode,omitempty"`
	Status     int  `json:"status,omitempty"`
}

PublicError is the structured projection shared by HTTP responses, CLI text rendering, and machine-readable command output. Details is the sealed PublicDetail slice (see details.go); each entry serializes to {"key": ..., "value": ...} per the v1 wire schema.

func OperatorProjection

func OperatorProjection(err error) []PublicError

OperatorProjection returns the operator structured projections for err.

func PublicProjection

func PublicProjection(err error) []PublicError

PublicProjection returns the public structured projections for err. Joined errors are flattened so machine consumers can route each error independently.

func (PublicError) MarshalJSON

func (p PublicError) MarshalJSON() ([]byte, error)

MarshalJSON keeps the details field schema-stable even when callers build a PublicError literal instead of using the projection helpers.

type PublicInteger

type PublicInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

PublicInteger covers every Go signed integer kind so callers do not need explicit int → int64 conversions (covers int, len()/cap() results, json.SyntaxError.Offset which is int64, etc). Stored as int64 internally. Unsigned kinds are intentionally excluded — no GoCell callsite uses uint*; callers that genuinely need uint values must cast at the call site to surface the truncation choice.

Directories

Path Synopsis
Package errcodetest provides typed assertion funnels for errcode-bearing test paths.
Package errcodetest provides typed assertion funnels for errcode-bearing test paths.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL