1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# CI loading: rename and re-shape errors

**Goal:** Rationalize the names and error categories around CI loading so the structure matches how the code actually thinks. No behavior changes.

## What's wrong now

Reading `src/ci/{mod,pipeline,lua}.rs` and `src/error.rs` back, three things feel arbitrary:

1. **"load" is overloaded.** `Ci::load`, `Pipeline::load`, and `LoadError` all use the word but mean different things. The overloading is triggered by the error name — once that's gone, the methods read fine.
2. **`ValidationError` is a grab bag.** Variants come from two distinct stages — registration-time rules (`ReservedSlash`, `EmptyInputs`, `DuplicateImage`) and post-graph rules (`Cycle`, `Unreachable`) — but share one enum, one suffix, and no structure to signal the difference.
3. **`Error::Validation` names a stage, not a domain.** Compare with the sibling `Error::Fennel`, which names its source. "Validation failed" reads weirdly next to "fennel error."

## New shape

### Two error categories

The split that already exists in the code (per the comment in `validate_post_graph`) is "caught at registration time" vs. "caught after the graph is built." Make that explicit:

```rust
pub enum DefinitionError {
    ReservedSlash { job_id: String, span: SourceSpan },
    EmptyInputs   { job_id: String, span: SourceSpan },
    DuplicateImage { span: SourceSpan },
}

pub enum StructureError {
    Cycle       { cycle_jobs: Vec<String>, spans: Vec<SourceSpan> },
    Unreachable { job_id: String, span: SourceSpan },
}

pub enum Diagnostic {
    Definition(DefinitionError),
    Structure(StructureError),
}
```

The per-job vs. pipeline-singleton distinction (DuplicateImage is the lone singleton today) is an implementation detail of detection, not a user-visible category, so two buckets is the right count.

### Error bag and top-level variant

```rust
pub struct PipelineError {
    pub src: NamedSource<String>,
    pub diagnostics: Vec<Diagnostic>,
}
```

At the top level: `Error::Pipeline(Box<PipelineError>)`. Replaces `Error::Validation(Box<LoadError>)`.

### Method and module renames

| Old | New | Reason |
|---|---|---|
| `Ci::load(commit)` | `Ci::pipeline(commit)` | Returns the pipeline at a commit; noun-style accessor reads more naturally than "load". |
| `Pipeline::load(src, name)` (method) | `pipeline::compile(src, name)` (free fn) | Source → runnable artifact is compilation. Free fn avoids "the type compiles itself." |
| `lua::parse(...)` | `lua::register(...)` | Phase naming. Fennel's actual parser runs inside `eval_raw`; this function performs the registration phase. |
| `ParseOutput` | `Registrations` | Names what came out, not what step ran. |
| `ValidationError` | split into `DefinitionError` + `StructureError` (see above) | Stage-aligned. |
| `LoadError` | `PipelineError` | Domain-named bag. |
| `Error::Validation` | `Error::Pipeline` | Domain-named, matches the bag. |

The verb chain reads top-down:

```
Ci::pipeline → pipeline::compile → lua::register
```

Each verb describes its own layer. "Register" is imperfect (the Fennel script does the registering; the Rust function provides the sinks and harvests), but it names the phase clearly and avoids "evaluate" — which would collide with Lua's eval terminology.

## Scope

In-scope:

- Renames listed above
- Splitting the validation enum into two
- Adding the `Diagnostic` wrapper for miette's `#[related]` iteration
- Updating tests in `src/ci/pipeline.rs`, `src/ci/lua.rs`, `src/ci/mod.rs`, and `src/error.rs`
- Updating call sites — at minimum `src/bin/quire/commands/ci.rs`

Out of scope:

- Behavioral changes to validation rules
- Merging `Error::Fennel` and `Error::Pipeline` (the user-facing "your ci.fnl is bad" framing wasn't a concern in this pass)
- Restructuring `lua.rs` runtime types (`Runtime`, `RuntimeHandle`, `ShOutput`)

## Verification

- `cargo test` passes with no behavior changes
- Error rendering for a multi-error ci.fnl still produces the same miette output (modulo the type names in `Debug`)
- No public API used outside `src/ci/` and `src/bin/` should change names

## Open questions

- "Register" is acceptable but not loved. If a better verb surfaces during implementation, revisit before committing.