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
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]>
114 lines
6 KiB
Rust
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);
|
|
}
|