Skip to content

Commit e9f5ada

Browse files
hyperpolymathclaude
andcommitted
feat: implement Phase 1 — energy/carbon budget enforcement pipeline
Rewrite manifest, ABI, and codegen modules from generic scaffolding to eclexiaiser's core purpose: energy, carbon, and resource-cost awareness. - Manifest: parse eclexiaiser.toml with per-function energy-budget-mj, carbon-budget-mg, carbon provider config (watttime/electricity-maps/static), and report format (text/json/csrd). Full semantic validation. - ABI: EnergyBudget, CarbonBudget, CarbonProvider, CarbonConfig, ComplianceStatus, FunctionMeasurement, SustainabilityReport types with carbon estimation formula and compliance threshold logic. - Codegen parser: validate function annotations and identifiers. - Codegen instrumenter: generate Rust measurement wrappers with per-function budget constants and Eclexia constraint files (.ecl S-expression format). - Codegen reporter: text, JSON, and EU CSRD-compliant sustainability reports with simulated measurements and actionable recommendations. - 10 integration tests + 23 unit tests (all passing). - Example green-service manifest in examples/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 62d20f9 commit e9f5ada

12 files changed

Lines changed: 3096 additions & 56 deletions

File tree

Cargo.lock

Lines changed: 907 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ categories = ["command-line-utilities", "development-tools"]
1414
clap = { version = "4", features = ["derive"] }
1515
serde = { version = "1", features = ["derive"] }
1616
toml = "0.8"
17+
serde_json = "1"
1718
anyhow = "1"
1819
thiserror = "2"
1920
handlebars = "6"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Example eclexiaiser manifest for a "green-service" web application.
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# This manifest declares energy and carbon budgets for two functions:
5+
# - process_batch: a batch data processing function (50 mJ, 10 mg CO2 max)
6+
# - render_page: a web page renderer (5 mJ max, no carbon budget)
7+
#
8+
# Run:
9+
# eclexiaiser validate -m examples/green-service/eclexiaiser.toml
10+
# eclexiaiser generate -m examples/green-service/eclexiaiser.toml -o generated/green-service
11+
# eclexiaiser info -m examples/green-service/eclexiaiser.toml
12+
13+
[project]
14+
name = "green-service"
15+
16+
[[functions]]
17+
name = "process_batch"
18+
source = "src/batch.rs"
19+
energy-budget-mj = 50.0 # millijoules max per call
20+
carbon-budget-mg = 10.0 # mg CO2 max per call
21+
22+
[[functions]]
23+
name = "render_page"
24+
source = "src/web.rs"
25+
energy-budget-mj = 5.0
26+
27+
[carbon]
28+
provider = "watttime" # watttime | electricity-maps | static
29+
region = "GB" # grid region
30+
static-intensity = 200.0 # mg CO2/kWh (fallback if provider = static)
31+
32+
[report]
33+
format = "text" # text | json | csrd
34+
include-recommendations = true

src/abi/mod.rs

