TypeScript target
Author private TypeScript specs, generate ordinary TypeScript, and validate it with the project-local compiler.
The TypeScript target is available behind version = 2 configuration. It keeps
Jaunt's Python CLI and Codex scheduler, but delegates TypeScript parsing and
typechecking to the project-local @usejaunt/ts worker. Discovery reads source and
tsconfig.json; it does not execute application modules, specs, Vite config, or
package scripts.
Install
Start a new project with:
uvx jaunt init --language ts
npm init -y && npm pkg set type=module
npm install -D @usejaunt/ts@^0.1.3 'typescript@^5.9' vitest fast-check @types/nodejaunt init does not edit package.json. Its human and JSON output includes the
package command you still need to run. For an existing manifest with no type, that
command is npm pkg set type=module. An explicit "type": "commonjs" is preserved;
the generated NodeNext config lowers the scaffold's import/export syntax to CommonJS.
The worker and protected test runner support Node >=20 <25. That is a Jaunt
tool-host constraint, not a requirement on the runtime that executes your generated
JavaScript.
For an existing project, add this target to jaunt.toml:
version = 2
[target.ts]
source_roots = ["src"]
test_roots = ["tests"]
projects = ["tsconfig.json"]
test_projects = ["tsconfig.test.json"]
tool_owner = "."
generated_dir = "__generated__"
test_runner = "vitest"
fast_check_runs = 50
[codex]
model = "gpt-5.6-sol"
reasoning_effort = "medium"
sandbox = "workspace-write"If the project already has a version-1 Python config, preview the mechanical rewrite first, then apply it:
jaunt migrate --config-v2
jaunt migrate --config-v2 --applyThe migration moves Python-only keys under [target.py], preserves the loaded Python
configuration exactly, and makes no model call.
When upgrading an existing TypeScript project, preview its artifact migration separately:
jaunt migrate --language ts
jaunt migrate --language ts --applyIn a TypeScript-only version-2 project, jaunt migrate selects this path without the
language flag. The command asks the project-local worker for current routes and
validated artifact bytes, but never calls Codex. It can repair mirrors, canonical
facades, placeholders, and sidecars, or freely re-stamp compatible fingerprint drift.
Contract changes and incompatible protocol, IR, or route records are listed as
model-rebuild with the exact module to rebuild.
Migration is plan-only unless --apply is present. The apply checks every analyzed
input again, refuses a dirty worktree unless you pass --force, and rolls back the
whole artifact manifest if a replacement fails. Older preview layouts that used an
ambiguous spec.jaunt.ts, __generated__/impl.ts, or loader hook are reported for
manual intervention; Jaunt does not guess the intended public facade or rename private
specs.
tool_owner is the package that directly declares both @usejaunt/ts and
typescript. Jaunt does not use a global compiler and does not download one during a
command.
The stable 0.1 line supports TypeScript 5.8 through 6.x. TypeScript 7.0 does not ship a stable programmatic compiler API, so the worker rejects it with install guidance rather than analyzing the project under different compiler semantics. TypeScript 7 support will follow its stable tooling API.
Author a module
A private spec uses the .jaunt.ts suffix:
import * as jaunt from "@usejaunt/ts/spec";
jaunt.magicModule();
/** Lowercase a title and join non-empty ASCII word runs with hyphens. */
export function slugify(title: string): string {
return jaunt.magic();
}Consumers import an ordinary committed facade:
export * from "./__generated__/index.js";When the spec exports standalone interfaces or type aliases, the generated facade
also re-exports those names explicitly from index.api.js. It does not use
export type *, because a class has both a type and a runtime value and star exports
would create ambiguous collisions.
The full layout is:
src/slug/
index.jaunt.ts private contract input; never emitted or executed
index.context.ts optional handwritten runtime leaf
index.ts public facade
__generated__/
index.api.ts deterministic declaration mirror
index.ts generated implementation
index.jaunt.json committed freshness recordSource imports use the owning project's runtime convention. NodeNext projects use
.js specifiers; TypeScript resolves them to source .ts files and emitted JavaScript
keeps valid runtime paths.
The checked matrix covers NodeNext ESM, NodeNext CommonJS, Bundler/Vite-style
resolution, and governed .tsx. The marker package exposes separate ESM and CommonJS
declaration conditions, so a CommonJS spec does not accidentally type against an ESM
entry. JSX uses the owning project's normal factory/runtime and global declarations;
Jaunt does not add a framework runtime.
Sync before the first build
uvx jaunt sync --language ts
uvx jaunt status --language tssync is deterministic and does not call Codex or the semantic gate. It writes the
API mirror, creates a canonical facade only when none exists, and supplies an exact
typed throwing placeholder for a missing implementation. The editor can then typecheck
the module, while status and check still report it as unbuilt. The placeholder
consumes its temporary parameters and runtime imports, so noUnusedLocals and
noUnusedParameters can stay enabled while sibling modules are still unbuilt.
The same narrow allowance applies to canonical jaunt.magic() marker parameters
when a dependency spec is compiled during sync; unused parameters in preserved or
other handwritten bodies still fail normally.
Full-workspace analysis and sync use bounded internal batches. Sync orders those
batches by Jaunt dependencies, validates every batch first, then commits one artifact
transaction. --target is narrower: contract responses, bootstrap placeholders,
and overlay compiler validation include the selected module and its explicit and
public-import closure through ordinary barrels and context files, not unrelated
configured sources. Configured global-declaration roots remain available. A broad
stable root or glob can therefore remain in
jaunt.toml; you should not need to rotate roots or tsconfig includes as each
module is built.
Build, test, and check
uvx jaunt build --language ts
uvx jaunt test --language ts
uvx jaunt check --language tsThe model writes reserved internal bindings. The worker rejects model-authored
exports, any, TypeScript suppressions, and unsafe boundary casts, then composes the
public exports itself. Candidate files are typechecked in an in-memory overlay. Jaunt
writes the implementation, API mirror, sidecar, and any new facade only after the
complete validation succeeds. Modules that merely share a package owner or project
reference are separate write transactions. Each successful unit is revalidated
against the artifacts currently committed for unselected siblings, then lands as
one atomic write before the next unit is checked. If one candidate fails, validated
independent siblings can still land; explicit Jaunt dependency-connected modules
remain atomic.
TypeScript battery generation receives --instruction plus persistent
[build].instructions and a declaration-only view of workspace-local types imported
by each selected target. The worker closes directly requested declarations over their
supporting declarations, re-exports, and import aliases, while stripping runtime
bodies and initializers. Requested chunks take priority over supporting closure inside
the 64 KiB UTF-8 transport budget; records use delimiter-safe framing, and an omission
marker records when lower-priority context did not fit.
For cross-repository development, a symlinked @usejaunt/ts package is supported only
while its built dist/ bytes stay fixed for the command. Build it once and serialize
later package rebuilds, or point the adopter at an immutable packed/copied install.
Jaunt pins the resolved runtime content and reports
JAUNT_TS_TOOLCHAIN_CHANGED_DURING_BUILD if it changes before the final commit
boundary; the affected artifact or battery transaction is rolled back.
Artifact journals move from prepared to committed only after that final runtime
seal. After a process interruption, Jaunt automatically retires a journal when every
path still matches the recorded before-state (nothing landed) or when a durable
committed journal still matches every recorded after-state (everything landed).
The writer holds one persistent workspace lease across the final input CAS, runtime
guard, prepared publication, replacement/seal, and retirement window; generation and
staging outside that short window remain parallel. Concurrent recovery skips a lease
that is still held. Mixed, malformed, and legacy journals without lease metadata remain
blocking for manual inspection; old markers cannot prove that their writer stopped or
that the final runtime seal ran.
Before a regenerated implementation is committed, Jaunt also typechecks and runs
every compatible committed Jaunt battery that targets that module against the exact
candidate overlay. A behavioral failure rejects the implementation and enters the
bounded implementation retry loop; a runner/infrastructure failure stops without
spending another model attempt. Ordinary native *.test.ts[x] and *.spec.ts[x]
files are not part of this gate yet because Jaunt has no deterministic ownership map
from those tests to one governed module. Keep running the repository's native suite
as its separate project gate.
Authored test intent lives in *.jaunt-test.ts. Generated batteries use Vitest and
import the public facade. Example-tier failures retain normal detail. Derived-tier
repair feedback contains only an opaque case ID and a normalized failure category.
Collection, timeout, runner, and protocol failures are infrastructure errors and do
not trigger an implementation repair. Example and derived generation honors
[test].jobs or --jobs. Every live attempt first passes the protected runner's
full-project overlay and static-loader policy. Only candidate-attributable diagnostics
enter the remaining retry budget. Runner/protocol outages stop without charging
another attempt or evicting the live or cached candidate; a bad committed baseline
likewise retains unrelated validated candidates for a later recovery. Typecheck-only
messages are bounded to 2,000 characters;
executed derived failures still expose only an opaque case ID and category. A late
request failure also preflights and stages its valid partial set. That partial landing
includes verified reheaders, so one exhausted sibling cannot leave every successfully
validated stamp stranded. If separately valid batteries conflict when combined, Jaunt
finds a deterministic compatible subset, runs it with the surviving committed battery
baseline, and commits it only when that complete subset is green. Conflicting paths
remain uncommitted and the command exits 3.
The last rejected candidate is preserved byte-for-byte under
.jaunt/typescript/rejected-tests/; its neighboring JSON record names the source,
tier, fingerprint, digest, errors, and consecutive attempt count. Terminal exhaustion
uses JAUNT_TS_TEST_GENERATION_EXHAUSTED, so CI can identify one battery rather than
waiving every stale battery. Prompt-only provenance drift keeps that code and candidate
pointer; changing the authored test or target API resets it to ordinary staleness. A
later success clears only the exact rejected record it observed, leaving a concurrent
newer failure intact. Only a candidate-attributable rejection removes the matching
cached response before the next run; infrastructure and baseline failures retain it.
Plain progress reports each path and tier, while JSON
includes attempts, retry_count, retry_reasons, candidate paths, terminal state,
any rejection_reasons, and cache_evicted when a cached response was rejected.
Every package that owns a configured test project must declare vitest directly in
devDependencies; add fast-check there too when that package uses @prop. A
hoisted installation alone is not authorization, and Jaunt checks the supported peer
version before spending a model call.
Ordinary co-located *.test.ts[x] and *.spec.ts[x] files are native project tests,
not implementation provenance. Their vitest imports do not need to be production
dependencies. Jaunt test intent still uses the distinct *.jaunt-test.ts[x] suffix.
Leave [target.ts].vitest_config empty for pure generated batteries unless they need
application-specific plugins or setup. Pointing Jaunt at a Vite application config can
change test discovery and collection. If collection fails, fix the runner configuration
and rerun; the implementation is not regenerated.
The analyzer request deadline defaults to 30 seconds and startup defaults to 10:
[target.ts]
worker_timeout_seconds = 60
worker_startup_timeout_seconds = 20
worker_heap_mb = 6144 # optional MiB overrideIncrease the request deadline when a large project closure is valid but slow. Timeout
errors include the latest worker phase timings and point to the relevant config key.
Status and sync validate dependency closures in bounded batches and release compiler
state between batches. If a single closure still exceeds Node's default heap, set
worker_heap_mb to at least 256; Jaunt translates it to --max-old-space-size while
continuing to strip arbitrary NODE_OPTIONS. Heap OOMs are deterministic and are not
replayed automatically.
Unexpected analyzer errors also name the last active phase. If an implementation
candidate passes its request validator but a later transaction-level overlay fails,
Jaunt repairs it immediately within the remaining per-module attempt budget, seeded by
the rejected source and exact conformance diagnostics. JSON candidate_outcomes reports
the attempt count, retry reasons, and whether the module committed or failed. The
candidate also remains in the response cache so an interrupted command can resume
without discarding the paid result.
Starting with @usejaunt/ts 0.1.0-alpha.2, sidecars persist a normalized semantic
environment identity. Stable 0.1.0 sidecars also retain package- and
workspace-scoped compatibility digests. Compatibility follows resolved
declarations and project metadata rather than every entry in a package-manager
lockfile; the full lockfile still participates in ordinary structural change
detection.
The root manifest's packageManager selector is tracked separately as tooling
provenance. Its exact path appears in environment-change JSON, but changing the
selector alone does not alter the model-facing compatibility digest.
Automatic builds stay conservative when the semantic environment changes. For an
existing candidate, run jaunt migrate --language ts --json first. If the saved
contract is otherwise identical and the implementation passes the current compiler,
static policy, public API, and consumer closure, the plan reports free-recompose.
Apply it, then run jaunt test --language ts --no-build and jaunt check --language ts. This path makes no model calls. A changed contract/dependency payload or failed
validation remains model-rebuild or manual-intervention and writes nothing.
The reuse proof also survives the normal two-command workflow: run jaunt build, then
run jaunt test. Jaunt stores the exact validated old-to-new API transition under
.jaunt/; the later test command accepts it only while the current analyzed API still
matches. Build JSON reports every model-free implementation reuse under refrozen and
the compatible-toolchain subset under recomposed, including in mixed workspaces.
Property intent uses one deterministic line:
/** @prop given bytes: fc.uint8Array() :: decode(encode(bytes)) equals bytes */The grammar accepts equals and does not equal. The input may be string,
number, boolean, bigint, Uint8Array, a supported array type, or a
compositional fc expression such as fc.integer({ min: 0, max: 100 }),
fc.tuple(...), or fc.record(...). Strategy expressions are restricted to
fast-check calls and data literals, then typechecked by the owning test project.
Jaunt writes the predicate, semantic case ID, seed, and run count itself. Malformed
prose, any, unknown invariant identifiers, and executable strategy escapes fail
before generation. An async invariant must write await explicitly, uses
fc.asyncProperty, and may consume typed fixtures normally.
Design and contract mode
@jauntDesign asks Codex for a declaration patch before implementation. Review the
dry-run diff, then apply the same source-digest-guarded proposal:
jaunt design --target ts:src/store/index#TokenStore
jaunt design --target ts:src/store/index#TokenStore --applyThe second command applies the recorded patch exactly; it does not call Codex again.
Existing committed TypeScript can enter contract mode without moving behind a generated facade:
jaunt adopt src/tokens/b64url.ts#encode --language ts
jaunt reconcile --language ts
jaunt check --language ts --contracts-only
jaunt eject src/tokens/b64url.ts#encode --language tsReconcile derives committed Vitest batteries from the declaration and TSDoc. With
[contract].strength = true, disposable mutation runs must show that the battery
kills supported behavioral mutants; surviving mutants block the transaction. Magic
eject turns a fresh generated module into ordinary TypeScript, retargets its tests,
and validates normal JavaScript emit, declaration emit, tests, package-output safety,
and runtime independence before removing private Jaunt artifacts.
Context and import rules
index.context.ts is an optional runtime dependency for generated code. It is a leaf:
it cannot value-import its own facade, generated implementation, or a private spec.
Handwritten code that calls the generated API belongs in another module that imports
the facade.
Production code cannot import *.jaunt.ts or *.jaunt-test.ts, even with a loader.
Specs must be excluded from emitting production and test projects. Jaunt checks this
before any model call.
Concrete classes and preserved methods
Class contracts may declare constructors, overloads, explicit this parameters,
generic constraints, methods, accessors, static/readonly fields, optional members,
and concrete inheritance. Jaunt validates each boundary through strict synthetic
adapters instead of relying on TypeScript's bivariant class assignment.
Use @jauntPreserve in a method or accessor's TSDoc when its handwritten body must be
copied into the generated class:
import * as jaunt from "@usejaunt/ts/spec";
import { normalize } from "./formatter.context.js";
jaunt.magicModule();
export class Formatter {
constructor() {
jaunt.magic();
}
/** Normalize one value. @jauntPreserve */
format(value: string): string {
return normalize(value);
}
}Preserved code may use parameters, this, local bindings, standard globals, and
runtime imports from the paired context module. It cannot pull arbitrary runtime
dependencies or private spec modules into generated code.
Module-level runtime constants and authored default objects are not preserved by the
current target. Keep them in the paired context module, or keep the public module as a
small handwritten wrapper around a generated pure core. Broad source_roots globs
can cover those core modules; targeted contract analysis does not require one literal
root per wrapper/core pair.
Mixed Python and TypeScript workspaces
A version-2 root may contain both [target.py] and [target.ts]. Commands operate on
both by default, and --language py|ts narrows a run. TypeScript IDs use the form
ts:src/slug/index#slugify; unprefixed dotted IDs keep their Python meaning.
Version-1 Python projects keep their existing output shape. Version-2 JSON adds a
targets.py / targets.ts partition and qualifies top-level IDs.
Project references and package workspaces
Point projects at a solution tsconfig.json when the workspace uses project
references. Jaunt loads the full reference DAG, keeps solution-only configs as graph
nodes, and assigns each spec to one unambiguous production project. Candidate
validation runs against the owner and every affected downstream project before any
file is replaced.
Cross-project deps should use the same package or path alias the authored spec uses.
Jaunt removes the private .jaunt segment for generated runtime imports instead of
reaching across another project's rootDir with a relative source path. Every config
in one reference graph must resolve the same project-local TypeScript compiler.
Dependency provenance follows the nearest package.json. Production imports must be
declared by that package; devDependencies are accepted only in configured test
roots. npm and pnpm layouts are supported, including pnpm's workspace-visible
compiler symlink into its external store. Keep production compilerOptions.types
explicit (the starter uses ["node"]) so a package manager's incidental hoisting of
transitive @types packages cannot change the ambient compilation contract.
tsconfig path aliases that resolve within the same package are local imports. An
alias that resolves into a sibling workspace package requires that sibling's
package.json name in the importing package's dependencies.
TypeScript npm skills are enabled by [skills].auto unless
[target.ts].auto_skills overrides it. Run jaunt status --json before a build to
inspect the planned file count and bytes. The current implementation plans all installed direct
runtime dependencies; it does not yet trim the plan to imports reachable from one
selected spec.
Security boundary
Static discovery never evaluates application code or executable config. The installed
worker and compiler do execute, and jaunt test executes trusted project tests in a
disposable child process. Jaunt is a build tool, not a sandbox for generated code or
tests.
Fresh generated batteries are still typechecked and run, but they do not call the
model again. A runner- or Vitest-only fingerprint change reheaders the unchanged
battery after overlay validation and reports it as refrozen. When the target API
digest and aggregate battery fingerprint changed, Jaunt verifies the committed battery
against the current implementation and reheaders it when green, with no model call.
Prompt, runner, and Vitest fingerprints may change in the same transition; the current
safety scan still runs before the compiler and Vitest proof.
Run that recovery explicitly in CI before the deterministic check:
jaunt test --language ts --no-build
jaunt check --language ts--no-run cannot prove the transition and therefore disables this shortcut. A failed
compiler or behavioral verification falls back to bounded battery generation; prompt,
fixture, property, body, and other contract drift regenerates directly. jaunt test --force always regenerates.
Current limits
The TypeScript target supports referenced project graphs, cross-spec dependencies, concrete class
inheritance, and @jauntPreserve. Put a preserve tag on the single concrete
implementation of a non-overloaded method or accessor. Its runtime closure is limited
to parameters, this, local bindings, standard globals, and imports from the paired
context module.
The analyzer still rejects .mts, .cts, JavaScript specs, abstract governed
classes, authored private/protected members, parameter properties, computed member
names, mixin or implements heritage, and preserve tags on overload groups. These
cases fail during discovery instead of receiving a weaker conformance check.
The repository JWT example under examples/typescript-jwt is generated by this worker.
It covers classes, contract batteries, held-out tests, mutation strength, and a packed
consumer. The smaller slugify and project-reference fixtures keep the basic authoring
and workspace-routing paths independently testable.
jaunt watch follows TypeScript source, project, package, and lockfile changes while
excluding generated output. Daemon jobs use qualified ts: artifact keys and park
only the exact implementation, API mirror, sidecar, and newly created canonical
facade paths returned by the validated build. Landing rechecks the target before it
commits a proposal; mixed workspaces keep Python and TypeScript job identities apart.
Runnable examples:
The complete design and staged compatibility matrix are in
docs/ts-port/IMPLEMENTATION_PLAN.md.