Files
AILang/docs/PROSE_ROUNDTRIP.md
T
Brummel 72e54f4fd3 iter ext-rename: .ailx → .ail across the live toolchain
The surface-form file extension changes from .ailx to .ail. AILang's
authoring surface now uses the same .ail stem as its canonical JSON
form (.ail.json), giving the language a single coherent extension
family: .ail is the LLM-authored Form A, .ail.json is the canonical
JSON-AST Form B.

Scope (touched):
- 61 example renames examples/**/*.ailx → .ail (git mv)
- 1 rename experiments/.../rendered/ailx.md → ail.md
- 35 content-edited live-toolchain files (crates/, docs/DESIGN.md,
  docs/roadmap.md, docs/PROSE_ROUNDTRIP.md, skills/, bench/reference/*.c,
  experiment crates under experiments/.../{render,harness,master})
- Experiment-crate cohort rename Cohort::Ailx → Cohort::Ail,
  Form::Ailx → Form::Ail, per_cohort/ailx → per_cohort/ail,
  {form-only: ailx} → {form-only: ail}, ```ailx → ```ail

Out of scope (deliberately untouched, to preserve honest history):
- docs/journal-archive.md (content-frozen per CLAUDE.md)
- docs/journals/, docs/specs/, docs/plans/, bench/orchestrator-stats/
- experiments/.../runs/ (frozen LLM-output artefacts; models actually
  saw .ailx — renaming would falsify the experimental record)

Verification: cargo build/test --workspace green; experiment crate
cargo test green; bench/check.py + compile_check.py + cross_lang.py
all 0-regressed; negative grep for ailx|Ailx|AILX outside the
out-of-scope paths returns zero matches.

Opens immediate follow-up: roadmap.md P2 todo `ail check`/build/run
accept .ail extension — after this rename, .ail is canonical
authoring surface but the CLI still produces a misleading JSON-parse
error on `ail check foo.ail`. That's the next iter.
2026-05-12 14:20:27 +02:00

158 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Prose round-trip cycle
## Why
The prose surface (`ail prose`, Iter 20a / 20b) is a deliberately
**lossy** projection of an `.ail.json` module: it strips `(con T)`
wrappings, drops redundant parentheses, infixes arithmetic, suppresses
schema rigor that the LLM can re-derive. That makes prose pleasant to
read and edit — but it also means there is no syntactic parser that
turns edited prose back into a `.ail.json`.
Re-integration of free-text edits therefore requires an **LLM
mediator**: a model that understands both the prose intent and the
load-bearing detail in the original module, and emits a new module
that respects both. This document describes the workflow, the prompt
template `ail merge-prose` composes for that mediator, and the failure
modes to watch for.
## What the LLM produces
Iter 20f revised a load-bearing piece of this cycle: the LLM emits
**Form-A** (an `.ail` document — the canonical authoring surface
fixed by Decision 6), not JSON-AST. JSON-AST is the canonical
hashable artefact, but it is not a writing surface — every type
reference wraps in `{"k": "con", ...}`, every term in
`{"t": "...", ...}`, and a single missing `param_modes` entry is a
schema error rather than a parse error with a position.
Form-A is the form `examples/*.ail` are written in, the form
`ail render` produces, and the form `ail parse` consumes. The full
LLM-targeted specification is shipped inside the binary as
`ailang_core::FORM_A_SPEC` (sourced from
`crates/ailang-core/specs/form_a.md`) and embedded verbatim in every
`merge-prose` prompt; the LLM does not need prior AILang exposure.
## The cycle
```
1. ail prose foo.ail.json > foo.prose.txt
2. $EDITOR foo.prose.txt # human edits freely
3. ail merge-prose foo.ail.json foo.prose.txt > prompt.txt
4. cat prompt.txt | <your-llm-cli> > foo.new.ail
5. ail parse foo.new.ail > foo.new.ail.json
6. ail check foo.new.ail.json
7. mv foo.new.ail.json foo.ail.json # if check is clean
```
Step 1 is the deterministic projection (Iter 20a / 20b).
Step 3 is the prompt composer this iter ships. The prompt embeds the
Form-A spec, the original module rendered as Form-A, and the edited
prose. The original module is loaded via `ailang_core::load_module`
(which validates the schema) and rendered via
`ailang_surface::print`.
Step 4 is the user's external LLM client — Claude Code, the
Anthropic API, OpenAI's CLI, anything that takes a prompt on stdin and
emits text on stdout. AILang ships no client of its own; see *Why no
built-in API client* below.
Step 5 parses the LLM's Form-A back into a JSON-AST. Parser errors
are positional and can be fed back to the LLM in a follow-up prompt.
Step 6 typechecks. The full diagnostic catalogue applies — mode
violations, exhaustiveness, effect closure, etc.
Step 7 accepts the new module if check is clean.
## The prompt template
`ail merge-prose <original.ail.json> <edited.prose.txt>` reads the
original (parsing it, then re-rendering as Form-A), reads the prose
file byte-for-byte, and prints the following structure to stdout —
no fences, no JSON envelope. The shape is exactly what a human would
compose by hand if they wanted to drive the same cycle without the
CLI helper.
```
You are integrating prose edits back into an AILang module.
ROLE
Your job is to produce an updated AILang module in Form-A (an .ail
file) that reflects the human's prose edits while preserving the
load-bearing semantic detail from the original module.
CONTRACT
The prose is the source of intent ... [list of preservation rules]
OUTPUT
Output ONLY the new Form-A bytes. ... [no fences, no commentary]
FORM-A SPECIFICATION
<<<FORM_A_SPEC
[ailang_core::FORM_A_SPEC, verbatim]
FORM_A_SPEC
ORIGINAL MODULE (Form-A)
<<<ORIGINAL_FORM_A
[ailang_surface::print(&original_module)]
ORIGINAL_FORM_A
EDITED PROSE
<<<EDITED_PROSE
[edited prose, verbatim]
EDITED_PROSE
Emit the updated Form-A module now.
```
The three payloads (spec, original Form-A, edited prose) are inserted
between heredoc-style markers. `merge-prose` does not strip,
re-encode, or otherwise transform the prose or the spec. The original
is parsed and re-rendered (which is lossless for any module that has
been through `ail parse` / `ail render` already; round-trip is a
gating contract on the surface crate).
## Failure modes
The mediator is an LLM, so the cycle is intentionally iterative.
Common failure modes and their fix:
- **Output wrapped in markdown fences (` ```ailang ... ``` `).**
Strip the fences and re-run `ail parse`. If the LLM does this
consistently, paste a corrective note ("emit raw Form-A, no
fences") at the top of the next prompt.
- **Output contains commentary before/after the module.** Same
treatment. Most modern LLMs respect "OUTPUT ONLY" but not all.
- **`ail parse` rejects the output.** The parser emits positional
errors. Re-run with the parse error pasted into the prompt as a
corrective note — "your previous output failed at byte N with
`expected X, got Y`; fix that and re-emit".
- **Parse-clean but `ail check` fails.** Re-run with the
diagnostic messages pasted in. Common cases on first-time LLM
use: missing mode annotation on a `(fn ...)` def
(`mode-required-on-fn-param`), `(app)` used where `(term-ctor)`
was needed, missing `(effects IO)` on a fn that calls `(do io/...)`.
- **Preserved detail dropped (mode flipped, doc string lost,
suppress clause stripped).** Re-run with a corrective note
naming the missing detail. The contract section calls these out
explicitly, but a long edit may push the LLM to re-derive
rather than preserve.
The cycle converges in 12 rounds for typical edits with a
modern frontier model.
## Why no built-in API client
AILang stays a compiler + tooling. Shipping an HTTP client to one
particular LLM vendor would mean an API key story, rate limiting,
streaming semantics, error mapping, version tracking — all carrying
zero language-level value. The user already has Claude Code, the
Anthropic SDK, the OpenAI CLI, `curl`, or any other client they
prefer; `ail merge-prose` composes the prompt and gets out of the
way. The Unix-pipe shape (`ail merge-prose ... | client | ail parse |
ail check`) makes the cycle scriptable without lock-in to one vendor.
A future iter may layer a tool-use schema (LLM calls back into
`ail parse` / `ail check` from inside its turn) or an MCP server
(any MCP-compatible client discovers AILang's resources, tools, and
prompts) on top of this cycle. Both are additive — the static-prompt
fallback documented here remains the lowest-common-denominator path
that always works.