Day 15 · Hardening and the Phase 2 Bridge
Day 14 finished the features. askr asks, streams, retrieves. Today is
about everything between “it runs” and “I’d let someone else depend on it”: configuration, error
handling, automated checks, and knowing where the time goes. It’s the quiet half of engineering — and
it’s where the trait-first design you’ve been building pays off, because the whole thing is already
testable. Then we look past Phase 1 to the chains you’ll build next.
Configuration: read it at the edges, fail loudly
Section titled “Configuration: read it at the edges, fail loudly”askr has exactly two knobs, both environment variables, both read in one small config module:
ANTHROPIC_API_KEY— required to reach the real API; absent, the program falls back to the mock and tells you.ANTHROPIC_MODEL— the model id, with a clearly-labelled placeholder default so the binary runs but you know to override it.
The principle: parse configuration once, at the boundary, into typed values — never reach for
env::var deep inside business logic. A missing key becomes a typed AskrError::MissingEnv with a
message that names the variable, not a panic three layers down.
let client: Box<dyn LlmClient> = if args.mock || config::api_key().is_err() { if !args.mock { eprintln!("note: ANTHROPIC_API_KEY not set — using the offline mock model.\n"); } Box::new(MockLlm::echo())} else { Box::new(ClaudeClient::new(config::api_key()?))};Error handling: one type, a clean exit
Section titled “Error handling: one type, a clean exit”Every failure in the crate funnels into AskrError, and main is a thin shell that turns it into a
process exit code and a human message — no stack traces dumped at the user:
#[tokio::main]async fn main() { if let Err(e) = run().await { eprintln!("error: {e}"); std::process::exit(1); }}This is the shape every CLI should have: run() returns Result<()>, main decides what a failure
means to the operating system (non-zero exit) and to the human (a one-line message). Because ?
propagates and thiserror gives each variant a Display, you get that for free.
A library, not just a binary
Section titled “A library, not just a binary”One structural change earns a lot here: askr is split into a library (src/lib.rs, all the logic)
and a thin binary (src/main.rs, just arg-parsing and wiring). Two payoffs:
- The logic is importable and unit-testable on its own —
cargo testdrivesLlmClient,Embedder,VectorStore, and the RAG functions directly, never throughmain. pubitems are a real public API, so the compiler stops nagging about “unused” helpers that only tests call — the API is legitimately exposed.
CI: make the machine enforce it
Section titled “CI: make the machine enforce it”Three checks should run on every push and pull request, and a human should never have to remember them:
name: cion: push: branches: [main] pull_request:
jobs: check: runs-on: ubuntu-latest defaults: run: working-directory: rust/askr steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Format run: cargo fmt --all -- --check - name: Lint run: cargo clippy --all-targets -- -D warnings - name: Test run: cargo test --allEach line earns its place:
cargo fmt --all -- --checkfails if the code isn’t formatted canonically. Formatting stops being an opinion; it’s just true or false.cargo clippy --all-targets -- -D warningsruns Rust’s linter and treats every warning as an error, so lint debt can’t accumulate.cargo test --allruns the suite. And here is where the trait-first design closes the loop: because every test usesMockLlm/MockEmbedder, CI needs no API key and no network. The pipeline is fast, free, and deterministic — exactly what a green check should be.
Profiling: measure before you optimize
Section titled “Profiling: measure before you optimize”askr has one obvious hot path — the cosine loop in Day 14 — and the
honest engineering move is to measure before assuming it’s slow. A few habits:
- Always profile a
--releasebuild. A debug build can be 10–50× slower; numbers from it are meaningless.cargo build --release/cargo run --release. - Benchmark the hot function in isolation with
criterion(a dev-dependency) — it runs many iterations and reports stable timings with confidence intervals, so you can tell a real change from noise. - Find the hot spot, don’t guess it.
cargo flamegraphshows where wall-clock time actually goes. Foraskr, you’ll find the answer is almost always “waiting on the network,” not the cosine math — which is exactly why streaming (Day 13) did more for the felt experience than any micro-optimization could.
The recurring thread, one last time: what does building this force you to understand? That correctness came first and for free — the borrow checker proved no chunk dangles, no error is swallowed, no stream corrupts a character — so by the time you reach performance, you’re tuning code you already trust. That ordering is Rust’s quiet superpower.
The bridge to Phase 2
Section titled “The bridge to Phase 2”Phase 1 is done. You’ve shipped four real programs and, with them, the Rust that matters: ownership and borrowing, errors that scale, traits and generics and lifetimes, iterators, fearless concurrency, async, HTTP both as server and client, and the production habits above. That’s a foundation, not a finish line.
Phase 2 (Days 16–30) turns that foundation toward the question you actually came here for — why do blockchains exist, and how would I build one? You’ll build three mini-chains from scratch, each answering “what problem did it solve?”:
Day 16–30: build the idea, smallest version that teaches it
Bitcoin-style → UTXO ledger + Proof-of-Work "how do strangers agree on history with no central server?"
Ethereum-style → account model + a tiny VM "what if the ledger could run programs, not just move coins?"
Solana-style → Proof-of-History clock + parallel execution "what if we could order and execute transactions far faster?"Each is a Rust crate in code/, built with the exact muscles you trained in Phase 1 — enums and
pattern-matching for transactions, Vec and hashing for the ledger, traits for pluggable consensus,
concurrency for parallel execution. The compiler keeps protecting you from the same bugs, now in a domain
where a single data race or unchecked error is also a financial bug.
Build it
Section titled “Build it”- Confirm config is read only in the
configmodule; confirmmainreturns a clean exit on error. - Add the CI workflow above and watch
fmt,clippy, andtestgo green — with no secrets configured. - Run
cargo build --releaseand trycargo flamegraph(or just time a RAG query) to see where the time really goes. - Skim
cargo audit; add it to CI if you like. - Take a breath — you finished Phase 1. Phase 2 starts with a UTXO ledger.
Check your understanding
Section titled “Check your understanding”- What’s the principle behind reading
ANTHROPIC_API_KEYandANTHROPIC_MODELonly in theconfigmodule, and what does a missing key become instead of a panic deep in the code? - The CI runs
fmt --check,clippy -D warnings, andtest. Why can theteststep run with no API key and no network — what design choice makes that possible? - Why does
cargo clippy -- -D warningsuse-D warningsrather than just running clippy plain? - Name two profiling habits the chapter insists on, and why profiling a debug build is a mistake.
- Phase 2 builds Bitcoin-, Ethereum-, and Solana-style chains. In one phrase each, what problem does each style answer — and how does building a Solana-style runtime by hand help with the reader’s Anchor goal?
Show answers
- Parse configuration once, at the boundary, into typed values — never call
env::vardeep in business logic. A missing key becomes a typedAskrError::MissingEnvthat names the variable (and letsaskrfall back to the mock), rather than a panic several layers down. - Because every test injects
MockLlm/MockEmbedderinstead of the real network clients. Hiding the network behind theLlmClient/Embeddertraits makes the suite hermetic — no key, no socket — so CI is fast, free, and deterministic. -D warningspromotes every clippy warning to a hard error, failing the build so lint debt can’t silently accumulate over time. Plain clippy would only print warnings, which are easy to ignore.- Two of: profile a
--releasebuild (debug is 10–50× slower and its numbers are meaningless); benchmark the hot function in isolation withcriterion; find the hot spot withcargo flamegraphrather than guessing. Profiling a debug build is a mistake because the optimizer is off, so the timings don’t reflect what ships. - Bitcoin-style: how strangers agree on one history with no central server (UTXO + Proof-of-Work). Ethereum-style: making the ledger run programs, not just move coins (accounts + a tiny VM). Solana-style: ordering and executing transactions much faster (Proof-of-History + parallel execution). Building the Solana-style runtime by hand makes Anchor’s model click, because you’ll have implemented the ideas Anchor abstracts — leaving the language as the easy part.
Prev: Day 14 · Vector Search · You’ve finished Phase 1. Next up: Phase 2 (Days 16–30) — building BTC → ETH → SOL mini-chains, then a bridge into real Solana/Anchor.