errlanes 0.18.1

Four error lanes (Rejected/Denied/Transient/Fatal) carried in the type, not re-derived at every layer
Documentation
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
# errlanes

Errors communicate that something went wrong up the call stack. What went wrong
can vary widely: a caller supplied an invalid amount, a user lacks permission,
a database transaction hit a deadlock, or the application encountered corrupt
state. Each calls for a different response, often from a different part of the
application. Representing that information so callers can decide what to do
becomes harder as errors pass through several layers.

This crate takes an opinionated approach: categorize errors into four *lanes*,
each with a distinct meaning for the caller. The layer that understands an error
assigns its lane; callers can then handle or propagate it without having to
interpret the original error again.

The lanes are represented by two carrier enums. Their structure looks like this:

```rust
use errlanes::{Denied, Fatal, Transient};

enum Fault {
    Denied(Denied),       // The caller is not authorized.
    Transient(Transient), // Retrying the operation may succeed.
    Fatal(Fatal),         // Operator attention is needed.
}

enum Fail<R> {
    Rejected(R),          // A domain outcome the caller can act on.
    Denied(Denied),
    Transient(Transient),
    Fatal(Fatal),
}
```

`Fault` carries the three outcomes whose handling does not depend on a domain
error type. `Fail` adds Rejected, carrying an application-defined type `R` so
callers can distinguish individual domain cases. Both preserve the underlying
source for diagnosis.

Which carrier a function returns is itself information: a `Fault` says that
nothing about this call is the caller's to correct.

## Faults and lane selection

A particular operation will usually produce only some of these outcomes.
For example, a storage function might return Transient or Fatal but have no
reason to deny access. Its signature can express that with
`Fault<lanes!(Transient, Fatal)>`.

The `lanes!` macro selects which fault lanes a type permits. That selection
tells callers what they need to handle. It can change as an error travels up
the call stack: a caller may introduce an additional outcome or take
responsibility for handling one.

A disabled lane is uninhabited, so no value can ever occupy it. A by-value
match therefore names exactly the lanes that were selected:

```rust
use errlanes::{Fault, lanes};

fn describe(failure: Fault<lanes!(Transient, Fatal)>) -> &'static str {
    match failure {
        Fault::Transient(_) => "worth another attempt",
        Fault::Fatal(_) => "needs a human",
    }
}

// With a single lane left, there is nothing to match at all.
fn unwrap_fatal(failure: Fault<lanes!(Fatal)>) -> errlanes::Fatal {
    let Fault::Fatal(fatal) = failure;
    fatal
}
# let _ = describe(errlanes::Transient::new(errlanes::TransientKind::Deadlock).into());
# let _ = unwrap_fatal(errlanes::Fatal::invariant("x").into());
```

### Example: Authorization adds a lane

Consider an inner operation that can only fail fatally. An outer function
checks whether the subject is allowed to perform it before calling it. The
outer function can therefore return Denied as well as Fatal:

```rust
use errlanes::{Denied, Fault, ResultExt, lanes};

fn authorize(subject: &str) -> Result<(), Denied> {
    if subject == "admin" { Ok(()) } else { Err(Denied::default()) }
}

fn inner() -> Result<u64, Fault<lanes!(Fatal)>> {
    Ok(42)
}

fn outer(subject: &str) -> Result<u64, Fault<lanes!(Denied, Fatal)>> {
    authorize(subject)?;
    let value = inner()?;
    Ok(value)
}

assert_eq!(outer("admin").unwrap(), 42);
assert!(matches!(outer("guest"), Err(Fault::Denied(_))));
```

The authorization failure enters the Denied lane through `?`. The inner
operation's result uses `?` to fit the outer function's larger set of
lanes. An inner Fatal remains Fatal, with its kind, context and
source intact.

### Example: Retrying handles a lane

The reverse situation occurs when an inner operation can return Transient,
but its caller owns retrying the operation. After handling Transient, that
caller only needs to expose Fatal to its own callers. `narrow_transient` turns
the attempt count into an outcome: a transient that never succeeded becomes
`Fatal(Exhausted)`, keeping the last transient as its source.

