# Project export: Saffron

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Saffron turns plain-English circuit ideas into verified Verilog, schematics, and waveforms using AI plus real EDA tools making hardware prototyping faster, clearer, and easier to learn.
- Devpost: https://devpost.com/software/saffron-5am27b
- GitHub: https://github.com/Shreyas-Yadav/saffron
- Video: https://www.youtube.com/embed/SAHTqhoiH7g?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Shreyas Yadav (17 commits), Claude Opus 4.8 (13 commits)

## Devpost submission (written by the team)

### Inspiration

Designing hardware is hard. Even a simple digital circuit can take time: writing Verilog, creating a testbench, running synthesis, checking simulation, reading waveforms, fixing errors, and repeating the process. As undergraduate electrical engineering students, we saw how much time gets spent on setup and iteration before we can focus on the actual circuit idea. Software developers have tools like Cursor to move from idea to working code faster. We wanted a similar experience for hardware: an assistant for Verilog, synthesis, simulation, and waveform-based debugging. That is why we built Saffron - an AI-powered hardware design assistant that helps users describe a circuit, generate Verilog, verify it with real EDA tools, and visualize the result through schematics and waveforms. Our goal is to make early-stage hardware prototyping faster, clearer, and easier to learn from.

### What it does

Saffron is an AI-powered hardware design assistant for early-stage digital circuit prototyping. Users describe a circuit in natural language, and Saffron generates Verilog for that design. The generated code is then checked using real EDA tools instead of being treated as correct by default. If the design fails, Saffron shows the error result and can use that feedback to retry and improve the Verilog. If the design succeeds, it produces a circuit schematic and runs simulation so users can see how the circuit behaves over time. Saffron also gives users a place to inspect and edit the generated Verilog, then rerun the workflow. This makes it useful not only for generating circuits, but also for learning, experimenting, and quickly testing design changes. In one flow, Saffron helps users move from idea to Verilog, verification, schematic visualization, simulation, and waveform output.

### How we built it

We built Saffron as a full-stack AI + EDA workflow that connects natural-language circuit design with real hardware verification tools. On the backend, we used Python and FastAPI to create the main API. When a user describes a circuit, the backend sends the request to Claude, which generates structured Verilog code and an explanation. The generated Verilog is then passed through a verification pipeline instead of being trusted blindly. For synthesis, we use Yosys to check whether the Verilog is valid and synthesizable. If synthesis succeeds, Yosys produces a gate-level netlist. That netlist is then rendered into a visual circuit schematic using netlistsvg and Graphviz. For simulation, we built an automatic testbench generator. It inspects the circuit inputs and outputs, creates test cases, and runs the design using Icarus Verilog. The simulator produces a VCD waveform file, which we parse and convert into waveform data that can be shown in the frontend. We also built an auto-repair loop. If Yosys or Icarus Verilog finds an error, Saffron captures the tool log, sends the error back to the AI with the broken Verilog, and asks it to fix the design. This loop helps turn AI output into actually working hardware code. On the frontend, we used Next.js, React, TypeScript, and Tailwind CSS. The interface includes a chat-style circuit prompt, a Verilog editor, schematic visualization, and waveform display. Users can generate a design, inspect the code, manually edit it, and rerun synthesis or simulation.

### Challenges we ran into

One challenge was connecting many different tools into one smooth workflow. AI generation, backend logic, EDA tools, frontend display, and user editing all had to work together correctly. Another challenge was working with unfamiliar hardware toolchains under limited time. We had to learn quickly, test often, and fix issues as they appeared. The hardest part was making Saffron feel useful instead of just impressive. We wanted the tool to help users understand and iterate on circuits, not only generate code.

### Accomplishments we're proud of

We are proud that we built Saffron in just one day while learning new EDA tools and hardware workflows along the way. The biggest accomplishment was turning many separate pieces into one usable experience. Instead of stopping at AI-generated code, Saffron gives users a way to create, check, edit, and visually understand their circuit designs. Even without OpenROAD integration yet, Saffron already helps users move faster in the early stages of hardware prototyping and build confidence in their designs.

### What we learned

Building Saffron showed us that AI is most powerful when it is connected to real tools, not used alone. We learned how much engineering happens between an idea and a working circuit: code generation, validation, simulation, visualization, and iteration all have to work together. We also learned that a team does not need the same background to build something complex. Hardware knowledge, software skills, planning, and shared momentum all mattered equally.

### What's next

