Day 1 — CLI and Owned Input
Today you edit exactly one file. Brandforge will accept a website snapshot path, read its bytes, and print a short preview. No profile and no model call yet.
If learning/brandforge does not exist, complete
Before Day 1 — Make Your Working Copy first.
Start in the correct directory
Section titled “Start in the correct directory”Run:
cd rust/rust-ai-engineering/learning/brandforgepwdThe last part of the printed path must be:
rust/rust-ai-engineering/learning/brandforgeToday’s edit map
Section titled “Today’s edit map”| Action | File | Purpose |
|---|---|---|
| REPLACE | src/main.rs | Parse --source, read the file, and print a borrowed text view |
| Do not edit | Cargo.toml | Dependencies are already present |
| Do not edit | fixtures/site.md | This is today’s frozen input |
Your tree remains:
learning/brandforge/├── Cargo.toml├── fixtures/│ └── site.md└── src/ └── main.rs # the only file you edit todayReplace src/main.rs
Section titled “Replace src/main.rs”Open rust/rust-ai-engineering/learning/brandforge/src/main.rs in your editor. Select everything in
that file and replace it with the complete block below.
REPLACE — src/main.rs — complete file:
use std::path::PathBuf;use std::process::ExitCode;
use clap::Parser;
/// Read a frozen website snapshot before any AI work begins.#[derive(Debug, Parser)]#[command(name = "brandforge", version, about)]struct Cli { /// Markdown file containing the captured website text. #[arg(long, value_name = "FILE")] source: 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);
println!("source: {}", cli.source.display()); println!("bytes: {}", source_bytes.len()); println!("first non-empty lines:");
for line in source.lines().filter(|line| !line.trim().is_empty()).take(5) { println!(" {line}"); }
ExitCode::SUCCESS}Save the file, then run the fast checks:
cargo fmtcargo checkIf the compiler reports a filename other than src/main.rs, first confirm that your terminal is in
learning/brandforge.
Run the program
Section titled “Run the program”cargo run -- --source fixtures/site.mdThe exact byte count may change if you intentionally edit the fixture. The important output is:
source: fixtures/site.mdbytes: ...first non-empty lines: # Northstar Gardens Outdoor spaces built for real life.The first -- separates Cargo’s options from Brandforge’s options. --source fixtures/site.md is
passed to your program.
What each part means
Section titled “What each part means”PathBuf is a filesystem path
Section titled “PathBuf is a filesystem path”source: PathBuf,The user supplies text in the terminal, but Brandforge stores it as a path type. Filesystem paths are operating-system values and are not guaranteed to be valid UTF-8 strings.
One value owns the file bytes
Section titled “One value owns the file bytes”let source_bytes = std::fs::read(&cli.source);After the match succeeds, source_bytes is a Vec<u8>. That vector owns the allocated bytes. It is
responsible for keeping them alive and freeing them when main ends.
The text view borrows when it can
Section titled “The text view borrows when it can”let source = String::from_utf8_lossy(&source_bytes);source is a Cow<'_, str>. When the bytes are valid UTF-8, it borrows the existing allocation. It
allocates a repaired string only when invalid bytes must be replaced. Calling source.lines() then
produces borrowed &str slices into that text.
source_bytes: Vec<u8> owns the allocation │ ├── source: Cow<str> borrows valid UTF-8 │ └── line: &str borrows one range └── len() reads the vector without moving itRust prevents a borrowed line from outliving the bytes it points into. You do not write a manual
free, and you cannot accidentally keep a dangling text pointer.
Break it deliberately
Section titled “Break it deliberately”Run with a missing path:
cargo run -- --source fixtures/missing.mdecho $?Expected behavior:
- the error names
fixtures/missing.md; - the process does not print a source preview;
- the exit code is nonzero.
Also run without --source:
cargo run --Clap should print a usage error before your file-reading code runs.
End-of-day checkpoint
Section titled “End-of-day checkpoint”You are done when all three commands behave as described:
cargo checkcargo run -- --source fixtures/site.mdcargo run -- --source fixtures/missing.mdOnly src/main.rs changed today. Tomorrow you will create src/profile.rs and then replace
src/main.rs so the program can decode typed model output.
Check your understanding
Section titled “Check your understanding”- Which file did you edit today?
- Why is
PathBufused for--sourceinstead ofString? - Which value owns the website bytes?
- What does each value returned by
source.lines()borrow from? - Why does the missing-file path return
ExitCode::FAILURE?
Next: Day 2 — Typed Model Output →.