Skip to content

Commit c291c07

Browse files
committed
docs+example: invariant-path as the echo-types application
classify_candidate is a non-injective classifier; the retained ClaimCandidate + ClassificationOutcome.losses IS the echo (fiber) over a Classification. Adds a runnable worked example + CI-covered integration tests (2 pass) showing two distinct paths collapsing to Overextended with distinct retained losses, plus docs/ECHO-TYPES.md mapping the estate's machine-checked Echo integration. https://claude.ai/code/session_01DQACj3RFmAPZaBPgR9SAaS
1 parent c5e33aa commit c291c07

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Worked echo-types example: classification as fiber-based structured loss.
5+
//
6+
// Invariant Path's classifier
7+
// classify_candidate : ClaimCandidate -> ClassificationOutcome
8+
// is a NON-INJECTIVE map: many distinct claim-paths collapse to the same
9+
// `Classification`. hyperpolymath/echo-types is the mechanised theory of
10+
// exactly this collapse — the fiber
11+
// Echo f y := Σ (x : A) , (f x ≡ y)
12+
// the proof-relevant record of *which* inputs map to y.
13+
//
14+
// This example materialises one echo: the fiber of `Overextended`. The bare
15+
// `Classification::Overextended` forgets which path produced it; Invariant
16+
// Path RETAINS the full candidate together with its `losses` — and that
17+
// retained structured loss IS the echo. Invariant Path is, in echo-types
18+
// terms, the runtime that keeps echoes instead of discarding them.
19+
//
20+
// Run: cargo run -p invariant-path-core --example echo_structured_loss
21+
// See: docs/ECHO-TYPES.md and the Agda companion
22+
// nextgen-typing/verification/proofs/agda/EchoTyping.agda
23+
24+
use invariant_path_core::classifier::classify_candidate;
25+
use invariant_path_core::model::{ClaimCandidate, Classification, ClassificationOutcome, Span};
26+
27+
/// Build a minimal candidate (spans are irrelevant to classification here).
28+
fn candidate(source: &str, target: &str, trigger: &str) -> ClaimCandidate {
29+
ClaimCandidate {
30+
span: Span { start: 0, end: 0 },
31+
source_text: source.to_string(),
32+
target_text: target.to_string(),
33+
trigger: trigger.to_string(),
34+
path_description: format!("{source} ⟶ {target}"),
35+
}
36+
}
37+
38+
fn main() {
39+
// Two genuinely different claim-paths …
40+
let c1 = candidate("benchmark accuracy", "general capability", "therefore");
41+
let c2 = candidate("the theorem proves", "production guarantee", "implies");
42+
43+
let o1 = classify_candidate(&c1);
44+
let o2 = classify_candidate(&c2);
45+
46+
// … collapse to the SAME classification: the classifier is non-injective.
47+
assert_eq!(o1.classification, Classification::Overextended);
48+
assert_eq!(o2.classification, Classification::Overextended);
49+
assert_ne!(c1, c2, "the two claim-paths are genuinely distinct");
50+
51+
// The echo (fiber over `Overextended`) is what Invariant Path RETAINS:
52+
// the distinct candidates plus their distinct structured losses, which the
53+
// bare `Classification` has forgotten.
54+
let fiber: Vec<(ClaimCandidate, ClassificationOutcome)> = vec![(c1, o1), (c2, o2)];
55+
56+
println!("Echo(classify) over `Overextended` — the retained fiber:\n");
57+
for (cand, out) in &fiber {
58+
println!(
59+
" • {:<32} losses = {:?}",
60+
cand.path_description, out.losses
61+
);
62+
}
63+
64+
// Distinct losses ⇒ the classification truly dropped information the echo keeps.
65+
assert_ne!(
66+
fiber[0].1.losses, fiber[1].1.losses,
67+
"each path's structured loss is distinct and retained"
68+
);
69+
70+
println!(
71+
"\nNon-injective: 2 distinct paths → 1 classification.\n\
72+
The echo (candidate + losses) is the structured loss Invariant Path retains,\n\
73+
and `path_description` is the section that recovers the source the verdict forgot."
74+
);
75+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
//
4+
// CI-covered companion to examples/echo_structured_loss.rs.
5+
//
6+
// Pins the echo-types correspondence as a test: `classify_candidate` is a
7+
// non-injective classifier, and the fiber (echo) over a classification retains
8+
// the per-candidate structured loss that the bare `Classification` discards.
9+
// See docs/ECHO-TYPES.md.
10+
11+
use invariant_path_core::classifier::classify_candidate;
12+
use invariant_path_core::model::{ClaimCandidate, Classification, Span};
13+
14+
fn candidate(source: &str, target: &str, trigger: &str) -> ClaimCandidate {
15+
ClaimCandidate {
16+
span: Span { start: 0, end: 0 },
17+
source_text: source.to_string(),
18+
target_text: target.to_string(),
19+
trigger: trigger.to_string(),
20+
path_description: format!("{source} -> {target}"),
21+
}
22+
}
23+
24+
/// Two distinct claim-paths collapse to one classification: the classifier is
25+
/// non-injective, so the classification alone cannot recover the path.
26+
#[test]
27+
fn classification_is_non_injective() {
28+
let c1 = candidate("benchmark accuracy", "general capability", "therefore");
29+
let c2 = candidate("the theorem proves", "production guarantee", "implies");
30+
31+
let o1 = classify_candidate(&c1);
32+
let o2 = classify_candidate(&c2);
33+
34+
assert_ne!(c1, c2);
35+
assert_eq!(o1.classification, Classification::Overextended);
36+
assert_eq!(o2.classification, Classification::Overextended);
37+
}
38+
39+
/// The echo retains what the classification drops: distinct candidates over the
40+
/// same classification carry distinct, retained `losses` (the structured loss).
41+
#[test]
42+
fn echo_retains_structured_loss() {
43+
let c1 = candidate("benchmark accuracy", "general capability", "therefore");
44+
let c2 = candidate("the theorem proves", "production guarantee", "implies");
45+
46+
let o1 = classify_candidate(&c1);
47+
let o2 = classify_candidate(&c2);
48+
49+
// same fiber (classification) …
50+
assert_eq!(o1.classification, o2.classification);
51+
// … but distinct, retained structured loss — the echo keeps it.
52+
assert_ne!(o1.losses, o2.losses);
53+
assert!(!o1.losses.is_empty() && !o2.losses.is_empty());
54+
}

docs/ECHO-TYPES.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell -->
3+
4+
# Invariant Path × echo-types — structured loss as a first-class object
5+
6+
Invariant Path is the **application example** of the
7+
[`echo-types`](https://github.com/hyperpolymath/echo-types) programme: it is a
8+
runtime that *keeps echoes instead of discarding them*.
9+
10+
## The correspondence
11+
12+
echo-types formalises **fiber-based structured loss**. For a function
13+
`f : A → B`, the *echo* of an output `y` is the fiber
14+
15+
```
16+
Echo f y := Σ (x : A) , (f x ≡ y)
17+
```
18+
19+
— the proof-relevant record of **which** inputs `f` collapsed onto `y`. A
20+
non-injective `f` *forgets* that "which"; the echo is exactly what you must
21+
retain to recover it.
22+
23+
Invariant Path's classifier is such an `f`:
24+
25+
```rust
26+
classify_candidate : ClaimCandidate -> ClassificationOutcome
27+
```
28+
29+
Many distinct claim-paths collapse to the same `Classification`
30+
(`ValidPath | Overextended | Conflation | Incomplete | Abstain`). The bare
31+
classification is the lossy codomain value; **the claim-path is the echo**.
32+
33+
| echo-types | Invariant Path |
34+
|---------------------------|--------------------------------------------------|
35+
| `f : A → B` (lossy map) | `classify_candidate` (claim-path → classification) |
36+
| codomain value `y` | `Classification` |
37+
| `Echo f y` (the fiber) | the retained `ClaimCandidate` + `ClassificationOutcome` |
38+
| residue / structured loss | `ClassificationOutcome.losses : Vec<String>` |
39+
| no-section (irreversible) | a `Classification` alone cannot recover its path |
40+
| the section that recovers | `Annotation.path_description` (retained provenance) |
41+
42+
The domain model already names the loss: `ClassificationOutcome` carries
43+
`losses` and `preserved` precisely because a classification that *dropped* the
44+
path would be untraceable. echo-types is the mechanised backbone of that design
45+
choice — Invariant Path is "a claim-path debugger, not a truth engine" exactly
46+
because it stores echoes.
47+
48+
## Worked example (runnable + tested)
49+
50+
```bash
51+
cargo run -p invariant-path-core --example echo_structured_loss
52+
cargo test -p invariant-path-core --test echo_structured_loss
53+
```
54+
55+
Two genuinely different claim-paths —
56+
`benchmark accuracy ⟶ general capability` and
57+
`the theorem proves ⟶ production guarantee` — both classify as `Overextended`,
58+
yet retain **distinct** structured losses (`task_transfer` vs
59+
`implementation_gap`). The classifier is non-injective; the echo keeps what it
60+
drops. See:
61+
62+
- `crates/invariant-path-core/examples/echo_structured_loss.rs`
63+
- `crates/invariant-path-core/tests/echo_structured_loss.rs`
64+
65+
## Cross-repo
66+
67+
The same `Echo` underlies the type-system integration across the estate, all
68+
machine-checked under `--safe --without-K`:
69+
70+
- **nextgen-typing**`verification/proofs/agda/EchoTyping.agda`: affine
71+
subtyping *is* echo `weaken`; refinement erasure *is* a fiber.
72+
- **nextgen-languages / kitchenspeak**`proofs/agda/EchoBridge.agda`: the `@`
73+
sensor witness *is* `Echo (fired …) true`.
74+
- **phronesis**`academic/formal-verification/agda/PhronesisEcho.agda`: an
75+
ethical verdict's provenance *is* `Echo verdict v`.
76+
77+
Invariant Path is where that theory is *used*: a tool whose reason to exist is
78+
to retain the echo.

0 commit comments

Comments
 (0)