The fix fleet proved the moat on fixes: a fix that ships through the gate at
the model tier is induced into a deterministic skill, and every recurrence
applies it model-free. This puts the same moat on REVIEW.
When the semantic (model) tier REJECTS an op and names the generalizable
cause, the cascade induces a ReviewSkill — a learned defect marker. It becomes
a new L0 "learned" tier, the cheapest of all: a known-marker match is an
instant model-free rejection. A recurring defect that first cost a model
decode (~5 J) is caught on recurrence for ~10 µJ — joules paid once, behind a
gate, never re-paid.
The asymmetry that keeps it honest: ONLY REJECTIONS AMORTIZE, NEVER APPROVALS.
A wrong learned rejection is a false request-for-changes an author overrides;
it cannot wave bad code through. A cached approval would be the silent-failure
lane the whole edifice closes, so every approval is paid fresh. The model
teaches the cheap tier to say no; never to say yes.
ReviewSkill/ReviewSkills are plain auditable JSON (.joule/review-skills.json),
deduped on (marker, stage). The semantic tier returns SemanticOutcome
{ reviews, induced } — inductions accompany a rejection only. Reviewer::reject
signs a learned-tier rejection (self-review still refused). Per-tier Estimator
energy: learned 10 µJ, structural 50 µJ, semantic 5 J. 9 tests; clippy clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
375 lines
15 KiB
Rust
375 lines
15 KiB
Rust
//! The **review cascade** with skill amortization: cheapest-sufficient review
|
||
//! that learns, the analog of the fix cascade and its induced skills.
|
||
//!
|
||
//! The fix fleet measured both halves of the moat: a deterministic L1 fix is
|
||
//! ~136 J, a local-model L3 fix is ~17,904 J (~132×), and a fix that ships
|
||
//! through the gate at the model tier is **induced into a deterministic skill**
|
||
//! so every recurrence applies it model-free — the model's joules paid once,
|
||
//! behind a gate, never re-paid. Review has the same shape, and this brings it
|
||
//! the same ladder *and* the same amortization.
|
||
//!
|
||
//! ## The ladder
|
||
//!
|
||
//! 1. **Learned** ([`ReviewTier::Learned`]) — known defect patterns the model
|
||
//! has already taught the cheap tier. A marker match is an instant,
|
||
//! near-zero-energy rejection: no structural scan, no model.
|
||
//! 2. **Structural** — the deterministic reviewer's built-in checks.
|
||
//! 3. **Semantic** — a model judgment, reached only when the cheaper tiers
|
||
//! find no defect.
|
||
//!
|
||
//! ## The asymmetry that keeps it honest
|
||
//!
|
||
//! **Only rejections amortize. Approvals never do.** When the semantic tier
|
||
//! *rejects* an op and names the generalizable cause, that becomes a learned
|
||
//! skill — a cheap structural check that catches the same defect next time.
|
||
//! The worst case of a wrong learned rejection is a false request-for-changes
|
||
//! an author overrides; it cannot wave bad code through. An *approval*, by
|
||
//! contrast, is never cached: a stale approval is exactly the silent-failure
|
||
//! lane the whole edifice exists to close, so every approval is paid fresh —
|
||
//! at the model tier, or as the shallow structural floor. The model teaches
|
||
//! the cheap tier to say *no*; it never teaches it to say *yes*.
|
||
|
||
use joule_code_cargo::SignedOp;
|
||
use joule_code_review::{ReviewStage, ReviewVerdict, SignedReview};
|
||
use joule_code_spec::SignedSpec;
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::{Reviewer, ReviewerError};
|
||
|
||
/// Which tier produced a review outcome.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum ReviewTier {
|
||
/// A learned defect pattern (induced from a prior model rejection). The
|
||
/// cheapest tier — a known-marker match.
|
||
Learned,
|
||
/// The deterministic, model-free structural reviewer.
|
||
Structural,
|
||
/// A model judgment of whether the code means what the clause requires.
|
||
Semantic,
|
||
}
|
||
|
||
impl ReviewTier {
|
||
/// A stable label.
|
||
pub fn label(self) -> &'static str {
|
||
match self {
|
||
ReviewTier::Learned => "learned",
|
||
ReviewTier::Structural => "structural",
|
||
ReviewTier::Semantic => "semantic",
|
||
}
|
||
}
|
||
|
||
/// An order-of-magnitude energy estimate, µJ, honest `Estimator` class —
|
||
/// a marker, not a measurement (this crate holds no meter). The ratios
|
||
/// mirror the measured fix ladder: a learned-marker match and a structural
|
||
/// scan are file-I/O class; a semantic judgment is a model decode.
|
||
pub fn estimate_uj(self) -> u64 {
|
||
match self {
|
||
ReviewTier::Learned => 10, // a known-marker match
|
||
ReviewTier::Structural => 50, // a byte scan
|
||
ReviewTier::Semantic => 5_000_000, // a model decode
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A learned review skill: a defect marker the model taught the cheap tier to
|
||
/// catch, with the finding to report and the stage it belongs to. Plain,
|
||
/// auditable data — read it, diff it, delete it.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub struct ReviewSkill {
|
||
/// A literal substring whose presence in an op's written bytes signals the
|
||
/// defect (e.g. `eval(`). The skill fires on any edit containing it.
|
||
pub marker: String,
|
||
/// The stage the rejection belongs to.
|
||
pub stage: ReviewStage,
|
||
/// The finding to report (the model's reason, generalized).
|
||
pub finding: String,
|
||
}
|
||
|
||
impl ReviewSkill {
|
||
/// Whether this skill fires on `op` — any edit's bytes contain the marker.
|
||
pub fn matches(&self, op: &SignedOp) -> bool {
|
||
op.op
|
||
.edits
|
||
.iter()
|
||
.any(|e| String::from_utf8_lossy(&e.replacement).contains(&self.marker))
|
||
}
|
||
}
|
||
|
||
/// The learned-skill set — persistable as plain JSON (`.joule/review-skills.json`).
|
||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||
pub struct ReviewSkills {
|
||
/// The induced reject-rules, applied newest-last.
|
||
pub skills: Vec<ReviewSkill>,
|
||
}
|
||
|
||
impl ReviewSkills {
|
||
/// The skills that fire on `op`.
|
||
pub fn matching(&self, op: &SignedOp) -> Vec<&ReviewSkill> {
|
||
self.skills.iter().filter(|s| s.matches(op)).collect()
|
||
}
|
||
|
||
/// Add a skill, de-duplicating on (marker, stage). Returns whether it was
|
||
/// new (a recurrence of a known marker teaches nothing further).
|
||
pub fn learn(&mut self, skill: ReviewSkill) -> bool {
|
||
if self.skills.iter().any(|s| s.marker == skill.marker && s.stage == skill.stage) {
|
||
return false;
|
||
}
|
||
self.skills.push(skill);
|
||
true
|
||
}
|
||
}
|
||
|
||
/// What a semantic reviewer returns: its signed reviews, plus any reject-rules
|
||
/// it identified for amortization. Inductions are offered **only** with a
|
||
/// rejection — there is no inducible signature of an approval.
|
||
pub struct SemanticOutcome {
|
||
/// The signed two-stage reviews.
|
||
pub reviews: Vec<SignedReview>,
|
||
/// Reject-rules the model identified as generalizable (empty on approval).
|
||
pub induced: Vec<ReviewSkill>,
|
||
}
|
||
|
||
/// A semantic (model) reviewer tier: a distinct principal that judges an op
|
||
/// with a model, signing reviews with its own key and naming the
|
||
/// generalizable cause of any rejection so the cheap tier can learn it.
|
||
pub trait SemanticReviewer {
|
||
/// Judge `op` against `spec` for the `claimed` clauses.
|
||
fn review(
|
||
&self,
|
||
op: &SignedOp,
|
||
spec: &SignedSpec,
|
||
claimed: &[String],
|
||
) -> Result<SemanticOutcome, ReviewerError>;
|
||
}
|
||
|
||
/// The outcome of cascading one op through the review ladder.
|
||
pub struct CascadeReview {
|
||
/// The signed reviews to record.
|
||
pub reviews: Vec<SignedReview>,
|
||
/// The tier that produced them.
|
||
pub tier: ReviewTier,
|
||
/// Whether the cheap tiers escalated to the semantic tier.
|
||
pub escalated: bool,
|
||
/// Estimated joules across the tiers consulted (Estimator class).
|
||
pub estimated_uj: u64,
|
||
/// Reject-rules the semantic tier produced this run — the caller persists
|
||
/// them with [`ReviewCascade::learn`] so recurrences are caught cheaply.
|
||
pub induced: Vec<ReviewSkill>,
|
||
}
|
||
|
||
/// A cheapest-sufficient, learning review ladder.
|
||
pub struct ReviewCascade {
|
||
structural: Reviewer,
|
||
semantic: Option<Box<dyn SemanticReviewer>>,
|
||
skills: ReviewSkills,
|
||
}
|
||
|
||
impl ReviewCascade {
|
||
/// A cascade with the structural floor and no learned skills yet.
|
||
pub fn new(structural: Reviewer) -> Self {
|
||
Self { structural, semantic: None, skills: ReviewSkills::default() }
|
||
}
|
||
|
||
/// Register the semantic tier — the only source of new learned skills.
|
||
pub fn with_semantic(mut self, semantic: Box<dyn SemanticReviewer>) -> Self {
|
||
self.semantic = Some(semantic);
|
||
self
|
||
}
|
||
|
||
/// Seed the learned skills (e.g. loaded from `.joule/review-skills.json`).
|
||
pub fn with_skills(mut self, skills: ReviewSkills) -> Self {
|
||
self.skills = skills;
|
||
self
|
||
}
|
||
|
||
/// The current learned skills, for persistence.
|
||
pub fn skills(&self) -> &ReviewSkills {
|
||
&self.skills
|
||
}
|
||
|
||
/// Fold an induced skill into the set (de-duplicated). Returns whether it
|
||
/// was new.
|
||
pub fn learn(&mut self, skill: ReviewSkill) -> bool {
|
||
self.skills.learn(skill)
|
||
}
|
||
|
||
/// Review `op` cheapest-first: learned skills, then structural, then —
|
||
/// only if both find no defect — the semantic tier. A learned or
|
||
/// structural rejection never consults the model.
|
||
pub fn review(
|
||
&self,
|
||
op: &SignedOp,
|
||
spec: &SignedSpec,
|
||
claimed: &[String],
|
||
) -> Result<CascadeReview, ReviewerError> {
|
||
// L0 — learned defects: an instant, model-free rejection.
|
||
let hits = self.skills.matching(op);
|
||
if !hits.is_empty() {
|
||
let mut reviews = Vec::new();
|
||
for stage in [ReviewStage::SpecCompliance, ReviewStage::Quality] {
|
||
let findings: Vec<String> =
|
||
hits.iter().filter(|s| s.stage == stage).map(|s| s.finding.clone()).collect();
|
||
if !findings.is_empty() {
|
||
reviews.push(self.structural.reject(op, spec, stage, findings)?);
|
||
}
|
||
}
|
||
return Ok(CascadeReview {
|
||
reviews,
|
||
tier: ReviewTier::Learned,
|
||
escalated: false,
|
||
estimated_uj: ReviewTier::Learned.estimate_uj(),
|
||
induced: vec![],
|
||
});
|
||
}
|
||
|
||
// L1 — structural.
|
||
let structural = self.structural.review(op, spec, claimed)?;
|
||
let rejected = structural.iter().any(|r| r.review.verdict == ReviewVerdict::RequestChanges);
|
||
if rejected || self.semantic.is_none() {
|
||
return Ok(CascadeReview {
|
||
reviews: structural,
|
||
tier: ReviewTier::Structural,
|
||
escalated: false,
|
||
estimated_uj: ReviewTier::Structural.estimate_uj(),
|
||
induced: vec![],
|
||
});
|
||
}
|
||
|
||
// L3 — semantic: the real approval (or a rejection that teaches).
|
||
let outcome = self.semantic.as_ref().unwrap().review(op, spec, claimed)?;
|
||
Ok(CascadeReview {
|
||
reviews: outcome.reviews,
|
||
tier: ReviewTier::Semantic,
|
||
escalated: true,
|
||
estimated_uj: ReviewTier::Structural.estimate_uj() + ReviewTier::Semantic.estimate_uj(),
|
||
induced: outcome.induced,
|
||
})
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use ed25519_dalek::SigningKey;
|
||
use joule_code_cargo::{Anchor, CodingOp, Edit};
|
||
use joule_code_review::{approval, Review, ReviewStage, ALG_ED25519 as RALG};
|
||
use joule_code_schema::{ContentId, Tier};
|
||
use joule_code_spec::{Clause, ClauseKind, Spec, ALG_ED25519 as SALG};
|
||
|
||
fn sk(s: u8) -> SigningKey {
|
||
SigningKey::from_bytes(&[s; 32])
|
||
}
|
||
|
||
fn spec(author: &SigningKey) -> SignedSpec {
|
||
Spec {
|
||
alg: SALG,
|
||
intent: "x".into(),
|
||
clauses: vec![Clause { id: "C1".into(), text: "persist".into(), kind: ClauseKind::Requirement }],
|
||
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(body.as_bytes()),
|
||
edits: vec![Edit { path: "src/s.rs".into(), anchor: Anchor::ByteRange { start: 0, end: 0 }, replacement: body.as_bytes().to_vec() }],
|
||
author: author.verifying_key().to_bytes(),
|
||
intent: "persist".into(),
|
||
tier: Tier::L3,
|
||
}
|
||
.sign(author)
|
||
.unwrap()
|
||
}
|
||
|
||
/// A test-double semantic reviewer: rejects ops containing `eval(` and
|
||
/// induces a skill for it; approves everything else. (Stands in for a
|
||
/// model judgment, deterministically, in-test.)
|
||
struct EvalRejectingSemantic(SigningKey);
|
||
impl SemanticReviewer for EvalRejectingSemantic {
|
||
fn review(&self, op: &SignedOp, spec: &SignedSpec, _claimed: &[String]) -> Result<SemanticOutcome, ReviewerError> {
|
||
let op_id = op.op.op_id()?;
|
||
let spec_id = spec.verify()?;
|
||
let bad = op.op.edits.iter().any(|e| String::from_utf8_lossy(&e.replacement).contains("eval("));
|
||
if bad {
|
||
let r = Review {
|
||
alg: RALG, op_id, stage: ReviewStage::Quality, spec_id: None,
|
||
verdict: ReviewVerdict::RequestChanges,
|
||
findings: vec!["uses eval() on untrusted input".into()],
|
||
round: 1, reviewer: self.0.verifying_key().to_bytes(),
|
||
}.sign(&self.0).unwrap();
|
||
Ok(SemanticOutcome {
|
||
reviews: vec![r],
|
||
induced: vec![ReviewSkill { marker: "eval(".into(), stage: ReviewStage::Quality, finding: "uses eval() on untrusted input (learned)".into() }],
|
||
})
|
||
} else {
|
||
let reviews = [ReviewStage::SpecCompliance, ReviewStage::Quality].into_iter().map(|stage| {
|
||
Review {
|
||
alg: RALG, op_id: op_id.clone(), stage,
|
||
spec_id: if stage == ReviewStage::SpecCompliance { Some(spec_id.clone()) } else { None },
|
||
verdict: ReviewVerdict::Approve, findings: vec![], round: 1,
|
||
reviewer: self.0.verifying_key().to_bytes(),
|
||
}.sign(&self.0).unwrap()
|
||
}).collect();
|
||
Ok(SemanticOutcome { reviews, induced: vec![] })
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_model_teaches_the_cheap_tier_a_defect_then_recurrences_are_caught_for_nanojoules() {
|
||
let author = sk(1);
|
||
let mut cascade = ReviewCascade::new(Reviewer::new(sk(2)))
|
||
.with_semantic(Box::new(EvalRejectingSemantic(sk(3))));
|
||
|
||
// first encounter: structural finds nothing wrong, escalates to the
|
||
// model, which rejects AND induces a skill.
|
||
let first = op(&author, "fn run() { eval(input); }");
|
||
let out = cascade.review(&first, &spec(&author), &["C1".into()]).unwrap();
|
||
assert_eq!(out.tier, ReviewTier::Semantic);
|
||
assert!(out.estimated_uj >= ReviewTier::Semantic.estimate_uj()); // paid the model
|
||
assert_eq!(out.induced.len(), 1);
|
||
assert!(!approval(&first, &out.reviews).is_approved());
|
||
|
||
// learn it (as a deployment would, persisting to review-skills.json)
|
||
for s in out.induced {
|
||
assert!(cascade.learn(s));
|
||
}
|
||
|
||
// recurrence (a different op, same defect): caught at the LEARNED tier,
|
||
// model never consulted, ~5 J → 10 µJ.
|
||
let again = op(&author, "fn other() { eval(x); }");
|
||
let out2 = cascade.review(&again, &spec(&author), &["C1".into()]).unwrap();
|
||
assert_eq!(out2.tier, ReviewTier::Learned);
|
||
assert!(!out2.escalated);
|
||
assert_eq!(out2.estimated_uj, ReviewTier::Learned.estimate_uj());
|
||
assert!(out2.estimated_uj * 100 < ReviewTier::Semantic.estimate_uj()); // orders cheaper
|
||
assert!(!approval(&again, &out2.reviews).is_approved());
|
||
}
|
||
|
||
#[test]
|
||
fn an_approval_is_never_learned() {
|
||
let author = sk(1);
|
||
let cascade = ReviewCascade::new(Reviewer::new(sk(2)))
|
||
.with_semantic(Box::new(EvalRejectingSemantic(sk(3))));
|
||
let clean = op(&author, "fn run() { write_to_disk(); }");
|
||
let out = cascade.review(&clean, &spec(&author), &["C1".into()]).unwrap();
|
||
assert_eq!(out.tier, ReviewTier::Semantic);
|
||
assert!(approval(&clean, &out.reviews).is_approved());
|
||
assert!(out.induced.is_empty()); // approvals induce nothing — never cached
|
||
}
|
||
|
||
#[test]
|
||
fn skills_persist_as_plain_json_and_dedupe() {
|
||
let mut skills = ReviewSkills::default();
|
||
let s = ReviewSkill { marker: "eval(".into(), stage: ReviewStage::Quality, finding: "f".into() };
|
||
assert!(skills.learn(s.clone()));
|
||
assert!(!skills.learn(s.clone())); // same marker+stage → not new
|
||
let json = serde_json::to_string(&skills).unwrap();
|
||
let back: ReviewSkills = serde_json::from_str(&json).unwrap();
|
||
assert_eq!(back.skills, skills.skills);
|
||
}
|
||
}
|