open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/full_cascade_with_triangle.rs
Transaction Science 9c01584950 jouleclaw: end-to-end demo binary — cascade + triangle + JCR-1
The consolidated end-to-end stake. Before this commit the workspace
had per-use-case examples (minimal_cache_only, rag_with_retrieval,
comprehend_demo, agent_with_skill_induction, save_memory_and_promote,
ledger_breakdown) plus a separate JCR-1 minter in jouleclaw-jcr1,
but no single binary that exercised the cascade + the new triangle +
a signed receipt together. A sharp reader who ran the project came
away without a single image of the whole stack working.

The new example, full_cascade_with_triangle.rs, runs three things in
one go:

1. Cascade walk over a free-form query through
   JouleClawStack::with_defaults() — same question twice; the first
   ask walks to L3 (~2 J), the second hits L0 (~7 nJ). Cheapest-tier-
   that-works in action.

2. The triangle (waves 41-45) — a templated request walks the
   elicitor → cone → conservation → oracle pipeline via
   jouleclaw_datum::dispatch_one_shot, producing a verified
   Candidate, a conservation-conforming finding set, and an oracle
   Pass chain. No model invoked.

3. JCR-1 receipt — at the end, one signed Receipt is minted over
   the session's total joules (cascade only; the triangle's
   deterministic path is free), using the wave-7 canonical det-CBOR
   + COSE_Sign1 path with the published test signing key. Output
   sizes (332 B core, 453 B envelope) and the EdDSA public key
   print so the reader can hand the envelope to the Python or Go
   reference verifier in conformance/.

Three things proven in one cargo run, no external dependencies.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-06-04 03:19:31 -04:00

