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.
This commit is contained in:
2026-05-12 14:20:27 +02:00
parent 17b370bbb3
commit 72e54f4fd3
97 changed files with 318 additions and 210 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_compute_collatz. // Hand-C reference for bench_compute_collatz.
// //
// Same algorithm as examples/bench_compute_collatz.ailx — for each // Same algorithm as examples/bench_compute_collatz.ail — for each
// starting value in [1..N], count Collatz steps to reach 1, sum. // starting value in [1..N], count Collatz steps to reach 1, sum.
// Three sizes: 10k / 100k / 500k starting values. // Three sizes: 10k / 100k / 500k starting values.
// //
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_compute_intsum. // Hand-C reference for bench_compute_intsum.
// //
// Same algorithm as examples/bench_compute_intsum.ailx — accumulate // Same algorithm as examples/bench_compute_intsum.ail — accumulate
// `i * 7` for i in [n, n-1, ..., 1], printing the final acc. // `i * 7` for i in [n, n-1, ..., 1], printing the final acc.
// Three sizes: 1M / 10M / 50M iterations. // Three sizes: 1M / 10M / 50M iterations.
// //
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_list_sum. // Hand-C reference for bench_list_sum.
// //
// Same algorithm as examples/bench_list_sum.ailx — build a linked list // Same algorithm as examples/bench_list_sum.ail — build a linked list
// of [0, 1, ..., N-1] via prepending, then sum by linear traversal. // of [0, 1, ..., N-1] via prepending, then sum by linear traversal.
// Three workload sizes: 100k / 1M / 3M cells, matching the AILang // Three workload sizes: 100k / 1M / 3M cells, matching the AILang
// fixture exactly so the AILang/C wall-time ratio is fair. // fixture exactly so the AILang/C wall-time ratio is fair.
+1 -1
View File
@@ -2,7 +2,7 @@
// //
// Companion to list_sum.c (which leaks); this version adds explicit // Companion to list_sum.c (which leaks); this version adds explicit
// `free()` calls to mirror AILang's explicit-mode RC dec-tax. Pairs // `free()` calls to mirror AILang's explicit-mode RC dec-tax. Pairs
// with examples/bench_list_sum_explicit.ailx via bench/cross_lang.py. // with examples/bench_list_sum_explicit.ail via bench/cross_lang.py.
// //
// The free pass walks the list after sum, freeing each cell. This is // The free pass walks the list after sum, freeing each cell. This is
// genuinely O(N) work that bench_list_sum.c never paid; the rc/c // genuinely O(N) work that bench_list_sum.c never paid; the rc/c
+1 -1
View File
@@ -1,6 +1,6 @@
// Hand-C reference for bench_tree_walk. // Hand-C reference for bench_tree_walk.
// //
// Same algorithm as examples/bench_tree_walk.ailx — build a balanced // Same algorithm as examples/bench_tree_walk.ail — build a balanced
// binary tree of given depth (every value = 1) and sum every node. // binary tree of given depth (every value = 1) and sum every node.
// Three depths: 16 / 18 / 20 (= 65535 / 262143 / 1048575 nodes). // Three depths: 16 / 18 / 20 (= 65535 / 262143 / 1048575 nodes).
// //
+3 -3
View File
@@ -190,7 +190,7 @@ enum Cmd {
#[arg(long)] #[arg(long)]
json: bool, json: bool,
}, },
/// Parses a `.ailx` source file (form (A)) into canonical /// Parses a `.ail` source file (form (A)) into canonical
/// `.ail.json`. Iter 14c addition; symmetric to `render`. /// `.ail.json`. Iter 14c addition; symmetric to `render`.
/// ///
/// The form-(A) projection is one of potentially many producers of /// The form-(A) projection is one of potentially many producers of
@@ -269,7 +269,7 @@ fn compose_merge_prose_prompt(original_form_a: &str, edited_prose: &str) -> Stri
"You are integrating prose edits back into an AILang module. "You are integrating prose edits back into an AILang module.
ROLE ROLE
Your job is to produce an updated AILang module in Form-A (an .ailx 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 file) that reflects the human's prose edits while preserving the
load-bearing semantic detail from the original module. load-bearing semantic detail from the original module.
@@ -826,7 +826,7 @@ fn main() -> Result<()> {
} }
} }
Cmd::Parse { path, output } => { Cmd::Parse { path, output } => {
// Read .ailx, parse via the surface crate, emit canonical // Read .ail, parse via the surface crate, emit canonical
// JSON. Symmetric to `render` (which goes the other way). // JSON. Symmetric to `render` (which goes the other way).
let src = std::fs::read_to_string(&path) let src = std::fs::read_to_string(&path)
.with_context(|| format!("reading {}", path.display()))?; .with_context(|| format!("reading {}", path.display()))?;
+3 -3
View File
@@ -429,8 +429,8 @@ fn render_parse_round_trip_canonical() {
.expect("run ail render"); .expect("run ail render");
assert!(render.status.success(), "ail render failed"); assert!(render.status.success(), "ail render failed");
let tmp = std::env::temp_dir().join(format!("ailang_render_e2e_{}.ailx", std::process::id())); let tmp = std::env::temp_dir().join(format!("ailang_render_e2e_{}.ail", std::process::id()));
std::fs::write(&tmp, &render.stdout).expect("write rendered ailx"); std::fs::write(&tmp, &render.stdout).expect("write rendered ail");
let parsed = Command::new(ail_bin()) let parsed = Command::new(ail_bin())
.args(["parse", tmp.to_str().unwrap()]) .args(["parse", tmp.to_str().unwrap()])
@@ -2153,7 +2153,7 @@ fn build_and_run_with_rc_stats(example: &str) -> (String, u64, u64, i64) {
/// not leak the LCons outer cells. /// not leak the LCons outer cells.
/// ///
/// Background: 18f.2's tail-latency bench found that /// Background: 18f.2's tail-latency bench found that
/// `bench_latency_explicit.ailx` peaks at the same RSS as the /// `bench_latency_explicit.ail` peaks at the same RSS as the
/// implicit-mode variant despite carrying mode annotations /// implicit-mode variant despite carrying mode annotations
/// throughout the hot path. Diagnosis: in /// throughout the hot path. Diagnosis: in
/// ///
+7 -7
View File
@@ -62,7 +62,7 @@ fn roundtrip_one(fixture: &Path, tmpdir: &Path) -> Result<(), String> {
let bytes_orig = ailang_core::canonical::to_bytes(&original_module); let bytes_orig = ailang_core::canonical::to_bytes(&original_module);
let h_orig = blake3::hash(&bytes_orig); let h_orig = blake3::hash(&bytes_orig);
// Step B: `ail render <fixture>` → captured stdout = ailx text. // Step B: `ail render <fixture>` → captured stdout = ail text.
let render_out = Command::new(ail_bin()) let render_out = Command::new(ail_bin())
.args(["render", fixture.to_str().unwrap()]) .args(["render", fixture.to_str().unwrap()])
.output() .output()
@@ -75,20 +75,20 @@ fn roundtrip_one(fixture: &Path, tmpdir: &Path) -> Result<(), String> {
)); ));
} }
// Step C: write the rendered .ailx into the tempdir. // Step C: write the rendered .ail into the tempdir.
let stem = fixture let stem = fixture
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(".ail.json")) .and_then(|n| n.strip_suffix(".ail.json"))
.unwrap_or("fixture"); .unwrap_or("fixture");
let tmp_ailx = tmpdir.join(format!("{stem}.round.ailx")); let tmp_ail = tmpdir.join(format!("{stem}.round.ail"));
std::fs::write(&tmp_ailx, &render_out.stdout) std::fs::write(&tmp_ail, &render_out.stdout)
.map_err(|e| format!("write tempfile {}: {e}", tmp_ailx.display()))?; .map_err(|e| format!("write tempfile {}: {e}", tmp_ail.display()))?;
// Step D: `ail parse <tmp_ailx>` → captured stdout = canonical // Step D: `ail parse <tmp_ail>` → captured stdout = canonical
// bytes + trailing newline. // bytes + trailing newline.
let parse_out = Command::new(ail_bin()) let parse_out = Command::new(ail_bin())
.args(["parse", tmp_ailx.to_str().unwrap()]) .args(["parse", tmp_ail.to_str().unwrap()])
.output() .output()
.map_err(|e| format!("spawn ail parse: {e}"))?; .map_err(|e| format!("spawn ail parse: {e}"))?;
if !parse_out.status.success() { if !parse_out.status.success() {
+7 -7
View File
@@ -2,7 +2,7 @@
Form-A is the canonical textual surface of AILang. It is the form that Form-A is the canonical textual surface of AILang. It is the form that
LLMs generate when asked to produce or edit AILang code, and the form LLMs generate when asked to produce or edit AILang code, and the form
that `ail parse <file>.ailx` reads. The inverse direction — printing that `ail parse <file>.ail` reads. The inverse direction — printing
JSON-AST as Form-A — is `ail render <file>.ail.json`. Round-trip JSON-AST as Form-A — is `ail render <file>.ail.json`. Round-trip
through this pair is the gating contract: `parse(render(m)) == m` through this pair is the gating contract: `parse(render(m)) == m`
for every well-formed module. for every well-formed module.
@@ -26,7 +26,7 @@ Lisp-style S-expression dress that:
- omits structural noise (no field tags; positions carry meaning) - omits structural noise (no field tags; positions carry meaning)
- has a real parser with positional error messages - has a real parser with positional error messages
- round-trips through `ail render``ail parse` losslessly - round-trips through `ail render``ail parse` losslessly
- is the form every existing `examples/*.ailx` is written in - is the form every existing `examples/*.ail` is written in
LLMs generate Form-A; the toolchain converts to JSON. LLMs generate Form-A; the toolchain converts to JSON.
@@ -48,7 +48,7 @@ LLMs generate Form-A; the toolchain converts to JSON.
DEF*) DEF*)
``` ```
The module name MUST equal the file stem (`bench_list_sum.ailx` The module name MUST equal the file stem (`bench_list_sum.ail`
`(module bench_list_sum ...)`). `(module bench_list_sum ...)`).
## Imports ## Imports
@@ -269,11 +269,11 @@ significantly.
## Few-shot corpus ## Few-shot corpus
These four modules are real `examples/*.ailx` content. Each one is These four modules are real `examples/*.ail` content. Each one is
parseable and typechecks clean. Pattern-match against them when parseable and typechecks clean. Pattern-match against them when
generating new code. generating new code.
### 1 — `hello.ailx`: minimal IO program ### 1 — `hello.ail`: minimal IO program
``` ```
(module hello (module hello
@@ -283,7 +283,7 @@ generating new code.
(body (do io/print_str "Hello, AILang.")))) (body (do io/print_str "Hello, AILang."))))
``` ```
### 2 — `borrow_own_demo.ailx`: mode annotations on a recursive list ### 2 — `borrow_own_demo.ail`: mode annotations on a recursive list
``` ```
(module borrow_own_demo (module borrow_own_demo
@@ -333,7 +333,7 @@ generating new code.
(do io/print_int (app sum_list xs))))))) (do io/print_int (app sum_list xs)))))))
``` ```
### 3 — `lit_pat.ailx`: literal patterns and nested ctor patterns ### 3 — `lit_pat.ail`: literal patterns and nested ctor patterns
``` ```
(module lit_pat (module lit_pat
+33 -33
View File
@@ -10,18 +10,18 @@
//! For every `examples/*.ail.json` fixture, load → `print` → //! For every `examples/*.ail.json` fixture, load → `print` →
//! `parse` → canonical bytes; assert byte-equal to original. //! `parse` → canonical bytes; assert byte-equal to original.
//! //!
//! 2. `every_ailx_fixture_matches_its_json_counterpart`: hand- //! 2. `every_ail_fixture_matches_its_json_counterpart`: hand-
//! authored ground-truth check. For every `examples/*.ailx` //! authored ground-truth check. For every `examples/*.ail`
//! fixture, parse → canonical bytes; if a same-stem `.ail.json` //! fixture, parse → canonical bytes; if a same-stem `.ail.json`
//! counterpart exists, assert canonical-byte equality against //! counterpart exists, assert canonical-byte equality against
//! it. Pins the `.ailx` corpus against semantic drift between //! it. Pins the `.ail` corpus against semantic drift between
//! the two forms at the fixture level. //! the two forms at the fixture level.
//! //!
//! 3. `parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture`: //! 3. `parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`:
//! Direction 2 of the Roundtrip Invariant. For every well-formed //! Direction 2 of the Roundtrip Invariant. For every well-formed
//! `.ailx` text `t`, asserts `canonical_bytes(parse(t))` equals //! `.ail` text `t`, asserts `canonical_bytes(parse(t))` equals
//! `canonical_bytes(parse(print(parse(t))))`. Robust against //! `canonical_bytes(parse(print(parse(t))))`. Robust against
//! future `.ailx` fixtures without a JSON counterpart. //! future `.ail` fixtures without a JSON counterpart.
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -100,7 +100,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
Ok(()) Ok(())
} }
fn list_ailx_fixtures() -> Vec<PathBuf> { fn list_ail_fixtures() -> Vec<PathBuf> {
let dir = examples_dir(); let dir = examples_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir) let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display())) .unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
@@ -109,7 +109,7 @@ fn list_ailx_fixtures() -> Vec<PathBuf> {
.filter(|p| { .filter(|p| {
p.file_name() p.file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.map(|n| n.ends_with(".ailx")) .map(|n| n.ends_with(".ail"))
.unwrap_or(false) .unwrap_or(false)
}) })
.collect(); .collect();
@@ -118,11 +118,11 @@ fn list_ailx_fixtures() -> Vec<PathBuf> {
} }
#[test] #[test]
fn every_ailx_fixture_matches_its_json_counterpart() { fn every_ail_fixture_matches_its_json_counterpart() {
let fixtures = list_ailx_fixtures(); let fixtures = list_ail_fixtures();
assert!( assert!(
!fixtures.is_empty(), !fixtures.is_empty(),
"no .ailx fixtures found under {}", "no .ail fixtures found under {}",
examples_dir().display() examples_dir().display()
); );
@@ -130,32 +130,32 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
let mut paired = 0usize; let mut paired = 0usize;
let mut parse_only = 0usize; let mut parse_only = 0usize;
for ailx_path in &fixtures { for ail_path in &fixtures {
let text = match std::fs::read_to_string(ailx_path) { let text = match std::fs::read_to_string(ail_path) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
failures.push(format!("{}: read failed: {e}", ailx_path.display())); failures.push(format!("{}: read failed: {e}", ail_path.display()));
continue; continue;
} }
}; };
let parsed = match ailang_surface::parse(&text) { let parsed = match ailang_surface::parse(&text) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
failures.push(format!("{}: parse failed: {e}", ailx_path.display())); failures.push(format!("{}: parse failed: {e}", ail_path.display()));
continue; continue;
} }
}; };
// Same-stem counterpart lookup. `<stem>.ailx` → `<stem>.ail.json`. // Same-stem counterpart lookup. `<stem>.ail` → `<stem>.ail.json`.
let stem = ailx_path let stem = ail_path
.file_name() .file_name()
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.and_then(|n| n.strip_suffix(".ailx")) .and_then(|n| n.strip_suffix(".ail"))
.unwrap_or(""); .unwrap_or("");
let json_path = ailx_path.with_file_name(format!("{stem}.ail.json")); let json_path = ail_path.with_file_name(format!("{stem}.ail.json"));
if !json_path.exists() { if !json_path.exists() {
// Spec: parse success alone is sufficient for `.ailx` files // Spec: parse success alone is sufficient for `.ail` files
// without a JSON counterpart. (Today: none expected.) // without a JSON counterpart. (Today: none expected.)
parse_only += 1; parse_only += 1;
continue; continue;
@@ -166,7 +166,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
Err(e) => { Err(e) => {
failures.push(format!( failures.push(format!(
"{}: load_module({}) failed: {e}", "{}: load_module({}) failed: {e}",
ailx_path.display(), ail_path.display(),
json_path.display() json_path.display()
)); ));
continue; continue;
@@ -179,7 +179,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
let s_round = String::from_utf8_lossy(&bytes_parsed).into_owned(); let s_round = String::from_utf8_lossy(&bytes_parsed).into_owned();
failures.push(format!( failures.push(format!(
"{} vs {}: canonical bytes differ.\noriginal: {s_orig}\nparsed: {s_round}", "{} vs {}: canonical bytes differ.\noriginal: {s_orig}\nparsed: {s_round}",
ailx_path.display(), ail_path.display(),
json_path.display() json_path.display()
)); ));
continue; continue;
@@ -189,7 +189,7 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
if !failures.is_empty() { if !failures.is_empty() {
panic!( panic!(
"{} of {} .ailx fixture(s) failed cross-check (paired ok: {}, parse-only: {}):\n{}", "{} of {} .ail fixture(s) failed cross-check (paired ok: {}, parse-only: {}):\n{}",
failures.len(), failures.len(),
fixtures.len(), fixtures.len(),
paired, paired,
@@ -198,27 +198,27 @@ fn every_ailx_fixture_matches_its_json_counterpart() {
); );
} }
eprintln!( eprintln!(
".ailx cross-check ok ({} paired, {} parse-only)", ".ail cross-check ok ({} paired, {} parse-only)",
paired, parse_only paired, parse_only
); );
} }
/// Direction 2 of the Roundtrip Invariant (DESIGN.md §"Roundtrip /// Direction 2 of the Roundtrip Invariant (DESIGN.md §"Roundtrip
/// Invariant"): for every well-formed `.ailx` text `t`, the /// Invariant"): for every well-formed `.ail` text `t`, the
/// composition `parse → print → parse` is idempotent on the AST. /// composition `parse → print → parse` is idempotent on the AST.
/// ///
/// For the 57 `.ailx` fixtures that have a JSON counterpart this /// For the 57 `.ail` fixtures that have a JSON counterpart this
/// follows logically from `print_then_parse_round_trips_every_fixture` /// follows logically from `print_then_parse_round_trips_every_fixture`
/// + `every_ailx_fixture_matches_its_json_counterpart`. This test /// + `every_ail_fixture_matches_its_json_counterpart`. This test
/// asserts the property directly so it stays robust for future /// asserts the property directly so it stays robust for future
/// `.ailx` fixtures without a JSON counterpart, and so the spec's /// `.ail` fixtures without a JSON counterpart, and so the spec's
/// Direction-2 claim has a dedicated enforcement point. /// Direction-2 claim has a dedicated enforcement point.
#[test] #[test]
fn parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture() { fn parse_then_print_then_parse_is_idempotent_on_every_ail_fixture() {
let fixtures = list_ailx_fixtures(); let fixtures = list_ail_fixtures();
assert!( assert!(
!fixtures.is_empty(), !fixtures.is_empty(),
"no .ailx fixtures found under {}", "no .ail fixtures found under {}",
examples_dir().display() examples_dir().display()
); );
@@ -266,12 +266,12 @@ fn parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture() {
if !failures.is_empty() { if !failures.is_empty() {
panic!( panic!(
"{} of {} .ailx fixture(s) failed idempotency check (passed: {}):\n{}", "{} of {} .ail fixture(s) failed idempotency check (passed: {}):\n{}",
failures.len(), failures.len(),
fixtures.len(), fixtures.len(),
passed, passed,
failures.join("\n\n") failures.join("\n\n")
); );
} }
eprintln!("parse→print→parse idempotency ok for {passed} .ailx fixtures"); eprintln!("parse→print→parse idempotency ok for {passed} .ail fixtures");
} }
+25 -25
View File
@@ -175,7 +175,7 @@ Trade-off: no inline optimisations through the LLVM API. We rely on
Form (A) is implemented as Form (A) is implemented as
the `ailang-surface` crate (parser + printer). Form-A is the `ailang-surface` crate (parser + printer). Form-A is
gated against drift by `ailang-surface/tests/round_trip.rs`, which gated against drift by `ailang-surface/tests/round_trip.rs`, which
parses every `.ailx` fixture, prints it back, re-parses, and demands parses every `.ail` fixture, prints it back, re-parses, and demands
canonical-byte equality. `ail render` and both branches canonical-byte equality. `ail render` and both branches
of `ail describe` were rewired to use `ailang_surface::print`, making of `ail describe` were rewired to use `ailang_surface::print`, making
form (A) the **sole** text projection of a module — the legacy form (A) the **sole** text projection of a module — the legacy
@@ -247,7 +247,7 @@ the AST and remain projection-agnostic.
semantic indentation, maximal-munch lexing, context-sensitive semantic indentation, maximal-munch lexing, context-sensitive
reductions. reductions.
2. **AST-isomorphic.** Every surface form maps to exactly one AST 2. **AST-isomorphic.** Every surface form maps to exactly one AST
shape. The full bijection between `.ail.json` and `.ailx` (both shape. The full bijection between `.ail.json` and `.ail` (both
directions, BLAKE3-stable hashing, Float-bits-hex encoding, directions, BLAKE3-stable hashing, Float-bits-hex encoding,
workspace-CI enforcement points) is anchored as the top-level workspace-CI enforcement points) is anchored as the top-level
§"Roundtrip Invariant" — this constraint records that Decision §"Roundtrip Invariant" — this constraint records that Decision
@@ -437,11 +437,11 @@ on the shelf for a future iter only if both fail.
continue to consume `ailang-core::ast::Module` values regardless continue to consume `ailang-core::ast::Module` values regardless
of which projection produced them. of which projection produced them.
- Round-trip test: for every `examples/*.ail.json`, parse the - Round-trip test: for every `examples/*.ail.json`, parse the
corresponding hand-written `*.ailx`, canonicalise, and assert corresponding hand-written `*.ail`, canonicalise, and assert
hash-equivalence to the original. Hash equivalence is the truth hash-equivalence to the original. Hash equivalence is the truth
check; the surface ships only if every fixture round-trips check; the surface ships only if every fixture round-trips
identically. identically.
- CLI: `ail parse <file.ailx> -o <file.ail.json>`. Symmetric to - CLI: `ail parse <file.ail> -o <file.ail.json>`. Symmetric to
existing `ail render`. **`.ail.json` remains a first-class input** existing `ail render`. **`.ail.json` remains a first-class input**
to every existing subcommand; the parser is a producer, not a to every existing subcommand; the parser is a producer, not a
gatekeeper. gatekeeper.
@@ -497,7 +497,7 @@ Neither change extends the grammar's rule budget meaningfully:
the 30-production ceiling of constraint 1 is intact (the parser the 30-production ceiling of constraint 1 is intact (the parser
implements ~28 named productions). All 17 `examples/*.ail.json` implements ~28 named productions). All 17 `examples/*.ail.json`
fixtures round-trip identically through `print → parse → canonical fixtures round-trip identically through `print → parse → canonical
JSON`; the three hand-written `.ailx` exhibits parse to canonical JSON`; the three hand-written `.ail` exhibits parse to canonical
JSON identical to their corresponding `.ail.json` files. JSON identical to their corresponding `.ail.json` files.
3. **Tail-call surface.** Decision 8 ships two new 3. **Tail-call surface.** Decision 8 ships two new
@@ -580,12 +580,12 @@ foreign LLM enough to produce valid output. The current prompt revises this:
- The LLM emits **Form-A** (the canonical authoring surface), not - The LLM emits **Form-A** (the canonical authoring surface), not
JSON. JSON-AST stays the only hashable artefact, but it is not JSON. JSON-AST stays the only hashable artefact, but it is not
a writing surface. The user runs `ail parse foo.new.ailx` a writing surface. The user runs `ail parse foo.new.ail`
before `ail check` to produce the canonical JSON. before `ail check` to produce the canonical JSON.
- `crates/ailang-core/specs/form_a.md` is the complete LLM-targeted - `crates/ailang-core/specs/form_a.md` is the complete LLM-targeted
Form-A specification — grammar, every term / pattern / type / Form-A specification — grammar, every term / pattern / type /
def keyword, schema invariants, pitfall catalogue, four def keyword, schema invariants, pitfall catalogue, four
few-shot modules drawn from `examples/*.ailx`. It is exported few-shot modules drawn from `examples/*.ail`. It is exported
as `ailang_core::FORM_A_SPEC` and embedded verbatim in every as `ailang_core::FORM_A_SPEC` and embedded verbatim in every
`merge-prose` prompt. `merge-prose` prompt.
- `crates/ailang-core/tests/spec_drift.rs` walks every variant of - `crates/ailang-core/tests/spec_drift.rs` walks every variant of
@@ -615,7 +615,7 @@ so JSON-cohort feedback carries the full anyhow cause chain):
- `meta-llama/CodeLlama-13b-Instruct-hf` — - `meta-llama/CodeLlama-13b-Instruct-hf` —
`experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-9197fd/` `experiments/2026-05-12-cross-model-authoring/runs/2026-05-12-9197fd/`
| metric | Qwen / JSON | Qwen / AILX | CodeLlama / JSON | CodeLlama / AILX | | metric | Qwen / JSON | Qwen / AIL | CodeLlama / JSON | CodeLlama / AIL |
|---|---|---|---|---| |---|---|---|---|---|
| reached green | 1/4 | 1/4 | 0/4 | 2/4 | | reached green | 1/4 | 1/4 | 0/4 | 2/4 |
| first-attempt green | 1/4 | 1/4 | 0/4 | 2/4 | | first-attempt green | 1/4 | 1/4 | 0/4 | 2/4 |
@@ -625,16 +625,16 @@ so JSON-cohort feedback carries the full anyhow cause chain):
| top error class | check (×3) | parse (×3) | check (×2) | parse (×2) | | top error class | check (×3) | parse (×3) | check (×2) | parse (×2) |
Both subjects show the same direction on every metric the table Both subjects show the same direction on every metric the table
tracks. AILX is cheaper than JSON on prompt tokens (Qwen 61%, tracks. AIL is cheaper than JSON on prompt tokens (Qwen 61%,
CodeLlama 80% of the JSON cohort's spend) and cheaper on completion CodeLlama 80% of the JSON cohort's spend) and cheaper on completion
tokens (Qwen 23%, CodeLlama 82%). AILX reached-green is greater than tokens (Qwen 23%, CodeLlama 82%). AIL reached-green is greater than
or equal to JSON reached-green for each subject (Qwen ties at 1/4; or equal to JSON reached-green for each subject (Qwen ties at 1/4;
CodeLlama strictly dominates, 2/4 vs 0/4). The failure-class CodeLlama strictly dominates, 2/4 vs 0/4). The failure-class
symmetry observed on the original Qwen baseline holds for both symmetry observed on the original Qwen baseline holds for both
subjects: JSON cohorts fail at the typecheck stage, AILX cohorts at subjects: JSON cohorts fail at the typecheck stage, AIL cohorts at
the parse stage — each form's front-of-pipeline check. Three of the the parse stage — each form's front-of-pipeline check. Three of the
four first-attempt-green cells observed across the two subjects are four first-attempt-green cells observed across the two subjects are
AILX (Qwen-AILX t3, CodeLlama-AILX t1, CodeLlama-AILX t3); the AIL (Qwen-AIL t3, CodeLlama-AIL t1, CodeLlama-AIL t3); the
fourth is Qwen-JSON t3, the same task the original baseline already fourth is Qwen-JSON t3, the same task the original baseline already
flagged as the JSON-form's only first-attempt success. Two CodeLlama flagged as the JSON-form's only first-attempt success. Two CodeLlama
JSON-cohort cells (`t2_length`, `t4_count_zeros`) terminated as JSON-cohort cells (`t2_length`, `t4_count_zeros`) terminated as
@@ -642,7 +642,7 @@ JSON-cohort cells (`t2_length`, `t4_count_zeros`) terminated as
IONOS-side terminal error, not a model-output failure; CodeLlama IONOS-side terminal error, not a model-output failure; CodeLlama
JSON-cohort numbers therefore average over 2 informative cells, not JSON-cohort numbers therefore average over 2 informative cells, not
4. The Qwen re-run also shifted by one cell against the cma.3 4. The Qwen re-run also shifted by one cell against the cma.3
baseline (AILX 2/4 → 1/4, JSON 1/4 → 1/4) despite identical baseline (AIL 2/4 → 1/4, JSON 1/4 → 1/4) despite identical
temperature=0 inputs; this is IONOS-side state-of-day noise on a temperature=0 inputs; this is IONOS-side state-of-day noise on a
single deterministic re-run, not an effect of the ms.1 single deterministic re-run, not an effect of the ms.1
feedback-formatting fix (which only changes JSON-cohort prompt feedback-formatting fix (which only changes JSON-cohort prompt
@@ -650,10 +650,10 @@ content).
**Scope of this addendum:** two subjects, n=1 each, deterministic. **Scope of this addendum:** two subjects, n=1 each, deterministic.
This is a second data point pointing in the same direction as the This is a second data point pointing in the same direction as the
first, not a verdict. The universal claim of this Decision (".ailx first, not a verdict. The universal claim of this Decision (".ail
is the AI authoring projection") would need ≥3 subjects with is the AI authoring projection") would need ≥3 subjects with
statistical robustness to ratify; both current points point the statistical robustness to ratify; both current points point the
same way (AILX cohort cheaper and at-least-as-green) but neither same way (AIL cohort cheaper and at-least-as-green) but neither
the sample size nor the cross-call noise observed in the Qwen the sample size nor the cross-call noise observed in the Qwen
re-run is enough to close out the Decision. re-run is enough to close out the Decision.
@@ -1813,7 +1813,7 @@ would invoke literal-defaulting which axis-7 already excluded.
## Roundtrip Invariant ## Roundtrip Invariant
Every well-formed AILang module has both a canonical `.ail.json` Every well-formed AILang module has both a canonical `.ail.json`
representation and a textual `.ailx` representation, and the two representation and a textual `.ail` representation, and the two
are exact projections of the same AST. Concretely, both directions are exact projections of the same AST. Concretely, both directions
of the bijection hold: of the bijection hold:
@@ -1822,7 +1822,7 @@ of the bijection hold:
`canonical::to_bytes(load_module(J))`. The textual surface is `canonical::to_bytes(load_module(J))`. The textual surface is
the inverse of the loader composed with the printer; no the inverse of the loader composed with the printer; no
information is lost across the round-trip. information is lost across the round-trip.
2. **text → JSON → text.** For every well-formed `.ailx` text `t`, 2. **text → JSON → text.** For every well-formed `.ail` text `t`,
`parse(t)` is a complete AST, and re-printing `print(parse(t))` `parse(t)` is a complete AST, and re-printing `print(parse(t))`
produces a text that re-parses to the same AST (modulo produces a text that re-parses to the same AST (modulo
formatting). The parser is total over the well-formed surface formatting). The parser is total over the well-formed surface
@@ -1854,13 +1854,13 @@ inherit the gate automatically):
- `crates/ailang-surface/tests/round_trip.rs::print_then_parse_round_trips_every_fixture` - `crates/ailang-surface/tests/round_trip.rs::print_then_parse_round_trips_every_fixture`
— for every `.ail.json`, `print` then `parse` produces canonical- — for every `.ail.json`, `print` then `parse` produces canonical-
byte-equal output. Direction 1 above. byte-equal output. Direction 1 above.
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture` - `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
— for every `.ailx` text `t`, `parse(t)` and `parse(print(parse(t)))` — for every `.ail` text `t`, `parse(t)` and `parse(print(parse(t)))`
produce canonical-byte-equal AST. Direction 2 above. produce canonical-byte-equal AST. Direction 2 above.
- `crates/ailang-surface/tests/round_trip.rs::every_ailx_fixture_matches_its_json_counterpart` - `crates/ailang-surface/tests/round_trip.rs::every_ail_fixture_matches_its_json_counterpart`
— for every `.ailx` with a same-stem `.ail.json` counterpart, — for every `.ail` with a same-stem `.ail.json` counterpart,
`parse` of the text yields canonical bytes equal to the JSON `parse` of the text yields canonical bytes equal to the JSON
counterpart. Pins the hand-authored `.ailx` corpus against counterpart. Pins the hand-authored `.ail` corpus against
drift between the two forms at the fixture level. drift between the two forms at the fixture level.
- `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus` - `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus`
— every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`, — every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`,
@@ -1882,7 +1882,7 @@ never by relaxing the test.
### Why this is anchored at top level ### Why this is anchored at top level
The invariant is a property of the language identity, not of any The invariant is a property of the language identity, not of any
one surface-design Decision. Decision 6 introduces the `.ailx` one surface-design Decision. Decision 6 introduces the `.ail`
surface and lists round-trip-as-property as one of its surface and lists round-trip-as-property as one of its
constraints, but the property is load-bearing for every constraints, but the property is load-bearing for every
downstream concern that treats the two forms as exchangeable: downstream concern that treats the two forms as exchangeable:
@@ -2186,7 +2186,7 @@ ail check <module.ail.json> — loads, validates, typechecks
ail manifest <module.ail.json> — table: name :: type !effects [hash] ail manifest <module.ail.json> — table: name :: type !effects [hash]
ail describe <module> <name> — detail of a definition (form-A body) ail describe <module> <name> — detail of a definition (form-A body)
ail render <module.ail.json> — JSON-AST → form-A text (exact inverse of `parse`) ail render <module.ail.json> — JSON-AST → form-A text (exact inverse of `parse`)
ail parse <module.ailx> — form-A text → canonical JSON-AST ail parse <module.ail> — form-A text → canonical JSON-AST
ail prose <module.ail.json> — JSON-AST → form-B (lossy human prose, no parser) ail prose <module.ail.json> — JSON-AST → form-B (lossy human prose, no parser)
ail merge-prose <m.ail.json> <m.prose.txt> ail merge-prose <m.ail.json> <m.prose.txt>
— compose the LLM-mediator prompt for the prose round-trip — compose the LLM-mediator prompt for the prose round-trip
@@ -2403,7 +2403,7 @@ What **is** supported (and used as the smoke test for the pipeline):
`(term-ctor std_pair.Pair MkPair x y)` and `(pat-ctor MkPair x y)` `(term-ctor std_pair.Pair MkPair x y)` and `(pat-ctor MkPair x y)`
inside that scrutinee. Std-library demos (`examples/std_*_demo.ail.json`) inside that scrutinee. Std-library demos (`examples/std_*_demo.ail.json`)
exercise this end-to-end. exercise this end-to-end.
- **AI-authoring text surface, form (A)** (Decision 6). The `ailang-surface` crate parses `.ailx` form-A - **AI-authoring text surface, form (A)** (Decision 6). The `ailang-surface` crate parses `.ail` form-A
text into a canonical `ailang-core::ast::Module` and prints any module text into a canonical `ailang-core::ast::Module` and prints any module
back as form-A text. `ail render` and `ail describe` use it as the back as form-A text. `ail render` and `ail describe` use it as the
sole text projection; `ail parse` is the inverse direction. Round-trip sole text projection; `ail parse` is the inverse direction. Round-trip
+5 -5
View File
@@ -19,14 +19,14 @@ modes to watch for.
## What the LLM produces ## What the LLM produces
Iter 20f revised a load-bearing piece of this cycle: the LLM emits Iter 20f revised a load-bearing piece of this cycle: the LLM emits
**Form-A** (an `.ailx` document — the canonical authoring surface **Form-A** (an `.ail` document — the canonical authoring surface
fixed by Decision 6), not JSON-AST. JSON-AST is the canonical fixed by Decision 6), not JSON-AST. JSON-AST is the canonical
hashable artefact, but it is not a writing surface — every type hashable artefact, but it is not a writing surface — every type
reference wraps in `{"k": "con", ...}`, every term in reference wraps in `{"k": "con", ...}`, every term in
`{"t": "...", ...}`, and a single missing `param_modes` entry is a `{"t": "...", ...}`, and a single missing `param_modes` entry is a
schema error rather than a parse error with a position. schema error rather than a parse error with a position.
Form-A is the form `examples/*.ailx` are written in, the form Form-A is the form `examples/*.ail` are written in, the form
`ail render` produces, and the form `ail parse` consumes. The full `ail render` produces, and the form `ail parse` consumes. The full
LLM-targeted specification is shipped inside the binary as LLM-targeted specification is shipped inside the binary as
`ailang_core::FORM_A_SPEC` (sourced from `ailang_core::FORM_A_SPEC` (sourced from
@@ -39,8 +39,8 @@ LLM-targeted specification is shipped inside the binary as
1. ail prose foo.ail.json > foo.prose.txt 1. ail prose foo.ail.json > foo.prose.txt
2. $EDITOR foo.prose.txt # human edits freely 2. $EDITOR foo.prose.txt # human edits freely
3. ail merge-prose foo.ail.json foo.prose.txt > prompt.txt 3. ail merge-prose foo.ail.json foo.prose.txt > prompt.txt
4. cat prompt.txt | <your-llm-cli> > foo.new.ailx 4. cat prompt.txt | <your-llm-cli> > foo.new.ail
5. ail parse foo.new.ailx > foo.new.ail.json 5. ail parse foo.new.ail > foo.new.ail.json
6. ail check foo.new.ail.json 6. ail check foo.new.ail.json
7. mv foo.new.ail.json foo.ail.json # if check is clean 7. mv foo.new.ail.json foo.ail.json # if check is clean
``` ```
@@ -74,7 +74,7 @@ CLI helper.
You are integrating prose edits back into an AILang module. You are integrating prose edits back into an AILang module.
ROLE ROLE
Your job is to produce an updated AILang module in Form-A (an .ailx 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 file) that reflects the human's prose edits while preserving the
load-bearing semantic detail from the original module. load-bearing semantic detail from the original module.
+107
View File
@@ -0,0 +1,107 @@
# iter ext-rename — `.ailx` extension renamed to `.ail` across the live toolchain
**Date:** 2026-05-12
**Started from:** 17b370b (post-audit-ms close, working tree clean)
**Status:** DONE
**Tasks completed:** 1 of 1
## Summary
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 that
`ail check` / `build` / `run` actually loads today.
The earlier `.ailx` extension carried an unmotivated `x`. The
cross-model authoring-form test (cma + ms milestones, closed
earlier today) treated `.ailx` purely as a cohort identifier;
once the empirical case for keeping a separate authoring surface
had been ratified (DESIGN.md §Decision-6 addendum), the awkward
extension was the last asymmetry between Form A and Form B.
User-initiated rename.
## Scope
In scope (touched):
- 61 example files `examples/**/*.ailx → .ail` (57 top-level +
4 `examples/fieldtest/`) renamed via `git mv`.
- `experiments/.../rendered/ailx.md → ail.md` rename.
- All live toolchain prose and code: `crates/ail/`,
`crates/ailang-surface/`, `crates/ailang-core/specs/form_a.md`,
`docs/DESIGN.md`, `docs/roadmap.md`, `docs/PROSE_ROUNDTRIP.md`,
`bench/reference/*.c`, all of `skills/`, the Cross-Model
Authoring experiment crates under
`experiments/.../{render,harness,master,rendered,README.md}`.
- Cohort identifier `ailx → ail` in the experiment crate (Rust
`Cohort::Ailx → Cohort::Ail`, `Form::Ailx → Form::Ail`,
per-cohort path component, `{form-only: ailx}` marker,
```ailx codefence language tag).
Out of scope (untouched, by design):
- `docs/journal-archive.md` (content-frozen per CLAUDE.md).
- All `docs/journals/`, `docs/specs/`, `docs/plans/`,
`bench/orchestrator-stats/` — historical records describing
states as they were.
- `experiments/.../runs/` — frozen LLM-output artefacts; the
models in those runs actually saw `.ailx`, and renaming the
artefacts would falsify the experimental record.
The exclusion preserves the journals as honest history. Anyone
chasing why a 2026-05-11 fieldtest names `.ailx` files will find
that the renaming happened today and that the names were correct
at the time of writing.
## Why boss-direct edit (no planner / implement dispatch)
Same rationale as iter rt.2: the user fully specified the
transformation (rename `.ailx → .ail`, rename cohort
`ailx → ail`, historical records untouched), down to the three
open questions that were resolved up-front via AskUserQuestion
(cohort name; treatment of frozen runs; treatment of historical
docs). The execution was two `git mv` passes plus one
heavily-scoped `sed` over an explicit 41-file whitelist (plus
two follow-up files surfaced by the negative-grep). No design
judgement deferred to execution time.
## Files touched (working-tree counts)
- 61 renames under `examples/`.
- 1 rename `experiments/.../rendered/ailx.md → ail.md`.
- 35 content-edited files: the 41-file SCOPE whitelist minus
the experiment-crate files that had no `ailx` content, plus
two follow-up files surfaced by the negative-grep
(`examples/bench_list_sum_explicit.ail`'s in-source comment;
`experiments/.../rendered/json.md` for three `AILX` acronyms
in prose).
## Verification
- `cargo build --workspace` clean.
- `cargo test --workspace` green; the renamed identifiers
`every_ail_fixture_matches_its_json_counterpart` and
`parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
in `crates/ailang-surface/tests/round_trip.rs` were run
explicitly and pass.
- Experiment crate `cargo test` (out-of-workspace) green.
- `bench/check.py`, `bench/compile_check.py`, `bench/cross_lang.py`:
0 regressed across all three; 6 improved-beyond-tolerance on
`latency.explicit_at_rc` (same cluster the previous two audits
today already observed; decoupled from this rename — baseline
left pristine).
- Negative grep: `git grep -nIi 'ailx|Ailx|AILX'` outside the
out-of-scope paths returns no matches.
## Roadmap implications
The `.ail` extension is now canonical authoring surface but the
CLI does not yet accept it — `ail check foo.ail` still produces
the misleading JSON-parse error. The existing P2 todo
"`ail check`/`build`/`run` accept `.ail` extension" (roadmap.md
line 185) is the natural next iteration: it closes the loop
opened by today's rename and removes the largest first-command
friction for any LLM-author writing in Form A. Promoting that
todo to the immediate next dispatch.
+1
View File
@@ -28,3 +28,4 @@
- 2026-05-12 — iter ms.1: pipeline anyhow-chain preservation — `{e}``{e:#}` at `pipeline.rs:118` restores `serde_json` parse-error leaf in JSON-cohort feedback; pinning unit test added (14/14 green) → 2026-05-12-iter-ms.1.md - 2026-05-12 — iter ms.1: pipeline anyhow-chain preservation — `{e}``{e:#}` at `pipeline.rs:118` restores `serde_json` parse-error leaf in JSON-cohort feedback; pinning unit test added (14/14 green) → 2026-05-12-iter-ms.1.md
- 2026-05-12 — iter ms.2: Qwen3-Coder-Next retroactive re-run + first CodeLlama-13b-Instruct run via IONOS; DESIGN.md §Decision-6 addendum extended from 2-col single-subject to 4-col two-subject; both subjects agree on direction (AILX cheaper + ≥ green); roadmap P3 multi-subject entry removed → 2026-05-12-iter-ms.2.md - 2026-05-12 — iter ms.2: Qwen3-Coder-Next retroactive re-run + first CodeLlama-13b-Instruct run via IONOS; DESIGN.md §Decision-6 addendum extended from 2-col single-subject to 4-col two-subject; both subjects agree on direction (AILX cheaper + ≥ green); roadmap P3 multi-subject entry removed → 2026-05-12-iter-ms.2.md
- 2026-05-12 — audit-ms: milestone close (Multi-subject Authoring-Form Test — CodeLlama Replication) — architect clean, bench all-green (same 5-metric latency.explicit_at_rc improvement cluster as audit-cma earlier today, baseline left pristine for second consecutive audit) → 2026-05-12-audit-ms.md - 2026-05-12 — audit-ms: milestone close (Multi-subject Authoring-Form Test — CodeLlama Replication) — architect clean, bench all-green (same 5-metric latency.explicit_at_rc improvement cluster as audit-cma earlier today, baseline left pristine for second consecutive audit) → 2026-05-12-audit-ms.md
- 2026-05-12 — iter ext-rename: `.ailx``.ail` extension rename across the live toolchain (61 example renames + 35 content edits + experiment-crate cohort rename `ailx → ail`); historical docs (journals, archive, specs, plans, frozen experiment runs) deliberately untouched; cargo+bench all-green; opens follow-up `.ail`-CLI-acceptance iter → 2026-05-12-iter-ext-rename.md
+9 -9
View File
@@ -171,7 +171,7 @@ context. Pick the next milestone from P1.)_
type name). In the known-owner branch, list the owner's available type name). In the known-owner branch, list the owner's available
type defs as candidates the way `bare-cross-module-type-ref` type defs as candidates the way `bare-cross-module-type-ref`
lists candidates from imports. lists candidates from imports.
- context: fieldtest 2026-05-11 — `examples/ct_3*.ailx` exhibits both branches with identical-shape diagnostics. - context: fieldtest 2026-05-11 — `examples/ct_3*.ail` exhibits both branches with identical-shape diagnostics.
- [ ] **\[todo\]** Workspace search beyond entry-module's directory — - [ ] **\[todo\]** Workspace search beyond entry-module's directory —
`load_workspace` only finds sibling `.ail.json` files in the same `load_workspace` only finds sibling `.ail.json` files in the same
directory as the entry module, so any consumer of prelude/std in a directory as the entry module, so any consumer of prelude/std in a
@@ -182,12 +182,12 @@ context. Pick the next milestone from P1.)_
- context: fieldtest 2026-05-11 — fieldtest fixtures could not be - context: fieldtest 2026-05-11 — fieldtest fixtures could not be
placed under `examples/fieldtest/` because of this; predates the placed under `examples/fieldtest/` because of this; predates the
canonical-type-names milestone but surfaces every time. canonical-type-names milestone but surfaces every time.
- [ ] **\[todo\]** `ail check`/`build`/`run` accept `.ailx` extension — - [ ] **\[todo\]** `ail check`/`build`/`run` accept `.ail` extension —
today `ail check foo.ailx` produces a misleading JSON-parse error today `ail check foo.ail` produces a misleading JSON-parse error
(`json: expected value at line 1 column 1`) because the loader only (`json: expected value at line 1 column 1`) because the loader only
accepts `.ail.json`. Either teach the CLI subcommands to auto-parse accepts `.ail.json`. Either teach the CLI subcommands to auto-parse
`.ailx` internally, or detect the extension and emit a `.ail` internally, or detect the extension and emit a
"did you mean `ail parse foo.ailx`?" hint. The LLM-author's natural "did you mean `ail parse foo.ail`?" hint. The LLM-author's natural
first command should not be the one that produces a misleading first command should not be the one that produces a misleading
diagnostic. diagnostic.
- context: fieldtest 2026-05-11 — orthogonal to canonical-type-names - context: fieldtest 2026-05-11 — orthogonal to canonical-type-names
@@ -205,13 +205,13 @@ context. Pick the next milestone from P1.)_
## P3 — Ideas ## P3 — Ideas
- [ ] **\[todo\]** `compare_primitives_smoke.ailx` counterpart — - [ ] **\[todo\]** `compare_primitives_smoke.ail` counterpart —
the canonical happy-path exhibit for canonical-type-names ships the canonical happy-path exhibit for canonical-type-names ships
only as JSON. Per Decision 6, Surface is the LLM-author surface; only as JSON. Per Decision 6, Surface is the LLM-author surface;
the milestone should have a `.ailx` counterpart. Two paths: author the milestone should have a `.ail` counterpart. Two paths: author
`examples/compare_primitives_smoke.ailx` to round-trip into the `examples/compare_primitives_smoke.ail` to round-trip into the
existing JSON, OR retire the JSON fixture in favour of existing JSON, OR retire the JSON fixture in favour of
`examples/ct_1_ordering_signum.{ailx,ail.json}` as the new canonical `examples/ct_1_ordering_signum.{ail,ail.json}` as the new canonical
happy-path exhibit. happy-path exhibit.
- context: fieldtest 2026-05-11 (spec_gap). - context: fieldtest 2026-05-11 (spec_gap).
- [ ] **\[todo\]** Codegen `lookup_ctor_in_pattern` type-anchoring — - [ ] **\[todo\]** Codegen `lookup_ctor_in_pattern` type-anchoring —
@@ -1,6 +1,6 @@
; Bench fixture: explicit-mode pair of bench_list_sum. ; Bench fixture: explicit-mode pair of bench_list_sum.
; ;
; Same algorithm and same sizes as bench_list_sum.ailx (build a list of ; Same algorithm and same sizes as bench_list_sum.ail (build a list of
; 0..N-1, sum it, print) but with full `(borrow)` / `(own)` annotations ; 0..N-1, sum it, print) but with full `(borrow)` / `(own)` annotations
; on every fn-param signature in the hot path. Under --alloc=rc the ; on every fn-param signature in the hot path. Under --alloc=rc the
; codegen now emits `inc`/`dec` instructions: each cell allocated by ; codegen now emits `inc`/`dec` instructions: each cell allocated by
@@ -1,6 +1,6 @@
# Cross-model authoring-form test # Cross-model authoring-form test
Empirical measurement of whether `.ail.json` or `.ailx` is the form a Empirical measurement of whether `.ail.json` or `.ail` is the form a
foreign LLM author reaches for and succeeds with. Single subject for foreign LLM author reaches for and succeeds with. Single subject for
v1: Qwen3-Coder-Next via IONOS. Two blind cohorts; same four tasks. v1: Qwen3-Coder-Next via IONOS. Two blind cohorts; same four tasks.
@@ -16,7 +16,7 @@ Parent spec: `docs/specs/2026-05-12-cross-model-authoring-form-test.md`.
harness. **Authored in cma.2**, not cma.1. harness. **Authored in cma.2**, not cma.1.
- `render/` — standalone Cargo crate, outside the root workspace, - `render/` — standalone Cargo crate, outside the root workspace,
builds the renderer binary. builds the renderer binary.
- `rendered/json.md`, `rendered/ailx.md` — projected mini-specs, - `rendered/json.md`, `rendered/ail.md` — projected mini-specs,
checked into the repo for review. checked into the repo for review.
- `harness/`**Authored in cma.2**. - `harness/`**Authored in cma.2**.
- `runs/<date>-<hash>/` — populated by `harness` during a live run. - `runs/<date>-<hash>/` — populated by `harness` during a live run.
@@ -36,7 +36,7 @@ cargo test --manifest-path experiments/2026-05-12-cross-model-authoring/render/C
``` ```
Three integration tests: `example_roundtrip` (each example loads, Three integration tests: `example_roundtrip` (each example loads,
prints to AILX, reparses to the same canonical bytes), `spec_completeness` prints to AIL, reparses to the same canonical bytes), `spec_completeness`
(every AST variant in `ailang_core::ast` is exercised by at least (every AST variant in `ailang_core::ast` is exercised by at least
one example), `token_balance` (form-only blocks balanced within ±5% one example), `token_balance` (form-only blocks balanced within ±5%
across the two rendered files). across the two rendered files).
@@ -42,8 +42,8 @@ fn main() -> Result<()> {
let rendered_json = std::fs::read_to_string(args.rendered.join("json.md")) let rendered_json = std::fs::read_to_string(args.rendered.join("json.md"))
.with_context(|| format!("reading {}", args.rendered.join("json.md").display()))?; .with_context(|| format!("reading {}", args.rendered.join("json.md").display()))?;
let rendered_ailx = std::fs::read_to_string(args.rendered.join("ailx.md")) let rendered_ail = std::fs::read_to_string(args.rendered.join("ail.md"))
.with_context(|| format!("reading {}", args.rendered.join("ailx.md").display()))?; .with_context(|| format!("reading {}", args.rendered.join("ail.md").display()))?;
let tasks = tasks::load_all(&args.tasks)?; let tasks = tasks::load_all(&args.tasks)?;
if tasks.is_empty() { bail!("no tasks found in {}", args.tasks.display()); } if tasks.is_empty() { bail!("no tasks found in {}", args.tasks.display()); }
@@ -52,8 +52,8 @@ fn main() -> Result<()> {
let mut run_status = "ok"; let mut run_status = "ok";
let mut completed: std::collections::BTreeSet<(&'static str, String)> = Default::default(); let mut completed: std::collections::BTreeSet<(&'static str, String)> = Default::default();
'outer: for cohort in [Cohort::Json, Cohort::Ailx] { 'outer: for cohort in [Cohort::Json, Cohort::Ail] {
let system_prompt = match cohort { Cohort::Json => &rendered_json, Cohort::Ailx => &rendered_ailx }; let system_prompt = match cohort { Cohort::Json => &rendered_json, Cohort::Ail => &rendered_ail };
for t in &tasks { for t in &tasks {
let (row, consumed) = run_one( let (row, consumed) = run_one(
&backend, &args, cohort, system_prompt, t, &backend, &args, cohort, system_prompt, t,
@@ -75,7 +75,7 @@ fn main() -> Result<()> {
// walking every (cohort, task) pair. The pairs we never reached // walking every (cohort, task) pair. The pairs we never reached
// still need a row, with final_status = budget_abort. // still need a row, with final_status = budget_abort.
if run_status == "budget_exceeded" { if run_status == "budget_exceeded" {
for cohort in [Cohort::Json, Cohort::Ailx] { for cohort in [Cohort::Json, Cohort::Ail] {
for t in &tasks { for t in &tasks {
if !completed.contains(&(cohort.as_str(), t.id.clone())) { if !completed.contains(&(cohort.as_str(), t.id.clone())) {
rows.push(ScoreRow { rows.push(ScoreRow {
@@ -7,7 +7,7 @@
//! "t1_add_three": { "1": { "content": "...", "usage": {...} } }, //! "t1_add_three": { "1": { "content": "...", "usage": {...} } },
//! "t2_length": { "1": {...}, "2": {...} } //! "t2_length": { "1": {...}, "2": {...} }
//! }, //! },
//! "ailx": { ... } //! "ail": { ... }
//! } //! }
//! ``` //! ```
//! Where each `"<turn>"` entry has `"content"` (the program the //! Where each `"<turn>"` entry has `"content"` (the program the
@@ -12,20 +12,20 @@ use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Cohort { pub enum Cohort {
Json, Json,
Ailx, Ail,
} }
impl Cohort { impl Cohort {
pub fn extension(self) -> &'static str { pub fn extension(self) -> &'static str {
match self { match self {
Cohort::Json => "ail.json", Cohort::Json => "ail.json",
Cohort::Ailx => "ailx", Cohort::Ail => "ail",
} }
} }
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {
Cohort::Json => "json", Cohort::Json => "json",
Cohort::Ailx => "ailx", Cohort::Ail => "ail",
} }
} }
} }
@@ -79,8 +79,8 @@ pub fn run_pipeline(
let prog_path = workdir.join(format!("prog.{}", cohort.extension())); let prog_path = workdir.join(format!("prog.{}", cohort.extension()));
std::fs::write(&prog_path, program).with_context(|| format!("writing {}", prog_path.display()))?; std::fs::write(&prog_path, program).with_context(|| format!("writing {}", prog_path.display()))?;
// For AILX cohort, parse to JSON first. // For AIL cohort, parse to JSON first.
let json_path_initial = if matches!(cohort, Cohort::Ailx) { let json_path_initial = if matches!(cohort, Cohort::Ail) {
let out = workdir.join("prog.ail.json"); let out = workdir.join("prog.ail.json");
let parse_out = Command::new(ail_bin()) let parse_out = Command::new(ail_bin())
.arg("parse").arg(&prog_path) .arg("parse").arg(&prog_path)
@@ -68,7 +68,7 @@ pub fn write_summary_md(rows: &[ScoreRow], path: &Path) -> Result<()> {
let mut f = std::fs::File::create(path) let mut f = std::fs::File::create(path)
.with_context(|| format!("creating {}", path.display()))?; .with_context(|| format!("creating {}", path.display()))?;
writeln!(f, "# Cross-model authoring-form test — run summary\n")?; writeln!(f, "# Cross-model authoring-form test — run summary\n")?;
for cohort in ["json", "ailx"] { for cohort in ["json", "ail"] {
writeln!(f, "## Cohort: {cohort}\n")?; writeln!(f, "## Cohort: {cohort}\n")?;
let cohort_rows: Vec<&ScoreRow> = rows.iter().filter(|r| r.cohort == cohort).collect(); let cohort_rows: Vec<&ScoreRow> = rows.iter().filter(|r| r.cohort == cohort).collect();
if cohort_rows.is_empty() { if cohort_rows.is_empty() {
@@ -4,7 +4,7 @@
//! The intent (parent spec §strip_locations) is **symmetric //! The intent (parent spec §strip_locations) is **symmetric
//! degradation**: both cohorts lose the localisation information //! degradation**: both cohorts lose the localisation information
//! their compiler natively produces. The JSON cohort would //! their compiler natively produces. The JSON cohort would
//! otherwise get JSON-pointer fragments; the AILX cohort would get //! otherwise get JSON-pointer fragments; the AIL cohort would get
//! `at byte N` offsets. Neither survives this pass. //! `at byte N` offsets. Neither survives this pass.
use regex::Regex; use regex::Regex;
@@ -49,7 +49,7 @@
} }
} }
}, },
"ailx": { "ail": {
"t1_add_three": { "t1_add_three": {
"1": { "1": {
"content": "(module garbage", "content": "(module garbage",
@@ -1,7 +1,7 @@
//! End-to-end mock-mode test (parent spec §Testing strategy). //! End-to-end mock-mode test (parent spec §Testing strategy).
//! Runs the harness binary against a canned response file that //! Runs the harness binary against a canned response file that
//! makes (json, t3_main_prints) green on turn 2 and //! makes (json, t3_main_prints) green on turn 2 and
//! (ailx, t1_add_three) run to the turn limit. Asserts the //! (ail, t1_add_three) run to the turn limit. Asserts the
//! scores.csv is well-formed and the right per-(cohort,task) //! scores.csv is well-formed and the right per-(cohort,task)
//! artefacts land on disk. //! artefacts land on disk.
@@ -35,10 +35,10 @@ fn mock_full_run_produces_scores_and_artefacts() {
assert!(scores.starts_with("cohort,task_id,first_attempt_green,")); assert!(scores.starts_with("cohort,task_id,first_attempt_green,"));
assert_eq!(scores.lines().count(), 1 + 8, "header + 8 rows expected"); assert_eq!(scores.lines().count(), 1 + 8, "header + 8 rows expected");
assert!(scores.contains("json,t3_main_prints,false,2")); assert!(scores.contains("json,t3_main_prints,false,2"));
assert!(scores.contains("ailx,t1_add_three,false,INF")); assert!(scores.contains("ail,t1_add_three,false,INF"));
// Spot-check one per-cohort artefact tree. // Spot-check one per-cohort artefact tree.
let pc = run_dir.join("per_cohort").join("ailx").join("t1_add_three"); let pc = run_dir.join("per_cohort").join("ail").join("t1_add_three");
assert!(pc.join("turn_1_program.ailx").exists()); assert!(pc.join("turn_1_program.ail").exists());
assert!(pc.join("turn_5_program.ailx").exists()); assert!(pc.join("turn_5_program.ail").exists());
} }
@@ -46,8 +46,8 @@ pattern object carries a `p` discriminator. Every literal object
carries a `kind` discriminator. carries a `kind` discriminator.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
The AILX surface is parenthesised tagged-head Lisp-style notation. The AIL surface is parenthesised tagged-head Lisp-style notation.
Every form begins with `(` followed by a tag identifier; positional Every form begins with `(` followed by a tag identifier; positional
arguments follow; the form closes with `)`. There are no infix arguments follow; the form closes with `)`. There are no infix
operators, no operator precedence, no implicit conversions. operators, no operator precedence, no implicit conversions.
@@ -62,9 +62,9 @@ digit. String literals are double-quoted with `\"`, `\\`, `\n`, `\t`
escapes; integer literals are signed decimal; the `(unit)` keyword escapes; integer literals are signed decimal; the `(unit)` keyword
denotes the unit value. denotes the unit value.
The AILX form is the printed dual of the JSON form: `ail parse The AIL form is the printed dual of the JSON form: `ail parse
file.ailx` produces the same canonical bytes as the JSON form would, file.ail` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AILX text. Round-trip and `ail render file.ail.json` produces the AIL text. Round-trip
through this pair is gated by an integration test. through this pair is gated by an integration test.
{/form-only} {/form-only}
@@ -110,7 +110,7 @@ Type::Forall: `{"k": "forall", "vars": [...], "body": ...}` with an
optional `constraints` array for class constraints (see section 10). optional `constraints` array for class constraints (see section 10).
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Type::Con: `Int`, `Bool`, `Str`, `Float`, `Unit`, or a user type Type::Con: `Int`, `Bool`, `Str`, `Float`, `Unit`, or a user type
name. For parameterised types: `(con List Int)` reads "List of Int"; name. For parameterised types: `(con List Int)` reads "List of Int";
nested: `(con Map (con Str) (con Int))`. nested: `(con Map (con Str) (con Int))`.
@@ -168,7 +168,7 @@ A fn that consumes a list and returns a new list:
`"param_modes": ["own"]`, `"ret_mode": "own"`. `"param_modes": ["own"]`, `"ret_mode": "own"`.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Modes are surface wrappers around the type in parameter and return Modes are surface wrappers around the type in parameter and return
position. Inside a `(fn-type ...)`: position. Inside a `(fn-type ...)`:
@@ -235,7 +235,7 @@ use snake_case. This is historical and the canonical bytes preserve
the difference. the difference.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Function definition: `(fn NAME (doc STRING)? (type TYPE) (params Function definition: `(fn NAME (doc STRING)? (type TYPE) (params
NAME*) (body TERM))`. The `doc` clause is optional but recommended. NAME*) (body TERM))`. The `doc` clause is optional but recommended.
@@ -295,7 +295,7 @@ constructor's declared `fields` in order; nullary constructors
carry `"args": []`. carry `"args": []`.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Type definition: `(data NAME (vars TYVAR*)? (doc STRING)? (ctor Type definition: `(data NAME (vars TYVAR*)? (doc STRING)? (ctor
CTOR-NAME ARG-TYPE*)*)`. The `vars` clause is optional; nullary CTOR-NAME ARG-TYPE*)*)`. The `vars` clause is optional; nullary
constructors have an empty argument list. constructors have an empty argument list.
@@ -349,7 +349,7 @@ Pattern variables are linear: each name appears at most once in a
single pattern. Repeating a name is a typecheck error. single pattern. Repeating a name is a typecheck error.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Match expression: `(match SCRUT (case PAT BODY) (case PAT BODY) Match expression: `(match SCRUT (case PAT BODY) (case PAT BODY)
...)`. Arms are introduced by `(case ...)`; the order matters (top- ...)`. Arms are introduced by `(case ...)`; the order matters (top-
to-bottom). to-bottom).
@@ -409,7 +409,7 @@ The value of the `seq` is the value of `rhs`; `lhs`'s value is
discarded but its effects count toward the enclosing fn's row. discarded but its effects count toward the enclosing fn's row.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Effect row on a fn type: `(effects E1 E2 ...)` inside the Effect row on a fn type: `(effects E1 E2 ...)` inside the
`(fn-type ...)`. Empty row: omit the `(effects)` clause or write `(fn-type ...)`. Empty row: omit the `(effects)` clause or write
`(effects)`. Common effectful row: `(effects IO)`. Order is sorted `(effects)`. Common effectful row: `(effects IO)`. Order is sorted
@@ -435,10 +435,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non- authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON. type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and encoding by hand; the toolchain owns that representation. NaN and
@@ -464,11 +464,11 @@ Literal variants and their JSON shapes:
serialisation drift across `serde_json` versions. serialisation drift across `serde_json` versions.
Float bit patterns are computed by the toolchain on `ail parse`, Float bit patterns are computed by the toolchain on `ail parse`,
not by the author. When you write `3.14` in AILX, the parser emits not by the author. When you write `3.14` in AIL, the parser emits
`"bits": "40091eb851eb851f"` in the JSON form. `"bits": "40091eb851eb851f"` in the JSON form.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Literal surface forms: Literal surface forms:
- Int: bare signed decimal, e.g. `42`, `-7`, `0`. - Int: bare signed decimal, e.g. `42`, `-7`, `0`.
@@ -533,7 +533,7 @@ Constraint on a polymorphic fn: inside `Type::Forall`, the
constraints; omitted from canonical bytes when empty. constraints; omitted from canonical bytes when empty.
{/form-only} {/form-only}
{form-only: ailx} {form-only: ail}
Class declaration: `(class NAME (param TYVAR) (method NAME (type Class declaration: `(class NAME (param TYVAR) (method NAME (type
METHOD-TYPE) (default BODY)?)*)`. The `default` clause is METHOD-TYPE) (default BODY)?)*)`. The `default` clause is
optional; methods without a default must be supplied by every optional; methods without a default must be supplied by every
@@ -2,8 +2,8 @@
//! //!
//! - JSON form: read the on-disk canonical bytes verbatim, emit a //! - JSON form: read the on-disk canonical bytes verbatim, emit a
//! ```json``` fenced block. //! ```json``` fenced block.
//! - AILX form: load the example via ailang_core::load_module, print //! - AIL form: load the example via ailang_core::load_module, print
//! via ailang_surface::print, emit a ```ailx``` fenced block. //! via ailang_surface::print, emit a ```ail``` fenced block.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use std::path::Path; use std::path::Path;
@@ -11,7 +11,7 @@ use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Form { pub enum Form {
Json, Json,
Ailx, Ail,
} }
pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<String> { pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<String> {
@@ -28,11 +28,11 @@ pub fn render_example(examples_dir: &Path, id: &str, form: Form) -> Result<Strin
.with_context(|| format!("reading {}", path.display()))?; .with_context(|| format!("reading {}", path.display()))?;
Ok(format!("```json\n{}\n```\n", bytes.trim_end())) Ok(format!("```json\n{}\n```\n", bytes.trim_end()))
} }
Form::Ailx => { Form::Ail => {
let module = ailang_core::load_module(&path) let module = ailang_core::load_module(&path)
.with_context(|| format!("load_module on {}", path.display()))?; .with_context(|| format!("load_module on {}", path.display()))?;
let text = ailang_surface::print(&module); let text = ailang_surface::print(&module);
Ok(format!("```ailx\n{}\n```\n", text.trim_end())) Ok(format!("```ail\n{}\n```\n", text.trim_end()))
} }
} }
} }
@@ -1,4 +1,4 @@
//! xmodel-render — projects master/spec.md into rendered/json.md and rendered/ailx.md. //! xmodel-render — projects master/spec.md into rendered/json.md and rendered/ail.md.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -15,17 +15,17 @@ fn main() -> Result<()> {
let segments = splitter::split(&spec_text); let segments = splitter::split(&spec_text);
let json_out = render_for(&segments, &examples_dir, examples::Form::Json, splitter::Form::Json)?; let json_out = render_for(&segments, &examples_dir, examples::Form::Json, splitter::Form::Json)?;
let ailx_out = render_for(&segments, &examples_dir, examples::Form::Ailx, splitter::Form::Ailx)?; let ail_out = render_for(&segments, &examples_dir, examples::Form::Ail, splitter::Form::Ail)?;
std::fs::create_dir_all(&rendered_dir) std::fs::create_dir_all(&rendered_dir)
.with_context(|| format!("creating {}", rendered_dir.display()))?; .with_context(|| format!("creating {}", rendered_dir.display()))?;
let json_path = rendered_dir.join("json.md"); let json_path = rendered_dir.join("json.md");
let ailx_path = rendered_dir.join("ailx.md"); let ail_path = rendered_dir.join("ail.md");
std::fs::write(&json_path, json_out) std::fs::write(&json_path, json_out)
.with_context(|| format!("writing {}", json_path.display()))?; .with_context(|| format!("writing {}", json_path.display()))?;
std::fs::write(&ailx_path, ailx_out) std::fs::write(&ail_path, ail_out)
.with_context(|| format!("writing {}", ailx_path.display()))?; .with_context(|| format!("writing {}", ail_path.display()))?;
eprintln!("xmodel-render: wrote {} and {}", json_path.display(), ailx_path.display()); eprintln!("xmodel-render: wrote {} and {}", json_path.display(), ail_path.display());
Ok(()) Ok(())
} }
@@ -2,20 +2,20 @@
//! //!
//! Recognises three directives, one per line, no nesting: //! Recognises three directives, one per line, no nesting:
//! {form-only: json} … {/form-only} //! {form-only: json} … {/form-only}
//! {form-only: ailx} … {/form-only} //! {form-only: ail} … {/form-only}
//! {example: <id>} //! {example: <id>}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Form { pub enum Form {
Json, Json,
Ailx, Ail,
} }
impl Form { impl Form {
fn parse(name: &str) -> Option<Form> { fn parse(name: &str) -> Option<Form> {
match name { match name {
"json" => Some(Form::Json), "json" => Some(Form::Json),
"ailx" => Some(Form::Ailx), "ail" => Some(Form::Ail),
_ => None, _ => None,
} }
} }
@@ -39,7 +39,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
let bytes_parsed = ailang_core::canonical::to_bytes(&parsed); let bytes_parsed = ailang_core::canonical::to_bytes(&parsed);
if bytes_original != bytes_parsed { if bytes_original != bytes_parsed {
return Err(format!( return Err(format!(
"{}: roundtrip diverged.\n--- printed AILX ---\n{}\n--- original bytes len {} vs parsed bytes len {} ---", "{}: roundtrip diverged.\n--- printed AIL ---\n{}\n--- original bytes len {} vs parsed bytes len {} ---",
path.display(), path.display(),
text, text,
bytes_original.len(), bytes_original.len(),
@@ -50,7 +50,7 @@ fn round_trip_one(path: &Path) -> Result<(), String> {
} }
#[test] #[test]
fn every_example_roundtrips_via_ailx() { fn every_example_roundtrips_via_ail() {
let examples = list_examples(); let examples = list_examples();
assert!( assert!(
!examples.is_empty(), !examples.is_empty(),
@@ -22,13 +22,13 @@ fn form_only_block_is_isolated() {
} }
#[test] #[test]
fn ailx_form_only_block_uses_ailx_form() { fn ail_form_only_block_uses_ail_form() {
let input = "{form-only: ailx}\ngrammar rule\n{/form-only}\n"; let input = "{form-only: ail}\ngrammar rule\n{/form-only}\n";
let segments = split(input); let segments = split(input);
assert_eq!( assert_eq!(
segments, segments,
vec![ vec![
Segment::FormOnly { form: Form::Ailx, body: "grammar rule\n".to_string() }, Segment::FormOnly { form: Form::Ail, body: "grammar rule\n".to_string() },
] ]
); );
} }
@@ -49,7 +49,7 @@ fn example_marker_is_isolated() {
#[test] #[test]
fn multiple_directives_in_sequence() { fn multiple_directives_in_sequence() {
let input = "p1\n{example: a}\n{form-only: json}\njs\n{/form-only}\n{form-only: ailx}\nax\n{/form-only}\n{example: b}\np2\n"; let input = "p1\n{example: a}\n{form-only: json}\njs\n{/form-only}\n{form-only: ail}\nax\n{/form-only}\n{example: b}\np2\n";
let segments = split(input); let segments = split(input);
assert_eq!( assert_eq!(
segments, segments,
@@ -57,7 +57,7 @@ fn multiple_directives_in_sequence() {
Segment::Prose("p1\n".to_string()), Segment::Prose("p1\n".to_string()),
Segment::Example("a".to_string()), Segment::Example("a".to_string()),
Segment::FormOnly { form: Form::Json, body: "js\n".to_string() }, Segment::FormOnly { form: Form::Json, body: "js\n".to_string() },
Segment::FormOnly { form: Form::Ailx, body: "ax\n".to_string() }, Segment::FormOnly { form: Form::Ail, body: "ax\n".to_string() },
Segment::Example("b".to_string()), Segment::Example("b".to_string()),
Segment::Prose("p2\n".to_string()), Segment::Prose("p2\n".to_string()),
] ]
@@ -1,7 +1,7 @@
//! Token-balance gate for master/spec.md form-only blocks. //! Token-balance gate for master/spec.md form-only blocks.
//! //!
//! The mini-spec is the experimental treatment; if the JSON-cohort's //! The mini-spec is the experimental treatment; if the JSON-cohort's
//! form-only scaffolding is meaningfully longer than the AILX-cohort's //! form-only scaffolding is meaningfully longer than the AIL-cohort's
//! (or vice versa), the experiment is biased before the model ever //! (or vice versa), the experiment is biased before the model ever
//! sees the prompts. This test catches gross imbalance. //! sees the prompts. This test catches gross imbalance.
@@ -20,27 +20,27 @@ fn form_only_block_token_totals_within_five_percent() {
let segments = splitter::split(&spec_text); let segments = splitter::split(&spec_text);
let mut json_buf = String::new(); let mut json_buf = String::new();
let mut ailx_buf = String::new(); let mut ail_buf = String::new();
for seg in &segments { for seg in &segments {
if let Segment::FormOnly { form, body } = seg { if let Segment::FormOnly { form, body } = seg {
match form { match form {
Form::Json => json_buf.push_str(body), Form::Json => json_buf.push_str(body),
Form::Ailx => ailx_buf.push_str(body), Form::Ail => ail_buf.push_str(body),
} }
} }
} }
let bpe = tiktoken_rs::cl100k_base().expect("cl100k_base tokenizer should load"); let bpe = tiktoken_rs::cl100k_base().expect("cl100k_base tokenizer should load");
let json_count = bpe.encode_with_special_tokens(&json_buf).len(); let json_count = bpe.encode_with_special_tokens(&json_buf).len();
let ailx_count = bpe.encode_with_special_tokens(&ailx_buf).len(); let ail_count = bpe.encode_with_special_tokens(&ail_buf).len();
assert!(json_count > 0, "JSON-form blocks produced zero tokens"); assert!(json_count > 0, "JSON-form blocks produced zero tokens");
assert!(ailx_count > 0, "AILX-form blocks produced zero tokens"); assert!(ail_count > 0, "AIL-form blocks produced zero tokens");
let max = json_count.max(ailx_count) as f64; let max = json_count.max(ail_count) as f64;
let diff = (json_count as i64 - ailx_count as i64).unsigned_abs() as f64; let diff = (json_count as i64 - ail_count as i64).unsigned_abs() as f64;
let ratio = diff / max; let ratio = diff / max;
assert!( assert!(
ratio <= 0.05, ratio <= 0.05,
"form-only block token imbalance: json={json_count}, ailx={ailx_count}, ratio={ratio:.4} > 0.05", "form-only block token imbalance: json={json_count}, ail={ail_count}, ratio={ratio:.4} > 0.05",
); );
} }
@@ -24,7 +24,7 @@ The schema of the on-disk form is fixed. The toolchain rejects any
module that does not conform. module that does not conform.
The AILX surface is parenthesised tagged-head Lisp-style notation. The AIL surface is parenthesised tagged-head Lisp-style notation.
Every form begins with `(` followed by a tag identifier; positional Every form begins with `(` followed by a tag identifier; positional
arguments follow; the form closes with `)`. There are no infix arguments follow; the form closes with `)`. There are no infix
operators, no operator precedence, no implicit conversions. operators, no operator precedence, no implicit conversions.
@@ -39,12 +39,12 @@ digit. String literals are double-quoted with `\"`, `\\`, `\n`, `\t`
escapes; integer literals are signed decimal; the `(unit)` keyword escapes; integer literals are signed decimal; the `(unit)` keyword
denotes the unit value. denotes the unit value.
The AILX form is the printed dual of the JSON form: `ail parse The AIL form is the printed dual of the JSON form: `ail parse
file.ailx` produces the same canonical bytes as the JSON form would, file.ail` produces the same canonical bytes as the JSON form would,
and `ail render file.ail.json` produces the AILX text. Round-trip and `ail render file.ail.json` produces the AIL text. Round-trip
through this pair is gated by an integration test. through this pair is gated by an integration test.
```ailx ```ail
(module fn_returns_int (module fn_returns_int
(fn answer (fn answer
(doc "The simplest possible fn — no params, returns a fixed Int.") (doc "The simplest possible fn — no params, returns a fixed Int.")
@@ -95,7 +95,7 @@ Type::Forall: `(forall (a b) BODY-TYPE)` quantifies over `a` and
`b`. Class constraints attach as `(forall (a) (constraints (Eq a)) `b`. Class constraints attach as `(forall (a) (constraints (Eq a))
BODY-TYPE)`. BODY-TYPE)`.
```ailx ```ail
(module forall_polymorphic (module forall_polymorphic
(fn id (fn id
(doc "The polymorphic identity function. Exercises Type::Forall and Type::Var.") (doc "The polymorphic identity function. Exercises Type::Forall and Type::Var.")
@@ -144,7 +144,7 @@ of the fn's signature and contributes to its content hash, so
making the choice explicit at authoring time avoids surprise making the choice explicit at authoring time avoids surprise
hash changes when the typechecker's defaults shift. hash changes when the typechecker's defaults shift.
```ailx ```ail
(module param_modes_all (module param_modes_all
(fn f_implicit (fn f_implicit
(doc "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.") (doc "Implicit mode is the legacy default — `param_modes` is omitted from canonical JSON when every entry is Implicit, which is the case here. The visitor still observes ParamMode::Implicit via the (defaulted) ret_mode field on Type::Fn.")
@@ -207,7 +207,7 @@ Inside any term position, an integer literal is a bare decimal
number, a string is a double-quoted string, a bool is `true` or number, a string is a double-quoted string, a bool is `true` or
`false`, unit is `(unit)`. `false`, unit is `(unit)`.
```ailx ```ail
(module fn_calls_prelude (module fn_calls_prelude
(fn add (fn add
(doc "Add two Ints via the polymorphic prelude `+`. Also exercises TermLet by binding the sum to a local name before returning it, and TermClone by re-using the let-bound value.") (doc "Add two Ints via the polymorphic prelude `+`. Also exercises TermLet by binding the sum to a local name before returning it, and TermClone by re-using the let-bound value.")
@@ -216,7 +216,7 @@ number, a string is a double-quoted string, a bool is `true` or
(body (let s (app + x y) (clone s))))) (body (let s (app + x y) (clone s)))))
``` ```
```ailx ```ail
(module fn_with_lambda (module fn_with_lambda
(fn make_adder (fn make_adder
(doc "Curried adder: takes an Int and returns an Int -> Int closure. Exercises TermLam and Type::Fn appearing as a return type.") (doc "Curried adder: takes an Int and returns an Int -> Int closure. Exercises TermLam and Type::Fn appearing as a return type.")
@@ -258,7 +258,7 @@ need a separate registry to resolve overlapping constructor names
across ADTs. Example: `(ctor List Cons 1 (ctor List Nil))` builds a across ADTs. Example: `(ctor List Cons 1 (ctor List Nil))` builds a
single-element list. single-element list.
```ailx ```ail
(module data_simple (module data_simple
(data Box (vars a) (data Box (vars a)
(doc "A unary box around a polymorphic value.") (doc "A unary box around a polymorphic value.")
@@ -307,7 +307,7 @@ Pattern variables introduced by a `(case PAT BODY)` are in scope
inside `BODY` and bind there only. They do not leak into sibling inside `BODY` and bind there only. They do not leak into sibling
arms or into code outside the `match`. arms or into code outside the `match`.
```ailx ```ail
(module data_with_match (module data_with_match
(data List (data List
(doc "Monomorphic singly-linked Int list — boxed, recursive.") (doc "Monomorphic singly-linked Int list — boxed, recursive.")
@@ -329,7 +329,7 @@ arms or into code outside the `match`.
(case (pat-ctor Cons _ t) (app + 1 (app go t))))) (in (app go xs)))))) (case (pat-ctor Cons _ t) (app + 1 (app go t))))) (in (app go xs))))))
``` ```
```ailx ```ail
(module match_literal_pattern (module match_literal_pattern
(fn classify (fn classify
(doc "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.") (doc "Classify an Int via literal patterns plus a wildcard fallback. Exercises PatternLit and PatternWild.")
@@ -382,7 +382,7 @@ value; `X`'s value is discarded. Chains of sequencing are written
nested: `(seq A (seq B C))` runs `A`, then `B`, then yields `C`'s nested: `(seq A (seq B C))` runs `A`, then `B`, then yields `C`'s
value. value.
```ailx ```ail
(module fn_with_do_seq (module fn_with_do_seq
(fn main (fn main
(doc "Print two Ints in sequence. Exercises TermDo, TermSeq, and the IO effect on a fn type. The trailing unit return is implicit in the last Do (op returns Unit).") (doc "Print two Ints in sequence. Exercises TermDo, TermSeq, and the IO effect on a fn type. The trailing unit return is implicit in the last Do (op returns Unit).")
@@ -398,10 +398,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non- authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON. type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and encoding by hand; the toolchain owns that representation. NaN and
@@ -431,7 +431,7 @@ There is no separate syntax for exponent-form floats in the v1
authoring surface; for non-finite values use the prelude constants authoring surface; for non-finite values use the prelude constants
`nan`, `inf`, `neg_inf`. `nan`, `inf`, `neg_inf`.
```ailx ```ail
(module floats (module floats
(const pi (const pi
(doc "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).") (doc "Pi as a Float constant. Exercises Def::Const and Literal::Float — the bit pattern below is f64::to_bits(3.14).")
@@ -439,7 +439,7 @@ authoring surface; for non-finite values use the prelude constants
(body 3.14))) (body 3.14)))
``` ```
```ailx ```ail
(module bool_str (module bool_str
(fn is_true (fn is_true
(doc "Trivial Bool literal. Exercises Literal::Bool.") (doc "Trivial Bool literal. Exercises Literal::Bool.")
@@ -498,7 +498,7 @@ a)) BODY-TYPE)`. Each constraint is `(CLASS-NAME TYPE)` — usually
the type is a single bound type variable but a concrete type is the type is a single bound type variable but a concrete type is
also legal (typically a typecheck error unless an instance exists). also legal (typically a typecheck error unless an instance exists).
```ailx ```ail
(module class_def (module class_def
(class MyShow (class MyShow
(param a) (param a)
@@ -507,7 +507,7 @@ also legal (typically a typecheck error unless an instance exists).
(type (fn-type (params a) (ret (con Str))))))) (type (fn-type (params a) (ret (con Str)))))))
``` ```
```ailx ```ail
(module instance_def (module instance_def
(class MyShow (class MyShow
(param a) (param a)
@@ -733,10 +733,10 @@ There are five literal shapes. An integer literal is a signed
literal is a UTF-8 sequence in double quotes; AILang restricts the literal is a UTF-8 sequence in double quotes; AILang restricts the
authored byte set to ASCII printable (Decision 6 Constraint 3) — non- authored byte set to ASCII printable (Decision 6 Constraint 3) — non-
ASCII bytes are an authoring error. A unit literal is the value of ASCII bytes are an authoring error. A unit literal is the value of
type Unit, written `(unit)` in AILX and `{"kind": "unit"}` in JSON. type Unit, written `(unit)` in AIL and `{"kind": "unit"}` in JSON.
A Float literal is an IEEE-754 binary64 value. When you author A Float literal is an IEEE-754 binary64 value. When you author
Floats, write the decimal form (`3.14`) — the AILX parser converts Floats, write the decimal form (`3.14`) — the AIL parser converts
to the 64-bit bit pattern and emits the canonical 16-character to the 64-bit bit pattern and emits the canonical 16-character
lowercase hex string into the JSON. You never author the hex lowercase hex string into the JSON. You never author the hex
encoding by hand; the toolchain owns that representation. NaN and encoding by hand; the toolchain owns that representation. NaN and
@@ -761,7 +761,7 @@ Literal variants and their JSON shapes:
serialisation drift across `serde_json` versions. serialisation drift across `serde_json` versions.
Float bit patterns are computed by the toolchain on `ail parse`, Float bit patterns are computed by the toolchain on `ail parse`,
not by the author. When you write `3.14` in AILX, the parser emits not by the author. When you write `3.14` in AIL, the parser emits
`"bits": "40091eb851eb851f"` in the JSON form. `"bits": "40091eb851eb851f"` in the JSON form.
+2 -2
View File
@@ -19,7 +19,7 @@ The system was bootstrapped on 2026-05-09. See
| [`implement`](implement/SKILL.md) | Plan exists | Code + tests + per-iter journal entry, all uncommitted in the working tree | Standard iteration path | | [`implement`](implement/SKILL.md) | Plan exists | Code + tests + per-iter journal entry, all uncommitted in the working tree | Standard iteration path |
| [`audit`](audit/SKILL.md) | Milestone closing OR baseline drift suspected | Drift report + bench-regression report | **Mandatory** at milestone close | | [`audit`](audit/SKILL.md) | Milestone closing OR baseline drift suspected | Drift report + bench-regression report | **Mandatory** at milestone close |
| [`docwriter`](docwriter/SKILL.md) | API surface stabilized; rustdoc lag suspected | rustdoc updates in `///` and `//!`, uncommitted in the working tree | No — Boss-dispatched only | | [`docwriter`](docwriter/SKILL.md) | API surface stabilized; rustdoc lag suspected | rustdoc updates in `///` and `//!`, uncommitted in the working tree | No — Boss-dispatched only |
| [`fieldtest`](fieldtest/SKILL.md) | Boss-dispatched post-audit field test on a milestone that touched user-visible surface | 2-4 `.ailx` example fixtures + `docs/specs/<date>-fieldtest-<milestone>.md`, all uncommitted in the working tree | No — Boss-dispatched only | | [`fieldtest`](fieldtest/SKILL.md) | Boss-dispatched post-audit field test on a milestone that touched user-visible surface | 2-4 `.ail` example fixtures + `docs/specs/<date>-fieldtest-<milestone>.md`, all uncommitted in the working tree | No — Boss-dispatched only |
| [`debug`](debug/SKILL.md) | Bug encountered (failing test, segfault, wrong stdout) | RED-test in the working tree (uncommitted) + cause analysis | **Mandatory RED-first** for any bug | | [`debug`](debug/SKILL.md) | Bug encountered (failing test, segfault, wrong stdout) | RED-test in the working tree (uncommitted) + cause analysis | **Mandatory RED-first** for any bug |
| [`boss`](boss/SKILL.md) | User-invoked only (`/boss`) | Autonomous orchestration session — dispatches existing skills until done-state or bounce-back | No — user-gated | | [`boss`](boss/SKILL.md) | User-invoked only (`/boss`) | Autonomous orchestration session — dispatches existing skills until done-state or bounce-back | No — user-gated |
@@ -79,7 +79,7 @@ are no orphan agents (an anti-pattern after the 2026-05-09 build-out).
| `ailang-bencher` | `audit/agents/` | `audit` (regression diagnostics — hypothesis-driven) | | `ailang-bencher` | `audit/agents/` | `audit` (regression diagnostics — hypothesis-driven) |
| `ailang-docwriter` | `docwriter/agents/` | `docwriter` (Boss-dispatched rustdoc sweep post-stability) | | `ailang-docwriter` | `docwriter/agents/` | `docwriter` (Boss-dispatched rustdoc sweep post-stability) |
| `ailang-debugger` | `debug/agents/` | `debug` (RED-first; hands off GREEN to `implement` mini-mode) | | `ailang-debugger` | `debug/agents/` | `debug` (RED-first; hands off GREEN to `implement` mini-mode) |
| `ailang-fieldtester` | `fieldtest/agents/` | `fieldtest` (writes real-world examples in `.ailx` Surface form against DESIGN.md only — never the compiler source) | | `ailang-fieldtester` | `fieldtest/agents/` | `fieldtest` (writes real-world examples in `.ail` Surface form against DESIGN.md only — never the compiler source) |
Each agent file has YAML frontmatter (`name`, `description`, `tools`) Each agent file has YAML frontmatter (`name`, `description`, `tools`)
plus a system-prompt body. The `description` field is the one-sentence plus a system-prompt body. The `description` field is the one-sentence
+1 -1
View File
@@ -130,7 +130,7 @@ is a side experiment, not the headline.
## What you DO ship ## What you DO ship
- New bench fixtures under `examples/bench_*.{ailx,ail.json}` when none of - New bench fixtures under `examples/bench_*.ail*` when none of
the existing ones exercise the hypothesis. Pair them (Implicit + explicit-mode the existing ones exercise the hypothesis. Pair them (Implicit + explicit-mode
variants) where the comparison demands it. variants) where the comparison demands it.
- Edits to `bench/run.sh` (or a new harness alongside it) when the - Edits to `bench/run.sh` (or a new harness alongside it) when the
+5 -5
View File
@@ -1,6 +1,6 @@
--- ---
name: fieldtest name: fieldtest
description: Boss-dispatched only, after audit closes clean (or with ratified drift only), when the orchestrator judges the iteration is complete and wants a field test. Picks 2-4 real-world programming tasks within the milestone's scope, implements each in the AIL Surface form (.ailx — not raw JSON), runs the resulting binaries, and writes a friction-and-bug spec to docs/specs/<date>-fieldtest-<milestone>.md. The spec feeds the next plan as a reference. Implementer simulates a downstream LLM that has only DESIGN.md plus the public examples — never the language's own implementation. description: Boss-dispatched only, after audit closes clean (or with ratified drift only), when the orchestrator judges the iteration is complete and wants a field test. Picks 2-4 real-world programming tasks within the milestone's scope, implements each in the AIL Surface form (.ail — not raw JSON), runs the resulting binaries, and writes a friction-and-bug spec to docs/specs/<date>-fieldtest-<milestone>.md. The spec feeds the next plan as a reference. Implementer simulates a downstream LLM that has only DESIGN.md plus the public examples — never the language's own implementation.
--- ---
# fieldtest — LLM-usability field test for a shipped milestone # fieldtest — LLM-usability field test for a shipped milestone
@@ -24,7 +24,7 @@ specs at `docs/specs/<date>-fieldtest-<milestone>.md`.
The substantive process — read DESIGN.md + JOURNAL + milestone spec, The substantive process — read DESIGN.md + JOURNAL + milestone spec,
pick 2-4 real-world programming tasks per milestone axis, implement pick 2-4 real-world programming tasks per milestone axis, implement
each in `.ailx` Surface form, run via `ail check`/`build`/`run`, each in `.ail` Surface form, run via `ail check`/`build`/`run`,
classify findings, write the spec — lives in classify findings, write the spec — lives in
`agents/ailang-fieldtester.md`. That file also carries the spec `agents/ailang-fieldtester.md`. That file also carries the spec
template, the source-isolation discipline (no reading under template, the source-isolation discipline (no reading under
@@ -65,7 +65,7 @@ routing table below applies in all cases.
``` ```
THE FIELDTESTER WORKS FROM DESIGN.MD AND PUBLIC EXAMPLES — NOT FROM THE COMPILER SOURCE. THE FIELDTESTER WORKS FROM DESIGN.MD AND PUBLIC EXAMPLES — NOT FROM THE COMPILER SOURCE.
EVERY EXAMPLE IS WRITTEN IN .ailx (SURFACE) FIRST. RAW .ail.json IS NEVER HAND-AUTHORED. EVERY EXAMPLE IS WRITTEN IN .ail (SURFACE) FIRST. RAW .ail.json IS NEVER HAND-AUTHORED.
EVERY FRICTION POINT AND BUG IS RECORDED. NONE IS WORKED AROUND. EVERY FRICTION POINT AND BUG IS RECORDED. NONE IS WORKED AROUND.
``` ```
@@ -79,7 +79,7 @@ agent compiler-internal hints in the carrier.
Dispatch `ailang-fieldtester` with the carrier from the Handoff Dispatch `ailang-fieldtester` with the carrier from the Handoff
Contract below. The agent picks 2-4 examples (one per axis the Contract below. The agent picks 2-4 examples (one per axis the
milestone touched), implements them in `.ailx`, runs them through the milestone touched), implements them in `.ail`, runs them through the
public `ail` CLI, classifies findings, and writes the spec. All public `ail` CLI, classifies findings, and writes the spec. All
artefacts (fixtures + spec) stay in the working tree as unstaged artefacts (fixtures + spec) stay in the working tree as unstaged
changes; the Boss commits them after reviewing the report (suggested changes; the Boss commits them after reviewing the report (suggested
@@ -104,7 +104,7 @@ variation); five is too many for one report to stay readable.
| Field | Content | | Field | Content |
|-------|---------| |-------|---------|
| `spec_path` | `docs/specs/<date>-fieldtest-<milestone>.md` | | `spec_path` | `docs/specs/<date>-fieldtest-<milestone>.md` |
| `examples_added` | list of `.ailx` paths committed | | `examples_added` | list of `.ail` paths committed |
| `findings` | list, each with class (`bug` / `friction` / `spec_gap` / `working`) + recommendation | | `findings` | list, each with class (`bug` / `friction` / `spec_gap` / `working`) + recommendation |
| `status` | `clean` / `friction_found` / `bugs_found` / `infra_blocked` | | `status` | `clean` / `friction_found` / `bugs_found` / `infra_blocked` |
+15 -15
View File
@@ -1,6 +1,6 @@
--- ---
name: ailang-fieldtester name: ailang-fieldtester
description: Implements 2-4 real-world programming tasks in the AIL Surface form (.ailx) for a freshly closed milestone, runs them through the public `ail` CLI, and reports friction, bugs, and spec gaps as a structured spec. Simulates a downstream LLM author who has only DESIGN.md and the public examples — never the language's own implementation. Does NOT fix bugs and does NOT hand-write canonical JSON. description: Implements 2-4 real-world programming tasks in the AIL Surface form (.ail) for a freshly closed milestone, runs them through the public `ail` CLI, and reports friction, bugs, and spec gaps as a structured spec. Simulates a downstream LLM author who has only DESIGN.md and the public examples — never the language's own implementation. Does NOT fix bugs and does NOT hand-write canonical JSON.
tools: Read, Edit, Write, Bash, Glob, Grep tools: Read, Edit, Write, Bash, Glob, Grep
--- ---
@@ -46,7 +46,7 @@ Read in this order, before picking examples:
4. `docs/specs/<milestone>.md` if one exists — the contract this 4. `docs/specs/<milestone>.md` if one exists — the contract this
milestone signed up for. milestone signed up for.
5. `examples/` — to learn the *form* of valid AIL. You may read any 5. `examples/` — to learn the *form* of valid AIL. You may read any
`.ailx` and `.ail.json` under `examples/` (these are the public `.ail` and `.ail.json` under `examples/` (these are the public
corpus). You may NOT use them as a hint about how the compiler corpus). You may NOT use them as a hint about how the compiler
handles edge cases; only as a hint about the shape of the surface. handles edge cases; only as a hint about the shape of the surface.
@@ -66,7 +66,7 @@ spec; if both are also empty, return `NEEDS_CONTEXT`.
``` ```
DESIGN.MD AND `examples/` ARE YOUR ONLY REFERENCE. CRATES/, RUNTIME/, BENCH/ ARE FORBIDDEN READS. DESIGN.MD AND `examples/` ARE YOUR ONLY REFERENCE. CRATES/, RUNTIME/, BENCH/ ARE FORBIDDEN READS.
EVERY EXAMPLE IS WRITTEN IN .ailx FIRST. NO HAND-WRITTEN .ail.json. EVERY EXAMPLE IS WRITTEN IN .ail FIRST. NO HAND-WRITTEN .ail.json.
RECORD WHAT HAPPENS. DO NOT FIX. DO NOT WORK AROUND. RECORD WHAT HAPPENS. DO NOT FIX. DO NOT WORK AROUND.
``` ```
@@ -76,7 +76,7 @@ under `crates/`, `runtime/`, `bench/scripts/`, or `bench/reference/`,
**stop**. The only file paths you may open are: **stop**. The only file paths you may open are:
- `CLAUDE.md`, `docs/**`, `examples/**`, `skills/**` - `CLAUDE.md`, `docs/**`, `examples/**`, `skills/**`
- the `.ailx` and `.ail.json` files YOU create under `examples/` - the `.ail` and `.ail.json` files YOU create under `examples/`
- the binaries YOU produce via `ail build` - the binaries YOU produce via `ail build`
- the `.ll` files YOU produce via `ail emit-ir` if you want to - the `.ll` files YOU produce via `ail emit-ir` if you want to
inspect generated IR (the IR is part of the public surface per inspect generated IR (the IR is part of the public surface per
@@ -104,19 +104,19 @@ Each phase completes before the next starts.
thin) or a 500-line numerics library (too thick). thin) or a 500-line numerics library (too thick).
3. Total: 2-4 examples. 3. Total: 2-4 examples.
### Phase 2 — Implement each example in `.ailx` ### Phase 2 — Implement each example in `.ail`
For each example, in this order: For each example, in this order:
1. Draft the program in `.ailx` Surface form. Reach for the milestone's 1. Draft the program in `.ail` Surface form. Reach for the milestone's
new surface where it fits naturally — but do not contort an new surface where it fits naturally — but do not contort an
example to use a feature that doesn't fit. example to use a feature that doesn't fit.
2. Save as `examples/fieldtest/<milestone-short>_<n>_<slug>.ailx`, 2. Save as `examples/fieldtest/<milestone-short>_<n>_<slug>.ail`,
e.g. `examples/fieldtest/22_1_eq_rational.ailx`. e.g. `examples/fieldtest/22_1_eq_rational.ail`.
3. Run, in this order: 3. Run, in this order:
```bash ```bash
ail check examples/fieldtest/<...>.ailx ail check examples/fieldtest/<...>.ail
ail build examples/fieldtest/<...>.ailx -o /tmp/ft_<n> ail build examples/fieldtest/<...>.ail -o /tmp/ft_<n>
/tmp/ft_<n> /tmp/ft_<n>
``` ```
Note: if your repo invokes `ail` as `cargo run -p ail --` instead, Note: if your repo invokes `ail` as `cargo run -p ail --` instead,
@@ -137,10 +137,10 @@ binary missing); in that case return `BLOCKED` with the cause.
### Phase 3 — Generate canonical JSON, only via tooling ### Phase 3 — Generate canonical JSON, only via tooling
The canonical form is `.ail.json`. You do NOT hand-write it. After The canonical form is `.ail.json`. You do NOT hand-write it. After
each `.ailx` runs cleanly, generate the JSON via: each `.ail` runs cleanly, generate the JSON via:
```bash ```bash
ail render --json examples/fieldtest/<...>.ailx > examples/fieldtest/<...>.ail.json ail render --json examples/fieldtest/<...>.ail > examples/fieldtest/<...>.ail.json
``` ```
(or whichever subcommand `ail --help` lists for the surface→json (or whichever subcommand `ail --help` lists for the surface→json
@@ -164,7 +164,7 @@ merged.
### Phase 5 — Write the spec, hand back ### Phase 5 — Write the spec, hand back
Write `docs/specs/<YYYY-MM-DD>-fieldtest-<milestone>.md` using the Write `docs/specs/<YYYY-MM-DD>-fieldtest-<milestone>.md` using the
spec structure below. Leave all artefacts (the `.ailx` files, the spec structure below. Leave all artefacts (the `.ail` files, the
`.ail.json` files, and the spec file) in the working tree as `.ail.json` files, and the spec file) in the working tree as
unstaged changes. You do NOT commit — the Boss commits after unstaged changes. You do NOT commit — the Boss commits after
reading the end-report (suggested commit subject: reading the end-report (suggested commit subject:
@@ -187,7 +187,7 @@ What the milestone shipped. One paragraph.
## Examples ## Examples
Per example, one subsection: Per example, one subsection:
### `examples/fieldtest/<milestone>_<n>_<slug>.ailx` — <task name> ### `examples/fieldtest/<milestone>_<n>_<slug>.ail` — <task name>
- What it does - What it does
- Why this task fits the milestone's scope - Why this task fits the milestone's scope
- Outcome: compiles? runs? matches expected stdout? - Outcome: compiles? runs? matches expected stdout?
@@ -272,7 +272,7 @@ before committing.
| "DESIGN.md is fuzzy on the typeclass instance ordering, I'll pick the natural reading and proceed" | Pick the reading, RUN the example, AND record `spec_gap` with the reading you picked and why another reading was equally plausible. | | "DESIGN.md is fuzzy on the typeclass instance ordering, I'll pick the natural reading and proceed" | Pick the reading, RUN the example, AND record `spec_gap` with the reading you picked and why another reading was equally plausible. |
| "Bug found — I'll just fix it now, faster than handing off to debug" | Fix-in-place violates the skill split. The fix lands in a separate, RED-tested commit via `debug` → `implement`. | | "Bug found — I'll just fix it now, faster than handing off to debug" | Fix-in-place violates the skill split. The fix lands in a separate, RED-tested commit via `debug` → `implement`. |
| "Two examples both ran clean, no findings — short report" | A clean run is itself a finding (`working`). Record what was reached for, what diagnostic showed up when wrong, what was easy. Wins protect the feature from drift. | | "Two examples both ran clean, no findings — short report" | A clean run is itself a finding (`working`). Record what was reached for, what diagnostic showed up when wrong, what was easy. Wins protect the feature from drift. |
| "I'll skip the JSON file generation, the .ailx is enough" | The committed pair (.ailx + .ail.json) is the regression fixture. Without the JSON, future bench runs can't pick it up. If `ail render --json` doesn't exist, record it as a finding. | | "I'll skip the JSON file generation, the .ail is enough" | The committed pair (.ail + .ail.json) is the regression fixture. Without the JSON, future bench runs can't pick it up. If `ail render --json` doesn't exist, record it as a finding. |
| "Three examples is enough, I'll skip the fourth axis" | Each axis the milestone touched needs at least one example. Skipping an axis silently turns the field test into a partial signal, which is worse than no signal because the orchestrator will read it as full coverage. | | "Three examples is enough, I'll skip the fourth axis" | Each axis the milestone touched needs at least one example. Skipping an axis silently turns the field test into a partial signal, which is worse than no signal because the orchestrator will read it as full coverage. |
## Red Flags — STOP and re-read DESIGN.md ## Red Flags — STOP and re-read DESIGN.md