iter ext-cli.1: CLI accepts .ail (Form A) sources alongside .ail.json

Closes the loop opened by this morning's .ailx → .ail rename. Every
path-taking ail subcommand (check, build, run, manifest, render,
prose, merge-prose, describe, emit-ir, diff, workspace, deps) now
accepts a .ail (Form A) input as well as .ail.json (Form B). For
.ail paths the subcommand parses through ailang_surface::parse
in-line and proceeds with the same loaded Module value .ail.json
would have produced; binary semantics are extension-agnostic.

Architecture: a new ailang_surface::{load_module, load_workspace}
pair dispatches on file extension. A new injection point
ailang_core::workspace::load_workspace_with<F> lets the surface
crate plug in an extension-aware loader without core having to
import surface (which would close a crate cycle). Import resolution
in workspace::visit now prefers a .ail sibling over a .ail.json
sibling. The CLI binary's 18 callsites switched to the surface
loaders. Surface parse failures route through
workspace_error_to_diagnostic as a new SurfaceParse variant of
WorkspaceLoadError, so `ail check --json foo.ail` on a malformed
.ail returns a parseable JSON diagnostic with code
surface-parse-error instead of the misleading
`json: expected value at line 1 column 1` fall-through.

Boss pre-commit correction: the implementer followed the plan
verbatim and used snake_case `surface_parse_error`, but every
existing diagnostic code in the codebase is kebab-case (verified
via `git grep` over crates/ailang-check/src and crates/ail/src);
realigned to `surface-parse-error` at three sites (the diagnostic
arm + two ct1 test strings). Journal Concerns section records
the correction.

Tests: cargo test --workspace 487 green (+7 from this iter:
1 core unit, 4 surface integration, 1 ail E2E pair, 1 ct1 CLI
diagnostic-shape). bench/check.py, compile_check.py, cross_lang.py
clean on re-run; the latency.{explicit,implicit}_at_rc tail
cluster occasionally flagged on first runs is the same
nondeterministic cluster the previous two audits documented;
baseline left pristine for the third consecutive iter.

Roadmap follow-up "ail check/build/run accept .ail extension" (P2)
removed.

