diff --git a/README.md b/README.md
index 1111111..2222222 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
 # Sample Project

-A tiny demo used for manual UI testing.
+A tiny demo used for manual UI testing of the file tree.
+
+This diff intentionally covers adds, edits, deletes, and renames.

 ## Usage

-Run `cargo run`.
+Run `cargo run` and enjoy.
diff --git a/Cargo.toml b/Cargo.toml
index 3333333..4444444 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [package]
 name = "sample"
-version = "0.1.0"
+version = "0.2.0"
 edition = "2021"

 [dependencies]
+serde = { version = "1", features = ["derive"] }
diff --git a/src/main.rs b/src/main.rs
index 5555555..6666666 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,10 +1,15 @@
-fn main() {
-    println!("Hello, world!");
-    let x = 1;
-    let y = 2;
-    println!("{}", x + y);
+use sample::greeting;
+
+fn main() {
+    println!("{}", greeting("world"));
+    let sum: i32 = (1..=10).sum();
+    println!("sum = {sum}");
 }

-fn unused() {
-    // to be removed later
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
 }
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..7777777
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,7 @@
+/// Return a friendly greeting for `name`.
+pub fn greeting(name: &str) -> String {
+    format!("Hello, {name}!")
+}
+
+pub mod util;
+pub mod api;
diff --git a/src/util/mod.rs b/src/util/mod.rs
new file mode 100644
index 0000000..8888888
--- /dev/null
+++ b/src/util/mod.rs
@@ -0,0 +1,5 @@
+pub mod string;
+
+pub fn is_empty(s: &str) -> bool {
+    s.trim().is_empty()
+}
diff --git a/src/util/string.rs b/src/util/string.rs
new file mode 100644
index 0000000..9999999
--- /dev/null
+++ b/src/util/string.rs
@@ -0,0 +1,9 @@
+pub fn shout(s: &str) -> String {
+    s.to_uppercase()
+}
+
+pub fn whisper(s: &str) -> String {
+    s.to_lowercase()
+}
+
+// TODO: add reverse() helper
diff --git a/src/api/handlers.rs b/src/api/handlers.rs
index aaaaaaa..bbbbbbb 100644
--- a/src/api/handlers.rs
+++ b/src/api/handlers.rs
@@ -5,9 +5,12 @@ pub struct Request {
 }

 pub fn handle(req: &Request) -> String {
-    format!("path={}", req.path)
+    format!("path={} method=GET", req.path)
 }

 pub fn health() -> &'static str {
-    "ok"
+    "OK"
+}
+
+pub fn version() -> &'static str {
+    env!("CARGO_PKG_VERSION")
 }
diff --git a/src/api/routes.rs b/src/api/old_routes.rs
similarity index 60%
rename from src/api/old_routes.rs
rename to src/api/routes.rs
index ccccccc..ddddddd 100644
--- a/src/api/old_routes.rs
+++ b/src/api/routes.rs
@@ -1,6 +1,8 @@
-pub const ROUTES: &[&str] = &[
+pub const ROUTES: &[(&str, &str)] = &[
-    "/health",
-    "/version",
-    "/echo",
+    ("GET", "/health"),
+    ("GET", "/version"),
+    ("POST", "/echo"),
+    ("GET", "/metrics"),
 ];
diff --git a/src/legacy/deprecated.rs b/src/legacy/deprecated.rs
deleted file mode 100644
index eeeeeee..0000000
--- a/src/legacy/deprecated.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-// This module was deprecated in v0.1.
-pub fn old_api() -> u32 {
-    42
-}
-
-pub fn old_helper() -> &'static str {
-    "legacy"
-}
diff --git a/tests/integration.rs b/tests/integration.rs
index fffffff..1010101 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -1,5 +1,10 @@
 use sample::greeting;

 #[test]
 fn greets_world() {
     assert_eq!(greeting("world"), "Hello, world!");
 }
