jcp-intercept: a reading meter meters a seam as one span, divided among its steps by CPU time #236

Merged
dcharlot merged 2 commits from feat/intercept-one-span-per-pass into main 2026-10-13 09:31:28 -04:00
6 changed files with 325 additions and 37 deletions

View file

@ -43,7 +43,7 @@ jcp/
- **`jcp-orchestrate`** — a **cross-harness orchestration profile**: a deterministic reconcile loop (poll → route → dispatch → reconcile → retry) that runs work items across a *heterogeneous* fleet of `Harness` backends, where every dispatch is **capability-gated** (a harness is granted only the task kinds it may handle), **joule-budgeted** (an exhausted harness fails over to the next one with capacity), and **sealed** as a JCR-1 decision receipt *before* it runs. A task whose kind no harness is granted is denied; a flaky harness retries up to a bound; the whole run is an ordered, verifiable ledger of sealed decisions. The compose-many-harnesses shape a meta-harness offers — with the capability, energy budget, and audit it leaves to the platform.
- **`jcp-prompt`** — a **signed, content-addressed prompt registry**: immutable prompt **versions** (double-brace `{{ variable }}` templates) with mutable **aliases** (`dev` / `staging` / `production`) resolved by `prompts:/<name>@<alias>` (or `prompts:/<name>/<version>`) URIs — the registry shape the serving ecosystem already uses. The difference: each registered version and each alias **promotion** seals as a hash-chained **`prompt/1`** receipt, and a version's id *is* its template, so swapping the text under a name changes the id and re-pointing an alias is a signed entry in an ordered log. The exact instructions an agent ran under — and which version was live when — become tamper-evident artifacts, not a row someone with write access can edit.
- **`jcp-trace`** — ingests an agent run's **OpenTelemetry GenAI trace** (`gen_ai.*` semantic conventions: `chat` / `tool` / `retriever` spans with token usage) and its **evaluation assessments** (judge / human / code feedback), attaches **per-span joules** (measured where a meter is present, else `Estimator` from token counts — provenance never silently upgrades), and seals one **`eval/1` JCR-1 receipt**: the per-span token + joule breakdown with the headline `uj_per_accepted_token` at the **weakest** provenance present, the assessments, a deterministic **quality-gate** verdict (the sealed, declarative analog of a CI assert), and a **readiness projection** an ARL validation-axis claim consumes. The observability stacks *record* a trace and *log* a judge score in a table; this is the interop projection that **seals** the same trace, puts a joule on it, and hash-chains it via `EvalLog` — no new cryptography. The **gate is content-addressed**, so the sealed receipt commits to exactly which bar it was judged against (a tamper-evident artifact, not a mutable in-code `assert`), and **`align_threshold` / `Judge`** calibrate a judge's decision threshold to human pass/fail labels — judge alignment as a reproducible number, not a UI click. Behind the `server` feature, a std-only **OTLP `/v1/traces` receiver** lets any OpenTelemetry exporter POST a `gen_ai.*` trace (with its assessments) and get the sealed `eval/1` receipt back the instant the run ends.
- **`jcp-intercept`** — **attested interceptor chains**: a composable, ordered pipeline of transforms *around* each model and tool call — the `before_model` / `after_model` / `before_tool` seams an agent's middleware stack runs, composed onion-order (forward in, reverse out) and stopping at the first refusal or human-approval hold. An interceptor may inspect or **rewrite** the request (redact a secret before it reaches the model, trim stale context), **refuse** it, or **hold** it for a human. The difference from the incumbent's arbitrary in-process hooks: every step seals an **`intercept/1`** JCR-1 receipt carrying the request digest **before and after** the step (so a redaction or a trim is provable — the content itself never sealed, only its hash), the seam, the outcome, and the **metered joules** at their honest provenance, hash-chained — the exact transform pipeline that produced a model request becomes reconstructable and verifiable by anyone, and a step cannot run un-sealed. Three reference interceptors — `Redact` (a content transform), `CallLimit` (a guardrail), `Approve` (a human-in-the-loop hold) — each reuse a primitive the family already holds rather than reinventing it.
- **`jcp-intercept`** — **attested interceptor chains**: a composable, ordered pipeline of transforms *around* each model and tool call — the `before_model` / `after_model` / `before_tool` seams an agent's middleware stack runs, composed onion-order (forward in, reverse out) and stopping at the first refusal or human-approval hold. An interceptor may inspect or **rewrite** the request (redact a secret before it reaches the model, trim stale context), **refuse** it, or **hold** it for a human. The difference from the incumbent's arbitrary in-process hooks: every step seals an **`intercept/1`** JCR-1 receipt carrying the request digest **before and after** the step (so a redaction or a trim is provable — the content itself never sealed, only its hash), the seam, the outcome, and the **metered joules** at their honest provenance (where the meter reads a counter, a seam pass is one reading divided among its steps by each step's thread CPU time, so the steps sum to it exactly and each share is graded `ModelBased`), hash-chained — the exact transform pipeline that produced a model request becomes reconstructable and verifiable by anyone, and a step cannot run un-sealed. Three reference interceptors — `Redact` (a content transform), `CallLimit` (a guardrail), `Approve` (a human-in-the-loop hold) — each reuse a primitive the family already holds rather than reinventing it.
- **`jcp-efficiency`** — the **energy-efficiency spec**, surfaced as a sealed receipt. The dark-factory economics turn on four figures — **joules per accepted token, tokens per second, value per token, and the tokens/joules/dollars spent per COMPLETED, VERIFIED unit of work** — and the incumbents publish, at best, a *rate* (cost per token) that can't tell you whether the spend bought a result. An **`efficiency/1`** JCR-1 receipt records all four at honest provenance, hash-chained, with the one thing the rates omit: the **outcome anchor** (`completed` + a verdict). A measurement that didn't complete is spend that bought nothing, and the receipt says so. `aggregate` then splits value-delivered from total spend across a chain — charging *every* attempt (including the failed ones) against the results actually delivered — so `tokens_per_completed_unit` / `uj_per_completed_unit` / `value_per_joule` become the honest efficiency a "cost per token" number is designed to hide. Integers and strings only (JCR-1 forbids bools/floats); the rate figures recompute from the sealed inputs on read.
- **`jcp-energy`** — the metering ladder (SPEC §5.1): RAPL package counters (Linux powercap, wraparound-corrected, `HwShunt`, only while every package counter is live — `rapl_status()`), NVML GPU energy behind the `nvml` feature (`HwShunt` and `Partial`, since it does not read the CPU's share; only while live — `nvml_status()`), the macOS kernel's per-process CPU energy count (`proc_pid_rusage`'s `ri_energy_nj`; `HwShunt`, whole for in-process CPU work, only while live — `process_energy_status()`; it covers this process's threads, not children, the GPU or DRAM, so a span that reaps a child process or ends with one is `Partial`; each end of a span is read only once the metering thread is seen to have left its core, because the kernel counts a thread's energy into its process's only then, and a span with an end not so checked is the estimate, never a reading; it is live on macOS 27.0.1 where IOReport's CPU rails are not), Apple Silicon IOReport (`HwShunt`, only while every CPU energy rail, each die's on a multi-die part, is live — on macOS 27.0.1 that rail is republished in lumps, so there the rung is declined and `ioreport_status()` says why; `Meter::detect_explained()` names the rung selected and why each one above it was declined), a time×power model at a power the host calibrated (`ModelBased`, `Meter::time_power`), and estimates no host calibrated (`Estimator`): time × the documented default power — the rung `Meter::detect()` falls to, always available — and a constant per-op figure. A vendor counter's reading is real but vendor-opaque and relayed by the platform, and it counts as a reading only while the counter is live (SPEC §5). Measured when the platform exposes it, estimated when it doesn't — and the provenance always says which; a hardware backend that fails mid-reading degrades honestly to the default-power estimate.
- **`jcp-gateway`** — the **energy-native MCP gateway**: a JCP enforcement point that sits in front of an MCP server so an existing client/server pair keeps its transport while every `tools/call` becomes capability-scoped, joule-budgeted, and sealed. The novel mechanic is two-phase metered enforcement: the call is *authorized* before it runs (no debit, because the cost isn't yet known), the upstream call is **metered** by the sibling Sandbox standard's energy probe (Apple Silicon IOReport, RAPL, NVML — vendor energy counters, whose readings are real but vendor-opaque and relayed by the platform, and which count as readings only while the counter is live for the work, SPEC §5; otherwise the figure is an estimate and is graded as one), and the metered joules are debited from the grant — so the budget that stops a runaway agent is denominated in joules, not a token estimate. Where the upstream server is a local process whose CPU energy the host's kernel counts live (macOS `proc_pid_rusage`), each call is read on that server's own process tree, the work's own process rather than the machine (SPEC §5.1, rung 3): sealed `HwShunt`, and `Whole` only when nothing was seen missing from the tree (no descendant reaped or exited in the span, no GPU client held, no in-process Neural Engine memory, every member read and quiet at both ends) and the operator has declared, with `--upstream-self-contained`, that the server hands no work to a process outside its tree; otherwise `Partial`, with the response's `_meta` naming why. Every decision, allow and deny, seals into a **hash-chained receipt log**: excise, reorder, or splice a receipt and the chain breaks at that point. A denied call never reaches the server (the live 2025–26 MCP exploits land on tool *output*, which the gateway frames as untrusted `data` that can never exercise a capability), and the `jcp-gateway` binary runs it over stdio in front of any MCP-server subprocess. This is the chokepoint the AI-gateway field occupies, on the two axes nobody there holds: the unit of account is the joule, and the audit is cryptographic and tamper-evident rather than a dollar figure and a span.

1
jcp/jcp-rs/Cargo.lock generated
View file

@ -657,6 +657,7 @@ dependencies = [
"jcp-energy",
"jcp-receipt",
"jcp-schema",
"libc",
"serde_json",
"thiserror 2.0.20",
]

View file

@ -5527,6 +5527,16 @@ impl Ladder<'static> {
}
}
/// CPU time the calling thread has used, user and system
/// (`clock_gettime(CLOCK_THREAD_CPUTIME_ID)`), or `None` where the clock cannot
/// be read. Exact to the nanosecond and live: unlike a process's energy count,
/// which a thread's work joins only when the thread leaves its core, the
/// thread's CPU time is computed at the read. So it divides a span's reading
/// among work that ran in turn on one thread, at no cost to the span.
pub fn thread_cpu_time() -> Option<std::time::Duration> {
liveness::thread_cpu_time()
}
impl Meter {
/// Pick the best backend this environment exposes. Never fails: when no
/// hardware interface exists the result is the time×power figure at the

View file

@ -18,3 +18,8 @@ thiserror = { workspace = true }
[lints]
workspace = true
# The real-counter test reads this process's energy count with its own FFI:
# an oracle that shares no code with the meter.
[target.'cfg(target_os = "macos")'.dev-dependencies]
libc = "0.2"

View file

@ -22,6 +22,18 @@
//! and a figure that covers only part of the step says so (`"coverage":
//! "Partial"`), rather than being sealed as the whole step's energy.
//!
//! Where the meter reads a counter, a seam is metered as one span around its
//! whole pass, not one per step ([`Chain::before_model`]). A counter read for
//! one process first takes the reading thread off its core, so each read
//! costs tens of microseconds and its own wake-up; an interceptor step is
//! often a few microseconds, which a span of its own would read as mostly the
//! meter. The pass's reading is divided among its steps by each step's own
//! thread CPU time ([`jcp_energy::thread_cpu_time`], exact and free to
//! read), so the steps' figures sum to the reading exactly; a step's share of
//! a reading is `ModelBased`, and a pass with one step keeps the reading's
//! own grade. A meter that reads nothing (a model or a constant) costs
//! nothing to run and still meters each step on its own.
//!
//! A [`Chain`] runs its interceptors forward through the `before_*` seams and in
//! reverse through `after_model` (the onion unwinds), stopping at the first
//! [`Step::Halt`] (a guardrail refused) or [`Step::Hold`] (a human must approve).
@ -32,7 +44,8 @@
use ed25519_dalek::SigningKey;
use jcp_energy::Meter;
use jcp_receipt::jcr1;
use jcp_schema::{ContentId, Coverage, Energy};
use jcp_schema::{ContentId, Coverage, Energy, Provenance};
use std::time::Duration;
use serde_json::{json, Value};
// ───────────────── the request/response/tool an interceptor transforms ─────────────────
@ -269,59 +282,98 @@ impl Chain {
/// Run the request through every interceptor's `before_model` seam, forward
/// order, sealing each step. Stops at the first refusal/hold.
///
/// Where the meter reads a counter, the whole pass is one span and each
/// step is sealed with its share of it by thread CPU time (see the crate
/// docs); otherwise each step is metered on its own.
pub fn before_model(&mut self, req: &mut ModelRequest) -> ChainOutcome {
for idx in 0..self.interceptors.len() {
let order: Vec<usize> = (0..self.interceptors.len()).collect();
self.run_seam(Seam::BeforeModel, &order, |interceptor| {
let before = req.digest();
// Split-borrow: the meter (shared) and one interceptor (mut) are
// disjoint fields, so the metered call touches no other chain state.
let (step, energy) = {
let meter = &self.meter;
let interceptor = &mut self.interceptors[idx];
meter.measure(|| interceptor.before_model(req))
};
let after = req.digest();
let name = self.interceptors[idx].name().to_string();
self.seal(&name, Seam::BeforeModel, &step, before, after, energy);
if let Some(outcome) = terminal(&name, &step) {
return outcome;
}
}
ChainOutcome::Proceed
let step = interceptor.before_model(req);
(step, before, req.digest())
})
}
/// Run the response through every interceptor's `after_model` seam, **reverse**
/// order (the onion unwinding), sealing each step.
pub fn after_model(&mut self, req: &ModelRequest, resp: &mut ModelResponse) -> ChainOutcome {
for idx in (0..self.interceptors.len()).rev() {
let order: Vec<usize> = (0..self.interceptors.len()).rev().collect();
self.run_seam(Seam::AfterModel, &order, |interceptor| {
let before = resp.digest();
let (step, energy) = {
let meter = &self.meter;
let interceptor = &mut self.interceptors[idx];
meter.measure(|| interceptor.after_model(req, resp))
};
let after = resp.digest();
let name = self.interceptors[idx].name().to_string();
self.seal(&name, Seam::AfterModel, &step, before, after, energy);
if let Some(outcome) = terminal(&name, &step) {
return outcome;
}
}
ChainOutcome::Proceed
let step = interceptor.after_model(req, resp);
(step, before, resp.digest())
})
}
/// Run a tool call through every interceptor's `before_tool` seam, forward
/// order, sealing each step. Stops at the first refusal/hold.
pub fn before_tool(&mut self, call: &mut ToolCall) -> ChainOutcome {
for idx in 0..self.interceptors.len() {
let order: Vec<usize> = (0..self.interceptors.len()).collect();
self.run_seam(Seam::BeforeTool, &order, |interceptor| {
let before = call.digest();
let (step, energy) = {
let step = interceptor.before_tool(call);
(step, before, call.digest())
})
}
/// Run one seam over the interceptors in `order`, stopping after the
/// first step that halts or holds, and seal every step that ran.
///
/// A meter that reads a counter meters the pass as one span; the steps are
/// sealed after it closes, each with its [`apportion`]ed share by its own
/// thread CPU time, in the order they ran. Any other meter meters each step
/// as it runs, as one span of its own.
fn run_seam(
&mut self,
seam: Seam,
order: &[usize],
mut step_at: impl FnMut(&mut dyn Interceptor) -> (Step, ContentId, ContentId),
) -> ChainOutcome {
if order.is_empty() {
return ChainOutcome::Proceed;
}
if self.meter.provenance() == Provenance::HwShunt {
let (ran, pass) = {
let meter = &self.meter;
let interceptors = &mut self.interceptors;
meter.measure(|| {
let mut ran = Vec::with_capacity(order.len());
for &idx in order {
let cpu_before = jcp_energy::thread_cpu_time();
let (step, before, after) = step_at(interceptors[idx].as_mut());
let cpu = match (cpu_before, jcp_energy::thread_cpu_time()) {
(Some(a), Some(b)) => Some(b.saturating_sub(a)),
_ => None,
};
let stops = !matches!(step, Step::Continue);
ran.push((idx, step, before, after, cpu));
if stops {
break;
}
}
ran
})
};
let cpu: Vec<Option<Duration>> = ran.iter().map(|r| r.4).collect();
let mut outcome = ChainOutcome::Proceed;
for ((idx, step, before, after, _), energy) in ran.into_iter().zip(apportion(&pass, &cpu)) {
let name = self.interceptors[idx].name().to_string();
self.seal(&name, seam, &step, before, after, energy);
if let Some(stopped) = terminal(&name, &step) {
outcome = stopped;
}
}
return outcome;
}
for &idx in order {
let ((step, before, after), energy) = {
let meter = &self.meter;
let interceptor = &mut self.interceptors[idx];
meter.measure(|| interceptor.before_tool(call))
meter.measure(|| step_at(interceptor.as_mut()))
};
let after = call.digest();
let name = self.interceptors[idx].name().to_string();
self.seal(&name, Seam::BeforeTool, &step, before, after, energy);
self.seal(&name, seam, &step, before, after, energy);
if let Some(outcome) = terminal(&name, &step) {
return outcome;
}
@ -371,6 +423,59 @@ impl Chain {
}
}
/// Divide one pass's figure among the steps that ran in it, by each step's
/// thread CPU time (`cpu`, in run order): integer micro-joules that sum to the
/// pass's figure exactly (largest remainder; a tie goes to the earlier step).
///
/// The shares are weighted by CPU time only where every step's was read and
/// they are not all zero; otherwise equally. A share of a reading is a model of
/// how the reading divides, so a pass graded `HwShunt` gives its steps
/// `ModelBased` shares; any other grade is kept. A pass of one step is not
/// divided and keeps its figure as it is. Coverage is the pass's: a `Partial`
/// reading gives `Partial` shares. The pass's span also holds the chain's own
/// work between steps (digests) and the meter's closing wake-up; all of it is
/// divided among the steps, so no step is under its own energy.
fn apportion(pass: &Energy, cpu: &[Option<Duration>]) -> Vec<Energy> {
let n = cpu.len();
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![*pass];
}
let total = pass.uj();
let weights: Vec<u128> = match cpu.iter().map(|c| c.map(|d| d.as_nanos())).collect::<Option<Vec<u128>>>() {
Some(w) if w.iter().any(|&x| x > 0) => w,
_ => vec![1; n],
};
let sum: u128 = weights.iter().sum();
let mut shares: Vec<u64> = weights.iter().map(|&w| (u128::from(total) * w / sum) as u64).collect();
let mut left = total - shares.iter().sum::<u64>();
// Largest remainder first; equal remainders go to the earlier step.
let mut order: Vec<usize> = (0..n).collect();
order.sort_by_key(|&i| std::cmp::Reverse(u128::from(total) * weights[i] % sum));
for &i in &order {
if left == 0 {
break;
}
shares[i] += 1;
left -= 1;
}
let provenance = match pass.provenance {
Provenance::HwShunt => Provenance::ModelBased,
other => other,
};
shares
.into_iter()
.map(|uj| Energy {
measured_uj: None,
estimated_uj: uj,
provenance,
coverage: pass.coverage,
})
.collect()
}
fn terminal(name: &str, step: &Step) -> Option<ChainOutcome> {
match step {
Step::Continue => None,
@ -791,6 +896,58 @@ mod tests {
assert!(verify(&env, c.steward(), &c.steward_pubkey()).is_err());
}
fn ns(n: u64) -> Option<Duration> {
Some(Duration::from_nanos(n))
}
/// A pass's reading divides among its steps by thread CPU time, in whole
/// micro-joules that sum to the reading exactly, as `ModelBased` shares.
#[test]
fn a_reading_divides_among_its_steps_by_cpu_time_and_sums_exactly() {
let pass = Energy::measured(1_000);
let shares = apportion(&pass, &[ns(1_000), ns(2_000), ns(7_000)]);
assert_eq!(shares.iter().map(Energy::uj).collect::<Vec<_>>(), vec![100, 200, 700]);
for e in &shares {
assert_eq!((e.provenance, e.coverage, e.measured_uj), (Provenance::ModelBased, Coverage::Whole, None));
e.check_provenance().unwrap();
}
// Remainders: 1000 µJ over thirds is 333/333/334, never 999 or 1002.
for (cpu, want) in [
(vec![ns(1), ns(1), ns(1)], vec![334, 333, 333]),
(vec![ns(3), ns(3), ns(1)], vec![429, 428, 143]),
(vec![ns(5), ns(0), ns(5)], vec![500, 0, 500]),
] {
let got: Vec<u64> = apportion(&pass, &cpu).iter().map(Energy::uj).collect();
assert_eq!(got, want, "{cpu:?}");
assert_eq!(got.iter().sum::<u64>(), 1_000);
}
}
/// One step is not divided: it keeps the reading, grade and all.
#[test]
fn a_pass_of_one_step_keeps_its_reading() {
let pass = Energy::measured(1_234);
assert_eq!(apportion(&pass, &[ns(9)]), vec![pass]);
assert!(apportion(&pass, &[]).is_empty());
}
/// Without every step's CPU time, or with none of it non-zero, the
/// reading divides equally; a partial reading gives partial shares; a
/// grade below a reading is kept.
#[test]
fn shares_fall_back_to_equal_and_keep_the_passs_coverage_and_lower_grade() {
let got: Vec<u64> = apportion(&Energy::measured(10), &[ns(5), None]).iter().map(Energy::uj).collect();
assert_eq!(got, vec![5, 5]);
let got: Vec<u64> = apportion(&Energy::measured(10), &[ns(0), ns(0)]).iter().map(Energy::uj).collect();
assert_eq!(got, vec![5, 5]);
for e in apportion(&Energy::measured_partial(9), &[ns(1), ns(2)]) {
assert_eq!((e.provenance, e.coverage), (Provenance::ModelBased, Coverage::Partial));
}
for e in apportion(&Energy::estimator(9), &[ns(1), ns(2)]) {
assert_eq!(e.provenance, Provenance::Estimator);
}
}
#[test]
fn energy_is_metered_and_accumulates_honestly() {
let mut c = chain()

View file

@ -0,0 +1,115 @@
//! A seam metered on this process's own energy count, as one span around the
//! whole pass, against an oracle that shares no code with the meter.
//!
//! Three interceptors each burn a different amount of their thread's CPU time
//! in one `before_model` pass. Each step is sealed with its share of the
//! pass's reading by thread CPU time (`ModelBased`), the shares sum to the
//! chain's figure, and that figure is the process's own energy over the pass:
//! no more than an oracle's window around it, and most of it. The oracle takes
//! its thread off its core its own way before each read (sleeps until the
//! process's context-switch count moves), then reads `ri_energy_nj` into a raw
//! buffer at the SDK's offset.
#![cfg(target_os = "macos")]
use ed25519_dalek::SigningKey;
use jcp_energy::Meter;
use jcp_intercept::{verify, Chain, Interceptor, ModelRequest, Step};
use jcp_schema::Provenance;
use std::time::Duration;
/// Burn `cpu` of this thread's own CPU time.
fn burn(cpu: Duration) {
let start = jcp_energy::thread_cpu_time().expect("the thread clock");
let mut x = 1u64;
while jcp_energy::thread_cpu_time().expect("the thread clock") - start < cpu {
for _ in 0..10_000 {
x = std::hint::black_box(x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1));
}
}
}
struct Burner {
name: &'static str,
cpu: Duration,
}
impl Interceptor for Burner {
fn name(&self) -> &str {
self.name
}
fn before_model(&mut self, _req: &mut ModelRequest) -> Step {
burn(self.cpu);
Step::Continue
}
}
/// This process's `ri_energy_nj` (`proc_pid_rusage`, `RUSAGE_INFO_V6`, the
/// u64 at byte 336), after this thread has left its core: sleep 1, 2, 4, 8 ms
/// until the process's context-switch count (`pti_csw` would do; the rusage
/// `ri_user_time` is not enough) has moved. `ru_nvcsw + ru_nivcsw` of this
/// process is read with `getrusage`.
fn oracle_nj() -> u64 {
fn switches() -> i64 {
// SAFETY: `ru` is a valid, writable `rusage`.
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
assert_eq!(unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }, 0);
ru.ru_nvcsw + ru.ru_nivcsw
}
let before = switches();
for ms in [1, 2, 4, 8] {
std::thread::sleep(Duration::from_millis(ms));
if switches() > before {
break;
}
}
let mut buf = [0u64; 58];
// SAFETY: `buf` is 464 writable bytes, the size of `rusage_info_v6`.
let rc = unsafe { libc::proc_pid_rusage(libc::getpid(), 6, buf.as_mut_ptr() as *mut libc::rusage_info_t) };
assert_eq!(rc, 0, "proc_pid_rusage");
buf[336 / 8]
}
#[test]
fn a_pass_is_one_reading_divided_among_its_steps_by_cpu_time() {
let meter = Meter::detect();
assert_eq!(meter.provenance(), Provenance::HwShunt, "this host's meter does not read a counter: {:?}", meter.rung());
let steward = SigningKey::from_bytes(&[7u8; 32]);
let mut misses = 0;
for round in 0..10 {
let mut c = Chain::new(steward.clone(), "did:jcp:test-intercept-pass")
.with_meter(Meter::detect())
.with(Box::new(Burner { name: "one", cpu: Duration::from_millis(2) }))
.with(Box::new(Burner { name: "two", cpu: Duration::from_millis(4) }))
.with(Box::new(Burner { name: "four", cpu: Duration::from_millis(8) }));
let before = oracle_nj();
assert!(c.before_model(&mut ModelRequest::default()).proceeds());
let oracle = oracle_nj() - before;
let recs: Vec<_> =
c.receipts().iter().map(|e| verify(e, c.steward(), &c.steward_pubkey()).expect("a sealed step verifies")).collect();
assert_eq!(recs.iter().map(|r| r.interceptor.as_str()).collect::<Vec<_>>(), ["one", "two", "four"]);
let figures: Vec<u64> = recs.iter().map(|r| r.energy_uj).collect();
let sum: u64 = figures.iter().sum();
eprintln!(
"intercept pass: round {round}: steps {figures:?} µJ ({:?}), sum {sum} µJ, chain {} µJ, oracle window {:.0} µJ",
recs.iter().map(|r| r.provenance.as_str()).collect::<Vec<_>>(),
c.energy_uj(),
oracle as f64 / 1e3
);
assert_eq!(sum, c.energy_uj(), "the steps' shares are not the chain's figure");
if recs.iter().any(|r| r.provenance != "ModelBased") {
// A pass whose ends could not be read is the estimate, divided as one.
assert!(recs.iter().all(|r| r.provenance == "Estimator"), "{recs:?}");
misses += 1;
continue;
}
// The shares follow the steps' own CPU time: 2 : 4 : 8 ms.
assert!(figures[0] < figures[1] && figures[1] < figures[2], "{figures:?}");
// The pass is the process's own energy over it: within the oracle's
// window, and most of it.
assert!(sum * 1_000 <= oracle + 1_000, "the pass ({sum} µJ) exceeds the oracle's window ({oracle} nJ)");
assert!(sum * 1_000 * 3 >= oracle * 2, "the pass ({sum} µJ) is under two thirds of the oracle's window ({oracle} nJ)");
}
assert!(misses <= 1, "{misses} of 10 passes could not be read");
}