Saffron already works as a rapid prototyping tool for early-stage digital design. Next, we want to expand it into a more complete hardware design platform. The next major step is integrating floor planning, layout generation, and routing, with AI helping automate and optimize each stage. Instead of only generating and verifying Verilog, Saffron could evaluate design metrics, improve layouts, and guide users toward more efficient hardware implementations. We also want to fine-tune the AI on hardware design workflows so it becomes better at understanding circuits, fixing errors, and making stronger design decisions. Long term, Saffron has the potential to redefine how students, engineers, and researchers build hardware by making chip design faster, more accessible, and more intelligent.

## README (from the GitHub repository)

# Saffron

AI hardware design assistant. Describe a circuit in natural language → get
synthesizable **Verilog**, a **real gate-level schematic** (Yosys + netlistsvg), and
(coming) a simulated **waveform**. The output is *verified* by real EDA tooling, not
just generated by an LLM.

## Prerequisites

```bash
brew install yosys icarus-verilog graphviz node   # EDA toolchain + Node
npm install -g netlistsvg                          # yosys JSON → SVG schematic
```

**Optional — static timing analysis (OpenSTA via Docker).** Timing is best-effort:
without it you still get the schematic, waveform, and formal results, and the Timing
tab degrades to a yosys area/cell estimate. To enable real max-frequency/critical-path
analysis, install Docker and pull the image (the Nangate45 library is already vendored
in `backend/fixtures/liberty/`):

```bash
docker pull --platform linux/amd64 openroad/opensta   # amd64 runs under emulation on Apple Silicon
docker run --rm --platform linux/amd64 openroad/opensta -version   # sanity check
```

## Backend (FastAPI, Python 3.12 via uv)

```bash
cd backend
cp .env.example .env          # then add your GEMINI_API_KEY
uv sync
uv run uvicorn app.main:app --reload --port 8000
```

- `GEMINI_API_KEY` (required for generation) and optional `GEMINI_MODEL`
  (default `gemini-2.5-flash`) live in `backend/.env`.
- Tests (use the real toolchain, no key needed): `uv run pytest`

## Frontend (Next.js + Tailwind)

```bash
cd frontend
npm install
npm run dev                   # http://localhost:3000
```

## Architecture

Built for loose coupling (SOLID): every external tool sits behind a small interface,
wired in one composition root (`backend/app/api/deps.py`).

- `llm/provider.py` — `LLMProvider` ← `GeminiProvider` (swap models in one line)
- `pipeline/synthesize.py` — `Synthesizer` (yosys) + `SchematicRenderer` (netlistsvg)
- `pipeline/sandbox.py` — sandboxed subprocess runner + `VerilogGuard` (rejects
  `$system`/`$fopen`/… before any tool runs)
- `pipeline/orchestrator.py` — generate → synthesize → **auto-repair** (feeds tool
  errors back to the LLM, retries up to 3×) → returns code + schematic together
- `pipeline/testbench.py` + `simulate.py` + `simulation.py` — auto-generate a
  testbench from the netlist ports (combinational sweep, or clocked stimulus for
  sequential modules — chosen automatically), simulate with Icarus, convert VCD →
  WaveDrom. Narrow signals render per-bit; wide datapaths as value segments.
- `pipeline/formal.py` + `verification.py` — `FormalVerifier` (yosys SAT). Proves
  LLM-emitted intent assertions for **all** inputs (combinational) — refutations
  render as a counterexample waveform — plus intent-independent invariants (no
  combinational loops, no accidental latches) on every design.
- `pipeline/timing.py` + `timing_pipeline.py` — `TimingAnalyzer` (`OpenStaTiming‑
  Analyzer`). Maps to the Nangate45 cell library with yosys, then times the critical
  path with OpenSTA (in Docker) → max frequency / critical path / area. Degrades to a
  yosys area estimate when Docker is absent.

Every analysis stage is best-effort and additive: a formal/timing/sim failure surfaces
in its own tab and never blocks the schematic. The repair loop targets synthesis only.

## Status

- [x] Toolchain spike, synthesis pipeline, schematic in browser
- [x] LLM generation + auto-repair loop
- [x] Simulation → waveform — combinational **and** sequential (clock/reset/enable)
- [x] Formal verification (yosys SAT) — intent proofs + invariants + counterexamples
- [x] Static timing analysis (OpenSTA) — max frequency, critical path, area


## Detected evidence (automated analysis)

Indexed codebase: 52 recognized source files, 182 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (64 of 64)

