Metadata-Version: 2.4
Name: jiti
Version: 1.5.1
Summary: Just In Time Implementation: declare a typed function or method by its interface and let an LLM write, validate, and cache a real implementation.
Keywords: llm,codegen,ai,anthropic,claude,litellm,tdd,agent
Author: Ryan Saxe
Author-email: Ryan Saxe <ryancsaxe+1@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Dist: litellm>=1.85.0
Requires-Dist: pydantic>=2.13.4
Requires-Dist: ruff>=0.15.14
Requires-Dist: ty>=0.0.39,<0.1
Requires-Python: >=3.13
Project-URL: Homepage, https://github.com/RyanSaxe/jiti
Project-URL: Documentation, https://ryansaxe.github.io/jiti/
Project-URL: Repository, https://github.com/RyanSaxe/jiti
Project-URL: Issues, https://github.com/RyanSaxe/jiti/issues
Description-Content-Type: text/markdown

# jiti

[![CI](https://github.com/RyanSaxe/jiti/actions/workflows/ci.yml/badge.svg)](https://github.com/RyanSaxe/jiti/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/jiti.svg)](https://pypi.org/project/jiti/)
[![Docs](https://img.shields.io/badge/docs-ryansaxe.github.io%2Fjiti-blue)](https://ryansaxe.github.io/jiti/)
![Python](https://img.shields.io/badge/python-3.13%2B-blue)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)

**Interface-first Python.** Declare the interfaces, wire them into a call graph, run the
program — an LLM writes the implementations the first time each one is called, and the
result is real, committable code that you keep.

## The idea

You decide *what*: typed signatures, docstrings, and tests. You decide *how the pieces
fit*: which function calls which. Then you run, and bodies appear just in time, get
validated against ruff + ty + your tests, and land as plain Python under `.jiti/`. Every
call after that is plain dispatch — no model, no API key, no network. When you're ready,
`jiti merge` folds the generated code back into your source and removes the decorator.

You can stop using jiti at any time and keep everything it wrote.

## A tiny example

```python
from jiti import jiti


@jiti
def slugify(text: str) -> str:
    """Convert text to a URL-safe slug."""
    ...
```

The first call to `slugify("Hello, World!")` runs an agent that inspects the real
arguments, explores your repo, drafts code, runs ruff + ty + any tests you've gated on it,
and writes the result to a file beside your source. Every call after that runs that file.

## Wiring a graph

Interface-first pays off when interfaces compose. You write the orchestration in plain
Python — that's *your* code, and that's where the graph lives. jiti writes the leaves.

```python
from jiti import jiti


@jiti
def satisfies(version: str, spec: str) -> bool:
    """True if `version` satisfies `spec`. Specs: exact, '>=', '>', '<=', '<', '~', '^'."""
    ...


@jiti
def sort_versions(versions: list[str]) -> list[str]:
    """Return the version strings sorted ascending by semver precedence."""
    ...


# Your code — plain Python — composing the jiti pieces:
def latest_matching(versions: list[str], spec: str) -> str | None:
    """Return the highest-precedence version satisfying `spec`, or None."""
    candidates = [v for v in versions if satisfies(v, spec)]
    return sort_versions(candidates)[-1] if candidates else None
```

`latest_matching` is yours — no decorator, no magic, just a function. The first call to
`latest_matching(["1.0.0", "2.0.0", "2.1.3"], "^2.0.0")` runs your code, which calls
`satisfies` and `sort_versions`, which jiti generates on demand (and which may themselves
need other stubs along the way — generation cascades). Every call after that is plain
dispatch.

The full runnable version (with a `Version` dataclass, more stubs, and a method) lives in
[`examples/semver/`](examples/semver/).

### Composition contracts

When the correct implementation of one piece *must* go through another, say so with
`uses=` instead of hoping the docstring is persuasive:

```python
@jiti(uses=[satisfies, sort_versions])
def latest_matching(versions: list[str], spec: str) -> str | None:
    """Return the highest-precedence version satisfying `spec`, or None."""
    ...
```

You pass the symbols themselves (functions or classes — type-checked at the call site,
refactor-safe). The agent is told each one's signature and docstring as a MUST-use, and
validation statically rejects any candidate that never references them — so jiti can't
quietly re-implement `satisfies` inline and drift from your real one. Changing the
`uses=` list regenerates, just like editing the docstring.

## Install

```bash
pip install jiti     # or: uv add jiti
```

Needs Python 3.13+. Generation uses LiteLLM and defaults to Claude Sonnet 4.6; set
`ANTHROPIC_API_KEY` for the default model, or set `JITI_MODEL` to any LiteLLM model id
and provide that provider's API key. Running already-generated code needs nothing — no
key, no network.

```bash
ANTHROPIC_API_KEY=... python your_script.py
OPENAI_API_KEY=... JITI_MODEL=openai/<model-id> python your_script.py
```

## Stubs

A stub is a function with a docstring and a placeholder body: `...`, `pass`, or
`raise NotImplementedError`. A real body is an error — `@jiti` means "write this for me."
A comment in the stub becomes a hint:

```python
@jiti
def parse_money(raw: str) -> Decimal:
    """Parse a currency string like '$1,234.56' into a Decimal."""
    # strip the symbols and separators, then Decimal()
    ...
```

Methods and `async def` functions work the same way — decorate the targets you want
generated, use `self` freely on methods, and `await` async targets normally (see
`Version.bump` in `examples/semver/core.py`).

> Strict type checkers flag an empty body with a non-`None` return (`empty-body`). That's
> your checker reacting to the stub, not jiti. Disable that rule or use `raise NotImplementedError`.

## Test-driven generation

State a function's definition of done from your test file with `@jiti.required_for(target)`.
Tests import the real code, so the reference is type-checked — and running `pytest` *is*
the loop: generation happens to make your tests pass, red → green.

```python
# tests/test_money.py
from app.money import parse_money
from jiti import jiti


@jiti.required_for(parse_money)        # real body → your gate test, run as-is
def test_parses_symbols():
    assert parse_money("$1,234.56") == Decimal("1234.56")


@jiti.required_for(parse_money)        # empty body → jiti writes this test from the interface
def test_rejects_garbage() -> None:
    """parse_money raises ValueError on '' and 'not money'."""
    ...
```

An empty-bodied stub is a **jiti-test**: written before the implementation exists, so it
can only see the interface and can't couple to internals. jiti writes it, commits it
under `.jiti/tests/`, and gates the implementation on it. Both are ordinary `test_*`
functions your own `pytest` run executes.

## Graduating off jiti

Interface-first is a *development mode* you can leave. `jiti merge` folds the generated
implementation back into your source, replaces the stub, removes `@jiti`, and cleans up
the mirror:

```bash
jiti status                  # what's generated, what you've hand-edited
jiti merge app.text.slugify  # inline one function into its source
jiti merge --all             # graduate the entire project
```

After `merge --all`, you have plain Python, no jiti dependency required. See the
[reference](https://ryansaxe.github.io/jiti/reference/) for the full CLI and
configuration surface.

## A few things worth knowing

- **The code is yours.** Edit a generated body and jiti runs it as-is — it tracks a hash
  and won't clobber your edits. Change a stub's signature, docstring, or gates and it
  regenerates; if you'd hand-edited that section, it surfaces a conflict instead.
- **git is yours.** jiti only writes files into `.jiti/`. Commit it (so production runs
  cached code with no key) or gitignore it. jiti never runs git.
- **Freeze for production.** Set `JITI_FROZEN=1` (or pass `Engine(frozen=True)`) to make
  any cache miss raise `FrozenError` instead of silently calling the LLM. Generate in
  development, commit `.jiti/`, then deploy with the freeze on — no key, no surprise
  latency, no surprise cost.
- **Concurrency.** Running generated code is fully safe — it's plain dispatch. Within a
  process, concurrent first calls to the same function share one generation (the losers
  wait, then run the winner's code). Across *processes* there is no locking — warm the
  cache once, then parallelize. Writes are atomic, so a reader never sees a half-written
  file.

## Where to go next

- [Documentation](https://ryansaxe.github.io/jiti/) — concepts, guides, the full
  reference, and an auto-generated API tour.
- [`examples/semver/`](examples/semver/) — a runnable interface-first walkthrough: stubs,
  a graph, tests, and the `merge` graduation.
- [`CHANGELOG.md`](CHANGELOG.md) — release history.

## Development

```bash
uv sync
uv run pre-commit install
```

The gate, exactly what CI runs — ruff-format, ruff, ty, then pytest (no API key; uses a
fake client):

```bash
uv run pre-commit run --all-files
uv run pytest
```

## Status

Today, jiti supports sync and async free functions and methods, lazy agentic generation
with cascading across the call graph, in-process validation (ruff + ty + pytest),
test-driven generation via `@jiti.required_for` (works on free functions and methods),
a score-gated refactor pass, common decorator stacks (`@staticmethod`, `@classmethod`,
`@property`, and `functools` wrappers), the edit/conflict lifecycle, and the `jiti` CLI
(`status` / `merge` / `test` / `clear`). Anthropic, OpenAI, Gemini, and other model
families are available through LiteLLM.

Not yet: a pytest plugin, whole-class generation, cross-process generation locking, and
dependency-aware invalidation.

## Inspiration

[This video](https://www.youtube.com/watch?v=HOO_yaidVWk) is what inspired me to make this package. I then found [this package](https://github.com/JirkaKlimes/jit-implementation), a joke implementation of the idea from 2 years ago. But now we've gotten to the point where LLMs are good enough that maybe, just maybe, this direction is worth entertaining.
