jcp: energy traceability across operation, activity and outcome #138

Merged
dcharlot merged 3 commits from efficiency/energy-trace into main 2026-10-10 09:49:57 -04:00
9 changed files with 3210 additions and 0 deletions

View file

@ -476,6 +476,148 @@ interceptor chain (`jcp-intercept`) carries it as `coverage`, beside its
`"Partial"`; a verifier MUST refuse any other value, `"Whole"` included, rather
than read it either way.
### 5.3 Traceability across operation, activity and outcome (REQUIRED where an outcome claims energy)
§5.1 grades a figure and §5.2 says how much of the work it covers. Neither says
which *level* the figure belongs to, and a deployed system is answerable at
three that are not interchangeable:
| level | artifact | what it answers |
|---|---|---|
| **operation** | the `jcp/2` work receipt (§8) | what one unit of work cost |
| **activity** | the AR-1 action receipt | what one *authorised* thing did — grant checked, environment attested, work performed, effect committed, bytes egressed |
| **outcome** | `efficiency/1` | whether any of it bought anything |
Two rules follow, and both exist because the levels were previously joined by
nothing.
**An outcome's energy MUST name its activities by content-id.** An outcome that
identifies its unit of work by a label can assert any total it likes, and the
level where the number is most load-bearing is then the level with no binding
at all. A verifier MUST be able to resolve each named activity and re-add the
figures. An outcome naming no activity is not traceable and MUST NOT be
presented as though it were.
**The difference MUST be named, not absorbed.** The sum of an outcome's
operations is not the outcome's energy. Authorising, attesting, verifying and
pushing bytes off the device all cost joules that no operation receipt claims —
AR-1 carries exactly one energy figure and it belongs to the work. That
difference is the cost of being accountable, and it is reported as a signed
residual in exact integer microjoules:
```text
residual_uj = outcome_total_uj − Σ activity.joules_uj
```
The residual MUST NOT be clamped. Negative means the bound activities claim more
than the outcome admits — double-counted work, or an activity bound to the wrong
outcome — which is a finding. An activity bound twice MUST be refused before the
sum: counting it twice inflates the accounted figure and shrinks the residual,
flattering the trace in the one direction that matters.
The composite provenance is the **floor** across the outcome and every activity,
never an average and never the best: one `Estimator` part makes the whole trace
estimated, because a trace cannot be more measured than the part of it that was
not measured. Coverage takes the floor the same way. Aggregation MUST NOT be a
step at which a grade improves.
### 5.4 Dynamism: what a deviation establishes, and what it never does (OPTIONAL)
A total cannot distinguish a throttled part from a contended host from a cold
cache, and on silicon nobody has profiled it cannot say what normal was. Where
an implementation carries energy as a time-resolved series, the following hold.
**Provenance and coverage are per-window.** Counters do not have uniform
resolution — a package counter may be continuous while an accelerator counter is
live only while a queue is — so one grade for a series is a grade for its best
window.
**A baseline MUST declare where it was measured, and MUST NOT be applied
outside it.** An expectation is a fit, and a fit is only a fit somewhere.
Comparing against a baseline taken on different silicon, at a different
occupancy, or at a coarser sampling window manufactures deviations that say
nothing about the workload. An undeclared baseline is inapplicable, so
forgetting to declare under-claims rather than inventing findings.
**A cause explains a deviation only with evidence.** A named operational cause
— thermal limit, host contention, cold start, frequency scaling, a meter
coverage gap — MUST carry a content-id for the reading that supports it.
Otherwise "explained" becomes the laundering channel: anything inconvenient is
labelled and disappears.
**Absence of detection is not absence.** A phenomenon shorter than one sampling
window is invisible to it. That case is *unresolvable*, never *within band*.
Reporting nothing found at a resolution that could not have found it is the
quietest available way to be wrong.
**There MUST NOT be a verdict of intent.** Energy does not carry intent. A
deviation says the joules did not match the expectation; it cannot say why, and
on an uncharted stack it cannot say the expectation was right. An unexplained
window is a request for a look, not a finding of wrongdoing, and a conformant
implementation MUST NOT collapse unexplained, inapplicable and unresolvable
windows into a single score — they mean "look at this", "we have no yardstick"
and "we could not have seen it", which call for different responses.
### 5.5 Conservation, not anomaly detection (REQUIRED where a grant is reconciled)
The strong instrument is conservation, and it needs no model of normal.
Because every figure is integer microjoules and the sums are exact, the smallest
representable discrepancy against a grant of `N` µJ is `1_000_000 / N` parts per
million — on a one-joule grant, **1 ppm**. A drift of a fraction of a percent is
three orders of magnitude above that floor. The question "do these joules fit
anywhere" is therefore answerable arithmetically, where "is this unusual" is not.
This property does not survive the usual construction, and that is the point.
Where an energy figure is *derived from a performance proxy* — utilisation times
a thermal design figure, tokens times a coefficient, instructions times
picojoules-per-operation — its model error is tens of percent, and a
fractional-percent drift is invisible by construction: it sits far inside the
error bar of the estimator that produced it. Deriving energy from performance
does not merely lose accuracy, it destroys the ability to notice that the books
do not balance. A conformant implementation MUST report whether the floor it
quotes rests on a measured figure or a modelled one; quoting a 1 ppm floor over
`Estimator` windows is exact arithmetic over a model's opinion.
### 5.6 The physical bound: refutation, never verification (OPTIONAL)
The joules were spent and the heat left the die; that quantity is not
negotiable. The *measurement* of it is exactly as forgeable as any other report
— a counter can be lied about, a figure fabricated, a `HwShunt` grade stamped on
a number from nowhere.
What energy has that a synthetic unit does not is an **external referent**. A
token count, a call count, a request rate can be any number whatsoever: nothing
outside the accounting system constrains them, so a fabricated one is
indistinguishable from a true one. A joule count is bounded by the part's power
envelope and the window's duration.
An implementation MAY therefore refute a figure that exceeds what the part could
have drawn, or falls below what a powered, executing part must draw — the second
being the shape a frozen counter takes, and the reason §5's liveness rule
exists. Bounds SHOULD be deliberately permissive (burst rating for the ceiling,
absolute idle for the floor) so that a refutation is unassailable.
Passing a physical bound MUST NOT be reported as verification. A fabricated
figure inside the envelope passes, and must: the bound establishes only that the
figure is not impossible.
**The one thing that cannot be faked is how the energy was consumed.** A report
of a profile is just numbers, but a part cannot draw the profile of one
computation while performing another. This closes the hole conservation leaves:
a total can be balanced by padding — invent an operation and attribute the
surplus to it — but a fictional operation contributes no draw, so the observed
character stops matching the declared work mix.
Character is also what survives unfamiliar hardware. The magnitudes move with
the process node, the power envelope and the memory hierarchy, which is why a
band baseline fails there; a compute-bound steady state is flat on any part, a
cold start ramps on any part, a queue-driven workload alternates on any part. A
declared shape MUST be declared in advance — classifying the observed shape and
then deciding what it must have been is post-hoc pattern matching. An
inconsistency refutes the declaration. It attributes no cause and names no
intent.
## 6. The Frame (REQUIRED)
Every message is a typed Frame — one envelope across the tool layer (MCP's job)

File diff suppressed because it is too large Load diff

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

@ -596,6 +596,7 @@ dependencies = [
"hex",
"jcp-receipt",
"jcp-schema",
"serde",
"serde_json",
"thiserror 2.0.20",
]

View file

@ -11,6 +11,7 @@ description = "The energy-efficiency spec for agentic work, surfaced as a sealed
jcp-schema = { workspace = true }
jcp-receipt = { workspace = true }
ed25519-dalek = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
hex = { workspace = true }
thiserror = { workspace = true }

View file

@ -0,0 +1,235 @@
//! Emit the `energy_trace/1` conformance pack to `jcp/conformance/v1/`.
//!
//! SPEC §5.3–5.6 states rules; a rule nothing emits is a proposal. This writes
//! the vectors a third party replays to check an independent implementation —
//! positives *and* the refusals, since the refusals are the load-bearing half:
//! an implementation that accepts an unbound outcome, a double-counted
//! activity, or a frozen counter has failed in the direction that matters.
//!
//! Run: `cargo run -p jcp-efficiency --bin gen_trace_vectors -- ../conformance/v1`
use std::path::PathBuf;
use jcp_efficiency::dynamics::{
Baseline, DrawShape, EnergyWindow, OperationalCause, PowerEnvelope,
};
use jcp_efficiency::trace::{ActivityRef, OutcomeTrace};
use jcp_schema::{ContentId, Coverage, Energy, Provenance};
use serde_json::{json, Value};
fn energy(uj: u64, p: Provenance, c: Coverage) -> Energy {
Energy { measured_uj: Some(uj), estimated_uj: uj, provenance: p, coverage: c }
}
fn act(tag: &[u8], uj: u64, p: Provenance, c: Coverage) -> ActivityRef {
ActivityRef::new(ContentId::of_bytes(tag), uj, p, c)
}
fn win(i: u64, uj: u64, p: Provenance) -> EnergyWindow {
EnergyWindow {
started_ns: i * 1_000_000,
duration_ns: 1_000_000,
joules_uj: uj,
provenance: p,
coverage: Coverage::Whole,
declared_work: None,
}
}
fn series(vals: &[u64]) -> Vec<EnergyWindow> {
vals.iter().enumerate().map(|(i, &v)| win(i as u64, v, Provenance::HwShunt)).collect()
}
/// A case carries the inputs and the verdict a conformant implementation must
/// reach. Reproducing the verdict is the whole test; a case that only carried
/// inputs would check that an implementation runs, not that it agrees.
fn case(note: &str, input: Value, expected: Value) -> Value {
json!({ "note": note, "input": input, "expected": expected })
}
fn main() {
let out = std::env::args()
.nth(1)
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1")
});
std::fs::create_dir_all(&out).expect("create output dir");
// ---- §5.3 conservation ------------------------------------------------
let overhead = OutcomeTrace::new(
energy(1_000_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![
act(b"a1", 400_000, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 350_000, Provenance::HwShunt, Coverage::Whole),
],
);
let r = overhead.reconcile().expect("reconciles");
assert_eq!(r.residual_uj, 250_000);
let floored = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![
act(b"a1", 300, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 300, Provenance::Estimator, Coverage::Partial),
],
);
let fr = floored.reconcile().expect("reconciles");
let unbound = OutcomeTrace::new(
energy(1_000_000, Provenance::HwShunt, Coverage::Whole), true, vec![],
);
let dup_act = act(b"same", 100, Provenance::HwShunt, Coverage::Whole);
let dup = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole), true,
vec![dup_act.clone(), dup_act],
);
let over = OutcomeTrace::new(
energy(100, Provenance::HwShunt, Coverage::Whole), true,
vec![act(b"a1", 90, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 50, Provenance::HwShunt, Coverage::Whole)],
);
let conservation = json!({
"positive": [
case("the residual names what no operation claims",
json!(&overhead),
json!({"accounted_uj": r.accounted_uj, "residual_uj": r.residual_uj,
"provenance_floor": r.provenance_floor, "coverage_floor": r.coverage_floor,
"unattributed_ppm": r.unattributed_ppm(overhead.total_uj())})),
case("provenance and coverage take the floor, never the average",
json!(&floored),
json!({"accounted_uj": fr.accounted_uj, "residual_uj": fr.residual_uj,
"provenance_floor": fr.provenance_floor, "coverage_floor": fr.coverage_floor})),
],
"negative": [
case("an outcome traceable to nothing is refused", json!(&unbound),
json!({"error": "Unbound"})),
case("a double-bound activity is refused before the sum", json!(&dup),
json!({"error": "DuplicateActivity"})),
case("activities claiming more than the outcome are refused", json!(&over),
json!({"error": "OverAccounted", "accounted_uj": 140, "total_uj": 100})),
]
});
// ---- §5.5 conservation against a grant --------------------------------
let budget = json!({
"positive": [
case("half a percent over a one-joule grant is 5000 ppm, floor 1 ppm",
json!({"granted_uj": 1_000_000u64, "accounted_uj": 1_005_000u64,
"provenance_floor": Provenance::HwShunt}),
json!({"drift_ppm": 5_000, "detectable_floor_ppm": 1,
"drift_is_resolvable": true, "floor_is_physical": true})),
case("the same floor over modelled windows is exact and meaningless",
json!({"granted_uj": 1_000_000u64, "accounted_uj": 1_000_005u64,
"provenance_floor": Provenance::Estimator}),
json!({"drift_ppm": 5, "detectable_floor_ppm": 1,
"floor_is_physical": false})),
case("underspend is signed, not clamped: allowed joules nothing claimed",
json!({"granted_uj": 1_000_000u64, "accounted_uj": 300_000u64,
"provenance_floor": Provenance::HwShunt}),
json!({"drift_ppm": -700_000})),
],
"negative": [
case("a ratio against a zero grant is not a figure",
json!({"granted_uj": 0u64, "accounted_uj": 5u64}),
json!({"drift_ppm": Value::Null, "detectable_floor_ppm": Value::Null})),
]
});
// ---- §5.6 physical refutation -----------------------------------------
let env = PowerEnvelope {
peak_mw: 60_000,
idle_floor_mw: 1_000,
source: "burst rating, deliberately permissive".into(),
};
let frozen = EnergyWindow { duration_ns: 400_000_000, joules_uj: 0, ..win(0, 0, Provenance::HwShunt) };
let impossible = EnergyWindow { duration_ns: 1_000_000, joules_uj: 10_000_000, ..win(0, 0, Provenance::HwShunt) };
let plausible = EnergyWindow { duration_ns: 1_000_000, joules_uj: 30_000, ..win(0, 0, Provenance::HwShunt) };
let instant = EnergyWindow { duration_ns: 0, joules_uj: 999, ..win(0, 0, Provenance::HwShunt) };
let physical = json!({
"envelope": &env,
"cases": [
case("a frozen counter on a busy window is refuted from below",
json!(&frozen), json!({"verdict": "RefutedBelow", "floor_uj": 400_000})),
case("a figure past the burst envelope is refuted from above",
json!(&impossible), json!({"verdict": "RefutedAbove"})),
case("inside the envelope is NotRefuted — which is not verification",
json!(&plausible), json!({"verdict": "NotRefuted"})),
case("a zero-duration window is unboundable, not clean",
json!(&instant), json!({"verdict": "Unboundable"})),
]
});
// ---- §5.6 draw character ----------------------------------------------
let padded = series(&[300_000, 10, 10, 10, 10, 10]);
let flat = series(&[1_000, 1_020, 980, 1_010, 990, 1_000]);
let scaled: Vec<u64> = [1_000u64, 1_020, 980, 1_010, 990, 1_000].iter().map(|v| v * 37).collect();
let ramp = series(&[100, 400, 700, 1_000, 1_100, 1_120]);
let bursty = series(&[100, 2_000, 100, 2_000, 100, 2_000]);
let shape = json!({
"idle_floor_uj": 100,
"cases": [
case("a total balanced by padding does not read as sustained",
json!({"declared": DrawShape::Sustained, "windows": &padded}),
json!({"verdict": "Inconsistent"})),
case("character is scale-invariant: same shape at 37x the magnitude",
json!({"declared": DrawShape::Sustained, "windows": &series(&scaled)}),
json!({"verdict": "Consistent", "shape": DrawShape::Sustained})),
case("flat is sustained", json!({"declared": DrawShape::Sustained, "windows": &flat}),
json!({"verdict": "Consistent", "shape": DrawShape::Sustained})),
case("rising then settling is a ramp", json!({"declared": DrawShape::Ramping, "windows": &ramp}),
json!({"verdict": "Consistent", "shape": DrawShape::Ramping})),
case("alternating is bursty", json!({"declared": DrawShape::Bursty, "windows": &bursty}),
json!({"verdict": "Consistent", "shape": DrawShape::Bursty})),
case("under three windows character is unresolvable, never consistent",
json!({"declared": DrawShape::Sustained, "windows": &series(&[1_000, 1_000])}),
json!({"verdict": "Unresolvable", "windows": 2})),
]
});
// ---- §5.4 baselines and causes ----------------------------------------
let w = win(0, 5_000, Provenance::HwShunt);
let undeclared = Baseline::new(1_000, 100, "harness:run-1", 1_000);
let declared = Baseline::new(1_000, 100, "harness:run-1", 1_000).declared_applicable();
let coarse = Baseline::new(1_000, 100, "harness:coarse", 10_000_000).declared_applicable();
let cause = OperationalCause::ThermalThrottle { reading: ContentId::of_bytes(b"thermal") };
let dynamism = json!({
"cases": [
case("an undeclared baseline makes no comparison",
json!({"window": &w, "baseline": &undeclared}),
json!({"deviation": "BaselineInapplicable"})),
case("a baseline fit coarser than the trace samples does not apply",
json!({"window": &w, "baseline": &coarse}),
json!({"deviation": "BaselineInapplicable"})),
case("outside the band with no evidenced cause stays unexplained",
json!({"window": &w, "baseline": &declared, "cause": Value::Null}),
json!({"deviation": "Unexplained", "excess_uj": 4_000})),
case("a cause with evidence explains; the token is the claim vocabulary",
json!({"window": &w, "baseline": &declared, "cause": &cause}),
json!({"deviation": "Explained", "cause_token": cause.token(),
"evidence": cause.evidence().to_hex(), "excess_uj": 4_000})),
],
"note": "there is no verdict of intent in this profile, by design"
});
let pack = json!({
"profile": "energy_trace/1",
"spec": "JCP v0.3 SPEC.md §5.3-5.6",
"conservation": conservation,
"budget": budget,
"physical": physical,
"shape": shape,
"dynamism": dynamism,
});
let path = out.join("energy_trace.json");
let body = serde_json::to_string_pretty(&pack).expect("serialize") + "\n";
std::fs::write(&path, body).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
println!("wrote {}", path.display());
}