```rust
use errlanes::{Fault, Transient, TransientKind, lanes};

const BUDGET: u32 = 3;

fn execute(
    mut attempt_once: impl FnMut() -> Result<u64, Fault<lanes!(Transient, Fatal)>>,
) -> Result<u64, Fault<lanes!(Fatal)>> {
    let mut attempts = 0;
    loop {
        attempts += 1;
        match attempt_once() {
            Ok(value) => return Ok(value),
            Err(failure) if failure.is_transient() && attempts < BUDGET => continue,
            Err(failure) => return Err(failure.narrow_transient(attempts)),
        }
    }
}

// A transient that later succeeds never reaches the caller.
let mut attempts = 0;
let value = execute(|| {
    attempts += 1;
    if attempts == 1 {
        Err(Transient::new(TransientKind::Deadlock).into())
    } else {
        Ok(42)
    }
});
assert_eq!(value.unwrap(), 42);
assert_eq!(attempts, 2);

// One that never succeeds is spent, and says so.
let spent = execute(|| Err(Transient::new(TransientKind::Deadlock).into())).unwrap_err();
let Fault::Fatal(fatal) = spent;
assert_eq!(fatal.kind, errlanes::FatalKind::Exhausted);
```

Here, `execute` takes the inner operation as a closure so it can call it again.
Its return type omits Transient because the loop handles that case: narrowing
consumed the lane, so `lanes!(Transient, Fatal)` went in and `lanes!(Fatal)`
came out.

## Narrowing a lane

As in the retry example, any lane can be narrowed once its owner has handled
it. `narrow_<lane>` removes exactly one lane and turns its value into a
`Fatal`, the only lane left once nobody can act on the removed one. A
narrowing is always a method call, never a `From`, so `?` cannot drop a lane
by accident. The same methods are available on a `Result` through `ResultExt`.

| narrowed lane | what the value is afterwards | method | `FatalKind` |
|---|---|---|---|
| `Transient` | a transient that ran out of retries | `narrow_transient(attempts)` | `Exhausted` |
| `Denied` | still a denial, fatal only because nobody can be told | `narrow_denied()` | `Denied` |
| `Rejected(D)` | a caller-correctable outcome with no caller, which is a bug | `narrow_rejected()` | `Invariant` |

## Handling the Rejected lane

A rejection is a domain outcome the caller can act on, so it is usually
handled rather than narrowed. `rejected()` hands the caller the rejection as
a value: `Result<T, Fail<D, L>>` becomes `Result<Result<T, D>, Fault<L>>`.
The outer `?` keeps the faults propagating; the inner `Result` is the domain
outcome, matched on the spot.

```rust
use errlanes::{Fail, Fault, ResultExt, lanes};

#[derive(Debug, errlanes::Rejection)]
#[rejection(code = "TIMED_OUT")]
struct TimedOut;

fn await_completion() -> Result<u64, Fail<TimedOut, lanes!(Transient, Fatal)>> {
    Err(Fail::Rejected(TimedOut))
}

fn poll_once() -> Result<Option<u64>, Fault<lanes!(Transient, Fatal)>> {
    match await_completion().rejected()? {
        Ok(outcome) => Ok(Some(outcome)),
        Err(TimedOut) => Ok(None),
    }
}

assert!(poll_once().unwrap().is_none());
```

`narrow_rejected()` is for the opposite situation: a frame that has already
proved the precondition has no caller left to correct the rejection, so the
rejection is an invariant there.

### Rejections across domain boundaries

As an error moves up the call stack, it may cross into a domain with its own
rejection enum. The fault lanes already have shared meanings, but two domain
enums need an explicit conversion. `derive(Lift)` generates it: the enum-level
`#[lift(Validation)]` names the source, and an annotation on each destination
variant says which source case it represents:

```rust
use errlanes::{Fail, Rejection, ResultExt, lanes};

#[derive(Debug, errlanes::Rejection)]
enum Validation {
    #[rejection(code = "INVALID_AMOUNT")]
    InvalidAmount,
}

#[derive(Debug, errlanes::Rejection, errlanes::Lift)]
#[lift(Validation)]
enum Payment {
    #[lift(Validation::InvalidAmount)]
    AmountNotPositive,
}

fn validate(amount: u64) -> Result<(), Validation> {
    if amount == 0 { Err(Validation::InvalidAmount) } else { Ok(()) }
}

fn outer() -> Result<(), Fail<Payment, lanes!(Denied, Fatal)>> {
    validate(0)?;
    Ok(())
}

let rejection = outer().unwrap_err().rejected().unwrap();
assert!(matches!(rejection, Payment::AmountNotPositive));
assert_eq!(Into::<&'static str>::into(rejection.code()), "INVALID_AMOUNT");
```

