design: ratify form (B) prose projection (iter 20e tidy)

Family-20 tidy-iter: ailang-architect drift review surfaced
DESIGN.md silence on the new prose surface as the highest-leverage
gap. Decision 6 now has a 'Form (B) — human prose projection'
subsection naming the lossy-vs-lossless contract, the no-parser-
by-design choice, and the LLM-mediated round-trip. CLI block
gained 'ail prose' and 'ail merge-prose'.

No code changes. Form (B) does not weaken any Decision 6
invariant: JSON-AST remains the only hashable artefact, form (A)
the canonical authoring surface, 30-production grammar unchanged.

Two architect findings deferred (prompt-template lockstep test;
snapshot-fixture cross-family coupling). Family 20 closes.
JOURNAL queue: empty.
This commit is contained in:
2026-05-08 18:27:34 +02:00
parent 9c09dfbc8d
commit 2852326dd3
2 changed files with 139 additions and 0 deletions
+62
View File
@@ -454,6 +454,64 @@ JSON identical to their corresponding `.ail.json` files.
constraint-1 budget. No new lexical rule (`tail-app` / `tail-do`
are bare ident tokens; no special casing).
### Form (B) — human prose projection (Family 20)
**Status: shipped.** Family 20 (Iter 20a20d, 2026-05-08) ships a
second textual projection of the AST: `ailang-prose`, a one-way
projection from `Module → human-readable text`. It is **not** an
authoring surface; it is the "display" projection that Decision 6's
architectural pin (line 167176) explicitly anticipated:
> *"Future projections are explicitly anticipated: a visual /
> graphical front-end is a plausible second projection for human
> review and inspection (display being the one case where non-AI
> eyes matter). The architecture leaves room: any producer of
> well-formed `ailang-core::ast::Module` values is a valid
> front-end."*
Form (B) targets the specific failure mode where a human reviewer
needs to read an AILang module quickly. Form (A) was designed to
fit a 30-production EBNF spec and to be parsed zero-shot by foreign
LLMs; that prioritisation makes it dense and visually noisy for
human readers. Form (B) inverts the trade-offs:
- **Rust-flavoured surface.** Braces and `=>` for match arms,
Rust-aligned 4-level operator precedence, infix arithmetic
(`a + b`, not `+(a, b)`), unary `!` for `not`.
- **Lossy by design.** Projection elides machinery the LLM can
re-derive: `(con T)` wrappers (`(con Int)` → `Int`), the
`(fn-type (params ...) (ret ...))` wrap, `(term-ctor T C ...)`
collapses to `C(...)`, redundant parens. Only the AST machinery
whose information is recoverable from typecheck context.
- **Lossless on load-bearing detail.** Mode annotations
(`own T`, `borrow T`), effects (`with IO`), explicit `clone`,
`reuse-as`, doc strings, type annotations on signatures and
lambdas, the `tail` flag — all preserved verbatim.
Critically, **form (B) has no parser**. Form (A) is round-trippable
by construction (Decision 6 constraint 2); form (B) deliberately is
not. Re-integrating prose edits requires an external LLM mediator,
not a compiler pass — see `docs/PROSE_ROUNDTRIP.md` for the
six-step cycle and the prompt template `ail merge-prose` composes.
Form (B) does not weaken any Decision 6 invariant:
- The JSON-AST remains the only hashable artefact. Prose is not
hashed, not content-addressed, not load-bearing for any
cross-module reference.
- Form (A) remains the canonical authoring surface. Foreign LLMs
still author against form (A); humans review and edit through
form (B).
- The 30-production grammar of form (A) is unchanged.
- `ailang-check` and `ailang-codegen` remain projection-agnostic;
`ailang-prose` is a downstream consumer of `ailang-core::ast`,
parallel to `ailang-surface` but in the rendering direction only.
The CLI gains `ail prose <m.ail.json>` (the deterministic
projection) and `ail merge-prose <m.ail.json> <edited.prose.txt>`
(the mediator-prompt composer); both are listed in the CLI section
below.
## Decision 7: redundancy removal — `Term::If` is not a primitive
**Status: REVERTED in Iter 14g.** This decision was made on shaky grounds —
@@ -1311,6 +1369,10 @@ ail manifest <module.ail.json> — table: name :: type !effects [hash]
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 parse <module.ailx> — form-A text → canonical JSON-AST
ail prose <module.ail.json> — JSON-AST → form-B (lossy human prose, no parser; Family 20)
ail merge-prose <m.ail.json> <m.prose.txt>
— compose the LLM-mediator prompt for the prose round-trip
(see docs/PROSE_ROUNDTRIP.md)
ail deps <module.ail.json> — list cross-module references
ail diff <a.ail.json> <b.ail.json> — content-addressed def-level diff
ail workspace <entry.ail.json> — list all modules transitively reachable from entry
+77
View File
@@ -9167,3 +9167,80 @@ practice the family stays as-shipped.
validates the merge product; `merge-prose` only composes the
prompt.
## 2026-05-08 — Family-20 tidy-iter (Iter 20e)
Per CLAUDE.md "Tidy-iter at family boundaries" — every named iter
family closes with a tidy-iter. `ailang-architect` ran a drift
review of family 20 and reported three actionable items, plus two
non-actionable observations.
### Architect findings
1. **DESIGN.md silent on form (B).** Decision 6 documents form (A)
extensively but never names the new prose surface. The CLI
section (`docs/DESIGN.md` "## CLI") lists `render`/`parse` but
not `prose`/`merge-prose`. JOURNAL has the design pinning, but
DESIGN.md is the canonical spec — this is the highest-leverage
fix.
2. **Prompt-template drift hazard.** `compose_merge_prose_prompt`
in `crates/ail/src/main.rs` and the prompt block in
`docs/PROSE_ROUNDTRIP.md` are byte-equivalent twins maintained
by hand. No test enforces equality. Bounded — the comment in
`main.rs` line 240242 already names the lockstep — but real.
3. **Snapshot-fixture coupling.** `ailang-prose/tests/snapshot.rs`
binds prose tests to four 18-family RC fixtures. Any future
iter touching those examples must re-render the `.prose.txt`.
Not actionable until a real refactor forces it.
### Resolved this iter
**Item 1 — ratified.** Two edits to `docs/DESIGN.md`:
- New subsection at the end of Decision 6 (after Iter 14e
refinements, before Decision 7): `### Form (B) — human prose
projection (Family 20)`. ~50 lines documenting form (B) as the
"display" projection Decision 6's architectural pin already
anticipated, the lossy-vs-lossless contract, and the fact that
form (B) has no parser by design (round-trip is LLM-mediated,
not compiler-passed).
- CLI block extended with `ail prose` and `ail merge-prose` lines.
The new subsection explicitly does NOT weaken any Decision 6
invariant: JSON-AST remains the only hashable artefact, form (A)
remains the canonical authoring surface, the 30-production grammar
is unchanged, and `check`/`codegen` stay projection-agnostic.
### Deferred
**Item 2 (prompt-template lockstep)** — the existing
`// Keep this template in lockstep with docs/PROSE_ROUNDTRIP.md`
comment at `main.rs:240242` is the audit trail. Adding an
equality test would require parsing the Markdown block in the
doc; brittle. Stays queued; will close if the next iter touching
the prompt actually surfaces the drift.
**Item 3 (snapshot-fixture coupling)** — not actionable; documented
as known consequence of cross-family snapshot binding.
### Family 20 final state
- 20a: prose renderer skeleton + `ail prose` CLI
- 20b: infix + paren elision + doc-wrap + nested-match
- 20c: rolled into 20a
- 20d: `ail merge-prose` + `docs/PROSE_ROUNDTRIP.md`
- 20e: this tidy-iter — DESIGN.md ratification
Three deferred polishes from 20b (let-inlining, `do io/print_int`
`print` sugar, doc-wrap widow control) remain queued; all three
need real-corpus signal to know whether the heuristic is right.
JOURNAL queue: empty again.
### Files touched this iter
- `docs/DESIGN.md` — Form (B) subsection in Decision 6, CLI block
extension. ~60 lines added.
- `docs/JOURNAL.md` — this entry.
No code changes. Build/test status unchanged from 20d (green).