```
.gitignore
backend/.env.example
backend/.python-version
backend/app/__init__.py
backend/app/api/__init__.py
backend/app/api/deps.py
backend/app/api/routes.py
backend/app/config.py
backend/app/llm/__init__.py
backend/app/llm/provider.py
backend/app/main.py
backend/app/models.py
backend/app/pipeline/__init__.py
backend/app/pipeline/agentic.py
backend/app/pipeline/formal.py
backend/app/pipeline/orchestrator.py
backend/app/pipeline/sandbox.py
backend/app/pipeline/sanitize.py
backend/app/pipeline/schematic.py
backend/app/pipeline/simulate.py
backend/app/pipeline/simulation.py
backend/app/pipeline/steps.py
backend/app/pipeline/synthesize.py
backend/app/pipeline/testbench.py
backend/app/pipeline/timing_pipeline.py
backend/app/pipeline/timing.py
backend/app/pipeline/verification.py
backend/fixtures/full_adder_tb.v
backend/fixtures/full_adder.v
backend/fixtures/liberty/NOTICE.md
backend/pyproject.toml
backend/README.md
backend/tests/__init__.py
backend/tests/test_agentic.py
backend/tests/test_formal.py
backend/tests/test_orchestrator.py
backend/tests/test_sanitize.py
backend/tests/test_schematic.py
backend/tests/test_simulation.py
backend/tests/test_steps.py
backend/tests/test_timing.py
backend/uv.lock
frontend/.gitignore
frontend/AGENTS.md
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/CLAUDE.md
frontend/components/FormalPanel.tsx
frontend/components/ResultTabs.tsx
frontend/components/SchematicPanel.tsx
frontend/components/StepsPanel.tsx
frontend/components/TestbenchPanel.tsx
frontend/components/TimingPanel.tsx
frontend/components/WaveformPanel.tsx
frontend/eslint.config.mjs
frontend/lib/api.ts
frontend/lib/types.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tsconfig.json
README.md
```

### Dependencies

- backend/pyproject.toml: anthropic[vertex]@>=0.40.0, fastapi@>=0.138.0, google-genai@>=2.9.0, pydantic@>=2.13.4, python-dotenv@>=1.2.2, pyvcd@>=0.4.1, uvicorn@>=0.49.0
- frontend/package.json: @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, babel-plugin-react-compiler@1.0.0, eslint@^9, eslint-config-next@16.2.9, next@16.2.9, react@19.2.4, react-dom@19.2.4, tailwindcss@^4, typescript@^5

### Recent commits (newest first)

- feat: enhance layout and functionality with new font and improved UI
- feat: agentic generation mode (Claude drives the pipeline via tool use)
- feat: student step explanations, multi-provider LLM, and SystemVerilog support
- Revert "feat: highlight the critical path (timing diagram + schematic endpoints)"
- Revert "feat: faithful critical-path schematic (gates + wires) in Timing tab"
- feat: faithful critical-path schematic (gates + wires) in Timing tab
- feat: highlight the critical path (timing diagram + schematic endpoints)
- feat(frontend): zoomable/pannable schematic viewer
- feat: auto-clean copy-paste artifacts in pasted Verilog
- feat: static timing analysis via OpenSTA (max freq / critical path / area)
- feat: formal verification (Yosys SAT — intent assertions + invariants)
- feat: sequential circuit waveforms (clocked testbench + hybrid rendering)
- feat: simulation -> waveform with auto-generated testbench (Step 5)
- feat(frontend): natural-language chat UI + schematic panel (Steps 1-4)
- feat(backend): synthesis pipeline, Gemini generation, auto-repair (Steps 1-4)
- chore: scaffold backend + frontend, add toolchain fixtures (Step 0)
- feat: git init

## Key source files (fetched from GitHub, selected and truncated for size)

### frontend/CLAUDE.md

```markdown
@AGENTS.md

```

### frontend/AGENTS.md

```markdown
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->

```

### backend/pyproject.toml

```
[project]
name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "anthropic[vertex]>=0.40.0",
    "fastapi>=0.138.0",
    "google-genai>=2.9.0",
    "pydantic>=2.13.4",
    "python-dotenv>=1.2.2",
    "pyvcd>=0.4.1",
    "uvicorn>=0.49.0",
]

[dependency-groups]
dev = [
    "pytest>=9.1.1",
]

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.2.9",
    "react": "19.2.4",
    "react-dom": "19.2.4"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "babel-plugin-react-compiler": "1.0.0",
    "eslint": "^9",
    "eslint-config-next": "16.2.9",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### backend/app/main.py

```python
"""FastAPI application entrypoint. Run: `uv run uvicorn app.main:app --reload`."""
from __future__ import annotations

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from .api.routes import router
from .llm.provider import LLMError
from .pipeline.agentic import AgenticError

