The real semantic judgment — does this op actually implement the clause? — by a model, behind a clean seam. LlmSemanticReviewer builds a review prompt from the clause + the op's diff, runs a model, parses the verdict, and signs two-stage reviews with its own key: a distinct principal the gate counts like any other, and it refuses to review its own ops. The model is a seam, not a dependency. `Generate` is "prompt in, text out" — a deployment plugs in whatever it runs (agent-zed already drives a GGUF model through jouleclaw-loader-gguf; wiring its l3_generate into Generate is the whole binding). Keeping the model behind the trait lets the real logic here — prompt construction, robust parsing, the fail-safe — be deterministic and fully tested, while the heavyweight non-deterministic inference stays where the model already lives (and out of a churning cross-repo dependency). FAIL-SAFE BY CONSTRUCTION: a model's output is untrusted text, so this reviewer never approves on ambiguity. Only an unambiguous VERDICT: APPROVE line approves; an unparseable verdict, a REQUEST_CHANGES, or a model/runtime error all send the op back with the reason as the finding. A confused model cannot wave code through — the same no-silent-lane stance as the rest of the system. On a rejection the model may name an INDUCE marker, which becomes a learned ReviewSkill (the amortization loop, now driven by a real model). Tests with a canned/dead model double: a clear APPROVE signs a two-stage approval; a REQUEST_CHANGES + INDUCE rejects and teaches; unparseable output and a dead backend both fail safe to rejection; and it drops into the cascade as the semantic tier whose rejection the learned tier then amortizes. 14 tests; clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
324 lines
13 KiB
Rust
324 lines
13 KiB
Rust
//! The **independent reviewer agent**: a separate principal that judges a
|
|
//! coding op against its spec and produces signed two-stage reviews.
|
|
//!
|
|
//! This is the other half of the review gate. The gate
|
|
//! (`joule-code-review`) checks that an op carries an independent two-stage
|
|
//! approval; this crate is something that *produces* one — a reviewer that is
|
|
//! a genuinely distinct principal (its own signing key) making a real,
|
|
//! falsifiable judgment, not a rubber stamp.
|
|
//!
|
|
//! Two invariants make it honest:
|
|
//!
|
|
//! - **Independence is enforced at the source.** [`Reviewer::review`] refuses
|
|
//! to review an op it authored (`op.author == reviewer key`) — a principal
|
|
//! cannot review its own work, whether that principal is a human or an
|
|
//! agent. Separation of duties is not left to the audit to catch; the
|
|
//! reviewer won't sign in the first place.
|
|
//! - **The judgment can fail.** A reviewer that always approves is a stamp,
|
|
//! not a reviewer. The structural checks here genuinely request changes —
|
|
//! an op that cites no clause, cites a *non-goal*, changes nothing, or ships
|
|
//! an unfinished marker (`todo!()`, `unimplemented!()`, `FIXME`) is sent
|
|
//! back with findings.
|
|
//!
|
|
//! This is the **structural** reviewer — the cheapest sufficient tier of
|
|
//! review: deterministic, reproducible, model-free. It checks the things a
|
|
//! machine can check for certain. A semantic reviewer (does the code actually
|
|
//! *mean* what the clause asks?) is the escalation tier above it — a model
|
|
//! judgment by another distinct principal — and composes the same way: it
|
|
//! signs reviews with its own key, and the gate counts them identically.
|
|
//!
|
|
//! Whoever the reviewer principal is — agent or human — its key is the trace.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use joule_code_cargo::SignedOp;
|
|
use joule_code_review::{Review, ReviewStage, ReviewVerdict, SignedReview, ALG_ED25519};
|
|
use joule_code_spec::{ClauseKind, SignedSpec};
|
|
|
|
pub mod cascade;
|
|
pub mod llm;
|
|
|
|
/// Errors a reviewer can raise (distinct from a *request-changes* verdict,
|
|
/// which is a normal review outcome, not an error).
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ReviewerError {
|
|
/// The reviewer was asked to review an op it authored — self-review is
|
|
/// refused at the source, regardless of the principal's species.
|
|
#[error("a reviewer must not review its own op (self-review)")]
|
|
SelfReview,
|
|
#[error("spec: {0}")]
|
|
Spec(#[from] joule_code_spec::SpecError),
|
|
#[error("review: {0}")]
|
|
Review(#[from] joule_code_review::ReviewError),
|
|
#[error("cargo: {0}")]
|
|
Cargo(#[from] joule_code_cargo::CargoError),
|
|
}
|
|
|
|
/// Unfinished-work markers a quality review refuses to pass.
|
|
const UNFINISHED_MARKERS: &[&str] = &["todo!(", "unimplemented!(", "unreachable!(", "FIXME", "XXX"];
|
|
|
|
/// The clause(s) an op claims, parsed from a leading `Cn:` tag in its intent
|
|
/// (`"C1: make todos persist"` → `["C1"]`) — the same convention the editor
|
|
/// uses. The reviewer checks the claim against the spec; it does not invent
|
|
/// one. An untagged intent claims nothing (`[]`), which spec-compliance then
|
|
/// sends back.
|
|
pub fn claimed_clauses(intent: &str) -> Vec<String> {
|
|
let Some((head, _)) = intent.split_once(':') else { return vec![] };
|
|
let id = head.trim();
|
|
if !id.is_empty() && id.len() <= 32 && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
|
|
vec![id.to_string()]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
|
|
/// An independent reviewer principal. Holds the signing key that *is* its
|
|
/// identity and the trace of everything it approves or rejects.
|
|
pub struct Reviewer {
|
|
key: SigningKey,
|
|
}
|
|
|
|
impl Reviewer {
|
|
/// A reviewer with the given signing identity. The caller is responsible
|
|
/// for this key being a *different* principal than the authors it reviews
|
|
/// (the reviewer also refuses self-review defensively).
|
|
pub fn new(key: SigningKey) -> Self {
|
|
Self { key }
|
|
}
|
|
|
|
/// This reviewer's principal — its verifying key.
|
|
pub fn principal(&self) -> [u8; 32] {
|
|
self.key.verifying_key().to_bytes()
|
|
}
|
|
|
|
/// Judge an op against its spec, producing signed two-stage reviews.
|
|
///
|
|
/// `claimed_clauses` are the clause ids the op claims to implement (the
|
|
/// citation, or the author's `Cn:` tag). The reviewer checks them against
|
|
/// the spec; it does not take the author's word for what the op does.
|
|
///
|
|
/// Returns one or two reviews: always a **spec-compliance** review, and —
|
|
/// only if that passes — a **quality** review (you do not quality-review
|
|
/// work that does not yet do what was asked). A reviewer that authored the
|
|
/// op refuses with [`ReviewerError::SelfReview`].
|
|
pub fn review(
|
|
&self,
|
|
op: &SignedOp,
|
|
spec: &SignedSpec,
|
|
claimed_clauses: &[String],
|
|
) -> Result<Vec<SignedReview>, ReviewerError> {
|
|
if op.op.author == self.principal() {
|
|
return Err(ReviewerError::SelfReview);
|
|
}
|
|
let op_id = op.op.op_id()?;
|
|
let spec_id = spec.verify()?;
|
|
|
|
let mut out = Vec::new();
|
|
let compliance = self.judge_compliance(op, spec, claimed_clauses);
|
|
let compliance_passed = compliance.0 == ReviewVerdict::Approve;
|
|
out.push(self.sign_review(
|
|
op_id.clone(),
|
|
ReviewStage::SpecCompliance,
|
|
Some(spec_id.clone()),
|
|
compliance,
|
|
)?);
|
|
|
|
if compliance_passed {
|
|
let quality = self.judge_quality(op);
|
|
out.push(self.sign_review(op_id, ReviewStage::Quality, None, quality)?);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// Spec-compliance: does the op implement a real requirement/constraint,
|
|
/// and nothing it shouldn't? Deterministic, and able to fail.
|
|
fn judge_compliance(
|
|
&self,
|
|
op: &SignedOp,
|
|
spec: &SignedSpec,
|
|
claimed: &[String],
|
|
) -> (ReviewVerdict, Vec<String>) {
|
|
let mut findings = Vec::new();
|
|
if claimed.is_empty() {
|
|
findings.push("op claims no spec clause; spec-compliance cannot be verified".into());
|
|
}
|
|
for id in claimed {
|
|
match spec.spec.clauses.iter().find(|c| &c.id == id) {
|
|
None => findings.push(format!("cites clause {id:?}, which the spec does not contain")),
|
|
Some(clause) if clause.kind == ClauseKind::NonGoal => {
|
|
findings.push(format!(
|
|
"cites non-goal {id:?} ({:?}) — implementing an out-of-scope item",
|
|
clause.text
|
|
));
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
if op.op.edits.is_empty() {
|
|
findings.push("op makes no edits — it implements nothing".into());
|
|
}
|
|
if findings.is_empty() {
|
|
(ReviewVerdict::Approve, vec![])
|
|
} else {
|
|
(ReviewVerdict::RequestChanges, findings)
|
|
}
|
|
}
|
|
|
|
/// Quality: is the change finished enough to ship? Scans the written bytes
|
|
/// for unfinished-work markers and trivial no-ops. Deterministic, and able
|
|
/// to fail.
|
|
fn judge_quality(&self, op: &SignedOp) -> (ReviewVerdict, Vec<String>) {
|
|
let mut findings = Vec::new();
|
|
let total_written: usize = op.op.edits.iter().map(|e| e.replacement.len()).sum();
|
|
if total_written == 0 {
|
|
findings.push("change writes no content (all replacements empty)".into());
|
|
}
|
|
for edit in &op.op.edits {
|
|
let text = String::from_utf8_lossy(&edit.replacement);
|
|
for marker in UNFINISHED_MARKERS {
|
|
if text.contains(marker) {
|
|
findings.push(format!("{}: ships an unfinished marker `{marker}`", edit.path));
|
|
}
|
|
}
|
|
}
|
|
if findings.is_empty() {
|
|
(ReviewVerdict::Approve, vec![])
|
|
} else {
|
|
(ReviewVerdict::RequestChanges, findings)
|
|
}
|
|
}
|
|
|
|
/// Sign a **request-changes** review at `stage` with `findings` — used to
|
|
/// emit a rejection from a learned review skill (a defect a model taught
|
|
/// the cheap tier to catch). Refuses self-review and requires non-empty
|
|
/// findings, like any rejection. `spec` supplies the id for a
|
|
/// spec-compliance stage.
|
|
pub fn reject(
|
|
&self,
|
|
op: &SignedOp,
|
|
spec: &SignedSpec,
|
|
stage: ReviewStage,
|
|
findings: Vec<String>,
|
|
) -> Result<SignedReview, ReviewerError> {
|
|
if op.op.author == self.principal() {
|
|
return Err(ReviewerError::SelfReview);
|
|
}
|
|
let spec_id = match stage {
|
|
ReviewStage::SpecCompliance => Some(spec.verify()?),
|
|
ReviewStage::Quality => None,
|
|
};
|
|
self.sign_review(op.op.op_id()?, stage, spec_id, (ReviewVerdict::RequestChanges, findings))
|
|
}
|
|
|
|
fn sign_review(
|
|
&self,
|
|
op_id: joule_code_schema::ContentId,
|
|
stage: ReviewStage,
|
|
spec_id: Option<joule_code_schema::ContentId>,
|
|
verdict: (ReviewVerdict, Vec<String>),
|
|
) -> Result<SignedReview, ReviewerError> {
|
|
Ok(Review {
|
|
alg: ALG_ED25519,
|
|
op_id,
|
|
stage,
|
|
spec_id,
|
|
verdict: verdict.0,
|
|
findings: verdict.1,
|
|
round: 1,
|
|
reviewer: self.principal(),
|
|
}
|
|
.sign(&self.key)?)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use joule_code_review::approval;
|
|
use joule_code_cargo::{Anchor, CodingOp, Edit};
|
|
use joule_code_schema::{ContentId, Tier};
|
|
use joule_code_spec::{Clause, Spec, ALG_ED25519 as SALG};
|
|
|
|
fn sk(s: u8) -> SigningKey {
|
|
SigningKey::from_bytes(&[s; 32])
|
|
}
|
|
|
|
fn spec(author: &SigningKey) -> SignedSpec {
|
|
Spec {
|
|
alg: SALG,
|
|
intent: "a persistent todo list".into(),
|
|
clauses: vec![
|
|
Clause { id: "C1".into(), text: "todos persist".into(), kind: ClauseKind::Requirement },
|
|
Clause { id: "NG1".into(), text: "no multiplayer".into(), kind: ClauseKind::NonGoal },
|
|
],
|
|
parent: None,
|
|
author: author.verifying_key().to_bytes(),
|
|
}
|
|
.sign(author)
|
|
.unwrap()
|
|
}
|
|
|
|
fn op(author: &SigningKey, body: &str) -> SignedOp {
|
|
CodingOp {
|
|
alg: 0x01,
|
|
prior: ContentId::of_bytes(b"t"),
|
|
edits: vec![Edit {
|
|
path: "src/store.rs".into(),
|
|
anchor: Anchor::ByteRange { start: 0, end: 0 },
|
|
replacement: body.as_bytes().to_vec(),
|
|
}],
|
|
author: author.verifying_key().to_bytes(),
|
|
intent: "persist todos".into(),
|
|
tier: Tier::L3,
|
|
}
|
|
.sign(author)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn a_good_op_gets_a_two_stage_approval_that_satisfies_the_gate() {
|
|
let author = sk(1);
|
|
let reviewer = Reviewer::new(sk(2));
|
|
let s = spec(&author);
|
|
let o = op(&author, "fn save() { write_to_disk(); }");
|
|
let reviews = reviewer.review(&o, &s, &["C1".into()]).unwrap();
|
|
assert_eq!(reviews.len(), 2); // both stages assessed
|
|
assert!(approval(&o, &reviews).is_approved());
|
|
}
|
|
|
|
#[test]
|
|
fn citing_a_non_goal_is_sent_back_at_spec_compliance() {
|
|
let author = sk(1);
|
|
let reviewer = Reviewer::new(sk(2));
|
|
let s = spec(&author);
|
|
let o = op(&author, "fn mp() {}");
|
|
let reviews = reviewer.review(&o, &s, &["NG1".into()]).unwrap();
|
|
assert_eq!(reviews.len(), 1); // quality not reached — compliance failed
|
|
assert_eq!(reviews[0].review.verdict, ReviewVerdict::RequestChanges);
|
|
assert!(reviews[0].review.findings[0].contains("non-goal"));
|
|
assert!(!approval(&o, &reviews).is_approved());
|
|
}
|
|
|
|
#[test]
|
|
fn an_unfinished_marker_fails_quality() {
|
|
let author = sk(1);
|
|
let reviewer = Reviewer::new(sk(2));
|
|
let s = spec(&author);
|
|
let o = op(&author, "fn save() { todo!() }");
|
|
let reviews = reviewer.review(&o, &s, &["C1".into()]).unwrap();
|
|
assert_eq!(reviews.len(), 2);
|
|
// compliance passed, quality requested changes
|
|
let quality = reviews.iter().find(|r| r.review.stage == ReviewStage::Quality).unwrap();
|
|
assert_eq!(quality.review.verdict, ReviewVerdict::RequestChanges);
|
|
assert!(quality.review.findings[0].contains("unfinished"));
|
|
assert!(!approval(&o, &reviews).is_approved());
|
|
}
|
|
|
|
#[test]
|
|
fn a_reviewer_refuses_to_review_its_own_op() {
|
|
let principal = sk(3);
|
|
let reviewer = Reviewer::new(principal.clone());
|
|
let s = spec(&sk(1));
|
|
let own = op(&principal, "fn f() {}"); // authored by the reviewer's principal
|
|
assert!(matches!(reviewer.review(&own, &s, &["C1".into()]), Err(ReviewerError::SelfReview)));
|
|
}
|
|
}
|