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]>
507 lines
19 KiB
Rust
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());
|
|
}
|
|
}
|