open-standards/joule-code/joule-code-rs/crates/joule-code-conformance/src/lib.rs
Transaction Science 7532f6a392 joule-code: repair jcp_schema::Grant literals — second cross-workspace break
joule-code does not compile against main. `jcp-schema`'s Grant gained
`budget_funds` (dual ceilings) and `caveats` (§4.9), and three Grant literals in
joule-code-jcp and joule-code-conformance were never updated, so the whole
workspace fails with E0063.

This is the SAME break already repaired in sandbox-jcp, in a second workspace I
missed. The cause is structural, not incidental: `jcp-schema` is path-depended on
from at least three OTHER cargo workspaces —

    sandbox/sandbox-rs  →  ../../../../jcp/jcp-rs/crates/jcp-schema
    joule-code/…        →  ../../../../jcp/jcp-rs/crates/jcp-schema
    jouledesk/…         →  ../../../../jcp/jcp-rs/crates/jcp-schema

— and none of them is in jcp's workspace, so `cargo test --workspace` inside
jcp/jcp-rs is green while three sibling workspaces are broken. Adding a required
field to a shared schema type silently breaks every consumer outside the
workspace that CI and local verification actually build.

Surveyed all four jcp-consuming workspaces rather than guessing: joule-code was
broken (2 errors), jouledesk imports the types but constructs no Grant literal,
sandbox was already repaired, jcp itself green. Only joule-code needed the fix.

Two of the three literals took the patch mechanically; the third used FIELD
SHORTHAND (`budget_uj,` with no colon) and escaped a `budget_uj:` pattern — the
same shorthand that escaped this same class of patch twice earlier. Worth
remembering: shorthand initializers are invisible to `field:`-shaped searches.

39 joule-code suites green; all four workspaces now compile.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 10:29:18 -04:00

585 lines
20 KiB
Rust

