Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Verify Frozen Listings

How this chapter was built

This is the first chapter written with every earlier primitive already shipped. Its narrative freezes each slice’s code, shows evolution with {{#diff}}, and annotates new modules with sidecar callouts. That’s the methodology the book has been building since ch.2, now self-supporting.

Story

As a book author, I want a single command that fails my CI when any frozen listing in my book is no longer the intact snapshot it claims to be, so that readers can trust that the code in the book is real.

Acceptance criteria

Errors (any one fails the build, non-zero exit):

  1. Every frozen file exists. A manifest record whose frozen path no longer resolves to a file is an error naming the tag and path.
  2. Every snapshot is intact. A frozen file whose bytes no longer hash to the sha256 recorded at freeze time is an error. The snapshot was edited or corrupted after freezing.
  3. Every reference resolves. A {{#include listings/…}} path or a {{#diff}} tag operand in chapter prose that names no manifest record is an error naming the chapter and line. A <tag>.callouts.toml sidecar whose <tag> names no frozen listing is the same kind of broken reference: its annotations would silently attach to nothing and the build never complains, so it too is an error.

Warnings (reported, but the build stays green):

  1. Orphans. A frozen file under src/listings/ that no manifest record claims is flagged for cleanup. Sidecar *.callouts.toml files are not orphan candidates; their consistency is covered by AC 3.
  2. Stability audit. Every live: operand is listed with its chapter and line. These are the places the book is deliberately coupled to moving source, and an author should be able to see the list at a glance.

Exit contract:

  1. Errors produce a non-zero exit (the CI gate). Warnings alone exit 0. Either way the command prints a one-line summary of what it checked.

What verify deliberately does not check

Verify never compares a frozen listing against the current content of its source file. The first sketch of this story did exactly that: “fail when the latest frozen listing no longer matches the source.” It sounds right until you hold it against what freezing is for. A frozen tag exists to keep prose stable while the code moves. In a book like this one, where each chapter freezes versioned snapshots of an evolving codebase, the latest freeze differs from live source almost all the time, by design. A check that flagged that difference would fail nearly every build.

The need behind that sketch already has a mechanism. A live: operand shows current source and accepts the instability in trade. So verify enforces what freezing promises, that each snapshot is still byte-for-byte what was frozen, and audits what live: trades away, without judging either choice.

The slice — outside-in narrative outline

SliceWhat it adds
1Snapshot integrity (ACs 1, 2, 6). A failing tests/verify.rs integration test drives the first real verify behavior: a fixture book whose frozen file was edited after freezing must fail with a diagnostic naming the tag and path. src/verify.rs re-hashes every frozen file against the manifest’s recorded sha256; the CLI handler replaces the not yet implemented bail.
2Reference resolution and orphans (ACs 3, 4). Chapter markdown is scanned with the shared directive scanner from ch.6 slice 11; {{#include listings/…}} paths and \{{#diff}} tag operands must resolve to manifest records. Files in src/listings/ that no record claims warn.
3live: stability audit (AC 5) and dogfooding. Every live: operand is reported with chapter and line. verify is wired into this repo’s CI, making this book its first production user; the audit flags ch.4’s live: diff as the demonstration.

Outside-in narrative

Sections appear here as slices ship. All three slices have shipped.

Slice 1 — snapshot integrity

The failure this slice catches: a frozen listing gets “fixed” in place. Someone corrects a typo directly in src/listings/foo-v1.rs instead of fixing the source and refreezing, and from that point the book renders code that was never frozen from anywhere. Nothing in the build notices. The manifest has carried a sha256 per listing since ch.3 for this case; until now, nothing read it back.

Tests first. The fixture freezes a source file through the real freeze subcommand, so the manifest entry and recorded hash are what production wrote:

Listing 7.1
#![allow(unused)]
fn main() {
// verify-tests-v1.rs
// @@ 14,45 @@
/// A temp book with one source file frozen via the real `freeze`
/// subcommand, so the manifest entry and sha256 are exactly what
/// production wrote.
struct FrozenFixtureBook {
    _tmp: TempDir,
    root: PathBuf,
}

impl FrozenFixtureBook {
    fn new() -> Self {
        let tmp = TempDir::new().expect("tempdir");
        let root = tmp.path().join("book");
        fs::create_dir_all(&root).unwrap();
        let source = tmp.path().join("compose.yaml");
        fs::write(&source, "services:\n  web:\n    image: nginx\n").unwrap();
        mdbook_listings()
            .args(["freeze", "--tag", "compose-v1", "--book-root"])
            .arg(&root)
            .arg(&source)
            .assert()
            .success();
        Self { _tmp: tmp, root }
    }

    fn root(&self) -> &Path {
        &self.root
    }

    fn frozen_path(&self) -> PathBuf {
        self.root.join("src/listings/compose-v1.yaml")
    }
}
}

The headline test edits the frozen file after freezing and demands a failing exit plus a diagnostic naming the tag, the path, and the hash mismatch:

Listing 7.2
#![allow(unused)]
fn main() {
// verify-tests-v1.rs
// @@ 61,81 @@
#[test]
fn verify_fails_when_a_frozen_file_was_edited_after_freezing() {
    let book = FrozenFixtureBook::new();
    // Simulate the classic mistake: "fixing" the snapshot in place
    // instead of refreezing, which silently breaks the book's claim
    // to show real code.
    fs::write(
        book.frozen_path(),
        "services:\n  web:\n    image: nginx:edited\n",
    )
    .unwrap();

    mdbook_listings()
        .args(["verify", "--book-root"])
        .arg(book.root())
        .assert()
        .failure()
        .stderr(contains("compose-v1"))
        .stderr(contains("src/listings/compose-v1.yaml"))
        .stderr(contains("sha256"));
}
}

Three more tests pin the rest of the contract: an intact book succeeds with a 1 frozen listing checked summary, a deleted frozen file is an error rather than a crash, and a book with two broken listings reports both. Verify is a report, not a first-failure bail.

The module is small. verify loads the manifest and runs the integrity pass; findings carry a severity that the CLI maps to the exit code (callout 7.3.1). The pass re-hashes each frozen file with the same helper freeze used to record it (callout 7.3.2):

Listing 7.3
#![allow(unused)]
fn main() {
// verify-v1.rs
// @@ 1,83 @@
//! `verify`: the CI gate behind the book's core promise — the code it
//! shows is real. A frozen listing is "verified" when it is still the
//! intact snapshot that `freeze` recorded; current source is never
//! consulted, because diverging from a moving codebase is what freezing
//! is *for*.

use std::fs;
use std::path::Path;

use anyhow::Result;

use crate::freeze::hex_sha256;
use crate::manifest::Manifest;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
    /// Breaks the book's claim to show real code — fails the build.
    Error,
    /// Worth a look, but the book is still sound — reported, exit 0.
    Warning,
}

#[derive(Debug)]
pub struct Finding {
    pub severity: Severity,
    pub message: String,
}

#[derive(Debug, Default)]
pub struct VerifyReport {
    pub findings: Vec<Finding>,
    pub listings_checked: usize,
}

impl VerifyReport {
    pub fn error_count(&self) -> usize {
        self.findings
            .iter()
            .filter(|f| f.severity == Severity::Error)
            .count()
    }
}

/// Run every verify pass against the book at `book_root`.
pub fn verify(book_root: &Path) -> Result<VerifyReport> {
    let manifest = Manifest::load(book_root)?;
    let mut report = VerifyReport::default();
    check_snapshot_integrity(book_root, &manifest, &mut report);
    Ok(report)
}

/// Each manifest record's frozen file must exist and still hash to the
/// sha256 recorded at freeze time. A mismatch usually means someone
/// "fixed" the snapshot in place instead of refreezing.
fn check_snapshot_integrity(book_root: &Path, manifest: &Manifest, report: &mut VerifyReport) {
    for listing in &manifest.listings {
        report.listings_checked += 1;
        let frozen_abs = book_root.join(&listing.frozen);
        let bytes = match fs::read(&frozen_abs) {
            Ok(bytes) => bytes,
            Err(_) => {
                report.findings.push(Finding {
                    severity: Severity::Error,
                    message: format!(
                        "frozen listing `{}` is missing: {}",
                        listing.tag, listing.frozen,
                    ),
                });
                continue;
            }
        };
        if hex_sha256(&bytes) != listing.sha256 {
            report.findings.push(Finding {
                severity: Severity::Error,
                message: format!(
                    "frozen listing `{}` no longer matches its recorded sha256: {} \
                     (edited after freezing? refreeze or restore the snapshot)",
                    listing.tag, listing.frozen,
                ),
            });
        }
    }
}
}

The only change to freeze.rs is visibility: hex_sha256 becomes pub(crate) so verify hashes bytes the same way freeze recorded them. One function, no drift between writer and checker:

Listing 7.4
--- freeze-v5
+++ freeze-v6
@@ -305,7 +305,9 @@
         .ok_or_else(|| anyhow!("path {} is not valid UTF-8", path.display()))
 }
 
-fn hex_sha256(bytes: &[u8]) -> String {
+/// Shared with `verify`, which must hash frozen bytes exactly the way
+/// `freeze` recorded them.
+pub(crate) fn hex_sha256(bytes: &[u8]) -> String {
     let mut hasher = Sha256::new();
     hasher.update(bytes);
     let digest = hasher.finalize();

The CLI handler replaces the not yet implemented bail the subcommand has carried since it was first added. Findings print to stderr with error:/warning: prefixes, the summary to stdout, and any error makes the exit non-zero for CI to gate on:

Listing 7.5
--- main-v15
+++ main-v16
@@ -13,6 +13,7 @@
 use mdbook_listings::include::splice_chapter as splice_includes;
 use mdbook_listings::install::{InstallOutcome, ensure_assets_fresh, install};
 use mdbook_listings::manifest::Manifest;
+use mdbook_listings::verify::{Severity, verify};
 use mdbook_preprocessor::book::BookItem;
 
 /// Managed code listings for mdbook: inline callouts, freezing, and verification.
@@ -159,8 +160,23 @@
             }
             Ok(())
         }
-        Some(Command::Verify { book_root: _ }) => {
-            anyhow::bail!("`mdbook-listings verify` is not yet implemented")
+        Some(Command::Verify { book_root }) => {
+            let book_root = book_root.unwrap_or_else(|| PathBuf::from("."));
+            let report = verify(&book_root)?;
+            for finding in &report.findings {
+                match finding.severity {
+                    Severity::Error => eprintln!("error: {}", finding.message),
+                    Severity::Warning => eprintln!("warning: {}", finding.message),
+                }
+            }
+            let n = report.listings_checked;
+            let plural = if n == 1 { "" } else { "s" };
+            println!("{n} frozen listing{plural} checked");
+            let errors = report.error_count();
+            if errors > 0 {
+                anyhow::bail!("verify found {errors} error(s)");
+            }
+            Ok(())
         }
         Some(Command::List { book_root }) => {
             let book_root = book_root.unwrap_or_else(|| PathBuf::from("."));

What a failure looks like:

$ mdbook-listings verify --book-root book
error: frozen listing `compose-v1` no longer matches its recorded sha256: src/listings/compose-v1.yaml (edited after freezing? refreeze or restore the snapshot)
1 frozen listing checked
error: verify found 1 error(s)

And a clean run:

$ mdbook-listings verify --book-root book
1 frozen listing checked

Slice 2 — references and orphans

Slice 1 proved each frozen snapshot is intact. Slice 2 asks the next question: does every reference to a listing point at something real, and is every file in src/listings/ accounted for? verify gains three purely additive passes (the integrity check is untouched):

Listing 7.6
#![allow(unused)]
fn main() {
// verify-v2.rs
// @@ 109,203 @@
/// Every `{{#include listings/TAG…}}` path and every `\{{#diff}}` tag
/// operand in chapter prose must name a manifest record. A dangling
/// reference is an error the build would also hit; verify reports it with
/// chapter:line up front. `live:` operands are not resolution targets
/// (they show current source); they are audited separately.
fn check_references(book_root: &Path, manifest: &Manifest, report: &mut VerifyReport) {
    let tags: HashSet<&str> = manifest.listings.iter().map(|l| l.tag.as_str()).collect();
    for (rel, content) in chapter_markdown(book_root) {
        for occ in scan_directives(&content, "\{{#include ", FencePolicy::Annotate) {
            let path = occ.args.trim();
            // Only listings/ includes resolve to a frozen tag; snippets/
            // and other paths are not manifest records.
            let Some(rest) = path.strip_prefix("listings/") else {
                continue;
            };
            // Drop any `:start:end` range suffix, then take the file stem.
            let file = rest.split(':').next().unwrap_or(rest);
            let stem = Path::new(file)
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("");
            if !tags.contains(stem) {
                report.error(format!(
                    "{rel}:{}: \{{{{#include listings/{file}}}}} names no frozen listing `{stem}`",
                    line_number(&content, occ.span.start),
                ));
            }
        }
        for occ in scan_directives(&content, "\{{#diff", FencePolicy::SkipInside) {
            let tokens: Vec<&str> = occ.args.split_whitespace().collect();
            // The diff splicer only processes 2-token (whole-file) or
            // 4-token (with ranges) forms; the first two tokens are the
            // operands. Other arities are left literal, so don't validate.
            if tokens.len() != 2 && tokens.len() != 4 {
                continue;
            }
            for operand in &tokens[..2] {
                if operand.starts_with("live:") {
                    continue;
                }
                if manifest.find(operand).is_none() {
                    report.error(format!(
                        "{rel}:{}: \{{{{#diff}}}} operand `{operand}` names no frozen listing",
                        line_number(&content, occ.span.start),
                    ));
                }
            }
        }
    }
}

/// Each `<tag>.callouts.toml` sidecar must sit next to a real frozen
/// listing — its `<tag>` must match a manifest record's frozen-file stem.
/// A dangling sidecar attaches annotations to nothing and the build never
/// complains, so verify treats it as a broken reference.
fn check_sidecars(book_root: &Path, manifest: &Manifest, report: &mut VerifyReport) {
    let stems: HashSet<&str> = manifest
        .listings
        .iter()
        .filter_map(|l| Path::new(&l.frozen).file_stem().and_then(|s| s.to_str()))
        .collect();
    for name in listing_dir_entries(book_root) {
        let Some(stem) = name.strip_suffix(".callouts.toml") else {
            continue;
        };
        if !stems.contains(stem) {
            report.error(format!(
                "sidecar `{name}` names no frozen listing `{stem}` (its annotations attach to nothing)",
            ));
        }
    }
}

/// A frozen file under `src/listings/` that no manifest record claims is
/// an orphan — reported as a warning (stray, not broken). Sidecars are
/// handled by [`check_sidecars`], not here.
fn check_orphans(book_root: &Path, manifest: &Manifest, report: &mut VerifyReport) {
    let claimed: HashSet<&str> = manifest
        .listings
        .iter()
        .map(|l| l.frozen.as_str())
        .collect();
    for name in listing_dir_entries(book_root) {
        if name.ends_with(".callouts.toml") {
            continue;
        }
        let rel = format!("{LISTINGS_REL}/{name}");
        if !claimed.contains(rel.as_str()) {
            report.warning(format!(
                "orphan frozen file: {rel} (no manifest record claims it)"
            ));
        }
    }
}
}

check_references (callout 7.6.1) reuses the directive scanner from ch.6 slice 11 to walk chapter prose. The include side resolves the listings/<tag> path against the manifest; snippets/ paths and live: operands are not manifest records, so they’re left to later checks or skipped. Wrong-arity {{#diff}} forms are skipped too, matching what the diff splicer leaves literal, so verify reports what would actually break and no more.

check_sidecars (callout 7.6.2) answers “what about the .callouts.toml files?” A sidecar attaches annotations to the listing whose stem it shares; if no such listing exists, those annotations silently attach to nothing. check_orphans (callout 7.6.3) is the gentler mirror: a frozen file no manifest record claims is a warning, stray rather than broken.

The tests grew the same way, one case per pass:

Listing 7.7
--- verify-tests-v1
+++ verify-tests-v2
@@ -42,6 +42,28 @@
     fn frozen_path(&self) -> PathBuf {
         self.root.join("src/listings/compose-v1.yaml")
     }
+
+    /// Freeze another source so a chapter can reference it as a real tag.
+    fn freeze(&self, tag: &str, body: &str) {
+        let source = self.root.parent().unwrap().join(format!("{tag}.yaml"));
+        fs::write(&source, body).unwrap();
+        mdbook_listings()
+            .args(["freeze", "--tag", tag, "--book-root"])
+            .arg(&self.root)
+            .arg(&source)
+            .assert()
+            .success();
+    }
+
+    /// Write a chapter markdown file under `src/`.
+    fn write_chapter(&self, slug: &str, content: &str) {
+        fs::write(self.root.join(format!("src/{slug}.md")), content).unwrap();
+    }
+
+    /// Write a raw file into `src/listings/` (e.g. an orphan or a sidecar).
+    fn write_listing_file(&self, name: &str, content: &str) {
+        fs::write(self.root.join("src/listings").join(name), content).unwrap();
+    }
 }
 
 #[test]
@@ -109,6 +131,84 @@
 }
 
 #[test]
+fn verify_fails_on_a_diff_operand_that_names_no_listing() {
+    let book = FrozenFixtureBook::new();
+    book.write_chapter("ch", "Diffing.\n\n{{#diff compose-v1 ghost-v1}}\n");
+
+    mdbook_listings()
+        .args(["verify", "--book-root"])
+        .arg(book.root())
+        .assert()
+        .failure()
+        .stderr(contains("ghost-v1"))
+        .stderr(contains("ch.md"));
+}
+
+#[test]
+fn verify_fails_on_an_include_that_names_no_listing() {
+    let book = FrozenFixtureBook::new();
+    book.write_chapter(
+        "ch",
+        "Including.\n\n```yaml\n{{#include listings/ghost.yaml}}\n```\n",
+    );
+
+    mdbook_listings()
+        .args(["verify", "--book-root"])
+        .arg(book.root())
+        .assert()
+        .failure()
+        .stderr(contains("ghost"))
+        .stderr(contains("ch.md"));
+}
+
+#[test]
+fn verify_succeeds_when_every_reference_resolves() {
+    let book = FrozenFixtureBook::new();
+    book.freeze("other-v1", "other: true\n");
+    book.write_chapter(
+        "ch",
+        "Real refs.\n\n```yaml\n{{#include listings/compose-v1.yaml}}\n```\n\n\
+         {{#diff compose-v1 other-v1}}\n",
+    );
+
+    mdbook_listings()
+        .args(["verify", "--book-root"])
+        .arg(book.root())
+        .assert()
+        .success();
+}
+
+#[test]
+fn verify_fails_on_a_sidecar_with_no_matching_listing() {
+    let book = FrozenFixtureBook::new();
+    book.write_listing_file(
+        "ghost.callouts.toml",
+        "[[callout]]\nline = 1\nlabel = \"x\"\n",
+    );
+
+    mdbook_listings()
+        .args(["verify", "--book-root"])
+        .arg(book.root())
+        .assert()
+        .failure()
+        .stderr(contains("ghost"))
+        .stderr(contains("callouts.toml"));
+}
+
+#[test]
+fn verify_warns_on_an_orphan_frozen_file_but_succeeds() {
+    let book = FrozenFixtureBook::new();
+    book.write_listing_file("orphan.yaml", "stray: true\n");
+
+    mdbook_listings()
+        .args(["verify", "--book-root"])
+        .arg(book.root())
+        .assert()
+        .success()
+        .stderr(contains("orphan"));
+}
+
+#[test]
 fn verify_reports_every_broken_listing_not_just_the_first() {
     let tmp = TempDir::new().expect("tempdir");
     let root = tmp.path().join("book");

A book with a dangling reference now fails fast:

$ mdbook-listings verify --book-root book
error: src/ch04.md:88: {{#diff}} operand `compose-v9` names no frozen listing
1 frozen listing checked
error: verify found 1 error(s)

Slice 3 — the live: audit, and verify finds drift in its own book

The last pass closes AC 5. A live: diff operand (ch.4) renders current source instead of a frozen snapshot — a deliberate trade of stability for currency. check_live_operands (callout 7.8.1) reports each one with chapter and line, a warning rather than an error:

Listing 7.8
#![allow(unused)]
fn main() {
// verify-v3.rs
// @@ 211,235 @@
/// Report every `live:` diff operand. A `live:` operand renders current
/// source instead of a frozen snapshot, so that spot tracks a moving
/// codebase — the freeze stability guarantee is deliberately traded away.
/// This is a warning, not an error: it's a legitimate choice the author
/// should simply be able to see at a glance.
fn check_live_operands(book_root: &Path, report: &mut VerifyReport) {
    for (rel, content) in chapter_markdown(book_root) {
        for occ in scan_directives(&content, "\{{#diff", FencePolicy::SkipInside) {
            let tokens: Vec<&str> = occ.args.split_whitespace().collect();
            if tokens.len() != 2 && tokens.len() != 4 {
                continue;
            }
            for operand in &tokens[..2] {
                if let Some(path) = operand.strip_prefix("live:") {
                    report.warning(format!(
                        "{rel}:{}: \{{{{#diff}}}} uses a live operand `live:{path}` — \
                         shows current source, not a frozen snapshot, so freeze \
                         stability is traded away here",
                        line_number(&content, occ.span.start),
                    ));
                }
            }
        }
    }
}
}

With every pass in place, this book becomes verify’s first production user: mdbook-listings verify --book-root book now runs in CI and as a pre-commit hook, the same gate any downstream book would wire up.

The first real run is the point of the whole chapter. It failed — and it was right to:

$ mdbook-listings verify --book-root book
error: frozen listing `e2e-callouts-v1` no longer matches its recorded sha256: ...
... (10 more) ...
116 frozen listings checked
error: verify found 11 error(s)

Eleven frozen snapshots had drifted from their recorded hashes. Not corruption: each had been edited in place by a legitimate sweep — the chapter renumber that turned ch04-… into ch05-… across the e2e listings, and the locator! migration from ch.5’s refactor — and none had been re-frozen, so the integrity records went stale. Confirming that (the freeze-commit diff for each showed only the deliberate edit), the fix was to re-seal: recompute each hash from the current bytes. The root cause got a fix too — the repo’s typos pre-commit hook was rewriting files under src/listings/, so it’s now scoped to leave frozen snapshots alone.

That is the chapter’s thesis demonstrated on itself. The book had quietly stopped being able to prove eleven of its listings were the snapshots it claimed — and the tool built to catch exactly that caught exactly that.

The slice-3 code delta — the live: audit, plus a fix for a false positive the dogfood surfaced (a {{#include listings/<tag>.callouts.toml}} that displays a sidecar file is not a listing reference):

Listing 7.9
--- verify-v2
+++ verify-v3
@@ -70,6 +70,7 @@
     check_references(book_root, &manifest, &mut report);
     check_sidecars(book_root, &manifest, &mut report);
     check_orphans(book_root, &manifest, &mut report);
+    check_live_operands(book_root, &mut report);
     Ok(report)
 }
 
@@ -121,8 +122,14 @@
             let Some(rest) = path.strip_prefix("listings/") else {
                 continue;
             };
-            // Drop any `:start:end` range suffix, then take the file stem.
+            // Drop any `:start:end` range suffix.
             let file = rest.split(':').next().unwrap_or(rest);
+            // A `listings/<tag>.callouts.toml` include displays a sidecar
+            // file, not a frozen listing — its existence is the sidecar
+            // pass's job, not a tag reference here.
+            if file.ends_with(".callouts.toml") {
+                continue;
+            }
             let stem = Path::new(file)
                 .file_stem()
                 .and_then(|s| s.to_str())
@@ -201,6 +208,32 @@
     }
 }
 
+/// Report every `live:` diff operand. A `live:` operand renders current
+/// source instead of a frozen snapshot, so that spot tracks a moving
+/// codebase — the freeze stability guarantee is deliberately traded away.
+/// This is a warning, not an error: it's a legitimate choice the author
+/// should simply be able to see at a glance.
+fn check_live_operands(book_root: &Path, report: &mut VerifyReport) {
+    for (rel, content) in chapter_markdown(book_root) {
+        for occ in scan_directives(&content, "\{{#diff", FencePolicy::SkipInside) {
+            let tokens: Vec<&str> = occ.args.split_whitespace().collect();
+            if tokens.len() != 2 && tokens.len() != 4 {
+                continue;
+            }
+            for operand in &tokens[..2] {
+                if let Some(path) = operand.strip_prefix("live:") {
+                    report.warning(format!(
+                        "{rel}:{}: \{{{{#diff}}}} uses a live operand `live:{path}` — \
+                         shows current source, not a frozen snapshot, so freeze \
+                         stability is traded away here",
+                        line_number(&content, occ.span.start),
+                    ));
+                }
+            }
+        }
+    }
+}
+
 /// Top-level file names in `<book_root>/src/listings/` (no subdirectories).
 fn listing_dir_entries(book_root: &Path) -> Vec<String> {
     let dir = book_root.join(LISTINGS_REL);
@@ -452,6 +485,68 @@
     }
 
     #[test]
+    fn check_references_ignores_sidecar_toml_includes() {
+        // A chapter that includes a `.callouts.toml` to display it (ch.6
+        // does this) is not referencing a frozen listing — its existence
+        // is the sidecar pass's job, not a tag reference here.
+        let (_t, root, manifest) = book_with_demo();
+        fs::write(
+            root.join("src/ch.md"),
+            "```toml\n{{#include listings/demo-v1.callouts.toml}}\n```\n",
+        )
+        .unwrap();
+
+        let mut report = VerifyReport::default();
+        check_references(&root, &manifest, &mut report);
+        assert_eq!(report.error_count(), 0, "got {:?}", report.findings);
+    }
+
+    #[test]
+    fn check_live_operands_warns_with_chapter_and_line() {
+        let (_t, root, _m) = book_with_demo();
+        fs::write(
+            root.join("src/ch.md"),
+            "intro\n\n{{#diff demo-v1 live:../src/foo.rs}}\n",
+        )
+        .unwrap();
+
+        let mut report = VerifyReport::default();
+        check_live_operands(&root, &mut report);
+        assert_eq!(report.error_count(), 0);
+        assert_eq!(report.findings.len(), 1, "got {:?}", report.findings);
+        assert_eq!(report.findings[0].severity, Severity::Warning);
+        let m = &report.findings[0].message;
+        assert!(m.contains("live:../src/foo.rs"), "got: {m}");
+        assert!(m.contains("ch.md:3"), "got: {m}");
+    }
+
+    #[test]
+    fn check_live_operands_ignores_wrong_arity_diff() {
+        // A 3-token diff is left literal by the splicer, so its operands
+        // (live: or not) are not audited — pins the arity guard.
+        let (_t, root, _m) = book_with_demo();
+        fs::write(
+            root.join("src/ch.md"),
+            "{{#diff demo-v1 live:../src/foo.rs extra}}\n",
+        )
+        .unwrap();
+
+        let mut report = VerifyReport::default();
+        check_live_operands(&root, &mut report);
+        assert!(report.findings.is_empty(), "got {:?}", report.findings);
+    }
+
+    #[test]
+    fn check_live_operands_silent_when_no_live_operand() {
+        let (_t, root, _m) = book_with_demo();
+        fs::write(root.join("src/ch.md"), "{{#diff demo-v1 demo-v1}}\n").unwrap();
+
+        let mut report = VerifyReport::default();
+        check_live_operands(&root, &mut report);
+        assert!(report.findings.is_empty(), "got {:?}", report.findings);
+    }
+
+    #[test]
     fn check_references_ignores_snippets_includes() {
         let (_t, root, manifest) = book_with_demo();
         fs::write(
Listing 7.10
(no changes between verify-tests-v2 and verify-tests-v3)

With the book re-sealed, verify is green — one warning remains, the live: operand ch.4 uses on purpose:

$ mdbook-listings verify --book-root book
warning: src/ch04-show-diffs-between-slices.md:413: {{#diff}} uses a live operand `live:../../src/diff.rs` — shows current source, not a frozen snapshot, so freeze stability is traded away here
116 frozen listings checked

What this story does not solve

verify is shallow: it confirms a snapshot is byte-for-byte what was frozen, not that the code still compiles or passes its tests. A deep verify — building or running the frozen listings — is a much larger story for a later release. So is auto-remediation: verify reports, and the author decides whether to re-seal, re-freeze, or leave drift in place; it never edits the manifest for you. And it has no notion of a listing that’s meant to track current source — that’s what live: is for, and verify audits rather than enforces it. A future opt-in “mirror” mode for reference-style books is sketched in ch. 9.