From one TOML file to a verified event loop

2026-06-21 — a worked example: spec → validate → derived branches → generated Rust → proof the generated code matches the hand-written reference.

The previous note argued the thesis: as agents write analysis code faster than anyone can review it, correctness has to move from soft guidance into the compiler. This post is the thesis made concrete — a real, end-to-end run you can reproduce from the repo today. We start from a physics-facing spec, and at every step the tool either derives the next artifact or rejects an inconsistent one. Nothing here is mocked; every block below is real output from cargo-built binaries in the tree.

1. The spec — what a physicist writes

A muon control region, the whole thing, in TOML (crates/nano-spec/examples/muon.toml):

[analysis]
name = "muon_demo"
year = "Run2018"

[objects.good_muon]
source = "Muon"
cuts = ["pt > 30 GeV", "abs(eta) < 2.4"]

[regions.signal]
require = ["count(good_muon) >= 1"]

[[outputs]]
name = "n_good_muon"
expr = "count(good_muon)"

[[outputs]]
name = "lead_muon_pt"
expr = "leading(good_muon).pt"

This is the only thing a physicist edits. It names an object (good_muon, derived from the Muon collection with two cuts), a region, and two outputs. Note pt > 30 GeV — the unit is part of the spec, not a comment.

2. Validate — the spec is checked before any code runs

$ nano validate crates/nano-spec/examples/muon.toml
OK validate crates/nano-spec/examples/muon.toml
analysis: muon_demo (Run2018)
catalogue: v9
objects: good_muon:Muon
regions: signal
outputs: n_good_muon, lead_muon_pt
read_branches: nMuon U32, Muon_eta VecF32, Muon_pt VecF32

Validation resolves the spec against the NanoAODv9 branch catalogue and proves the things review usually has to catch by eye:

A typo like Muon_ptt, a Run2018 branch that only exists in v12, or pt > 30 without a unit doesn't produce a wrong number downstream — it fails right here, with a precise error, before a single event is read.

3. Derive the read set — never "read everything just in case"

The exact set of input branches to bind is computed from the spec, not written by hand:

$ nano branches crates/nano-spec/examples/muon.toml
nMuon U32
Muon_eta VecF32
Muon_pt VecF32

$ nano branches crates/nano-spec/examples/muon.toml --json
{
  "command": "branches",
  "status": "ok",
  "spec_path": "crates/nano-spec/examples/muon.toml",
  "catalogue_version": "v9",
  "branches": [
    { "name": "nMuon",    "branch_type": "U32" },
    { "name": "Muon_eta", "branch_type": "VecF32" },
    { "name": "Muon_pt",  "branch_type": "VecF32" }
  ]
}

Three branches — exactly the ones the cuts (pt, eta) and the counter (nMuon) require, and nothing else. This is what the streaming reader binds, so the I/O cost is a function of the spec, not of the file's 1000+ branches. The --json form is the same fact in machine-readable shape — this is what the MCP server and an orchestrating agent consume.

4. Codegen — the spec becomes Rust

$ nano codegen crates/nano-spec/examples/muon.toml
// @generated by nano-spec from analysis `muon_demo`. Do not edit by hand.

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GenRow {
    pub n_good_muon: u32,
    pub lead_muon_pt: f32,
}

pub struct GeneratedProducer;

impl GeneratedProducer {
    pub fn analyze(event: &nano_core::Event) -> nano_core::Result<Option<GenRow>> {
        let good_muon_collection = event.collection("Muon")?;
        let mut n_good_muon = 0_u32;
        let mut lead_muon_pt: Option<f32> = None;

        for good_muon_item in good_muon_collection.iter() {
            let good_muon_eta = good_muon_item.get::<f32>("eta")?;
            let good_muon_pt = good_muon_item.get::<f32>("pt")?;
            if (good_muon_pt > 30.0_f32) && (good_muon_eta.abs() < 2.4_f32) {
                n_good_muon += 1;
                lead_muon_pt = Some(
                    lead_muon_pt.map_or(good_muon_pt, |lead| lead.max(good_muon_pt)),
                );
            }
        }

        if !(n_good_muon >= 1_u32) {
            return Ok(None);
        }
        let Some(lead_muon_pt) = lead_muon_pt else {
            return Ok(None);
        };
        Ok(Some(GenRow {
            n_good_muon: n_good_muon,
            lead_muon_pt: lead_muon_pt,
        }))
    }
}

This is the generated event loop, verbatim. Read it as a physicist: the cut order matches the spec, the >30 and <2.4 thresholds are right there, the region requirement count(good_muon) >= 1 is the return Ok(None) veto, and the two outputs are the struct fields. It is readable on purpose — the whole design contract is "agents (here, the generator) write; humans review the physics." And because it is generated from the validated plan, it can only read branches that validation already proved exist with the right type: get::<f32>("pt") cannot be wired to a branch that isn't there.

5. The guarantee — generated code equals the hand-written reference

The claim that codegen is faithful isn't a promise in prose — it's a test in CI. nano-gen-demo generates the producer at build time (build.rs runs the exact same validate + generate_producer_source), then checks it against the hand-written MuonProducer on synthetic events spanning the edge cases (a muon just below 30 GeV, one at |η| = 2.39, an empty event):

let generated   = GeneratedProducer::analyze(&event).unwrap().map(|r| (r.n_good_muon, r.lead_muon_pt));
let handwritten = MuonProducer::analyze(&event)     .unwrap().map(|r| (r.n_good_muon, r.lead_muon_pt));
assert_eq!(generated, handwritten, "entry {entry}");
$ cargo test -p nano-gen-demo
test generated_muon_producer_matches_handwritten_producer_on_synthetic_events ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

So the chain closes: spec → validated plan → generated kernel → proven equivalent to the reference physicists trust. The human reviews the spec and the reference once; the generator reproduces it; the test guarantees the two agree.

The workflow, in one line per step

nano validate   spec.toml   # reject inconsistent physics before any I/O
nano branches   spec.toml   # derive the exact read set (nothing "just in case")
nano codegen    spec.toml   # emit a readable, typed event loop
cargo test                  # prove the generated loop == the trusted reference

Every arrow is mechanical. A physicist's edits live in the TOML; an agent's edits live in code that has to compile against a validated plan and match a golden reference to survive. The same four operations are exposed over the MCP protocol (docs/agent-interface.md), so an orchestrating agent drives this exact loop through typed tools whose effects are gated by the validator — the orchestrator proposes, the compiler disposes.

Reproduce it / watch it

Everything above is one cargo build away:

$ cargo build -p nano-cli
$ nano validate crates/nano-spec/examples/muon.toml
$ nano branches crates/nano-spec/examples/muon.toml
$ nano codegen  crates/nano-spec/examples/muon.toml
$ cargo test -p nano-gen-demo

For a terminal recording of the full run — including a wrongly-defined analysis getting blocked by the validator — see the companion screencast post. It's a reproducible asciinema cast (asciinema rec docs/site/demo.cast -c "bash scripts/demo_session.sh") driven by the live binaries, no hand-edited frames — same spirit as everything else here. Next in this series: declaring an ML tagger in the same spec and watching its score become a typed column the region cut can use — codegen for inference.