+
+#[test]
+fn greets_empty() {
+    assert_eq!(greeting(""), "Hello, !");
+}
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 0000000..2020202
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,11 @@
+# Guide
+
+## Getting started
+
+Install with cargo:
+
+```sh
+cargo add sample
+```
+
+See `README.md` for more details.
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000..3030303
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+cargo fmt --all
+cargo clippy --all-targets -- -D warnings
+cargo build --release
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4040404
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,12 @@
+name: CI
+on:
+  push:
+    branches: [main]
+  pull_request:
+jobs:
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dtolnay/rust-toolchain@stable
+      - run: cargo test --all
diff --git a/long.rs b/long.rs
index cddfa4c..3d5d59f 100644
--- a/long.rs
+++ b/long.rs
@@ -2,7 +2,8 @@
     let _ = 2; // descriptive context line 2 in long.rs
     let _ = 3; // descriptive context line 3 in long.rs
     let _ = 4; // descriptive context line 4 in long.rs
-    let _ = 5; // descriptive context line 5 in long.rs
+    let _ = 5; // FIXED descriptive context line 5 in long.rs
+    // inserted preamble note near the top of the file
     let _ = 6; // descriptive context line 6 in long.rs
 fn step_7(n: usize) -> usize { n + 7 }
     let _ = 8; // descriptive context line 8 in long.rs
@@ -77,7 +78,9 @@ fn step_67(n: usize) -> usize { n + 67 }
     let _ = 77; // descriptive context line 77 in long.rs
     let _ = 78; // descriptive context line 78 in long.rs
     let _ = 79; // descriptive context line 79 in long.rs
-// ---- chapter boundary for line 80 ----
+    let _ = 81; // ADJUSTED descriptive context line 81 in long.rs
+    // patch: new guard at line ~81
+    // patch: second new line at ~82
     let _ = 81; // descriptive context line 81 in long.rs
     let _ = 82; // descriptive context line 82 in long.rs
     let _ = 83; // descriptive context line 83 in long.rs
@@ -178,7 +181,7 @@ fn step_167(n: usize) -> usize { n + 167 }
     let _ = 178; // descriptive context line 178 in long.rs
     let _ = 179; // descriptive context line 179 in long.rs
 // ---- chapter boundary for line 180 ----
-    let _ = 181; // descriptive context line 181 in long.rs
+fn step_167_renamed(n: usize) -> usize { n + 167 + 1 }
     let _ = 182; // descriptive context line 182 in long.rs
     let _ = 183; // descriptive context line 183 in long.rs
     let _ = 184; // descriptive context line 184 in long.rs
@@ -245,7 +248,6 @@ fn step_227(n: usize) -> usize { n + 227 }
     let _ = 245; // descriptive context line 245 in long.rs
     let _ = 246; // descriptive context line 246 in long.rs
 fn step_247(n: usize) -> usize { n + 247 }
-    let _ = 248; // descriptive context line 248 in long.rs
     let _ = 249; // descriptive context line 249 in long.rs
     let _ = 250; // descriptive context line 250 in long.rs
     let _ = 251; // descriptive context line 251 in long.rs
@@ -316,6 +318,9 @@ fn step_307(n: usize) -> usize { n + 307 }
     let _ = 316; // descriptive context line 316 in long.rs
     let _ = 317; // descriptive context line 317 in long.rs
     let _ = 318; // descriptive context line 318 in long.rs
+// inserted block A at ~320
+// inserted block B at ~321
+// inserted block C at ~322
     let _ = 319; // descriptive context line 319 in long.rs
 // ---- chapter boundary for line 320 ----
     let _ = 321; // descriptive context line 321 in long.rs
@@ -394,7 +399,7 @@ fn step_387(n: usize) -> usize { n + 387 }
     let _ = 394; // descriptive context line 394 in long.rs
     let _ = 395; // descriptive context line 395 in long.rs
     let _ = 396; // descriptive context line 396 in long.rs
-    let _ = 397; // descriptive context line 397 in long.rs
+    let _ = 397; // TAIL descriptive context line 397 in long.rs
     let _ = 398; // descriptive context line 398 in long.rs
     let _ = 399; // descriptive context line 399 in long.rs
 // ---- chapter boundary for line 400 ----