View file

@ -0,0 +1,904 @@
//! Time-resolved energy, and the honest limit of what a deviation establishes.
//!
//! # Why a total is not enough
//!
//! [`crate::trace`] reconciles an outcome against its activities and names the
//! residual. That is conservation, and it is static: one scalar per outcome.
//! Real compute is not static. The same declared work costs different joules on
//! a throttled part, a contended host, a cold cache, or a frequency-scaled
//! core — and a single total cannot tell any of those apart from each other,
//! let alone from interference.
//!
//! So energy has to be carried as a **series**, each window graded on its own,
//! and compared against a baseline that says where it was measured.
//!
//! # The thing this module refuses to do
//!
//! There is no `Malicious` variant in [`Deviation`], and that is the most
//! important line in this file.
//!
//! Energy does not carry intent. A deviation says the joules did not match the
//! expectation; it cannot say why, and on a stack whose normal behaviour has
//! never been charted it cannot even say the expectation was right. A type with
//! a `Malicious` arm would be used — some caller would map `Unexplained` onto
//! it, a dashboard would colour it red, and a number with no evidential basis
//! would start informing decisions about people. That is the same failure as a
//! prover returning `Proved` because a stand-in said so, moved into security.
//!
//! What this module produces is a **partition**: attributable, explained by a
//! named cause *with evidence*, unexplained, or unresolvable. `Unexplained` is
//! a request for a look, not a verdict. The difference between interference and
//! an uncharted-but-legitimate stack is a question evidence answers, and this
//! makes that question askable instead of pre-answering it wrongly.
//!
//! # Explaining requires evidence, not a label
//!
//! [`OperationalCause`] never explains a deviation on its own. Each cause
//! carries a content-id for the reading that supports it — a thermal sample, a
//! co-tenancy attestation, a cache-state record. A cause asserted without
//! evidence leaves the window `Unexplained`, because otherwise "explained"
//! becomes the laundering channel: anything inconvenient gets labelled
//! `ThermalThrottle` and disappears.
//!
//! # Absence of detection is not absence
//!
//! A window is a sampling interval, and a phenomenon shorter than one window is
//! invisible to it. That case is [`Deviation::Unresolvable`], never
//! `WithinBand`. Reporting "nothing found" at a resolution that could not have
//! found it is the quietest way to be wrong, and the one an auditor is least
//! able to catch.
use serde::{Deserialize, Serialize};
use jcp_schema::{ContentId, Coverage, Provenance};
/// One time-resolved energy sample.
///
/// Provenance and coverage are per-window on purpose. Counters do not have
/// uniform resolution: a host may expose a package counter continuously and an
/// accelerator counter only while a queue is active, so the same trace can be
/// `HwShunt`/`Whole` in one window and `Estimator`/`Partial` in the next. A
/// single grade for the series would be a grade for its best window.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnergyWindow {
/// Monotonic start, nanoseconds.
pub started_ns: u64,
/// Window length, nanoseconds. This is the trace's resolution floor.
pub duration_ns: u64,
/// Energy attributed to this window.
pub joules_uj: u64,
/// How this window's figure was acquired.
pub provenance: Provenance,
/// How much of the window's work the figure accounts for.
pub coverage: Coverage,
/// The operation this window is declared to cover, when one is named.
pub declared_work: Option<ContentId>,
}
/// Where a baseline was measured, so a reader can tell whether it applies.
///
/// The same discipline as `mesh-router`'s `Calibration`: an expectation is a
/// fit, and a fit is only a fit somewhere. A baseline taken on different
/// silicon, at a different occupancy, or before a toolchain change is not
/// wrong — it is *inapplicable*, and the two must not be confused, because
/// comparing against an inapplicable baseline manufactures deviations that say
/// nothing about the workload.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Baseline {
/// The expected energy for one window of this shape, in µJ.
pub expected_uj: u64,
/// Half-width of the accepted band, in µJ. Deviation is measured against
/// the band, not the point: a point expectation would flag every window.
pub band_uj: u64,
/// Where the expectation came from — a harness run id, a receipt
/// content-id, a document. Names the measurement; does not substitute for
/// it.
pub source: String,
/// The window length the baseline was fit at. A baseline fit at a coarser
/// resolution than the trace cannot speak to the trace's windows.
pub fit_window_ns: u64,
/// Whether the operator has declared this baseline applicable to the host
/// and workload being traced. Defaults to false at construction: an
/// undeclared baseline is treated as inapplicable, so forgetting
/// under-claims rather than manufacturing findings.
pub declared_applicable: bool,
}
impl Baseline {
/// A baseline that does not yet claim to apply anywhere.
pub fn new(expected_uj: u64, band_uj: u64, source: impl Into<String>, fit_window_ns: u64) -> Self {
Baseline {
expected_uj,
band_uj,
source: source.into(),
fit_window_ns,
declared_applicable: false,
}
}
/// Declare this baseline applicable to the host and workload being traced.
/// An explicit act, because it is the step that turns a number from
/// somewhere else into a yardstick here.
pub fn declared_applicable(mut self) -> Self {
self.declared_applicable = true;
self
}
}
/// A named, evidenced reason a window's energy departed from expectation.
///
/// Every variant carries the content-id of a reading that supports it. The
/// evidence is the whole point: without it a cause is a label, and labels
/// explain anything.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum OperationalCause {
/// The part was thermally limited. Evidence: a thermal sample covering the
/// window.
ThermalThrottle { reading: ContentId },
/// The host was shared with work this trace does not account for.
/// Evidence: a co-tenancy or utilisation record.
ContendedHost { reading: ContentId },
/// First execution: cold caches, JIT or kernel compilation. Evidence: a
/// record showing this was a first run.
ColdStart { reading: ContentId },
/// The core ran at a different operating point than the baseline's.
/// Evidence: a frequency or power-state sample.
FrequencyScaled { reading: ContentId },
/// The meter does not cover part of this window's work, so the figure is
/// structurally low. Evidence: the coverage record.
MeterCoverageGap { reading: ContentId },
}
impl OperationalCause {
/// The reading that supports this cause.
pub fn evidence(&self) -> ContentId {
match self {
OperationalCause::ThermalThrottle { reading }
| OperationalCause::ContendedHost { reading }
| OperationalCause::ColdStart { reading }
| OperationalCause::FrequencyScaled { reading }
| OperationalCause::MeterCoverageGap { reading } => *reading,
}
}
/// The §-style token for this cause, for a receipt's claim vocabulary.
pub fn token(&self) -> &'static str {
match self {
OperationalCause::ThermalThrottle { .. } => "thermal_throttle",
OperationalCause::ContendedHost { .. } => "contended_host",
OperationalCause::ColdStart { .. } => "cold_start",
OperationalCause::FrequencyScaled { .. } => "frequency_scaled",
OperationalCause::MeterCoverageGap { .. } => "meter_coverage_gap",
}
}
}
/// What one window's energy establishes against its baseline.
///
/// **There is deliberately no `Malicious` variant.** See the module docs: a
/// deviation cannot carry intent, and a type that offered that arm would have it
/// used.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Deviation {
/// Inside the baseline's band. Says the window is unremarkable *at this
/// resolution and against this baseline* — nothing more.
WithinBand,
/// Outside the band, and a named cause with evidence accounts for it.
Explained { cause: OperationalCause, excess_uj: i128 },
/// Outside the band with no evidenced cause. **This is not a finding of
/// wrongdoing.** It is the set a human should look at, and on an uncharted
/// stack it will often be the largest set — which is a statement about the
/// stack's charting, not about the workload's honesty.
Unexplained { excess_uj: i128 },
/// The baseline does not apply here — undeclared, or fit at a resolution
/// coarser than this window. No comparison is made, because comparing
/// against an inapplicable baseline manufactures deviations.
BaselineInapplicable { reason: &'static str },
/// The window is longer than the phenomenon being asked about, so this
/// window could not have shown it. Distinct from `WithinBand`: one is
/// "looked and saw nothing", the other is "could not have seen it".
Unresolvable { window_ns: u64, asked_ns: u64 },
}
impl EnergyWindow {
/// Classify this window against a baseline.
pub fn classify(&self, baseline: &Baseline) -> Deviation {
if !baseline.declared_applicable {
return Deviation::BaselineInapplicable {
reason: "baseline not declared applicable to this host and workload",
};
}
if baseline.fit_window_ns > self.duration_ns {
return Deviation::BaselineInapplicable {
reason: "baseline fit at a coarser window than this trace samples",
};
}
let lo = baseline.expected_uj.saturating_sub(baseline.band_uj);
let hi = baseline.expected_uj.saturating_add(baseline.band_uj);
if self.joules_uj >= lo && self.joules_uj <= hi {
return Deviation::WithinBand;
}
Deviation::Unexplained {
excess_uj: i128::from(self.joules_uj) - i128::from(baseline.expected_uj),
}
}
/// Classify, offering a cause. The cause is accepted only because it
/// carries evidence; `None` leaves the window unexplained rather than
/// quietly excused.
pub fn classify_with(
&self,
baseline: &Baseline,
cause: Option<OperationalCause>,
) -> Deviation {
match (self.classify(baseline), cause) {
(Deviation::Unexplained { excess_uj }, Some(cause)) => {
Deviation::Explained { cause, excess_uj }
}
(other, _) => other,
}
}
/// Whether this window could resolve a phenomenon of `asked_ns`.
pub fn resolves(&self, asked_ns: u64) -> Deviation {
if asked_ns < self.duration_ns {
Deviation::Unresolvable { window_ns: self.duration_ns, asked_ns }
} else {
Deviation::WithinBand
}
}
}
/// The partition over a window series. Counts, never a score.
///
/// There is no single number here, and that is deliberate. A "risk score" would
/// collapse `unexplained` and `inapplicable` and `unresolvable` into one figure
/// that reads as confidence, when the three mean "look at this", "we have no
/// yardstick", and "we could not have seen it" — which call for completely
/// different responses.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Partition {
pub within_band: usize,
pub explained: usize,
pub unexplained: usize,
pub baseline_inapplicable: usize,
pub unresolvable: usize,
/// Σ excess over the unexplained windows, signed.
pub unexplained_excess_uj: i128,
/// The weakest provenance across every window classified. `None` until a
/// window has been folded in — an empty partition has no grade to report,
/// and inventing one would be a claim about nothing.
pub provenance_floor: Option<Provenance>,
}
impl Partition {
/// Fold one classified window in.
pub fn record(&mut self, window: &EnergyWindow, d: &Deviation) {
match d {
Deviation::WithinBand => self.within_band += 1,
Deviation::Explained { .. } => self.explained += 1,
Deviation::Unexplained { excess_uj } => {
self.unexplained += 1;
self.unexplained_excess_uj += *excess_uj;
}
Deviation::BaselineInapplicable { .. } => self.baseline_inapplicable += 1,
Deviation::Unresolvable { .. } => self.unresolvable += 1,
}
self.provenance_floor = Some(match self.provenance_floor {
None => window.provenance,
Some(have) => weakest(have, window.provenance),
});
}
/// How many windows were actually compared against an applicable baseline.
///
/// The denominator that matters. A partition reporting zero unexplained
/// windows out of a hundred means nothing if ninety-nine of them had no
/// applicable baseline — and that is the shape most "nothing anomalous
/// found" reports take on an uncharted stack.
pub fn compared(&self) -> usize {
self.within_band + self.explained + self.unexplained
}
/// Whether this partition supports saying anything at all about the series.
///
/// `false` when nothing was compared. A report built on an empty comparison
/// is not a quiet report, it is a vacuous one.
pub fn is_substantive(&self) -> bool {
self.compared() > 0
}
}
fn weakest(a: Provenance, b: Provenance) -> Provenance {
fn rank(p: Provenance) -> u8 {
match p {
Provenance::HwShunt => 2,
Provenance::ModelBased => 1,
Provenance::Estimator => 0,
}
}
if rank(b) < rank(a) { b } else { a }
}
/// A granted energy budget against what the windows actually account for.
///
/// # Why fractions of a percent are detectable here and nowhere else
///
/// Everything in this family carries energy as **integer microjoules**, and the
/// sums are exact. That makes the detection floor a property of arithmetic
/// rather than of statistics: the smallest representable discrepancy is 1 µJ,
/// so against a budget of `N` µJ the floor is `1_000_000 / N` parts per
/// million. On a one-joule budget that is **1 ppm — one ten-thousandth of a
/// percent**. A drift of a fraction of a percent sits three orders of magnitude
/// above the floor, which is why it is not a subtle signal but an obvious one.
///
/// This is the part that does not survive the usual construction. Almost
/// everywhere in computing, an energy figure is *derived from a performance
/// proxy* — utilisation times a thermal design figure, tokens times a
/// coefficient, instructions times picojoules-per-op — and never measured. The
/// model error in such a figure is tens of percent. A fractional-percent drift
/// is then invisible by construction: it is far inside the error bar of the
/// estimator that produced the number, and no amount of resolution recovers it.
/// Deriving energy from performance does not merely lose accuracy; it destroys
/// the ability to notice that the books do not balance.
///
/// So the instrument is conservation, not anomaly detection. A band comparison
/// asks "is this unusual against a model of normal", which an uncharted stack
/// cannot answer. Conservation asks "do these joules fit anywhere", which needs
/// no model of normal — only exact arithmetic and a grant that said how many
/// were allowed. [`Provenance`] remains load-bearing throughout: a budget
/// reconciled against `Estimator` windows is reconciled against a model's
/// opinion, and the floor below is then fiction. The floor is real only to the
/// extent the windows were measured.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Budget {
/// Microjoules the grant authorised.
pub granted_uj: u64,
/// Σ of the windows' energy.
pub accounted_uj: u128,
/// The weakest provenance across the windows that produced
/// [`Self::accounted_uj`]. Carried because the detection floor is only as
/// real as the measurement under it.
pub provenance_floor: Option<Provenance>,
}
impl Budget {
/// Reconcile a window series against a grant.
pub fn reconcile(granted_uj: u64, windows: &[EnergyWindow]) -> Self {
let accounted_uj: u128 = windows.iter().map(|w| u128::from(w.joules_uj)).sum();
let provenance_floor = windows
.iter()
.map(|w| w.provenance)
.reduce(weakest);
Budget { granted_uj, accounted_uj, provenance_floor }
}
/// `accounted - granted`, signed. Positive is overspend; negative is energy
/// the grant allowed that nothing claimed. Never clamped — both directions
/// are findings, and the negative one is how unattributed work hides.
pub fn drift_uj(&self) -> i128 {
self.accounted_uj as i128 - i128::from(self.granted_uj)
}
/// The drift in parts per million of the grant. `None` for a zero grant: a
/// ratio against nothing is not a figure.
pub fn drift_ppm(&self) -> Option<i128> {
if self.granted_uj == 0 {
return None;
}
Some(self.drift_uj() * 1_000_000 / i128::from(self.granted_uj))
}
/// The smallest drift this budget can represent, in ppm — one microjoule
/// expressed against the grant.
///
/// Exact, not estimated. This is the number that says whether a given
/// tolerance is even askable: a 0.1 % tolerance on a 1 mJ grant is 1 ppm of
/// nothing useful, while on a 1 J grant it is a thousand times the floor.
pub fn detectable_floor_ppm(&self) -> Option<u64> {
if self.granted_uj == 0 {
return None;
}
Some((1_000_000 / self.granted_uj).max(1))
}
/// Whether the observed drift is above this budget's representational floor.
///
/// `false` means the books balance to the limit of what integers can say at
/// this grant size — which is a real and bounded statement, unlike "no
/// anomaly detected".
pub fn drift_is_resolvable(&self) -> bool {
match self.detectable_floor_ppm() {
None => false,
Some(floor) => self.drift_ppm().is_some_and(|d| d.unsigned_abs() >= u128::from(floor)),
}
}
/// Whether the floor is a claim about measurement or about a model.
///
/// A budget whose windows are `Estimator` has a floor that is arithmetically
/// exact and physically meaningless: the estimator's own error dwarfs it.
/// Reporting a 1 ppm floor over modelled windows would be the most precise
/// way to mislead available here.
pub fn floor_is_physical(&self) -> bool {
matches!(self.provenance_floor, Some(Provenance::HwShunt))
}
}
/// The physical envelope a part cannot leave, used to **refute** energy claims.
///
/// # Energy cannot be faked; the measurement of it can
///
/// The joules were spent. The heat left the die. That quantity is not
/// negotiable — but the *report* of it is exactly as corruptible as any other
/// report. A counter can be lied about, a figure can be fabricated, and a
/// `HwShunt` grade can be stamped on a number that came from nowhere. A meter
/// reading a frozen register while claiming hardware measurement is not a
/// hypothetical: it is a defect that ships.
///
/// So energy buys no immunity. What it buys is something no synthetic unit has:
/// **an external referent**. A token count, an API-call count, a
/// request-per-second figure can be any number whatsoever — nothing outside the
/// accounting system constrains them, so a fabricated one is indistinguishable
/// from a true one. A joule count is bounded by physics. The part has a power
/// envelope; the window has a duration; the product of the two is a ceiling no
/// honest figure can exceed, and the idle draw is a floor no powered part can
/// fall below while doing work.
///
/// That makes a faked energy figure *falsifiable*, which is a strictly weaker
/// and strictly more honest property than unfakeable.
///
/// # Refutes, never verifies
///
/// [`PhysicalVerdict`] has no `Verified` arm, for the same reason
/// `sandbox-attest`'s locality bound has none: passing a physical check means
/// the figure is *not impossible*, which is not the same as true. A figure
/// inside the envelope may still be fabricated, and the envelope will never
/// say so.
///
/// The bounds are deliberately **permissive** — peak burst rating for the
/// ceiling, not typical; absolute idle for the floor. A refutation from a
/// generous bound is unassailable: nobody can argue the part was merely running
/// hot. The same reason the locality check uses the speed of light in vacuum
/// rather than in fibre.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PowerEnvelope {
/// Absolute peak the part can draw, milliwatts. Use the burst rating, not
/// the sustained or typical figure.
pub peak_mw: u64,
/// Floor the part cannot fall below while powered and executing,
/// milliwatts. Zero disables the floor check for parts with no credible
/// idle figure — honest, and it gives up the ability to catch a dead
/// counter.
pub idle_floor_mw: u64,
/// Where these bounds come from — a datasheet, a measured sweep, a receipt.
/// Names the source; does not substitute for it.
pub source: String,
}
/// What a physical bound establishes about an energy figure.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PhysicalVerdict {
/// The figure is inside the envelope. **Not** a finding that it is correct:
/// a fabricated figure inside the envelope passes this check, and must.
NotRefuted { headroom_uj: u64 },
/// The figure exceeds what the part could draw for the window's duration.
/// Physically impossible, whatever the counter said.
RefutedAbove { claimed_uj: u64, ceiling_uj: u64 },
/// The figure is below what a powered, executing part must draw. This is
/// the shape a dead or frozen counter takes: a busy window reporting
/// implausibly little.
RefutedBelow { claimed_uj: u64, floor_uj: u64 },
/// The window has no duration, so no bound can be computed. Not a pass.
Unboundable,
}
impl PowerEnvelope {
/// The most the part could have spent over `duration_ns`, in µJ.
///
/// mW × ns = 1e-3 W × 1e-9 s = 1e-12 J = 1e-6 µJ, so the product divides by
/// 1e6 to land in microjoules. Computed in `u128` and saturating: a
/// ceiling that overflowed to a small number would refute honest figures.
pub fn ceiling_uj(&self, duration_ns: u64) -> u128 {
u128::from(self.peak_mw).saturating_mul(u128::from(duration_ns)) / 1_000_000
}
/// The least a powered, executing part must have spent over `duration_ns`.
pub fn floor_uj(&self, duration_ns: u64) -> u128 {
u128::from(self.idle_floor_mw).saturating_mul(u128::from(duration_ns)) / 1_000_000
}
/// Try to refute a window's energy figure against physics.
pub fn check(&self, window: &EnergyWindow) -> PhysicalVerdict {
if window.duration_ns == 0 {
return PhysicalVerdict::Unboundable;
}
let claimed = u128::from(window.joules_uj);
let ceiling = self.ceiling_uj(window.duration_ns);
if claimed > ceiling {
return PhysicalVerdict::RefutedAbove {
claimed_uj: window.joules_uj,
ceiling_uj: ceiling.min(u128::from(u64::MAX)) as u64,
};
}
let floor = self.floor_uj(window.duration_ns);
if floor > 0 && claimed < floor {
return PhysicalVerdict::RefutedBelow {
claimed_uj: window.joules_uj,
floor_uj: floor.min(u128::from(u64::MAX)) as u64,
};
}
PhysicalVerdict::NotRefuted {
headroom_uj: (ceiling - claimed).min(u128::from(u64::MAX)) as u64,
}
}
}
/// The character of a draw over time — the one thing here that cannot be faked.
///
/// # Why shape, and not the total
///
/// The quantity is physical and the measurement is forgeable, but **how** the
/// energy was consumed is the signature of what the silicon actually did. You
/// can report any number you like; you cannot make a part draw the profile of a
/// sustained matmul while it executes a memory-bound scan. The shape is a
/// property of the computation.
///
/// This closes a hole conservation alone leaves open. A total can be balanced
/// by padding — invent an operation, attribute the surplus joules to it, and the
/// books add up. The profile does not cooperate: a fictional operation
/// contributes no draw, so the observed character stops matching the declared
/// work mix.
///
/// # Why it survives an uncharted stack
///
/// This is the part that answers the hard question. On unfamiliar silicon the
/// *magnitudes* move — a different process node, a different power envelope, a
/// different memory hierarchy all shift the absolute joules, which is exactly
/// why a band baseline fails there. The *character* does not: a compute-bound
/// steady state is flat on any part, a cold start ramps on any part, a
/// queue-driven workload alternates on any part. So a declaration can be
/// refuted by shape on hardware nobody has ever profiled, without any model of
/// what normal looks like on it.
///
/// # Refutes a declaration, not a person
///
/// An inconsistency says the declared work did not produce this draw. It does
/// not say what did, and it never says intent. The declaration has to be made
/// **in advance** — classifying the shape first and then deciding what it must
/// have been is post-hoc pattern matching, which is the anomaly-detection trap
/// this module exists to avoid.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DrawShape {
/// Flat within tolerance. Compute-bound steady state.
Sustained,
/// Rising then settling. Cold caches, kernel or JIT compilation, page-in.
Ramping,
/// Alternating high and low. Queue-driven, memory-bound, or I/O-gated.
Bursty,
/// At or near the floor throughout. The part was not doing the declared
/// work — or the counter is dead, which looks identical from here and is
/// why [`PowerEnvelope::check`] is the companion test.
Idle,
}
/// What comparing a declared shape against an observed one establishes.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShapeVerdict {
/// Observed character matches what was declared. Not a finding of honesty:
/// a different workload of the same character also matches.
Consistent { shape: DrawShape },
/// The declared work did not produce this draw. A refutation of the
/// declaration — not an attribution of cause, and never of intent.
Inconsistent { declared: DrawShape, observed: DrawShape },
/// Too few windows to speak to character. Three is the minimum at which
/// flat, rising and alternating are distinguishable at all; fewer is
/// `Unresolvable`, never `Consistent`.
Unresolvable { windows: usize },
}
/// Classify the character of a window series.
///
/// Integer arithmetic throughout, in µJ. `idle_floor_uj` is the per-window
/// floor from the [`PowerEnvelope`]; a series at or under it is `Idle`
/// regardless of its variation.
pub fn observe_shape(windows: &[EnergyWindow], idle_floor_uj: u64) -> Option<DrawShape> {
if windows.len() < 3 {
return None;
}
let v: Vec<u64> = windows.iter().map(|w| w.joules_uj).collect();
let n = v.len() as u128;
let sum: u128 = v.iter().map(|&x| u128::from(x)).sum();
let mean = sum / n;
if mean <= u128::from(idle_floor_uj) {
return Some(DrawShape::Idle);
}
// Mean absolute deviation as a permille of the mean — integer, no floats,
// so the classification is reproducible rather than platform-dependent.
let mad: u128 = v
.iter()
.map(|&x| {
let x = u128::from(x);
if x > mean { x - mean } else { mean - x }
})
.sum::<u128>()
/ n;
let spread_permille = if mean == 0 { 0 } else { mad * 1000 / mean };
// Flat enough to be one steady state.
if spread_permille <= 100 {
return Some(DrawShape::Sustained);
}
// Rising: the second half's mean clearly exceeds the first half's, and the
// series does not swing back and forth.
let half = v.len() / 2;
let first: u128 = v[..half].iter().map(|&x| u128::from(x)).sum::<u128>() / half as u128;
let last: u128 =
v[v.len() - half..].iter().map(|&x| u128::from(x)).sum::<u128>() / half as u128;
let direction_changes = v
.windows(3)
.filter(|w| (w[1] > w[0]) != (w[2] > w[1]))
.count();
if last > first + first / 4 && direction_changes * 4 < v.len() {
return Some(DrawShape::Ramping);
}
Some(DrawShape::Bursty)
}
/// Compare a shape declared in advance against the series actually observed.
pub fn check_shape(
declared: DrawShape,
windows: &[EnergyWindow],
idle_floor_uj: u64,
) -> ShapeVerdict {
match observe_shape(windows, idle_floor_uj) {
None => ShapeVerdict::Unresolvable { windows: windows.len() },
Some(observed) if observed == declared => ShapeVerdict::Consistent { shape: observed },
Some(observed) => ShapeVerdict::Inconsistent { declared, observed },
}
}
#[cfg(test)]
mod tests {
use super::*;
fn win(started_ns: u64, joules_uj: u64) -> EnergyWindow {
EnergyWindow {
started_ns,
duration_ns: 1_000_000, // 1 ms
joules_uj,
provenance: Provenance::HwShunt,
coverage: Coverage::Whole,
declared_work: None,
}
}
fn series(vals: &[u64]) -> Vec<EnergyWindow> {
vals.iter().enumerate().map(|(i, &v)| win(i as u64 * 1_000_000, v)).collect()
}
/// The claim this whole module rests on, stated as a test: fractions of a
/// percent are detectable because the arithmetic is exact integers.
#[test]
fn a_fractional_percent_drift_is_three_orders_above_the_floor() {
// A 1 J grant, overspent by 0.5 % — 5 mJ.
let granted = 1_000_000u64; // 1 J in µJ
let b = Budget {
granted_uj: granted,
accounted_uj: u128::from(granted) + 5_000,
provenance_floor: Some(Provenance::HwShunt),
};
assert_eq!(b.drift_ppm(), Some(5_000), "0.5 % is 5000 ppm");
assert_eq!(b.detectable_floor_ppm(), Some(1), "1 µJ of 1 J is 1 ppm");
assert!(b.drift_is_resolvable());
assert!(b.floor_is_physical(), "and the floor means something: measured windows");
}
/// The floor is arithmetic, not physics, when the windows are modelled.
/// Reporting 1 ppm over an estimator's output would be the most precise way
/// to mislead available here.
#[test]
fn a_modelled_budget_has_an_exact_but_meaningless_floor() {
let b = Budget {
granted_uj: 1_000_000,
accounted_uj: 1_000_005,
provenance_floor: Some(Provenance::Estimator),
};
assert_eq!(b.detectable_floor_ppm(), Some(1), "arithmetically exact");
assert!(!b.floor_is_physical(), "and physically meaningless — say so");
}
/// Underspend is a finding too: energy the grant allowed that nothing
/// claimed is how unattributed work hides.
#[test]
fn drift_is_signed_in_both_directions() {
let under = Budget::reconcile(1_000_000, &series(&[100_000, 100_000, 100_000]));
assert!(under.drift_uj() < 0, "300 mJ claimed against a 1 J grant");
assert_eq!(under.drift_ppm(), Some(-700_000));
assert_eq!(Budget::reconcile(0, &series(&[1, 2, 3])).drift_ppm(), None);
}
/// The physical floor refutes a dead counter. This is not hypothetical —
/// it is the shape of a real defect: a busy window whose counter is frozen
/// reports implausibly little while the meter still claims hardware.
#[test]
fn the_physical_floor_refutes_a_frozen_counter() {
let env = PowerEnvelope {
peak_mw: 60_000, // 60 W burst — deliberately generous
idle_floor_mw: 1_000, // 1 W floor while executing
source: "datasheet burst rating".into(),
};
// A 400 ms busy window reporting zero.
let dead = EnergyWindow { duration_ns: 400_000_000, joules_uj: 0, ..win(0, 0) };
match env.check(&dead) {
PhysicalVerdict::RefutedBelow { claimed_uj, floor_uj } => {
assert_eq!(claimed_uj, 0);
assert_eq!(floor_uj, 400_000, "1 W for 400 ms is 400 mJ");
}
other => panic!("a frozen counter must be refutable: {other:?}"),
}
}
/// And the ceiling refutes a fabricated figure no part could have drawn.
#[test]
fn the_ceiling_refutes_a_physically_impossible_figure() {
let env = PowerEnvelope {
peak_mw: 60_000,
idle_floor_mw: 0,
source: "burst rating".into(),
};
let absurd = EnergyWindow { duration_ns: 1_000_000, joules_uj: 10_000_000, ..win(0, 0) };
assert!(matches!(env.check(&absurd), PhysicalVerdict::RefutedAbove { .. }));
// A plausible figure is NOT refuted — and that is not a finding of
// correctness. There is no `Verified` arm, deliberately.
let ok = EnergyWindow { duration_ns: 1_000_000, joules_uj: 30_000, ..win(0, 0) };
assert!(matches!(env.check(&ok), PhysicalVerdict::NotRefuted { .. }));
}
/// A zero-duration window yields no bound. Not a pass.
#[test]
fn an_instant_window_is_unboundable_not_clean() {
let env = PowerEnvelope { peak_mw: 1, idle_floor_mw: 1, source: "x".into() };
let instant = EnergyWindow { duration_ns: 0, joules_uj: 999, ..win(0, 0) };
assert_eq!(env.check(&instant), PhysicalVerdict::Unboundable);
}
/// The hole conservation leaves open: a total balanced by padding. The
/// shape does not cooperate — a fictional operation contributes no draw, so
/// the declared character stops matching.
#[test]
fn shape_catches_a_total_balanced_by_padding() {
// Declared: a sustained compute-bound run.
// Observed: a short burst and then nothing, padded to the same total.
let padded = series(&[300_000, 10, 10, 10, 10, 10]);
match check_shape(DrawShape::Sustained, &padded, 100) {
ShapeVerdict::Inconsistent { declared, observed } => {
assert_eq!(declared, DrawShape::Sustained);
assert_ne!(observed, DrawShape::Sustained);
}
other => panic!("padding must not read as sustained: {other:?}"),
}
}
/// Character survives a change of scale, which is why this works on silicon
/// nobody has profiled: the magnitudes move, the shape does not.
#[test]
fn character_is_scale_invariant() {
let small = series(&[1_000, 1_020, 980, 1_010, 990, 1_000]);
let large: Vec<u64> = [1_000u64, 1_020, 980, 1_010, 990, 1_000]
.iter()
.map(|v| v * 37)
.collect();
assert_eq!(observe_shape(&small, 10), Some(DrawShape::Sustained));
assert_eq!(observe_shape(&series(&large), 10), Some(DrawShape::Sustained));
}
/// Ramping is distinguishable from alternating, and both from flat.
#[test]
fn the_three_characters_are_distinguishable() {
assert_eq!(
observe_shape(&series(&[100, 400, 700, 1_000, 1_100, 1_120]), 10),
Some(DrawShape::Ramping)
);
assert_eq!(
observe_shape(&series(&[100, 2_000, 100, 2_000, 100, 2_000]), 10),
Some(DrawShape::Bursty)
);
assert_eq!(
observe_shape(&series(&[1_000, 1_000, 1_000, 1_000]), 10),
Some(DrawShape::Sustained)
);
}
/// Below three windows, character is unresolvable — never "consistent".
/// Reporting agreement at a resolution that could not disagree is the
/// quietest way to be wrong.
#[test]
fn two_windows_cannot_speak_to_character() {
assert_eq!(
check_shape(DrawShape::Sustained, &series(&[1_000, 1_000]), 10),
ShapeVerdict::Unresolvable { windows: 2 }
);
assert_eq!(observe_shape(&series(&[1, 2]), 0), None);
}
/// An undeclared baseline makes no comparison. Forgetting to declare must
/// not manufacture findings.
#[test]
fn an_undeclared_baseline_refuses_to_compare() {
let w = win(0, 5_000);
let b = Baseline::new(1_000, 100, "harness:run-1", 1_000_000);
assert!(matches!(w.classify(&b), Deviation::BaselineInapplicable { .. }));
// Declared, and now the window is outside the band.
let b = b.declared_applicable();
assert!(matches!(w.classify(&b), Deviation::Unexplained { .. }));
}
/// A baseline fit at a coarser window than the trace samples cannot speak
/// to the trace's windows.
#[test]
fn a_coarser_baseline_does_not_apply_to_finer_windows() {
let w = win(0, 5_000); // 1 ms window
let b = Baseline::new(1_000, 100, "harness", 10_000_000).declared_applicable();
assert!(matches!(w.classify(&b), Deviation::BaselineInapplicable { .. }));
}
/// A cause explains a deviation only because it carries evidence. Without
/// one the window stays unexplained rather than quietly excused.
#[test]
fn a_cause_without_evidence_does_not_explain() {
let w = win(0, 5_000);
let b = Baseline::new(1_000, 100, "harness", 1_000).declared_applicable();
assert!(matches!(w.classify_with(&b, None), Deviation::Unexplained { .. }));
let cause = OperationalCause::ThermalThrottle { reading: ContentId::of_bytes(b"thermal") };
match w.classify_with(&b, Some(cause.clone())) {
Deviation::Explained { cause: got, .. } => {
assert_eq!(got.token(), "thermal_throttle");
assert_eq!(got.evidence(), ContentId::of_bytes(b"thermal"));
}
other => panic!("an evidenced cause explains: {other:?}"),
}
}
/// The denominator that matters: zero unexplained windows means nothing if
/// nothing was compared.
#[test]
fn a_partition_that_compared_nothing_is_vacuous_not_clean() {
let mut p = Partition::default();
let w = win(0, 5_000);
let undeclared = Baseline::new(1_000, 100, "harness", 1_000);
for _ in 0..99 {
p.record(&w, &w.classify(&undeclared));
}
assert_eq!(p.unexplained, 0, "nothing was flagged");
assert_eq!(p.compared(), 0, "because nothing was compared");
assert!(!p.is_substantive(), "so the partition supports no claim");
assert_eq!(p.baseline_inapplicable, 99);
assert_eq!(p.provenance_floor, Some(Provenance::HwShunt));
}
/// There is no `Malicious` verdict, and that is the load-bearing absence.
/// Unexplained is a request for a look.
#[test]
fn unexplained_is_not_a_verdict_about_intent() {
let w = win(0, 50_000);
let b = Baseline::new(1_000, 100, "harness", 1_000).declared_applicable();
let d = w.classify(&b);
match d {
Deviation::Unexplained { excess_uj } => assert_eq!(excess_uj, 49_000),
other => panic!("expected unexplained: {other:?}"),
}
// The partition counts it; it produces no score, and there is no arm
// anywhere in this module that names intent.
let mut p = Partition::default();
p.record(&w, &d);
assert_eq!(p.unexplained, 1);
assert_eq!(p.unexplained_excess_uj, 49_000);
}
}

