iter form-a.1 (Tasks 6-12): milestone close

Second half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All seven tasks DONE; cargo
test --workspace green at every per-task boundary.

T6 — Bench-driver suffix flip from .ail.json to .ail across 4
Python scripts + run.sh. compile_check.py + cross_lang.py exit 0.

T7 — Re-author e2e.rs raw-JSON-inspect tests:
- diff_detects_changed_def — derive sum.ail.json on-the-fly via
  `ail parse examples/sum.ail` into tempdir, then mutate + diff.
- borrow_own_demo_modes_are_metadata_only — same pattern.
- reuse_as_demo_under_rc_uses_inplace_rewrite — same pattern.
- render_parse_round_trip_canonical — RETIRED (subsumed by T1's
  cli_parse_then_render_then_parse_is_idempotent over whole corpus).
- ail_run_accepts_ail_source_with_same_stdout_as_ail_json —
  re-authored to derive hello.ail.json in a per-process tempdir
  from hello.ail via `ail parse`, then assert dual-form stdout.

T8 — Bulk-delete 156 non-carve-out .ail.json. Inventory:
8 .ail.json (carve-outs, alphabetical: broken_unbound + prelude
+ 3× test_22b2_* + 3× test_ct1_*) + 157 .ail. carve_out_inventory
test un-#[ignore]'d and green. Forward-pulled 20 repairs that the
T1-5 dispatch's recon missed (12 Group-B suffix + 5 Group-A
load_workspace + 3 ail_run sites). Also forward-pulled T9 Step 5
(schema_coverage corpus flip from .ail.json to .ail) to satisfy
T8's green-gate.

T9 — Retire obsolete roundtrip tests:
- print_then_parse_round_trips_every_fixture (round_trip.rs)
- every_ail_fixture_matches_its_json_counterpart (round_trip.rs)
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
- Dead helpers: list_json_fixtures ×2, round_trip_one,
  strip_trailing_newlines.
Schema-coverage corpus already flipped in T8 (forward-pull).

T10 — DESIGN.md §"Roundtrip Invariant" (lines 2027-2109) restated
with parse-determinism + idempotency + CLI-pipeline-idempotency +
carve-out-anchor framing. Five surviving enforcement tests named.
§"Float literals" and §"Why anchored at top level" preserved.

T11 — §A4 doctrine edits: CLAUDE.md:5-6 + DESIGN.md:465-466.
Canonical form remains JSON-AST; authoring projection is .ail;
build derives JSON-AST in-process via ailang_surface::parse.

T12 — Milestone close:
- WhatsNew entry: user-facing language, lead with the change.
- Roadmap: [milestone] form-a struck [x] with closing note.
- Final inventory verified: 8 .ail.json + 157 .ail.
- Final cargo test --workspace: 557 passed, 0 failed, 3 ignored.
- bench/compile_check.py + bench/cross_lang.py: exit 0.

Test math: pre-iter 558 baseline + 3 new T1 tests = 561, − 1
(T7 retire) − 3 (T9 retire) = 557 final.

INDEX.md appended with the full iter summary covering T1-T12 (the
T1-5 commit at 77b28ad deferred the INDEX line to full-iter close).

