Jaunt
Guides

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/node

jaunt 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 --apply

The 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 --apply

In 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:

src/slug/index.jaunt.ts
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:

src/slug/index.ts
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 record

Source 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 ts

sync 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 ts

The 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 override

Increase 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 --apply

The 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 ts

Reconcile 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.

On this page