Skip to content

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.

Terminal window
cd rust/rust-ai-engineering/learning/brandforge
cargo check
ActionFilePurpose
REPLACEsrc/profile.rsAdd claim status, audit records, normalization, and evidence tests
REPLACEsrc/main.rsPrint the audit and fail when any claim is unsupported
Do not editsrc/model.rsThe scripted provider boundary stays unchanged

After the edits, the filenames do not change:

src/
├── main.rs # replace
├── model.rs # unchanged
└── profile.rs # replace

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(&quote) {
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 — 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
}
Terminal window
cargo fmt
cargo test
cargo run -- \
--source fixtures/site.md \
--profile fixtures/business-profile.json

Both audit records should contain:

"status": "supported"
Terminal window
cargo run -- \
--source fixtures/site.md \
--profile fixtures/business-profile-unsupported.json
echo $?

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.

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.

Run:

Terminal window
cargo test
cargo run -- \
--source fixtures/site.md \
--profile fixtures/business-profile.json

Tomorrow 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.

Next: Day 5 — Artifacts and Tests →.