open-standards/joule-code/joule-code-rs/crates/joule-code-remediate/src/lib.rs
Transaction Science 3589be01f3 joule-code-remediate: ship a formal.json template; make the parse error actionable
The formal tier's input is hand-authored, and its serde forms are easy to guess
wrong (PascalCase enums, a stringified ContentAddress, JR-N rigor). Removes that
friction without touching the upstream schema serde:

  - formal.example.json — a working, copy-pasteable template (the netting
    postcondition), with the gotchas spelled out in an ignored _comment key.
  - the FormalSpec doc and the parse-error message now name the exact forms and
    point at the template.
  - a test deserializes the shipped template and checks its indices, so it can
    never drift from the schema.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 01:56:19 -04:00

1537 lines
65 KiB
Rust

//! The reference verified-fix harness.
//!
//! What every commercial remediation product gets wrong, this harness makes
//! structural: the verification pipeline is not a feature bolted beside the
//! fix — **the gates ARE the contract obligations**. A run builds a
//! JouleContract whose `pre`/`post`/`inv` clauses are the pipeline stages
//! (baseline capture, build, no-new-test-failures, lint-delta, frame
//! confinement, diff guard), discharges each one with the stage's actual
//! outcome, and seals the result with hardware-measured energy. Three
//! consequences, by construction:
//!
//! 1. **A fix that fails its gate is reverted and never ships** — but the
//! refusal still produces a receipt: an undischarged obligation on the
//! record beats a silently dropped fix.
//! 2. **The energy figure is measured, not estimated**, whenever the host has
//! a hardware counter ([`sandbox_meter::PowerMeter`]); the meter
//! attestation inside the JouleContract receipt is a sealed sandbox JCR-1
//! envelope — one standard's receipt is the other's evidence.
//! 3. **The receipt grades honestly**: receipt-local validation, JR-2 at
//! most, generator and verifier self-declared and labeled as such. Higher
//! rigor is a deployment's verifier-of-record work, exactly per the
//! JouleContract two-stage rule.
//!
//! v0 remediates at the **L1 lawful tier only** — `cargo clippy --fix` and
//! `rustfmt`, deterministic codemods, zero inference — which is not a
//! limitation but the doctrine: the cheapest sufficient tier closes the
//! request. Model tiers (L3 local, L4 federation) hang off the same gate.
use joule_code_cargo::{Anchor, CodingOp, Edit};
use joule_code_schema::{ContentId, Tier};
use joulecontract_receipt::{
validate, DischargeEntry, DischargeResult, Engagement, FrameMode, Generator, MeterBinding,
Qualification, Receipt, TierClass, TrustRoot,
};
use joulecontract_schema::{
Ceiling, ContentAddress, Contract, EnergyClass, GovernanceClass, ObligationKind, Profile,
Rigor,
};
use sandbox_meter::{run_attributed, PowerMeter};
use serde::{Deserialize, Serialize};
// The formally-verified fix tier. `DischargeStatus`/`Formalization`/
// `ContractDischarge` come from the proof bridge; the bridge's own
// `DischargeEntry` is NOT imported — that name already belongs to
// `joulecontract_receipt` above (the operational receipt's entry).
use joulecontract_proof::{discharge as proof_discharge, DischargeStatus, Formalization};
use proof_cascade::{ProofCache, Router};
use proof_solvers::{Propositional, Z3Smt};
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant};
/// Accumulates per-process energy attribution across a run's cargo
/// subprocesses (the real compute). The harness's `run()` helper feeds this
/// for every `cargo` invocation; git and rustfmt are negligible overhead and
/// run unmetered.
struct MeterAcc {
meter: PowerMeter,
/// Σ machine-wide SoC-CPU energy over the metered windows (the upper bound).
soc_uj: u64,
/// Σ energy attributed to this run's process trees (ModelBased apportion).
attr_uj: u64,
/// True once at least one window rested on real per-process accounting.
attributed: bool,
}
#[derive(Debug, thiserror::Error)]
pub enum HarnessError {
#[error("not a git work tree: {0} (the harness needs revert-ability before it will touch anything)")]
NotGit(PathBuf),
#[error("workspace has uncommitted changes; commit or stash first (the harness must own the diff)")]
Dirty,
#[error("command failed to launch: {0}")]
Spawn(String),
#[error("contract error: {0}")]
Contract(String),
}
/// One pipeline stage's outcome — the discharge evidence for one obligation.
#[derive(Clone, Debug, Serialize)]
pub struct GateOutcome {
pub gate: String,
pub passed: bool,
pub detail: String,
}
/// The formal-tier input, read from `<workspace>/.joule-fix/formal.json` when
/// present: a domain [`Contract`] and the [`Formalization`]s that encode the
/// obligations the fix must honor. Absent file ⇒ no formal gate (the harness
/// runs exactly as before). The formalizations are an explicit, grounded input
/// — the harness never invents one (autoformalization is an upstream concern).
///
/// `formal.example.json` (next to this crate's `Cargo.toml`) is a working,
/// copy-pasteable template. The serde forms that trip up a hand-written file:
/// `rigor_target` is `"JR-1".."JR-4"`; `ceiling.enb_ref` is a **string**
/// `"sha2-256:<64-hex>"` (not an object); `kind` is one of
/// `Trivial|Arithmetic|Decidable|General|Hard`; `grounding` is one of
/// `Ungrounded|HumanReviewed|CrossChecked` — all PascalCase. `obligation`
/// indexes the contract's obligations in declaration order (pre, then post,
/// then inv, then frame).
#[derive(Debug, Deserialize)]
struct FormalSpec {
contract: Contract,
formalizations: Vec<Formalization>,
}
/// One formalized obligation's proof outcome, for the report and evidence file.
#[derive(Clone, Debug, Serialize)]
pub struct FormalEntry {
/// The obligation text as the domain contract declares it.
pub obligation: String,
/// `proved` | `refuted` | `unproved`.
pub verdict: String,
/// The tier that closed it (when proved).
pub closed_by: Option<String>,
/// The counter-model witness (when refuted by a decision procedure).
pub counter_model: Option<String>,
/// Measured joules for this obligation's proof attempts.
pub uj: u64,
}
/// The formal-tier verdict for a run: did every supplied formalization prove?
#[derive(Clone, Debug, Serialize)]
pub struct FormalOutcome {
pub proved: usize,
pub refuted: usize,
pub unproved: usize,
/// Total measured joules across all proof attempts (`ModelBased`, in-process
/// — distinct from the cargo-attributed gate energy).
pub proof_uj: u64,
/// True iff at least one formalization was supplied and **every** one
/// proved. This is the gate: any refutation or unproven goal fails it, and
/// the fix reverts like any other failed gate.
pub all_proved: bool,
pub entries: Vec<FormalEntry>,
}
/// The result of one harness run.
#[derive(Debug)]
pub struct RunReport {
/// Findings before the fix (clippy warning count).
pub findings_before: usize,
/// Findings after (counted only when a fix was applied).
pub findings_after: Option<usize>,
pub changed_files: Vec<String>,
pub gates: Vec<GateOutcome>,
/// All gates green and a non-empty fix was applied.
pub shipped: bool,
/// The branch carrying the fix, when shipped.
pub branch: Option<String>,
/// Energy attributed to this run's process trees (`ModelBased`
/// apportionment of a real SoC measurement), microjoules — the receipt's
/// figure, and the honest per-workload cost.
pub attributed_uj: u64,
/// Machine-wide SoC-CPU energy over the metered windows, microjoules —
/// the upper bound, for context.
pub soc_cpu_uj: u64,
/// True when the figure rests on real per-process accounting.
pub attributed: bool,
pub wall: Duration,
pub meter_source: String,
pub contract: Contract,
pub receipt: Receipt,
/// The DSSE envelope (in-toto Statement, JouleContract predicate).
pub envelope_json: String,
/// The sealed sandbox JCR-1 meter envelope backing the meter binding.
pub meter_envelope: Vec<u8>,
/// The receipt-local verdict claim string.
pub local_claim: String,
/// The formal-tier outcome, when a `.joule-fix/formal.json` was present and
/// the fix carried formalizations. `None` ⇒ no formal gate ran (the default,
/// behaving exactly as before). A failed formal gate appears in `gates` and
/// blocks shipping like any other.
pub formal: Option<FormalOutcome>,
}
impl RunReport {
/// Microjoules for the whole verified run (measure → fix → gate): the
/// energy ATTRIBUTED to this run's process trees, not the machine-wide
/// total. `soc_cpu_uj` carries the machine-wide upper bound.
pub fn total_uj(&self) -> u64 {
self.attributed_uj
}
/// The headline metric nobody else publishes: microjoules per verified,
/// shipped fix. `None` when nothing shipped (energy was still spent and
/// is still on the receipt — refusals aren't free, and saying so is the
/// point).
pub fn uj_per_verified_fix(&self) -> Option<u64> {
if !self.shipped {
return None;
}
let fixed = self
.findings_after
.map(|after| self.findings_before.saturating_sub(after))
.unwrap_or(0);
if fixed == 0 {
None
} else {
Some(self.total_uj() / fixed as u64)
}
}
}
/// One induced remediation skill: a deterministic line transform keyed by
/// the lint's message text, compiled from a fix that SHIPPED through the
/// full gate at a stochastic tier. Recurrences of the same pattern never
/// consult a model again — the skill is the L1 form of an L3 lesson.
#[derive(Clone, Debug, Serialize, serde::Deserialize)]
pub struct Skill {
/// The normalized lint message (everything after `warning:`/`error:`).
pub message_key: String,
/// The exact offending line (as it appeared, trimmed).
pub old_line: String,
/// Its replacement (trimmed; original indentation is preserved on apply).
pub new_line: String,
/// How many times the skill has closed a recurrence.
pub hits: u64,
}
/// The skill store: a JSON file of induced rules. Plain data, no model,
/// no registry service — a skill you can read is a skill you can audit.
#[derive(Default, Serialize, serde::Deserialize)]
pub struct SkillStore {
pub skills: Vec<Skill>,
}
impl SkillStore {
pub fn load(path: &std::path::Path) -> SkillStore {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, path: &std::path::Path) {
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(json) = serde_json::to_string_pretty(self) {
let _ = std::fs::write(path, json);
}
}
}
/// The lint message with its location stripped: the class key.
fn message_key(finding: &str) -> Option<String> {
let tail = finding
.split_once(": warning: ")
.or_else(|| finding.split_once(": error: "))?
.1;
Some(tail.trim().to_string())
}
/// Harness configuration: identities and limits a deployment binds once.
pub struct Harness {
pub workspace: PathBuf,
/// Maximum changed lines a single run may ship.
pub max_diff_lines: usize,
/// L1 tier energy ceiling, joules (cargo subprocess work is host-priced,
/// not token-priced; default is deliberately generous and recorded).
pub ceiling_joules: f64,
/// Steward label for the meter seal + receipt signer derivation.
pub steward: String,
signing: ed25519_dalek::SigningKey,
/// The meter subsystem's own key: the energy attestation must not be
/// signed by the receipt signer (JouleContract §4.2 — a self-attested
/// meter does not bind). The sandbox meter is genuinely a different
/// component; it gets a different key.
meter_signing: ed25519_dalek::SigningKey,
/// The L3 stochastic fixer, as a subprocess seam: a command that reads a
/// remediation prompt on stdin and writes SEARCH/REPLACE blocks on
/// stdout. Any local model CLI plugs in here (a jouleclaw decode binary,
/// a federation leg) — and whatever comes back faces the same gates as
/// the deterministic tier, which is what makes a weak model SAFE to run.
/// Defaults from `JOULE_FIX_L3_CMD`; `None` disables the tier.
pub l3_cmd: Option<String>,
/// Where induced skills persist. Defaults from `JOULE_FIX_SKILLS`, else
/// `~/.joule-fix/skills.json`; the store is created on first induction.
pub skill_store: PathBuf,
/// Per-process energy attribution, accumulated across cargo subprocesses.
/// `RefCell` because `run()` takes `&self` and feeds it call by call.
meter: RefCell<MeterAcc>,
}
impl Harness {
pub fn new(workspace: impl Into<PathBuf>) -> Result<Harness, HarnessError> {
let workspace = workspace.into();
// Inside ANY git work tree counts — a workspace may be a subdirectory
// of a monorepo. Revert-ability is the requirement, not a local .git.
let inside = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.current_dir(&workspace)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !inside {
return Err(HarnessError::NotGit(workspace));
}
Ok(Harness {
workspace,
max_diff_lines: 400,
ceiling_joules: 2_000.0,
steward: "joule-fix.local".into(),
signing: ed25519_dalek::SigningKey::from_bytes(&[11u8; 32]),
meter_signing: ed25519_dalek::SigningKey::from_bytes(&[12u8; 32]),
l3_cmd: std::env::var("JOULE_FIX_L3_CMD").ok().filter(|s| !s.trim().is_empty()),
skill_store: std::env::var("JOULE_FIX_SKILLS")
.map(PathBuf::from)
.unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".joule-fix/skills.json")
}),
meter: RefCell::new(MeterAcc {
meter: PowerMeter::detect(),
soc_uj: 0,
attr_uj: 0,
attributed: false,
}),
})
}
/// Use a real signing key instead of the published demo key.
pub fn with_signing_key(mut self, key: ed25519_dalek::SigningKey) -> Self {
self.signing = key;
self
}
fn run(&self, program: &str, args: &[&str]) -> Result<(bool, String), HarnessError> {
// The real compute is `cargo`; meter and attribute it. Other programs
// (git, rustfmt) are negligible harness overhead and run unmetered.
if program == "cargo" {
let mut acc = self.meter.borrow_mut();
let MeterAcc { meter, soc_uj, attr_uj, attributed } = &mut *acc;
let (ok, text, a) = run_attributed(meter, program, args, &self.workspace)
.map_err(|e| HarnessError::Spawn(format!("{program}: {e}")))?;
*soc_uj = soc_uj.saturating_add(a.soc_cpu_uj);
*attr_uj = attr_uj.saturating_add(a.attributed_uj);
*attributed |= a.attributed;
return Ok((ok, text));
}
let out = Command::new(program)
.args(args)
.current_dir(&self.workspace)
.output()
.map_err(|e| HarnessError::Spawn(format!("{program}: {e}")))?;
let mut text = String::from_utf8_lossy(&out.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&out.stderr));
Ok((out.status.success(), text))
}
fn git_head(&self) -> Result<String, HarnessError> {
let (ok, out) = self.run("git", &["rev-parse", "HEAD"])?;
if !ok {
return Err(HarnessError::Spawn("git rev-parse HEAD".into()));
}
Ok(out.trim().to_string())
}
fn git_clean(&self) -> Result<bool, HarnessError> {
let (_, out) = // -uno: the harness owns the TRACKED diff; untracked build artifacts
// (target/, .joule-fix/) are not its concern and must not wedge it.
self.run("git", &["status", "--porcelain", "--untracked-files=no", "--", "."])?;
Ok(out.trim().is_empty())
}
fn clippy_warning_count(&self) -> Result<usize, HarnessError> {
let (_, out) = self.run(
"cargo",
&["clippy", "--workspace", "--message-format=short", "--quiet"],
)?;
// A finding is a per-location diagnostic: "<path>:<line>:<col>: warning|error".
// Deny-by-default lints arrive as errors and are findings too (they
// don't block `cargo build`, only clippy itself). Summary lines
// ("error: could not compile …") carry no location and don't count.
Ok(out
.lines()
.filter(|l| {
l.contains(".rs:") && (l.contains(": warning:") || l.contains(": error:"))
})
.count())
}
fn test_failures(&self) -> Result<BTreeSet<String>, HarnessError> {
// NOT --quiet: quiet mode suppresses the per-test result lines the
// baseline diff is built from.
let (_, out) = self.run("cargo", &["test", "--workspace"])?;
Ok(out
.lines()
.filter_map(|l| l.strip_prefix("test ").and_then(|r| r.strip_suffix(" ... FAILED")))
.map(str::to_string)
.collect())
}
fn changed_files(&self) -> Result<Vec<String>, HarnessError> {
let (_, out) = self.run("git", &["diff", "--name-only", "--relative", "--", "."])?;
Ok(out.lines().map(str::to_string).filter(|s| !s.is_empty()).collect())
}
fn diff_lines(&self) -> Result<usize, HarnessError> {
let (_, out) = self.run("git", &["diff", "--numstat", "--relative", "--", "."])?;
Ok(out
.lines()
.filter_map(|l| {
let mut it = l.split_whitespace();
let add: usize = it.next()?.parse().ok()?;
let del: usize = it.next()?.parse().ok()?;
Some(add + del)
})
.sum())
}
/// One full verified-remediation run.
pub fn run_once(&self) -> Result<RunReport, HarnessError> {
if !self.git_clean()? {
return Err(HarnessError::Dirty);
}
// Load+validate the formal spec FIRST — a malformed spec must fail on a
// clean tree, before any edit the harness would then have to revert.
let formal_spec = self.load_formal_spec()?;
let prior_rev = self.git_head()?;
let prior = ContentId::of_bytes(prior_rev.as_bytes());
// Reset the per-run attribution accumulator; cargo calls feed it.
{
let mut acc = self.meter.borrow_mut();
acc.soc_uj = 0;
acc.attr_uj = 0;
}
let wall_start = Instant::now();
// ---- Stage 0: baseline (the `pre` obligation's evidence).
let baseline_failures = self.test_failures()?;
let findings_before = self.clippy_warning_count()?;
let baseline_captured = true;
let mut gates: Vec<GateOutcome> = vec![GateOutcome {
gate: "baseline".into(),
passed: baseline_captured,
detail: format!(
"{} clippy findings, {} pre-existing test failures recorded",
findings_before,
baseline_failures.len()
),
}];
// ---- Fix at the L1 lawful tier: deterministic codemods, no model.
// Nothing to fix ⇒ nothing runs: a clean workspace gets a verified-
// clean receipt, not a formatting pass dressed as remediation.
if findings_before > 0 {
let (_, _fix_out) = self.run(
"cargo",
&[
"clippy",
"--workspace",
"--fix",
"--allow-dirty",
"--allow-no-vcs",
"--quiet",
],
)?;
// Format ONLY the files the fix touched — reformatting untouched
// files is out-of-frame noise, not remediation.
let touched = self.changed_files()?;
for f in touched.iter().filter(|f| f.ends_with(".rs")) {
let _ = self.run("rustfmt", &["--edition", "2021", f]);
}
}
// ---- Induced skills next: lessons already paid for at a stochastic
// tier re-apply deterministically, before any model is consulted.
let mut skills_applied = 0usize;
if findings_before > 0 && self.clippy_warning_count()? > 0 {
skills_applied = self.skill_pass()?;
}
// ---- Escalate to L3 only what L1 + skills left behind (cheapest
// sufficient tier: the model is consulted last, and only when
// configured). Snapshot finding files first — a shipped L3 fix is
// the raw material skills are induced from.
let mut used_l3 = false;
// (path, message_key, offending line trimmed, old content)
let mut l3_snapshot: Vec<(String, String, String, String)> = Vec::new();
if self.l3_cmd.is_some() && findings_before > 0 && self.clippy_warning_count()? > 0 {
for f in self.current_findings()? {
let mut parts = f.split(':');
let (Some(path), line_no, Some(key)) =
(parts.next(), parts.next().and_then(|n| n.parse::<usize>().ok()), message_key(&f))
else {
continue;
};
if !path.ends_with(".rs") {
continue;
}
if let (Some(n), Ok(body)) = (line_no, std::fs::read_to_string(self.workspace.join(path))) {
if let Some(line) = body.lines().nth(n.saturating_sub(1)) {
l3_snapshot.push((path.to_string(), key, line.trim().to_string(), body));
}
}
}
used_l3 = self.l3_pass()?;
}
let changed = self.changed_files()?;
// ---- Gates (each one is an obligation's discharge evidence).
let build_ok = changed.is_empty() || self.run("cargo", &["build", "--workspace", "--quiet"])?.0;
gates.push(GateOutcome {
gate: "build".into(),
passed: build_ok,
detail: if changed.is_empty() { "no fix applied".into() } else { "cargo build --workspace".into() },
});
let post_failures = if changed.is_empty() { baseline_failures.clone() } else { self.test_failures()? };
let new_failures: Vec<&String> = post_failures.difference(&baseline_failures).collect();
let tests_ok = new_failures.is_empty();
gates.push(GateOutcome {
gate: "tests-vs-baseline".into(),
passed: tests_ok,
detail: if tests_ok {
"no new test failures".into()
} else {
format!("NEW failures: {new_failures:?}")
},
});
let findings_after = self.clippy_warning_count()?;
let lint_ok = changed.is_empty() || findings_after < findings_before;
gates.push(GateOutcome {
gate: "lint-delta".into(),
passed: lint_ok,
detail: format!("{findings_before} → {findings_after} findings"),
});
// Frame: the fix may touch Rust sources and manifests anywhere in the
// workspace — and nothing else (no CI files, no secrets, no scripts).
let frame_paths: Vec<String> = vec!["src/".into(), "crates/".into(), "Cargo.toml".into(), "Cargo.lock".into()];
let op = CodingOp {
alg: 0x01,
prior: prior.clone(),
edits: changed
.iter()
.map(|p| Edit {
path: p.clone(),
anchor: Anchor::ByteRange { start: 0, end: 0 },
replacement: Vec::new(), // identity carried by git; anchors are per-file markers here
})
.collect(),
author: self.signing.verifying_key().to_bytes(),
intent: format!(
"remediate {findings_before} lint findings ({}{})",
if used_l3 { "L1 lawful + L3 stochastic, gate-verified" } else { "L1 lawful tier" },
if skills_applied > 0 {
format!("; {skills_applied} induced skill(s) applied — zero inference")
} else {
String::new()
}
),
tier: if used_l3 { Tier::L3 } else { Tier::L1 },
};
let contract = self.contract(&frame_paths, findings_before);
let frame_ok = changed.is_empty()
|| joule_code_joulecontract::frame_check(&contract, &op).is_ok();
gates.push(GateOutcome {
gate: "frame".into(),
passed: frame_ok,
detail: format!("{} files changed, footprint {:?}", changed.len(), frame_paths),
});
let diff = self.diff_lines()?;
let diff_ok = diff <= self.max_diff_lines;
gates.push(GateOutcome {
gate: "diff-guard".into(),
passed: diff_ok,
detail: format!("{diff} changed lines (limit {})", self.max_diff_lines),
});
// ---- Formal tier (opt-in): if the run carries formalizations
// (`.joule-fix/formal.json`), the fix's formal obligations must be
// machine-checked by a decision procedure. A refuted or unproven goal
// fails the gate — which reverts the fix exactly like a failed test.
// The rung above build+test, and the one the market has not reached.
let formal = formal_spec.as_ref().map(|s| self.discharge_formal(s));
if let Some(f) = &formal {
gates.push(GateOutcome {
gate: "formal-proof".into(),
passed: f.all_proved,
detail: format!(
"{} proved, {} refuted, {} unproved ({:.3} J, real z3)",
f.proved,
f.refuted,
f.unproved,
f.proof_uj as f64 / 1e6,
),
});
}
// ---- Verdict: every gate green AND a real fix happened.
let all_green = gates.iter().all(|g| g.passed);
let shipped = all_green && !changed.is_empty();
let branch = if shipped {
let name = format!("joule-fix/{}", &prior_rev[..12.min(prior_rev.len())]);
let _ = self.run("git", &["checkout", "-b", &name]);
// -u: stage only tracked modifications — the harness owns the
// tracked diff; untracked artifacts (target/) are not its fix.
let _ = self.run("git", &["add", "-u", "--", "."]);
let _ = self.run(
"git",
&["commit", "-m", &format!("joule-fix: {} lint findings remediated, all gates green", findings_before.saturating_sub(findings_after))],
);
// A gate-verified stochastic fix is a lesson worth keeping:
// compile it into a deterministic skill so the next recurrence
// never pays the model's joules again.
if used_l3 {
self.induce_skills(&l3_snapshot);
}
Some(name)
} else {
// Never ship on a failed gate: revert the work tree — and VERIFY
// the revert took. A swallowed revert failure leaves the rejected
// diff in the user's tree, which is worse than shipping it.
let _ = self.run("git", &["checkout", "--", "."]);
if !self.git_clean()? {
let _ = self.run("git", &["restore", "--source=HEAD", "--worktree", "--", "."]);
}
if !self.git_clean()? {
return Err(HarnessError::Spawn(
"revert failed: rejected changes remain in the work tree — manual cleanup required".into(),
));
}
None
};
// ---- Energy: read the per-process attribution accumulated across the
// run's cargo subprocesses. The receipt carries the ATTRIBUTED figure
// (ModelBased — an apportionment of a real SoC measurement), not the
// machine-wide total; the SoC total rides along as honest context.
let (soc_cpu_uj, attributed_uj, attributed) = {
let acc = self.meter.borrow();
(acc.soc_uj, acc.attr_uj, acc.attributed && acc.meter.is_hardware())
};
let wall = wall_start.elapsed();
let meter_source = if attributed {
"attributed(ModelBased over IOReport HwShunt)".to_string()
} else {
"estimator".to_string()
};
// The sandbox meter receipt records the attributed figure as a
// ModelBased CPU reading; the JouleContract receipt anchors to it.
let device = sandbox_meter::DeviceEnergy {
class: sandbox_schema::DeviceClass::Cpu,
measured_uj: None,
estimated_uj: attributed_uj,
provenance: sandbox_meter::Provenance::ModelBased,
};
let meter_receipt = sandbox_meter::Receipt::build(
sandbox_schema::ContentId::of_bytes(prior_rev.as_bytes()),
sandbox_schema::Tier::S0,
vec![device],
1,
[3u8; 8],
)
.map_err(|e| HarnessError::Contract(e.to_string()))?;
let meter_envelope = meter_receipt
.seal(&self.steward, &self.meter_signing)
.map_err(|e| HarnessError::Contract(e.to_string()))?;
let receipt = self.receipt(
&contract,
&op,
attributed_uj,
attributed,
&meter_envelope,
&gates,
findings_before,
findings_after,
)?;
let verdict =
validate(&contract, &receipt).map_err(|e| HarnessError::Contract(e.to_string()))?;
let envelope = joulecontract_wire::emit(&receipt, &self.signing)
.map_err(|e| HarnessError::Contract(e.to_string()))?;
Ok(RunReport {
findings_before,
findings_after: if changed.is_empty() { None } else { Some(findings_after) },
changed_files: changed,
gates,
shipped,
branch,
attributed_uj,
soc_cpu_uj,
attributed,
wall,
meter_source,
contract,
receipt,
envelope_json: serde_json::to_string_pretty(&envelope)
.map_err(|e| HarnessError::Contract(e.to_string()))?,
meter_envelope,
local_claim: verdict.claim(),
formal,
})
}
/// Load and validate `<workspace>/.joule-fix/formal.json` (the formal tier's
/// input) **before any edit is applied**, so a malformed spec fails fast on
/// a clean tree rather than mid-run. `None` when no formal spec is present —
/// the default, leaving the run identical to before. Index/duplicate checks
/// mirror the proof bridge's so the later discharge cannot fail.
fn load_formal_spec(&self) -> Result<Option<FormalSpec>, HarnessError> {
let path = self.workspace.join(".joule-fix/formal.json");
let Ok(text) = std::fs::read_to_string(&path) else {
return Ok(None);
};
let spec: FormalSpec = serde_json::from_str(&text).map_err(|e| {
HarnessError::Contract(format!(
"formal.json: {e} — see formal.example.json for the exact serde forms \
(rigor_target JR-1..JR-4, enb_ref a 'sha2-256:<64-hex>' string, \
kind/grounding PascalCase)"
))
})?;
let n = spec.contract.obligations().len();
let mut seen = BTreeSet::new();
for f in &spec.formalizations {
if f.obligation >= n {
return Err(HarnessError::Contract(format!(
"formal.json: formalization names obligation {} but the contract declares {n}",
f.obligation
)));
}
if !seen.insert(f.obligation) {
return Err(HarnessError::Contract(format!(
"formal.json: duplicate formalization for obligation {}",
f.obligation
)));
}
}
Ok(Some(spec))
}
/// The formally-verified fix tier: discharge the loaded formalizations
/// through the proof cascade over a real `z3` — a **decision-procedure
/// ladder only** (`Propositional`, then `Z3Smt`): a formal gate must never
/// fall through to a model stand-in. Writes the full per-obligation
/// discharge to `.joule-fix/formal-discharge.json` as re-checkable evidence.
fn discharge_formal(&self, spec: &FormalSpec) -> FormalOutcome {
let router = Router::new(vec![Box::new(Propositional), Box::new(Z3Smt)]);
let mut cache = ProofCache::new();
// Indices were validated in `load_formal_spec`, so the only `Err` here
// would be a bug; fail the gate safely rather than ship an unproven fix.
let Ok(dis) = proof_discharge(&spec.contract, &spec.formalizations, &router, &mut cache)
else {
return FormalOutcome {
proved: 0,
refuted: 0,
unproved: spec.formalizations.len(),
proof_uj: 0,
all_proved: false,
entries: Vec::new(),
};
};
let (mut proved, mut refuted, mut unproved) = (0usize, 0usize, 0usize);
let mut entries = Vec::new();
for e in &dis.entries {
// Only obligations that were actually formalized belong to THIS
// gate; unformalized ones are the operational gates' business and
// the bridge records them separately (demoted, never dropped).
let (verdict, closed_by, counter_model, uj) = match &e.status {
DischargeStatus::Proved(r) => {
proved += 1;
("proved", r.closed_by.map(|t| format!("{t:?}")), None, r.total_uj)
}
DischargeStatus::Unproved(r) if r.refuted => {
refuted += 1;
("refuted", None, r.counter_model.clone(), r.total_uj)
}
DischargeStatus::Unproved(r) => {
unproved += 1;
("unproved", None, None, r.total_uj)
}
DischargeStatus::Unformalized => continue,
};
entries.push(FormalEntry {
obligation: e.obligation.text.clone(),
verdict: verdict.into(),
closed_by,
counter_model,
uj,
});
}
let supplied = proved + refuted + unproved;
// The gate: at least one formalization, and every one proved. Any
// refutation or unproven goal fails it (and reverts the fix).
let all_proved = supplied > 0 && refuted == 0 && unproved == 0;
let proof_uj = entries.iter().map(|e| e.uj).sum();
// Evidence an auditor can re-check independently of this report.
let evidence = self.workspace.join(".joule-fix/formal-discharge.json");
if let Some(dir) = evidence.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(j) = serde_json::to_string_pretty(&dis) {
let _ = std::fs::write(&evidence, j);
}
FormalOutcome { proved, refuted, unproved, proof_uj, all_proved, entries }
}
/// The current per-location findings (short-format diagnostic lines).
fn current_findings(&self) -> Result<Vec<String>, HarnessError> {
let (_, lint_out) = self.run(
"cargo",
&["clippy", "--workspace", "--message-format=short", "--quiet"],
)?;
Ok(lint_out
.lines()
.filter(|l| l.contains(".rs:") && (l.contains(": warning:") || l.contains(": error:")))
.map(str::to_string)
.collect())
}
/// The induced-skill tier: for each remaining finding whose message
/// matches a stored skill, apply the line transform deterministically.
/// Zero inference; the gates still judge the result like anyone else's.
fn skill_pass(&self) -> Result<usize, HarnessError> {
let mut store = SkillStore::load(&self.skill_store);
if store.skills.is_empty() {
return Ok(0);
}
let mut applied = 0usize;
for finding in self.current_findings()? {
let (Some(path), Some(key)) = (finding.split(':').next(), message_key(&finding)) else {
continue;
};
if !path.ends_with(".rs") {
continue;
}
let Some(skill) = store.skills.iter_mut().find(|s| s.message_key == key) else {
continue;
};
let full = self.workspace.join(path);
let Ok(content) = std::fs::read_to_string(&full) else { continue };
let mut lines: Vec<String> = content.lines().map(str::to_string).collect();
let mut hit = false;
for line in lines.iter_mut() {
if line.trim() == skill.old_line {
let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
*line = format!("{indent}{}", skill.new_line);
hit = true;
break;
}
}
if hit {
let mut out = lines.join("\n");
if content.ends_with('\n') {
out.push('\n');
}
if std::fs::write(&full, out).is_ok() {
skill.hits += 1;
applied += 1;
}
}
}
if applied > 0 {
store.save(&self.skill_store);
}
Ok(applied)
}
/// Induce skills from a SHIPPED stochastic fix. The rule is anchored to
/// the finding's OFFENDING LINE: when it vanished from the fixed file,
/// its replacement is the new line sharing the longest common prefix
/// (models reformat around the fix; the lesson is the line, not the
/// file). Only gate-verified lessons compile; a reverted fix teaches
/// nothing — and a badly-induced skill cannot ship damage either, since
/// every application faces the same gates.
fn induce_skills(&self, snapshot: &[(String, String, String, String)]) {
let mut store = SkillStore::load(&self.skill_store);
let mut induced = false;
for (path, key, offending, old_content) in snapshot {
if offending.is_empty() || store.skills.iter().any(|s| s.message_key == *key) {
continue;
}
let Ok(new_content) = std::fs::read_to_string(self.workspace.join(path)) else {
continue;
};
let new_trimmed: Vec<&str> =
new_content.lines().map(str::trim).filter(|l| !l.is_empty()).collect();
if new_trimmed.contains(&offending.as_str()) {
continue; // the offending line survived: nothing was learned
}
let old_set: BTreeSet<&str> = old_content.lines().map(str::trim).collect();
let common_prefix = |a: &str, b: &str| -> usize {
a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count()
};
let Some((best, plen)) = new_trimmed
.iter()
.filter(|l| !old_set.contains(**l))
.map(|l| (*l, common_prefix(l, offending)))
.max_by_key(|(_, p)| *p)
else {
continue;
};
// The replacement must visibly descend from the offending line.
if plen < 6.max(offending.len() / 4) {
continue;
}
store.skills.push(Skill {
message_key: key.clone(),
old_line: offending.clone(),
new_line: best.to_string(),
hits: 0,
});
induced = true;
}
if induced {
store.save(&self.skill_store);
}
}
/// One L3 consultation: prompt = the remaining findings plus the touched
/// files' contents; reply = SEARCH/REPLACE blocks, applied verbatim.
/// Returns true when the model changed anything. The GATES decide whether
/// any of it survives — this function trusts nothing it applies.
fn l3_pass(&self) -> Result<bool, HarnessError> {
use std::io::Write;
let cmd = self.l3_cmd.as_ref().expect("caller checked");
let all = self.current_findings()?;
let findings: Vec<&str> = all.iter().map(String::as_str).take(8).collect();
let mut files: BTreeSet<String> = BTreeSet::new();
for f in &findings {
if let Some(path) = f.split(':').next() {
if path.ends_with(".rs") && self.workspace.join(path).exists() {
files.insert(path.to_string());
}
}
}
let single_file = (files.len() == 1).then(|| files.iter().next().cloned().unwrap());
let mut prompt = if let Some(f) = &single_file {
// Whole-file replacement: the easier task for a small model, and
// the gates plus the diff guard bound the blast radius.
format!(
"Fix the Rust lint findings below by correcting the file. Reply with the COMPLETE corrected contents of {f} in a single ```rust code fence. No explanation.\n\nFindings:\n"
)
} else {
String::from(
"Fix the following Rust lint findings. Reply ONLY with one or more blocks of the form:\n <<<<<<< SEARCH <path>\n<exact text to find>\n=======\n<replacement>\n>>>>>>> REPLACE\n\nFindings:\n",
)
};
for f in &findings {
prompt.push_str(f);
prompt.push('\n');
}
for path in &files {
if let Ok(body) = std::fs::read_to_string(self.workspace.join(path)) {
prompt.push_str(&format!("\n--- {path} ---\n{body}"));
}
}
let mut child = Command::new("sh")
.args(["-c", cmd])
.current_dir(&self.workspace)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| HarnessError::Spawn(format!("L3 cmd: {e}")))?;
if let Some(stdin) = child.stdin.as_mut() {
let _ = stdin.write_all(prompt.as_bytes());
}
let out = child
.wait_with_output()
.map_err(|e| HarnessError::Spawn(format!("L3 cmd: {e}")))?;
let reply = String::from_utf8_lossy(&out.stdout).into_owned();
if self.apply_search_replace(&reply) {
return Ok(true);
}
if let Some(f) = &single_file {
return Ok(self.apply_fenced_file(&reply, f));
}
Ok(false)
}
/// Apply a whole-file reply: the first fenced code block becomes the
/// file's new contents. Trusts nothing — the gates judge the result.
fn apply_fenced_file(&self, reply: &str, path: &str) -> bool {
let Some(open) = reply.find("```") else { return false };
let after = &reply[open + 3..];
let Some(nl) = after.find('\n') else { return false };
let body = &after[nl + 1..];
let Some(close) = body.find("\n```") else { return false };
let new_content = &body[..close + 1];
if new_content.trim().is_empty() {
return false;
}
std::fs::write(self.workspace.join(path), new_content).is_ok()
}
/// Apply SEARCH/REPLACE blocks. Unparseable or non-matching blocks are
/// skipped — the model gets no error channel and no second chance; the
/// lint-delta gate will simply find its work insufficient.
fn apply_search_replace(&self, reply: &str) -> bool {
let mut applied = false;
let mut rest = reply;
while let Some(start) = rest.find("<<<<<<< SEARCH ") {
let block = &rest[start + "<<<<<<< SEARCH ".len()..];
let Some(nl) = block.find('\n') else { break };
let path = block[..nl].trim().to_string();
let body = &block[nl + 1..];
let (Some(sep), Some(end)) = (body.find("\n=======\n"), body.find("\n>>>>>>> REPLACE")) else {
break;
};
if sep < end {
let search = &body[..sep];
let replace = &body[sep + "\n=======\n".len()..end];
let full = self.workspace.join(&path);
// Path safety belongs to the frame gate, but directory
// traversal never even gets applied.
if !path.contains("..") && full.exists() {
if let Ok(content) = std::fs::read_to_string(&full) {
if content.contains(search) {
let new_content = content.replacen(search, replace, 1);
if std::fs::write(&full, new_content).is_ok() {
applied = true;
}
}
}
}
}
rest = &body[end..];
}
applied
}
/// The run's contract: the gates, stated as obligations.
fn contract(&self, frame: &[String], findings: usize) -> Contract {
Contract {
joulecontract: "0.8.0".into(),
subject: format!("L1 lint remediation of {findings} findings in {}", self.workspace.display()),
rigor_target: Rigor::Jr2,
pre: vec!["baseline captured: test failures and lint findings recorded before any edit".into()],
post: vec![
"build succeeds after the fix".into(),
"no test failures beyond the recorded baseline".into(),
"lint finding count strictly decreased".into(),
],
inv: vec![format!(
"total diff within {} changed lines",
self.max_diff_lines
)],
frame: frame.to_vec(),
ceiling: Ceiling {
enpi_joules: self.ceiling_joules,
unit: "EOC-J".into(),
enb_ref: ContentAddress::of_bytes(b"joule-fix L1 EnB v0: host cargo pipeline, no inference"),
derivation: None, // honestly unanchored until an energy office signs a basis
},
sector_equivalence: None,
amends: None,
}
}
/// Build the receipt: one discharge entry per obligation, result taken
/// from the matching gate's ACTUAL outcome.
#[allow(clippy::too_many_arguments)]
fn receipt(
&self,
contract: &Contract,
op: &CodingOp,
attributed_uj: u64,
attributed: bool,
meter_envelope: &[u8],
gates: &[GateOutcome],
before: usize,
after: usize,
) -> Result<Receipt, HarnessError> {
let gate_for = |kind: &ObligationKind, text: &str| -> &GateOutcome {
let name = match (kind, text) {
(ObligationKind::Pre, _) => "baseline",
(ObligationKind::Post, t) if t.starts_with("build") => "build",
(ObligationKind::Post, t) if t.starts_with("no test") => "tests-vs-baseline",
(ObligationKind::Post, _) => "lint-delta",
(ObligationKind::Inv, _) => "diff-guard",
(ObligationKind::Frame, _) => "frame",
};
gates.iter().find(|g| g.gate == name).expect("gate exists")
};
let harness_key = ContentAddress::of_bytes(self.signing.verifying_key().as_bytes());
let tier_addr = ContentAddress::of_bytes(b"joule-fix L1 lawful pipeline v0");
let qualification = Qualification {
attestation: ContentAddress::of_bytes(b"joule-fix gate suite v0: cargo build/test/clippy as deterministic verifiers"),
signer_key: harness_key.clone(),
valid_until: u64::MAX,
validated_at: 0,
root: Some(TrustRoot::ReferenceImpl),
};
let discharge: Vec<DischargeEntry> = contract
.obligations()
.into_iter()
.map(|ob| {
let gate = gate_for(&ob.kind, &ob.text);
let is_frame = ob.kind == ObligationKind::Frame;
DischargeEntry {
obligation: ob,
tier: tier_addr.clone(),
tier_class: TierClass::Deterministic,
tier_operator_keys: BTreeSet::from([harness_key.clone()]),
result: if gate.passed {
DischargeResult::Closed
} else {
DischargeResult::Undischarged {
reason: format!("{}: {}", gate.gate, gate.detail),
}
},
mode: is_frame.then_some(FrameMode::Observed),
qualification: Some(qualification.clone()),
verifier_keys: BTreeSet::from([harness_key.clone()]),
// Self-run harness: verifier == generator party, stated
// plainly. Independence computes false and the receipt
// grades accordingly — that's the honest local ceiling.
verifier_entity: None,
engagement: Some(Engagement {
selected_by: self.steward.clone(),
compensated_by: self.steward.clone(),
}),
recourse: None,
re_discharged_by: None,
}
})
.collect();
// The figure is an apportionment of a real SoC measurement: ModelBased,
// never Metered (a process is not a shunt). A ModelBased receipt is
// anchored to the sealed sandbox meter receipt that carries the
// measurement the apportionment rests on; without hardware it is
// honestly Unaccounted.
let provenance = if attributed {
EnergyClass::ModelBased
} else {
EnergyClass::Unaccounted
};
let energy_anchor =
attributed.then(|| ContentAddress::of_bytes(meter_envelope));
// No MeterBinding: that attests a Metered (direct-shunt) figure, which
// this is not. The energy_anchor is the ModelBased evidence path.
let meter: Option<MeterBinding> = None;
Ok(Receipt {
contract: contract
.content_address()
.map_err(|e| HarnessError::Contract(e.to_string()))?,
transition: joule_code_joulecontract::bridge_id(&op.prior),
generator: Generator {
system: format!("joule-fix v0 (L1 lawful: clippy --fix + rustfmt; {before}→{after} findings)"),
version_digest: ContentAddress::of_bytes(env!("CARGO_PKG_VERSION").as_bytes()),
attestation: None, // self-run: honestly generator_unattested
attestation_signer: None,
key_set: BTreeSet::from([harness_key.clone()]),
entity: None,
human_principals: vec![self.steward.clone()],
loop_governance: None,
},
discharge,
execution: vec![tier_addr],
joules_spent: attributed_uj as f64 / 1e6,
meter,
energy_anchor,
joules_allowed: contract.ceiling.enpi_joules,
provenance,
enb_ref: contract.ceiling.enb_ref.clone(),
grid_mix: None,
register_pin: ContentAddress::of_bytes(b"joule-fix register v0"),
evidence: None, // local receipt: evidence:external, stated not hidden
claimed_profile: Profile {
rigor: Rigor::Jr1, // self-verified: independence computes false
energy: provenance,
governance: GovernanceClass::Declared,
},
receipt_signer: harness_key,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sh(dir: &std::path::Path, cmd: &str) {
let ok = Command::new("sh")
.arg("-c")
.arg(cmd)
.current_dir(dir)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
assert!(ok, "setup command failed: {cmd}");
}
/// Build a scratch git crate with deterministic, clippy-fixable findings.
fn scratch_crate(label: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("joule-fix-test-{label}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"scratch\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[workspace]\n",
)
.unwrap();
std::fs::write(
dir.join("src/main.rs"),
r#"fn answer() -> u64 { return 42; }
fn main() { let x: u64 = answer(); println!("{}", x); }
#[test]
fn answers() { assert_eq!(answer(), 42); }
"#,
)
.unwrap();
sh(&dir, "git init -q && git add -A && git -c user.email=t@t -c user.name=t commit -qm init");
dir
}
#[test]
fn verified_fix_end_to_end_ships_with_receipt() {
let dir = scratch_crate("ship");
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
let report = h.run_once().unwrap();
assert!(report.findings_before > 0, "scratch crate has findings");
assert!(report.shipped, "gates: {:?}", report.gates);
assert!(report.gates.iter().all(|g| g.passed));
assert_eq!(report.findings_after, Some(0));
assert!(report.branch.is_some());
assert!(report.total_uj() > 0);
assert!(report.uj_per_verified_fix().is_some());
// The receipt validates locally and tells the truth about itself:
// self-verified ⇒ independence false ⇒ JR-1 ceiling, stated.
let v = validate(&report.contract, &report.receipt).unwrap();
assert!(v.permits(&report.receipt.claimed_profile), "claim: {}", v.claim());
assert!(report.envelope_json.contains("in-toto"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn l3_fix_passes_the_same_gates() {
// A finding clippy --fix can NOT auto-fix (approx_constant): L1
// leaves it, the configured L3 subprocess fixes it, the gates verify.
let dir = scratch_crate("l3ship");
std::fs::write(
dir.join("src/main.rs"),
"fn main() { let pi = 3.14; println!(\"{}\", pi); }\n",
)
.unwrap();
// The mock L3 fixer: a deterministic stand-in at the exact seam a
// jouleclaw decode binary plugs into.
let fixer = dir.join("mock-l3.sh");
std::fs::write(
&fixer,
"#!/bin/sh\ncat > /dev/null\nprintf '%s\\n' \
'<<<<<<< SEARCH src/main.rs' \
'fn main() { let pi = 3.14; println!(\"{}\", pi); }' \
'=======' \
'fn main() { let pi = std::f64::consts::PI; println!(\"{}\", pi); }' \
'>>>>>>> REPLACE'\n",
)
.unwrap();
sh(&dir, "chmod +x mock-l3.sh && git add -A && git -c user.email=t@t -c user.name=t commit -qm pi");
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
h.l3_cmd = Some("./mock-l3.sh".into());
let report = h.run_once().unwrap();
assert!(report.findings_before > 0);
assert!(report.shipped, "gates: {:?}", report.gates);
assert_eq!(report.findings_after, Some(0));
assert_eq!(report.receipt.discharge.len(), report.contract.obligations().len());
// The op records the stochastic tier; the receipt still validates.
assert!(report.local_claim.contains("JR-1"));
let src = std::fs::read_to_string(dir.join("src/main.rs")).unwrap();
assert!(src.contains("std::f64::consts::PI"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn shipped_l3_fix_induces_a_skill_that_closes_the_recurrence_without_a_model() {
let store = std::env::temp_dir().join(format!("joule-fix-skills-{}.json", std::process::id()));
let _ = std::fs::remove_file(&store);
// ---- First occurrence: the model (mock) fixes it; skill induced.
let a = scratch_crate("skill-a");
std::fs::write(
a.join("src/main.rs"),
"fn main() {\n let pi = 3.14;\n println!(\"{}\", pi);\n}\n",
)
.unwrap();
std::fs::write(
a.join("mock-l3.sh"),
"#!/bin/sh\ncat > /dev/null\nprintf '%s\\n' \
'```rust' \
'fn main() {' \
' let pi = std::f64::consts::PI;' \
' println!(\"{}\", pi);' \
'}' \
'```'\n",
)
.unwrap();
sh(&a, "chmod +x mock-l3.sh && git add -A && git -c user.email=t@t -c user.name=t commit -qm pi");
let mut h = Harness::new(&a).unwrap();
h.skill_store = store.clone();
h.l3_cmd = Some("./mock-l3.sh".into());
let first = h.run_once().unwrap();
assert!(first.shipped, "gates: {:?}", first.gates);
let stored = SkillStore::load(&store);
assert_eq!(stored.skills.len(), 1, "the verified fix must induce");
// ---- Recurrence, DIFFERENT crate, NO model configured at all: the
// induced skill closes it deterministically.
let b = scratch_crate("skill-b");
std::fs::write(
b.join("src/main.rs"),
"fn circumference(r: f64) -> f64 {\n let pi = 3.14;\n 2.0 * pi * r\n}\nfn main() { println!(\"{}\", circumference(1.0)); }\n",
)
.unwrap();
sh(&b, "git add -A && git -c user.email=t@t -c user.name=t commit -qm pi2");
let mut h2 = Harness::new(&b).unwrap();
h2.skill_store = store.clone();
h2.l3_cmd = None; // no model exists here
let second = h2.run_once().unwrap();
assert!(second.shipped, "gates: {:?}", second.gates);
assert_eq!(second.findings_after, Some(0));
assert!(!second.receipt.discharge[0].obligation.text.is_empty());
let src = std::fs::read_to_string(b.join("src/main.rs")).unwrap();
assert!(src.contains("std::f64::consts::PI"));
assert!(src.contains(" let pi"), "indentation preserved");
assert_eq!(SkillStore::load(&store).skills[0].hits, 1);
let _ = std::fs::remove_file(&store);
let _ = std::fs::remove_dir_all(&a);
let _ = std::fs::remove_dir_all(&b);
}
#[test]
fn l3_garbage_is_caught_by_the_gate_and_reverted() {
// The mock model emits a build-breaking "fix": the gate must catch
// it, revert it, and put the refusal on the record. This is the
// weak-model-made-safe property, tested.
let dir = scratch_crate("l3refuse");
std::fs::write(
dir.join("src/main.rs"),
"fn main() { let pi = 3.14; println!(\"{}\", pi); }\n",
)
.unwrap();
let fixer = dir.join("mock-l3.sh");
std::fs::write(
&fixer,
"#!/bin/sh\ncat > /dev/null\nprintf '%s\\n' \
'<<<<<<< SEARCH src/main.rs' \
'fn main() { let pi = 3.14; println!(\"{}\", pi); }' \
'=======' \
'fn main() { this is not rust at all }' \
'>>>>>>> REPLACE'\n",
)
.unwrap();
sh(&dir, "chmod +x mock-l3.sh && git add -A && git -c user.email=t@t -c user.name=t commit -qm pi");
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
h.l3_cmd = Some("./mock-l3.sh".into());
let report = h.run_once().unwrap();
assert!(!report.shipped, "broken fix must not ship");
let build_gate = report.gates.iter().find(|g| g.gate == "build").unwrap();
assert!(!build_gate.passed);
// Reverted: the original (lint and all) is back.
let src = std::fs::read_to_string(dir.join("src/main.rs")).unwrap();
assert!(src.contains("3.14"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn failed_gate_reverts_and_never_ships() {
let dir = scratch_crate("refuse");
// A test that pins the lint: fixing `return 42;` to `42` breaks it.
std::fs::write(
dir.join("src/main.rs"),
r#"fn answer() -> u64 { return 42; }
fn main() { let x: u64 = answer(); println!("{}", x); }
#[test]
fn pins_source_text() {
// The needle is assembled at runtime so the assert's own text doesn't
// satisfy it (include_str! includes this test too).
let src = include_str!("main.rs");
let needle = ["ret", "urn 42;"].concat();
assert!(src.contains(&needle), "fix would break this test");
}
"#,
)
.unwrap();
sh(&dir, "git add -A && git -c user.email=t@t -c user.name=t commit -qm pin");
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
let report = h.run_once().unwrap();
assert!(!report.shipped, "the pinned test must block shipping");
assert!(report.branch.is_none());
let tests_gate = report.gates.iter().find(|g| g.gate == "tests-vs-baseline").unwrap();
assert!(!tests_gate.passed);
// The work tree was reverted — the fix is gone.
let src = std::fs::read_to_string(dir.join("src/main.rs")).unwrap();
assert!(src.contains("return 42;"));
// And the refusal is on the record: an undischarged obligation.
assert!(report
.receipt
.discharge
.iter()
.any(|e| matches!(&e.result, DischargeResult::Undischarged { reason } if reason.contains("tests-vs-baseline"))));
let _ = std::fs::remove_dir_all(&dir);
}
/// A `.joule-fix/formal.json` over a netting postcondition (pre=0, post=1,
/// frame=2): the post is formalized as the SMT goal `net_def`; pre and frame
/// stay natural language (demoted, never dropped). `net_def` is the `net`
/// expression — `(- debits credits)` proves, `(+ debits credits)` refutes.
fn write_formal_spec(dir: &std::path::Path, net_def: &str) {
use proof_schema::{GoalKind, SpecGrounding};
let contract = Contract {
joulecontract: "joulecontract/0.6".into(),
subject: "scratch netting".into(),
rigor_target: Rigor::Jr2,
pre: vec!["debits and credits are integers".into()],
post: vec!["when debits == credits, the net is zero".into()],
inv: vec![],
frame: vec!["the ledger row".into()],
ceiling: Ceiling {
enpi_joules: 10.0,
unit: "joules/settlement".into(),
enb_ref: ContentAddress::of_bytes(b"enb"),
derivation: None,
},
sector_equivalence: None,
amends: None,
};
let statement = format!(
"(declare-const debits Int)(declare-const credits Int)(declare-const net Int)\
(assert (= net {net_def}))(assert (= debits credits))(assert (not (= net 0)))"
);
let f = Formalization {
obligation: 1,
statement,
kind: GoalKind::Decidable,
grounding: SpecGrounding::CrossChecked,
};
let spec = serde_json::json!({ "contract": contract, "formalizations": [f] });
std::fs::create_dir_all(dir.join(".joule-fix")).unwrap();
std::fs::write(dir.join(".joule-fix/formal.json"), spec.to_string()).unwrap();
}
#[test]
fn a_proven_formal_obligation_lets_the_fix_ship() {
if !proof_solvers::z3_available() {
return; // no solver: the formal gate can't run — covered below
}
let dir = scratch_crate("formal-ok");
write_formal_spec(&dir, "(- debits credits)"); // the correct postcondition
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
let report = h.run_once().unwrap();
let formal = report.formal.as_ref().expect("formal gate ran");
assert!(formal.all_proved, "entries: {:?}", formal.entries);
assert_eq!((formal.proved, formal.refuted, formal.unproved), (1, 0, 0));
assert!(formal.proof_uj > 0, "real z3 burned measured joules");
let g = report.gates.iter().find(|g| g.gate == "formal-proof").unwrap();
assert!(g.passed);
assert!(report.shipped, "all gates incl. formal green: {:?}", report.gates);
// the discharge evidence was written for an auditor
assert!(dir.join(".joule-fix/formal-discharge.json").exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_refuted_formal_obligation_reverts_the_fix() {
if !proof_solvers::z3_available() {
return;
}
let dir = scratch_crate("formal-refute");
write_formal_spec(&dir, "(+ debits credits)"); // the BUG: z3 finds a counter-model
let mut h = Harness::new(&dir).unwrap();
h.skill_store = dir.join("skills.json");
let report = h.run_once().unwrap();
let formal = report.formal.as_ref().expect("formal gate ran");
assert_eq!(formal.refuted, 1, "z3 refuted the false postcondition");
assert!(!formal.all_proved);
// the witness rode through from the cascade
assert!(formal.entries.iter().any(|e| e.verdict == "refuted"));
let g = report.gates.iter().find(|g| g.gate == "formal-proof").unwrap();
assert!(!g.passed);
// a refuted formal obligation reverts the fix exactly like a failed test
assert!(!report.shipped, "a refuted proof must block shipping");
let src = std::fs::read_to_string(dir.join("src/main.rs")).unwrap();
assert!(src.contains("return 42;"), "the operational fix was reverted");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_shipped_formal_template_parses_and_validates() {
// Guards `formal.example.json` against schema drift: the template we
// tell users to copy must always deserialize and index validly.
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/formal.example.json");
let text = std::fs::read_to_string(path).expect("template present");
let spec: FormalSpec = serde_json::from_str(&text).expect("template parses");
assert_eq!(spec.formalizations.len(), 1);
for f in &spec.formalizations {
assert!(
f.obligation < spec.contract.obligations().len(),
"template formalization indexes a real obligation"
);
}
}
}