Skip to content

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.

From rust/rust-ai-engineering:

Terminal window
cargo run -p brandforge -- --help
cargo run -p brandforge -- audit \
--source fixtures/brandforge/site.md \
--profile fixtures/brandforge/business-profile.json

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

The companion already exists. To recreate the learning sequence in a scratch directory:

Terminal window
cargo new brandforge --bin
cd brandforge
cargo add clap --features derive
cargo add serde --features derive
cargo add serde_json thiserror sha2
cargo check

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

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 bytes

Moving 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().

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.

Terminal window
cargo run -p brandforge -- audit \
--source fixtures/brandforge/missing.md \
--profile fixtures/brandforge/business-profile.json
echo $?

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.

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.

  1. Why is PathBuf more honest than String for --source?
  2. What owns the website bytes, and which values merely borrow them?
  3. Why does a live scrape make a poor required test fixture?
  4. What automation bug appears if a failed build exits with status zero?

Next: Day 2 — Typed Model Output →.