//! 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, /// 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, /// 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 { 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, 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 { 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 { 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, } impl SignedSpec { /// Verify structure, content address, and signature. pub fn verify(&self) -> Result { 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, /// 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 { self.validate()?; Ok(ContentId::of_canonical(self)?) } /// Preimage: `citation_id ‖ op_id ‖ spec_id`. fn preimage(&self) -> Result, 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 { 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, } impl SignedCitation { /// Verify structure and signature. pub fn verify(&self) -> Result { 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 { 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)>, /// Requirement/constraint clause ids no op cites — open obligations. pub uncovered: Vec, /// 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, /// Ops in the citation set whose citations failed verification or named /// unknown clauses — never silently dropped. pub invalid: Vec, } 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 { let spec_id = spec.verify()?; let mut by_clause: std::collections::BTreeMap<&str, Vec> = 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()); } }