open-standards/joule-code/joule-code-rs/crates/joule-code-spec/src/lib.rs
Transaction Science 31093b2680 family: the methodology layer, made auditable — spec, review, vericoding
The field converged on spec-driven development with two-stage review
(Superpowers, spec-kit, Amplifier), but every artifact in that consensus is
prompt-enforced testimony. These three crates make the methodology
checkable, in the family's house style (canonical det-CBOR, content
addresses, Ed25519 over id ‖ context preimages):

- joule-code-spec: the Spec artifact — intent + individually addressable
  clauses (requirements, constraints, and first-class NON-GOALS), signed
  and content-addressed, revisions chained by parent. A signed Citation
  binds a coding op to the clauses it claims to implement (the op stays
  wire-identical; traceability is additive, like the receipt). coverage()
  is the deterministic audit: open obligations, cited non-goals (a flag,
  not a pass), and invalid citations — recorded, never silently dropped.
  Completes the chain spec → clause → op → receipt.

- joule-code-review: two-stage review (spec-compliance, then quality) as
  signed rounds. Separation of duties is ENFORCED, not advised: a review
  whose reviewer key equals the op author's key fails verification —
  self-approval is structurally void. RequestChanges without findings is
  malformed; approval() audits the latest verified round per stage, with
  rejected rounds kept on the record (the cost of convergence is part of
  the artifact).

- joulecontract-proof: vericoding for the contract — each obligation with
  an explicit Formalization (statement + grounding tag; this crate NEVER
  invents one) becomes a ProofGoal citing the contract's content address
  and runs the JouleProof cascade. Per-obligation ledger: Proved (full
  CascadeResult, cache flywheel included) / Unproved (attempts + joules on
  record) / Unformalized (demoted, never dropped). discharged() is true
  only when every obligation proved — a contract is not "mostly"
  discharged.

16 new tests across the three crates; clippy clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-10 17:45:47 -04:00

507 lines
19 KiB
Rust