View file

@ -35,6 +35,10 @@ use jcp_receipt::jcr1;
use jcp_schema::{ContentId, Energy};
pub mod avoided;
/// Energy traceability across operation, activity and outcome.
pub mod trace;
/// Time-resolved energy and the limits of what a deviation establishes.
pub mod dynamics;
pub mod training;
use serde_json::{json, Value};

View file

@ -0,0 +1,736 @@
//! Energy traceability across the three levels an AI system is accountable at:
//! **operation**, **activity**, and **outcome**.
//!
//! # The levels, and where the family already measures
//!
//! A deployed system is answerable at three different grains, and they are not
//! interchangeable:
//!
//! - **Operation** — one unit of work. `jcp-receipt`'s JCR-1 work receipt
//! carries `joules_uj` with a [`Provenance`] grade. This level is measured.
//! - **Activity** — one *authorised* thing done: a grant checked, an
//! environment attested, work performed, an effect committed, bytes egressed.
//! `jcp-action-receipt`'s AR-1 binds all five by content-id.
//! - **Outcome** — whether any of it *bought* anything. `efficiency/1` anchors
//! on `completed` and totals the energy the unit spent.
//!
//! # The two seams this module closes
//!
//! **An outcome's energy was unbound.** [`crate::Efficiency::task`] is a
//! `String`. An outcome could claim any total it liked and nothing tied that
//! figure to the activities that produced it — the one level where a number is
//! most load-bearing was the one level identified by free text. Here an outcome
//! names its activities by [`ContentId`], so a verifier can resolve each one
//! and add the figures up itself.
//!
//! **An activity had no energy of its own.** AR-1 carries exactly one joules
//! figure and it belongs to *binding 3: work*. The cost of the authorisation
//! check, the attestation, the verification, and of pushing bytes off the
//! device is real and was attributed nowhere. `Egress` counts bytes and no
//! joules.
//!
//! So the sum of an outcome's operations is **not** the outcome's energy, and
//! the difference is not noise — it is the cost of being accountable. This
//! module names that difference instead of letting it vanish.
//!
//! # Conservation, with the residual named
//!
//! Reconciliation is exact integer arithmetic in µJ, summed in `u128`:
//!
//! ```text
//! residual_uj = total_uj - Σ activity.joules_uj
//! ```
//!
//! The residual is **signed** and never clamped. Negative means the bound
//! activities claim more energy than the outcome admits — double-counted work,
//! or a wrong binding — and that is a finding, not a rounding error. Clamping
//! it to zero would hide exactly the case an auditor needs.
//!
//! This mirrors `eoc-carbon`'s allocation discipline, where
//! `Σ shares + residual == total` holds exactly and the residual is a named
//! field rather than a silent remainder.
//!
//! # A chain is only as graded as its weakest link
//!
//! The composite [`Provenance`] is the **floor** across the parts, never an
//! average and never the best: one `Estimator` activity makes the whole trace
//! estimated, because the trace cannot be more measured than the thing in it
//! that was not measured. Coverage takes the floor the same way — one `Partial`
//! part makes the composite `Partial`, since the missing share does not become
//! accounted for by sitting next to accounted shares.
//!
//! This is the same rule as `Vmm::attained_tier` returning the floor and
//! `Backing` defaulting to `Unstated`: aggregation must not be a laundering
//! step.
use serde::{Deserialize, Serialize};
use jcp_schema::{ContentId, Coverage, Energy, Provenance};
/// One activity bound into an outcome, as a verifier reads it.
///
/// The fields are copied from the AR-1 receipt named by [`Self::receipt`] so a
/// reconciliation can be checked without resolving every receipt — but
/// [`Self::receipt`] is what makes the claim falsifiable: resolve it and the
/// copied figures must match.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityRef {
/// The AR-1 action receipt. The binding, and the only thing here that
/// cannot be fabricated without also fabricating a signed receipt.
pub receipt: ContentId,
/// The work's energy, from AR-1 `joules_uj`.
pub joules_uj: u64,
/// How that figure was acquired, from AR-1 `energy_provenance`. Carried
/// verbatim; never upgraded here.
pub provenance: Provenance,
/// How much of the activity's work the figure accounts for.
pub coverage: Coverage,
}
impl ActivityRef {
/// Bind one activity.
pub fn new(
receipt: ContentId,
joules_uj: u64,
provenance: Provenance,
coverage: Coverage,
) -> Self {
ActivityRef { receipt, joules_uj, provenance, coverage }
}
}
/// Why a trace cannot be presented as traceable.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TraceError {
/// The outcome names no activities. Its energy total is then an assertion
/// about nothing a verifier can resolve — the free-text case this module
/// exists to retire.
Unbound,
/// The bound activities claim more energy than the outcome admits.
/// Double-counted work, or an activity bound to the wrong outcome.
OverAccounted { accounted_uj: u128, total_uj: u64 },
/// The same activity receipt appears twice. Summing it twice inflates the
/// accounted figure and shrinks the residual, which flatters the trace in
/// exactly the direction that matters.
DuplicateActivity { receipt: ContentId },
}
impl core::fmt::Display for TraceError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
TraceError::Unbound => write!(
f,
"outcome names no activities: its energy total is not traceable \
to anything a verifier can resolve"
),
TraceError::OverAccounted { accounted_uj, total_uj } => write!(
f,
"bound activities account for {accounted_uj} uJ but the outcome \
admits only {total_uj} uJ; the work is double-counted or an \
activity is bound to the wrong outcome"
),
TraceError::DuplicateActivity { receipt } => write!(
f,
"activity {} is bound twice; summing it twice inflates the \
accounted figure and shrinks the residual",
receipt.to_hex()
),
}
}
}
impl std::error::Error for TraceError {}
/// What reconciling an outcome against its activities establishes.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Reconciliation {
/// Σ of the bound activities' work energy.
pub accounted_uj: u128,
/// `total - accounted`. **Signed and never clamped.** Positive is the cost
/// of being accountable — authorisation, attestation, verification, egress
/// — which no operation receipt claims. Zero means the outcome admits
/// exactly its operations and nothing else, which for a gated, attested,
/// receipt-emitting system is itself worth a second look.
pub residual_uj: i128,
/// The weakest provenance across the outcome and every bound activity.
pub provenance_floor: Provenance,
/// `Partial` if the outcome or any activity is partial.
pub coverage_floor: Coverage,
/// How many activities were reconciled.
pub activities: usize,
}
impl Reconciliation {
/// The share of the outcome's energy that no operation receipt claims, in
/// parts per million.
///
/// This is the number the three-level question actually turns on: how much
/// of what an outcome cost is attributable to a measured operation, and how
/// much is the overhead of accountability itself. `None` when the total is
/// zero — a ratio against nothing is not a figure.
pub fn unattributed_ppm(&self, total_uj: u64) -> Option<i128> {
if total_uj == 0 {
return None;
}
Some(self.residual_uj * 1_000_000 / i128::from(total_uj))
}
}
/// An outcome, the activities that produced it, and the arithmetic between them.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutcomeTrace {
/// The outcome's own energy figure, as `efficiency/1` records it.
pub total: Energy,
/// Whether the outcome was reached. Carried because energy spent on an
/// outcome that did not complete is a different claim from energy spent on
/// one that did, and a trace that drops it loses the anchor.
pub completed: bool,
/// The activities this outcome is traced to.
pub activities: Vec<ActivityRef>,
}
impl OutcomeTrace {
/// Trace an outcome to its activities.
pub fn new(total: Energy, completed: bool, activities: Vec<ActivityRef>) -> Self {
OutcomeTrace { total, completed, activities }
}
/// The outcome's energy in µJ, preferring the measured figure.
///
/// `measured_uj` when present, `estimated_uj` otherwise — and the choice is
/// already graded by [`Energy::provenance`], so this does not re-decide it.
pub fn total_uj(&self) -> u64 {
self.total.measured_uj.unwrap_or(self.total.estimated_uj)
}
/// Reconcile the outcome against its activities, or refuse to.
///
/// Refuses an unbound outcome, a duplicated activity, and an over-accounted
/// total. Everything else reconciles, including a large positive residual —
/// that is a legitimate and interesting result, not an error.
pub fn reconcile(&self) -> Result<Reconciliation, TraceError> {
if self.activities.is_empty() {
return Err(TraceError::Unbound);
}
// Duplicate detection before summing: the whole point of the residual
// is that it is exact, and a double-counted activity corrupts it
// silently in the flattering direction.
for (i, a) in self.activities.iter().enumerate() {
if self.activities[..i].iter().any(|b| b.receipt == a.receipt) {
return Err(TraceError::DuplicateActivity { receipt: a.receipt });
}
}
// u128 so a long trace cannot overflow into a small number — a wrapped
// sum would under-account and inflate the residual.
let accounted_uj: u128 =
self.activities.iter().map(|a| u128::from(a.joules_uj)).sum();
let total_uj = self.total_uj();
if accounted_uj > u128::from(total_uj) {
return Err(TraceError::OverAccounted { accounted_uj, total_uj });
}
let residual_uj = i128::from(total_uj) - accounted_uj as i128;
let provenance_floor = self
.activities
.iter()
.map(|a| a.provenance)
.fold(self.total.provenance, weakest_provenance);
let coverage_floor = self
.activities
.iter()
.map(|a| a.coverage)
.fold(self.total.coverage, weakest_coverage);
Ok(Reconciliation {
accounted_uj,
residual_uj,
provenance_floor,
coverage_floor,
activities: self.activities.len(),
})
}
}
/// The weaker of two acquisition classes: `HwShunt` > `ModelBased` > `Estimator`.
///
/// A composite is never more measured than its least-measured part. Averaging
/// would let one hardware reading launder a pile of estimates.
fn weakest_provenance(a: Provenance, b: Provenance) -> Provenance {
fn rank(p: Provenance) -> u8 {
match p {
Provenance::HwShunt => 2,
Provenance::ModelBased => 1,
Provenance::Estimator => 0,
}
}
if rank(b) < rank(a) {
b
} else {
a
}
}
/// `Partial` if either is partial. An unaccounted share does not become
/// accounted for by being summed with accounted ones.
fn weakest_coverage(a: Coverage, b: Coverage) -> Coverage {
match (a, b) {
(Coverage::Whole, Coverage::Whole) => Coverage::Whole,
_ => Coverage::Partial,
}
}
#[cfg(test)]
mod seal_gate_tests {
use super::*;
use crate::dynamics::{EnergyWindow, PowerEnvelope};
fn energy(uj: u64, p: Provenance, c: Coverage) -> Energy {
Energy { measured_uj: Some(uj), estimated_uj: uj, provenance: p, coverage: c }
}
fn act(tag: &[u8], uj: u64, p: Provenance, c: Coverage) -> ActivityRef {
ActivityRef::new(ContentId::of_bytes(tag), uj, p, c)
}
fn win(uj: u64, dur_ns: u64) -> EnergyWindow {
EnergyWindow {
started_ns: 0,
duration_ns: dur_ns,
joules_uj: uj,
provenance: Provenance::HwShunt,
coverage: Coverage::Whole,
declared_work: None,
}
}
fn env() -> PowerEnvelope {
PowerEnvelope { peak_mw: 60_000, idle_floor_mw: 1_000, source: "burst".into() }
}
fn sound() -> OutcomeTrace {
OutcomeTrace::new(
energy(1_000_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![act(b"a1", 400_000, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 350_000, Provenance::HwShunt, Coverage::Whole)],
)
}
/// The doctrine, as a test: traceability is a precondition. An untraceable
/// outcome cannot be admitted for sealing at all — a signature over an
/// unchecked claim is the most durable way to publish it.
#[test]
fn an_untraceable_outcome_cannot_be_sealed() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole), true, vec![],
);
assert_eq!(
t.admit(&[], None),
Err(SealRefusal::NotTraceable(TraceError::Unbound))
);
}
/// A frozen counter cannot be sealed as anything. This is the live defect
/// shape: a busy window reporting nothing while the meter claims hardware.
#[test]
fn a_frozen_counter_cannot_be_sealed() {
let t = sound();
let dead = win(0, 400_000_000); // 400 ms claiming zero
match t.admit(&[dead], Some(&env())) {
Err(SealRefusal::PhysicallyRefuted { window_index, detail }) => {
assert_eq!(window_index, 0);
assert!(detail.contains("frozen counter"), "{detail}");
}
other => panic!("a dead counter must not seal: {other:?}"),
}
}
/// Nor can a figure no part could have drawn.
#[test]
fn a_physically_impossible_figure_cannot_be_sealed() {
let t = sound();
let absurd = win(10_000_000, 1_000_000);
assert!(matches!(
t.admit(&[absurd], Some(&env())),
Err(SealRefusal::PhysicallyRefuted { .. })
));
}
/// The grade is derived, never supplied. Claiming hardware over modelled
/// evidence is refused rather than quietly downgraded: the caller has a
/// bug, and writing the weaker grade silently would hide it.
#[test]
fn a_grade_the_evidence_does_not_support_is_refused() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![act(b"a1", 300, Provenance::Estimator, Coverage::Whole)],
);
assert_eq!(
t.admit(&[], None),
Err(SealRefusal::GradeNotEarned {
claimed: Provenance::HwShunt,
floor: Provenance::Estimator
})
);
}
/// A sound trace is admitted, and what comes back carries the grade the
/// evidence earned — so the sealer writes that instead of the claim.
#[test]
fn an_admitted_trace_carries_the_earned_grade_not_the_claimed_one() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::ModelBased, Coverage::Whole),
true,
vec![act(b"a1", 300, Provenance::ModelBased, Coverage::Partial)],
);
let a = t.admit(&[], None).expect("admits");
assert_eq!(a.earned_provenance(), Provenance::ModelBased);
assert_eq!(
a.earned_coverage(), Coverage::Partial,
"one partial part makes the receipt partial"
);
assert_eq!(a.reconciliation().residual_uj, 700);
}
/// Claiming LESS than the evidence supports is allowed — under-claiming is
/// always permitted, and the receipt still records the floor.
#[test]
fn under_claiming_is_permitted() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::Estimator, Coverage::Whole),
true,
vec![act(b"a1", 300, Provenance::HwShunt, Coverage::Whole)],
);
let a = t.admit(&[], None).expect("under-claiming admits");
assert_eq!(a.earned_provenance(), Provenance::Estimator, "the floor is the claim");
}
/// With no envelope supplied no physical check runs, and that is not a
/// pass — the trace is admitted on its arithmetic alone, which is exactly
/// what the absence of an envelope means.
#[test]
fn no_envelope_means_no_physical_claim_was_made() {
let t = sound();
let implausible = win(0, 400_000_000);
assert!(t.admit(&[implausible], None).is_ok(), "arithmetic closes; physics unchecked");
assert!(t.admit(&[win(0, 400_000_000)], Some(&env())).is_err(), "with an envelope it does not");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn energy(uj: u64, p: Provenance, c: Coverage) -> Energy {
Energy { measured_uj: Some(uj), estimated_uj: uj, provenance: p, coverage: c }
}
fn act(tag: &[u8], uj: u64, p: Provenance, c: Coverage) -> ActivityRef {
ActivityRef::new(ContentId::of_bytes(tag), uj, p, c)
}
/// The seam this module closes: an outcome whose energy is traceable to
/// nothing is refused, rather than published as a number.
#[test]
fn an_unbound_outcome_is_refused() {
let t = OutcomeTrace::new(
energy(1_000_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![],
);
assert_eq!(t.reconcile(), Err(TraceError::Unbound));
}
/// The residual is the cost of being accountable — authorisation,
/// attestation, verification, egress — which no operation receipt claims.
/// It is named, not absorbed.
#[test]
fn the_residual_names_what_no_operation_claims() {
let t = OutcomeTrace::new(
energy(1_000_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![
act(b"a1", 400_000, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 350_000, Provenance::HwShunt, Coverage::Whole),
],
);
let r = t.reconcile().expect("reconciles");
assert_eq!(r.accounted_uj, 750_000);
assert_eq!(r.residual_uj, 250_000, "a quarter of the outcome is overhead");
assert_eq!(r.activities, 2);
// A quarter of a joule unattributed, expressed against the whole.
assert_eq!(r.unattributed_ppm(t.total_uj()), Some(250_000));
}
/// Over-accounting is a finding, not a rounding error: the work is
/// double-counted or an activity is bound to the wrong outcome.
#[test]
fn activities_claiming_more_than_the_outcome_are_refused() {
let t = OutcomeTrace::new(
energy(100, Provenance::HwShunt, Coverage::Whole),
true,
vec![act(b"a1", 90, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 50, Provenance::HwShunt, Coverage::Whole)],
);
assert_eq!(
t.reconcile(),
Err(TraceError::OverAccounted { accounted_uj: 140, total_uj: 100 })
);
}
/// A duplicated activity inflates the accounted figure and shrinks the
/// residual — flattering in exactly the direction that matters, so it is
/// caught before the sum.
#[test]
fn a_duplicated_activity_is_refused_before_it_can_flatter() {
let dup = act(b"same", 100, Provenance::HwShunt, Coverage::Whole);
let t = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![dup.clone(), dup.clone()],
);
match t.reconcile() {
Err(TraceError::DuplicateActivity { receipt }) => {
assert_eq!(receipt, ContentId::of_bytes(b"same"));
}
other => panic!("a double-bound activity must be refused: {other:?}"),
}
}
/// A chain is only as graded as its weakest link. One estimate makes the
/// whole trace estimated — averaging would let a hardware reading launder
/// a pile of models.
#[test]
fn provenance_takes_the_floor_never_the_average() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![
act(b"a1", 300, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 300, Provenance::Estimator, Coverage::Whole),
],
);
let r = t.reconcile().unwrap();
assert_eq!(r.provenance_floor, Provenance::Estimator);
}
/// An unaccounted share does not become accounted for by sitting next to
/// accounted ones.
#[test]
fn coverage_takes_the_floor_too() {
let t = OutcomeTrace::new(
energy(1_000, Provenance::HwShunt, Coverage::Whole),
true,
vec![
act(b"a1", 300, Provenance::HwShunt, Coverage::Whole),
act(b"a2", 300, Provenance::HwShunt, Coverage::Partial),
],
);
assert_eq!(t.reconcile().unwrap().coverage_floor, Coverage::Partial);
}
/// A trace whose operations exactly equal its outcome admits no overhead at
/// all — for a gated, attested, receipt-emitting system that is itself
/// worth a second look, so the residual reports zero rather than hiding it.
#[test]
fn a_zero_residual_is_reported_not_hidden() {
let t = OutcomeTrace::new(
energy(500, Provenance::HwShunt, Coverage::Whole),
true,
vec![act(b"a1", 500, Provenance::HwShunt, Coverage::Whole)],
);
let r = t.reconcile().unwrap();
assert_eq!(r.residual_uj, 0);
assert_eq!(r.unattributed_ppm(500), Some(0));
}
/// Energy spent on an outcome that did not complete is a different claim
/// from energy spent on one that did; the anchor is carried, not dropped.
#[test]
fn the_outcome_anchor_survives_into_the_trace() {
let t = OutcomeTrace::new(
energy(900, Provenance::HwShunt, Coverage::Whole),
false,
vec![act(b"a1", 900, Provenance::HwShunt, Coverage::Whole)],
);
assert!(!t.completed, "the joules bought nothing, and the trace says so");
assert!(t.reconcile().is_ok());
}
/// A ratio against nothing is not a figure.
#[test]
fn no_ratio_against_a_zero_total() {
let t = OutcomeTrace::new(
energy(0, Provenance::Estimator, Coverage::Whole),
true,
vec![act(b"a1", 0, Provenance::Estimator, Coverage::Whole)],
);
assert_eq!(t.reconcile().unwrap().unattributed_ppm(0), None);
}
}
// ---------------------------------------------------------------------------
// Enforcement: a precondition, not a report
// ---------------------------------------------------------------------------
/// Why an outcome receipt must not be sealed.
///
/// §5 of the spec says JCP "makes it a precondition, not a report". Until this
/// existed, traceability was a report: [`crate::Efficiency::seal`] serialises
/// and signs whatever it is handed, so an untraceable outcome, a
/// double-counted activity or a physically impossible figure could all be
/// sealed, signed, and indistinguishable afterwards from a sound one. A
/// signature over an unchecked claim is the most durable way to publish it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SealRefusal {
/// The trace does not reconcile. Carries the reason.
NotTraceable(TraceError),
/// A window's figure is impossible for the part and duration — above the
/// envelope, or below the floor a powered executing part must draw. The
/// second is the shape a frozen counter takes, and §5's liveness rule
/// exists because of it.
PhysicallyRefuted { window_index: usize, detail: String },
/// The receipt claims a hardware grade the evidence under it does not
/// support. Refused rather than silently downgraded **in this direction**,
/// because a caller asking to seal `HwShunt` over modelled windows has a
/// bug, not a preference.
GradeNotEarned { claimed: Provenance, floor: Provenance },
}
impl core::fmt::Display for SealRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SealRefusal::NotTraceable(e) => write!(f, "not traceable: {e}"),
SealRefusal::PhysicallyRefuted { window_index, detail } => {
write!(f, "window {window_index} is physically refuted: {detail}")
}
SealRefusal::GradeNotEarned { claimed, floor } => write!(
f,
"receipt claims {claimed:?} but the evidence under it supports only \
{floor:?}; a signature would make the stronger claim permanent"
),
}
}
}
impl std::error::Error for SealRefusal {}
/// What a trace establishes once every precondition has held.
///
/// Construction is the proof: there is no way to build one except through
/// [`OutcomeTrace::admit`], so a function taking an `AdmittedTrace` knows the
/// checks ran rather than hoping the caller remembered. The same reason
/// `sandbox-attest` hands back a sealed run rather than a boolean.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdmittedTrace {
reconciliation: Reconciliation,
/// The grade the evidence actually supports — **derived**, never the one
/// the caller supplied. Written into the receipt in place of the claim, so
/// aggregation cannot be the step at which a grade improves.
earned_provenance: Provenance,
earned_coverage: Coverage,
}
impl AdmittedTrace {
pub fn reconciliation(&self) -> &Reconciliation {
&self.reconciliation
}
/// The provenance the receipt is entitled to carry.
pub fn earned_provenance(&self) -> Provenance {
self.earned_provenance
}
/// The coverage the receipt is entitled to carry.
pub fn earned_coverage(&self) -> Coverage {
self.earned_coverage
}
}
impl OutcomeTrace {
/// Admit this trace for sealing, or refuse it.
///
/// Three gates, in the order a verifier would apply them:
///
/// 1. **It reconciles.** An unbound, duplicated or over-accounted trace is
/// refused — the arithmetic has to close before anything else matters.
/// 2. **No window is physically impossible.** Checked against the part's
/// envelope when one is supplied. A figure above the burst ceiling or
/// below the idle floor is refused regardless of what the counter said,
/// because the counter is the thing in question.
/// 3. **The grade is earned, not asserted.** The receipt may carry no
/// stronger provenance than the floor across its parts. A caller asking
/// for more is refused rather than quietly downgraded: asking to seal
/// `HwShunt` over `Estimator` evidence is a bug in the caller, and
/// silently writing the weaker grade would hide it.
///
/// The returned [`AdmittedTrace`] carries the *derived* grade, so the
/// sealer writes what the evidence supports instead of what it was handed.
pub fn admit(
&self,
windows: &[crate::dynamics::EnergyWindow],
envelope: Option<&crate::dynamics::PowerEnvelope>,
) -> Result<AdmittedTrace, SealRefusal> {
use crate::dynamics::PhysicalVerdict;
let reconciliation = self.reconcile().map_err(SealRefusal::NotTraceable)?;
if let Some(env) = envelope {
for (i, w) in windows.iter().enumerate() {
match env.check(w) {
PhysicalVerdict::RefutedAbove { claimed_uj, ceiling_uj } => {
return Err(SealRefusal::PhysicallyRefuted {
window_index: i,
detail: format!(
"{claimed_uj} uJ exceeds the {ceiling_uj} uJ the part \
could draw in {} ns",
w.duration_ns
),
})
}
PhysicalVerdict::RefutedBelow { claimed_uj, floor_uj } => {
return Err(SealRefusal::PhysicallyRefuted {
window_index: i,
detail: format!(
"{claimed_uj} uJ is below the {floor_uj} uJ a powered, \
executing part must draw in {} ns — the shape a frozen \
counter takes",
w.duration_ns
),
})
}
// Inside the envelope, or unboundable. Neither is a
// verification; both are permitted to seal, because the
// envelope only ever refutes.
_ => {}
}
}
}
if self.total.provenance != reconciliation.provenance_floor
&& stronger(self.total.provenance, reconciliation.provenance_floor)
{
return Err(SealRefusal::GradeNotEarned {
claimed: self.total.provenance,
floor: reconciliation.provenance_floor,
});
}
Ok(AdmittedTrace {
earned_provenance: reconciliation.provenance_floor,
earned_coverage: reconciliation.coverage_floor,
reconciliation,
})
}
}
/// Whether `a` claims a stronger acquisition class than `b`.
fn stronger(a: Provenance, b: Provenance) -> bool {
fn rank(p: Provenance) -> u8 {
match p {
Provenance::HwShunt => 2,
Provenance::ModelBased => 1,
Provenance::Estimator => 0,
}
}
rank(a) > rank(b)
}