By default a lift is exhaustive. The derive emits `From<Validation>`, so `?`
places the rejection in the outer Rejected lane, and if `Validation` gains a
case the mapping must be updated before it compiles. Unit, tuple and named
payloads forward automatically; `#[lift(Source::Variant, with = mapper)]`
transforms a payload, `into` converts it through `Into`, and `field = name`
keeps one named field of it. A plain lift arm also forwards the source case's
code and level, which is why `AmountNotPositive` above still reports
`INVALID_AMOUNT`; a `with`, `into` or `field` arm declares its own
`#[rejection(code = "...")]` instead, or `#[rejection(delegate)]` to take
them from the converted payload.

Sometimes only a subset of the source cases makes sense in the destination
domain. `#[lift(Source, unhandled = fatal)]` allows a partial mapping: an
unmapped rejection becomes `Fatal(Invariant)` with the original as its source,
so the destination must enable Fatal and the call site is `.lift()?` rather
than `?`. A named-field struct can select one source variant this way with
`variant = Case`, renaming fields with `#[lift(from = source_field)]`:

```rust
use errlanes::{Fail, ResultExt, lanes};
use std::time::Duration;

#[derive(Debug, errlanes::Rejection)]
enum SubscriptionRejection {
    CaughtUpTimeout { checkpoint: u64, target: u64, waited: Duration },
    NoSuchJob { key: String },
}

#[derive(Debug, errlanes::Rejection, errlanes::Lift)]
#[rejection(code = "EC_CAUGHT_UP_TIMEOUT")]
#[error("checkpoint {applied} had not reached {frontier} after {waited:?}")]
#[lift(SubscriptionRejection, variant = CaughtUpTimeout, unhandled = fatal)]
struct EcCaughtUpTimeout {
    #[lift(from = checkpoint)]
    applied: u64,
    #[lift(from = target)]
    frontier: u64,
    waited: Duration,
}

let result: Result<(), Fail<EcCaughtUpTimeout, lanes!(Fatal)>> =
    Err(SubscriptionRejection::CaughtUpTimeout {
        checkpoint: 3,
        target: 8,
        waited: Duration::from_secs(2),
    }).lift();
let Fail::Rejected(timeout) = result.unwrap_err() else { panic!("expected timeout") };
assert_eq!((timeout.applied, timeout.frontier), (3, 8));
```

A struct source lifts as a whole value: `#[lift(Payload)]` with no variant
suffix, on both the enum and the variant that receives it.

## Errors from other crates

Every local error type says, through one trait, how it enters the lanes:

```rust,ignore
pub trait Classify: Error + Send + Sync + 'static {
    type Rejected: RejectedSlot; // a `Rejection`, or `Infallible` if nothing is ever rejected
    type Lanes: LaneProfile;     // the narrowest profile the fault part can occupy
    fn classify(self) -> Fail<Self::Rejected, Self::Lanes>;
}
```

A `Rejection` gets `Classify` from a blanket impl, so a pure domain outcome
needs only `#[derive(errlanes::Rejection)]`. Everything else is written with
`#[derive(errlanes::Classify)]`, and a type is one or the other, never both.

`sqlx::Error`, `serde_json::Error`, and anything else outside this crate never
implements `Classify` directly — errlanes cannot know what a `404` from one
caller's upstream means versus another's. The fix is the same one always: wrap
it in a local type, and give *that* a lane. A one-off call site does this with
`.classify::<W>()`, which turns a `Result<T, Foreign>` into a `Result<T, W>`:

```rust
use errlanes::{FatalKind, ResultExt};

#[derive(Debug, errlanes::Classify)]
#[classify(fatal(CorruptState), from)]
struct Stored(std::io::Error);

fn decode() -> Result<u8, std::io::Error> { Err(std::io::Error::other("x")) }

let wrapped: Result<u8, Stored> = decode().classify::<Stored>();
assert_eq!(wrapped.unwrap_err().0.to_string(), "x");
```

A function whose own error type already implements `Classify` gets this for
free through `From`, with bare `?` — no `.classify()` needed at the call site:

```rust,ignore
mod price_feed {
    // foreign → wrapper by `?` (the `from` flag's `From` impl)
    pub fn spot() -> Result<Quote, PriceFeed> {
        let response = client.get(url).send()?;
        Ok(response.json()?)
    }
}
```