DESIGN.md §Decision 6 / "CLI" bullet gained the symmetric upward
sentence: both .ail and .ail.json are first-class CLI inputs.
This commit is contained in:
2026-05-12 14:45:03 +02:00
parent efecbaa3e6
commit 79cc78507b
15 changed files with 611 additions and 35 deletions
+10
View File
@@ -445,6 +445,16 @@ on the shelf for a future iter only if both fail.
existing `ail render`. **`.ail.json` remains a first-class input**
to every existing subcommand; the parser is a producer, not a
gatekeeper.
**Both extensions accepted.** Since iter ext-cli.1 (2026-05-12),
every path-taking CLI subcommand accepts either `.ail` (Form A)
or `.ail.json` (Form B) as input. For `.ail` paths the subcommand
parses through `ailang_surface::parse` in-line and then proceeds
with the same loaded `Module` value that `.ail.json` would have
produced. `ail parse` remains the explicit converter — it does
nothing the implicit dispatch in the other subcommands does not,
but it is the supported way to materialise a stable `.ail.json`
snapshot from a `.ail` source for diff / hash / cache purposes.
- Stdlib (`std_list`, `std_maybe`, ...) authored in
form (A) from day one **because that is the AI authoring
projection**, not because JSON authoring is forbidden. The
+137
View File
@@ -0,0 +1,137 @@
# iter ext-cli.1 — CLI accepts `.ail` (Form A) source files alongside `.ail.json`
**Date:** 2026-05-12
**Started from:** efecbaa
**Status:** DONE
**Tasks completed:** 4 of 4
## Summary
Closes the loop opened by this morning's `.ailx → .ail` rename. Every
path-taking `ail` subcommand (`check`, `build`, `run`, `manifest`,
`render`, `prose`, `merge-prose`, `describe`, `emit-ir`, `diff`,
`workspace`, `deps`) now accepts a `.ail` (Form A) input as well as
`.ail.json` (Form B). For `.ail` paths the subcommand parses through
`ailang_surface::parse` in-line and proceeds with the same loaded
`Module` value `.ail.json` would have produced; the binary semantics
are extension-agnostic.
Architecture: a new `ailang_surface::{load_module, load_workspace}`
pair dispatches on file extension. A new injection point in
`ailang_core::workspace::load_workspace_with<F>` lets the surface
crate plug in an extension-aware loader without `ailang-core` having
to import `ailang-surface` (which would close a crate cycle). Import
resolution in `workspace::visit` now prefers a `.ail` sibling over a
`.ail.json` sibling of the same module name. The CLI binary's 18
`ailang_core::load_*` callsites all switched to the new
`ailang_surface::load_*`. Surface parse failures route through the
existing `workspace_error_to_diagnostic` channel as a new
`SurfaceParse` variant of `WorkspaceLoadError`, so
`ail check --json foo.ail` on a malformed `.ail` returns a parseable
JSON diagnostic with code `surface_parse_error` instead of the
pre-iter misleading `json: expected value at line 1 column 1`
fall-through.
## Per-task notes
- iter ext-cli.1.1: `WorkspaceLoadError::SurfaceParse { path,
message }` variant added to `ailang_core::workspace`;
`load_workspace` refactored into a thin wrapper around a new
`pub fn load_workspace_with<F>(entry, loader: F)` with `F:
Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy`; `visit`'s
signature gained the loader parameter and its import-path
construction now does `.ail`-first / `.ail.json`-fallback; new
test `load_workspace_with_custom_loader_is_called` pins the
injection. `tempfile` added as `ailang-core` dev-dependency.
- iter ext-cli.1.2: new `crates/ailang-surface/src/loader.rs` with
`pub fn load_module` (extension-dispatching: `.ail` → `parse`,
else delegate to `ailang_core::load_module`) and
`pub fn load_workspace` (calls `load_workspace_with(entry,
load_module)`); `lib.rs` re-exports both. Three integration tests
in `crates/ailang-surface/tests/loader.rs` pin the dispatch on
each form individually + the mixed-extension workspace case.
`tempfile` added as `ailang-surface` dev-dependency.
- iter ext-cli.1.3: every `ailang_core::load_module` /
`ailang_core::load_workspace` callsite in `crates/ail/src/main.rs`
(18 sites total) replaced with `ailang_surface::load_module` /
`ailang_surface::load_workspace`. Workspace-build placeholder
`W::SurfaceParse { .. } => None` arm added to
`workspace_error_to_diagnostic` (replaced in Task 4) so the
exhaustive match compiles. Two new E2E tests in
`crates/ail/tests/e2e.rs`: `ail_check_accepts_ail_source` and
`ail_run_accepts_ail_source_with_same_stdout_as_ail_json`.
- iter ext-cli.1.4: placeholder arm replaced with proper
`W::SurfaceParse { path, message } => Some(Diagnostic::error(
"surface_parse_error", message).with_ctx({"path": ...}))` —
mirroring the `SchemaMismatch` arm's builder pattern. DESIGN.md
§Decision 6 / "CLI" bullet gained the one-paragraph addendum
(verbatim from the plan, dated). New ct1 test
`ail_check_json_on_ail_with_syntax_error_returns_structured_diagnostic`
pins the JSON-diagnostic shape.
Plus one E2E coverage test added during Phase 3:
`load_workspace_prefers_ail_when_both_extensions_exist` —
protects the `.ail`-first precedence when both extensions are
present for the same module (the existing 1.2 test only exercised
the fallback half).
## Concerns
- **Diagnostic code convention drift (Boss-corrected pre-commit).**
The plan-specified code `surface_parse_error` used snake_case;
every other diagnostic code in the codebase is kebab-case
(`type-mismatch`, `unbound-var`, `over-strict-mode`,
`reuse-as-shape-mismatch`, ...). Boss verified the convention
via `git grep` over `crates/ailang-check/src` and
`crates/ail/src` before commit and aligned the literal to
`surface-parse-error` at three sites: the `SurfaceParse` arm
in `workspace_error_to_diagnostic` (`crates/ail/src/main.rs`)
and the two matching strings in
`crates/ail/tests/ct1_check_cli.rs` (the rustdoc on the test +
the `assert_eq!` against `first["code"]`). The plan file is
left at the snake_case form as historical record; the source
of truth is now the corrected code.
- **bench/check.py first run noisy.** First execution showed 4
regressions on `latency.{explicit,implicit}_at_rc.{p99_9,max}_us`
(tail-latency tier). Re-runs (×2) returned 0 regressed
(one even showed 4 improvements beyond tolerance). This is the
same `latency.explicit_at_rc.*` cluster audit-cma and audit-ms
already documented as nondeterministic; the iter's actual code
change introduces ONE extra `surface_path.is_file()` syscall per
import-resolution step plus a single-byte extension check per
load, neither of which can move us-scale tail latencies. Net:
baseline left pristine, consistent with the last two audits.
## Known debt
- The `ail parse` subcommand is still the only way to materialise
a stable `.ail.json` snapshot from a `.ail` source. Now that
every subcommand silently dispatches `.ail` through the parser,
one might argue for a "did you mean `ail parse`?" hint on paths
with non-`.ail`/non-`.ail.json` extensions. The roadmap note for
ext-cli.1 mentions this as a possible same-iter or follow-up
scope; nothing surfaced as load-bearing during impl, deferring.
## Files touched
Code:
- `crates/ailang-core/Cargo.toml` (+ tempfile dev-dep)
- `crates/ailang-core/src/workspace.rs` (SurfaceParse variant,
load_workspace_with, visit threading, .ail-first precedence,
new unit test)
- `crates/ailang-surface/Cargo.toml` (+ tempfile dev-dep)
- `crates/ailang-surface/src/lib.rs` (mod loader + re-exports)
- `crates/ailang-surface/src/loader.rs` (NEW)
- `crates/ailang-surface/tests/loader.rs` (NEW; 4 tests)
- `crates/ail/src/main.rs` (18 callsite rewires + SurfaceParse
diagnostic arm)
- `crates/ail/tests/e2e.rs` (2 new E2E tests)
- `crates/ail/tests/ct1_check_cli.rs` (1 new diagnostic-shape test)
Spec / metadata:
- `docs/DESIGN.md` (§Decision 6 / CLI bullet addendum)
- `Cargo.lock` (tempfile dev-dep transitive)
## Stats
bench/orchestrator-stats/2026-05-12-iter-ext-cli.1.json
+1
View File
@@ -29,3 +29,4 @@
- 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 — 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
- 2026-05-12 — iter ext-cli.1: `ail check`/build/run/...all 12 path-taking subcommands now accept `.ail` (Form A) inputs via `ailang_surface::{load_module, load_workspace}` (extension-dispatching loaders + injection point `core::load_workspace_with<F>`); workspace imports prefer `.ail` sibling over `.ail.json`; new `WorkspaceLoadError::SurfaceParse` variant routes surface parse errors as `surface-parse-error` structured diagnostics through `ail check --json`; DESIGN.md §Decision 6 CLI addendum; +7 new tests; closes the loop opened by iter ext-rename → 2026-05-12-iter-ext-cli.1.md
-10
View File
@@ -182,16 +182,6 @@ context. Pick the next milestone from P1.)_
- context: fieldtest 2026-05-11 — fieldtest fixtures could not be
placed under `examples/fieldtest/` because of this; predates the
canonical-type-names milestone but surfaces every time.
- [ ] **\[todo\]** `ail check`/`build`/`run` accept `.ail` extension —
today `ail check foo.ail` produces a misleading JSON-parse error
(`json: expected value at line 1 column 1`) because the loader only
accepts `.ail.json`. Either teach the CLI subcommands to auto-parse
`.ail` internally, or detect the extension and emit a
"did you mean `ail parse foo.ail`?" hint. The LLM-author's natural
first command should not be the one that produces a misleading
diagnostic.
- context: fieldtest 2026-05-11 — orthogonal to canonical-type-names
but every fieldtest invocation hits this on its first command.
- [ ] **\[todo\]** `check_in_workspace` per-module overlay narrowing —
`crates/ailang-check/src/lib.rs:1234` still clears+rebuilds
`env.ctor_index` per-module; ct.3.2 narrowed the analogous mono