app = FastAPI(title="Saffron", description="AI hardware design assistant")


@app.exception_handler(LLMError)
@app.exception_handler(AgenticError)
def _llm_error_handler(_: Request, exc: Exception) -> JSONResponse:
    # Misconfigured key / upstream failure / agent turn-limit surfaces as a clean 502,
    # even when the provider fails to construct during dependency resolution.
    return JSONResponse(status_code=502, content={"detail": str(exc)})

# Dev CORS: allow the Next.js dev server.
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(router)

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Bricolage_Grotesque, Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

// Characterful display face — used with restraint for the hero and headings.
const bricolage = Bricolage_Grotesque({
  variable: "--font-bricolage",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Saffron — natural language to verified circuits",
  description:
    "Describe a circuit in plain English. Saffron synthesizes it to gates, simulates the waveform, and formally proves it correct.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} ${bricolage.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">{children}</body>
    </html>
  );
}

```

### frontend/app/page.tsx

```typescript
"use client";

import { useState } from "react";

import { ResultTabs } from "@/components/ResultTabs";
import { chat, synthesize } from "@/lib/api";
import type { ChatMessage, SchematicResult } from "@/lib/types";

const SEED_VERILOG = `// Describe a circuit above, or edit Verilog directly, then Synthesize.
module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);
    assign sum  = a ^ b ^ cin;
    assign cout = (a & b) | (cin & (a ^ b));
endmodule`;

const EXAMPLES = [
  "a 4-bit ripple-carry adder",
  "a 2-to-1 multiplexer",
  "a D flip-flop with reset",
  "a 3-bit up counter",
];