View file

@ -0,0 +1,184 @@
//! Replay the published `energy_trace/1` pack from disk.
//!
//! SPEC §5.3–5.6 states rules, and `gen_trace_vectors` emits the vectors. This
//! reads those bytes back — the same bytes a third party would — and checks the
//! reference implementation reproduces every recorded verdict. A pack only its
//! own generator can read is not a conformance pack.
//!
//! The refusals matter more than the acceptances. An implementation that admits
//! an unbound outcome, a double-counted activity or a frozen counter has failed
//! in the direction that flatters, which is the direction nobody notices.
use jcp_efficiency::dynamics::{
DrawShape, EnergyWindow, PhysicalVerdict, PowerEnvelope,
};
use jcp_efficiency::trace::{OutcomeTrace, TraceError};
use serde_json::Value;
fn pack() -> Value {
let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../conformance/v1/energy_trace.json");
// Assert, not skip. A `return` here would make this test pass green in the
// one case it exists to catch — the pack going missing or moving.
assert!(
p.exists(),
"conformance pack missing at {} — it is what this test replays",
p.display()
);
let bytes = std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
serde_json::from_slice(&bytes).expect("pack parses")
}
#[test]
fn the_pack_declares_its_profile_and_spec() {
let v = pack();
assert_eq!(v["profile"], "energy_trace/1");
assert!(v["spec"].as_str().unwrap().contains("5.3-5.6"));
}
/// Every positive conservation vector reproduces its recorded reconciliation.
#[test]
fn conservation_positives_reproduce_their_recorded_verdicts() {
let v = pack();
let cases = v["conservation"]["positive"].as_array().expect("positives");
assert!(cases.len() >= 2, "expected the published positive cases");
for c in cases {
let note = c["note"].as_str().unwrap_or("?");
let t: OutcomeTrace =
serde_json::from_value(c["input"].clone()).unwrap_or_else(|e| panic!("{note}: {e}"));
let r = t.reconcile().unwrap_or_else(|e| panic!("{note}: must reconcile: {e}"));
let exp = &c["expected"];
assert_eq!(
serde_json::to_value(r.accounted_uj).unwrap(), exp["accounted_uj"],
"{note}: accounted"
);
assert_eq!(
serde_json::to_value(r.residual_uj).unwrap(), exp["residual_uj"],
"{note}: the residual is the whole point"
);
assert_eq!(
serde_json::to_value(r.provenance_floor).unwrap(), exp["provenance_floor"],
"{note}: provenance takes the floor"
);
assert_eq!(
serde_json::to_value(r.coverage_floor).unwrap(), exp["coverage_floor"],
"{note}: coverage takes the floor"
);
}
}
/// Every refusal is actually refused, and for the recorded reason. An
/// implementation that accepted these would be wrong in the flattering
/// direction.
#[test]
fn conservation_negatives_are_refused_for_the_recorded_reason() {
let v = pack();
let cases = v["conservation"]["negative"].as_array().expect("negatives");
assert!(cases.len() >= 3, "expected the three published refusals");
for c in cases {
let note = c["note"].as_str().unwrap_or("?");
let t: OutcomeTrace =
serde_json::from_value(c["input"].clone()).unwrap_or_else(|e| panic!("{note}: {e}"));
let err = t.reconcile().expect_err(&format!("{note}: must be refused"));
let want = c["expected"]["error"].as_str().unwrap();
let got = match err {
TraceError::Unbound => "Unbound",
TraceError::DuplicateActivity { .. } => "DuplicateActivity",
TraceError::OverAccounted { .. } => "OverAccounted",
};
assert_eq!(got, want, "{note}: refused, but for the wrong reason");
}
}
/// The physical bound refutes above the envelope and below the floor — the
/// second being the shape a frozen counter takes.
#[test]
fn physical_bounds_reproduce_their_recorded_verdicts() {
let v = pack();
let env: PowerEnvelope =
serde_json::from_value(v["physical"]["envelope"].clone()).expect("envelope");
let cases = v["physical"]["cases"].as_array().expect("cases");
assert!(cases.len() >= 4);
let mut saw_below = false;
for c in cases {
let note = c["note"].as_str().unwrap_or("?");
let w: EnergyWindow =
serde_json::from_value(c["input"].clone()).unwrap_or_else(|e| panic!("{note}: {e}"));
let got = match env.check(&w) {
PhysicalVerdict::NotRefuted { .. } => "NotRefuted",
PhysicalVerdict::RefutedAbove { .. } => "RefutedAbove",
PhysicalVerdict::RefutedBelow { claimed_uj, floor_uj } => {
saw_below = true;
if let Some(f) = c["expected"]["floor_uj"].as_u64() {
assert_eq!(floor_uj, f, "{note}: floor");
assert_eq!(claimed_uj, 0, "{note}: a frozen counter claims nothing");
}
"RefutedBelow"
}
PhysicalVerdict::Unboundable => "Unboundable",
};
assert_eq!(got, c["expected"]["verdict"].as_str().unwrap(), "{note}");
}
assert!(saw_below, "the frozen-counter refutation must be in the pack");
}
/// Character survives a change of scale, and a padded total does not read as
/// sustained. These two together are what the shape check is for.
#[test]
fn draw_character_reproduces_its_recorded_verdicts() {
let v = pack();
let floor = v["shape"]["idle_floor_uj"].as_u64().expect("idle floor");
let cases = v["shape"]["cases"].as_array().expect("cases");
assert!(cases.len() >= 6);
let mut saw_inconsistent = false;
for c in cases {
let note = c["note"].as_str().unwrap_or("?");
let declared: DrawShape =
serde_json::from_value(c["input"]["declared"].clone()).expect("declared");
let windows: Vec<EnergyWindow> =
serde_json::from_value(c["input"]["windows"].clone()).expect("windows");
let got = jcp_efficiency::dynamics::check_shape(declared, &windows, floor);
let want = c["expected"]["verdict"].as_str().unwrap();
let name = match &got {
jcp_efficiency::dynamics::ShapeVerdict::Consistent { shape } => {
if let Some(s) = c["expected"]["shape"].as_str() {
assert_eq!(
serde_json::to_value(shape).unwrap().as_str().unwrap(), s,
"{note}: shape"
);
}
"Consistent"
}
jcp_efficiency::dynamics::ShapeVerdict::Inconsistent { .. } => {
saw_inconsistent = true;
"Inconsistent"
}
jcp_efficiency::dynamics::ShapeVerdict::Unresolvable { windows } => {
if let Some(n) = c["expected"]["windows"].as_u64() {
assert_eq!(*windows as u64, n, "{note}: window count");
}
"Unresolvable"
}
};
assert_eq!(name, want, "{note}");
}
assert!(saw_inconsistent, "the padding refutation must be in the pack");
}
/// Anti-vacuity across the whole pack: a wrong path, a renamed key or an empty
/// array would otherwise let every test above pass having checked nothing.
#[test]
fn the_pack_carries_every_section_it_claims() {
let v = pack();
for section in ["conservation", "budget", "physical", "shape", "dynamism"] {
assert!(!v[section].is_null(), "section {section} missing from the pack");
}
let total: usize = v["conservation"]["positive"].as_array().map_or(0, Vec::len)
+ v["conservation"]["negative"].as_array().map_or(0, Vec::len)
+ v["physical"]["cases"].as_array().map_or(0, Vec::len)
+ v["shape"]["cases"].as_array().map_or(0, Vec::len)
+ v["dynamism"]["cases"].as_array().map_or(0, Vec::len)
+ v["budget"]["positive"].as_array().map_or(0, Vec::len)
+ v["budget"]["negative"].as_array().map_or(0, Vec::len);
assert!(total >= 20, "expected the full published pack, replayed {total} cases");
}