211 lines
8.7 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `full_cascade_with_triangle` — one workload, three things at once:
//!
//! 1. **Cascade walk** — the L0–L4 default `JouleClawStack` resolves a
//! free-form query, printing tier + joules. A second ask of the
//! same question hits the L0 cache (cheapest-tier-that-works).
//!
//! 2. **The triangle (waves 41–45)** — a templated request walks the
//! elicitor → cone → conservation → oracle pipeline via
//! `jouleclaw_datum::dispatch_one_shot`, producing a verified
//! `Candidate` with a conservation-conforming finding set and an
//! oracle `Pass`. The harness, with no model called, is what
//! decided this was a known template-class request and routed it.
//!
//! 3. **JCR-1 receipt** — at the end, a single signed `Receipt` is
//! minted over the session's total joules (sum of cascade + triangle)
//! using the JCR-1 wave-7 canonical det-CBOR + COSE_Sign1 path,
//! cross-verifiable by the Python and Go reference verifiers in
//! `conformance/`.
//!
//! ## Why all three in one file
//!
//! Before this example, the workspace had per-use-case demos
//! (`agent_with_skill_induction`, `rag_with_retrieval`,
//! `comprehend_demo`, …) and a separate JCR-1 minter
//! (`jouleclaw-jcr1::jcr1_mint`), but no single binary that exercised
//! the cascade + the new triangle + a signed receipt together. A sharp
//! reader who runs this gets the whole story — runtime, the determinism
//! gates, and the audit artifact — from one `cargo run` invocation.
//!
//! Run:
//! cargo run -p jouleclaw-stack --example full_cascade_with_triangle
use ed25519_dalek::SigningKey;
use jouleclaw_cascade::tier::Runtime;
use jouleclaw_cascade::types::{ContextRef, JouleBudget, QualityFloor, Query, QueryInput};
use jouleclaw_datum::{
AlwaysPassOracle, BandDispatch, CanonicalJsonByteCount, ConeCoord, Elicitor, NoveltyBand,
RawRequest, Slot, SlotKind, SlotValue, SlotWalker, Template, TemplateId, dispatch_one_shot,
};
use jouleclaw_jcr1::{TEST_KEYS_JSON, canonical_signed_core, cose_sign1_envelope, to_jcr1_wire};
use jouleclaw_prov::{CascadeTier, ReceiptBuilder, ToolTouch, input_hash};
use jouleclaw_stack::JouleClawStack;
const CLOSED_AT: &str = "2026-06-04T12:00:00Z";
fn make_query(text: &str) -> Query {
Query {
input: QueryInput::Text(text.to_string()),
budget: JouleBudget::expensive(),
quality: QualityFloor::any(),
context: ContextRef::fresh(),
deadline: None,
}
}
fn cascade_walk(runtime: &mut Runtime, question: &str) -> (String, f64) {
let answer = runtime
.answer(make_query(question))
.expect("cascade resolves a single-shot text query");
(answer.tier_used.wire_tag().to_string(), answer.joules_spent)
}
fn hello_template() -> Template {
Template {
id: TemplateId::new("hello-1"),
signature: "print hello to NAME".into(),
coord: ConeCoord::new(0.1, 0.05),
language: "rust".into(),
slots: vec![Slot {
id: "name".into(),
name: "Name".into(),
prompt: "Whom should we greet?".into(),
kind: SlotKind::Identifier,
required: true,
}],
body: "fn main() { println!(\"hello, <<name>>\"); }".into(),
provenance: "demo".into(),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// ── 1. Cascade walk over a free-form query ─────────────────────
println!("== 1. Cascade walk ==");
let mut stack = JouleClawStack::with_defaults();
let question = "what is the magic word?";
let (tier_first, joules_first) = cascade_walk(&mut stack.runtime, question);
let (tier_second, joules_second) = cascade_walk(&mut stack.runtime, question);
println!(" first ask: tier={tier_first:<6} joules={joules_first:.9}");
println!(" second ask: tier={tier_second:<6} joules={joules_second:.9}");
println!(
" Δ (cheapest-tier-that-works did its job): {:.9} J\n",
joules_first - joules_second
);
// ── 2. The triangle: elicitor → cone → conservation → oracle ──
println!("== 2. The triangle (waves 41-45) ==");
let template = hello_template();
let request = RawRequest::new("greet the world");
// Elicitor: walk the requester through the template's required slots.
let mut walker = SlotWalker::new(&request, template.slots.clone());
let step = walker
.submit(jouleclaw_datum::Answer::new(
"name",
SlotValue::Identifier {
ident: "world".into(),
},
))
.expect("answer accepted");
let draft = step
.draft()
.expect("required slots filled in one shot")
.clone();
println!(" elicitor: filled {} slot(s)", draft.filled.len());
// Cone + dispatch_one_shot: classify, resolve, conservation-check,
// oracle-gate. AlwaysPassOracle stands in for a real toolchain
// oracle (e.g. rustc + cargo test) the consumer would plug in.
let measure = CanonicalJsonByteCount;
let dispatch = BandDispatch::new(NoveltyBand::datum_default()).expect("valid band table");
let oracle = AlwaysPassOracle;
let outcome = dispatch_one_shot(
&dispatch,
&template,
&draft,
request.text.as_str(),
&oracle,
&measure,
/* allow_stochastic = */ false,
)
.await
.expect("dispatch wires");
match &outcome {
jouleclaw_datum::DatumOutcome::Verified {
candidate,
chain,
findings,
tier_id,
} => {
println!(" tier: {tier_id}");
println!(
" conservation: {findings} finding(s) — all Conforming",
findings = findings.len()
);
println!(" oracle chain: {chain:?}");
println!(" candidate: {}", candidate.content);
}
other => panic!("expected Verified, got {other:?}"),
}
println!();
// ── 3. JCR-1 receipt over the session's total joules ──────────
println!("== 3. JCR-1 receipt ==");
// Session energy = sum of every joule charged across the two
// cascade walks above. The triangle's deterministic path costs
// nothing the wire-form accounts for (it's lookup), so the
// receipt accounts only for the cascade tiers that actually
// drew energy.
let session_joules_uj = ((joules_first + joules_second) * 1_000_000.0).round().max(1.0) as u64;
// Use the published test seed so the resulting envelope is
// verifiable by anyone holding `conformance/test-keys.json`.
let keys: serde_json::Value =
serde_json::from_str(TEST_KEYS_JSON).expect("test-keys.json parses");
let seed_hex = keys["ed25519_seed_hex"].as_str().expect("test seed present");
let seed: [u8; 32] = hex::decode(seed_hex)
.expect("seed is hex")
.try_into()
.expect("seed is 32 bytes");
let sk = SigningKey::from_bytes(&seed);
let steward = keys["steward_kid"].as_str().expect("steward kid");
let tool_id = "jouleclaw:cascade.demo";
let receipt = ReceiptBuilder::new()
.input_hash(&input_hash(question))
.tier(CascadeTier::L0Cache)
.steward(steward)
.account_tool(ToolTouch {
tool_id: tool_id.into(),
joules_uj: session_joules_uj,
energy_provenance: jouleclaw_energy::Provenance::Estimator,
})
.seal()
.expect("seal");
let mut wire = to_jcr1_wire(&receipt).expect("project to wire form");
{
let obj = wire.as_object_mut().expect("wire is a map");
obj.insert("id".into(), serde_json::json!("rcpt-demo-0001"));
obj.insert("closed_at".into(), serde_json::json!(CLOSED_AT));
}
let core = jouleclaw_jcr1::signed_core_bytes(&wire).expect("canonical signed core");
let envelope = cose_sign1_envelope(&core, &sk, steward);
// The wire-form path above is authoritative; `canonical_signed_core`
// off the typed receipt is computed only to exercise the second
// code path. The two diverge when the demo patches `id`/`closed_at`
// on the wire form (that's what the patch is for — the receipt's
// builder uses fresh values).
let _typed_core = canonical_signed_core(&receipt).expect("typed core");
println!(" spec: JCR-1");
println!(" energy: {session_joules_uj} µJ (Estimator)");
println!(" signed core: {} bytes (det-CBOR)", core.len());
println!(" COSE_Sign1: {} bytes (EdDSA/Ed25519)", envelope.len());
println!(
" pub key (hex): {}",
hex::encode(sk.verifying_key().to_bytes())
);
println!();
println!("✓ Cascade walked, triangle gated, receipt sealed — one run.");
}