# Error Display Format

Human-readable error rendering for terminal output. Machine-readable output is covered in [machine-output.md](./machine-output.md).

## Format

```text
error unresolved-import (E001):
at ci.ops:3:6:

   |
 2 | @use std.log as log
 3 | @use ./validaton as validate  <--- The issue is `./validaton`
 4 |
   |

Cannot find module 'validaton'. Did you mean './validation'?
```

### Anatomy

```text
<severity> <key> (<code>):
at <file>:<line>:<col>:

   |
 N | <context line before>
 N | <error line>  <--- The issue is `<span>`
 N | <context line after>
   |

<message>. <help>
```

1. **Header** — `error <key> (<code>):` — key first (human scans left-to-right for meaning), code in parens for grep/filtering.
2. **Location** — `at file:line:col:` on its own line.
3. **Snippet** — gutter with line numbers on context lines (before, on, and after the error line). Blank lines and gutter bars above and below for breathing room.
4. **Arrow** — `<--- The issue is \`<span>\`` points at the error line and names the relevant chunk with a short prose phrase. Span text is truncated to ~40 characters with `...` if long. The arrow's job is spatial and human: "look here, at this thing."
5. **Message + help** — merged into one block at the bottom. The message explains what went wrong; the help says what to do about it. Redundancy with the arrow is intentional — they serve different reading modes (scanning vs understanding).

## Warnings

Same layout, different header word and color:

```text
warn unused-import (E008):
at ci.ops:4:1:

   |
 3 | @use ./validation as validate
 4 | @use std.log as log  <--- The issue is `std.log`
 5 |
   |

Import 'std.log' is never used. Remove the unused @use declaration.
```

## Multiple errors

Errors are separated by a blank line. A summary line follows the last error:

```text
error unresolved-import (E001):
at ci.ops:3:6:

   |
 2 | @use std.log as log
 3 | @use ./validaton as validate  <--- The issue is `./validaton`
 4 |
   |

Cannot find module 'validaton'. Did you mean './validation'?

error type-mismatch (E004):
at deploy.ops:12:24:

   |
11 | env = {
12 |   @port SERVER_PORT = "not-a-port"  <--- The issue is `"not-a-port"`
13 | }
   |

Expected @port, got @string. @port values must be integers between 1 and 65535.

2 errors emitted
```

## No-snippet errors

Some errors have no source location (e.g., global configuration issues):

```text
error missing-dependency (E006):

Required op 'infra.provision' is not installed. Check that the op is registered and available in your ops path.
```

## Runtime errors (user throws)

Runtime errors don't use the snippet + arrow format — the user intentionally wrote the throw. What matters is the message, help, and where the error traveled.

### Simple runtime error

```text
error deploy-failed (e001):

Deployment to beta failed after 3 attempts. Check the datacenter's health endpoint and retry, or use --allow-remote to skip.
```

### Runtime error with rethrow stack

Stack is top-down: most recent location first, origin last.

```text
error deploy-failed (e001):

Deployment to beta failed after 3 attempts. Check the datacenter's health endpoint and retry, or use --allow-remote to skip.

  at ci.ops:12:3 (rethrown)
  at deploy.ops:45:5 (thrown)
```

## Long span truncation

When the relevant span is longer than ~40 characters, the arrow description truncates:

```text
error syntax-error (E003):
at ci.ops:8:3:

   |
 7 | @release {
 8 |   some.very.deeply.nested.thing.that.goes.on.forever(arg1, arg2, arg3)  <--- The issue is `some.very.deeply.nested.thing.that...`
 9 | }
   |

Unexpected invocation in declarative block. Move this to an imperative block or a scripts block.
```

## Colors

| Element        | Color          |
|----------------|----------------|
| `error`        | red bold       |
| `warn`         | yellow bold    |
| key            | bold (default) |
| code in parens | dim            |
| location       | cyan           |
| gutter / pipes | dim            |
| arrow + span   | red (error) / yellow (warn) |
| message + help | default (message), blue (help) |

## Implementation

The shared renderer lives in `engine/errors/opx_error.lib.sh` and is the source of truth for terminal layout, catalog lookup, and color behavior. Rust callers use the `errors` crate to build the `@error`/diagnostic object, extract the source line plus one line of context, and shell out to `opx_error.lib.sh` to get the human-readable string. That keeps normal `Display` formatting and CLI output on the same rendering path the emitted POSIX runtime will use.

`--color` is passed as a preference (`auto`, `always`, or `never`), but the shell renderer owns the final color decision. Since Rust captures renderer stderr to return a string, it also passes whether the original stderr was a terminal so `auto` mode is not confused by the pipe used for capture.

## Design decisions

- **Key before code** — humans read the key; machines grep the code. Key-first optimizes for the common case.
- **`at` instead of `-->`** — reads as English, not compiler notation.
- **Arrow instead of underline** — `<--- The issue is \`span\`` names the problem location with a short human phrase. More informative than tildes/carets: it tells you *what* to look at, not just *where*. Kept short — the arrow is a pointer, not an explanation.
- **Context lines** — showing the line before and after the error gives spatial orientation without dumping half the file. Line numbers on all three lines reinforce where you are.
- **Message + help merged** — one block at the bottom reads as a natural thought: what's wrong, then what to do. No `= help:` prefix. Color distinguishes the help portion for terminals that support it.
- **Intentional redundancy** — the arrow and the message may overlap. That's a feature. The arrow serves quick scanning ("where?"), the message serves understanding ("what and why?"). Different entry points for the same information increase absorption when the reader is already confused.
- **No brackets around the code** — `(E001)` is lighter than `[E001]`.
- **Breathing room** — blank lines and gutter bars above/below the snippet prevent the diagnostic from feeling like a wall of text.

---

*Cross-reference: error object schema — [error-handling.md](../features/error-handling.md). Error codes and catalog — [error-catalog.md](./error-catalog.md). Machine-readable output — [machine-output.md](./machine-output.md).*