//! Joule Code conformance harness (SPEC §8).
//!
//! Loads the published wire vectors and runs the four suites:
//! C1 content addressing, C2 signing, C3 receipt, C4 cascade. Each vector is
//! reported PASS / FAIL / SKIP. SKIP marks a positive crypto vector whose
//! derived field is a placeholder pending regeneration against this library
//! (see `conformance/v1/regeneration.md`); the structural and negative checks,
//! the real Ed25519 known-answer vectors, and the entire cascade suite run for
//! real. Any FAIL makes the run non-zero (CI-ready).
use std::collections::BTreeMap;
use std::path::Path;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use joule_code_cargo::{Anchor, CargoError, CodingOp, Edit};
use joule_code_receipt::{Energy, Provenance, Receipt, ReceiptError};
use joule_code_cascade::Cascade;
use joule_code_schema::{ContentId, Tier};
use serde::Deserialize;
/// The outcome of a run over one or more suites.
#[derive(Default, Debug)]
pub struct Report {
pub pass: u32,
pub fail: u32,
pub skip: u32,
pub lines: Vec<String>,
}
impl Report {
fn pass(&mut self, name: &str) {
self.pass += 1;
self.lines.push(format!(" PASS {name}"));
}
fn fail(&mut self, name: &str, why: &str) {
self.fail += 1;
self.lines.push(format!(" FAIL {name}: {why}"));
}
fn skip(&mut self, name: &str, why: &str) {
self.skip += 1;
self.lines.push(format!(" SKIP {name}: {why}"));
}
/// Merge another report into this one.
pub fn absorb(&mut self, other: Report) {
self.pass += other.pass;
self.fail += other.fail;
self.skip += other.skip;
self.lines.extend(other.lines);
}
}
fn is_placeholder(s: &str) -> bool {
s.contains("PLACEHOLDER")
}
// ---------- JSON mirrors of the vector files ----------
#[derive(Deserialize)]
struct OpVector {
name: String,
op: OpJson,
expected_op_id: Option<String>,
error: Option<String>,
}
#[derive(Deserialize)]
struct OpJson {
alg: u8,
prior: String,
edits: Vec<EditJson>,
author: String,
intent: String,
tier: Tier,
}
#[derive(Deserialize)]
struct EditJson {
path: String,
anchor: AnchorJson,
replacement_hex: String,
}
#[derive(Deserialize)]
enum AnchorJson {
ByteRange { start: u64, end: u64 },
Hunk { hunk_id: String },
}
impl OpJson {
fn to_op(&self) -> Result<CodingOp, String> {
let mut author = [0u8; 32];
let bytes = hex::decode(&self.author).map_err(|e| e.to_string())?;
if bytes.len() != 32 {
return Err("author key must be 32 bytes".into());
}
author.copy_from_slice(&bytes);
let edits = self
.edits
.iter()
.map(|e| {
Ok(Edit {
path: e.path.clone(),
anchor: match &e.anchor {
AnchorJson::ByteRange { start, end } => Anchor::ByteRange {
start: *start,
end: *end,
},
AnchorJson::Hunk { hunk_id } => Anchor::Hunk {
hunk_id: ContentId(hunk_id.clone()),
},
},
replacement: hex::decode(&e.replacement_hex).map_err(|x| x.to_string())?,
})
})
.collect::<Result<Vec<_>, String>>()?;
Ok(CodingOp {
alg: self.alg,
prior: ContentId(self.prior.clone()),
edits,
author,
intent: self.intent.clone(),
tier: self.tier,
})
}
}
#[derive(Deserialize)]
struct SigVector {
name: String,
verifying_key: Option<String>,
message_hex: Option<String>,
signature: Option<String>,
/// Present for op-binding cases: the signature preimage is `op_id ‖ prior`.
op_id: Option<String>,
prior: Option<String>,
expected_verify: bool,
}
#[derive(Deserialize)]
struct ReceiptVector {
name: String,
receipt: ReceiptJson,
steward: Option<String>,
endpoint_verifying_key: Option<String>,
expected_envelope: Option<String>,
expected_verify: bool,
error: Option<String>,
}
#[derive(Deserialize)]
struct ReceiptJson {
op_id: String,
tier: Tier,
energy: EnergyJson,
sequence: u64,
nonce: String,
}
#[derive(Deserialize)]
struct EnergyJson {
measured_uj: Option<u64>,
estimated_uj: u64,
provenance: Provenance,
}
#[derive(Deserialize)]
struct CascadeVector {
name: String,
stages: Vec<Tier>,
stage_closes: BTreeMap<String, bool>,
expected_closing_tier: Option<Tier>,
}
fn parse_tier(s: &str) -> Option<Tier> {
match s {
"L0" => Some(Tier::L0),
"L1" => Some(Tier::L1),
"L2" => Some(Tier::L2),
"L3" => Some(Tier::L3),
"L4" => Some(Tier::L4),
_ => None,
}
}
// Map a CargoError to the stable error token used in the vectors.
fn cargo_error_token(e: &CargoError) -> &'static str {
match e {
CargoError::EditsOverlap => "edits_overlap",
CargoError::EditsMisordered => "edits_misordered",
CargoError::UnknownAlgorithm(_) => "unknown_algorithm",
CargoError::NoEdits => "no_edits",
_ => "other",
}
}
fn receipt_error_token(e: &ReceiptError) -> &'static str {
match e {
ReceiptError::HwShuntRequiresMeasured => "hwshunt_requires_measured",
ReceiptError::EstimatorMustNotCarryMeasured => "estimator_must_not_carry_measured",
_ => "other",
}
}
// ---------- the four suites ----------
/// C1 — content addressing. Recompute `op_id`; honor expected errors.
pub fn verify_coding_ops(json: &str) -> Result<Report, String> {
let vectors: Vec<OpVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
let mut r = Report::default();
for v in vectors {
let op = match v.op.to_op() {
Ok(op) => op,
Err(e) => {
r.fail(&v.name, &format!("malformed vector: {e}"));
continue;
}
};
match (op.op_id(), v.error.as_deref()) {
// Expected an error and got the right one.
(Err(e), Some(expected)) => {
if cargo_error_token(&e) == expected {
r.pass(&v.name);
} else {
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", cargo_error_token(&e)));
}
}
// Expected an error, got an id.
(Ok(_), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got an op_id")),
// Expected success.
(Ok(id), None) => match v.expected_op_id {
Some(expected) if is_placeholder(&expected) => {
r.skip(&v.name, &format!("op_id pending regen (computed {id})"));
}
Some(expected) => {
if id.as_str() == expected {
r.pass(&v.name);
} else {
r.fail(&v.name, &format!("op_id mismatch: got {id}, want {expected}"));
}
}
None => r.fail(&v.name, "no expected_op_id and no error"),
},
(Err(e), None) => r.fail(&v.name, &format!("unexpected error: {e}")),
}
}
Ok(r)
}
/// C2 — signing. Real Ed25519 known-answer cases run; op-binding cases whose
/// signature is a placeholder are skipped pending regeneration.
pub fn verify_signatures(json: &str) -> Result<Report, String> {
let vectors: Vec<SigVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
let mut r = Report::default();
for v in vectors {
let (Some(vk_hex), Some(sig_hex)) = (&v.verifying_key, &v.signature) else {
r.skip(&v.name, "no key/signature");
continue;
};
if is_placeholder(sig_hex) {
r.skip(&v.name, "signature pending regen");
continue;
}
let ok = (|| -> Result<bool, String> {
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
.map_err(|e| e.to_string())?
.try_into()
.map_err(|_| "vk len".to_string())?;
let sig_bytes: [u8; 64] = hex::decode(sig_hex)
.map_err(|e| e.to_string())?
.try_into()
.map_err(|_| "sig len".to_string())?;
// KAT cases verify a raw message; op-binding cases verify over the
// preimage `op_id ‖ prior` (joule-code-cargo's signing preimage).
let msg: Vec<u8> = if let Some(msg_hex) = &v.message_hex {
hex::decode(msg_hex).map_err(|e| e.to_string())?
} else if let (Some(op_id), Some(prior)) = (&v.op_id, &v.prior) {
let mut pre = op_id.as_bytes().to_vec();
pre.extend_from_slice(prior.as_bytes());
pre
} else {
return Err("vector has neither message_hex nor op_id+prior".into());
};
let vk = VerifyingKey::from_bytes(&vk_bytes).map_err(|e| e.to_string())?;
Ok(vk.verify(&msg, &Signature::from_bytes(&sig_bytes)).is_ok())
})();
match ok {
Ok(got) if got == v.expected_verify => r.pass(&v.name),
Ok(got) => r.fail(&v.name, &format!("verify={got}, expected {}", v.expected_verify)),
Err(e) => r.fail(&v.name, &e),
}
}
Ok(r)
}
/// C3 — receipt. The provenance↔measured coupling runs for real; positive
/// signature reproduction is skipped pending regeneration.
pub fn verify_receipts(json: &str) -> Result<Report, String> {
let vectors: Vec<ReceiptVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
let mut r = Report::default();
for v in vectors {
let energy = Energy {
measured_uj: v.receipt.energy.measured_uj,
estimated_uj: v.receipt.energy.estimated_uj,
provenance: v.receipt.energy.provenance,
};
match (energy.check_provenance(), v.error.as_deref()) {
(Err(e), Some(expected)) => {
if receipt_error_token(&e) == expected {
r.pass(&v.name);
} else {
r.fail(&v.name, &format!("wrong error: got {}, want {expected}", receipt_error_token(&e)));
}
}
(Ok(()), Some(expected)) => r.fail(&v.name, &format!("expected error {expected}, got valid")),
(Err(e), None) => r.fail(&v.name, &format!("unexpected provenance error: {e}")),
(Ok(()), None) => {
// Coupling holds; verify the JCR-1 COSE_Sign1 envelope for real.
let Some(env_hex) = v.expected_envelope.as_deref() else {
r.fail(&v.name, "positive vector missing expected_envelope");
continue;
};
if is_placeholder(env_hex) {
r.skip(&v.name, "coupling OK; envelope pending regen");
continue;
}
match verify_receipt_envelope(&v, energy, env_hex) {
Ok(ok) if ok == v.expected_verify => r.pass(&v.name),
Ok(ok) => r.fail(&v.name, &format!("verify={ok}, expected {}", v.expected_verify)),
Err(e) => r.fail(&v.name, &e),
}
}
}
}
Ok(r)
}
/// Reconstruct the [`Receipt`] from a vector and verify its JCR-1 COSE_Sign1
/// envelope against the endpoint key + steward (SPEC §6).
fn verify_receipt_envelope(v: &ReceiptVector, energy: Energy, env_hex: &str) -> Result<bool, String> {
let steward = v.steward.as_deref().ok_or("missing steward")?;
let vk_hex = v
.endpoint_verifying_key
.as_deref()
.ok_or("missing endpoint_verifying_key")?;
let vk_bytes: [u8; 32] = hex::decode(vk_hex)
.map_err(|e| e.to_string())?
.try_into()
.map_err(|_| "endpoint key len".to_string())?;
let envelope = hex::decode(env_hex).map_err(|e| e.to_string())?;
let nonce: [u8; 8] = hex::decode(&v.receipt.nonce)
.map_err(|e| e.to_string())?
.try_into()
.map_err(|_| "nonce len".to_string())?;
let receipt = Receipt {
op_id: ContentId(v.receipt.op_id.clone()),
tier: v.receipt.tier,
energy,
sequence: v.receipt.sequence,
nonce,
};
Ok(receipt.verify_envelope(&envelope, steward, &vk_bytes).is_ok())
}
/// C4 — cascade. Fully checkable: select the closing tier under
/// stop-at-first-close and compare to the vector.
pub fn verify_cascade(json: &str) -> Result<Report, String> {
let vectors: Vec<CascadeVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
let mut r = Report::default();
for v in vectors {
let closes = |t: Tier| -> bool {
v.stage_closes
.iter()
.any(|(k, &c)| parse_tier(k) == Some(t) && c)
};
let got = Cascade::select_closing_tier(&v.stages, closes);
if got == v.expected_closing_tier {
r.pass(&v.name);
} else {
r.fail(
&v.name,
&format!("closing tier got {got:?}, expected {:?}", v.expected_closing_tier),
);
}
}
Ok(r)
}
// ---------- C5: the JCP-gated cascade ----------
#[derive(Deserialize)]
struct GatedStageVector {
tier: String,
closes: bool,
}
#[derive(Deserialize)]
struct GatedExpect {
closing_tiers: Vec<Option<String>>,
spent_uj: u64,
verdicts: Vec<String>,
deny_reasons: Vec<String>,
}
#[derive(Deserialize)]
struct GatedVector {
name: String,
stages: Vec<GatedStageVector>,
granted_actions: Vec<String>,
budget_uj: u64,
runs: u32,
expected: GatedExpect,
}
/// Scripted stage for C5: closes (or not) at its tier.
struct Scripted {
tier: Tier,
closes: bool,
}
impl joule_code_cascade::Stage for Scripted {
fn tier(&self) -> Tier {
self.tier
}
fn try_resolve(
&self,
_req: &joule_code_cascade::Request,
) -> Option<joule_code_cascade::Resolution> {
if !self.closes {
return None;
}
Some(joule_code_cascade::Resolution {
op: None,
answer: Some(format!("closed at {:?}", self.tier)),
receipt: Receipt {
op_id: ContentId::of_bytes(b"c5-op"),
tier: self.tier,
energy: Energy {
measured_uj: None,
estimated_uj: joule_code_jcp::tier_cost_estimate(self.tier),
provenance: Provenance::Estimator,
},
sequence: 0,
nonce: [0; 8],
},
})
}
}
/// C5 — the JCP-gated cascade (SPEC §5.1). Runs the REAL jcp runtime: per
/// vector, issue a grant naming `granted_actions` with `budget_uj` over the
/// fixed fixture identities, wrap the scripted stages with the gate, run the
/// cascade `runs` times, and compare closing tiers, total debit, and the
/// sealed receipt verdicts (deny reasons included — a denial is evidence, not
/// a silent skip).
///
/// Fixture constants (third-party implementations must use the same):
/// issuer key = [1;32], agent key = [2;32], steward key = [7;32],
/// steward did = "did:example:joule-code",
/// resource = ContentId::of_bytes(b"repo:joule-code"),
/// request = { prior: "tree-hash", intent: "rename the function" },
/// per-tier costs = `tier_cost_estimate` (SPEC §1.1 class markers).
pub fn verify_gated_cascade(json: &str) -> Result<Report, String> {
use ed25519_dalek::SigningKey;
use jcp_schema::{Capability, Did, Grant};
let vectors: Vec<GatedVector> = serde_json::from_str(json).map_err(|e| e.to_string())?;
let mut r = Report::default();
let issuer = SigningKey::from_bytes(&[1; 32]);
let agent = SigningKey::from_bytes(&[2; 32]);
let steward_sk = SigningKey::from_bytes(&[7; 32]);
let steward = "did:example:joule-code";
let steward_vk = steward_sk.verifying_key().to_bytes();
let tree = jcp_schema::ContentId::of_bytes(b"repo:joule-code");
'vectors: for v in vectors {
let grant = Grant {
issuer: Did::from_verifying_key(&issuer.verifying_key()),
subject: Did::from_verifying_key(&agent.verifying_key()),
capability: Capability::new(tree, v.granted_actions.iter().map(String::as_str)),
budget_uj: v.budget_uj,
budget_funds: None,
caveats: Vec::new(),
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [0; 16],
};
let chain = match jcp_grant::issue(&issuer, grant) {
Ok(sg) => vec![sg],
Err(e) => {
r.fail(&v.name, &format!("grant issue: {e:?}"));
continue;
}
};
let gate = joule_code_jcp::JcpGate::new(
chain,
Did::from_verifying_key(&agent.verifying_key()),
steward_sk.clone(),
steward,
);
let mut stages: Vec<Box<dyn joule_code_cascade::Stage>> = Vec::new();
for s in &v.stages {
let Some(tier) = parse_tier(&s.tier.to_uppercase()) else {
r.fail(&v.name, &format!("bad tier {:?}", s.tier));
continue 'vectors;
};
stages.push(Box::new(gate.wrap(Scripted { tier, closes: s.closes })));
}
let cascade = Cascade::new(stages);
let req = joule_code_cascade::Request {
prior: "tree-hash".into(),
intent: "rename the function".into(),
};
let mut closings: Vec<Option<String>> = Vec::new();
for _ in 0..v.runs {
closings.push(
cascade
.resolve(&req)
.map(|res| format!("{:?}", res.receipt.tier).to_lowercase()),
);
}
let expected_closings: Vec<Option<String>> = v
.expected
.closing_tiers
.iter()
.map(|t| t.as_ref().map(|s| s.to_lowercase()))
.collect();
if closings != expected_closings {
r.fail(&v.name, &format!("closings got {closings:?}, want {expected_closings:?}"));
continue;
}
if gate.spent() != v.expected.spent_uj {
r.fail(&v.name, &format!("spent got {}, want {}", gate.spent(), v.expected.spent_uj));
continue;
}
let mut verdicts = Vec::new();
let mut deny_reasons = Vec::new();
for sealed in gate.receipts() {
match jcp_receipt::extract(&sealed, steward, &steward_vk) {
Ok(rec) => {
verdicts.push(rec.verdict.clone());
if let Some(reason) = rec.deny_reason {
deny_reasons.push(reason);
}
}
Err(e) => {
r.fail(&v.name, &format!("receipt extract: {e:?}"));
continue 'vectors;
}
}
}
if verdicts != v.expected.verdicts {
r.fail(&v.name, &format!("verdicts got {verdicts:?}, want {:?}", v.expected.verdicts));
continue;
}
if deny_reasons != v.expected.deny_reasons {
r.fail(&v.name, &format!("deny reasons got {deny_reasons:?}, want {:?}", v.expected.deny_reasons));
continue;
}
r.pass(&v.name);
}
Ok(r)
}
/// Verify a whole pack directory (the five suite files).
pub fn verify_pack(dir: &Path) -> Result<Report, String> {
let mut report = Report::default();
type Suite = (&'static str, fn(&str) -> Result<Report, String>);
let suites: &[Suite] = &[
("coding_op_vectors.json", verify_coding_ops),
("signature_vectors.json", verify_signatures),
("receipt_vectors.json", verify_receipts),
("cascade_vectors.json", verify_cascade),
("gated_cascade_vectors.json", verify_gated_cascade),
];
for (file, f) in suites {
let path = dir.join(file);
report.lines.push(format!("[{file}]"));
let json = std::fs::read_to_string(&path)
.map_err(|e| format!("{}: {e}", path.display()))?;
report.absorb(f(&json)?);
}
Ok(report)
}