Jaunt
Writing Specs

Spec Writing Tips

Practical patterns for getting better output from smaller specs.

You don't "use Jaunt" once. You live in a loop: write spec → build → review → tighten spec → rebuild.

Iterating On Specs

This loop IS the development process:

  1. Write a spec with your best guess at the contract.
  2. Run jaunt build.
  3. Read the generated code. Does it match your intent?
  4. If not: tighten the docstring, add edge cases, use prompt= for constraints.
  5. Re-run jaunt build. Jaunt only regenerates what changed.

You'll usually get 80% right on the first pass. The second pass — where you name the edge cases you forgot — is where the spec gets good. Don't try to write the perfect spec upfront. Iterate.

Vague vs. Precise Docstrings

Bad (vague):

@jaunt.magic()
def parse_user_agent(ua: str) -> dict:
    """Parse a user agent string."""
    raise RuntimeError("spec stub (generated at build time)")

Better (contract):

@jaunt.magic()
def parse_user_agent(ua: str) -> dict[str, str]:
    """
    Parse a user agent string into a small, stable dict.

    Contract:
    - Return keys: "browser", "os".
    - If unknown, use "unknown" (do not raise).
    - Input may be empty or junk; treat as unknown.
    """
    raise RuntimeError("spec stub (generated at build time)")

The difference: the vague spec leaves every decision to the LLM. The precise spec names the return shape, the failure mode, and the edge case. Better spec → better code → less iteration.

Use prompt= When The Docstring Isn't Enough

If a constraint is non-negotiable, say it twice:

@jaunt.magic(prompt="Use only the standard library. Do not import third-party deps.")
def stable_hash(text: str) -> str:
    """Return a stable hex sha256 of UTF-8 text."""
    raise RuntimeError("spec stub (generated at build time)")

Good uses of prompt=:

  • Force a specific library: prompt="Use the cryptography library, not hashlib."
  • Architectural constraints: prompt="Must be async-safe. No global state."
  • Style preferences: prompt="Prefer explicit loops over list comprehensions for readability."

Don't Edit Generated Files

Generated output is disposable. Specs are the product.

If you need a behavior change, change the spec and rebuild so your intent is versioned. Editing __generated__/ files directly means your changes will be overwritten on the next build.

Whole-Class vs Per-Method @magic

Jaunt supports two styles of class specs. Pick whichever matches your situation:

Whole-class — decorate the class itself:

@jaunt.magic()
class LRUCache:
    """Fixed-capacity LRU cache. get/set/size methods."""
    pass

Use this when the class is a self-contained data structure or algorithm and you want the LLM to decide internals (__init__, data layout, helper methods).

Per-method — decorate individual methods:

class TaskBoard:
    def __init__(self) -> None:
        self._tasks: list[dict[str, object]] = []

    @jaunt.magic()
    def add(self, title: str, priority: int) -> dict[str, object]:
        """..."""
        raise RuntimeError("spec stub (generated at build time)")

Use this when you want to control the class shape — __init__, attributes, inheritance, non-generated helper methods — and only generate specific methods. This is natural for service objects, repositories, and classes that mix custom logic with generated glue.

You cannot combine both styles on the same class.

When Not To Use Jaunt

Jaunt is best for glue code and boring-but-correct utilities (parsers, validators, formatters).

Avoid it (for now) for:

  • performance-critical hot paths
  • complex stateful systems with tight invariants
  • code that depends on exact library versions or fragile runtime behavior

Next: Guides.

On this page