Jaunt
Concepts

Change Detection

Two-layer freshness: AST-normalized digests plus a semantic gate that re-freezes equivalent edits.

Jaunt decides what to rebuild in two layers. The first is deterministic and free; the second spends one cheap model call to avoid a full rebuild when an edit changed the wording of a spec but not its meaning. Between them, cosmetic edits cost nothing and clarifying edits rarely cost a rebuild.

Layer A: AST-normalized digests

Spec freshness comes from an AST-normalized contract digest, not the raw file bytes. Ruff reformatting, comment edits, whitespace, and quote-style changes leave the digest untouched, so they never mark a module stale. This layer is deterministic, applies to both build and test freshness, and cannot be turned off.

What does move the digest is anything that changes the contract:

  • the signature — parameters, annotations, return type
  • the cleaned docstring, anywhere in it
  • decorator kwargs such as deps= or prompt=
  • for a whole-class spec, adding, removing, or re-signing a declared member
  • a change to a dependency's exported API

Repo-map content does not. Adding or editing a sibling spec's repo-map entry never restales an unchanged module — jaunt build and jaunt check ignore repo-map drift, and jaunt status surfaces it as an informational note at most. Turning the repo map on or off is a different matter; that toggle is a fingerprint input.

Layer B: the semantic gate

When a spec's docstring genuinely changed but its signature and structure did not, a small judge model decides whether the edit is behaviorally meaningful:

  • Equivalent — a typo fix, a reword, a clearer sentence that says the same thing. Jaunt re-freezes the module: it rewrites the header digests over the validated, unchanged generated body instead of paying for a full rebuild with the main model. --json lists these under "refrozen".
  • Meaningful — the module rebuilds normally.

Structural changes, validation failures, and any error from the gate itself fail safe to a rebuild. The gate can only save work; it can never skip a real change.

Skip it for one run with jaunt build --no-semantic-gate (or the same flag on jaunt test), and every digest change rebuilds. Layer A still applies. The [semantic_gate] keys — which judge model, what reasoning effort — live in the configuration reference.

The gate model has to run through codex exec (gpt-5.6-luna is the default). gpt-5.4-nano does not: codex exec attaches a tool_search tool that nano rejects.

Converting to module style

Moving a spec from a @jaunt.magic decorator to magic_module governance is digest-neutral as long as the stub body form does not change. The decorator line never entered the digest, and Jaunt renders a module-governed spec's signature through the same path a decorated one uses, so the digest comes out byte-for-byte identical and the module stays fresh.

Two conversions do move the digest, both correctly:

  • Rewriting a raise RuntimeError("spec stub") body to .... That older body is not a recognized stub form, so its text is part of the current digest. In a governed module it would not even classify as a spec. Rewriting it to ... restales the module once — a one-time cost you pay when you adopt the convention.
  • Adding or editing a magic_module kwarg. Module defaults like prompt= are part of every governed spec's contract, so editing one restales every spec in the file. That is the same rule that restales a spec when you edit its own prompt=.

Does this save tokens?

Yes, potentially a lot — but it depends on when you adopted Jaunt and on the shape of your codebase.

What is unconditional: an unchanged module skips the model entirely, formatting and comment churn never trigger a rebuild, and a reworded-but-equivalent docstring re-freezes through the small [semantic_gate] judge instead of paying for a full generation. Day to day, most edits touch a few modules and leave the rest cached.

What it depends on:

  • Adoption timing. The first build after you adopt Jaunt regenerates everything, so the savings start accruing from the second build onward.
  • Dependency shape. Staleness follows the graph. A change to a module's exported API restales every dependent, so a hub with wide fan-out rebuilds a lot when it moves; many small, independent modules amortize best.

Inherited base API

A whole-class spec that inherits from a spec'd base in another module also tracks that base's generated public API. Jaunt hashes the rendered inherited-API block — method signatures and docstrings, read from the base's built artifact — into the subclass's digest and records it in the header as base_api_digest.

  • Change the base's public API — a signature or a docstring on any generated public method — and the subclass restales, even if you never touched the subclass's own spec.
  • A body-only rebuild of the base, with the same signatures and docstrings, leaves the block unchanged, so the subclass stays fresh.
  • The header stores the post-dependency value, so a later jaunt status recomputes the same digest from the same artifacts and nothing flaps.

A moved base API can mean the subclass body genuinely needs to change, so the Layer B re-freeze is refused when the base's API digest moved. Jaunt falls back to a full rebuild rather than re-freeze over a body that may now be wrong.

What else counts as a change

Beyond the spec contract, Jaunt fingerprints the machinery that produced the code: the generation engine and its model settings, the prompt templates (including the preamble), and each module's exported dependency API. Change any of those and the affected modules go stale without --force. jaunt status shows the resulting stale set, upstream fallout included, and the configuration reference lists the exact keys.

Next: Repo Context.

On this page