**Same foreign error, two crates, two meanings.** A third-party price feed's
`404` on a known route is a domain outcome; its timeouts and 5xx retry; its
`401`/`403` are *our* credentials, not the caller's, so they are narrowed.
A different crate wrapping the exact same `reqwest::Error` from an in-house
service can decide every failure there is a deployment problem instead:

```rust,ignore
#[derive(Debug, errlanes::Rejection)]
pub enum PriceFeedRejection {
    #[rejection(code = "PRICE_FEED_UNKNOWN_SYMBOL")]
    UnknownSymbol(String),
}

#[derive(Debug, errlanes::Classify)]
pub enum PriceFeed {
    #[classify(delegate)]                 Rejected(PriceFeedRejection),
    #[classify(delegate, narrow(Denied))]  Http(reqwest::Error),
}
impl From<reqwest::Error> for PriceFeed { /* 404 on /symbols/{s} -> Rejected(UnknownSymbol(s)), else Http(e) */ }

#[derive(Debug, errlanes::Classify)]
#[classify(fatal(Config), from)]
pub struct FxRates(reqwest::Error); // our own service: any failure is ours

async fn quote() -> Result<Quote, Fail<QuoteRejection, lanes!(Transient, Fatal)>> {
    let spot = feed.get(url).send().await.classify::<PriceFeed>()?;  // lifts PriceFeedRejection
    let rate = rates.get(url).send().await.classify::<FxRates>()?;   // always Fatal(Config)
    Ok(spot * rate)
}
```

`delegate` forwards to the field's own `Classify`; `narrow(Denied)` then
removes just the denied lane (an upstream `401`/`403` becomes `Fatal(Denied)`
instead) before the arm's contribution is folded into the enclosing type's
`Lanes`. `Rejected` and `Lanes` are always inferred this way — never named on
the derive. The payload is the variant's only field, or — among several named
fields — the one marked `#[source]`, or named `source`. `from` additionally
needs the payload to be the only field, since `From` has nothing to fill
siblings with.

**`classify-sqlx`, `classify-serde-json`, and `classify-reqwest`** are
`impl Classify` for the three foreign types errlanes blesses on your behalf —
always `Rejected = Infallible`, the narrowest `Lanes` each warrants (see the
module docs for the exact table). With the feature enabled, bare `?` works on
the foreign type directly, and a wrapper's `delegate` arm can name it like any
other `Classify` payload. `classify-reqwest`'s `Denied` means *the subject of
this call* is unauthorized: an upstream `401`/`403` returned to a service
account is usually a credential or configuration fault at *our* layer, not the
caller's — narrow it with `#[classify(delegate, narrow(Denied))]` (→
`Fatal(Denied)`), or match the status in a hand-written wrapper to get
`Fatal(Config)` instead. A proxy forwarding the caller's own token to upstream
is the case that keeps `Denied` as-is.

## At a `Box<dyn Error>` boundary

A `Classify` wrapper carries its lane in its
`impl Classify`, not in the value, so it must reach a carrier *before* it is
boxed; a boundary reading the box afterwards would only see the foreign error
underneath it. `.into_fault()?` lands a never-rejecting wrapper in its own
`Fault<W::Lanes>`, and `.into_fail()?` lands a rejecting one in its own
`Fail<W::Rejected, W::Lanes>`, so nothing has to be named even though `?`
into a box leaves the destination unconstrained. Raw foreign errors and
carriers need no such step. On the way back out, `Fault::classify(&*boxed)`
borrows the error and returns the first lane payload or blessed foreign error
in its `source()` chain, or `Fatal(Dependency)` if it finds neither. A
rejection cannot cross a box as a rejection: handle or narrow it first.

```rust
use errlanes::{Fault, ResultExt};

#[derive(Debug, errlanes::Classify)]
#[classify(fatal(CorruptState), from)]
#[error("could not decode a stored row")]
struct StoredRow(#[source] std::io::Error);

fn decode() -> Result<u8, StoredRow> {
    Err(StoredRow(std::io::Error::other("bad bytes")))
}

// Inside a boundary whose own trait returns a box:
fn run() -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
    Ok(decode().into_fault()?)
}

let fault = Fault::classify(&*run().unwrap_err());
assert!(matches!(fault, Fault::Fatal(f) if f.kind == errlanes::FatalKind::CorruptState));
```

## Moving between signatures

A failure changes shape as it travels: the lane set grows when a caller can
produce outcomes the callee could not, and the rejection type changes when a
failure crosses into a domain with its own vocabulary. The appropriate
conversion depends on what changes:

