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=orprompt= - 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.
--jsonlists 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_modulekwarg. Module defaults likeprompt=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 ownprompt=.
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 statusrecomputes 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.