From 456b247b47b9af2209c49c08e2219ac7688c8e74 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 17 Oct 2026 04:54:47 -0400 Subject: [PATCH] jcp-trace: an undefined wedge metric is not zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sibling of the jcp-efficiency fix, in the `eval/1` receipt: t.uj_per_accepted_token = t.uj_total.checked_div(t.output_tokens).unwrap_or(0); `uj_per_accepted_token` is a cost-per-result figure, so 0 is its optimum. A run that burned prompt tokens and accepted no output has no such figure — the division is undefined — and reporting 0 ranked it as the cheapest entry in the ledger. Like the efficiency receipt, this one seals the figure into the signed core (`eval_wire`) and reads it straight back out in `verify()` rather than recomputing it, so the conflation was sealed and consumed as the headline. `mesh/SPEC.md` §6 states the family convention — the figure is "omitted when no tokens were accepted" — and `MeshReceipt` already implements it. This brings `eval/1` onto the same shape: - `Totals::uj_per_accepted_token` and `EvalReceipt::uj_per_accepted_token` become `Option`; `Default` is `None`. - `eval_wire` omits the key when the figure is undefined instead of sealing 0. Canonical CBOR sorts keys, so the conditional insert does not change the wire of a receipt that has one. - `verify()` reads it as optional, so an older receipt that carries the figure still round-trips. - `examples/ingest.rs` prints "—" rather than a number. `a_run_that_accepted_nothing_seals_no_wedge_metric` builds a trace with 800 prompt tokens and no accepted output: 8 000 µJ of real spend, no figure, the key absent from the signed core, and the envelope still verifying with the spend on it. Verified with `--features server` as well as the default build. The `server` module is behind a non-default feature, so `cargo test --workspace` never compiles it, and `server::tests::http_roundtrip_returns_sealed_eval_receipt` asserts on this exact wire field — it passes, but CI would not have told us: the jcp row's `feature_tests` in standards-build.yml names only `-p jcp-energy --features nvml`. Co-Authored-By: Claude Opus 5 --- .../crates/jcp-trace/examples/ingest.rs | 9 +- jcp/jcp-rs/crates/jcp-trace/src/lib.rs | 83 ++++++++++++++++--- jcp/jcp-rs/crates/jcp-trace/src/otel.rs | 2 +- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/jcp/jcp-rs/crates/jcp-trace/examples/ingest.rs b/jcp/jcp-rs/crates/jcp-trace/examples/ingest.rs index 553b328d9..624d6a3d6 100644 --- a/jcp/jcp-rs/crates/jcp-trace/examples/ingest.rs +++ b/jcp/jcp-rs/crates/jcp-trace/examples/ingest.rs @@ -77,7 +77,14 @@ fn main() { println!("\n── sealed eval/1 receipt ──"); println!(" tokens : {} in / {} out", t.input_tokens, t.output_tokens); println!(" energy : {} µJ ({})", t.uj_total, t.energy_provenance.jcr1_label()); - println!(" uj_per_accepted_token: {}", t.uj_per_accepted_token); + println!( + " uj_per_accepted_token: {}", + // "—" when no tokens were accepted: 0 is this figure's optimum, so + // printing it would rank a run that delivered nothing above every run + // that did. + t.uj_per_accepted_token + .map_or_else(|| "—".to_string(), |uj| uj.to_string()) + ); println!(" envelope : {} bytes, content id {}", envelope.len(), &ContentId::of_bytes(&envelope).to_hex()[..16]); diff --git a/jcp/jcp-rs/crates/jcp-trace/src/lib.rs b/jcp/jcp-rs/crates/jcp-trace/src/lib.rs index 9bca85dbc..d2548524d 100644 --- a/jcp/jcp-rs/crates/jcp-trace/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-trace/src/lib.rs @@ -277,8 +277,12 @@ impl GenAiTrace { max_end = max_end.max(span.end_ns); } t.total_tokens = t.input_tokens.saturating_add(t.output_tokens); - // The wedge metric: joules per accepted output token. - t.uj_per_accepted_token = t.uj_total.checked_div(t.output_tokens).unwrap_or(0); + // The wedge metric: joules per accepted output token. `None` when no + // tokens were accepted — the division is undefined then, and 0 is this + // figure's optimum, so returning it reported spend that bought nothing + // as the cheapest run in the ledger. `mesh/SPEC.md` §6 states the family + // convention: "omitted when no tokens were accepted". + t.uj_per_accepted_token = t.uj_total.checked_div(t.output_tokens); // The total is only as trustworthy as its weakest measured component. t.energy_provenance = prov.unwrap_or(Provenance::Estimator); t.wall_ms = if max_end > min_start && min_start != u64::MAX { @@ -301,8 +305,9 @@ pub struct Totals { pub total_tokens: u64, /// Total microjoules across all spans that carry a reading. pub uj_total: u64, - /// Microjoules per accepted output token — the headline figure. - pub uj_per_accepted_token: u64, + /// Microjoules per accepted output token — the headline figure. `None` when + /// no tokens were accepted (the division is undefined, not zero-cost). + pub uj_per_accepted_token: Option, /// The weakest provenance among the spans (the honest aggregate trust). pub energy_provenance: Provenance, /// Wall-clock span of the run in milliseconds. @@ -316,7 +321,7 @@ impl Default for Totals { output_tokens: 0, total_tokens: 0, uj_total: 0, - uj_per_accepted_token: 0, + uj_per_accepted_token: None, energy_provenance: Provenance::Estimator, wall_ms: 0, } @@ -768,7 +773,7 @@ fn eval_wire( steward: &str, ) -> Value { let trace_cid = trace.content_id(); - json!({ + let mut o = json!({ "eval_receipt": "eval/1", "trace_id": trace_cid.to_hex(), "gate_spec_id": gate_spec_id, @@ -777,7 +782,6 @@ fn eval_wire( "output_tokens": totals.output_tokens, "total_tokens": totals.total_tokens, "uj_total": totals.uj_total, - "uj_per_accepted_token": totals.uj_per_accepted_token, "energy_provenance": totals.energy_provenance.jcr1_label(), "wall_ms": totals.wall_ms, "assessments": assessments.iter().map(Assessment::wire).collect::>(), @@ -792,7 +796,13 @@ fn eval_wire( "seq": seq, "prev": prev.to_hex(), "steward": steward, - }) + }); + // Omitted when undefined rather than sealed as 0. Canonical CBOR sorts keys, + // so inserting here does not change the wire for a receipt that has one. + if let Some(uj) = totals.uj_per_accepted_token { + o["uj_per_accepted_token"] = json!(uj); + } + o } /// A sealed eval receipt plus the projections that went into it. @@ -850,8 +860,9 @@ pub struct EvalReceipt { pub output_tokens: u64, /// Total microjoules. pub uj_total: u64, - /// Microjoules per accepted token. - pub uj_per_accepted_token: u64, + /// Microjoules per accepted token. `None` when the receipt sealed no figure + /// because no tokens were accepted. + pub uj_per_accepted_token: Option, /// The aggregate energy provenance. pub energy_provenance: String, /// Did the quality gate pass? @@ -899,7 +910,7 @@ pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result