| From | To | Use |
|---|---|---|
| the same error type | itself | `?` |
| a lane payload (`Transient`, `Fatal`, `Denied`) | a built-in or carrier enabling it | `?` |
| `Fault<S>` | `Fault<D>` or `Fail<R, D>`, `S ⊆ D` | `?` |
| `Fail<R, S>` | `Fail<R, D>`, `S ⊆ D` | `?` |
| a bare rejection `C` | `Fail<R, D>`, given a total `R: From<C>` | `?` |
| `W: Classify` | `Fail<R, D>`, given a total `R: From<W::Rejected>` and `W::Lanes ⊆ D` | `?` |
| `W: Classify<Rejected = Infallible>` | `Fault<D>`, `W::Lanes ⊆ D` | `?` |
| a rejecting result (`Fail`, bare rejection, wrapper, carrier) | `Fail<P, D>` or a `Fail`-like carrier, with `Fatal` enabled and a total or partial `P: Lift<R>` | `.lift()?` |
| a never-rejecting result | its own `Fault<W::Lanes>` at a box or foreign carrier boundary | `.into_fault()?` |
| a rejecting result | its own `Fail<R, W::Lanes>` at a foreign carrier boundary | `.into_fail()?` |
| `Result<T, Foreign>` | `Result<T, W>` | `.classify::<W>()`, given `W: Classify + From<Foreign>` |
| `Result<T, Fail<R, L>>` | `Result<T, Fault<L>>` | `.narrow_rejected()` |
| `Result<T, R>`, a bare rejection | `Result<T, Fatal>` after the precondition is proven | `.narrow_rejected()?` |
| `Result<T, Fail<R, L>>` | `Result<Result<T, R>, Fault<L>>` | `.rejected()` |
| `Result<T, Fail<R, L>>` | `Result<T, Fail<P, L>>` | `.map_rejected(f)` for call-site data |
| a `Fault` or `Fail` result | lanes narrowed | `.narrow_transient(attempts)` or `.narrow_denied()` |

`?` expands lanes without dropping one. It also carries a *total* mapping from
a bare rejection or `Classify` wrapper into `Fail`; a partial mapping requires
`.lift()?`. A `Fail<R, S>` changing its rejection to `P` also uses `.lift()?`,
whether the lift is total or partial. `ResultExt::lift` enables `Fatal` before
the following `?`, so an unmapped case has a place to go. Consequently its
destination must enable `Fatal`. For a total `Fail` lift into a signature
without `Fatal`, use the value-level `Fail::lift::<P, D>` explicitly. Narrowing
removes a lane by name after its outcome has been handled.

## Carriers: your own `Fault` / `Fail`

`Fault<L>` and `Fail<R, L>` are one type per profile, so two crates that both
return `Fault<lanes!(Transient, Fatal)>` share it, and a `thiserror` enum that
wants `#[from]` for both collides. A **carrier** is a crate-local type that
stands in for one of them:

```rust
# use errlanes::Rejection;
# #[derive(Debug, errlanes::Rejection)]
# pub enum CustomerRejection { Closed }
#[derive(Debug, errlanes::Carrier)]
pub enum HostFault {
    Transient(errlanes::Transient),
    Fatal(errlanes::Fatal),
}

#[derive(Debug, errlanes::Carrier)]
pub enum CustomerError {
    Rejected(CustomerRejection),
    Transient(errlanes::Transient),
    Fatal(errlanes::Fatal),
}

#[derive(Debug, errlanes::Carrier)]
pub enum RepoWriteError<R> {
    Rejected(R),
    Transient(errlanes::Transient),
    Fatal(errlanes::Fatal),
}

#[derive(Debug, errlanes::Carrier)]
#[carrier(from(HostFault))]
pub enum PartyFault {
    Denied(errlanes::Denied),
    Transient(errlanes::Transient),
    Fatal(errlanes::Fatal),
}
```

Each is an **enum with exactly the declared lanes**, so it matches like the
built-in, and a lane it does not declare is not a variant at all:

```rust
# #[derive(Debug, errlanes::Carrier)]
# pub enum HostFault {
#     Transient(errlanes::Transient),
#     Fatal(errlanes::Fatal),
# }
fn page(e: HostFault) -> &'static str {
    match e {
        HostFault::Transient(_) => "retry",
        HostFault::Fatal(_) => "page",
    }
}
# assert_eq!(page(HostFault::Fatal(errlanes::Fatal::invariant("x"))), "page");
```