Milestone [Form-A as the default authoring surface] structurally
closed. The compile-time-embed carve-out (prelude.ail.json) is
the subject of the queued follow-up milestone [Prelude embed:
Form-A as compile-time source]. audit-form-a runs as the next
dispatch.
This commit is contained in:
2026-05-13 11:31:39 +02:00
parent 9332d1e155
commit 9fdc4cacff
173 changed files with 551 additions and 6645 deletions
+55 -37
View File
@@ -462,8 +462,14 @@ on the shelf for a future iter only if both fail.
### What this Decision deliberately does not do
- It does not promote form (A) to the source of truth. Form (A) is
one projection; the JSON-AST stays canonical.
- It does not change which form is canonical: the JSON-AST remains
the hashable, content-addressed representation, and all hashing,
content-addressing, and cross-module references flow through it
unchanged. What did change in 2026-05-13 (this milestone) is
which form is authored: authors now write Form A; the JSON-AST
is materialised in-process by callers that need it. The two
forms are byte-isomorphic by the round-trip invariant. Form (A)
is one projection; the JSON-AST stays canonical.
- It does not foreclose visual or graphical front-ends. The crate
layout (`core` owns AST; `surface`/`visual`/... are siblings)
reserves that lane.
@@ -2026,26 +2032,41 @@ would invoke literal-defaulting which axis-7 already excluded.
## Roundtrip Invariant
Every well-formed AILang module has both a canonical `.ail.json`
representation and a textual `.ail` representation, and the two
are exact projections of the same AST. Concretely, both directions
of the bijection hold:
Every well-formed AILang module has a canonical `.ail.json`
representation (the hashable, content-addressed JSON-AST) and a
hand-authored `.ail` representation (Form A, the authoring
projection). The JSON-AST is derived from `.ail` in-process by
consumers that need it; the round-trip is now the *property* of
that derivation, not the byte-level agreement of two on-disk forms.
1. **JSON → text → JSON.** For every valid `.ail.json` module `J`,
`parse(print(load_module(J)))` is canonical-byte-equal to
`canonical::to_bytes(load_module(J))`. The textual surface is
the inverse of the loader composed with the printer; no
information is lost across the round-trip.
2. **text → JSON → text.** For every well-formed `.ail` text `t`,
`parse(t)` is a complete AST, and re-printing `print(parse(t))`
produces a text that re-parses to the same AST (modulo
formatting). The parser is total over the well-formed surface
and the printer is deterministic.
Concretely:
1. **Parse-determinism.** For every well-formed `.ail` text `t`,
`ailang_surface::parse(t)` produces a unique AST. The parser is
a pure function of input — no randomness, no time dependence,
no environment leak. Hashing `canonical::to_bytes(parse(t))` is
therefore well-defined for any `.ail` source.
2. **Idempotency under print.** For every well-formed `.ail` text
`t`, `canonical(parse(t)) == canonical(parse(print(parse(t))))`.
The printer is a left-inverse of the parser modulo canonical
form: print-then-parse is a no-op on the canonical bytes.
3. **CLI-pipeline idempotency.** For every `.ail` fixture, the
public CLI pipeline `ail parse | ail render | ail parse` is
byte-identical to direct `ail parse` of the source `.ail`.
Pins drift that crate-internal tests cannot see.
4. **Carve-out anchor.** Eight `.ail.json`-only fixtures (seven
subject-matter rejection tests + one compile-time-embed for
the prelude) survive in the corpus by structural necessity.
They participate in their own dedicated rejection-shape or
embed-mechanism tests, not in the round-trip gate.
Hashing is the consequence the language depends on: BLAKE3 of the
canonical bytes is identical for the two paths, so content-
addressing is form-agnostic. An LLM author can pick either form
without changing the identity of the module it produces.
canonical bytes is well-defined for any `.ail` source via parse-
determinism. An LLM author writes `.ail`; the build derives the
canonical hash without ambiguity.
### Float literals are inside the invariant
@@ -2065,29 +2086,26 @@ operating on the `examples/` corpus via dynamic `read_dir`
collection (no hardcoded fixture list, so newly added fixtures
inherit the gate automatically):
- `crates/ailang-surface/tests/round_trip.rs::print_then_parse_round_trips_every_fixture`
— for every `.ail.json`, `print` then `parse` produces canonical-
byte-equal output. Direction 1 above.
- `crates/ailang-surface/tests/round_trip.rs::parse_is_deterministic_over_every_ail_fixture`
— for every `.ail`, parse twice and assert canonical-byte
equality between the two ASTs. Direction 1 above.
- `crates/ailang-surface/tests/round_trip.rs::parse_then_print_then_parse_is_idempotent_on_every_ail_fixture`
— for every `.ail` text `t`, `parse(t)` and `parse(print(parse(t)))`
produce canonical-byte-equal AST. Direction 2 above.
- `crates/ailang-surface/tests/round_trip.rs::every_ail_fixture_matches_its_json_counterpart`
— for every `.ail` with a same-stem `.ail.json` counterpart,
`parse` of the text yields canonical bytes equal to the JSON
counterpart. Pins the hand-authored `.ail` corpus against
drift between the two forms at the fixture level.
- `crates/ail/tests/roundtrip_cli.rs::cli_parse_then_render_then_parse_is_idempotent`
— for every `.ail`, the public CLI pipeline `ail parse | ail render | ail parse`
reproduces canonical bytes byte-identical to direct `ail parse`.
Pins drift internal tests cannot see.
- `crates/ailang-core/tests/schema_coverage.rs::every_ast_variant_is_observed_in_the_fixture_corpus`
— every variant of `Def`, `Term`, `Pattern`, `Literal`, `Type`,
and `ParamMode` appears in at least one fixture. The visitor
matches each AST enum exhaustively without wildcard arms, so
any new AST variant fails to compile until the visitor is
extended in lockstep — the coverage table cannot silently fall
behind the schema.
- `crates/ail/tests/roundtrip_cli.rs::cli_render_then_parse_preserves_canonical_bytes_on_every_fixture`
— for every `.ail.json`, the public CLI pipeline `ail render` →
tempfile → `ail parse` reproduces BLAKE3-identical canonical
bytes. Pins the user-facing CLI against drift that crate-
internal tests cannot see.
and `ParamMode` appears in at least one `.ail` fixture (corpus
flipped from `.ail.json` to `.ail` at iter form-a.1). New AST
variants fail compile until the visitor and corpus are extended
in lockstep.
- `crates/ailang-core/tests/carve_out_inventory.rs::examples_ail_json_inventory_matches_carve_outs`
— exactly the eight named carve-out files exist under
`examples/*.ail.json` at any commit. A new `.ail.json` or a
missing carve-out fails the test.
A new fixture or a new AST variant that violates the invariant
fails one of these tests; the fix is in render or parse code,