Skip to content

Instantly share code, notes, and snippets.

@snoble
Last active January 25, 2026 05:03
Show Gist options
  • Select an option

  • Save snoble/8a3adb53e7979aaffb84e8dbd3cb700e to your computer and use it in GitHub Desktop.

Select an option

Save snoble/8a3adb53e7979aaffb84e8dbd3cb700e to your computer and use it in GitHub Desktop.
BurritoScript Tutorial - renderToString demo
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>BurritoScript Tutorial</title><meta name="description" content="Learn BurritoScript by building a recipe app with AI assistance."><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap');
:root {
--bg-deep: #0a0a0f;
--bg-surface: #12121a;
--bg-elevated: #1a1a24;
--accent-primary: #ff6b35;
--accent-secondary: #00d4aa;
--accent-tertiary: #7c3aed;
--text-primary: #fafafa;
--text-secondary: #a0a0b0;
--text-muted: #6a6a7a;
--border: #2a2a3a;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-deep);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
code, pre, .mono {
font-family: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
}
a { color: inherit; text-decoration: none; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 16px 0;
background: rgba(10, 10, 15, 0.95);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
z-index: 100;
}
.nav-inner {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-weight: 800;
font-size: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.logo-icon { font-size: 24px; }
.nav-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-muted);
font-size: 14px;
}
.nav-breadcrumb span { color: var(--text-secondary); }
.tutorial-page {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
padding-top: 60px;
}
.sidebar {
position: sticky;
top: 60px;
height: calc(100vh - 60px);
background: var(--bg-surface);
border-right: 1px solid var(--border);
padding: 32px 24px;
overflow-y: auto;
}
.sidebar-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
letter-spacing: 0.1em;
margin-bottom: 20px;
}
.chapter-list { list-style: none; }
.chapter-item { margin-bottom: 6px; }
.chapter-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
color: var(--text-secondary);
transition: all 0.2s;
min-width: 0;
overflow-wrap: break-word;
word-break: break-word;
}
.chapter-link:hover {
background: var(--bg-elevated);
color: var(--text-primary);
}
.chapter-link.active {
background: var(--accent-primary);
color: var(--bg-deep);
}
.chapter-link.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.chapter-num {
width: 28px;
height: 28px;
background: var(--bg-deep);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
}
.chapter-link.active .chapter-num {
background: rgba(0,0,0,0.2);
}
.chapter-info { flex: 1; min-width: 0; overflow-wrap: break-word; word-break: break-word; }
.chapter-info-title { font-size: 14px; font-weight: 500; overflow-wrap: break-word; word-break: break-word; }
.chapter-info-sub { font-size: 11px; opacity: 0.7; overflow-wrap: break-word; word-break: break-word; }
.chapter-badge {
font-size: 9px;
padding: 2px 6px;
background: var(--bg-deep);
color: var(--text-muted);
border-radius: 4px;
}
.main-content {
padding: 48px 64px;
max-width: 800px;
overflow-wrap: break-word;
word-break: break-word;
}
.chapter-header {
margin-bottom: 48px;
padding-bottom: 32px;
border-bottom: 1px solid var(--border);
}
.chapter-label {
display: inline-block;
padding: 6px 14px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
color: var(--bg-deep);
font-size: 12px;
font-weight: 700;
border-radius: 6px;
margin-bottom: 16px;
}
.chapter-title {
font-size: 42px;
font-weight: 800;
letter-spacing: -0.02em;
margin-bottom: 12px;
overflow-wrap: break-word;
word-break: break-word;
}
.chapter-subtitle {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.chapter-meta {
color: var(--text-muted);
font-size: 14px;
}
.section { margin-bottom: 40px; }
.section-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 16px;
}
.section-text {
font-size: 17px;
line-height: 1.8;
color: var(--text-secondary);
overflow-wrap: break-word;
word-break: break-word;
}
.section-text p { margin-bottom: 16px; overflow-wrap: break-word; word-break: break-word; }
.section-text strong { color: var(--text-primary); }
.section-text code {
background: var(--bg-elevated);
padding: 2px 8px;
border-radius: 4px;
font-size: 15px;
color: var(--accent-secondary);
overflow-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.code-block-wrapper { margin: 28px 0; }
.code-filename {
display: inline-block;
padding: 10px 18px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-bottom: none;
border-radius: 12px 12px 0 0;
font-size: 13px;
color: var(--text-secondary);
overflow-wrap: break-word;
word-break: break-all;
max-width: 100%;
}
.code-block {
position: relative;
background: var(--bg-deep);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.code-filename + .code-block { border-top-left-radius: 0; }
.code-block pre {
margin: 0;
padding: 24px;
overflow-x: auto;
overflow-wrap: break-word;
word-break: break-all;
font-size: 14px;
line-height: 1.7;
color: var(--text-secondary);
}
.copy-btn {
position: absolute;
top: 12px;
right: 12px;
padding: 8px 14px;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover {
background: var(--accent-primary);
color: var(--bg-deep);
border-color: var(--accent-primary);
}
.prompt-block {
margin: 28px 0;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(0, 212, 170, 0.05));
border: 1px solid var(--accent-tertiary);
border-radius: 16px;
overflow: hidden;
}
.prompt-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
background: rgba(124, 58, 237, 0.15);
border-bottom: 1px solid var(--accent-tertiary);
}
.prompt-icon { font-size: 20px; }
.prompt-label {
font-size: 14px;
font-weight: 600;
color: var(--accent-tertiary);
}
.prompt-content {
position: relative;
padding: 20px 24px;
}
.prompt-content pre {
margin: 0;
font-family: inherit;
font-size: 16px;
line-height: 1.7;
color: var(--text-secondary);
white-space: pre-wrap;
}
.output-block { margin: 28px 0; }
.output-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
letter-spacing: 0.1em;
margin-bottom: 8px;
}
.output-content {
background: var(--bg-surface);
border: 1px solid var(--accent-secondary);
border-radius: 12px;
padding: 20px 24px;
}
.output-content pre {
margin: 0;
font-size: 14px;
line-height: 1.7;
color: var(--accent-secondary);
}
.tip-block {
display: flex;
gap: 16px;
margin: 28px 0;
padding: 24px;
background: rgba(255, 107, 53, 0.05);
border: 1px solid rgba(255, 107, 53, 0.2);
border-radius: 12px;
}
.tip-icon { font-size: 28px; flex-shrink: 0; }
.tip-content {
font-size: 16px;
line-height: 1.7;
color: var(--text-secondary);
overflow-wrap: break-word;
word-break: break-word;
}
.tip-title {
font-weight: 600;
color: var(--accent-primary);
margin-bottom: 8px;
}
.summary-list {
list-style: none;
margin-top: 16px;
}
.summary-list li {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
font-size: 16px;
color: var(--text-secondary);
}
.summary-list li::before {
content: '✓';
color: var(--accent-secondary);
font-weight: 600;
}
.chapter-nav {
display: flex;
justify-content: space-between;
margin-top: 64px;
padding-top: 32px;
border-top: 1px solid var(--border);
}
.chapter-nav-btn {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 15px;
transition: all 0.2s;
}
.chapter-nav-btn:hover {
background: var(--bg-elevated);
border-color: var(--accent-primary);
}
.chapter-nav-btn.disabled {
opacity: 0.4;
cursor: not-allowed;
}
.nav-arrow { font-size: 18px; }
/* Hamburger menu button - hidden by default */
.hamburger {
display: none;
flex-direction: column;
position: fixed;
top: 16px;
left: 16px;
z-index: 1001;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
cursor: pointer;
width: 44px;
height: 44px;
align-items: center;
justify-content: center;
}
.hamburger-line {
display: block;
width: 20px;
height: 2px;
background: var(--text-primary);
margin: 3px 0;
}
/* Sidebar overlay for mobile */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
}
/* Tablet: 768px - 900px */
@media (max-width: 900px) {
.tutorial-page { grid-template-columns: 1fr; }
.sidebar {
position: relative;
top: 0;
height: auto;
border-right: none;
border-bottom: 1px solid var(--border);
}
.main-content { padding: 32px 24px; }
.chapter-title { font-size: 32px; }
}
/* Mobile: < 768px */
@media (max-width: 768px) {
.tutorial-page {
grid-template-columns: 1fr;
padding-top: 0;
}
/* Show hamburger */
.hamburger { display: flex; }
/* Sidebar as drawer */
.sidebar {
position: fixed;
top: 0;
left: -280px;
width: 280px;
height: 100vh;
z-index: 100;
transition: left 0.3s ease;
padding-top: 72px;
border-right: 1px solid var(--border);
border-bottom: none;
}
.sidebar.open { left: 0; }
.sidebar-overlay.visible { display: block; }
/* Touch-friendly chapter links (44px min) */
.chapter-link {
padding: 16px;
min-height: 44px;
}
.chapter-info-title { font-size: 15px; }
.chapter-info-sub { font-size: 12px; }
/* Main content adjustments */
.main-content {
padding: 24px 16px;
padding-top: 72px;
}
.chapter-title { font-size: 28px; }
.chapter-subtitle { font-size: 16px; }
/* Code blocks horizontal scroll */
.code-block pre {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
max-width: calc(100vw - 32px);
overflow-wrap: break-word;
word-break: break-all;
}
/* Prevent text overflow in sidebar */
.chapter-info {
min-width: 0;
overflow-wrap: break-word;
word-break: break-word;
}
/* Prevent title overflow */
.chapter-title {
overflow-wrap: break-word;
word-break: break-word;
}
/* Section spacing */
.section { margin-bottom: 32px; }
.section-title { font-size: 20px; }
/* Navigation buttons */
.chapter-nav { padding: 24px 16px; gap: 12px; }
.chapter-nav-btn {
padding: 14px 20px;
font-size: 14px;
min-height: 44px;
}
/* Tip boxes */
.tip-box { padding: 16px; }
/* Output blocks */
.output-content { font-size: 12px; }
}
/* Very small screens (iPhone SE) */
@media (max-width: 375px) {
.main-content { padding: 16px 12px; padding-top: 72px; }
.chapter-title { font-size: 24px; }
.chapter-nav-btn { padding: 12px 16px; }
}
.chapter-wrapper {
margin-bottom: 80px;
padding-bottom: 40px;
border-bottom: 2px solid var(--border);
}
.chapter-wrapper:last-child {
border-bottom: none;
}
.explorer-placeholder {
padding: 40px;
background: var(--bg-elevated);
border: 1px dashed var(--border);
border-radius: 12px;
text-align: center;
margin: 28px 0;
}
.explorer-icon { font-size: 32px; margin-bottom: 12px; }
.explorer-text { color: var(--text-muted); font-size: 14px; }
/* Try it callout */
.try-it-block {
margin: 28px 0;
background: linear-gradient(135deg, rgba(0, 212, 170, 0.1), rgba(124, 58, 237, 0.05));
border: 1px solid var(--accent-secondary);
border-radius: 16px;
overflow: hidden;
}
.try-it-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
background: rgba(0, 212, 170, 0.15);
border-bottom: 1px solid var(--accent-secondary);
}
.try-it-icon { font-size: 20px; }
.try-it-label {
font-size: 14px;
font-weight: 600;
color: var(--accent-secondary);
}
.try-it-content {
padding: 20px 24px;
font-size: 16px;
line-height: 1.7;
color: var(--text-secondary);
overflow-wrap: break-word;
word-break: break-word;
}
/* Demo block (side-by-side code + output) */
.demo-block {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin: 28px 0;
}
@media (max-width: 900px) {
.demo-block {
grid-template-columns: 1fr;
}
}
.demo-code { }
.demo-output {
display: flex;
flex-direction: column;
}
.demo-output-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
letter-spacing: 0.1em;
margin-bottom: 8px;
}
.demo-output-content {
background: var(--bg-surface);
border: 1px solid var(--accent-secondary);
border-radius: 12px;
padding: 20px 24px;
flex: 1;
}
.demo-output-content pre {
margin: 0;
font-size: 14px;
line-height: 1.7;
color: var(--accent-secondary);
}
/* Lesson divider */
.lesson-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 48px 0 32px;
padding-bottom: 16px;
border-bottom: 2px solid var(--border);
}
.lesson-badge {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
color: var(--bg-deep);
flex-shrink: 0;
}
.lesson-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
</style></head><body><button class="hamburger" aria-label="Toggle navigation"><span class="hamburger-line"></span><span class="hamburger-line"></span><span class="hamburger-line"></span></button><nav><div class="container nav-inner"><a href="../demos/brochure.html" class="logo"><span class="logo-icon">🪼</span>BurritoScript</a><div class="nav-breadcrumb"><span>Tutorial</span></div></div></nav><div class="sidebar-overlay"></div><div class="tutorial-page"><aside class="sidebar"><div class="sidebar-title">Chapters</div><ul class="chapter-list"><li class="chapter-item"><a class="chapter-link" href="#burritoscript-programs"><span class="chapter-num">1</span><span class="chapter-info"><span class="chapter-info-title">BurritoScript Programs</span><span class="chapter-info-sub">Write async/await TypeScript. Get automatic tracing and analysis.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#burrito-service"><span class="chapter-num">2</span><span class="chapter-info"><span class="chapter-info-title">BurritoService</span><span class="chapter-info-sub">Define service handlers with declarative safety and resilience config.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#burrito-ui"><span class="chapter-num">3</span><span class="chapter-info"><span class="chapter-info-title">BurritoUI</span><span class="chapter-info-sub">Build reactive UIs that update in O(1) time—no virtual DOM diffing.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#mission-apps"><span class="chapter-num">4</span><span class="chapter-info"><span class="chapter-info-title">Mission Apps</span><span class="chapter-info-sub">Declare your data model. Get a GraphQL API automatically.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#simulation-applets"><span class="chapter-num">5</span><span class="chapter-info"><span class="chapter-info-title">Simulation Applets</span><span class="chapter-info-sub">Build real-time physics and economics simulations with canvas.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#provenance"><span class="chapter-num">6</span><span class="chapter-info"><span class="chapter-info-title">Provenance</span><span class="chapter-info-sub">Trace any value back to its source. Debug by asking "why?"</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#verification"><span class="chapter-num">7</span><span class="chapter-info"><span class="chapter-info-title">Verification</span><span class="chapter-info-sub">Prove your concurrent code is race-free. Test all interleavings.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#batching"><span class="chapter-num">8</span><span class="chapter-info"><span class="chapter-info-title">Automatic Batching</span><span class="chapter-info-sub">Eliminate N+1 queries. Zero boilerplate.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#service-ui"><span class="chapter-num">9</span><span class="chapter-info"><span class="chapter-info-title">Service + UI</span><span class="chapter-info-sub">Build full-stack features with reactive data binding.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#mission-service"><span class="chapter-num">10</span><span class="chapter-info"><span class="chapter-info-title">Mission + Service</span><span class="chapter-info-sub">Data models that generate type-safe service APIs.</span></span></a></li><li class="chapter-item"><a class="chapter-link" href="#complete-app"><span class="chapter-num">11</span><span class="chapter-info"><span class="chapter-info-title">Complete Application</span><span class="chapter-info-sub">MoneyFlow: Everything you've learned, working together.</span></span></a></li></ul></aside><main class="main-content"><div class="chapter-wrapper"><div class="chapter-header" id="burritoscript-programs"><div class="chapter-label">Chapter 1</div><h1 class="chapter-title">BurritoScript Programs</h1><p class="chapter-subtitle">Write async/await TypeScript. Get automatic tracing and analysis.</p><div class="chapter-meta">⏱️ ~18 min read</div></div><div class="section"><h3 class="section-title">What is BurritoScript?</h3><div class="section-text"><p>Here's the thing: you don't need to learn a new language. BurritoScript is just TypeScript—the same async/await code you already write. The difference? When AI generates this code for you, BurritoScript can automatically tell you if it's wired correctly. No reading required.</p><p><strong>BurritoScript is TypeScript designed for AI-assisted development.</strong> You describe what you want, an AI writes the code, and BurritoScript gives you the tools to verify it works—without reading every line.</p><p>It's not magic. It's architecture. By restricting TypeScript to a specific pattern (async/await with explicit effect interfaces), BurritoScript can automatically track where every value comes from, detect race conditions, and optimize performance.</p></div></div><div class="section"><h3 class="section-title">Why does this matter?</h3><div class="section-text"><p>When AI writes your code, you have two choices: read every line carefully (slow, defeats the purpose), or trust blindly (dangerous). BurritoScript gives you a third option: <strong>verification tools</strong>.</p><p>Run <code>burrito trace</code> to see where values came from. Run <code>burrito infra race</code> with an invariant to test concurrent code for race conditions. Run <code>burrito service analyze</code> to find N+1 queries. The tools tell you if the code is correct.</p><p><strong>Here's why this makes AI more likely to succeed:</strong> When AI generates code, you can give it concrete feedback. "The trace shows this value comes from the wrong source—fix it." The AI iterates with precise, tool-backed feedback instead of vague "this doesn't work" messages. <strong>Verification tools make AI-assisted development iterative and reliable.</strong></p><p><strong>You tell the agent what to build. BurritoScript tells you if the agent built it correctly.</strong></p></div></div><div class="section"><h3 class="section-title">Who is this for?</h3><div class="section-text"><p>If you're using AI to write code and want confidence without line-by-line review—this is for you.</p><p>In the next 5 minutes, you'll ask an AI to write a BurritoScript function. Then you'll run one command to see exactly where every value came from. No logging. No manual instrumentation. Just ask "where did this come from?" and get a precise answer.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Programs vs Services</div><p>BurritoScript has two main application types:</p><p><strong>BurritoProgram</strong> (this chapter): Runs → produces output → exits. Like a CLI tool or a script. You give it input, it does work, it returns a result.</p><p><strong>BurritoService</strong> (next chapter): Stays alive and responds to requests. Like a web server or background daemon. It starts, waits for input, handles requests, and keeps running.</p><p>This chapter covers Programs. Chapter 2 covers Services.</p></div></div><div class="section"><h3 class="section-title">Your First Program: Temperature Converter</h3><div class="section-text"><p>Let's start with something simple. We'll build a temperature converter that fetches the current temperature and converts it to Fahrenheit.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoScript program that fetches weather and converts temperature. Use effect interfaces."</p><p>Did it produce something like this?</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">temp-converter.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">// Effect interfaces - declare what external operations we need
interface WeatherAPI {
getTemperature(city: string): Promise&lt;number&gt;
}
interface Converter {
toFahrenheit(celsius: number): Promise&lt;number&gt;
}
// The function - just normal async/await
export async function convertTemp(
weather: WeatherAPI,
converter: Converter
): Promise&lt;string&gt; {
const celsius = await weather.getTemperature("Paris")
const fahrenheit = await converter.toFahrenheit(celsius)
return `${celsius}°C = ${fahrenheit}°F`
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="try-it-block"><div class="try-it-header"><span class="try-it-icon">🤖</span><span class="try-it-label">Try it</span></div><div class="try-it-content"><p><strong>Verify it:</strong> Run <code>burrito trace temp-converter.burrito.ts</code>. See how every value traces back to its source? That's how you verify AI-generated code without reading it line by line.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Effect Interfaces: Why They Exist</div><p>See those interfaces at the top? <code>WeatherAPI</code> and <code>Converter</code>? They're contracts that say "my code needs these things to work."</p><p>Here's why this matters for AI: when you tell an AI "use effect interfaces for external calls," you're forcing the AI to be explicit about side effects. No hidden database calls. No surprise network requests. Everything that reaches outside the function is declared upfront—which makes verification possible.</p><p>The actual implementations come from a config file. This separation is what gives BurritoScript its superpowers.</p></div></div><div class="section"><div class="section-text"><p>Now we need a config file that provides mock implementations:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">temp-converter.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
weather: {
async getTemperature(city: string) {
// Mock: return 22°C for any city
return 22
}
},
converter: {
async toFahrenheit(celsius: number) {
return celsius * 9/5 + 32
}
}
}
export const entryFunction = 'convertTemp'</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Run it with the BurritoScript CLI:</p></div></div><div class="demo-block"><div class="demo-code"><div class="code-block-wrapper"><div class="code-filename mono">Run it</div><div class="code-block"><pre><code class="language-bash">pnpm burrito run temp-converter.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div></div><div class="demo-output"><div class="demo-output-label">Output</div><div class="demo-output-content"><pre class="mono">"22°C = 71.6°F"</pre></div></div></div><div class="section"><h3 class="section-title">Verify Without Reading: Automatic Tracing</h3><div class="section-text"><p>Here's where BurritoScript differs from regular TypeScript. Run the same code with <code>trace</code>:</p></div></div><div class="demo-block"><div class="demo-code"><div class="code-block-wrapper"><div class="code-filename mono">Verify it</div><div class="code-block"><pre><code class="language-bash">pnpm burrito trace temp-converter.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div></div><div class="demo-output"><div class="demo-output-label">Output</div><div class="demo-output-content"><pre class="mono">PROVENANCE TRACE
════════════════════════════════════════════════════════════
Total nodes: 4
Result node: n3
Provenance Graph:
────────────────────────────────────────
n0: celsius ← weather.getTemperature("Paris") temp-converter.burrito.ts:15
→ used by: n1, n3
n1: fahrenheit ← converter.toFahrenheit(22) temp-converter.burrito.ts:16
→ used by: n3
n3: result ← pure temp-converter.burrito.ts:17 ← RESULT
Every value traced to its source.</pre></div></div></div><div class="section"><div class="section-text"><p><strong>Every value can be traced back to its origin.</strong> The <code>celsius</code> value came from <code>weather.getTemperature("Paris")</code>. The <code>fahrenheit</code> value came from <code>converter.toFahrenheit(22)</code>.</p><p>This is the first superpower: <strong>provenance</strong>. When something goes wrong, you can ask "where did this value come from?" and get a precise answer.</p><p><strong>This is how you check AI-generated code.</strong> You don't need to read the code to understand the data flow. Just run <code>burrito trace</code> and see the complete dependency graph.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>You wrote normal async/await TypeScript. BurritoScript tracked both service calls automatically. No logging. No manual instrumentation.</p><p>When AI generates this code, you verify it the same way: run <code>burrito trace</code> and check the output. If the provenance graph shows the expected data flow, the code is correct—no reading required.</p></div></div><div class="section"><h3 class="section-title">Building MoneyFlow: Check Balance</h3><div class="section-text"><p>Now let's apply what we learned to a real app. Throughout this tutorial, we'll build <strong>MoneyFlow</strong>—a personal finance tracker.</p><ul class="summary-list"><li><strong>What we'll build over 11 chapters:</strong></li><li>Accounts, transfers, and balance tracking (Services)</li><li>A reactive dashboard with surgical updates (UI)</li><li>A data model with transactions and categories (Mission)</li><li>Provenance debugging when calculations go wrong</li><li>Verification that concurrent transfers are race-free</li><li>Automatic batching for monthly statements</li></ul><p>Let's start with the simplest feature: checking an account balance.</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">get-balance.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">// Effect interface for our banking service
interface BankService {
getAccount(accountId: string): Promise&lt;Account&gt;
}
interface Account {
id: string
name: string
type: 'checking' | 'savings' | 'investment'
balance: number
}
// Simple function to get balance
export async function getBalance(bank: BankService): Promise&lt;number&gt; {
const account = await bank.getAccount("checking")
return account.balance
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-filename mono">get-balance.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
bank: {
async getAccount(accountId: string) {
// Mock data for development
const accounts: Record&lt;string, any&gt; = {
'checking': {
id: 'checking',
name: 'Main Checking',
type: 'checking',
balance: 1250.00
},
'savings': {
id: 'savings',
name: 'Emergency Fund',
type: 'savings',
balance: 5000.00
}
}
return accounts[accountId] || null
}
}
}
export const entryFunction = 'getBalance'</code></pre><button class="copy-btn">Copy</button></div></div><div class="demo-block"><div class="demo-code"><div class="code-block-wrapper"><div class="code-filename mono">Trace it</div><div class="code-block"><pre><code class="language-bash">pnpm burrito trace get-balance.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div></div><div class="demo-output"><div class="demo-output-label">Output</div><div class="demo-output-content"><pre class="mono">PROVENANCE TRACE
════════════════════════════════════════════════════════════
Total nodes: 2
Result node: n1
Provenance Graph:
────────────────────────────────────────
n0: account ← bank.getAccount("checking") get-balance.burrito.ts:14
→ { id: "checking", name: "Main Checking", balance: 1250.00 }
n1: balance ← account.balance get-balance.burrito.ts:15 ← RESULT
→ 1250.00
Result: 1250</pre></div></div></div><div class="section"><div class="section-text"><p>This is the first piece of MoneyFlow. Simple, but already:</p><ul class="summary-list"><li>The <code>BankService</code> interface declares exactly what operations we need</li><li>Mock implementations let us develop without a real database</li><li>Tracing shows us exactly where the balance value came from</li></ul><p><strong>Try it:</strong> Ask your AI to write this same function. Then run <code>burrito trace</code> and verify the provenance graph matches what you expected. That's the verification workflow.</p><p>In the next chapter, we'll add the ability to <strong>transfer money between accounts</strong>—and see how BurritoScript keeps that safe.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>Our finance app starts with a simple balance check. BurritoScript tracks every operation, so we can trace any value back to its source.</p><p>When AI generates this code, you verify it the same way: check the provenance trace. If the data flow is correct, the code is correct—no reading required.</p></div></div><div class="section"><h3 class="section-title">The Pattern: Describe → Generate → Verify</h3><div class="section-text"><p>Here's the workflow you'll use throughout this tutorial:</p><p>1. <strong>Describe</strong> what you want → Tell your AI what to build 2. <strong>Generate</strong> → AI writes the code 3. <strong>Verify</strong> → Run <code>burrito trace</code>, <code>burrito service analyze</code>, or <code>burrito infra race</code> to check correctness 4. <strong>Ship</strong> → Confidence without reading every line</p><p>This pattern works because BurritoScript's architecture makes verification possible. Effect interfaces force explicit side effects. Static analysis tracks dependencies. Provenance traces every value.</p><p><strong>You don't need to understand the code to verify it.</strong> The tools tell you if it's correct.</p><p>In Chapter 11, we'll see the complete development loop with all the verification tools working together.</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>In this chapter, you learned two BurritoScript CLI commands:</p><ul class="summary-list"><li><code>burrito run &lt;file&gt;</code> - Execute a BurritoScript file with mock implementations</li><li><code>burrito trace &lt;file&gt;</code> - Run with provenance tracking to see where every value comes from</li></ul><p>Both commands automatically find and load the corresponding <code>.config.ts</code> file.</p></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Programs run and exit</strong>: Give input, get output, done</li><li><strong>Effect interfaces</strong> declare what external operations your code needs</li><li><strong>Config files</strong> provide the actual implementations (mocks for dev, real services for prod)</li><li><strong>Provenance tracing</strong> shows exactly where every value came from</li><li>BurritoScript is just TypeScript—no special syntax, just async/await</li><li><strong>The verification workflow</strong>: Describe → Generate → Verify → Ship</li></ul><p>Next up: <strong>BurritoService</strong>—where we'll build services that stay alive and respond to requests, with safety rules and resilience handled automatically.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="burrito-service"><div class="chapter-label">Chapter 2</div><h1 class="chapter-title">BurritoService</h1><p class="chapter-subtitle">Define service handlers with declarative safety and resilience config.</p><div class="chapter-meta">⏱️ ~20 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>In Chapter 1, we wrote <strong>Programs</strong>—functions that run, produce output, and exit. Now we're building <strong>Services</strong>—long-lived processes that stay alive and respond to requests.</p><ul class="summary-list"><li>Think of the difference:</li><li><strong>Program</strong>: A CLI tool that converts a file and exits</li><li><strong>Service</strong>: A web server that handles thousands of requests over days</li></ul><p>BurritoService is your platform for building daemons, APIs, and background workers. It handles the infrastructure concerns so your business logic stays clean.</p></div></div><div class="section"><h3 class="section-title">The Two-File Pattern: Why It Exists</h3><div class="section-text"><p>Here's the thing: the two-file pattern isn't just clean code—it's optimal for AI.</p><p><strong>One file per concern = one prompt per file.</strong> When you ask an AI to write business logic, it doesn't need to think about retries, timeouts, or circuit breakers. When you ask it to configure safety, it doesn't need to understand the business logic.</p><p><strong>Why this helps AI succeed:</strong> The agent never needs to hold both concerns in mind simultaneously. Smaller context = fewer mistakes. When context is focused, AI can get it right the first time. When context is cluttered, AI mixes concerns and makes errors.</p><p><strong>Business logic</strong> (<code>.burrito.ts</code>): What the code does. Clean, readable, testable.</p><p><strong>Infrastructure config</strong> (<code>.config.ts</code>): How it runs safely. Retries, timeouts, transactions, circuit breakers.</p><p>Two different concerns. Two different files. Two different prompts. <strong>The architecture keeps context small, which keeps AI focused and accurate.</strong></p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Service Lifecycle</div><p>A BurritoService:</p><p>1. <strong>Starts up</strong> and initializes resources (database connections, etc.) 2. <strong>Stays alive</strong> waiting for requests 3. <strong>Handles requests</strong> by routing them to your handler functions 4. <strong>Applies safety rules</strong> (retries, timeouts, circuit breakers) automatically 5. <strong>Keeps running</strong> until you shut it down</p><p>You write the handlers. BurritoService handles the lifecycle, routing, and resilience.</p></div></div><div class="section"><div class="section-text"><p>Real services need more than just handlers: <strong>retries</strong> when things fail, <strong>timeouts</strong> so requests don't hang forever, <strong>transactions</strong> so operations are atomic.</p><p>The traditional approach? Wrap your code in try/catch, add retry loops, configure timeouts. Your clean business logic becomes a mess of infrastructure concerns.</p><p>BurritoService solves this with <strong>separation of concerns</strong>: business logic in one file, safety rules in another. Neither touches the other.</p></div></div><div class="section"><h3 class="section-title">Try It: Ask AI for a Payment Handler</h3><div class="section-text"><p>Let's build a payment handler that charges a credit card. This involves two steps: authorize the payment, then capture it.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoService handler that charges a credit card. Use a PaymentGateway interface with authorize and capture methods. Just the business logic—no retry or timeout handling."</p><p>Did it produce something like this?</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">payment.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">// Effect interface for payment gateway
interface PaymentGateway {
authorize(amount: number): Promise&lt;{ id: string; status: string }&gt;
capture(authId: string): Promise&lt;{ id: string; status: string }&gt;
}
interface Receipt {
authId: string
captureId: string
amount: number
}
// Business logic only - no retry loops, no timeouts
export async function chargeCard(
payment: PaymentGateway,
amount: number
): Promise&lt;Receipt&gt; {
const auth = await payment.authorize(amount)
const capture = await payment.capture(auth.id)
return { authId: auth.id, captureId: capture.id, amount }
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Notice what's <strong>not</strong> in that code: no try/catch for retries, no timeout handling, no transaction wrapping. Just the core logic.</p><p>Now the config file adds safety without touching the business logic:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">payment.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
payment: {
async authorize(amount: number) {
// Mock: always succeeds
return { id: 'auth_' + Math.random().toString(36).slice(2), status: 'approved' }
},
async capture(authId: string) {
return { id: 'cap_' + Math.random().toString(36).slice(2), status: 'captured' }
}
}
}
export const entryFunction = 'chargeCard'
// Safety and resilience configuration
export const handlers = {
chargeCard: {
safety: {
concurrency: 'transaction', // Both operations atomic
idempotencyKey: 'args[1]' // Dedupe by amount
},
resilience: {
retry: 3, // Retry up to 3 times
timeout: 10000, // 10 second timeout
circuitBreaker: {
threshold: 5, // Open after 5 failures
resetAfter: 30000 // Try again after 30s
}
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="try-it-block"><div class="try-it-header"><span class="try-it-icon">🤖</span><span class="try-it-label">Try it</span></div><div class="try-it-content"><p><strong>Verify it:</strong> Run <code>burrito service analyze payment.burrito.ts</code>. The analyzer shows I/O patterns and detects that you have a two-phase operation (authorize → capture). It confirms your config provides transaction wrapping and retry policies.</p><p>This is how you verify AI-generated service code: check the analysis output.</p></div></div><div class="demo-block"><div class="demo-code"><div class="code-block-wrapper"><div class="code-filename mono">Verify it</div><div class="code-block"><pre><code class="language-bash">pnpm burrito service analyze payment.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div></div><div class="demo-output"><div class="demo-output-label">Output</div><div class="demo-output-content"><pre class="mono">SERVICE ANALYSIS: chargeCard
════════════════════════════════════════════════════════════
I/O Operations:
1. payment.authorize(amount) - WRITE
2. payment.capture(auth.id) - WRITE
Detected Patterns:
✓ Two-phase operation (authorize → capture)
✓ Config provides transaction wrapping
✓ Retry policy: 3 attempts with backoff
✓ Circuit breaker configured
Suggestions:
• Consider adding fallback for circuit breaker open state</pre></div></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>Business logic is one file. Safety rules are another. The <code>chargeCard</code> function stays clean—it doesn't know about retries, timeouts, or circuit breakers. That's all in config.</p><p><strong>AI wrote the logic. You added infrastructure in config.</strong> Two prompts, two files, clean separation.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Transfer Money</h3><div class="section-text"><p>Now let's add the most important feature to MoneyFlow: <strong>transferring money between accounts</strong>.</p><p>This is where things get interesting. A transfer involves multiple operations (check balance, withdraw, deposit) that must happen atomically. If the deposit fails after the withdrawal, we have a problem.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoService handler that transfers money between accounts. Use a BankService interface with getAccount, withdraw, and deposit methods. Just the business logic—no concurrency handling."</p><p>Let's see what it produces:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">transfer.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface BankService {
getAccount(accountId: string): Promise&lt;Account&gt;
withdraw(accountId: string, amount: number): Promise&lt;void&gt;
deposit(accountId: string, amount: number): Promise&lt;void&gt;
}
interface Account {
id: string
name: string
balance: number
}
interface TransferReceipt {
from: string
to: string
amount: number
timestamp: number
}
export async function transfer(
bank: BankService,
from: string,
to: string,
amount: number
): Promise&lt;TransferReceipt&gt; {
// Check if source account has sufficient funds
const fromAccount = await bank.getAccount(from)
if (fromAccount.balance &lt; amount) {
throw new Error(`Insufficient funds: ${fromAccount.balance} &lt; ${amount}`)
}
// Perform the transfer
await bank.withdraw(from, amount)
await bank.deposit(to, amount)
return { from, to, amount, timestamp: Date.now() }
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>This code looks clean, but it has a <strong>race condition</strong>. What if two transfers run at the same time? Both might see sufficient funds, both withdraw, and we've lost money.</p><p><strong>Here's where the two-file pattern shines:</strong> The AI wrote the business logic. Now you add safety config without touching the code.</p><p><strong>Try it:</strong> Ask your AI: "Add safety config for the transfer handler: serializable concurrency, 3 retries, 5-second timeout."</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">transfer.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
bank: {
// In-memory mock accounts
accounts: {
'checking': { id: 'checking', name: 'Main Checking', balance: 1250.00 },
'savings': { id: 'savings', name: 'Emergency Fund', balance: 5000.00 }
},
async getAccount(accountId: string) {
return this.accounts[accountId] || null
},
async withdraw(accountId: string, amount: number) {
this.accounts[accountId].balance -= amount
},
async deposit(accountId: string, amount: number) {
this.accounts[accountId].balance += amount
}
}
}
export const entryFunction = 'transfer'
// This is the key: serializable concurrency prevents race conditions
export const handlers = {
transfer: {
safety: {
concurrency: 'serializable' // Only one transfer at a time
},
resilience: {
retry: 3,
timeout: 5000
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Concurrency Modes</div><p>The <code>concurrency</code> setting controls how concurrent calls are handled:</p><ul class="summary-list"><li><code>'none'</code> - No protection (default)</li><li><code>'serializable'</code> - Only one call at a time (prevents race conditions)</li><li><code>'transaction'</code> - All operations in a database transaction</li></ul><p>For money transfers, <code>'serializable'</code> ensures we never have two transfers checking balances simultaneously.</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito trace transfer.burrito.ts -- checking savings 100</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Transfer Output</div><div class="output-content"><pre class="mono">PROVENANCE TRACE
════════════════════════════════════════════════════════════
Total nodes: 5
Result node: n4
Provenance Graph:
────────────────────────────────────────
n0: fromAccount ← bank.getAccount("checking") transfer.burrito.ts:20
→ { id: "checking", balance: 1250.00 }
n1: (balance check passed) transfer.burrito.ts:22
n2: ← bank.withdraw("checking", 100) transfer.burrito.ts:26
n3: ← bank.deposit("savings", 100) transfer.burrito.ts:27
n4: receipt ← pure transfer.burrito.ts:29 ← RESULT
→ { from: "checking", to: "savings", amount: 100 }
Result: { from: "checking", to: "savings", amount: 100, timestamp: 1706140800000 }</pre></div></div><div class="section"><div class="section-text"><p>MoneyFlow can now transfer money between accounts. The business logic is clean—just check balance, withdraw, deposit. The config handles:</p><ul class="summary-list"><li><strong>Serializable concurrency</strong> - No race conditions</li><li><strong>Retries</strong> - Automatic retry on transient failures</li><li><strong>Timeout</strong> - Won't hang forever</li></ul><p><strong>Verify it:</strong> Run <code>burrito service analyze transfer.burrito.ts</code> to see the I/O patterns. In Chapter 7, we'll <strong>prove</strong> this transfer is race-condition free by testing all possible execution orders.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow can transfer money safely. The <code>transfer</code> function doesn't know about concurrency control—that's in config. Business logic stays clean.</p><p><strong>The pattern:</strong> AI writes business logic. You add safety config. Two prompts, two files, verified with analysis tools.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often mix concerns.</strong> Watch for:</p><p>1. <strong>Retry loops in business logic</strong> - If you see try/catch wrapping service calls for retry purposes, ask for it in config instead. The AI should write clean business logic, not infrastructure.</p><p>2. <strong>Missing concurrency config</strong> - For operations that read-then-write (like transfers), always add <code>concurrency: 'serializable'</code>. The analyzer will suggest this, but it's easy to miss.</p><p>3. <strong>Wrong concurrency mode</strong> - <code>'transaction'</code> is for database atomicity, <code>'serializable'</code> is for preventing race conditions. Make sure the AI uses the right one.</p><p><strong>The fix:</strong> Show the AI the analysis output. "The analyzer detected a read-then-write pattern. Add serializable concurrency to the config."</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>In this chapter, you learned new BurritoScript service commands:</p><ul class="summary-list"><li><code>burrito service analyze &lt;file&gt;</code> - Show I/O operations, patterns, and suggestions</li><li><code>burrito service validate &lt;file&gt;</code> - Check BurritoScript syntax and config validity</li><li><code>burrito service serve &lt;file&gt;</code> - Run as an HTTP endpoint (for production)</li></ul><p>The config file supports these properties:</p><ul class="summary-list"><li><code>handlers.&lt;name&gt;.safety</code> - Concurrency control, idempotency</li><li><code>handlers.&lt;name&gt;.resilience</code> - Retry, timeout, circuit breaker</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Services stay alive</strong>: Unlike Programs (run → exit), Services wait for requests and keep running</li><li><strong>Separation of concerns</strong>: Business logic in <code>.burrito.ts</code>, safety rules in <code>.config.ts</code></li><li><strong>Concurrency modes</strong>: <code>'serializable'</code> prevents race conditions</li><li><strong>Resilience config</strong>: Retries, timeouts, and circuit breakers—without cluttering your code</li><li>The business logic function never knows about infrastructure concerns</li><li><strong>The two-file pattern is optimal for AI</strong>: One prompt per file, smaller context, better results</li></ul><ul class="summary-list"><li><strong>When to use which:</strong></li><li>Use <strong>BurritoProgram</strong> (Chapter 1) for scripts, CLI tools, one-shot tasks</li><li>Use <strong>BurritoService</strong> (this chapter) for APIs, servers, background workers</li></ul><p>Next up: <strong>BurritoUI</strong>—where we'll build a dashboard for MoneyFlow using reactive components.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="burrito-ui"><div class="chapter-label">Chapter 3</div><h1 class="chapter-title">BurritoUI</h1><p class="chapter-subtitle">Build reactive UIs that update in O(1) time—no virtual DOM diffing.</p><div class="chapter-meta">⏱️ ~22 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>React popularized the idea of declarative UI: describe what you want, let the framework figure out how to update the DOM. But React's approach has a cost—<strong>virtual DOM diffing</strong>. Every state change requires walking the entire component tree to find what changed.</p><p>BurritoUI takes a different approach: <strong>static analysis</strong> determines at build time which DOM nodes depend on which state. When state changes, updates go directly to those nodes. No diffing. No tree walking. O(1) updates.</p><p>You'll write components as async functions, and the framework handles the rest.</p><p><strong>Here's why this matters for AI:</strong> When AI generates UI code, the most common bugs are stale closures and missing dependencies. BurritoUI makes that impossible—dependencies are discovered automatically via static analysis. You verify the code by running <code>burrito ui analyze</code>, not by reading it.</p></div></div><div class="section"><h3 class="section-title">Mini-App: Click Counter</h3><div class="section-text"><p>Let's build a simple counter. Components are async functions that read state and return VNodes.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoUI counter component. Use state.read() to get the count, and action() for the increment button. The component should be an async function."</p><p>Did it produce something like this?</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">counter.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">import { h, text, action } from 'burritoscript/ui'
import type { VNode } from 'burritoscript/ui'
interface State {
read&lt;T&gt;(path: string[]): Promise&lt;T&gt;
}
// Component: async function that reads state and returns VNode
export async function Counter(state: State): Promise&lt;VNode&gt; {
const count = await state.read&lt;number&gt;(['count'])
return h('div', { className: 'counter' }, [
h('span', { id: 'count-display' }, [text(`Count: ${count}`)]),
h('button', { onClick: action('increment') }, [text('+')])
])
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-filename mono">counter.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const handlers = {
increment: (state: { count: number }) =&gt; ({
...state,
count: state.count + 1
})
}
export const initialState = { count: 0 }</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Run it with the CLI:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito ui preview counter.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="try-it-block"><div class="try-it-header"><span class="try-it-icon">🤖</span><span class="try-it-label">Try it</span></div><div class="try-it-content"><p><strong>Verify it:</strong> Run <code>burrito ui analyze counter.burrito.ts</code>. The analyzer shows exactly which DOM nodes depend on which state. If the dependencies match what you expected, the code is correct—no reading required.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">The Key Insight: Static Analysis</div><p>When the CLI processes your component, it analyzes the async function and discovers:</p><ul class="summary-list"><li>The component reads <code>['count']</code> from state</li><li>That value is used in the span with id <code>count-display</code></li><li>Therefore: when <code>count</code> changes, update that span's text</li></ul><p>No runtime tree walking. No virtual DOM. Just: "count changed → update that span."</p><p><strong>You never call analysis functions directly</strong>—the CLI handles this when you run <code>burrito ui preview</code> or <code>burrito ui analyze</code>.</p></div></div><div class="section"><div class="section-text"><p>Compare this to React:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">React equivalent (for comparison)</div><div class="code-block"><pre><code class="language-typescript">// React: runtime diffing
function Counter() {
const [count, setCount] = useState(0)
return (
&lt;div className="counter"&gt;
&lt;span&gt;{count}&lt;/span&gt; {/* React doesn't know this depends on count */}
&lt;button onClick={() =&gt; setCount(c =&gt; c + 1)}&gt;+&lt;/button&gt;
&lt;/div&gt;
)
}
// When count changes: re-render entire component, diff old vs new, update DOM</code></pre></div></div><div class="section"><div class="section-text"><p>React re-renders the entire component and diffs the result. BurritoUI knows exactly which DOM node to update before the code even runs.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>Components are async functions that read state and return VNodes. The framework analyzes them at build time to determine DOM bindings. Updates are O(1)—no diffing.</p><p><strong>When AI generates this code, you verify it the same way:</strong> Run <code>burrito ui analyze</code> and check the dependencies. If they match what you expected, the code is correct.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Account Dashboard</h3><div class="section-text"><p>Now let's build a dashboard for MoneyFlow. It shows a list of accounts on the left, and details for the selected account on the right.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoUI dashboard component with an account list and detail view. Use state.read() for accounts and selectedId. Use action() for selecting accounts."</p><p>This is a more realistic component with multiple state dependencies:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">dashboard.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">import { h, text, fragment, action } from 'burritoscript/ui'
import type { VNode } from 'burritoscript/ui'
interface Account {
id: string
name: string
type: string
balance: number
}
interface State {
read&lt;T&gt;(path: string[]): Promise&lt;T&gt;
}
// Dashboard component - reads multiple pieces of state
export async function Dashboard(state: State): Promise&lt;VNode&gt; {
const accounts = await state.read&lt;Account[]&gt;(['accounts'])
const selectedId = await state.read&lt;string | null&gt;(['selectedId'])
const selected = accounts.find(a =&gt; a.id === selectedId)
return h('div', { className: 'dashboard' }, [
// Account list (left sidebar)
h('aside', { className: 'account-list' }, [
h('h2', {}, [text('Accounts')]),
fragment(accounts.map(account =&gt;
h('div', {
className: `account-item ${selectedId === account.id ? 'selected' : ''}`,
onClick: action('selectAccount', { id: account.id })
}, [
h('span', { className: 'account-name' }, [text(account.name)]),
h('span', { className: 'account-balance' }, [
text(`$${account.balance.toFixed(2)}`)
])
])
))
]),
// Account detail (main area)
h('main', { className: 'account-detail', id: 'detail-pane' }, [
selected
? fragment([
h('h1', {}, [text(selected.name)]),
h('p', { className: 'account-type' }, [text(selected.type)]),
h('div', { className: 'balance-display' }, [
h('span', { className: 'balance-label' }, [text('Balance')]),
h('span', { className: 'balance-amount' }, [
text(`$${selected.balance.toFixed(2)}`)
])
])
])
: h('p', { className: 'no-selection' }, [text('Select an account')])
])
])
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>The handlers go in a separate config file:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">dashboard.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const handlers = {
selectAccount: (state, payload: { id: string }) =&gt; ({
...state,
selectedId: payload.id
})
}
export const initialState = {
accounts: [
{ id: 'checking', name: 'Main Checking', type: 'checking', balance: 1250.00 },
{ id: 'savings', name: 'Emergency Fund', type: 'savings', balance: 5000.00 },
{ id: 'investment', name: 'Retirement', type: 'investment', balance: 25000.00 }
],
selectedId: null
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="try-it-block"><div class="try-it-header"><span class="try-it-icon">🤖</span><span class="try-it-label">Try it</span></div><div class="try-it-content"><p><strong>Verify it:</strong> Run <code>burrito ui analyze dashboard.burrito.ts</code>. The analyzer shows exactly what updates when state changes. This is how you verify AI-generated UI code without reading it.</p></div></div><div class="section"><div class="section-text"><p>Run the UI analyzer to see what the framework discovers:</p></div></div><div class="demo-block"><div class="demo-code"><div class="code-block-wrapper"><div class="code-filename mono">Verify it</div><div class="code-block"><pre><code class="language-bash">pnpm burrito ui analyze dashboard.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div></div><div class="demo-output"><div class="demo-output-label">Output</div><div class="demo-output-content"><pre class="mono">dashboard.burrito.ts
═══════════════════════════════════════════════════════════════
Data Dependencies:
state['accounts'] → .account-list children
state['selectedId'] → .account-item.selected class, #detail-pane content
Update Impact:
When state['accounts'] changes → re-render account list only
When state['selectedId'] changes → update selected class + detail pane
No full re-renders needed. All updates are surgical.</pre></div></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">What Gets Updated When</div><p>When you click an account:</p><p>1. <code>selectAccount</code> action fires with the account ID 2. <code>selectedId</code> state changes 3. BurritoUI knows exactly what depends on <code>selectedId</code>: • The <code>selected</code> class on account items • The content of <code>#detail-pane</code> 4. Only those DOM nodes update—the account list doesn't re-render</p><p>This is surgical precision. React would re-render the entire Dashboard component and diff the result.</p></div></div><div class="section"><div class="section-text"><p>For server-side rendering or static site generation, use <code>renderToString()</code>:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-typescript">import { h, text, renderToString } from 'burritoscript/ui'
// Generate static HTML
const html = renderToString(
h('div', { className: 'dashboard' }, [
// ... VNode tree
])
)
// Result: '&lt;div class="dashboard"&gt;...&lt;/div&gt;'</code></pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow has a dashboard. Clicking an account updates only the detail pane and the selected class—the account list doesn't re-render. Static analysis makes updates surgical.</p><p><strong>The verification workflow:</strong> AI generates the component. You run <code>burrito ui analyze</code> to see dependencies. If they match what you expected, the code is correct.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents trained on React often make these errors:</strong></p><p>1. <strong>Using React hooks</strong> - No <code>useState</code>, <code>useEffect</code>, <code>useCallback</code>. BurritoUI uses <code>state.read()</code>. If you see hooks, ask for a rewrite.</p><p>2. <strong>Inline event handlers</strong> - Don't use <code>onClick={() =&gt; setX(...)}</code>. Use <code>onClick: action('actionName', payload)</code>. The analyzer can't track inline handlers.</p><p>3. <strong>Manual dependency arrays</strong> - BurritoUI discovers dependencies via static analysis. No <code>[dep1, dep2]</code> arrays needed.</p><p>4. <strong>Synchronous components</strong> - Components must be <code>async</code> and return <code>Promise&lt;VNode&gt;</code>. If it's not async, ask for a rewrite.</p><p>5. <strong>Calling runtime functions</strong> - If you see <code>createRuntime()</code> or <code>mount()</code>, ask for just the component code. The CLI handles runtime setup.</p><p><strong>The fix:</strong> Show the AI the analysis output. "The analyzer shows missing dependencies. Make sure the component is async and uses state.read()."</p></div></div><div class="section"><h3 class="section-title">CLI Commands</h3><div class="section-text"><p>BurritoUI is driven by the CLI:</p><ul class="summary-list"><li><strong>Development:</strong></li><li><code>burrito ui preview &lt;file&gt;</code> - Start local preview server with hot reload</li><li><code>burrito ui analyze &lt;file&gt;</code> - Show data dependencies and update impact</li><li><code>burrito ui validate &lt;file&gt;</code> - Validate BurritoScript syntax</li></ul><ul class="summary-list"><li><strong>Production:</strong></li><li><code>burrito ui embed &lt;file&gt;</code> - Generate embeddable HTML</li><li><code>renderToString(vnode)</code> - Generate HTML string for SSR</li></ul></div></div><div class="section"><h3 class="section-title">API Summary</h3><div class="section-text"><ul class="summary-list"><li><strong>VNode Construction:</strong></li><li><code>h(tag, props, children)</code> - Create an element</li><li><code>text(content)</code> - Create a text node</li><li><code>fragment(children)</code> - Group without a wrapper element</li></ul><ul class="summary-list"><li><strong>Event Handling:</strong></li><li><code>action(type)</code> - Declare an action to dispatch</li><li><code>action(type, payload)</code> - Action with data</li></ul><ul class="summary-list"><li><strong>Config File Pattern:</strong></li><li><code>handlers</code> - Object mapping action names to state reducers</li><li><code>initialState</code> - Starting state for the component</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li>Components are <strong>async functions</strong> that read state and return <strong>VNodes</strong></li><li>Static analysis runs at <strong>build time</strong> to discover state dependencies</li><li>Updates go directly to the DOM nodes that changed—<strong>O(1) updates</strong></li><li>No virtual DOM diffing, no tree walking</li><li><strong>You never write runtime setup code</strong>—the CLI handles it</li><li><code>renderToString()</code> for server rendering and static generation</li><li><strong>Static analysis makes AI-generated UI code verifiable</strong>—run <code>burrito ui analyze</code> to check dependencies</li></ul><p>Next up: <strong>Mission Apps</strong>—where we'll define MoneyFlow's data model and get a GraphQL API automatically.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="mission-apps"><div class="chapter-label">Chapter 4</div><h1 class="chapter-title">Mission Apps</h1><p class="chapter-subtitle">Declare your data model. Get a GraphQL API automatically.</p><div class="chapter-meta">⏱️ ~20 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>So far we've written service functions by hand. But most apps need standard CRUD operations: create users, update records, query with filters. Writing these by hand is tedious and error-prone.</p><p><strong>Here's the thing:</strong> CRUD is exactly what AI is good at. It's mechanical, repetitive, and follows patterns. But you still need to review it—which defeats the purpose of AI-assisted development.</p><p><strong>Mission</strong> solves this: you define the data model once, and Mission generates the CRUD automatically. Your review time goes to zero for generated CRUD, so you can focus on custom business logic.</p><p>Mission is BurritoScript's declarative data layer. You define resources with schemas and relations, and Mission generates:</p><ul class="summary-list"><li>GraphQL queries and mutations</li><li>Drizzle ORM schema</li><li>Built-in permissions and scopes</li><li>Automatic DataLoaders (no N+1 queries)</li></ul><p><strong>The pattern:</strong> AI writes the data model definition. Mission generates the CRUD. You verify the generated GraphQL matches what you expected. No reviewing boilerplate.</p></div></div><div class="section"><h3 class="section-title">Mini-App: Task Manager</h3><div class="section-text"><p>Let's build a simple task manager with users and tasks.</p><p><strong>Try it:</strong> Ask your AI: "Create a Mission resource for User with fields: id, email, name, role. Add a hasMany relation to Tasks. Set permissions so users can only read their own data."</p><p>Did it produce something like this?</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">resources.mission.ts</div><div class="code-block"><pre><code class="language-typescript">import { defineResource, belongsTo, hasMany } from 'burritoscript/mission'
import { z } from 'zod'
// User resource
export const User = defineResource('users', {
typeName: 'User',
schema: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}),
relations: {
tasks: hasMany('tasks', 'assigneeId')
},
permissions: {
read: 'users:read',
create: 'users:create'
}
})
// Task resource
export const Task = defineResource('tasks', {
typeName: 'Task',
schema: z.object({
id: z.string().uuid(),
title: z.string(),
completed: z.boolean().default(false),
assigneeId: z.string().uuid().optional()
}),
relations: {
assignee: belongsTo('users', 'assigneeId')
},
permissions: {
read: 'tasks:read:own',
create: 'tasks:create',
update: 'tasks:update:own'
},
scopes: {
read: 'own', // Users only see their own tasks
update: 'own' // Users only update their own tasks
}
})</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">What defineResource() Does</div><p>Each <code>defineResource()</code> call creates:</p><ul class="summary-list"><li><strong>Database table</strong> - Generated Drizzle schema</li><li><strong>GraphQL type</strong> - With all fields from the schema</li><li><strong>Queries</strong> - <code>task(id)</code>, <code>tasks(where, orderBy, limit)</code></li><li><strong>Mutations</strong> - <code>createTask</code>, <code>updateTask</code>, <code>deleteTask</code></li><li><strong>Relations</strong> - <code>task { assignee { name } }</code> just works</li><li><strong>Permissions</strong> - Automatic checks before any operation</li></ul></div></div><div class="section"><div class="section-text"><p>What GraphQL API does this generate? Let's see:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">Generated GraphQL (automatic)</div><div class="code-block"><pre><code class="language-graphql"># Queries - generated automatically
type Query {
task(id: ID!): Task
tasks(where: TaskFilter, orderBy: TaskOrderBy, limit: Int): [Task!]!
user(id: ID!): User
users(where: UserFilter, orderBy: UserOrderBy, limit: Int): [User!]!
}
# Mutations - generated automatically
type Mutation {
createTask(input: CreateTaskInput!): Task!
updateTask(id: ID!, input: UpdateTaskInput!): Task!
deleteTask(id: ID!): Boolean!
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
# Types with relations - generated automatically
type Task {
id: ID!
title: String!
completed: Boolean!
assigneeId: ID
assignee: User # Relation resolved automatically
}
type User {
id: ID!
name: String!
email: String!
tasks: [Task!]! # Relation resolved automatically
}</code></pre></div></div><div class="section"><div class="section-text"><p>Query tasks with their assignees:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-graphql">query {
tasks(where: { completed: false }) {
id
title
assignee {
name
email
}
}
}</code></pre></div></div><div class="output-block"><div class="output-label">Result</div><div class="output-content"><pre class="mono">{
"data": {
"tasks": [
{
"id": "task_1",
"title": "Review PR #42",
"assignee": {
"name": "Alice",
"email": "alice@example.com"
}
},
{
"id": "task_2",
"title": "Deploy to staging",
"assignee": {
"name": "Bob",
"email": "bob@example.com"
}
}
]
}
}</pre></div></div><div class="try-it-block"><div class="try-it-header"><span class="try-it-icon">🤖</span><span class="try-it-label">Try it</span></div><div class="try-it-content"><p><strong>Verify it:</strong> Run <code>burrito mission generate resources.mission.ts</code> (or check the generated GraphQL). The API should include queries for <code>task</code>, <code>tasks</code>, <code>user</code>, <code>users</code>, and mutations for creating/updating. If the generated API matches what you expected, the code is correct—no reviewing CRUD boilerplate needed.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>We defined two resources with schemas, relations, and permissions. Mission generated a complete GraphQL API with queries, mutations, filtering, and relation resolution. No boilerplate.</p><p><strong>The pattern:</strong> AI writes the resource definitions. Mission generates the CRUD. You verify the GraphQL matches expectations. Review time: zero for generated code.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Data Model</h3><div class="section-text"><p>Now let's define the data model for MoneyFlow. We need:</p><ul class="summary-list"><li><strong>Accounts</strong> - checking, savings, investment</li><li><strong>Transactions</strong> - deposits, withdrawals, transfers</li><li><strong>Categories</strong> - for organizing transactions</li></ul><p><strong>Try it:</strong> Ask your AI: "Create Mission resources for Account, Transaction, and Category. Accounts have ownerId and belong to users. Transactions belong to accounts and categories. Set scopes so users only see their own accounts and transactions."</p><p>Here's what it should produce:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">moneyflow.mission.ts</div><div class="code-block"><pre><code class="language-typescript">import { defineResource, belongsTo, hasMany } from 'burritoscript/mission'
import { z } from 'zod'
// Account resource
export const Account = defineResource('accounts', {
typeName: 'Account',
schema: z.object({
id: z.string().uuid(),
name: z.string(),
type: z.enum(['checking', 'savings', 'investment']),
balance: z.number().default(0),
ownerId: z.string().uuid()
}),
relations: {
owner: belongsTo('users', 'ownerId'),
transactions: hasMany('transactions', 'accountId')
},
permissions: {
read: 'accounts:read:own',
create: 'accounts:create',
update: 'accounts:update:own'
},
scopes: {
read: 'own',
update: 'own'
}
})
// Transaction resource
export const Transaction = defineResource('transactions', {
typeName: 'Transaction',
schema: z.object({
id: z.string().uuid(),
accountId: z.string().uuid(),
amount: z.number(),
type: z.enum(['deposit', 'withdrawal', 'transfer']),
description: z.string().optional(),
categoryId: z.string().uuid().optional(),
timestamp: z.date().default(() =&gt; new Date())
}),
relations: {
account: belongsTo('accounts', 'accountId'),
category: belongsTo('categories', 'categoryId')
},
permissions: {
read: 'transactions:read:own',
create: 'transactions:create'
},
// Lifecycle hook: validate account ownership before creating
beforeCreate: async (db, ctx, input) =&gt; {
const account = await db.accounts.find(input.accountId)
if (account.ownerId !== ctx.userId) {
throw new Error('Cannot create transaction for account you do not own')
}
return input
}
})
// Category resource
export const Category = defineResource('categories', {
typeName: 'Category',
schema: z.object({
id: z.string().uuid(),
name: z.string(),
color: z.string().default('#6366f1'),
icon: z.string().optional()
}),
permissions: {
read: 'categories:read',
create: 'categories:create'
}
})</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Lifecycle Hooks</div><p>The <code>beforeCreate</code> hook runs before every transaction is created. It validates that the user owns the account.</p><ul class="summary-list"><li>Available hooks:</li><li><code>beforeCreate</code>, <code>afterCreate</code></li><li><code>beforeUpdate</code>, <code>afterUpdate</code></li><li><code>beforeDelete</code>, <code>afterDelete</code></li></ul><p>Use hooks for validation, audit logging, or triggering side effects.</p></div></div><div class="section"><div class="section-text"><p>Now we can query MoneyFlow data with GraphQL:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-graphql"># Get account with recent transactions
query {
account(id: "checking") {
name
type
balance
transactions(orderBy: { timestamp: DESC }, limit: 5) {
amount
type
description
category {
name
color
}
}
}
}</code></pre></div></div><div class="output-block"><div class="output-label">Result</div><div class="output-content"><pre class="mono">{
"data": {
"account": {
"name": "Main Checking",
"type": "checking",
"balance": 1250.00,
"transactions": [
{
"amount": -50.00,
"type": "withdrawal",
"description": "Coffee shop",
"category": { "name": "Food &amp; Drink", "color": "#f59e0b" }
},
{
"amount": 1500.00,
"type": "deposit",
"description": "Paycheck",
"category": { "name": "Income", "color": "#10b981" }
}
]
}
}
}</pre></div></div><div class="section"><div class="section-text"><p>MoneyFlow now has a complete data model:</p><ul class="summary-list"><li><strong>Accounts</strong> with owner relationships and scoped access</li><li><strong>Transactions</strong> with category tagging and lifecycle validation</li><li><strong>Categories</strong> for organizing spending</li></ul><p>All with automatic GraphQL queries, mutations, and relation resolution.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow has a real data model with permissions. Users only see their own accounts and transactions. The GraphQL API is generated automatically—no resolvers to write.</p><p><strong>The verification workflow:</strong> AI writes resource definitions. You check the generated GraphQL. If it matches expectations, you're done. No reviewing CRUD code.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often get these wrong:</strong></p><p>1. <strong>Missing foreign key columns</strong> - If you have <code>belongsTo('users', 'userId')</code>, the schema must include <code>userId</code>. The generated GraphQL will fail if foreign keys are missing.</p><p>2. <strong>Wrong relation type</strong> - <code>belongsTo</code> is many-to-one (this resource has one), <code>hasMany</code> is one-to-many (this resource has many). Check the generated GraphQL to verify relations work.</p><p>3. <strong>Overly permissive scopes</strong> - Default to <code>'own'</code> scope for user data, not <code>'any'</code>. Test the generated queries to ensure scoping works.</p><p>4. <strong>Forgetting lifecycle hooks</strong> - For validation that needs database queries (like checking ownership), use <code>beforeCreate</code>. The generated CRUD won't include this—you need to add hooks.</p><p><strong>The fix:</strong> Show the AI the generated GraphQL output. "The query doesn't filter by userId. Add 'own' scope to the read permission."</p></div></div><div class="section"><h3 class="section-title">API Summary</h3><div class="section-text"><p>Mission provides these core functions:</p><ul class="summary-list"><li><strong>Resource Definition:</strong></li><li><code>defineResource(tableName, config)</code> - Define a data resource</li></ul><ul class="summary-list"><li><strong>Relations:</strong></li><li><code>belongsTo(target, foreignKey)</code> - Many-to-one relation</li><li><code>hasMany(target, foreignKey)</code> - One-to-many relation</li><li><code>hasOne(target, foreignKey)</code> - One-to-one relation</li></ul><ul class="summary-list"><li><strong>Config Options:</strong></li><li><code>schema</code> - Zod schema for validation</li><li><code>relations</code> - Define how resources connect</li><li><code>permissions</code> - Required permissions per operation</li><li><code>scopes</code> - <code>'own'</code> or <code>'any'</code> for read/update</li><li><code>beforeCreate</code>, <code>afterCreate</code>, etc. - Lifecycle hooks</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><code>defineResource()</code> creates database tables and GraphQL types</li><li><strong>Relations</strong> are defined declaratively—<code>belongsTo</code>, <code>hasMany</code></li><li><strong>Permissions</strong> and <strong>scopes</strong> control access automatically</li><li><strong>CRUD is generated automatically</strong>—no reviewing boilerplate</li><li><strong>The pattern:</strong> AI writes resource definitions, Mission generates CRUD, you verify the GraphQL</li><li><strong>Lifecycle hooks</strong> run before/after CRUD operations</li><li>The GraphQL API is generated—no resolvers to write</li></ul><p><strong>Coming in Chapter 10:</strong> We'll see how Mission resources generate type-safe service methods automatically. Define your data model once, get a full CRUD API for free.</p><p>Next up: <strong>Simulation Applets</strong>—a palette cleanser where we'll build interactive physics and economics visualizations. (Feel free to skip ahead to Chapter 6 if you're eager to see the cross-cutting features.)</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="simulation-applets"><div class="chapter-label">Chapter 5</div><h1 class="chapter-title">Simulation Applets</h1><p class="chapter-subtitle">Build real-time physics and economics simulations with canvas.</p><div class="chapter-meta">⏱️ ~18 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>Time for something different. We've been building MoneyFlow—now let's take a break and build some standalone simulations.</p><p><strong>Why simulations?</strong> They're the ultimate test of a reactive framework. Real-time updates at 60fps, complex state interdependencies, canvas rendering—if the framework handles this well, it handles everything well. Plus, simulations make abstract concepts visual and tangible.</p><p><strong>Note:</strong> This chapter is standalone. MoneyFlow doesn't need simulations, and you can skip this chapter if you're focused on building web apps. But the patterns here—pure physics functions, effectful state management—apply everywhere.</p><p><strong>For AI-assisted development:</strong> Simulations are a good test case. The physics logic is pure (easy for AI to write), and the effectful wrapper is minimal (easy to verify). Ask your AI to write the physics, then verify the dependencies with static analysis.</p><p>BurritoScript's effect system is perfect for simulations:</p><ul class="summary-list"><li><strong>State reads/writes</strong> track what changes</li><li><strong>time.onFrame</strong> sets up animation automatically</li><li><strong>canvas.render</strong> draws efficiently when state changes</li><li><strong>Static analysis</strong> discovers dependencies—no manual wiring</li></ul><p>You'll build a bouncing ball (physics) and see an economics simulation (deadweight loss). Both use the same patterns you've already learned.</p></div></div><div class="section"><h3 class="section-title">Mini-App: Bouncing Ball</h3><div class="section-text"><p>Let's build a ball that bounces around a canvas with gravity. First, we define the state types:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">ball-types.ts</div><div class="code-block"><pre><code class="language-typescript">interface BallState {
readonly x: number
readonly y: number
readonly vx: number // velocity x
readonly vy: number // velocity y
}
interface Config {
readonly gravity: number
readonly bounciness: number // 0-1, energy retained on bounce
readonly width: number
readonly height: number
readonly ballRadius: number
}
interface SimState {
readonly ball: BallState
readonly config: Config
readonly running: boolean
}
const initialState: SimState = {
ball: { x: 200, y: 50, vx: 100, vy: 0 },
config: {
gravity: 500,
bounciness: 0.8,
width: 400,
height: 300,
ballRadius: 20
},
running: true
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Now the physics—a <strong>pure function</strong> with no effects:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">physics.ts (pure function)</div><div class="code-block"><pre><code class="language-typescript">const updatePhysics = (ball: BallState, config: Config, dt: number): BallState =&gt; {
// Apply gravity to velocity
const vy1 = ball.vy + config.gravity * dt
// Update position
const x1 = ball.x + ball.vx * dt
const y1 = ball.y + vy1 * dt
// Bounce off walls (x-axis)
const { x: x2, vx: vx2 } =
x1 - config.ballRadius &lt; 0
? { x: config.ballRadius, vx: -ball.vx * config.bounciness }
: x1 + config.ballRadius &gt; config.width
? { x: config.width - config.ballRadius, vx: -ball.vx * config.bounciness }
: { x: x1, vx: ball.vx }
// Bounce off floor/ceiling (y-axis)
const { y: y3, vy: vy3 } =
y1 - config.ballRadius &lt; 0
? { y: config.ballRadius, vy: -vy1 * config.bounciness }
: y1 + config.ballRadius &gt; config.height
? { y: config.height - config.ballRadius, vy: -vy1 * config.bounciness }
: { y: y1, vy: vy1 }
return { x: x2, y: y3, vx: vx2, vy: vy3 }
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Pure Physics, Effectful Simulation</div><p>Notice the physics function is <strong>pure</strong>—it takes state and returns new state. No side effects. This makes it easy to test and reason about.</p><p>The BurritoScript simulation function handles the <strong>effects</strong>: reading state, writing state, rendering to canvas. This separation keeps code clean.</p></div></div><div class="section"><div class="section-text"><p>Now the BurritoScript simulation that wires it all together:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">bouncing-ball.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">// Effect interfaces provided by the framework
interface State {
read&lt;T&gt;(path: readonly string[]): Promise&lt;T&gt;
write&lt;T&gt;(path: readonly string[], value: T): Promise&lt;void&gt;
}
interface Time {
onFrame(callback: (dt: number) =&gt; Promise&lt;void&gt;): Promise&lt;void&gt;
}
interface Canvas {
render(slot: string, options: {
width: number
height: number
draw: (ctx: CanvasRenderingContext2D) =&gt; Promise&lt;void&gt;
}): Promise&lt;void&gt;
}
// The simulation function
async function simulate(state: State, time: Time, canvas: Canvas): Promise&lt;void&gt; {
// Read config once (static during simulation)
const config = await state.read&lt;Config&gt;(['config'])
// Animation loop - framework detects this and sets up RAF
await time.onFrame(async (dt) =&gt; {
const running = await state.read&lt;boolean&gt;(['running'])
if (!running) return
const ball = await state.read&lt;BallState&gt;(['ball'])
const nextBall = updatePhysics(ball, config, dt)
await state.write(['ball'], nextBall)
})
// Render canvas - framework tracks dependencies from state.read calls
await canvas.render('main', {
width: config.width,
height: config.height,
draw: async (ctx) =&gt; {
const ball = await state.read&lt;BallState&gt;(['ball'])
const cfg = await state.read&lt;Config&gt;(['config'])
// Clear background
ctx.fillStyle = '#1a1a2e'
ctx.fillRect(0, 0, cfg.width, cfg.height)
// Draw ball
ctx.beginPath()
ctx.arc(ball.x, ball.y, cfg.ballRadius, 0, Math.PI * 2)
ctx.fillStyle = '#e94560'
ctx.fill()
}
})
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">What Static Analysis Discovers</div><p>By analyzing the Effect AST, BurritoScript discovers:</p><ul class="summary-list"><li><strong>State Reads:</strong></li><li><code>['config']</code> - read once at start (static)</li><li><code>['running']</code> - read every frame</li><li><code>['ball']</code> - read every frame and in canvas draw</li></ul><ul class="summary-list"><li><strong>State Writes:</strong></li><li><code>['ball']</code> - written every frame</li></ul><ul class="summary-list"><li><strong>Canvas Dependencies:</strong></li><li><code>'main'</code> canvas depends on <code>['ball']</code> and <code>['config']</code></li></ul><ul class="summary-list"><li><strong>Animation:</strong></li><li><code>time.onFrame</code> detected → framework sets up RAF</li><li>onFrame writes <code>['ball']</code> → canvas depends on it → auto-redraw</li></ul><p>No manual wiring needed. The framework generates the update loop.</p></div></div><div class="section"><div class="section-text"><p>Compare to React—which requires manual <code>useEffect</code>, <code>useCallback</code>, and RAF cleanup:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">React equivalent (for comparison)</div><div class="code-block"><pre><code class="language-typescript">// React: manual dependency management
function BouncingBall() {
const [ball, setBall] = useState({ x: 200, y: 50, vx: 100, vy: 0 })
const [running, setRunning] = useState(true)
const canvasRef = useRef&lt;HTMLCanvasElement&gt;(null)
// Animation loop - manual RAF with cleanup
useEffect(() =&gt; {
if (!running) return
let lastTime = performance.now()
let rafId: number
const animate = () =&gt; {
const now = performance.now()
const dt = (now - lastTime) / 1000
lastTime = now
// BUG: ball and config are stale closures!
// Must use functional update or refs
setBall(b =&gt; updatePhysics(b, config, dt))
rafId = requestAnimationFrame(animate)
}
rafId = requestAnimationFrame(animate)
return () =&gt; cancelAnimationFrame(rafId) // Manual cleanup
}, [running]) // Missing deps cause bugs!
// Canvas rendering - another useEffect
useEffect(() =&gt; {
const ctx = canvasRef.current?.getContext('2d')
if (!ctx) return
// ... draw ball ...
}, [ball]) // More manual deps
}</code></pre></div></div><div class="section"><div class="section-text"><ul class="summary-list"><li>React requires:</li><li>Manual RAF setup and cleanup</li><li>Manual dependency arrays (easy to miss)</li><li>Stale closure bugs (ball, config captured at wrong time)</li><li>Multiple useEffect calls for different concerns</li></ul><p>BurritoScript handles all of this automatically via static analysis.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>Physics is a pure function. The simulation uses <code>state.read/write</code> and <code>time.onFrame</code> for effects. Static analysis wires everything together—no manual dependency arrays, no RAF cleanup, no stale closures.</p></div></div><div class="section"><h3 class="section-title">Showcase: Deadweight Loss</h3><div class="section-text"><p>Here's a more complex simulation: an animated marketplace that teaches economics.</p><p><strong>The scenario:</strong> Buyers and sellers want to trade concert tickets. Each buyer has a maximum they'll pay. Each seller has a minimum they'll accept. When buyer's max exceeds seller's min, they can make a deal—both gain "happiness" (economic surplus).</p><p><strong>The twist:</strong> A transaction tax. If the tax exceeds the potential happiness from a trade, the deal doesn't happen. Both parties walk away sad. This destroyed value is called <strong>deadweight loss</strong>.</p><p>The simulation shows people walking around a marketplace, meeting, negotiating, and either shaking hands (deal!) or walking away sad (blocked by tax).</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">marketplace-types.ts</div><div class="code-block"><pre><code class="language-typescript">interface AnimatedBuyer {
readonly id: number
readonly name: string
readonly emoji: string
readonly maxWillingToPay: number
readonly x: number
readonly y: number
readonly state: 'wandering' | 'walking' | 'negotiating' | 'happy' | 'sad'
}
interface AnimatedSeller {
readonly id: number
readonly name: string
readonly emoji: string
readonly minWillingToAccept: number
readonly x: number
readonly y: number
readonly state: 'wandering' | 'walking' | 'negotiating' | 'happy' | 'sad'
}
interface MarketplaceState {
readonly buyers: readonly AnimatedBuyer[]
readonly sellers: readonly AnimatedSeller[]
readonly taxPerUnit: number
readonly phase: 'gathering' | 'matching' | 'negotiating' | 'resolving' | 'aftermath'
readonly completedDeals: number
readonly blockedDeals: number
readonly totalHappiness: number
readonly happinessLost: number
}</code></pre></div></div><div class="section"><div class="section-text"><p>The simulation flow:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">marketplace-phases.ts</div><div class="code-block"><pre><code class="language-typescript">// Phase 1: Gathering - people wander around
// Phase 2: Matching - optimal buyer-seller pairs calculated
// Phase 3: Negotiating - pairs meet in the middle
// Phase 4: Resolving - deal or no deal?
// Phase 5: Aftermath - happy or sad people walk away
const updateAnimation = (state: MarketplaceState, dt: number): MarketplaceState =&gt; {
if (state.phase === 'gathering' &amp;&amp; state.time &gt; 1) {
// Calculate optimal pairs and start matching
const pairs = calculatePairs(state.buyers, state.sellers, state.taxPerUnit)
return { ...state, phase: 'matching', pairs }
}
if (state.phase === 'matching') {
// Move people toward meeting points
const movedBuyers = state.buyers.map(b =&gt; moveToward(b, target))
const movedSellers = state.sellers.map(s =&gt; moveToward(s, target))
// ... check if arrived, transition to negotiating
}
if (state.phase === 'resolving') {
// Determine if deal happens
const surplus = buyer.maxWillingToPay - seller.minWillingToAccept
const willTrade = surplus &gt;= state.taxPerUnit
if (willTrade) {
// Both happy! Update happiness counters
} else {
// Both sad. This is deadweight loss.
}
}
// ... continue phases
}</code></pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">What This Teaches</div><p><strong>Deadweight loss</strong> is NOT the tax money—that goes to the government.</p><ul class="summary-list"><li>It's the happiness that gets <strong>destroyed</strong> because trades stop happening. When a deal is blocked:</li><li>The buyer doesn't get the ticket they wanted</li><li>The seller doesn't get the money they wanted</li><li>Nobody benefits—the value just disappears</li></ul><ul class="summary-list"><li>Try the simulation with different tax rates:</li><li><code>$0</code> tax: All beneficial trades happen</li><li>High tax: Many trades blocked, lots of sad people</li></ul></div></div><div class="section"><div class="section-text"><p>The BurritoScript simulation follows the same pattern as the bouncing ball—just with more state:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">deadweight-loss.burrito.ts (simplified)</div><div class="code-block"><pre><code class="language-typescript">async function DeadweightLoss(state: State, time: Time, canvas: Canvas): Promise&lt;void&gt; {
// Animation loop
await time.onFrame(async (dt) =&gt; {
const market = await state.read&lt;MarketplaceState&gt;(['market'])
if (!market.running) return
const nextMarket = updateAnimation(market, dt)
await state.write(['market'], nextMarket)
})
// Render animated marketplace
await canvas.render('simulation', {
width: 700,
height: 500,
draw: async (ctx) =&gt; {
const market = await state.read&lt;MarketplaceState&gt;(['market'])
renderMarketplace(ctx, market) // Pure rendering function
}
})
}</code></pre></div></div><div class="section"><div class="section-text"><ul class="summary-list"><li>Same pattern:</li><li>Pure functions for logic (<code>updateAnimation</code>, <code>renderMarketplace</code>)</li><li>Effects for state access (<code>state.read</code>, <code>state.write</code>)</li><li>Framework handles animation via <code>time.onFrame</code></li><li>Canvas redraws when market state changes</li></ul><p>The complexity is in the pure functions. The effectful simulation stays simple.</p></div></div><div class="section"><h3 class="section-title">Prompting Guide</h3><div class="section-text"><p><strong>When asking an agent to write simulations:</strong></p><p><strong>Good prompt:</strong> &gt; "Write a BurritoScript simulation of a bouncing ball with gravity. Separate the physics into a pure function and use state.read/write and time.onFrame for the simulation loop."</p><ul class="summary-list"><li><strong>Key details to include:</strong></li><li>What physics/logic should be pure functions</li><li>What state the simulation tracks</li><li>Canvas dimensions and rendering requirements</li></ul><p><strong>The agent should produce:</strong> 1. Pure functions for physics/logic (no effects) 2. An async simulation function using <code>state</code>, <code>time</code>, <code>canvas</code> interfaces 3. State types with <code>readonly</code> fields</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Validation Checklist</div><p>After an agent generates simulation code, verify:</p><p>- [ ] Physics functions are <strong>pure</strong> (take state, return new state) - [ ] Simulation uses <code>time.onFrame(callback)</code> for animation - [ ] State accessed via <code>state.read/write</code>, not direct mutation - [ ] Canvas rendering in <code>canvas.render()</code> callback - [ ] No React patterns (<code>useEffect</code>, <code>useRef</code>, <code>requestAnimationFrame</code>)</p><p><strong>Key insight:</strong> If the physics function has no effects, it's easy to test.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Common Mistakes</div><p><strong>Agents trained on React simulations often:</strong></p><p>1. <strong>Mix physics and effects</strong> - Physics should be pure. If the update function calls <code>state.write</code>, it's not pure.</p><p>2. <strong>Manual RAF management</strong> - No <code>requestAnimationFrame</code> calls needed. Use <code>time.onFrame</code>.</p><p>3. <strong>Stale closure bugs</strong> - In React, <code>useCallback</code> deps are tricky. BurritoScript's static analysis avoids this.</p><p>4. <strong>Mutable state</strong> - Use <code>readonly</code> for state types. Return new objects, don't mutate.</p></div></div><div class="section"><h3 class="section-title">API Summary</h3><div class="section-text"><p>Simulation applets use these effect interfaces:</p><ul class="summary-list"><li><strong>State:</strong></li><li><code>state.read&lt;T&gt;(path)</code> - Read state at path</li><li><code>state.write&lt;T&gt;(path, value)</code> - Write state at path</li></ul><ul class="summary-list"><li><strong>Time:</strong></li><li><code>time.onFrame(callback)</code> - Called every animation frame with delta time</li></ul><ul class="summary-list"><li><strong>Canvas:</strong></li><li><code>canvas.render(slot, { width, height, draw })</code> - Render to a canvas slot</li><li>The <code>draw</code> function receives a <code>CanvasRenderingContext2D</code></li></ul><ul class="summary-list"><li><strong>Patterns:</strong></li><li>Keep physics/logic as pure functions</li><li>Use effects only for state access and rendering</li><li>Let static analysis wire dependencies</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><code>state.read/write</code> tracks what changes</li><li><code>time.onFrame</code> sets up animation automatically</li><li><code>canvas.render</code> draws when dependencies change</li><li><strong>Static analysis</strong> discovers dependencies—no manual wiring</li><li>Keep physics <strong>pure</strong>, effects <strong>minimal</strong></li><li>Compare to React: no useEffect, no manual deps, no stale closures</li></ul><p>This was a palette cleanser—standalone simulations that showcase BurritoScript's strengths for real-time visualization.</p><p>Next up: Back to building features that <strong>cut across</strong> the application types you've learned—provenance, verification, and batching.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="provenance"><div class="chapter-label">Chapter 6</div><h1 class="chapter-title">Provenance</h1><p class="chapter-subtitle">Trace any value back to its source. Debug by asking "why?"</p><div class="chapter-meta">⏱️ ~22 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p><strong>This is BurritoScript's killer feature for AI-assisted development.</strong></p><p>When AI writes your code, you need to verify without reading. Provenance lets you ask "where did this value come from?" and get a precise answer. Run the debugger, ask questions, find bugs—without understanding the code.</p><p><strong>Here's why this makes AI more reliable:</strong> When AI generates buggy code, you can give it concrete, debugger-backed feedback. "The value at node n5 comes from the wrong account. Fix the getBalance call." Instead of "this doesn't work," you give precise, actionable feedback. The AI can iterate to correctness with tool-backed guidance.</p><p>Every value in a BurritoScript program has a <strong>provenance</strong>—a complete record of where it came from and how it was computed. This chapter shows you how to use provenance to debug problems that would otherwise require hours of printf debugging.</p><ul class="summary-list"><li>You'll learn to:</li><li>Trace any value back to its source</li><li>Ask "why did I get this result?" and get a precise answer</li><li>Explore scope at any point in execution</li><li>Evaluate expressions with captured values</li></ul><p><strong>You don't need to understand the code to debug it.</strong> Just run <code>burrito debug</code> and ask questions. <strong>The debugger makes AI iteration precise and reliable.</strong></p></div></div><div class="section"><h3 class="section-title">Mini-App: Tip Calculator Debug</h3><div class="section-text"><p>You've built a tip calculator, but the math seems off. Someone reports that a $50 bill with 20% tip is calculating as $70 instead of $60.</p><p><strong>Try it:</strong> Ask your AI to write this tip calculator. Then run <code>burrito debug</code> and trace the wrong value back to its source. This is how you debug AI-generated code without reading it.</p><p>Here's the buggy code:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">tip-calculator.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface Calculator {
multiply(a: number, b: number): Promise&lt;number&gt;
}
interface Display {
format(cents: number): Promise&lt;string&gt;
}
export async function calculateTip(
calc: Calculator,
display: Display,
billCents: number,
tipPercent: number
): Promise&lt;string&gt; {
// Bug: tipPercent is 20, not 0.20!
const tipCents = await calc.multiply(billCents, tipPercent)
const totalCents = billCents + tipCents
return await display.format(totalCents)
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-filename mono">tip-calculator.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
calc: {
async multiply(a: number, b: number) {
return a * b
}
},
display: {
async format(cents: number) {
return `$${(cents / 100).toFixed(2)}`
}
}
}
export const entryFunction = 'calculateTip'</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Run with trace to see what's happening:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito trace tip-calculator.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Provenance Output</div><div class="output-content"><pre class="mono">Running: tip-calculator.burrito.ts
════════════════════════════════════════════════════════════
Result: "$1050.00"
════════════════════════════════════════════════════════════
PROVENANCE TRACE
════════════════════════════════════════════════════════════
Total nodes: 4
Result node: n3
Result value: "$1050.00"
Provenance Graph:
────────────────────────────────────────
n0: tipCents ← calc.multiply() tip-calculator.burrito.ts:14
→ used by: n1
n1: totalCents ← pure tip-calculator.burrito.ts:15
→ used by: n2
n2: &lt;string&gt; ← display.format() tip-calculator.burrito.ts:16 ← RESULT</pre></div></div><div class="section"><h3 class="section-title">Interactive Debugging</h3><div class="section-text"><p>The trace shows the result is "$1050.00"—way too high! Let's dig deeper with the interactive debugger:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito debug tip-calculator.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Now use the <code>explain</code> command to understand where the result came from:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; explain</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Explain Output</div><div class="output-content"><pre class="mono">n2: "$1050.00" ← display.format()
├─ n1: totalCents = 105000 ← pure
│ ├─ n0: tipCents = 100000 ← calc.multiply()
│ └─ billCents = 5000 (parameter)</pre></div></div><div class="section"><div class="section-text"><p>The tree shows the problem: <code>tipCents</code> is 100000 (should be 1000). Let's see exactly what went into that calculation:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; snippet n0</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Snippet Output</div><div class="output-content"><pre class="mono">n0 at tip-calculator.burrito.ts:14
→ 14 │ const tipCents = await calc.multiply(billCents, tipPercent)
15 │ const totalCents = billCents + tipCents
Scope:
billCents = 5000
tipPercent = 20
Result:
tipCents = 100000</pre></div></div><div class="section"><div class="section-text"><p><strong>Found it!</strong> <code>tipPercent</code> is 20, not 0.20. The calculation is <code>5000 * 20 = 100000</code> when it should be <code>5000 * 0.20 = 1000</code>.</p><p>You can verify this with the <code>eval</code> command:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; eval n0 "billCents * tipPercent / 100"</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">Expression: billCents * tipPercent / 100
Result: 1000
Scope used:
billCents = 5000
tipPercent = 20</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>The <code>snippet</code> command shows you the exact scope (variable values) at any point in execution. The <code>eval</code> command lets you test corrections without modifying code. Together, they turn debugging from guesswork into investigation.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Budget Miscalculation</h3><div class="section-text"><p>A user reports their budget summary is wrong. They have a $2000 monthly income and expenses of $1500, but the remaining budget shows "-$500" instead of "$500".</p><p>Here's the budget calculator:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">budget-summary.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface BankService {
getIncome(userId: string, month: string): Promise&lt;number&gt;
getExpenses(userId: string, month: string): Promise&lt;number&gt;
}
interface Formatter {
formatCurrency(cents: number): Promise&lt;string&gt;
}
export async function getBudgetSummary(
bank: BankService,
fmt: Formatter,
userId: string,
month: string
): Promise&lt;{ income: string; expenses: string; remaining: string }&gt; {
const income = await bank.getIncome(userId, month)
const expenses = await bank.getExpenses(userId, month)
// Bug: expenses already includes the sign!
const remaining = income - expenses
return {
income: await fmt.formatCurrency(income),
expenses: await fmt.formatCurrency(expenses),
remaining: await fmt.formatCurrency(remaining)
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-filename mono">budget-summary.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const implementations = {
bank: {
async getIncome(userId: string, month: string) {
return 200000 // $2000 in cents
},
async getExpenses(userId: string, month: string) {
// Bug: returning negative value!
return -150000 // Should be 150000
}
},
fmt: {
async formatCurrency(cents: number) {
const sign = cents &lt; 0 ? '-' : ''
return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`
}
}
}
export const entryFunction = 'getBudgetSummary'</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito debug budget-summary.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Start by listing all nodes to see the full computation:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; list --values</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">n0: income = 200000 ← bank.getIncome() budget-summary.burrito.ts:12
n1: expenses = -150000 ← bank.getExpenses() budget-summary.burrito.ts:13
n2: remaining = 350000 ← pure budget-summary.burrito.ts:16
n3: &lt;string&gt; = "$2000.00" ← fmt.formatCurrency() budget-summary.burrito.ts:19
n4: &lt;string&gt; = "-$1500.00" ← fmt.formatCurrency() budget-summary.burrito.ts:20
n5: &lt;string&gt; = "$3500.00" ← fmt.formatCurrency() budget-summary.burrito.ts:21 ← RESULT</pre></div></div><div class="section"><div class="section-text"><p>Look at n1: <code>expenses = -150000</code>. That's suspicious—expenses should be positive! Let's trace where that came from:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; snippet n1</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">n1 at budget-summary.burrito.ts:13
→ 13 │ const expenses = await bank.getExpenses(userId, month)
14 │
Scope:
userId = "user123"
month = "2024-01"
Result:
expenses = -150000</pre></div></div><div class="section"><div class="section-text"><p>The bug is in the mock implementation: <code>getExpenses</code> returns -150000 instead of 150000. When we do <code>income - expenses</code>, we get <code>200000 - (-150000) = 350000</code> instead of <code>200000 - 150000 = 50000</code>.</p><p>Let's verify:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">debug&gt; eval n2 "200000 - 150000"</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">Expression: 200000 - 150000
Result: 50000</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>Provenance debugging found a bug in the mock implementation—not the business logic. The <code>list --values</code> command shows all values at once, making sign errors and unexpected values immediately visible.</p></div></div><div class="section"><h3 class="section-title">The Agent-Era Superpower</h3><div class="section-text"><p><strong>This is BurritoScript's killer feature for agent-assisted development.</strong></p><p>When an agent writes your code, you need to verify it works correctly without reading every line. Provenance tracing lets you:</p><ul class="summary-list"><li><strong>Trace unexpected values</strong> - "Why is this null?" → <code>explain</code> shows exactly where it came from</li><li><strong>Verify data flow</strong> - "Did the agent wire the API call correctly?" → <code>deps</code> shows what fed into each result</li><li><strong>Test edge cases</strong> - "What if this was 0?" → <code>eval</code> lets you test with captured scope</li></ul><p><strong>You don't need to understand the code to debug it.</strong> Just run <code>burrito debug</code> and ask questions.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Prompting Guide</div><p><strong>When something goes wrong with agent-generated code:</strong></p><p>Don't ask the agent to explain—use provenance instead:</p><p>1. Run <code>burrito debug &lt;file&gt;</code> 2. Find the wrong value with <code>find &lt;value&gt;</code> 3. Run <code>explain &lt;nodeId&gt;</code> to see where it came from 4. Check <code>snippet &lt;nodeId&gt;</code> to see the code in context</p><p><strong>Then tell the agent what's wrong:</strong> &gt; "The transfer amount at node n5 comes from the wrong account. Fix the getBalance call to use the 'from' account."</p><p>Concrete, debugger-backed feedback produces better agent fixes.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Common Debugging Patterns</div><p><strong>Agent code often has these issues:</strong></p><p>1. <strong>Wrong variable wired</strong> - Use <code>deps</code> to see what actually fed into a calculation.</p><p>2. <strong>Missing await</strong> - If a value is a Promise instead of a result, the agent forgot <code>await</code>.</p><p>3. <strong>Stale mock data</strong> - <code>explain</code> often reveals the bug is in the mock implementation, not the business logic.</p><p>4. <strong>Off-by-one errors</strong> - Use <code>eval</code> to test boundary conditions with the captured scope.</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>This chapter introduced the interactive debugger and its commands:</p><ul class="summary-list"><li><strong>Running:</strong></li><li><code>burrito trace &lt;file&gt;</code> - Run and print provenance summary (good for quick checks)</li><li><code>burrito debug &lt;file&gt;</code> - Run and open interactive debugger (good for investigation)</li></ul><p><strong>When to use which:</strong> Use <code>trace</code> when you want a quick overview of what happened. Use <code>debug</code> when you need to dig into specific values, explore scope, or test expressions with <code>eval</code>.</p><ul class="summary-list"><li><strong>Exploring (in debugger):</strong></li><li><code>list</code> - List all nodes (add <code>--values</code> to see values)</li><li><code>explain [nodeId]</code> - Show dependency tree</li><li><code>snippet &lt;nodeId&gt;</code> - Show code with scope values</li><li><code>eval &lt;nodeId&gt; &lt;expr&gt;</code> - Evaluate expression with captured scope</li><li><code>deps &lt;nodeId&gt;</code> - Direct dependencies</li><li><code>usages &lt;nodeId&gt;</code> - What uses this node</li><li><code>find &lt;value&gt;</code> - Find nodes by value</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Provenance</strong> tracks where every value came from</li><li><strong>explain</strong> shows the dependency tree from any node</li><li><strong>snippet</strong> shows code with the exact scope at that point</li><li><strong>eval</strong> lets you test expressions with captured values</li><li>Bugs often hide in mocks/implementations, not business logic</li></ul><p>Next up: <strong>Verification</strong>—proving your concurrent code is race-free.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="verification"><div class="chapter-label">Chapter 7</div><h1 class="chapter-title">Verification</h1><p class="chapter-subtitle">Prove your concurrent code is race-free. Test all interleavings.</p><div class="chapter-meta">⏱️ ~25 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p><strong>When AI writes concurrent code, how do you know it's race-free?</strong> Code review? Testing? Hope?</p><p>BurritoScript lets you test all possible interleavings. Run one command with an invariant, get certainty.</p><p><strong>Here's why this makes AI more reliable:</strong> Concurrent code is where AI makes the most mistakes. It writes code that works in isolation but fails under concurrency. Traditional feedback is vague: "sometimes it breaks." BurritoScript gives you the exact interleaving that fails. You show the AI: "At interleaving [A1, B1, A2, B2], both operations read before either writes. Add serializable concurrency." Precise feedback → precise fixes → AI gets it right.</p><p>Concurrent code is hard to test. Two requests that work perfectly alone can corrupt data when they interleave. Traditional testing can't catch these bugs because they depend on timing.</p><p>BurritoScript solves this with <strong>exhaustive interleaving verification</strong>. Instead of hoping your tests catch race conditions, you test all possible interleavings.</p><p><strong>The pattern:</strong> AI writes concurrent code. You run <code>burrito infra race</code> with an invariant. If it passes, the code is correct for all tested interleavings. If it fails, you get the exact interleaving that breaks. <strong>Verification tools turn vague "it's broken" into precise "fix this interleaving" feedback.</strong></p><ul class="summary-list"><li>You'll learn to:</li><li>Test all possible interleavings of concurrent operations</li><li>Define invariants that must hold in all states</li><li>Fix race conditions with safety config</li><li>Generate TLA+ specs for formal verification</li></ul></div></div><div class="section"><h3 class="section-title">Mini-App: Counter Race Condition</h3><div class="section-text"><p>Here's a classic bug: two concurrent increments on a counter. Each reads the value, adds one, and writes it back. When they interleave badly, an increment gets lost.</p><p><strong>Try it:</strong> Ask your AI to write this increment function. Then verify it's race-free. This is how you check AI-generated concurrent code.</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">counter.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface Database {
read(key: string): Promise&lt;number&gt;
write(key: string, value: number): Promise&lt;void&gt;
}
export async function increment(db: Database): Promise&lt;number&gt; {
const current = await db.read('counter')
const next = current + 1
await db.write('counter', next)
return next
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>This looks fine for a single call. But what happens when two increments run concurrently? Let's verify:</p></div></div><div class="section"><div class="section-text"><p><code>burrito infra verify</code> enumerates all interleavings but doesn't check invariants—it just counts them. To actually verify correctness, we need <code>burrito infra race</code> with an invariant. Let's check if the counter ends at 2 when we start at 0 and run 2 increments:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra race counter.burrito.ts --instances=2 --invariant="state['counter'] === 2"</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Race Analysis Output</div><div class="output-content"><pre class="mono">counter.burrito.ts:
Handler: counter
Concurrent instances: 2
Total interleavings: 6
Invariant violations: 2
Invariant: state['counter'] === 2
⚠️ RACE CONDITION DETECTED
Example violation:
Final state: {"counter":1}</pre></div></div><div class="section"><div class="section-text"><p><strong>Found it!</strong> 2 out of 6 interleavings end with <code>counter = 1</code> instead of <code>counter = 2</code>. This is the "lost update" bug—both increments read 0, both write 1.</p><p>Here's the problematic interleaving:</p></div></div><div class="output-block"><div class="output-label">Lost Update Interleaving</div><div class="output-content"><pre class="mono">Timeline:
[Instance 1] db.read('counter') → 0
[Instance 2] db.read('counter') → 0 ← Both read 0!
[Instance 1] db.write('counter', 1)
[Instance 2] db.write('counter', 1) ← Lost update!
Final state: counter = 1 (should be 2)</pre></div></div><div class="section"><h3 class="section-title">Fixing with Safety Config</h3><div class="section-text"><p>The fix is to make the read-then-write atomic. Add a safety config:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">counter.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
safety: {
'counter.burrito.ts': {
concurrency: 'serializable' // Only one at a time
}
}
}
export const implementations = {
db: {
store: { counter: 0 },
async read(key: string) {
return this.store[key] ?? 0
},
async write(key: string, value: number) {
this.store[key] = value
}
}
}
export const entryFunction = 'increment'</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>With <code>concurrency: 'serializable'</code>, the runtime ensures increments run one at a time. Verify it works:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra verify-config counter.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">counter.burrito.ts: ✓ Fixed</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>Exhaustive verification found a race condition that random testing would miss. The <code>--invariant</code> flag lets you specify what "correct" means. The safety config fixes it without changing business logic.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Prove Transfers Safe</h3><div class="section-text"><p>Remember the transfer function from Chapter 2? It moves money between accounts with <code>concurrency: 'serializable'</code> in the config. But how do we <strong>know</strong> it's actually safe?</p><p>Let's verify that MoneyFlow never creates or destroys money—the total balance must be conserved no matter how transfers interleave.</p><p>Here's the transfer handler (same as Chapter 2):</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">transfer.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface BankService {
getBalance(accountId: string): Promise&lt;number&gt;
withdraw(accountId: string, amount: number): Promise&lt;void&gt;
deposit(accountId: string, amount: number): Promise&lt;void&gt;
}
export async function transfer(
bank: BankService,
from: string,
to: string,
amount: number
): Promise&lt;{ success: boolean }&gt; {
const balance = await bank.getBalance(from)
if (balance &lt; amount) {
return { success: false }
}
await bank.withdraw(from, amount)
await bank.deposit(to, amount)
return { success: true }
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>The invariant we want to prove: <strong>total money is conserved</strong>. If we start with $200 total (Account A: $100, Account B: $100), we should end with $200 total regardless of how transfers interleave.</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra tla transfer.burrito.ts --instances=2 --invariant="state['A'] + state['B'] === 200"</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">TLA+ Generation Output</div><div class="output-content"><pre class="mono">transfer.burrito.ts:
Module: transfer
Concurrent instances: 2
Invariant: state['A'] + state['B'] === 200
⚠️ TLC: Invariant violation found
Counterexample:
transfer(A, B, 50) - getBalance(A) → 100
transfer(A, B, 50) - withdraw(A, 50)
transfer(B, A, 30) - getBalance(B) → 100 ← Stale read!
transfer(A, B, 50) - deposit(B, 50)
transfer(B, A, 30) - withdraw(B, 30) ← Overdraft!
transfer(B, A, 30) - deposit(A, 30)
TLA+ Spec:
────────────────────────────────────────────────────────────
---- MODULE transfer ----
EXTENDS Integers, Sequences, TLC
VARIABLES state, pc
Init ==
/\ state = [A |-&gt; 100, B |-&gt; 100]
/\ pc = [i \in 1..2 |-&gt; "getBalance"]
(* ... full spec omitted for brevity ... *)
Invariant == state["A"] + state["B"] = 200
────────────────────────────────────────────────────────────</pre></div></div><div class="section"><div class="section-text"><p>The formal verification found a bug! With two concurrent transfers (A→B and B→A), the stale reads can cause overdrafts.</p><p>Let's fix it with serializable transactions:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">transfer.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
safety: {
'transfer.burrito.ts': {
concurrency: 'serializable',
invariant: 'total_balance_conserved'
}
},
verification: {
'transfer.burrito.ts': {
invariants: ['state.A + state.B === 200'],
tlaVerify: {
instances: 2,
initialState: { A: 100, B: 100 }
}
}
}
}
export const implementations = {
bank: {
balances: { A: 100, B: 100 } as Record&lt;string, number&gt;,
async getBalance(accountId: string) {
return this.balances[accountId] ?? 0
},
async withdraw(accountId: string, amount: number) {
this.balances[accountId] = (this.balances[accountId] ?? 0) - amount
},
async deposit(accountId: string, amount: number) {
this.balances[accountId] = (this.balances[accountId] ?? 0) + amount
}
}
}
export const entryFunction = 'transfer'</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra verify-config transfer.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">transfer.burrito.ts: ✓ Fixed
Invariant: state.A + state.B === 200
Verified with 2 concurrent instances</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>TLA+ formal verification found a subtle race condition in MoneyFlow's transfer. The config-based fix ensures correctness without cluttering the business logic with locks or transactions.</p></div></div><div class="section"><h3 class="section-title">Mathematical Proof, Not Manual Review</h3><div class="section-text"><p><strong>When an agent writes concurrent code, how do you know it's race-free?</strong></p><p>Traditional answer: Code review, hope you catch bugs, write lots of tests.</p><p>BurritoScript answer: <strong>Run `burrito infra race` with an invariant to test all interleavings.</strong></p><p>The verifier tests every possible ordering of operations. If it finds a race condition, it shows you exactly which interleaving causes it. If it passes, the code is <strong>correct for all tested interleavings</strong>—not just "we didn't find bugs."</p><p><strong>This is the agent-era development loop:</strong> 1. Tell the agent what to build 2. Agent writes concurrent code 3. Run <code>burrito infra race</code> with an invariant to verify it's safe 4. If unsafe, show the agent the failing interleaving 5. Agent fixes, repeat until verified</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often produce code that fails verification because:</strong></p><p>1. <strong>Missing concurrency config</strong> - Agent writes business logic but forgets safety. Run <code>burrito infra race</code> with an invariant, show the failure, ask for config.</p><p>2. <strong>Wrong concurrency mode</strong> - <code>transaction</code> doesn't prevent read-before-write races. Use <code>serializable</code>. Show the AI the failing interleaving: "Both operations read before either writes. Add serializable concurrency."</p><p>3. <strong>Incorrect invariant</strong> - Make sure the invariant expression is what you actually want to prove. Test it with <code>eval</code> first.</p><p>4. <strong>Too many instances</strong> - Start with <code>--instances=2</code>. More instances = more interleavings = slower but more thorough.</p><p><strong>The fix pattern:</strong> Show the AI the verification output. "The verifier found a race condition at interleaving [X, Y, Z]. Add serializable concurrency to the config."</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>This chapter introduced verification commands:</p><ul class="summary-list"><li><strong>Verification:</strong></li><li><code>burrito infra race &lt;path&gt; --invariant="..."</code> - Test concurrent interleavings with invariant checking</li><li><code>burrito infra race &lt;path&gt;</code> - Check invariant across interleavings</li><li><code>burrito infra tla &lt;path&gt;</code> - Generate TLA+ and run model checker</li><li><code>burrito infra verify-config &lt;path&gt;</code> - Confirm config fixes issues</li></ul><ul class="summary-list"><li><strong>Options:</strong></li><li><code>--instances=N</code> - Number of concurrent instances (default: 2)</li><li><code>--invariant="expr"</code> - JavaScript expression to verify</li><li><code>--json</code> - Output as JSON</li></ul><p><strong>Safety Config:</strong> <code>`</code>typescript safety: { 'handler.burrito.ts': { concurrency: 'serializable' | 'transaction' | 'none' } } <code>`</code></p></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Exhaustive interleaving</strong> tests all possible orderings</li><li><strong>Invariants</strong> define what "correct" means</li><li><strong>Safety config</strong> fixes races without code changes</li><li><strong>TLA+ generation</strong> enables formal mathematical proofs</li><li>Race conditions are tested exhaustively, not just "not found in random testing"</li></ul><p>Next up: <strong>Automatic Batching</strong>—eliminating N+1 queries without DataLoader boilerplate.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="batching"><div class="chapter-label">Chapter 8</div><h1 class="chapter-title">Automatic Batching</h1><p class="chapter-subtitle">Eliminate N+1 queries. Zero boilerplate.</p><div class="chapter-meta">⏱️ ~20 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p><strong>N+1 queries are the most common performance bug AI creates.</strong> It writes the obvious loop: fetch users, then fetch department for each user. That's the natural implementation—and it's a performance disaster at scale.</p><p>BurritoScript's static analysis detects the pattern automatically. You enable batching in config, and the runtime handles the rest. <strong>The AI never needs to learn batching.</strong></p><p>The N+1 query problem is everywhere: you fetch a list of items, then fetch related data for each one. 10 items = 11 queries. 1000 items = 1001 queries.</p><p>The standard fix is DataLoader—but that means writing batch functions, managing cache keys, and threading loaders through your code.</p><p><strong>The pattern:</strong> AI writes the obvious loop. You run <code>burrito service analyze</code> to detect N+1. You add batching config—no code changes needed.</p><ul class="summary-list"><li>You'll learn to:</li><li>Detect N+1 patterns with <code>burrito service analyze</code></li><li>Configure automatic batching</li><li>See queries collapse without code changes</li></ul></div></div><div class="section"><h3 class="section-title">Mini-App: User Directory</h3><div class="section-text"><p>You're building a user directory. For each user, you fetch their department.</p><p><strong>Try it:</strong> Ask your AI: "Write a function that fetches all users and their departments." It will naturally write a loop—that's the obvious implementation. Then run <code>burrito service analyze</code> to see the N+1 pattern.</p><p>Classic N+1:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">user-directory.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface UserDB {
getAllUsers(): Promise&lt;User[]&gt;
getDepartment(deptId: string): Promise&lt;Department&gt;
}
interface User {
id: string
name: string
deptId: string
}
interface Department {
id: string
name: string
}
export async function buildDirectory(db: UserDB): Promise&lt;DirectoryEntry[]&gt; {
const users = await db.getAllUsers()
const entries: DirectoryEntry[] = []
for (const user of users) {
// N+1: This runs once per user!
const dept = await db.getDepartment(user.deptId)
entries.push({
userName: user.name,
departmentName: dept.name
})
}
return entries
}
interface DirectoryEntry {
userName: string
departmentName: string
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Let's see how bad it is with <code>burrito service analyze</code>:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito service analyze user-directory.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Analysis Output</div><div class="output-content"><pre class="mono">user-directory.burrito.ts
═══════════════════════════════════════════════════════════════
Operations detected:
• db.getAllUsers() - 1 call
• db.getDepartment(deptId) - N calls (inside loop)
⚠️ N+1 QUERY PATTERN DETECTED
db.getDepartment() called in a loop over db.getAllUsers() results
Queries without batching: 1 + N
Queries with batching: 2
Batchable operations:
• db.getDepartment - batchKey: deptId
Suggested config:
batching: {
'db.getDepartment': {
enabled: true,
batchKey: 'deptId'
}
}</pre></div></div><div class="section"><h3 class="section-title">Enabling Automatic Batching</h3><div class="section-text"><p>The analysis found the N+1 and suggests a fix. Let's add it to the config:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">user-directory.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
batching: {
'db.getDepartment': {
enabled: true,
batchKey: 'deptId',
maxBatchSize: 100
}
}
}
export const implementations = {
db: {
async getAllUsers() {
console.log('→ db.getAllUsers()')
return [
{ id: '1', name: 'Alice', deptId: 'eng' },
{ id: '2', name: 'Bob', deptId: 'eng' },
{ id: '3', name: 'Carol', deptId: 'sales' },
{ id: '4', name: 'Dave', deptId: 'eng' },
{ id: '5', name: 'Eve', deptId: 'sales' }
]
},
async getDepartment(deptId: string) {
console.log(`→ db.getDepartment(${deptId})`)
const depts: Record&lt;string, { id: string; name: string }&gt; = {
eng: { id: 'eng', name: 'Engineering' },
sales: { id: 'sales', name: 'Sales' }
}
return depts[deptId]!
},
// Batch version - runtime calls this instead
async getDepartmentBatch(deptIds: string[]) {
console.log(`→ db.getDepartmentBatch([${deptIds.join(', ')}])`)
const depts: Record&lt;string, { id: string; name: string }&gt; = {
eng: { id: 'eng', name: 'Engineering' },
sales: { id: 'sales', name: 'Sales' }
}
return deptIds.map(id =&gt; depts[id]!)
}
}
}
export const entryFunction = 'buildDirectory'</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito run user-directory.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Batched Execution</div><div class="output-content"><pre class="mono">Running: user-directory.burrito.ts
──────────────────────────────────────────────────────────────
→ db.getAllUsers()
→ db.getDepartmentBatch([eng, sales])
Result:
[
{ "userName": "Alice", "departmentName": "Engineering" },
{ "userName": "Bob", "departmentName": "Engineering" },
{ "userName": "Carol", "departmentName": "Sales" },
{ "userName": "Dave", "departmentName": "Engineering" },
{ "userName": "Eve", "departmentName": "Sales" }
]</pre></div></div><div class="section"><div class="section-text"><p><strong>5 users, 2 queries</strong>. Without batching, this would be 6 queries (1 for users + 5 for departments). With batching, it's 2 queries (1 for users + 1 batched department fetch).</p><ul class="summary-list"><li>Notice:</li><li>The business logic didn't change at all</li><li>The runtime automatically batched <code>getDepartment</code> calls</li><li>Duplicate <code>deptId</code> values (3 users in "eng") are deduplicated</li></ul></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>BurritoScript's static analysis detects N+1 patterns. You enable batching in config, and the runtime automatically batches operations—no DataLoader boilerplate, no threading context.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Statement Generator</h3><div class="section-text"><p>MoneyFlow needs to generate monthly statements. For each transaction, we need to fetch the category name. Classic N+1 pattern:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">statement.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface TransactionDB {
getMonthlyTransactions(accountId: string, month: string): Promise&lt;Transaction[]&gt;
getCategory(categoryId: string): Promise&lt;Category&gt;
}
interface Transaction {
id: string
amount: number
categoryId: string
description: string
date: string
}
interface Category {
id: string
name: string
icon: string
}
interface StatementLine {
date: string
description: string
category: string
amount: string
}
export async function generateStatement(
db: TransactionDB,
accountId: string,
month: string
): Promise&lt;StatementLine[]&gt; {
const transactions = await db.getMonthlyTransactions(accountId, month)
const lines: StatementLine[] = []
for (const tx of transactions) {
// N+1: Fetches category for each transaction
const category = await db.getCategory(tx.categoryId)
lines.push({
date: tx.date,
description: tx.description,
category: category.name,
amount: formatCurrency(tx.amount)
})
}
return lines
}
function formatCurrency(cents: number): string {
const sign = cents &lt; 0 ? '-' : ''
return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra perf statement.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Performance Analysis</div><div class="output-content"><pre class="mono">Analyzed 1 files, 50 queries could be saved
statement.burrito.ts:
📊 51 queries → 2 with batching
Potential savings: 49 queries
Batchable operations:
• db.getCategory</pre></div></div><div class="section"><div class="section-text"><p>For a user with 50 transactions, we're making 51 queries when we could make 2. Let's fix it:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">statement.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
batching: {
'db.getCategory': {
enabled: true,
batchKey: 'categoryId',
maxBatchSize: 100,
batchWindowMs: 0 // No delay, batch immediately
}
}
}
export const implementations = {
db: {
async getMonthlyTransactions(accountId: string, month: string) {
// Return mock transactions
return Array.from({ length: 50 }, (_, i) =&gt; ({
id: `tx-${i}`,
amount: -((i + 1) * 100), // $1, $2, $3...
categoryId: ['groceries', 'dining', 'transport', 'utilities'][i % 4]!,
description: `Transaction ${i + 1}`,
date: `${month}-${String(i + 1).padStart(2, '0')}`
}))
},
async getCategory(categoryId: string) {
const categories: Record&lt;string, Category&gt; = {
groceries: { id: 'groceries', name: 'Groceries', icon: '🛒' },
dining: { id: 'dining', name: 'Dining', icon: '🍽️' },
transport: { id: 'transport', name: 'Transport', icon: '🚗' },
utilities: { id: 'utilities', name: 'Utilities', icon: '💡' }
}
return categories[categoryId]!
},
// Batch version
async getCategoryBatch(categoryIds: string[]) {
const categories: Record&lt;string, Category&gt; = {
groceries: { id: 'groceries', name: 'Groceries', icon: '🛒' },
dining: { id: 'dining', name: 'Dining', icon: '🍽️' },
transport: { id: 'transport', name: 'Transport', icon: '🚗' },
utilities: { id: 'utilities', name: 'Utilities', icon: '💡' }
}
return categoryIds.map(id =&gt; categories[id]!)
}
}
}
export const entryFunction = 'generateStatement'
interface Category {
id: string
name: string
icon: string
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito run statement.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">Running: statement.burrito.ts
──────────────────────────────────────────────────────────────
Queries executed:
• db.getMonthlyTransactions(checking, 2024-01) - 1 call
• db.getCategoryBatch([groceries, dining, transport, utilities]) - 1 call
Total: 2 queries (saved 49 queries with batching)
Result: [50 statement lines...]</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow's statement generator went from 51 queries to 2—a 96% reduction—with a single config change. The business logic stays clean and readable while the infrastructure handles optimization.</p></div></div><div class="section"><h3 class="section-title">Let the Analyzer Find Performance Bugs</h3><div class="section-text"><p><strong>N+1 queries are the most common performance bug agents create.</strong></p><p>An agent asked to "fetch users with their posts" will naturally write a loop that queries each user's posts individually. That's the obvious implementation—and it's a performance disaster at scale.</p><p><strong>BurritoScript's approach:</strong> 1. Let the agent write the obvious implementation 2. Run <code>burrito service analyze</code> to detect N+1 patterns 3. Add batching config—no code changes needed</p><p><strong>You don't need to spot N+1 patterns in agent code.</strong> The analyzer finds them automatically and tells you exactly what to add to config.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often get these wrong:</strong></p><p>1. <strong>Missing batch method</strong> - Config says batch, but there's no <code>*Batch</code> method implemented. Show the AI: "Add db.getCategoryBatch(ids: string[]) that fetches all categories in one query."</p><p>2. <strong>Wrong return order</strong> - Batch method must return results in the same order as input IDs. The analyzer will catch this, but make sure the AI knows.</p><p>3. <strong>Not handling missing items</strong> - Batch should return null/undefined for IDs that don't exist, not skip them.</p><p>4. <strong>Forgetting deduplication</strong> - BurritoScript deduplicates automatically, but the batch method should handle duplicate IDs gracefully.</p><p><strong>The fix pattern:</strong> Show the AI the analysis output. "The analyzer found N+1: 50 calls to db.getCategory. Add batching config with batchKey 'id' and implement db.getCategoryBatch."</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>This chapter introduced batching analysis:</p><ul class="summary-list"><li><strong>Analysis:</strong></li><li><code>burrito service analyze &lt;file&gt;</code> - Detect I/O patterns and N+1 queries</li><li><code>burrito infra perf &lt;path&gt;</code> - Find batching opportunities across files</li></ul><p><strong>Batching Config:</strong> <code>`</code>typescript batching: { 'interface.method': { enabled: true, batchKey: 'paramName', // Which param to batch on maxBatchSize: 100, // Max items per batch batchWindowMs: 0 // Delay before executing batch } } <code>`</code></p><p><strong>Batch Function Convention:</strong> If you configure batching for <code>db.getUser</code>, implement <code>db.getUserBatch(ids: string[])</code>. The runtime calls the batch version automatically.</p></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Static analysis</strong> detects N+1 patterns automatically</li><li><strong>Batching config</strong> enables optimization without code changes</li><li><strong>Batch functions</strong> are called automatically by the runtime</li><li><strong>Deduplication</strong> happens automatically (same ID = one lookup)</li><li>N+1 goes from "inevitable performance bug" to "solved in config"</li></ul><p>Next up: <strong>Service + UI Integration</strong>—building full-stack apps by composing what you've learned.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="service-ui"><div class="chapter-label">Chapter 9</div><h1 class="chapter-title">Service + UI</h1><p class="chapter-subtitle">Build full-stack features with reactive data binding.</p><div class="chapter-meta">⏱️ ~24 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>You've learned BurritoService (Chapter 2) and BurritoUI (Chapter 3) separately. Now let's combine them into full-stack features where backend services power reactive UIs.</p><p><strong>Here's why this matters for AI:</strong> Full-stack AI prompting works best in layers. Ask for the service first. Then ask for the UI that calls it. BurritoScript's separation means each prompt has minimal context—better results.</p><p><strong>Why smaller context helps AI succeed:</strong> When you ask for "a full-stack feature," the AI tries to hold service logic, UI rendering, state management, and error handling all in context at once. It gets overwhelmed and makes mistakes. By breaking it into layers—service first, then UI—each prompt has focused context. The AI can get each layer right before moving to the next.</p><p>The key insight: <strong>BurritoUI's static analysis works with service calls</strong>. When your UI calls a service, the analyzer tracks which data flows where—enabling surgical updates when data changes.</p><p><strong>The pattern:</strong> AI writes the service. AI writes the UI. You run <code>burrito ui analyze</code> to verify dependencies. Two prompts, clean separation, verifiable integration.</p><ul class="summary-list"><li>You'll learn to:</li><li>Connect services to UI components</li><li>Handle loading and error states</li><li>Update UIs reactively when data changes</li><li>Build real-time features without manual state management</li></ul></div></div><div class="section"><h3 class="section-title">Mini-App: Todo List with Real-Time Updates</h3><div class="section-text"><p>Let's build a todo list where adding items updates the list instantly. We'll have a service for CRUD operations and a UI that stays in sync.</p><p><strong>Try it:</strong> Ask your AI: "Write a BurritoService handler for todo CRUD operations: getAll, add, toggle, remove." Then ask: "Write a BurritoUI component that calls the todo service and displays the list."</p><p>Two prompts, two files, clean separation.</p><p>First, the service:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">todo-service.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">interface TodoDB {
getAll(): Promise&lt;Todo[]&gt;
add(text: string): Promise&lt;Todo&gt;
toggle(id: string): Promise&lt;Todo&gt;
remove(id: string): Promise&lt;void&gt;
}
interface Todo {
id: string
text: string
completed: boolean
}
export async function getAllTodos(db: TodoDB): Promise&lt;Todo[]&gt; {
return await db.getAll()
}
export async function addTodo(db: TodoDB, text: string): Promise&lt;Todo&gt; {
return await db.add(text)
}
export async function toggleTodo(db: TodoDB, id: string): Promise&lt;Todo&gt; {
return await db.toggle(id)
}
export async function removeTodo(db: TodoDB, id: string): Promise&lt;void&gt; {
await db.remove(id)
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Now the UI. Notice how we call services directly from the render function—BurritoUI tracks these dependencies:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">todo-ui.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">import { h, text, fragment, action } from 'burritoscript/ui'
interface State {
read&lt;T&gt;(path: string[]): Promise&lt;T&gt;
write(path: string[], value: unknown): Promise&lt;void&gt;
}
interface TodoService {
getAll(): Promise&lt;Todo[]&gt;
add(text: string): Promise&lt;Todo&gt;
toggle(id: string): Promise&lt;Todo&gt;
remove(id: string): Promise&lt;void&gt;
}
interface Todo {
id: string
text: string
completed: boolean
}
export async function TodoApp(
state: State,
todoService: TodoService
): Promise&lt;VNode&gt; {
// Fetch current todos
const todos = await todoService.getAll()
const inputText = await state.read&lt;string&gt;(['inputText']) ?? ''
return h('div', { className: 'todo-app' }, [
h('h1', {}, [text('Todo List')]),
// Input form
h('form', {
onSubmit: action('addTodo', { text: inputText })
}, [
h('input', {
type: 'text',
value: inputText,
placeholder: 'What needs to be done?',
onInput: action('updateInput')
}),
h('button', { type: 'submit' }, [text('Add')])
]),
// Todo list
h('ul', { className: 'todo-list' }, [
fragment(todos.map(todo =&gt;
h('li', {
className: todo.completed ? 'completed' : '',
key: todo.id
}, [
h('input', {
type: 'checkbox',
checked: todo.completed,
onChange: action('toggleTodo', { id: todo.id })
}),
h('span', { className: 'todo-text' }, [text(todo.text)]),
h('button', {
className: 'delete-btn',
onClick: action('removeTodo', { id: todo.id })
}, [text('×')])
])
))
]),
// Stats
h('div', { className: 'todo-stats' }, [
text(`${todos.filter(t =&gt; !t.completed).length} items left`)
])
])
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>The magic happens in the action handlers. When <code>addTodo</code> runs, it calls the service, which updates the database—and the UI automatically re-renders with the new data.</p><p><strong>How does re-rendering work?</strong> When a handler calls a service that modifies data, the runtime detects that the data backing the UI has changed. It re-executes the component function (which calls <code>todoService.getAll()</code> again), gets fresh data, and applies surgical updates to the DOM based on what's different. You don't manage this—it just happens.</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">todo-ui.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">export const handlers = {
addTodo: {
async handler(state, todoService, payload) {
const { text } = payload
if (text.trim()) {
await todoService.add(text)
await state.write(['inputText'], '') // Clear input
}
}
},
toggleTodo: {
async handler(state, todoService, payload) {
await todoService.toggle(payload.id)
}
},
removeTodo: {
async handler(state, todoService, payload) {
await todoService.remove(payload.id)
}
},
updateInput: {
async handler(state, todoService, payload) {
await state.write(['inputText'], payload.value)
}
}
}
export const implementations = {
todoService: {
todos: [] as Todo[],
nextId: 1,
async getAll() {
return this.todos
},
async add(text: string) {
const todo = { id: String(this.nextId++), text, completed: false }
this.todos.push(todo)
return todo
},
async toggle(id: string) {
const todo = this.todos.find(t =&gt; t.id === id)!
todo.completed = !todo.completed
return todo
},
async remove(id: string) {
this.todos = this.todos.filter(t =&gt; t.id !== id)
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>The UI calls services directly. When services modify data, the UI re-renders with fresh data. No manual state management, no useEffect, no stale closures—just functions that call functions.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Live Dashboard</h3><div class="section-text"><p>Now let's build MoneyFlow's live dashboard. It shows accounts, recent transactions, and budget status—all updating in real-time as transfers complete.</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">dashboard.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">import { h, text, fragment, action } from 'burritoscript/ui'
interface AccountService {
getAccounts(): Promise&lt;Account[]&gt;
getBalance(accountId: string): Promise&lt;number&gt;
transfer(from: string, to: string, amount: number): Promise&lt;TransferResult&gt;
}
interface TransactionService {
getRecent(accountId: string, limit: number): Promise&lt;Transaction[]&gt;
}
interface BudgetService {
getStatus(): Promise&lt;BudgetStatus&gt;
}
interface Account {
id: string
name: string
type: 'checking' | 'savings' | 'investment'
}
interface Transaction {
id: string
amount: number
description: string
date: string
}
interface TransferResult {
success: boolean
message?: string
}
interface BudgetStatus {
spent: number
limit: number
remaining: number
}
export async function Dashboard(
state: State,
accounts: AccountService,
transactions: TransactionService,
budget: BudgetService
): Promise&lt;VNode&gt; {
// Fetch all data in parallel
const [accountList, budgetStatus] = await Promise.all([
accounts.getAccounts(),
budget.getStatus()
])
const selectedId = await state.read&lt;string&gt;(['selectedAccountId'])
const selectedAccount = accountList.find(a =&gt; a.id === selectedId)
// Get recent transactions for selected account
const recentTxs = selectedAccount
? await transactions.getRecent(selectedId!, 5)
: []
return h('div', { className: 'dashboard' }, [
// Header with budget status
h('header', { className: 'dashboard-header' }, [
h('h1', {}, [text('MoneyFlow')]),
h('div', { className: 'budget-pill' }, [
text(`Budget: $${budgetStatus.remaining} remaining`)
])
]),
// Main content
h('div', { className: 'dashboard-content' }, [
// Account list sidebar
h('aside', { className: 'account-sidebar' }, [
h('h2', {}, [text('Accounts')]),
h('ul', { className: 'account-list' }, [
fragment(accountList.map(account =&gt;
h('li', {
className: `account-item ${selectedId === account.id ? 'selected' : ''}`,
onClick: action('selectAccount', { id: account.id })
}, [
h('span', { className: 'account-name' }, [text(account.name)]),
h('span', { className: 'account-type' }, [text(account.type)])
])
))
])
]),
// Selected account detail
h('main', { className: 'account-detail' }, [
selectedAccount
? fragment([
h('h2', {}, [text(selectedAccount.name)]),
h('div', { className: 'balance-card' }, [
h('span', { className: 'balance-label' }, [text('Current Balance')]),
h('span', { className: 'balance-amount', id: 'balance-display' }, [
text(`$${await accounts.getBalance(selectedId!)}`)
])
]),
// Quick transfer form
h('div', { className: 'quick-transfer' }, [
h('h3', {}, [text('Quick Transfer')]),
h('form', { onSubmit: action('transfer') }, [
h('select', { name: 'toAccount' }, [
fragment(accountList
.filter(a =&gt; a.id !== selectedId)
.map(a =&gt; h('option', { value: a.id }, [text(a.name)]))
)
]),
h('input', {
type: 'number',
name: 'amount',
placeholder: 'Amount'
}),
h('button', { type: 'submit' }, [text('Transfer')])
])
]),
// Recent transactions
h('div', { className: 'recent-transactions' }, [
h('h3', {}, [text('Recent Transactions')]),
h('ul', { className: 'transaction-list' }, [
fragment(recentTxs.map(tx =&gt;
h('li', { className: 'transaction-item' }, [
h('span', { className: 'tx-desc' }, [text(tx.description)]),
h('span', {
className: `tx-amount ${tx.amount &lt; 0 ? 'negative' : 'positive'}`
}, [text(formatCurrency(tx.amount))])
])
))
])
])
])
: h('div', { className: 'no-selection' }, [
text('Select an account to view details')
])
])
])
])
}
function formatCurrency(cents: number): string {
const sign = cents &lt; 0 ? '-' : '+'
return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Let's analyze what happens when data changes. Run the analyzer:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito ui analyze dashboard.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">UI Analysis</div><div class="output-content"><pre class="mono">dashboard.burrito.ts
═══════════════════════════════════════════════════════════════
Data Dependencies:
accounts.getAccounts() → .account-list, .account-sidebar
accounts.getBalance(selectedId) → #balance-display
transactions.getRecent(selectedId) → .transaction-list
budget.getStatus() → .budget-pill
state['selectedAccountId'] → .account-item.selected, .account-detail
Update Impact:
When accounts.getAccounts() changes → re-render sidebar only
When accounts.getBalance() changes → update balance display only
When state['selectedAccountId'] changes → update selected class + detail pane
No full re-renders needed. All updates are surgical.</pre></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow's dashboard fetches data from multiple services, and the static analyzer tracks exactly which DOM elements depend on which data. Transfers update balances without re-rendering the entire page.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often struggle with:</strong></p><p>1. <strong>State synchronization</strong> - Don't manually sync state. Let the UI re-fetch from services. Show the AI: "The UI should call the service in render, not cache state."</p><p>2. <strong>Optimistic updates</strong> - If you want optimistic UI, specify it in the prompt. Default is wait for service response.</p><p>3. <strong>Error handling in UI</strong> - Make sure actions handle service errors and update UI accordingly. The analyzer will show if error paths are missing.</p><p>4. <strong>Missing action handlers</strong> - Every <code>action('name')</code> in UI needs a matching handler in config. Run <code>burrito ui validate</code> to catch this.</p><p><strong>The fix pattern:</strong> Break full-stack prompts into layers. "Write the service first, then the UI that calls it." Each prompt has minimal context—better results.</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>This chapter introduced UI analysis:</p><ul class="summary-list"><li><strong>Analysis:</strong></li><li><code>burrito ui analyze &lt;file&gt;</code> - Show data dependencies and update impact</li></ul><ul class="summary-list"><li><strong>Patterns:</strong></li><li>Call services directly in render functions</li><li>Use <code>action()</code> to trigger handlers</li><li>Handlers call services and update state</li><li>UI re-renders with fresh data automatically</li></ul><p><strong>Key Insight:</strong> Services become the source of truth. UI is a function of service data. No manual synchronization needed.</p></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Services power UIs</strong> - call services directly in render</li><li><strong>Actions trigger updates</strong> - handlers modify data, UI follows</li><li><strong>Static analysis</strong> tracks data → DOM dependencies</li><li><strong>Surgical updates</strong> - only affected elements re-render</li><li>No useState, no useEffect, no stale closures</li></ul><p>Next up: <strong>Mission + Service</strong>—data models that generate APIs automatically.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="mission-service"><div class="chapter-label">Chapter 10</div><h1 class="chapter-title">Mission + Service</h1><p class="chapter-subtitle">Data models that generate type-safe service APIs.</p><div class="chapter-meta">⏱️ ~22 min read</div></div><div class="section"><h3 class="section-title">What You'll Learn</h3><div class="section-text"><p>In Chapter 4, you learned Mission for declarative data models. In Chapter 2, you learned BurritoService for backend handlers. Now let's combine them: <strong>data models that generate service APIs</strong>.</p><p><strong>Here's the key insight:</strong> Don't review generated CRUD—it's mechanical. Focus your attention on custom business logic. This is how you scale AI-assisted development.</p><p><strong>Why this helps AI succeed:</strong> CRUD is exactly what AI is good at—mechanical, pattern-based code. But reviewing CRUD defeats the purpose. By generating it automatically, you eliminate review time for the code AI handles best. You focus review on custom logic where AI needs more guidance. <strong>Smaller review surface = faster iteration = AI gets to correctness faster.</strong></p><ul class="summary-list"><li>Instead of writing CRUD boilerplate, you define the schema once. Mission generates:</li><li>Type-safe service methods</li><li>GraphQL resolvers</li><li>Permission checks</li><li>Validation hooks</li></ul><p><strong>The pattern:</strong> AI writes the Mission resource. Mission generates CRUD. AI writes custom handlers. You review only the custom handlers—the generated CRUD is trusted.</p><ul class="summary-list"><li>You'll learn to:</li><li>Connect Mission resources to services</li><li>Use generated CRUD operations</li><li>Add custom service methods alongside generated ones</li><li>Keep business logic separate from data access</li></ul></div></div><div class="section"><h3 class="section-title">Mini-App: User Management CRUD</h3><div class="section-text"><p>Let's build a user management system. Define the model once, get a full API for free.</p><p><strong>Try it:</strong> Ask your AI: "Create a Mission resource for User with CRUD operations. Then write a custom handler promoteToAdmin that updates the role." Notice how you review only the custom handler—the generated CRUD is trusted.</p><p>First, the Mission resource:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">user.mission.ts</div><div class="code-block"><pre><code class="language-typescript">import { defineResource, hasMany } from 'burritoscript/mission'
import { z } from 'zod'
export const User = defineResource('users', {
typeName: 'User',
schema: z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'member', 'guest']),
createdAt: z.date().default(() =&gt; new Date()),
updatedAt: z.date().default(() =&gt; new Date())
}),
relations: {
posts: hasMany('posts', 'authorId'),
comments: hasMany('comments', 'userId')
},
permissions: {
read: 'users:read',
create: 'users:create',
update: 'users:update',
delete: 'users:delete'
},
// Validation hooks
beforeCreate: async (db, ctx, input) =&gt; {
// Check email uniqueness
const existing = await db.users.findBy({ email: input.email })
if (existing) {
throw new Error('Email already registered')
}
return input
},
beforeUpdate: async (db, ctx, id, input) =&gt; {
// Set updatedAt timestamp
return { ...input, updatedAt: new Date() }
}
})</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Mission generates a complete service from this definition. Let's see what we get:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito mission generate user.mission.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Generated Service</div><div class="output-content"><pre class="mono">Generated: user.service.ts
Available methods:
• users.findById(id: string): Promise&lt;User | null&gt;
• users.findMany(filter?: UserFilter): Promise&lt;User[]&gt;
• users.create(input: CreateUserInput): Promise&lt;User&gt;
• users.update(id: string, input: UpdateUserInput): Promise&lt;User&gt;
• users.delete(id: string): Promise&lt;void&gt;
GraphQL types generated:
• type User { id, email, name, role, posts, comments }
• input CreateUserInput { email, name, role }
• input UpdateUserInput { email?, name?, role? }
• input UserFilter { email?, role?, limit?, offset? }
Permission checks applied:
• findById, findMany → requires 'users:read'
• create → requires 'users:create'
• update → requires 'users:update'
• delete → requires 'users:delete'</pre></div></div><div class="section"><div class="section-text"><p>Now use these generated methods in a service handler:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">user-management.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">// Import generated service
import { users } from './user.service'
interface Logger {
info(message: string, data?: unknown): Promise&lt;void&gt;
}
// Use generated CRUD - type-safe!
export async function listUsers(
filter?: { role?: string; limit?: number }
): Promise&lt;User[]&gt; {
return await users.findMany(filter)
}
export async function getUser(id: string): Promise&lt;User | null&gt; {
return await users.findById(id)
}
export async function createUser(
input: { email: string; name: string; role: 'admin' | 'member' | 'guest' }
): Promise&lt;User&gt; {
// Validation hook (beforeCreate) runs automatically
return await users.create(input)
}
export async function updateUser(
id: string,
input: { name?: string; role?: 'admin' | 'member' | 'guest' }
): Promise&lt;User&gt; {
// beforeUpdate hook sets updatedAt
return await users.update(id, input)
}
// Custom business logic alongside generated methods
export async function promoteToAdmin(
logger: Logger,
userId: string
): Promise&lt;User&gt; {
const user = await users.findById(userId)
if (!user) {
throw new Error('User not found')
}
if (user.role === 'admin') {
throw new Error('User is already an admin')
}
await logger.info('Promoting user to admin', { userId, previousRole: user.role })
return await users.update(userId, { role: 'admin' })
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Mini-App Takeaway</div><p>Define the data model once in Mission, get type-safe CRUD methods automatically. Add custom business logic as separate service handlers. The generated code handles validation, permissions, and timestamps.</p></div></div><div class="section"><h3 class="section-title">Main App: MoneyFlow - Budget Rules</h3><div class="section-text"><p>MoneyFlow needs budget rules: spending limits per category, alerts when approaching limits, automatic categorization. Let's build it with Mission + Service.</p><p>First, the data models:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">budget.mission.ts</div><div class="code-block"><pre><code class="language-typescript">import { defineResource, belongsTo, hasMany } from 'burritoscript/mission'
import { z } from 'zod'
export const BudgetRule = defineResource('budget_rules', {
typeName: 'BudgetRule',
schema: z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
categoryId: z.string().uuid(),
monthlyLimit: z.number().positive(),
alertThreshold: z.number().min(0).max(100).default(80), // % of limit
isActive: z.boolean().default(true),
createdAt: z.date().default(() =&gt; new Date())
}),
relations: {
user: belongsTo('users', 'userId'),
category: belongsTo('categories', 'categoryId'),
alerts: hasMany('budget_alerts', 'ruleId')
},
// Permissions: what capability is required
permissions: {
read: 'budget:read:own', // Needs 'budget:read:own' capability
create: 'budget:create',
update: 'budget:update:own',
delete: 'budget:delete:own'
},
// Scopes: what data is visible (filters the query)
// 'own' means the query is automatically filtered to userId = currentUser
scopes: {
read: 'own',
update: 'own',
delete: 'own'
}
})
export const BudgetAlert = defineResource('budget_alerts', {
typeName: 'BudgetAlert',
schema: z.object({
id: z.string().uuid(),
ruleId: z.string().uuid(),
triggeredAt: z.date().default(() =&gt; new Date()),
spentAmount: z.number(),
limitAmount: z.number(),
percentUsed: z.number(),
acknowledged: z.boolean().default(false)
}),
relations: {
rule: belongsTo('budget_rules', 'ruleId')
},
permissions: {
read: 'budget:read:own',
update: 'budget:update:own'
}
})</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Now the service that uses these models to implement budget tracking:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">budget-service.burrito.ts</div><div class="code-block"><pre><code class="language-typescript">import { budgetRules, budgetAlerts } from './budget.service'
import { transactions } from './transaction.service'
import { categories } from './category.service'
interface NotificationService {
send(userId: string, message: string, type: 'info' | 'warning' | 'alert'): Promise&lt;void&gt;
}
// Get spending status for all budget rules
export async function getBudgetStatus(
userId: string
): Promise&lt;BudgetStatus[]&gt; {
const rules = await budgetRules.findMany({ userId, isActive: true })
const statuses: BudgetStatus[] = []
for (const rule of rules) {
const category = await categories.findById(rule.categoryId)
const monthlySpent = await getMonthlySpending(userId, rule.categoryId)
statuses.push({
ruleId: rule.id,
categoryName: category?.name ?? 'Unknown',
monthlyLimit: rule.monthlyLimit,
spent: monthlySpent,
remaining: rule.monthlyLimit - monthlySpent,
percentUsed: (monthlySpent / rule.monthlyLimit) * 100,
isOverBudget: monthlySpent &gt; rule.monthlyLimit
})
}
return statuses
}
// Check if a transaction would trigger budget alerts
export async function checkBudgetOnTransaction(
notifications: NotificationService,
userId: string,
categoryId: string,
amount: number
): Promise&lt;{ allowed: boolean; alerts: BudgetAlert[] }&gt; {
const rule = await budgetRules.findOne({ userId, categoryId, isActive: true })
if (!rule) {
return { allowed: true, alerts: [] }
}
const currentSpent = await getMonthlySpending(userId, categoryId)
const newTotal = currentSpent + amount
const percentUsed = (newTotal / rule.monthlyLimit) * 100
const alerts: BudgetAlert[] = []
// Check alert threshold
if (percentUsed &gt;= rule.alertThreshold &amp;&amp; (currentSpent / rule.monthlyLimit) * 100 &lt; rule.alertThreshold) {
const alert = await budgetAlerts.create({
ruleId: rule.id,
spentAmount: newTotal,
limitAmount: rule.monthlyLimit,
percentUsed
})
alerts.push(alert)
const category = await categories.findById(categoryId)
await notifications.send(
userId,
`You've used ${percentUsed.toFixed(0)}% of your ${category?.name} budget`,
'warning'
)
}
// Check if over budget
if (newTotal &gt; rule.monthlyLimit) {
await notifications.send(
userId,
`This purchase would exceed your monthly budget!`,
'alert'
)
}
return { allowed: true, alerts }
}
// Helper: Get monthly spending for a category
async function getMonthlySpending(userId: string, categoryId: string): Promise&lt;number&gt; {
const startOfMonth = new Date()
startOfMonth.setDate(1)
startOfMonth.setHours(0, 0, 0, 0)
const txs = await transactions.findMany({
userId,
categoryId,
dateAfter: startOfMonth
})
return txs.reduce((sum, tx) =&gt; sum + Math.abs(tx.amount), 0)
}
interface BudgetStatus {
ruleId: string
categoryName: string
monthlyLimit: number
spent: number
remaining: number
percentUsed: number
isOverBudget: boolean
}
interface BudgetAlert {
id: string
ruleId: string
triggeredAt: Date
spentAmount: number
limitAmount: number
percentUsed: number
acknowledged: boolean
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>The config ties everything together with permission checks and resilience:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">budget-service.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
permissions: {
'budget-service.burrito.ts': {
getBudgetStatus: {
requires: ['budget:read:own']
},
checkBudgetOnTransaction: {
requires: ['budget:read:own', 'transactions:read:own']
}
}
},
// Batch the category lookups for performance
batching: {
'categories.findById': {
enabled: true,
batchKey: 'id',
maxBatchSize: 50
}
},
// Ensure budget checks don't race with transactions
safety: {
'budget-service.burrito.ts': {
concurrency: 'transaction'
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Main App Takeaway</div><p>MoneyFlow's budget system combines Mission models (BudgetRule, BudgetAlert) with service handlers that implement business logic. Generated CRUD handles data access; custom handlers add domain-specific behavior like alert thresholds and notifications.</p></div></div><div class="section"><h3 class="section-title">Let Mission Generate the Boilerplate</h3><div class="section-text"><p><strong>CRUD operations are exactly what agents are good at—and what you shouldn't spend time reviewing.</strong></p><p>Mission generates type-safe CRUD automatically. The agent's job becomes: 1. Define the data model (schema, relations, permissions) 2. Write custom business logic where generated CRUD isn't enough</p><ul class="summary-list"><li><strong>The review pattern:</strong></li><li>Generated CRUD: Trust it (it's mechanical)</li><li>Custom handlers: Review these carefully</li><li>Hooks: Check validation logic</li></ul></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Prompting Guide</div><p><strong>For data models:</strong> &gt; "Create a Mission resource for BudgetRule with fields: categoryId, monthlyLimit, warningThreshold. Add a belongsTo relation to Category. Scope reads to own user."</p><p><strong>For custom handlers:</strong> &gt; "Add a custom handler checkBudget that gets spending for a category and compares it to the budget rule. Return whether the user is over, near, or under budget."</p><p><strong>Key insight:</strong> Ask for the schema first, then ask for custom handlers. Don't mix model definition with business logic prompts.</p></div></div><div class="section"><h3 class="section-title">Common Issues: What Goes Wrong</h3><div class="section-text"><p><strong>Agents often:</strong></p><p>1. <strong>Write CRUD by hand</strong> - If you see findById, create, update, delete implementations, ask for a Mission resource instead. Show the AI: "Use Mission to generate CRUD, then add custom handlers."</p><p>2. <strong>Forget permission scopes</strong> - Generated methods check permissions. Make sure scopes are set correctly. Test the generated GraphQL to verify scoping.</p><p>3. <strong>Mix concerns in hooks</strong> - Hooks should validate/transform, not implement business logic. Keep that in handlers. Show the AI: "Move this logic to a custom handler, not a hook."</p><p>4. <strong>Duplicate generated methods</strong> - No need to write findMany if Mission generates it. Check the generated service file first.</p><p><strong>The fix pattern:</strong> Trust generated CRUD. Review custom handlers. Show the AI what's wrong: "This should use Mission's generated findById, not a custom implementation."</p></div></div><div class="section"><h3 class="section-title">Commands Introduced</h3><div class="section-text"><p>This chapter introduced Mission + Service integration:</p><ul class="summary-list"><li><strong>Generation:</strong></li><li><code>mission schema &lt;file&gt;</code> - Generate Drizzle schema from Mission resource</li></ul><p><strong>Generated Methods:</strong> <code>`</code>typescript resource.findById(id) // Single record by ID resource.findOne(filter) // Single record by filter resource.findMany(filter) // Multiple records resource.create(input) // Create with validation resource.update(id, input) // Update with hooks resource.delete(id) // Delete with permission check <code>`</code></p><ul class="summary-list"><li><strong>Permission Scopes:</strong></li><li><code>own</code> - User can only access their own resources</li><li><code>any</code> - User can access all resources (if permitted)</li></ul></div></div><div class="section"><h3 class="section-title">What You Learned</h3><div class="section-text"><ul class="summary-list"><li><strong>Mission generates services</strong> - define schema, get type-safe CRUD</li><li><strong>Hooks add custom logic</strong> - beforeCreate, beforeUpdate, afterDelete</li><li><strong>Scopes enforce access</strong> - <code>own</code> scope filters to user's data</li><li><strong>Custom handlers</strong> complement generated ones</li><li>Data access and business logic stay cleanly separated</li></ul><p>Next up: <strong>Complete Application</strong>—seeing how all 11 chapters fit together.</p></div></div></div><div class="chapter-wrapper"><div class="chapter-header" id="complete-app"><div class="chapter-label">Chapter 11</div><h1 class="chapter-title">Complete Application</h1><p class="chapter-subtitle">MoneyFlow: Everything you've learned, working together.</p><div class="chapter-meta">⏱️ ~18 min read</div></div><div class="section"><h3 class="section-title">The Full Picture</h3><div class="section-text"><p>You've built MoneyFlow piece by piece across 10 chapters. Now let's see how everything fits together into a complete, production-ready application.</p><p><strong>Remember the pattern from Chapter 1:</strong> You tell the agent what to build. BurritoScript tells you if the agent built it correctly. This chapter shows that pattern working across the entire application.</p><ul class="summary-list"><li>MoneyFlow is a personal finance app with:</li><li><strong>Account management</strong> with real-time balance updates</li><li><strong>Money transfers</strong> with race condition protection</li><li><strong>Budget tracking</strong> with category-based spending limits</li><li><strong>Transaction history</strong> with automatic categorization</li><li><strong>Live dashboard</strong> with surgical UI updates</li><li><strong>Complete audit trail</strong> via provenance tracking</li></ul><p>Every feature was built using the same workflow: describe → generate → verify → ship.</p></div></div><div class="section"><h3 class="section-title">Architecture Overview</h3><div class="section-text"><p>Here's how the application types and cross-cutting features compose:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">MoneyFlow Architecture</div><div class="code-block"><pre><code class="language-text">┌─────────────────────────────────────────────────────────────────────┐
│ MoneyFlow Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ BurritoUI │────▶│ BurritoService │ │
│ │ (Chapter 3) │ │ (Chapter 2) │ │
│ │ │ │ │ │
│ │ • Dashboard │ │ • Transfer handler │ │
│ │ • Account list │ │ • Budget checker │ │
│ │ • Transaction │ │ • Statement generator │ │
│ │ history │ │ │ │
│ └────────────────────┘ └────────────────┬───────────────────┘ │
│ │ │ │
│ │ burrito ui analyze │ Generated CRUD │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ Static Analysis │ │ Mission │ │
│ │ │ │ (Chapter 4) │ │
│ │ • Data deps │ │ │ │
│ │ • Update impact │ │ • Account model │ │
│ │ • Surgical render │ │ • Transaction model │ │
│ └────────────────────┘ │ • BudgetRule model │ │
│ │ • Category model │ │
│ └────────────────────────────────────┘ │
│ │
├─────────────── Cross-Cutting Features ──────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ Provenance │ │ Verification │ │ Automatic Batching │ │
│ │ (Chapter 6) │ │ (Chapter 7) │ │ (Chapter 8) │ │
│ │ │ │ │ │ │ │
│ │ • trace cmd │ │ • infra verify │ │ • N+1 elimination │ │
│ │ • debug cmd │ │ • TLA+ specs │ │ • Batch config │ │
│ │ • explain │ │ • Safety config │ │ • 96% query savings │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘</code></pre></div></div><div class="section"><h3 class="section-title">File Organization</h3><div class="section-text"><p>Here's how MoneyFlow's files are organized:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">Project Structure</div><div class="code-block"><pre><code class="language-text">moneyflow/
├── src/
│ ├── models/ # Mission data models
│ │ ├── account.mission.ts # Account schema + relations
│ │ ├── transaction.mission.ts # Transaction with categories
│ │ ├── budget-rule.mission.ts # Budget limits + alerts
│ │ └── category.mission.ts # Spending categories
│ │
│ ├── services/ # BurritoService handlers
│ │ ├── account.burrito.ts # Balance, transfer
│ │ ├── account.burrito.config.ts # Safety: serializable
│ │ ├── budget.burrito.ts # Budget checking
│ │ ├── budget.burrito.config.ts # Permissions: own scope
│ │ ├── statement.burrito.ts # Monthly statements
│ │ └── statement.burrito.config.ts # Batching: categories
│ │
│ ├── ui/ # BurritoUI components
│ │ ├── dashboard.burrito.ts # Main dashboard
│ │ ├── account-detail.burrito.ts # Account view
│ │ ├── transfer-form.burrito.ts # Transfer UI
│ │ └── budget-status.burrito.ts # Budget display
│ │
│ └── generated/ # Auto-generated
│ ├── account.service.ts # From account.mission.ts
│ ├── transaction.service.ts # From transaction.mission.ts
│ └── schema.graphql # GraphQL schema
├── burrito.config.ts # Global config
└── package.json</code></pre></div></div><div class="section"><h3 class="section-title">Safety: Race Condition Protection</h3><div class="section-text"><p>MoneyFlow's transfer operation is protected against race conditions. Here's the complete safety configuration:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">account.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
safety: {
// Transfer must be serializable - no concurrent transfers
'account.burrito.ts#transfer': {
concurrency: 'serializable',
invariant: 'total_balance_conserved'
},
// Withdrawals also need protection
'account.burrito.ts#withdraw': {
concurrency: 'transaction'
}
},
verification: {
'account.burrito.ts#transfer': {
invariants: [
'fromBalance + toBalance === initialTotal',
'fromBalance &gt;= 0',
'toBalance &gt;= 0'
],
tlaVerify: {
instances: 2,
initialState: { fromBalance: 100, toBalance: 100 }
}
}
},
resilience: {
'bank.*': {
retry: { attempts: 3, backoff: 'exponential' },
timeout: '5s',
circuitBreaker: {
failureThreshold: 5,
resetTimeout: '30s'
}
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="section"><div class="section-text"><p>Verify that the safety configuration protects against race conditions:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra verify-config src/services/account.burrito.ts</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">account.burrito.ts#transfer: ✓ Fixed
Invariant: total_balance_conserved
Verified with 2 concurrent instances
account.burrito.ts#withdraw: ✓ Fixed
Transaction wrapper applied</pre></div></div><div class="section"><h3 class="section-title">Performance: Automatic Batching</h3><div class="section-text"><p>Statement generation was making N+1 queries. The batching config fixed it:</p></div></div><div class="code-block-wrapper"><div class="code-filename mono">statement.burrito.config.ts</div><div class="code-block"><pre><code class="language-typescript">import { BurritoConfig } from 'burritoscript'
export const config: BurritoConfig = {
batching: {
// Batch category lookups in statements
'categories.findById': {
enabled: true,
batchKey: 'id',
maxBatchSize: 100
},
// Batch merchant lookups
'merchants.findById': {
enabled: true,
batchKey: 'id',
maxBatchSize: 100
}
}
}</code></pre><button class="copy-btn">Copy</button></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito infra perf src/services/</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">Analyzed 4 files, 0 queries could be saved
All batching opportunities are configured. ✓
Current optimization:
statement.burrito.ts: 51 queries → 2 (96% reduction)
budget.burrito.ts: 12 queries → 3 (75% reduction)</pre></div></div><div class="section"><h3 class="section-title">Debugging: Provenance Tracking</h3><div class="section-text"><p>Every computation in MoneyFlow has provenance. When a user reports a budget miscalculation:</p></div></div><div class="code-block-wrapper"><div class="code-block"><pre><code class="language-bash">pnpm burrito debug traces/user-123-budget-2024-01.json</code></pre><button class="copy-btn">Copy</button></div></div><div class="output-block"><div class="output-label">Output</div><div class="output-content"><pre class="mono">Trace loaded: 47 nodes
debug&gt; explain spent
n23: spent = 185000 ← sum()
├─ n15: tx1.amount = 50000 ← transactions.findMany()
├─ n16: tx2.amount = 35000 ← transactions.findMany()
├─ n17: tx3.amount = 100000 ← transactions.findMany() ← Suspicious!
└─ ... (12 more)
debug&gt; snippet n17
n17 at budget.burrito.ts:45
→ 45 │ const txs = await transactions.findMany({ userId, categoryId })
Scope:
userId = "user-123"
categoryId = "dining" ← Wrong category!
Result:
[{ amount: 100000, description: "Rent payment", ... }]</pre></div></div><div class="section"><div class="section-text"><p>The provenance trace reveals that a $1000 rent payment was miscategorized as "dining", inflating the budget spend. This kind of bug is invisible without provenance tracking.</p></div></div><div class="section"><h3 class="section-title">What You Built</h3><div class="section-text"><p>Over 11 chapters, you built a complete application with:</p><ul class="summary-list"><li><strong>Application Types (Chapters 1-5):</strong></li><li><strong>Programs</strong> - Async/await functions with automatic tracing</li><li><strong>Services</strong> - Backend handlers with safety + resilience</li><li><strong>UI</strong> - Reactive components with surgical updates</li><li><strong>Mission</strong> - Declarative data models with generated APIs</li><li><strong>Simulation</strong> - Physics and economics visualizations</li></ul><ul class="summary-list"><li><strong>Cross-Cutting Features (Chapters 6-8):</strong></li><li><strong>Provenance</strong> - Trace any value to its source</li><li><strong>Verification</strong> - Prove concurrent code is race-free</li><li><strong>Batching</strong> - Eliminate N+1 queries automatically</li></ul><ul class="summary-list"><li><strong>Integration (Chapters 9-11):</strong></li><li><strong>Service + UI</strong> - Full-stack features with reactive data</li><li><strong>Mission + Service</strong> - Data models that generate APIs</li><li><strong>Complete App</strong> - Everything working together</li></ul></div></div><div class="section"><h3 class="section-title">The Agent-First Development Loop</h3><div class="section-text"><p><strong>Here's the workflow that makes BurritoScript ideal for AI-assisted development:</strong></p><p>1. <strong>Describe what you want</strong> → Agent writes business logic 2. <strong>Run analysis</strong> → BurritoScript finds issues automatically 3. <strong>Show agent the output</strong> → "Add batching for this N+1" or "Fix this race condition" 4. <strong>Add config</strong> → Safety, batching, resilience without code changes 5. <strong>Verify</strong> → Provenance, verification, performance analysis 6. <strong>Ship</strong> → Confidence without reading every line</p><p><strong>The key insight:</strong> You tell the agent what to build. BurritoScript tells you if the agent built it correctly.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Verification Tools Summary</div><p>| What You Want to Verify | Command | |------------------------|---------| | "Did the agent wire data correctly?" | <code>burrito trace &lt;file&gt;</code> | | "Is this concurrent code race-free?" | <code>burrito infra race &lt;file&gt; --invariant="..."</code> | | "Does this have N+1 queries?" | <code>burrito service analyze &lt;file&gt;</code> | | "What DOM updates will happen?" | <code>burrito ui analyze &lt;file&gt;</code> | | "Is the config valid?" | <code>burrito infra verify-config &lt;file&gt;</code> |</p><p><strong>No need to read the code.</strong> Let the tools verify correctness.</p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Why This Architecture Matters</div><p><strong>Smaller files = better agent results.</strong></p><ul class="summary-list"><li>BurritoScript's two-file pattern means:</li><li>Business logic has no infrastructure noise</li><li>Config changes don't touch business logic</li><li>Agents work with focused context (one concern at a time)</li><li>Verification can happen file-by-file</li></ul><p><strong>The separation of concerns isn't just good architecture—it's optimal for AI-assisted development.</strong></p></div></div><div class="tip-block"><span class="tip-icon">💡</span><div class="tip-content"><div class="tip-title">Key Insights</div><p><strong>Separation of concerns works.</strong> Business logic in .burrito.ts files stays clean. Infrastructure concerns (safety, batching, resilience) live in .burrito.config.ts files. Change one without touching the other.</p><p><strong>Static analysis enables optimization.</strong> The compiler sees your code structure and applies optimizations automatically—surgical UI updates, automatic batching, race condition detection.</p><p><strong>Config-driven infrastructure.</strong> Instead of sprinkling try/catch, DataLoader, and mutex locks throughout your code, declare what you need in config. The runtime handles the rest.</p><p><strong>Provenance is your debugging superpower.</strong> When something goes wrong, you can trace any value back to its source. No more printf debugging.</p></div></div><div class="section"><h3 class="section-title">Next Steps</h3><div class="section-text"><p>You've completed the BurritoScript tutorial. Here's where to go next:</p><ul class="summary-list"><li><strong>Explore the CLI:</strong></li><li><code>burrito --help</code> - All CLI commands</li><li><code>burrito mission --help</code> - Data model commands</li><li><code>burrito infra --help</code> - Infrastructure commands</li><li><code>burrito service --help</code> - Service analysis and running</li></ul><ul class="summary-list"><li><strong>Build something:</strong></li><li>Start with a simple Program (Chapter 1 pattern)</li><li>Add Services for backend logic (Chapter 2 pattern)</li><li>Layer in UI for frontend (Chapter 3 pattern)</li><li>Use Mission for data models (Chapter 4 pattern)</li><li>Enable cross-cutting features as needed (Chapters 6-8)</li></ul><p><strong>Explore the demos:</strong> The <code>demos/</code> directory contains complete examples showing each application type and feature in action. Start by running them and reading their source.</p><p>Welcome to BurritoScript. Build with confidence.</p></div></div><div class="section"><h3 class="section-title">Chapter Summary</h3><div class="section-text"><p>What you learned across 11 chapters:</p><ul class="summary-list"><li><strong>Chapter 1:</strong> BurritoScript programs with effect interfaces</li><li><strong>Chapter 2:</strong> Services with safety and resilience config</li><li><strong>Chapter 3:</strong> UI with static analysis and surgical updates</li><li><strong>Chapter 4:</strong> Mission data models with generated GraphQL</li><li><strong>Chapter 5:</strong> Simulations with state, time, and canvas effects</li><li><strong>Chapter 6:</strong> Provenance debugging with trace, explain, snippet</li><li><strong>Chapter 7:</strong> Verification with exhaustive interleavings and TLA+</li><li><strong>Chapter 8:</strong> Automatic batching that eliminates N+1 queries</li><li><strong>Chapter 9:</strong> Service + UI integration for full-stack features</li><li><strong>Chapter 10:</strong> Mission + Service for data-driven APIs</li><li><strong>Chapter 11:</strong> Complete application architecture</li></ul><p>MoneyFlow is now a production-ready personal finance app with verified concurrent safety, optimized queries, and complete audit trails.</p><p><strong>Happy building!</strong></p></div></div></div></main></div><script>document.querySelectorAll(".copy-btn").forEach(btn => {
btn.addEventListener("click", async () => {
const codeBlock = btn.closest(".code-block, .prompt-content");
const code = codeBlock.querySelector("pre").textContent;
try {
await navigator.clipboard.writeText(code);
btn.textContent = "Copied!";
setTimeout(() => { btn.textContent = "Copy"; }, 2000);
} catch (e) {
btn.textContent = "Failed";
setTimeout(() => { btn.textContent = "Copy"; }, 2000);
}
});
});
// Mobile sidebar toggle
const hamburger = document.querySelector('.hamburger');
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay');
function openSidebar() {
sidebar.classList.add('open');
overlay.classList.add('visible');
// Save state to sessionStorage
sessionStorage.setItem('sidebar-open', 'true');
}
function closeSidebar() {
sidebar.classList.remove('open');
overlay.classList.remove('visible');
sessionStorage.setItem('sidebar-open', 'false');
}
if (hamburger) {
hamburger.addEventListener('click', () => {
if (sidebar.classList.contains('open')) {
closeSidebar();
} else {
openSidebar();
}
});
}
if (overlay) {
overlay.addEventListener('click', closeSidebar);
}
// Close sidebar when clicking a chapter link (mobile)
document.querySelectorAll('.chapter-link').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 768) {
closeSidebar();
}
});
});
// Restore sidebar state on mobile
if (window.innerWidth <= 768 && sessionStorage.getItem('sidebar-open') === 'true') {
openSidebar();
}
</script><script>
(function() {
function highlightAll() {
if (window.hljs && typeof window.hljs.highlightAll === 'function') {
try {
window.hljs.highlightAll();
var highlighted = document.querySelectorAll('code.hljs').length;
console.log('Highlight.js highlighting applied to ' + highlighted + ' blocks');
} catch (e) {
console.error('Highlight.js error:', e);
// Retry once
setTimeout(function() {
if (window.hljs && typeof window.hljs.highlightAll === 'function') {
window.hljs.highlightAll();
}
}, 500);
}
} else {
// Retry if highlight.js isn't ready yet
setTimeout(highlightAll, 200);
}
}
function init() {
// Load highlight.js core
var coreScript = document.createElement('script');
coreScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
coreScript.onload = function() {
if (!window.hljs) {
console.error('hljs not available after script load');
return;
}
// Load language definitions
var languages = ['typescript', 'bash', 'graphql', 'json'];
var langScripts = languages.map(function(lang) {
return 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/' + lang + '.min.js';
});
var loaded = 0;
var total = langScripts.length;
function checkDone() {
loaded++;
if (loaded === total) {
// All languages loaded, highlight now
setTimeout(highlightAll, 300);
}
}
// Load all language scripts
langScripts.forEach(function(src) {
var langScript = document.createElement('script');
langScript.src = src;
langScript.onload = checkDone;
langScript.onerror = function() {
console.warn('Failed to load language:', src);
checkDone(); // Continue even if one fails
};
document.head.appendChild(langScript);
});
// Fallback: try highlighting after delay even if some languages fail
setTimeout(function() {
if (window.hljs) {
highlightAll();
}
}, 2000);
};
coreScript.onerror = function() {
console.error('Failed to load highlight.js core');
};
document.head.appendChild(coreScript);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script></body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment