Day 1 — CLI and Owned Input
Today you build the smallest honest slice: a user gives Brandforge a source file and a profile file, and the program proves it can locate and read both. No model call yet. AI systems become easier to debug when you can separate the evidence supplied to the model from the model’s response.
Run before reading
Section titled “Run before reading”From rust/rust-ai-engineering:
cargo run -p brandforge -- --helpcargo run -p brandforge -- audit \ --source fixtures/brandforge/site.md \ --profile fixtures/brandforge/business-profile.jsonThe first command proves the CLI shape. The second reads frozen input and prints a claim audit. By the end of today, concentrate on the path from shell arguments to owned file bytes.
Scaffold the binary
Section titled “Scaffold the binary”The companion already exists. To recreate the learning sequence in a scratch directory:
cargo new brandforge --bincd brandforgecargo add clap --features derivecargo add serde --features derivecargo add serde_json thiserror sha2cargo checkUse PathBuf, not String, for filesystem paths. A path is an operating-system value and is not
guaranteed to be valid UTF-8.
#[derive(Debug, clap::Parser)]struct Cli { #[command(subcommand)] command: Command,}
#[derive(Debug, clap::Subcommand)]enum Command { Build { #[arg(long)] source: std::path::PathBuf, #[arg(long)] profile: std::path::PathBuf, #[arg(long, default_value = "target/brandforge")] output: std::path::PathBuf, },}Clap turns command-line text into a typed enum. Once parsing succeeds, Build always has three paths;
there is no later if command == "build" string guessing.
Read once, borrow many times
Section titled “Read once, borrow many times”The website snapshot enters as owned bytes:
let source_bytes: Vec<u8> = std::fs::read(&source_path)?;let source = String::from_utf8_lossy(&source_bytes);source_bytes owns the allocation. source is a Cow<'_, str>: it borrows the existing bytes when
they are valid UTF-8 and allocates a repaired string only when necessary. Later validators receive
&str views; they do not need their own copies.
This is the ownership question in product form:
pipeline owns Vec<u8> ├── UTF-8 view borrows bytes ├── claim matcher borrows text └── SHA-256 function borrows bytesMoving Vec<u8> would move its pointer/length/capacity descriptor. Cloning it would duplicate the
whole website snapshot. Rust makes that expensive choice visible as .clone().
Freeze the evidence
Section titled “Freeze the evidence”Do not let every test scrape a live site. Pages change, networks fail, bot defenses respond
differently, and a passing run becomes impossible to reproduce. Save a small Markdown snapshot under
fixtures/brandforge/site.md. The optional acquisition command may refresh it later; every audit must
name the exact bytes it used.
Try changing one sentence in the fixture and rerun the finished build. The source_sha256 in
run-report.json changes. That identity is how you distinguish a model regression from changed input.
Break it deliberately
Section titled “Break it deliberately”cargo run -p brandforge -- audit \ --source fixtures/brandforge/missing.md \ --profile fixtures/brandforge/business-profile.jsonecho $?You should see a path-specific error and a nonzero exit status. A CLI that prints an error but exits successfully lies to shell scripts and CI.
What you built
Section titled “What you built”You now have a typed command boundary and one owned snapshot that downstream stages can borrow. You
learned Cargo, modules, PathBuf, Vec<u8>, String, borrowing, and exit codes because a real input
pipeline required them.
Check your understanding
Section titled “Check your understanding”- Why is
PathBufmore honest thanStringfor--source? - What owns the website bytes, and which values merely borrow them?
- Why does a live scrape make a poor required test fixture?
- What automation bug appears if a failed build exits with status zero?
Next: Day 2 — Typed Model Output →.