export default function Home() {
  const [verilog, setVerilog] = useState(SEED_VERILOG);
  const [result, setResult] = useState<SchematicResult | null>(null);
  const [synthLoading, setSynthLoading] = useState(false);

  // Conversation history drives iteration ("now make it 4-bit") in Step 4.
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [prompt, setPrompt] = useState("");
  const [genLoading, setGenLoading] = useState(false);
  const [note, setNote] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  // Lets the user jump straight into the editor from the hero, before any result.
  const [showEditor, setShowEditor] = useState(false);

  // The prompt is the hero until there's something to show or the user opts into
  // the editor; then the workspace takes over.
  const inWorkspace =
    result !== null ||
    messages.length > 0 ||
    showEditor ||
    genLoading ||
    synthLoading;

  async function onGenerate(override?: string) {
    const text = (override ?? prompt).trim();
    if (!text || genLoading) return;
    setGenLoading(true);
    setError(null);
    setNote(null);
    setResult(null);
    const history: ChatMessage[] = [...messages, { role: "user", content: text }];
    try {
      // Backend generates, synthesizes, and auto-repairs in one call, returning
      // both the Verilog and the rendered schematic.
      const res = await chat(history);
      setVerilog(res.verilog);
      setMessages([...history, { role: "assistant", content: res.verilog }]);
      setPrompt("");
      const repaired = res.attempts > 1 ? ` (auto-fixed in ${res.attempts} tries)` : "";
      setNote(res.explanation + repaired);
      setResult({
        svg: res.svg,
        renderer: res.renderer,
        netlist_json: null,
        logs: "",
        error: res.error,
        wavedrom: res.wavedrom,
        sim_error: res.sim_error,
        testbench: res.testbench,
        formal: res.formal,
        timing: res.timing,
        steps: res.steps,
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : "generation failed");
    } finally {
      setGenLoading(false);
    }
  }

  async function onSynthesize() {
    setSynthLoading(true);
    setResult(null);
    try {
      setResult(await synthesize({ verilog }));
    } catch (err) {
      setResult({
        svg: null,
        renderer: null,
        netlist_json: null,
        logs: "",
        error: err instanceof Error ? err.message : "request failed",
        wavedrom: null,
        sim_error: null,
        testbench: null,
        formal: null,
        timing: null,
        steps: [],
      });
    } finally {
      setSynthLoading(false);
    }
  }

  if (!inWorkspace) {
    return (
      <Hero
        prompt={prompt}
        setPrompt={setPrompt}
        onGenerate={onGenerate}
        onExamplePick={(t) => {
          setPrompt(t);
          onGenerate(t);
        }}
        onWriteVerilog={() => setShowEditor(true)}
        error={error}
      />
    );
  }

  return (
    <main className="flex h-screen flex-col bg-ink text-bone">
      <header className="flex flex-wrap items-center gap-x-5 gap-y-2 border-b border-hairline px-6 py-3">
        <Wordmark />
        <form
          className="flex min-w-0 flex-1 gap-2"
          onSubmit={(e) => {
            e.preventDefault();
            onGenerate();
          }}
        >
          <input
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            placeholder="Describe a change — e.g. now make it 4-bit"
            className="min-w-0 flex-1 rounded-md border border-hairline bg-ink-2 px-3 py-2 text-sm text-bone placeholder:text-bone-faint outline-none focus:border-saffron"
          />
          <button
            type="submit"
            disabled={genLoading}
            className="shrink-0 rounded-md bg-saffron px-4 py-2 text-sm font-medium text-ink transition-colors hover:bg-ember disabled:opacity-50"
          >
            {genLoading ? "Generating…" : "Generate"}
          </button>
        </form>
        {(note || error) && (
          <p
            className={`w-full text-xs ${error ? "text-err" : "text-bone-dim"}`}
          >
            {error ?? note}
          </p>
        )}
      </header>

      <div className="flex flex-1 flex-col overflow-hidden lg:flex-row">
        {/* Left: generated / editable Verilog */}
        <section className="flex min-h-0 flex-1 flex-col border-hairline max-lg:border-b lg:border-r">
          <div className="flex items-center justify-between border-b border-hairline px-4 py-2">
            <span className="text-[11px] font-medium uppercase tracking-[0.18em] text-bone-faint">
              Verilog source
            </span>
            <button
              onClick={onSynthesize}
              disabled={synthLoading}
              className="rounded-md border border-hairline px-3 py-1.5 text-xs font-medium text-bone transition-colors hover:border-saffron hover:text-ember disabled:opacity-50"
            >
              {synthLoading ? "Synthesizing…" : "Synthesize"}
            </button>
          </div>
          <textarea
            value={verilog}
            onChange={(e) => setVerilog(e.target.value)}
            spellCheck={false}
            className="mi
[truncated — 3849 more characters]
```

### frontend/next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  reactCompiler: true,
};

export default nextConfig;

```

### backend/tests/test_schematic.py

```python
"""Golden test: the real toolchain turns the full-adder fixture into a gate SVG.

Uses the Step-0 fixture and the actual yosys/netlistsvg binaries (no LLM), so it
validates the whole synthesis pipeline deterministically.
"""
from pathlib import Path

import pytest

from app.api.deps import get_schematic_pipeline
from app.pipeline.sandbox import UnsafeVerilogError, VerilogGuard

FIXTURE = Path(__file__).parent.parent / "fixtures" / "full_adder.v"


def test_full_adder_synthesizes_to_gate_svg():
    verilog = FIXTURE.read_text()
    result = get_schematic_pipeline().build(verilog, top="full_adder")

    assert result.error is None, result.error
    assert result.svg and result.svg.lstrip().startswith("<svg")
    assert result.renderer == "netlistsvg"
    # netlistsvg emits a <g> per cell; a full adder has xor/and/or gates.
    assert "cell_$xor" in result.svg or "$xor" in result.svg


def test_guard_rejects_system_task():
    with pytest.raises(UnsafeVerilogError):
        VerilogGuard().check('module m; initial $system("rm -rf /"); endmodule')

```

### frontend/lib/api.ts

```typescript
// The one module that knows how to talk to the backend. Components depend on these
// functions, never on `fetch` directly, so the transport can change in one place.
import type {
  ChatMessage,
  ExplainStepRequest,
  GenerateOutcome,
  SchematicResult,
  StepExplanation,
  SynthesizeRequest,
} from "./types";

const API_BASE =
  process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8000";

async function postJSON<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${API_BASE}${path}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    // Surface the backend's {detail: ...} message when present (e.g. missing key).
    let detail = `${res.status} ${res.statusText}`;
    try {
      const body = await res.json();
      if (body?.detail) detail = body.detail;
    } catch {
      /* non-JSON error body */
    }
    throw new Error(detail);
  }
  return res.json() as Promise<T>;
}

export function synthesize(
  req: SynthesizeRequest,
): Promise<SchematicResult> {
  return postJSON<SchematicResult>("/api/synthesize", req);
}

export function chat(messages: ChatMessage[]): Promise<GenerateOutcome> {
  return postJSON<GenerateOutcome>("/api/chat", { messages });
}

export function explainStep(
  req: ExplainStepRequest,
): Promise<StepExplanation> {
  return postJSON<StepExplanation>("/api/explain-step", req);
}

```

[36 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]