Skip to content

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.

Run:

Terminal window
cd rust/rust-ai-engineering/learning/brandforge
pwd

The last part of the printed path must be:

rust/rust-ai-engineering/learning/brandforge
ActionFilePurpose
REPLACEsrc/main.rsParse --source, read the file, and print a borrowed text view
Do not editCargo.tomlDependencies are already present
Do not editfixtures/site.mdThis is today’s frozen input

Your tree remains:

learning/brandforge/
├── Cargo.toml
├── fixtures/
│ └── site.md
└── src/
└── main.rs # the only file you edit today

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:

Terminal window
cargo fmt
cargo check

If the compiler reports a filename other than src/main.rs, first confirm that your terminal is in learning/brandforge.

Terminal window
cargo run -- --source fixtures/site.md

The exact byte count may change if you intentionally edit the fixture. The important output is:

source: fixtures/site.md
bytes: ...
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.

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.

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.

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 it

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

Run with a missing path:

Terminal window
cargo run -- --source fixtures/missing.md
echo $?

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:

Terminal window
cargo run --

Clap should print a usage error before your file-reading code runs.

You are done when all three commands behave as described:

Terminal window
cargo check
cargo run -- --source fixtures/site.md
cargo run -- --source fixtures/missing.md

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

  1. Which file did you edit today?
  2. Why is PathBuf used for --source instead of String?
  3. Which value owns the website bytes?
  4. What does each value returned by source.lines() borrow from?
  5. Why does the missing-file path return ExitCode::FAILURE?

Next: Day 2 — Typed Model Output →.