Jaunt
Tutorials

Adopt jaunt in an existing project

Hand one function of a working repo to jaunt without disturbing the rest.

You have a real codebase with real tests. You do not want a rewrite. You want to try jaunt on exactly one function, keep your existing tests as the safety net, and gate the result in CI. That is what this tutorial does, using a small token-bucket rate limiter as the host project. (It is also exactly how jaunt adopted jaunt — the framework converted its own modules with this protocol, existing tests as the gate.)

Every command and its output ran against jaunt 1.3.0.

The starting point

The project is an ordinary src-layout package with hand-written code and passing pytest tests. The interesting part is one pure function, refill_tokens, buried in src/ratelimiter/bucket.py:

def refill_tokens(
    current: float,
    capacity: float,
    refill_per_second: float,
    elapsed: float,
) -> float:
    """Return the token count after `elapsed` seconds of refilling."""
    if elapsed < 0:
        raise ValueError("elapsed must not be negative")
    replenished = current + refill_per_second * elapsed
    return min(capacity, replenished)


@dataclass
class TokenBucket:
    capacity: float
    refill_per_second: float
    # ... __post_init__ and a lock ...

    def allow(self, cost: float = 1.0) -> bool:
        with self._lock:
            now = time.monotonic()
            self._tokens = refill_tokens(
                self._tokens, self.capacity, self.refill_per_second, now - self._last
            )
            self._last = now
            if self._tokens >= cost:
                self._tokens -= cost
                return True
            return False

refill_tokens is a good first target: pure, well-specified, correctness-heavy, and covered by tests. The tests pass today:

PYTHONPATH=src uv run pytest -q
....                                                                     [100%]
4 passed in 0.01s

Add jaunt.toml

Add jaunt and drop a config at the repo root. Point source_roots at the parent of your package, not the package itself.

uv add jaunt
version = 1

[paths]
source_roots = ["src"]
test_roots = ["tests"]
generated_dir = "__generated__"

[build]
jobs = 4
emit_stubs = true

jaunt 1.3.0 validates this file strictly. A typo is a hard error with a suggestion, not a silently ignored key. Write job = 4 instead of jobs and any command stops:

error: unknown key 'job' in [build] — did you mean 'jobs'?

That is exit code 2. If you are unsure of a key, uv run jaunt instructions prints the full annotated schema. Fix the typo and move on.

Convert one function

Add the decorator, keep the signature, and move the body's behavior into the docstring. Delete the implementation:

import jaunt


@jaunt.magic()
def refill_tokens(
    current: float,
    capacity: float,
    refill_per_second: float,
    elapsed: float,
) -> float:
    """Return the token count after `elapsed` seconds of refilling.

    Tokens accrue at `refill_per_second` and never exceed `capacity`.
    Raise ValueError if `elapsed` is negative.
    """
    ...

TokenBucket.allow still calls refill_tokens(...) exactly as before. Nothing else in the module changes.

Build

uv run jaunt build --progress plain
[build] ratelimiter.bucket: generating
[build] ratelimiter.bucket: done (attempt 1)
[build] 1/1 ok=1 fail=0 ratelimiter.bucket
Cost: 1 API call(s), 0 cache hit(s)
  Estimated cost: $0.2205
Emitted 1 .pyi stub(s).
Built 1 module(s), skipped 0.

The generated body lands in src/ratelimiter/__generated__/bucket.py:

def refill_tokens(
    current: float,
    capacity: float,
    refill_per_second: float,
    elapsed: float,
) -> float:
    """Return the token count after `elapsed` seconds of refilling."""
    if elapsed < 0:
        raise ValueError("elapsed must be non-negative")

    return min(capacity, current + (refill_per_second * elapsed))

Run the existing tests again. They never referenced __generated__; they call refill_tokens through the spec module, and it still behaves:

....                                                                     [100%]
4 passed in 0.01s

Your own tests are the acceptance gate for the generated code, and they stay that way after the conversion — keep them in CI alongside jaunt check. Jaunt's validation proves the generated module is structurally sound (signatures, imports, no fallback ladders); it cannot prove semantics. A docstring that under-specifies an edge case produces generated code that honestly implements the under-specified contract — your characterization tests are what catch it, and the fix is a docstring edit plus a rebuild, not a hand-patch. That is the point of converting a covered function first.

The .pyi stub

emit_stubs wrote src/ratelimiter/bucket.pyi beside the source. Your editor and type checker read the real signatures from it, and its header records the digest of the inputs it was built from:

# This .pyi stub was generated by jaunt. DO NOT EDIT.
# jaunt:kind=stub
# jaunt:source_module=ratelimiter.bucket

def refill_tokens(current: float, capacity: float, refill_per_second: float, elapsed: float) -> float: ...

@dataclass
class TokenBucket:
    def allow(self, cost: float = 1.0) -> bool: ...
    def tokens(self) -> float: ...

Coverage

A spec body is unreachable: the runtime forwards every call into __generated__/, so the ... in the stub never runs. Coverage.py does not count a bare ... as a statement, so the default @jaunt.magic stub costs you nothing on a coverage report.

If you prefer a louder sentinel body, that line will read as missing:

    raise NotImplementedError("spec stub")

Exclude it once, and your --cov-fail-under gate stays honest:

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    'raise NotImplementedError\("spec stub"\)',
]

Gate it in CI

jaunt check is the deterministic gate. It calls no model and needs no API key. It confirms that every built module still matches its spec:

uv run jaunt check
Contract check: 0 contract function(s).
Magic freshness: all modules fresh.

Change a docstring without rebuilding and the same command blocks with exit code 4:

Magic freshness: 0 unbuilt, 1 stale.
[BLOCK] ratelimiter.bucket: stale (prose)

Because you committed the generated code (see below), CI runs jaunt check with no Codex access at all. A minimal GitHub Actions job:

name: jaunt
on: [push, pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uvx --from jaunt jaunt check
      - run: uv run pytest -q

The --json flag turns check into machine-readable output for annotations; see the JSON output reference.

Committing generated code

jaunt init does not gitignore __generated__/. The default posture is to commit it, which is what the CI job above assumes: deterministic installs, generated diffs visible in review, no model access in automation. Track the generated files and the .pyi stubs alongside your source. The alternative, regenerating in CI, means giving automation a Codex login; pick one and hold to it.

Where to next

On this page