Lines changed: 331 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,333 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3-
// ABI module for eclexiaiser — Idris2 proof types for Eclexia interface correctness.
3+
//
4+
// ABI module for eclexiaiser — core types for energy budgets, carbon intensity,
5+
// sustainability reporting, and Eclexia constraint generation.
6+
//
7+
// These types form the contract between manifest parsing, code instrumentation,
8+
// and report generation. In the full ABI-FFI architecture, corresponding Idris2
9+
// definitions in `src/abi/*.idr` would provide formal proofs of interface
10+
// correctness, and Zig FFI in `ffi/zig/` would expose C-ABI bindings.
11+
12+
use serde::{Deserialize, Serialize};
13+
14+
/// Energy budget for a single function, expressed in millijoules (mJ).
15+
///
16+
/// An `EnergyBudget` constrains how much energy a single invocation of a
17+
/// function is allowed to consume. Exceeding the budget triggers a warning
18+
/// or error in the sustainability report.
19+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20+
pub struct EnergyBudget {
21+
/// Maximum energy consumption per call, in millijoules.
22+
pub max_millijoules: f64,
23+
}
24+
25+
impl EnergyBudget {
26+
/// Create a new energy budget with the given millijoule limit.
27+
///
28+
/// # Panics
29+
/// Panics if `max_millijoules` is negative.
30+
pub fn new(max_millijoules: f64) -> Self {
31+
assert!(
32+
max_millijoules >= 0.0,
33+
"Energy budget cannot be negative: {max_millijoules}"
34+
);
35+
Self { max_millijoules }
36+
}
37+
38+
/// Check whether a measured energy value exceeds this budget.
39+
pub fn is_exceeded_by(&self, measured_millijoules: f64) -> bool {
40+
measured_millijoules > self.max_millijoules
41+
}
42+
43+
/// Calculate the percentage of budget used by a measurement.
44+
pub fn usage_percent(&self, measured_millijoules: f64) -> f64 {
45+
if self.max_millijoules == 0.0 {
46+
if measured_millijoules > 0.0 {
47+
return f64::INFINITY;
48+
}
49+
return 0.0;
50+
}
51+
(measured_millijoules / self.max_millijoules) * 100.0
52+
}
53+
}
54+
55+
/// Carbon budget for a single function, expressed in milligrams of CO2 equivalent.
56+
///
57+
/// This constrains the carbon emissions attributable to a function call, computed
58+
/// from energy consumption multiplied by grid carbon intensity.
59+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60+
pub struct CarbonBudget {
61+
/// Maximum CO2 emissions per call, in milligrams.
62+
pub max_mg_co2: f64,
63+
}
64+
65+
impl CarbonBudget {
66+
/// Create a new carbon budget with the given mg CO2 limit.
67+
pub fn new(max_mg_co2: f64) -> Self {
68+
assert!(
69+
max_mg_co2 >= 0.0,
70+
"Carbon budget cannot be negative: {max_mg_co2}"
71+
);
72+
Self { max_mg_co2 }
73+
}
74+
75+
/// Check whether a measured carbon value exceeds this budget.
76+
pub fn is_exceeded_by(&self, measured_mg_co2: f64) -> bool {
77+
measured_mg_co2 > self.max_mg_co2
78+
}
79+
}
80+
81+
/// Carbon intensity provider — the source of grid carbon intensity data.
82+
///
83+
/// Eclexia supports multiple providers for real-time or static carbon intensity
84+
/// values, enabling accurate carbon accounting per grid region.
85+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86+
#[serde(rename_all = "lowercase")]
87+
pub enum CarbonProvider {
88+
/// WattTime API — real-time marginal emissions data.
89+
Watttime,
90+
/// Electricity Maps API — real-time lifecycle emissions.
91+
ElectricityMaps,
92+
/// Static intensity value, useful for offline or testing scenarios.
93+
Static,
94+
}
95+
96+
impl CarbonProvider {
97+
/// Return a human-readable display name for the provider.
98+
pub fn display_name(&self) -> &'static str {
99+
match self {
100+
CarbonProvider::Watttime => "WattTime",
101+
CarbonProvider::ElectricityMaps => "Electricity Maps",
102+
CarbonProvider::Static => "Static",
103+
}
104+
}
105+
}
106+
107+
/// Carbon configuration section — provider, region, and static fallback.
108+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
109+
pub struct CarbonConfig {
110+
/// Which carbon intensity provider to use.
111+
pub provider: CarbonProvider,
112+
/// Grid region code (e.g., "GB", "DE", "US-CAL-CISO").
113+
pub region: String,
114+
/// Static intensity in mg CO2 per kWh (used when provider is Static).
115+
#[serde(rename = "static-intensity", default)]
116+
pub static_intensity: f64,
117+
}
118+
119+
impl CarbonConfig {
120+
/// Compute carbon emissions in mg CO2 from energy in millijoules.
121+
///
122+
/// Uses the static intensity value. For real-time providers, the actual
123+
/// intensity would be fetched from the API at measurement time; this method
124+
/// serves as the fallback calculation.
125+
///
126+
/// Formula: mg_co2 = (millijoules / 3_600_000) * (mg_co2_per_kwh)
127+
/// Because 1 kWh = 3,600,000,000 mJ, but intensity is in mg/kWh:
128+
/// mg_co2 = millijoules * intensity / 3_600_000_000
129+
pub fn estimate_carbon_mg(&self, millijoules: f64) -> f64 {
130+
millijoules * self.static_intensity / 3_600_000_000.0
131+
}
132+
}
133+
134+
/// Report format for sustainability output.
135+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136+
#[serde(rename_all = "lowercase")]
137+
pub enum ReportFormat {
138+
/// Plain text report, human-readable.
139+
Text,
140+
/// JSON report, machine-readable.
141+
Json,
142+
/// EU CSRD (Corporate Sustainability Reporting Directive) compliant format.
143+
Csrd,
144+
}
145+
146+
/// Report configuration section.
147+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148+
pub struct ReportConfig {
149+
/// Output format for the sustainability report.
150+
pub format: ReportFormat,
151+
/// Whether to include actionable recommendations for reducing impact.
152+
#[serde(rename = "include-recommendations", default)]
153+
pub include_recommendations: bool,
154+
}
155+
156+
/// A single function's energy/carbon budget configuration, as declared in the manifest.
157+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158+
pub struct FunctionBudget {
159+
/// Function name (fully qualified or short).
160+
pub name: String,
161+
/// Source file containing the function.
162+
pub source: String,
163+
/// Energy budget in millijoules per call (None = unbounded).
164+
#[serde(rename = "energy-budget-mj")]
165+
pub energy_budget_mj: Option<f64>,
166+
/// Carbon budget in mg CO2 per call (None = unbounded).
167+
#[serde(rename = "carbon-budget-mg")]
168+
pub carbon_budget_mg: Option<f64>,
169+
}
170+
171+
impl FunctionBudget {
172+
/// Build an `EnergyBudget` from this function's config, if specified.
173+
pub fn energy_budget(&self) -> Option<EnergyBudget> {
174+
self.energy_budget_mj.map(EnergyBudget::new)
175+
}
176+
177+
/// Build a `CarbonBudget` from this function's config, if specified.
178+
pub fn carbon_budget(&self) -> Option<CarbonBudget> {
179+
self.carbon_budget_mg.map(CarbonBudget::new)
180+
}
181+
}
182+
183+
/// Compliance status for a single function measurement against its budget.
184+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185+
pub enum ComplianceStatus {
186+
/// Within budget (usage <= 100%).
187+
Compliant,
188+
/// Approaching budget (usage > 80% but <= 100%).
189+
Warning,
190+
/// Over budget (usage > 100%).
191+
Exceeded,
192+
/// No budget was set, so compliance is not applicable.
193+
Unbounded,
194+
}
195+
196+
impl ComplianceStatus {
197+
/// Determine compliance status from a budget usage percentage.
198+
pub fn from_usage_percent(percent: f64) -> Self {
199+
if percent > 100.0 {
200+
ComplianceStatus::Exceeded
201+
} else if percent > 80.0 {
202+
ComplianceStatus::Warning
203+
} else {
204+
ComplianceStatus::Compliant
205+
}
206+
}
207+
208+
/// Return a short label for display purposes.
209+
pub fn label(&self) -> &'static str {
210+
match self {
211+
ComplianceStatus::Compliant => "COMPLIANT",
212+
ComplianceStatus::Warning => "WARNING",
213+
ComplianceStatus::Exceeded => "EXCEEDED",
214+
ComplianceStatus::Unbounded => "UNBOUNDED",
215+
}
216+
}
217+
}
218+
219+
/// A measurement result for a single function, including energy, carbon, and compliance.
220+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221+
pub struct FunctionMeasurement {
222+
/// Name of the measured function.
223+
pub function_name: String,
224+
/// Measured energy in millijoules.
225+
pub measured_energy_mj: f64,
226+
/// Estimated carbon in mg CO2.
227+
pub estimated_carbon_mg: f64,
228+
/// Energy budget compliance.
229+
pub energy_compliance: ComplianceStatus,
230+
/// Carbon budget compliance.
231+
pub carbon_compliance: ComplianceStatus,
232+
}
233+
234+
/// A complete sustainability report for a project.
235+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236+
pub struct SustainabilityReport {
237+
/// Project name.
238+
pub project_name: String,
239+
/// Grid region used for carbon calculations.
240+
pub region: String,
241+
/// Carbon intensity provider name.
242+
pub provider: String,
243+
/// Per-function measurement results.
244+
pub measurements: Vec<FunctionMeasurement>,
245+
/// Total energy across all measured functions, in millijoules.
246+
pub total_energy_mj: f64,
247+
/// Total estimated carbon across all measured functions, in mg CO2.
248+
pub total_carbon_mg: f64,
249+
/// Overall compliance: true if all functions are compliant or warning.
250+
pub all_compliant: bool,
251+
/// Actionable recommendations (empty if not requested).
252+
pub recommendations: Vec<String>,
253+
}
254+
255+
impl SustainabilityReport {
256+
/// Return the count of functions that exceeded their energy budget.
257+
pub fn energy_violations(&self) -> usize {
258+
self.measurements
259+
.iter()
260+
.filter(|m| m.energy_compliance == ComplianceStatus::Exceeded)
261+
.count()
262+
}
263+
264+
/// Return the count of functions that exceeded their carbon budget.
265+
pub fn carbon_violations(&self) -> usize {
266+
self.measurements
267+
.iter()
268+
.filter(|m| m.carbon_compliance == ComplianceStatus::Exceeded)
269+
.count()
270+
}
271+
}
272+
273+
#[cfg(test)]
274+
mod tests {
275+
use super::*;
276+
277+
#[test]
278+
fn test_energy_budget_compliance() {
279+
let budget = EnergyBudget::new(50.0);
280+
assert!(!budget.is_exceeded_by(49.9));
281+
assert!(!budget.is_exceeded_by(50.0));
282+
assert!(budget.is_exceeded_by(50.1));
283+
assert!((budget.usage_percent(25.0) - 50.0).abs() < f64::EPSILON);
284+
}
285+
286+
#[test]
287+
fn test_carbon_budget_compliance() {
288+
let budget = CarbonBudget::new(10.0);
289+
assert!(!budget.is_exceeded_by(10.0));
290+
assert!(budget.is_exceeded_by(10.001));
291+
}
292+
293+
#[test]
294+
fn test_carbon_estimation() {
295+
let config = CarbonConfig {
296+
provider: CarbonProvider::Static,
297+
region: "GB".to_string(),
298+
static_intensity: 200.0,
299+
};
300+
// 3,600,000,000 mJ = 1 kWh => at 200 mg/kWh => 200 mg
301+
// 3,600,000 mJ => 0.001 kWh => 0.2 mg
302+
let carbon = config.estimate_carbon_mg(3_600_000.0);
303+
assert!((carbon - 0.2).abs() < 1e-6);
304+
}
305+
306+
#[test]
307+
fn test_compliance_status_from_percent() {
308+
assert_eq!(
309+
ComplianceStatus::from_usage_percent(50.0),
310+
ComplianceStatus::Compliant
311+
);
312+
assert_eq!(
313+
ComplianceStatus::from_usage_percent(85.0),
314+
ComplianceStatus::Warning
315+
);
316+
assert_eq!(
317+
ComplianceStatus::from_usage_percent(101.0),
318+
ComplianceStatus::Exceeded
319+
);
320+
}
321+
322+
#[test]
323+
fn test_function_budget_builders() {
324+
let fb = FunctionBudget {
325+
name: "process".to_string(),
326+
source: "src/lib.rs".to_string(),
327+
energy_budget_mj: Some(50.0),
328+
carbon_budget_mg: None,
329+
};
330+
assert!(fb.energy_budget().is_some());
331+
assert!(fb.carbon_budget().is_none());
332+
}
333+
}

0 commit comments

Comments
 (0)