terni 0.5.0

Ternary error handling: Success, Partial with measured loss, Failure. Because computation is not binary.
Documentation
# Pipeline

The `.eh()` method is the terni-functor bind. It chains operations and accumulates loss through the middle state.

## The bind

```rust
pub fn eh<U>(self, f: impl FnOnce(T) -> Imperfect<U, E, L>) -> Imperfect<U, E, L>
```

Takes a function from `T` to `Imperfect<U, E, L>`. Returns a new `Imperfect<U, E, L>` with loss accumulated.

## How loss accumulates

Four rules. No exceptions.

### Success x Success = Success

No loss on either side. The pipeline is perfect.

```rust
use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|x| Imperfect::Success(x + 1));

assert_eq!(result, Imperfect::Success(2));
```

### Success x Partial = Partial

The function introduced loss. It carries forward.

```rust
use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|x| Imperfect::Partial(x + 1, ConvergenceLoss::new(3)));

assert!(result.is_partial());
assert_eq!(result.loss().steps(), 3);
```

### Partial x Partial = Partial (combined)

Both sides had loss. Losses combine.

```rust
use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Partial(1, ConvergenceLoss::new(3))
    .eh(|x| Imperfect::Partial(x + 1, ConvergenceLoss::new(5)));

assert!(result.is_partial());
assert_eq!(result.loss().steps(), 5);  // max(3, 5) for ConvergenceLoss
```

### Anything x Failure = Failure (loss carried)

Failure short-circuits. If the input is `Failure(E, L)`, `f` is never called — the carried loss is preserved. If `f` returns `Failure`, prior loss is combined with the failure's loss — the value is gone, but the cost of getting here is measured.

```rust
use terni::{Imperfect, ConvergenceLoss};

// Failure input: f is never called, carried loss preserved
let result = Imperfect::<i32, String, ConvergenceLoss>::Failure("gone".into(), ConvergenceLoss::new(4))
    .eh(|x| Imperfect::Success(x + 1));

assert!(result.is_err());
assert_eq!(result.loss().steps(), 4);  // carried loss, not total()

// Partial then failure: losses combine
let result = Imperfect::<i32, String, ConvergenceLoss>::Partial(1, ConvergenceLoss::new(3))
    .eh(|_| Imperfect::<i32, String, ConvergenceLoss>::Failure("broke".into(), ConvergenceLoss::new(5)));

assert!(result.is_err());
assert_eq!(result.loss().steps(), 5);  // max(3, 5) for ConvergenceLoss
```

## Chaining

`.eh()` composes naturally. Each step sees the value from the previous step. Loss accumulates across the entire chain.

```rust
use terni::{Imperfect, ConvergenceLoss};

fn validate(input: &str) -> Imperfect<i32, String, ConvergenceLoss> {
    match input.parse::<i32>() {
        Ok(n) if n > 0 => Imperfect::Success(n),
        Ok(n) => Imperfect::Partial(n.abs(), ConvergenceLoss::new(1)),  // corrected sign
        Err(_) => Imperfect::Failure(format!("not a number: {}", input), ConvergenceLoss::zero()),
    }
}

fn normalize(n: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    if n > 100 {
        Imperfect::Partial(100, ConvergenceLoss::new(1))  // clamped
    } else {
        Imperfect::Success(n)
    }
}

fn score(n: i32) -> Imperfect<f64, String, ConvergenceLoss> {
    Imperfect::Success(n as f64 / 100.0)
}

// Full pipeline
let result = validate("-150")
    .eh(normalize)
    .eh(score);

assert!(result.is_partial());
assert_eq!(result.ok(), Some(1.0));
assert_eq!(result.loss().steps(), 1);  // max(1, 1) = 1 — sign corrected + clamped
```

## Recovery

`.recover()` attempts to salvage a value from `Failure`. Recovery from `Failure` always produces `Partial` — the failure happened, and that cost is carried forward.

```rust
use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|_| Imperfect::<i32, String, ConvergenceLoss>::Failure("broke".into(), ConvergenceLoss::new(3)))
    .recover(|_e| Imperfect::Success(0));

assert!(result.is_partial());  // never Success — the failure was real
assert_eq!(result.ok(), Some(0));
assert_eq!(result.loss().steps(), 3);  // cost survives
```

`.unwrap_or()` and `.unwrap_or_else()` are shorthand for recovery with a default:

```rust
use terni::{Imperfect, ConvergenceLoss};

let failed: Imperfect<i32, String, ConvergenceLoss> =
    Imperfect::Failure("gone".into(), ConvergenceLoss::new(5));

let recovered = failed.unwrap_or(0);
assert!(recovered.is_partial());
assert_eq!(recovered.ok(), Some(0));
assert_eq!(recovered.loss().steps(), 5);
```

Success and Partial pass through `.recover()`, `.unwrap_or()`, and `.unwrap_or_else()` unchanged.

## Aliases

`.imp()` and `.tri()` are identical to `.eh()`. Same function, different name.

- **`.eh()`** — the shrug. Short, informal, gets the point across.
- **`.imp()`** — the word. Self-documenting in code that reads like prose.
- **`.tri()`** — the math. For code where "terni-functor" is the right frame.

Use whichever makes your code clearest. They compile to the same thing.

```rust
use terni::{Imperfect, ConvergenceLoss};

// All three are identical
let a = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|x| Imperfect::Success(x + 1));
let b = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .imp(|x| Imperfect::Success(x + 1));
let c = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .tri(|x| Imperfect::Success(x + 1));

assert_eq!(a, b);
assert_eq!(b, c);
```

[Back to README](../README.md) · [Context →](context.md)