What `?` does, in and out of a carrier `E`:

- **In:** any lane payload `E` declares; a `Fault<S>` with `S ⊆ E`'s lanes; for
  a `Fail`-like carrier, a `Fail<R, S>` with the same rejection; any
  `Classify` wrapper whose lanes fit; a bare `Rejection` that lifts totally
  (`Fail`-like only); and each carrier listed in `from(..)`.
- **Out:** into `Fault<M>` (`Fault`-like carriers, `S ⊆ M`), into `Fail<D, M>`, into
  a bare `Fatal` / `Transient` when it has just that lane, and into a foreign
  enum with `#[from] E`.
- `.lift()`, `into_fault`, `into_fail`, `narrow_transient`, `narrow_denied`, `narrow_rejected`,
  `rejected` and `map_rejected` work on `Result<T, E>`. A narrowing returns the
  narrowed **built-in** (`Fault<..>` / `Fail<..>`), which `?` carries on.
  `E: Laned` for retry, `record` and `#[errlanes::instrument]`.

**Carrier to carrier is not automatic.** `?` is `From::from`, and a blanket
`impl From<AnyCarrier> for E` would also cover `E` itself, which overlaps the
standard library's reflexive `impl<T> From<T> for T`. So the conversion you
want is listed: `#[carrier(from(HostFault))]` adds `impl From<HostFault> for E`. The list
only works for carriers declared in the *same crate* as `E` (see below). The
list-free route works for any carrier, in any crate: hand it over as its
built-in, and the outer `?` absorbs that.

```rust
# use errlanes::{ResultExt};
# #[derive(Debug, errlanes::Carrier)]
# pub enum HostFault {
#     Transient(errlanes::Transient),
#     Fatal(errlanes::Fatal),
# }
# #[derive(Debug, errlanes::Carrier)]
# pub enum PartyFault {
#     Denied(errlanes::Denied),
#     Transient(errlanes::Transient),
#     Fatal(errlanes::Fatal),
# }
fn host() -> Result<u8, HostFault> {
    Ok(1)
}

fn party() -> Result<u8, PartyFault> {
    let v = host().into_fault()?;
    Ok(v)
}
# assert_eq!(party().unwrap(), 1);
```

The variant names (`Rejected`, `Denied`, `Transient`, `Fatal`) are the
profile: each is a one-field tuple variant, at most one of each, and
`Rejected(T)` makes the carrier `Fail`-like. Variants may carry doc comments,
and `Debug` is yours to derive.

**Limitation.** `from(..)` cannot list a carrier declared in another crate,
and neither can a hand-written `impl From<OtherCrate::Carrier> for E`: the
carrier's one blanket inbound `From` overlaps it, because rustc cannot know
the other crate's carrier is not a plain lane source. Use
`.into_fault()?` or `.into_fail()?` across crates, and in generic code over a foreign
carrier, bound on its built-in (`E: From<Fault<lanes!(Transient, Fatal)>>`)
rather than on the carrier.

## Composing rejection families

An outer domain may also want to expose every case from an inner rejection
family, including cases added in the future. In that situation, there is no
individual mapping decision to make for each case.
`#[errlanes::compose(..)]` expresses that relationship directly. It takes the
source families to import and generates both `Rejection` and `Lift` for the
composed enum.

A source listed as `Source as Prefix` imports every one of its cases under a
prefixed name:

```rust
use errlanes::Rejection;

#[derive(Debug, errlanes::Rejection)]
pub enum Validation {
    #[rejection(code = "INVALID_AMOUNT")]
    InvalidAmount,
}

#[errlanes::compose(Validation as Amount)]
#[derive(Debug)]
pub enum Payment {
    WindowClosed,
}

let payment = Payment::from(Validation::InvalidAmount);
assert!(matches!(payment, Payment::AmountInvalidAmount));
assert_eq!(Into::<&'static str>::into(payment.code()), "INVALID_AMOUNT");
```

`Validation as Amount` expands into real variants such as
`AmountInvalidAmount`. The imported cases keep their payloads, codes, levels,
formatting, and sources. The attribute supplies both `Rejection` and `Lift`,
including total `From` conversions, so the resulting family supports
`?` for bare rejections and `.lift()?` for failures, and it can sit alongside
explicit lifts from other sources.

Composition includes new source cases automatically. Explicit lifts are
useful when each addition needs review or individual cases need different
names. When an outer layer adds no semantics of its own, it can simply reuse
the inner rejection type.