//! The **Spec** artifact: spec-driven development made auditable.
//!
//! The field converged on spec-driven development — the spec, not the code, is
//! the canonical artifact, and agents derive plans and implementations from
//! it. What no methodology layer provides is *proof of traceability*: nothing
//! binds the code that shipped to the spec that was approved, so "we followed
//! the spec" is testimony, not evidence.
//!
//! This crate closes that gap with three artifacts in the family's house
//! style (canonical det-CBOR, BLAKE3 content addresses, Ed25519 over
//! `id ‖ context` preimages):
//!
//! - a [`Spec`]: an intent plus ordered, individually addressable **clauses**
//! (requirements, constraints, and — first-class — non-goals), signed by its
//! author and content-addressed, with revisions chained by `parent` so spec
//! evolution is itself tamper-evident history;
//! - a [`Citation`]: a signed binding from one coding operation
//! (`joule-code-cargo::CodingOp`, by `op_id`) to the clauses it claims to
//! implement — the op cites the spec the way a receipt cites the op;
//! - a [`Coverage`] report: the deterministic audit over a spec and a set of
//! citations — which clauses are implemented, which are still open, which
//! ops claim no clause at all, and which citations name clauses the spec
//! does not contain (a *mis-citation* is never silently dropped).
//!
//! Together with the existing chain (op → receipt → attested build), this
//! completes: **spec → clause → op → receipt** — every joule and every edit
//! traceable to an approved, signed requirement, and every requirement
//! checkable for whether anything implements it.
//!
//! Nothing here invokes a model. Brainstorming produces the spec text
//! upstream however the deployment likes; this crate is the part an auditor
//! can hold.
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use joule_code_cargo::SignedOp;
use joule_code_schema::ContentId;
use serde::{Deserialize, Serialize};
/// Signature algorithm id for Ed25519 (matches `joule-code-schema`).
pub const ALG_ED25519: u8 = 0x01;
/// Errors from spec construction, signing, or verification.
#[derive(Debug, thiserror::Error)]
pub enum SpecError {
#[error("spec must contain at least one clause")]
Empty,
#[error("clause ids must be unique: duplicate {0:?}")]
DuplicateClause(String),
#[error("clause id must be non-empty and contain no whitespace: {0:?}")]
BadClauseId(String),
#[error("clause text must be non-empty: {0:?}")]
EmptyClause(String),
#[error("intent must be non-empty")]
EmptyIntent,
#[error("unsupported algorithm id {0:#04x}")]
BadAlg(u8),
#[error("signature verification failed")]
BadSignature,
#[error("citation must name at least one clause")]
EmptyCitation,
#[error("schema: {0}")]
Schema(#[from] joule_code_schema::SchemaError),
#[error("cargo: {0}")]
Cargo(#[from] joule_code_cargo::CargoError),
}
/// What kind of statement a clause is. Non-goals are first-class: writing
/// down what the system must *not* do is half the value of a brainstorm, and
/// an auditor checks them the same way.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClauseKind {
/// Behavior the implementation must exhibit.
Requirement,
/// A bound the implementation must stay inside (style, dependency,
/// energy, interface).
Constraint,
/// Behavior deliberately out of scope. An op citing a non-goal is a
/// design conversation, not an implementation.
NonGoal,
}
/// One individually addressable statement of the spec.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Clause {
/// Stable id within the spec (`"C1"`, `"auth-2"`, …). Unique, no
/// whitespace — it appears verbatim in citations and audit output.
pub id: String,
/// The testable statement.
pub text: String,
/// Requirement, constraint, or non-goal.
pub kind: ClauseKind,
}
/// The canonical spec artifact (unsigned form).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Spec {
/// Signature algorithm id (`0x01` = Ed25519).
pub alg: u8,
/// The distilled intent — *why* this exists, in the author's words.
pub intent: String,
/// Ordered, non-empty clauses with unique ids.
pub clauses: Vec<Clause>,
/// The spec this one revises, if any — revisions chain by content
/// address, so the history of what was approved is tamper-evident.
pub parent: Option<ContentId>,
/// The author's Ed25519 verifying key.
pub author: [u8; 32],
}
impl Spec {
/// Structural validation: non-empty intent, at least one clause, unique
/// well-formed clause ids, known algorithm.
pub fn validate(&self) -> Result<(), SpecError> {
if self.alg != ALG_ED25519 {
return Err(SpecError::BadAlg(self.alg));
}
if self.intent.trim().is_empty() {
return Err(SpecError::EmptyIntent);
}
if self.clauses.is_empty() {
return Err(SpecError::Empty);
}
let mut seen = std::collections::BTreeSet::new();
for clause in &self.clauses {
if clause.id.is_empty() || clause.id.chars().any(char::is_whitespace) {
return Err(SpecError::BadClauseId(clause.id.clone()));
}
if clause.text.trim().is_empty() {
return Err(SpecError::EmptyClause(clause.id.clone()));
}
if !seen.insert(clause.id.as_str()) {
return Err(SpecError::DuplicateClause(clause.id.clone()));
}
}
Ok(())
}
/// The spec id: content address of the canonical form. Validates first —
/// a non-canonical spec has no id.
pub fn spec_id(&self) -> Result<ContentId, SpecError> {
self.validate()?;
Ok(ContentId::of_canonical(self)?)
}
/// The signature preimage: `spec_id ‖ parent` (the empty string when this
/// is a root spec) — signing binds the revision to its lineage, the same
/// shape as the op's `op_id ‖ prior`.
fn preimage(&self) -> Result<Vec<u8>, SpecError> {
let spec_id = self.spec_id()?;
let mut pre = spec_id.as_str().as_bytes().to_vec();
if let Some(parent) = &self.parent {
pre.extend_from_slice(parent.as_str().as_bytes());
}
Ok(pre)
}
/// Sign with the author's key, producing a [`SignedSpec`].
pub fn sign(self, key: &SigningKey) -> Result<SignedSpec, SpecError> {
let sig = key.sign(&self.preimage()?);
Ok(SignedSpec { spec: self, sig: sig.to_bytes().to_vec() })
}
/// Begin a revision of this spec: same intent and clauses (edit them
/// before signing), `parent` set to this spec's id, author replaced by
/// the reviser.
pub fn revise(&self, author: [u8; 32]) -> Result<Spec, SpecError> {
Ok(Spec {
alg: self.alg,
intent: self.intent.clone(),
clauses: self.clauses.clone(),
parent: Some(self.spec_id()?),
author,
})
}
}
/// A spec plus its author's signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedSpec {
pub spec: Spec,
/// Ed25519 signature (64 bytes) over `spec_id ‖ parent`.
pub sig: Vec<u8>,
}
impl SignedSpec {
/// Verify structure, content address, and signature.
pub fn verify(&self) -> Result<ContentId, SpecError> {
let pre = self.spec.preimage()?;
let key =
VerifyingKey::from_bytes(&self.spec.author).map_err(|_| SpecError::BadSignature)?;
let sig_bytes: [u8; 64] =
self.sig.as_slice().try_into().map_err(|_| SpecError::BadSignature)?;
key.verify(&pre, &Signature::from_bytes(&sig_bytes))
.map_err(|_| SpecError::BadSignature)?;
self.spec.spec_id()
}
}
/// A signed binding from one coding operation to the spec clauses it claims
/// to implement. The op stays wire-identical (`joule-code-cargo` is
/// untouched); traceability is an additive artifact, the way the receipt is.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Citation {
/// Signature algorithm id (`0x01` = Ed25519).
pub alg: u8,
/// The op making the claim.
pub op_id: ContentId,
/// The spec being cited.
pub spec_id: ContentId,
/// The clause ids the op claims to implement. Non-empty.
pub clauses: Vec<String>,
/// The citing author's Ed25519 verifying key (normally the op's author).
pub author: [u8; 32],
}
impl Citation {
/// Structural validation.
pub fn validate(&self) -> Result<(), SpecError> {
if self.alg != ALG_ED25519 {
return Err(SpecError::BadAlg(self.alg));
}
if self.clauses.is_empty() {
return Err(SpecError::EmptyCitation);
}
Ok(())
}
/// The citation id: content address of the canonical form.
pub fn citation_id(&self) -> Result<ContentId, SpecError> {
self.validate()?;
Ok(ContentId::of_canonical(self)?)
}
/// Preimage: `citation_id ‖ op_id ‖ spec_id`.
fn preimage(&self) -> Result<Vec<u8>, SpecError> {
let id = self.citation_id()?;
let mut pre = id.as_str().as_bytes().to_vec();
pre.extend_from_slice(self.op_id.as_str().as_bytes());
pre.extend_from_slice(self.spec_id.as_str().as_bytes());
Ok(pre)
}
/// Sign, producing a [`SignedCitation`].
pub fn sign(self, key: &SigningKey) -> Result<SignedCitation, SpecError> {
let sig = key.sign(&self.preimage()?);
Ok(SignedCitation { citation: self, sig: sig.to_bytes().to_vec() })
}
}
/// A citation plus its signature.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedCitation {
pub citation: Citation,
/// Ed25519 signature (64 bytes) over `citation_id ‖ op_id ‖ spec_id`.
pub sig: Vec<u8>,
}
impl SignedCitation {
/// Verify structure and signature.
pub fn verify(&self) -> Result<ContentId, SpecError> {
let pre = self.citation.preimage()?;
let key = VerifyingKey::from_bytes(&self.citation.author)
.map_err(|_| SpecError::BadSignature)?;
let sig_bytes: [u8; 64] =
self.sig.as_slice().try_into().map_err(|_| SpecError::BadSignature)?;
key.verify(&pre, &Signature::from_bytes(&sig_bytes))
.map_err(|_| SpecError::BadSignature)?;
self.citation.citation_id()
}
}
/// Cite a signed op against a signed spec: verifies both artifacts, checks
/// every named clause exists in the spec, and returns the signed citation.
/// This is the convenience path; the artifacts compose manually too.
pub fn cite(
op: &SignedOp,
spec: &SignedSpec,
clauses: &[&str],
key: &SigningKey,
) -> Result<SignedCitation, SpecError> {
let spec_id = spec.verify()?;
let op_id = op.op.op_id()?;
let known: std::collections::BTreeSet<&str> =
spec.spec.clauses.iter().map(|c| c.id.as_str()).collect();
for clause in clauses {
if !known.contains(clause) {
return Err(SpecError::BadClauseId((*clause).to_string()));
}
}
Citation {
alg: ALG_ED25519,
op_id,
spec_id,
clauses: clauses.iter().map(|c| (*c).to_string()).collect(),
author: key.verifying_key().to_bytes(),
}
.sign(key)
}
/// The deterministic spec-coverage audit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coverage {
/// Clause ids with at least one valid citation, with the citing op ids.
pub covered: Vec<(String, Vec<ContentId>)>,
/// Requirement/constraint clause ids no op cites — open obligations.
pub uncovered: Vec<String>,
/// Non-goal clause ids that ARE cited — each one a flag, not a pass:
/// an op claims to implement something the spec says not to build.
pub non_goals_cited: Vec<String>,
/// Ops in the citation set whose citations failed verification or named
/// unknown clauses — never silently dropped.
pub invalid: Vec<ContentId>,
}
impl Coverage {
/// A spec is satisfied when every requirement and constraint is covered,
/// no non-goal is cited, and no citation is invalid.
pub fn is_complete(&self) -> bool {
self.uncovered.is_empty() && self.non_goals_cited.is_empty() && self.invalid.is_empty()
}
}
/// Audit a spec against a set of citations. Citations for other specs are
/// counted invalid (they cannot witness this spec); so are citations that
/// fail signature verification or name clauses the spec lacks.
pub fn coverage(spec: &SignedSpec, citations: &[SignedCitation]) -> Result<Coverage, SpecError> {
let spec_id = spec.verify()?;
let mut by_clause: std::collections::BTreeMap<&str, Vec<ContentId>> =
spec.spec.clauses.iter().map(|c| (c.id.as_str(), Vec::new())).collect();
let mut invalid = Vec::new();
for signed in citations {
let ok = signed.verify().is_ok()
&& signed.citation.spec_id == spec_id
&& signed
.citation
.clauses
.iter()
.all(|c| by_clause.contains_key(c.as_str()));
if !ok {
invalid.push(signed.citation.op_id.clone());
continue;
}
for clause in &signed.citation.clauses {
by_clause
.get_mut(clause.as_str())
.expect("validated above")
.push(signed.citation.op_id.clone());
}
}
let mut covered = Vec::new();
let mut uncovered = Vec::new();
let mut non_goals_cited = Vec::new();
for clause in &spec.spec.clauses {
let ops = &by_clause[clause.id.as_str()];
match clause.kind {
ClauseKind::NonGoal => {
if !ops.is_empty() {
non_goals_cited.push(clause.id.clone());
}
}
ClauseKind::Requirement | ClauseKind::Constraint => {
if ops.is_empty() {
uncovered.push(clause.id.clone());
} else {
covered.push((clause.id.clone(), ops.clone()));
}
}
}
}
Ok(Coverage { covered, uncovered, non_goals_cited, invalid })
}
#[cfg(test)]
mod tests {
use super::*;
use joule_code_cargo::{Anchor, CodingOp, Edit};
use joule_code_schema::Tier;
fn sk(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn spec_fixture(author: &SigningKey) -> SignedSpec {
Spec {
alg: ALG_ED25519,
intent: "a minesweeper with persistent state".into(),
clauses: vec![
Clause {
id: "C1".into(),
text: "board state survives a page reload".into(),
kind: ClauseKind::Requirement,
},
Clause {
id: "C2".into(),
text: "no network calls".into(),
kind: ClauseKind::Constraint,
},
Clause {
id: "NG1".into(),
text: "no multiplayer".into(),
kind: ClauseKind::NonGoal,
},
],
parent: None,
author: author.verifying_key().to_bytes(),
}
.sign(author)
.expect("valid spec signs")
}
fn op_fixture(author: &SigningKey, marker: u8) -> SignedOp {
CodingOp {
alg: 0x01,
prior: ContentId::of_bytes(&[marker]),
edits: vec![Edit {
path: "src/store.rs".into(),
anchor: Anchor::ByteRange { start: 0, end: 0 },
replacement: vec![marker],
}],
author: author.verifying_key().to_bytes(),
intent: "persist board state".into(),
tier: Tier::L3,
}
.sign(author)
.expect("valid op signs")
}
#[test]
fn a_spec_signs_verifies_and_is_tamper_evident() {
let author = sk(1);
let signed = spec_fixture(&author);
let id = signed.verify().expect("verifies");
// flip one clause character → different artifact, dead signature
let mut tampered = signed.clone();
tampered.spec.clauses[0].text = "board state survives a page reload!".into();
assert!(tampered.verify().is_err());
assert_ne!(tampered.spec.spec_id().unwrap(), id);
}
#[test]
fn revisions_chain_by_content_address() {
let author = sk(1);
let v1 = spec_fixture(&author);
let v1_id = v1.verify().unwrap();
let reviser = sk(2);
let mut v2 = v1.spec.revise(reviser.verifying_key().to_bytes()).unwrap();
v2.clauses.push(Clause {
id: "C3".into(),
text: "dark mode".into(),
kind: ClauseKind::Requirement,
});
let v2 = v2.sign(&reviser).unwrap();
assert_eq!(v2.spec.parent.as_ref(), Some(&v1_id));
v2.verify().expect("revision verifies");
}
#[test]
fn citing_an_unknown_clause_is_refused() {
let author = sk(1);
let spec = spec_fixture(&author);
let op = op_fixture(&author, 7);
let err = cite(&op, &spec, &["C99"], &author).unwrap_err();
assert!(matches!(err, SpecError::BadClauseId(_)));
}
#[test]
fn coverage_finds_open_obligations_and_flags_non_goals() {
let author = sk(1);
let spec = spec_fixture(&author);
let op1 = op_fixture(&author, 1);
let c1 = cite(&op1, &spec, &["C1"], &author).unwrap();
let report = coverage(&spec, std::slice::from_ref(&c1)).unwrap();
assert_eq!(report.covered.len(), 1); // C1 witnessed by op1
assert_eq!(report.uncovered, vec!["C2".to_string()]); // C2 still open
assert!(!report.is_complete());
// an op citing the non-goal is flagged, not passed
let op2 = op_fixture(&author, 2);
let ng = cite(&op2, &spec, &["NG1"], &author).unwrap();
let report = coverage(&spec, &[c1.clone(), ng]).unwrap();
assert_eq!(report.non_goals_cited, vec!["NG1".to_string()]);
assert!(!report.is_complete());
// covering C2 (and dropping the non-goal op) completes the spec
let op3 = op_fixture(&author, 3);
let c2 = cite(&op3, &spec, &["C2"], &author).unwrap();
let report = coverage(&spec, &[c1, c2]).unwrap();
assert!(report.is_complete());
}
#[test]
fn a_citation_against_a_different_spec_is_invalid_not_dropped() {
let author = sk(1);
let spec_a = spec_fixture(&author);
let mut other = spec_a.spec.clone();
other.intent = "a different product".into();
let spec_b = other.sign(&author).unwrap();
let op = op_fixture(&author, 9);
let citation_b = cite(&op, &spec_b, &["C1"], &author).unwrap();
let report = coverage(&spec_a, &[citation_b]).unwrap();
assert_eq!(report.invalid.len(), 1); // recorded, never silently dropped
assert!(!report.is_complete());
}
}