Skip to content

Commit 59caf38

Browse files
Seamclaude
andcommitted
πŸ”§ final review: v0.5.0 β€” clippy clean, fmt clean, doc fix, version bump
- Suppressed intentional clippy::bind_instead_of_map in benchmarks (and_then chains are deliberate Result baselines, not candidates for map) - Fixed "EhContext" -> "Eh" in benchmarks.md (type was renamed) - Applied cargo fmt across benches and lib.rs - Bumped version 0.4.1 -> 0.5.0 - Included pending README updates (constructors section, new doc links) - Included pending loss-types.md stdlib Loss documentation - Included pending flight-recorder.md navigation link WARN: Collection Loss impls (Vec, HashSet, BTreeSet, String) have total() == zero() β€” the absorbing element property does not hold. Documented in code comments and loss-types.md. Monoid identity + associativity are sound. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 71360ab commit 59caf38

7 files changed

Lines changed: 78 additions & 21 deletions

File tree

β€ŽCargo.tomlβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "terni"
3-
version = "0.4.1"
3+
version = "0.5.0"
44
edition = "2021"
55
license = "Apache-2.0"
66
description = "Ternary error handling: Success, Partial with measured loss, Failure. Because computation is not binary."

β€ŽREADME.mdβ€Ž

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Ternary error handling for Rust. Because computation is not binary.
88
[![docs.rs](https://docs.rs/terni/badge.svg)](https://docs.rs/terni)
99
[![license](https://img.shields.io/crates/l/terni.svg)](https://github.com/systemic-engineering/prism/blob/main/imperfect/LICENSE)
1010

11+
**The cost of honesty is 0.65 nanoseconds per step, only when there's something to be honest about. Otherwise: zero.**
12+
1113
## `eh`
1214

1315
The type. Three states instead of two.
@@ -47,6 +49,21 @@ Three loss types ship with the crate:
4749

4850
The two empty cells on the left are the argument. `Result` doesn't have a row for partial success or honest failure. That's why terni exists.
4951

52+
### Constructors
53+
54+
Four ways to build an `Imperfect`:
55+
56+
```rust
57+
use terni::{Imperfect, ConvergenceLoss};
58+
59+
let a = Imperfect::<i32, String, ConvergenceLoss>::success(42);
60+
let b = Imperfect::<i32, String, ConvergenceLoss>::partial(42, ConvergenceLoss::new(3));
61+
let c = Imperfect::<i32, String, ConvergenceLoss>::failure("gone".into());
62+
let d = Imperfect::<i32, String, ConvergenceLoss>::failure_with_loss("gone".into(), ConvergenceLoss::new(5));
63+
```
64+
65+
`.failure()` carries zero loss. `.failure_with_loss()` carries accumulated loss from prior steps.
66+
5067
[Loss types in depth β†’](docs/loss-types.md) Β· [Full migration guide β†’](docs/migration.md)
5168

5269
## `eh!`
@@ -104,11 +121,13 @@ Block macro for implicit loss accumulation β€” `eh! { }` will do what `Eh` does
104121

105122
## More
106123

107-
- [Loss types](docs/loss-types.md) β€” the `Loss` trait, shipped types, custom implementations
124+
- [Loss types](docs/loss-types.md) β€” the `Loss` trait, shipped types, stdlib impls, custom implementations
108125
- [Pipeline](docs/pipeline.md) β€” `.eh()` bind in depth, loss accumulation rules
109126
- [Context](docs/context.md) β€” `Eh` struct, mixing `Imperfect` and `Result`
110127
- [Terni-functor](docs/terni-functor.md) β€” the math behind `.eh()`
111128
- [Migration](docs/migration.md) β€” moving from `Result<T, E>` to `Imperfect<T, E, L>`
129+
- [Flight recorder](docs/flight-recorder.md) β€” `Failure(E, L)` as production telemetry, not stack traces
130+
- [Benchmarks](docs/benchmarks.md) β€” 0.65 ns per honest step, zero on the success path
112131

113132
## License
114133

β€Žbenches/imperfect.rsβ€Ž

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(clippy::bind_instead_of_map)]
12
//! Benchmarks for terni β€” the cost of honesty, measured in nanoseconds.
23
//!
34
//! The thesis: `.eh()` on the Success path is zero-cost compared to Result's
@@ -74,8 +75,7 @@ fn bench_eh_pipeline(c: &mut Criterion) {
7475
// 10 chained .eh() on Success β€” should be within noise of Result
7576
group.bench_function("Imperfect::eh_success", |b| {
7677
b.iter(|| {
77-
let i: Imperfect<i32, String, ConvergenceLoss> =
78-
Imperfect::Success(black_box(1));
78+
let i: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(1));
7979
i.eh(|x| Imperfect::Success(x + 1))
8080
.eh(|x| Imperfect::Success(x + 1))
8181
.eh(|x| Imperfect::Success(x + 1))
@@ -173,8 +173,7 @@ fn bench_eh_partial(c: &mut Criterion) {
173173
// Mixed: starts Success, hits Partial midway. Loss appears at step 5.
174174
group.bench_function("Imperfect::eh_mixed_partial_at_5", |b| {
175175
b.iter(|| {
176-
let i: Imperfect<i32, String, ConvergenceLoss> =
177-
Imperfect::Success(black_box(1));
176+
let i: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(1));
178177
i.eh(|x| Imperfect::Success(x + 1))
179178
.eh(|x| Imperfect::Success(x + 1))
180179
.eh(|x| Imperfect::Success(x + 1))
@@ -216,8 +215,7 @@ fn bench_recover(c: &mut Criterion) {
216215
// Recovery from success (passthrough β€” no work)
217216
group.bench_function("Imperfect::recover_noop", |b| {
218217
b.iter(|| {
219-
let i: Imperfect<i32, String, ConvergenceLoss> =
220-
Imperfect::Success(black_box(42));
218+
let i: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(42));
221219
i.recover(|_e| Imperfect::Success(0))
222220
})
223221
});
@@ -241,8 +239,7 @@ fn bench_map(c: &mut Criterion) {
241239

242240
group.bench_function("Imperfect::map_success", |b| {
243241
b.iter(|| {
244-
let i: Imperfect<i32, String, ConvergenceLoss> =
245-
Imperfect::Success(black_box(42));
242+
let i: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(42));
246243
i.map(|x| x * 2)
247244
})
248245
});
@@ -275,10 +272,8 @@ fn bench_compose(c: &mut Criterion) {
275272

276273
group.bench_function("success_then_success", |b| {
277274
b.iter(|| {
278-
let a: Imperfect<i32, String, ConvergenceLoss> =
279-
Imperfect::Success(black_box(1));
280-
let next: Imperfect<i32, String, ConvergenceLoss> =
281-
Imperfect::Success(black_box(2));
275+
let a: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(1));
276+
let next: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(2));
282277
a.compose(next)
283278
})
284279
});
@@ -297,8 +292,7 @@ fn bench_compose(c: &mut Criterion) {
297292
b.iter(|| {
298293
let a: Imperfect<i32, String, ConvergenceLoss> =
299294
Imperfect::Failure(black_box(String::from("nope")), ConvergenceLoss::new(1));
300-
let next: Imperfect<i32, String, ConvergenceLoss> =
301-
Imperfect::Success(black_box(2));
295+
let next: Imperfect<i32, String, ConvergenceLoss> = Imperfect::Success(black_box(2));
302296
a.compose(next)
303297
})
304298
});

β€Ždocs/benchmarks.mdβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Same pattern. Success tracks Result. Partial adds the loss-preservation cost.
6666
| All Success | 648 ps |
6767
| All Partial | 6.06 ns |
6868

69-
The `EhContext` struct adds zero overhead beyond the loss accumulation itself. Same performance characteristics as raw `eh` chains. The context is a zero-cost coordinator.
69+
The `Eh` struct adds zero overhead beyond the loss accumulation itself. Same performance characteristics as raw `eh` chains. The context is a zero-cost coordinator.
7070

7171
### Loss type combination
7272

@@ -83,3 +83,5 @@ ConvergenceLoss and RoutingLoss combine in under 1 ns. They're scalar max operat
8383
Result is a two-state type that optimizes for the fast path by *deleting the middle*. Imperfect is a three-state type that preserves it. The benchmarks say: preservation is free until there's something to preserve. Then it costs 0.65 ns per step.
8484

8585
That's not a tax. That's a price. And it buys you the flight recorder that Result said you couldn't have.
86+
87+
[Back to README](../README.md) Β· [Flight recorder β†’](flight-recorder.md) Β· [Pipeline β†’](pipeline.md)

β€Ždocs/flight-recorder.mdβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,5 @@ and "Always on." Result doesn't have answers for these because Result can't
126126
represent them. The type doesn't have a place to put the information.
127127

128128
Those empty cells are why this crate exists.
129+
130+
[Back to README](../README.md) Β· [Loss types β†’](loss-types.md) Β· [Benchmarks β†’](benchmarks.md)

β€Ždocs/loss-types.mdβ€Ž

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,48 @@ assert_eq!(result.loss().runner_up_gap(), 0.3);
103103
**`zero()`** β€” 0.0 entropy, 1.0 gap. One model at 100%.
104104
**`total()`** β€” infinite entropy, 0.0 gap. Maximum uncertainty.
105105

106+
## Standard library Loss implementations
107+
108+
`Loss` is implemented for common standard library types out of the box:
109+
110+
### Numeric losses
111+
112+
| Type | `combine` | `zero()` | `total()` |
113+
|------|-----------|----------|-----------|
114+
| `usize` | saturating add | `0` | `usize::MAX` |
115+
| `u64` | saturating add | `0` | `u64::MAX` |
116+
| `f64` | addition (IEEE) | `0.0` | `f64::INFINITY` |
117+
118+
### Collection losses
119+
120+
Track *what* was lost, not just *how much*.
121+
122+
| Type | `combine` | `zero()` / `total()` |
123+
|------|-----------|----------------------|
124+
| `Vec<T: Clone>` | append | empty |
125+
| `HashSet<T: Eq + Hash + Clone>` | union | empty |
126+
| `BTreeSet<T: Ord + Clone>` | union | empty |
127+
| `String` | join with `"; "` | empty |
128+
129+
> **Limitation:** Collections have no natural absorbing element. `total()` returns the same as `zero()` (empty). The monoid identity and associativity laws hold, but the absorbing property does not. If you need absorbing semantics, use a numeric loss type.
130+
131+
### Tuple combinator
132+
133+
`(A, B)` where both `A: Loss` and `B: Loss` composes two loss dimensions independently. Each component combines, zeros, and totals on its own.
134+
135+
```rust
136+
use terni::{Imperfect, ConvergenceLoss, RoutingLoss};
137+
138+
type PipelineLoss = (ConvergenceLoss, RoutingLoss);
139+
140+
let result: Imperfect<i32, String, PipelineLoss> = Imperfect::Partial(
141+
42,
142+
(ConvergenceLoss::new(3), RoutingLoss::new(0.5, 0.2)),
143+
);
144+
```
145+
146+
See the [flight recorder guide](flight-recorder.md) for more composition patterns.
147+
106148
## Implementing your own Loss type
107149

108150
Implement `Loss` for any domain-specific measurement. The only requirements: `Clone + Default`, the four trait methods, and `combine` must be associative.

β€Žsrc/lib.rsβ€Ž

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2130,8 +2130,7 @@ mod tests {
21302130

21312131
#[test]
21322132
fn string_in_imperfect() {
2133-
let i: Imperfect<u32, &str, String> =
2134-
Imperfect::Partial(42, "precision lost".into());
2133+
let i: Imperfect<u32, &str, String> = Imperfect::Partial(42, "precision lost".into());
21352134
assert_eq!(i.loss(), "precision lost");
21362135
}
21372136

@@ -2293,8 +2292,7 @@ mod tests {
22932292
#[test]
22942293
fn tuple_in_imperfect() {
22952294
// Composite loss: count + magnitude
2296-
let i: Imperfect<&str, &str, (usize, f64)> =
2297-
Imperfect::Partial("value", (2, 0.5));
2295+
let i: Imperfect<&str, &str, (usize, f64)> = Imperfect::Partial("value", (2, 0.5));
22982296
let loss = i.loss();
22992297
assert_eq!(loss.0, 2);
23002298
assert_eq!(loss.1, 0.5);

0 commit comments

Comments
Β (0)