Files
AILang/docs/journals/2026-05-12-iter-ext-cli.1.md
T
Brummel 79cc78507b 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.
2026-05-12 14:45:03 +02:00

138 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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