Day 4 — Ground Every Claim
Today Brandforge stops trusting a prompt instruction such as “do not invent facts.” Every promotional claim must carry a quote found in the frozen website snapshot.
Start in the correct directory
Section titled “Start in the correct directory”cd rust/rust-ai-engineering/learning/brandforgecargo checkToday’s edit map
Section titled “Today’s edit map”| Action | File | Purpose |
|---|---|---|
| REPLACE | src/profile.rs | Add claim status, audit records, normalization, and evidence tests |
| REPLACE | src/main.rs | Print the audit and fail when any claim is unsupported |
| Do not edit | src/model.rs | The scripted provider boundary stays unchanged |
After the edits, the filenames do not change:
src/├── main.rs # replace├── model.rs # unchanged└── profile.rs # replaceReplace src/profile.rs
Section titled “Replace src/profile.rs”This is a complete replacement, not a small addition. Select everything in src/profile.rs first.
REPLACE — src/profile.rs — complete file:
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]#[serde(deny_unknown_fields)]pub struct Palette { pub primary: String, pub background: String, pub accent: String,}
#[derive(Debug, Deserialize)]#[serde(deny_unknown_fields)]pub struct Claim { pub text: String, pub evidence_quote: String,}
#[derive(Debug, Deserialize)]#[serde(deny_unknown_fields)]pub struct BusinessProfile { pub name: String, pub website: String, pub tagline: String, pub service_area: String, pub services: Vec<String>, pub palette: Palette, pub claims: Vec<Claim>,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "snake_case")]pub enum ClaimStatus { Supported, Unsupported,}
#[derive(Debug, Serialize)]pub struct ClaimAudit { pub claim: String, pub evidence_quote: String, pub status: ClaimStatus,}
impl BusinessProfile { pub fn validate(&self) -> Result<(), String> { require_text("name", &self.name)?; require_text("website", &self.website)?; require_text("tagline", &self.tagline)?; require_text("service_area", &self.service_area)?;
if self.services.is_empty() { return Err("services must contain at least one item".to_owned()); } for service in &self.services { require_text("services[]", service)?; }
for (name, color) in [ ("palette.primary", &self.palette.primary), ("palette.background", &self.palette.background), ("palette.accent", &self.palette.accent), ] { if !is_hex_color(color) { return Err(format!( "{name} must be a six-digit hex color, got `{color}`" )); } }
for claim in &self.claims { require_text("claims[].text", &claim.text)?; require_text("claims[].evidence_quote", &claim.evidence_quote)?; }
Ok(()) }
pub fn audit_claims(&self, source: &str) -> Vec<ClaimAudit> { let normalized_source = normalize(source);
self.claims .iter() .map(|claim| { let quote = normalize(&claim.evidence_quote); let status = if !quote.is_empty() && normalized_source.contains("e) { ClaimStatus::Supported } else { ClaimStatus::Unsupported };
ClaimAudit { claim: claim.text.clone(), evidence_quote: claim.evidence_quote.clone(), status, } }) .collect() }}
fn require_text(field: &str, value: &str) -> Result<(), String> { if value.trim().is_empty() { return Err(format!("{field} must not be empty")); } Ok(())}
fn is_hex_color(value: &str) -> bool { value.len() == 7 && value.starts_with('#') && value.bytes().skip(1).all(|byte| byte.is_ascii_hexdigit())}
fn normalize(value: &str) -> String { value .split_whitespace() .collect::<Vec<_>>() .join(" ") .to_lowercase()}
#[cfg(test)]mod tests { use super::*;
fn profile() -> BusinessProfile { BusinessProfile { name: "Northstar Gardens".to_owned(), website: "https://northstar.example".to_owned(), tagline: "Outdoor spaces built for real life".to_owned(), service_area: "Toronto and the GTA".to_owned(), services: vec!["Landscape design".to_owned()], palette: Palette { primary: "#174D2A".to_owned(), background: "#F7F4EA".to_owned(), accent: "#E4A11B".to_owned(), }, claims: vec![Claim { text: "Serving Toronto and the GTA".to_owned(), evidence_quote: "Serving Toronto and the GTA".to_owned(), }], } }
#[test] fn evidence_matching_ignores_case_and_whitespace() { let audits = profile().audit_claims("We are SERVING TORONTO and the GTA today."); assert_eq!(audits[0].status, ClaimStatus::Supported); }
#[test] fn unsupported_metrics_do_not_pass() { let mut value = profile(); value.claims[0] = Claim { text: "98% client satisfaction".to_owned(), evidence_quote: "98% client satisfaction".to_owned(), };
let audits = value.audit_claims("Beautiful landscapes for Toronto."); assert_eq!(audits[0].status, ClaimStatus::Unsupported); }}Replace src/main.rs
Section titled “Replace src/main.rs”REPLACE — src/main.rs — complete file:
mod model;mod profile;
use std::path::PathBuf;use std::process::ExitCode;
use clap::Parser;use model::{ProfileModel, ScriptedProfileModel};use profile::ClaimStatus;
#[derive(Debug, Parser)]#[command(name = "brandforge", version, about)]struct Cli { #[arg(long, value_name = "FILE")] source: PathBuf,
#[arg(long, value_name = "FILE")] profile: PathBuf,}
fn main() -> ExitCode { let cli = Cli::parse();
let source_bytes = match std::fs::read(&cli.source) { Ok(bytes) => bytes, Err(error) => { eprintln!("could not read `{}`: {error}", cli.source.display()); return ExitCode::FAILURE; } }; let source = String::from_utf8_lossy(&source_bytes);
let response_bytes = match std::fs::read(&cli.profile) { Ok(bytes) => bytes, Err(error) => { eprintln!("could not read `{}`: {error}", cli.profile.display()); return ExitCode::FAILURE; } };
let model = ScriptedProfileModel::new(response_bytes); let profile = match model.extract(&source) { Ok(profile) => profile, Err(error) => { eprintln!("profile model failed: {error}"); return ExitCode::FAILURE; } };
let audits = profile.audit_claims(&source); println!( "{}", serde_json::to_string_pretty(&audits).expect("claim audits always serialize") );
let unsupported = audits .iter() .filter(|audit| audit.status == ClaimStatus::Unsupported) .count();
if unsupported > 0 { eprintln!("campaign contains {unsupported} unsupported claim(s)"); return ExitCode::FAILURE; }
ExitCode::SUCCESS}Check the supported profile
Section titled “Check the supported profile”cargo fmtcargo testcargo run -- \ --source fixtures/site.md \ --profile fixtures/business-profile.jsonBoth audit records should contain:
"status": "supported"Check the dangerous profile
Section titled “Check the dangerous profile”cargo run -- \ --source fixtures/site.md \ --profile fixtures/business-profile-unsupported.jsonecho $?You should see the audit record with "status": "unsupported", followed by a nonzero exit code. The
model proposed a plausible metric, but the source did not support it.
Understand the evidence rule
Section titled “Understand the evidence rule”The baseline rule normalizes whitespace and case, then checks whether the exact evidence quote occurs in the frozen source. This is strict and limited:
- it can reject a valid paraphrase;
- it cannot tell whether a quote is current;
- it cannot tell whether the quote came from a complaint;
- it does stop a number from passing merely because the model says it saw one.
“Grounded” must always name the actual verification rule.
End-of-day checkpoint
Section titled “End-of-day checkpoint”Run:
cargo testcargo run -- \ --source fixtures/site.md \ --profile fixtures/business-profile.jsonTomorrow you will switch from the small learning checkpoint to the final product-shaped layout. The page will name every file that is created or replaced before showing the final build command.