open-standards/jcp/jcp-rs/crates/jcp-trace/examples/ingest.rs
Transaction Science 456b247b47
All checks were successful
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m0s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 11s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m35s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m13s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m15s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m36s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 57s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m8s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 1m46s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 2m53s
jcp-trace: an undefined wedge metric is not zero
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<u64>`; `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 <[email protected]>
2026-10-17 04:54:47 -04:00

114 lines
6 KiB
Rust

//! Ingest a real OTLP/JSON GenAI agent trace, attach joules, evaluate a quality
//! gate, and seal one `eval/1` JCR-1 receipt — then verify it and show the ARL
//! readiness projection.
//!
//! ```text
//! cargo run -p jcp-trace --example ingest
//! ```
use ed25519_dalek::SigningKey;
use jcp_schema::ContentId;
use jcp_trace::{otel, Assessment, EvalLog, Gate};
use serde_json::json;
fn main() {
// 1. The incumbent's output: an OTLP/JSON trace with gen_ai.* attributes —
// an agent that planned, searched a tool, retrieved, and answered.
let otlp = json!({
"resourceSpans": [{ "scopeSpans": [{ "spans": [
{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a0",
"name": "invoke_agent support-bot",
"startTimeUnixNano": "1700000000000000000", "endTimeUnixNano": "1700000004000000000",
"status": { "code": 1 },
"attributes": [{ "key": "mlflow.spanType", "value": { "stringValue": "AGENT" } }] },
{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a1", "parentSpanId": "a0",
"name": "chat plan", "startTimeUnixNano": "1700000000200000000", "endTimeUnixNano": "1700000001000000000",
"status": { "code": 1 },
"attributes": [
{ "key": "gen_ai.operation.name", "value": { "stringValue": "chat" } },
{ "key": "gen_ai.request.model", "value": { "stringValue": "ssm-1b" } },
{ "key": "gen_ai.usage.input_tokens", "value": { "intValue": "900" } },
{ "key": "gen_ai.usage.output_tokens", "value": { "intValue": "110" } } ] },
{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a2", "parentSpanId": "a0",
"name": "execute_tool search_docs",
"attributes": [{ "key": "gen_ai.operation.name", "value": { "stringValue": "execute_tool" } }] },
{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a3", "parentSpanId": "a0",
"name": "retrieve",
"attributes": [{ "key": "mlflow.spanType", "value": { "stringValue": "RETRIEVER" } }] },
{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "spanId": "a4", "parentSpanId": "a0",
"name": "chat answer", "startTimeUnixNano": "1700000002000000000", "endTimeUnixNano": "1700000003800000000",
"status": { "code": 1 },
"attributes": [
{ "key": "gen_ai.operation.name", "value": { "stringValue": "chat" } },
{ "key": "gen_ai.request.model", "value": { "stringValue": "ssm-1b" } },
{ "key": "gen_ai.usage.input_tokens", "value": { "intValue": "1800" } },
{ "key": "gen_ai.usage.output_tokens", "value": { "intValue": "240" } } ] }
] }] }]
});
let mut trace = otel::from_otlp_json(&otlp).expect("valid OTLP gen_ai trace");
println!("ingested trace {} — {} spans", &trace.trace_id[..12], trace.spans.len());
// 2. Attach joules. Here: no hardware meter, so an honest Estimator from
// token counts (12 µJ/token). A real deployment passes a measured reading
// per span and the provenance reads HwShunt instead.
trace.attach_estimator_energy(12);
// 3. The incumbent's other output: assessments (a judge, a human).
let assessments = vec![
Assessment::judge_pass("correctness", "openai:/gpt-4o")
.with_rationale("all expected facts supported by the answer"),
Assessment::judge_pass("safety", "family:/safety-judge"),
Assessment::judge_score("groundedness", "openai:/gpt-4o", 0.93),
Assessment::human("helpfulness", "reviewer:alice", true),
];
// 4. The quality gate: correctness AND safety must pass; groundedness >= 0.85.
let gate = Gate::require_pass(["correctness", "safety"]).and_min("groundedness", 0.85);
// 5. Seal it into a chained log (one ordered ledger of evaluated runs).
let sk = SigningKey::from_bytes(&[9u8; 32]);
let pk = sk.verifying_key().to_bytes();
let steward = "did:example:eval-steward";
let mut log = EvalLog::new(sk, steward);
let envelope = log.append(&trace, &assessments, &gate).expect("seal").to_vec();
let t = trace.totals();
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: {}",
// "—" 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]);
// 6. Verify it — recover the gate verdict + readiness from the sealed bytes.
let r = jcp_trace::verify(&envelope, steward, &pk).expect("verifies");
println!("\n── verified by steward key ──");
println!(" gate_passed : {}", r.gate_passed);
if !r.gate_failures.is_empty() {
println!(" gate_failures : {:?}", r.gate_failures);
}
println!(" sample_size : {}", r.sample_size);
println!(" ARL validation hint : depth {} (a projection; the ARL rubric makes the claim)",
r.validation_depth_hint);
// 7. Tamper-evidence: flip one byte → the seal no longer verifies.
let mut bad = envelope.clone();
let n = bad.len();
bad[n / 2] ^= 0x01;
assert!(jcp_trace::verify(&bad, steward, &pk).is_err());
println!("\n tamper check : a single flipped byte fails verification ✓");
// 8. The chain: a second run links to the first by content id.
let _ = log.append(&trace, &assessments, &gate).expect("seal #2");
let r1 = jcp_trace::verify(&log.entries()[1], steward, &pk).expect("verifies");
let first_cid = ContentId::of_bytes(&log.entries()[0]).to_hex();
println!(" chain : entry #1.prev == entry #0 id: {}", r1.prev == first_cid);
}