Error codes
Every failure this harness can raise carries a stable machine-readable code alongside its human-readable message. The reporting layer classifies outcomes by code, the JUnit and Markdown renderers group by it, and scenario assertions branch on it, so the code — not the message text — is the interface.
Two properties this document exists to guarantee:
- Every code the harness can raise is listed here, so a consumer that observes one in a report can look up what it means without reading the source.
- Every code listed here exists, because a stale table is worse than no table: consumers trust it.
Both are enforced by scripts/check-error-codes.py, which discovers every
crate’s code() method, extracts the codes it can return, and compares them
against the tables below. It runs in CI, so drift is a build failure.
Naming, and how it differs from the simulator
Codes here are UPPER_SNAKE_CASE (ASSERTION_TYPE_MISMATCH). The
crucible-simulator domain codes are snake_case (insufficient_balance),
and those codes pass through this harness unchanged — an adapter never
re-codes a simulator failure, so one condition always reports one code.
The two conventions are currently a family inconsistency rather than a
deliberate distinction; harmonising them is tracked separately. Until then,
treat an UPPER_SNAKE_CASE code as a harness failure (this repository’s
own machinery: definitions, fixtures, adapters, assertions, the runner) and a
snake_case code as a domain failure reported by the system under test.
The stability contract
- Never rename a code. Reporting, classification and consumer
matchstatements depend on the string. - Never reuse a retired code for a different condition.
- An unrecognised code means “a failure I do not specifically classify” and must be reported as such, not treated as a panic or an unreachable branch.
Harness definitions and registry
scenario-core
Definition-time validation: a scenario, its metadata, or its environment is structurally wrong, so it is rejected before anything executes.
| Code | Retryable | Raised when |
|---|---|---|
INVALID_ID | no | A scenario id is malformed or does not match the required shape. |
DUPLICATE_ID | no | A scenario id is registered twice. |
INVALID_METADATA | no | Scenario metadata is missing a required field or is inconsistent. |
UNKNOWN_ACTOR | no | A step references an actor the scenario never declares. |
UNKNOWN_TOKEN | no | A step references a token the scenario never declares. |
UNKNOWN_STATE | no | A step references a named state that was never captured. |
UNKNOWN_REFERENCE | no | A step references a value, snapshot, or fixture that does not exist. |
UNAVAILABLE_CAPABILITY | no | The scenario requires a capability the harness does not provide. |
UNSUPPORTED_ENVIRONMENT | no | The scenario targets an environment this harness cannot build. |
INVALID_SEED | no | A seed is malformed or out of range. |
INVALID_TIMEOUT | no | A timeout value is malformed or not a positive duration. |
CONFIDENTIAL_PUBLIC_INPUT | no | Privacy guard: a proof-generation step’s public inputs name a confidential field. Rejected at definition time so private material cannot be published by construction, not merely by convention. |
SERIALIZATION | no | Encoding or decoding a scenario, vector, or result failed. |
scenario-registry
| Code | Retryable | Raised when |
|---|---|---|
DUPLICATE_SCENARIO | no | A scenario with that id is already registered. |
UNKNOWN_SCENARIO | no | A lookup, filter, or replay names a scenario that is not registered. |
INVALID_FILTER | no | A registry filter (category, tag, capability, id prefix) is malformed. |
Runner
scenario-runner
Execution, lifecycle, and isolation failures. These describe the harness’s own behaviour around a scenario, not a verdict about the system under test.
| Code | Retryable | Raised when |
|---|---|---|
CONFIGURATION | no | The runner was configured inconsistently (for example, parallel workers with serial-only scenarios). |
SCENARIO_DEFINITION | no | The definition is valid per scenario-core but unusable as written. |
ENVIRONMENT | no | Building the scenario environment failed. |
TIMEOUT | yes | The scenario exceeded its timeout. Retryable only because a timing-dependent failure may not recur; a deterministic failure never will, and is classified as such. |
CANCELLED | yes | The run was cancelled before the scenario completed. Classified distinctly from a failure, so an abort is never reported as a defect. |
RETRY_EXHAUSTED | no | The declared retry limit was reached. |
NON_RETRYABLE | no | A failure occurred that the retry policy refuses to retry, by design: deterministic failures are never retried. |
ISOLATION | no | A scenario’s state leaked into another, or an isolation boundary was violated. |
CLEANUP | no | A cleanup hook failed after the scenario ran. |
HOOK | no | A lifecycle hook failed. |
INFRASTRUCTURE | yes | A transport or harness-level dependency failed. Distinct from NON_RETRYABLE precisely so the retry policy can treat the two differently. |
Assertions and fixtures
assertions
| Code | Retryable | Raised when |
|---|---|---|
ASSERTION_MISSING_OBSERVATION | no | An assertion expected an observation the run never produced. Usually a scenario bug rather than a system failure. |
ASSERTION_TYPE_MISMATCH | no | The observed value’s type does not match what the assertion compares against. |
ASSERTION_MISSING_FIXTURE | no | An assertion references fixture data that does not exist. |
ASSERTION_INTERNAL | no | An internal inconsistency in the assertion engine — a bug here, not a finding. |
fixtures
| Code | Retryable | Raised when |
|---|---|---|
FIXTURE_MISSING | no | A referenced fixture is absent from the catalog. |
FIXTURE_MALFORMED | no | A fixture failed to parse or violates its schema. |
FIXTURE_VERSION_MISMATCH | no | A fixture’s declared version is not one this harness supports. |
FIXTURE_INTERNAL | no | An internal inconsistency while loading fixtures. |
Adapters
Adapters translate the harness’s operation vocabulary into a backing surface. A translation failure is an adapter fault: it means the operation could not be expressed, which is a different finding from the surface rejecting it.
adapters/simulator
| Code | Retryable | Raised when |
|---|---|---|
INVALID_OPERATION | no | The operation could not be translated into a simulator call. |
adapters/prover
| Code | Retryable | Raised when |
|---|---|---|
NO_PROOF_FIXTURE | no | A proof was required but the pack provides no fixture posture for it. |
GENERATION | no | Proof generation through the prover surface failed. |
UNSUPPORTED_OPERATION | no | The prover surface has no handling for this operation. |
adapters/soroban
| Code | Retryable | Raised when |
|---|---|---|
UNKNOWN_CONTRACT | no | The contract surface names a function on a contract it does not know. |
UNKNOWN_FUNCTION | no | The operation has no contract-function translation. |
MISSING_ARGUMENT | no | A call was translated without a required argument. |
INVALID_ARGUMENT | no | An argument does not match the call’s expected type. |
CONFIDENTIAL_INVOCATION | no | Privacy guard: an invocation would place confidential material in publicly visible contract arguments. |
adapters/testnet
| Code | Retryable | Raised when |
|---|---|---|
TESTNET_NOT_CONFIGURED | no | The live-network path was used without explicit opt-in configuration. The adapter never silently falls back to a network. |
INVALID_CONFIGURATION | no | The network configuration is malformed (bad RPC URL, unsupported network). |
INVALID_EXECUTION | no | A network execution attempt is structurally invalid. |
POLL_TIMEOUT | yes | Polling for a transaction result exceeded its budget. The network may simply be slow. |
MALFORMED_STATUS | no | A network status response did not match the expected shape. |
Codes shared across crates
Two codes appear in more than one module on purpose: the same condition has the same name wherever it surfaces, so a consumer never has to special-case which crate produced it.
| Code | Retryable | Raised when |
|---|---|---|
FIXTURE_INCONSISTENCY | no | A fixture contradicts another fixture or a declared expectation. Raised by the simulator and prover adapters as well as the Soroban adapter. |
INTERNAL | no | An internal inconsistency indicating a bug in this repository rather than a finding about the system under test. Raised by every module that defines codes. |
Using codes
#![allow(unused)]
fn main() {
if outcome.code() == "CONFIDENTIAL_PUBLIC_INPUT" {
// A privacy guard tripped: this is a finding about the scenario, not a
// flaky run, so it must never be retried or classified as infrastructure.
report(Classification::PrivacyViolation);
}
}
Prefer the typed error variant inside Rust, where match is exhaustive and the
compiler enforces it. Codes exist for the surfaces a type cannot reach: the JSON
report, the JUnit XML a CI server ingests, the Markdown summary, and the exit
code of the CLI.