ail: embed Form-A spec in merge-prose prompt (iter 20f)
Closes a design hole shipped in 20d: the merge-prose prompt instructed
the LLM to emit JSON-AST and gave a 12-line schema-essentials reminder.
JSON-AST is the canonical hashable artefact, not a writing surface; the
reminder was not a language spec. Foreign LLMs had no realistic shot.
20f makes two coupled changes:
1) The LLM now emits Form-A (the canonical authoring surface fixed by
Decision 6). merge-prose loads the original via ailang_core::load_module
and re-renders via ailang_surface::print before embedding (round-trip
is a gating contract on the surface crate, so this is lossless). The
user runs `ail parse foo.new.ailx` to recover JSON, then `ail check`.
2) crates/ailang-core/specs/form_a.md is the complete LLM-targeted
Form-A specification — grammar, every term/pattern/type/def keyword,
schema invariants, pitfall catalogue, four few-shot modules from
examples/*.ailx. Exported as ailang_core::FORM_A_SPEC via include_str!
and embedded verbatim in every merge-prose prompt.
Drift detection in crates/ailang-core/tests/spec_drift.rs: every variant
of Term, Pattern, Type, Def, Literal is reached via exhaustive `match`.
The arms are not the assertion — adding a new variant without updating
the match is a compile error before the test runs. Once matched, an
anchor string is asserted to appear in FORM_A_SPEC. 8 tests, all green.
The hand-written + mechanical-drift-test combo addresses the user's
"distance to code is too big" concern about a docs-only spec. Generator
overkill rejected: structural drift is mechanically caught, but prose
explanation, schema-invariant catalogue, pitfall list, and few-shot
corpus cannot be emitted from AST shape alone.
Tests:
- ailang-core: +8 spec_drift tests; existing 12 unchanged
- ail unit (3): rewritten in lockstep — assert (own T)/(effects IO)
landmarks, FORM-A SPECIFICATION header, FORM_A_SPEC body verbatim
- ail e2e merge_prose_prints_framed_prompt: rewritten — assert
`(module foo` + `FORM-A SPECIFICATION` instead of `ailang/v0`
- Workspace: all green
Richer integration paths (LLM tool-use, MCP server, LSP) were named
in the design discussion and deferred per "kiss". All three layer
additively on the static-prompt path; static prompt remains the
lowest-common-denominator fallback.
This commit is contained in:
+13
-3
@@ -2387,6 +2387,11 @@ fn merge_prose_prints_framed_prompt() {
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let orig_path = tmp.join("orig.ail.json");
|
||||
let prose_path = tmp.join("edited.prose.txt");
|
||||
// Iter 20f: merge-prose now loads the module via ailang_core,
|
||||
// renders to Form-A, and embeds *that* in the prompt — so the
|
||||
// input must be a well-formed ailang/v0 JSON-AST that
|
||||
// load_module accepts. The schema, name, and an empty defs list
|
||||
// are the minimum.
|
||||
let orig_bytes =
|
||||
b"{\"schema\":\"ailang/v0\",\"name\":\"foo\",\"imports\":[],\"defs\":[]}";
|
||||
let prose_bytes = b"// module foo\n\nfn bar() -> Int { 42 }\n";
|
||||
@@ -2410,13 +2415,18 @@ fn merge_prose_prints_framed_prompt() {
|
||||
stdout.contains("integrating prose edits"),
|
||||
"prompt missing role-statement landmark; got:\n{stdout}"
|
||||
);
|
||||
// Both inputs must round-trip into the prompt verbatim.
|
||||
// Original module is now embedded as Form-A, not JSON.
|
||||
assert!(
|
||||
stdout.contains("ailang/v0"),
|
||||
"prompt missing original .ail.json content"
|
||||
stdout.contains("(module foo"),
|
||||
"prompt missing original Form-A content; got:\n{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("fn bar() -> Int { 42 }"),
|
||||
"prompt missing edited prose content"
|
||||
);
|
||||
// The embedded spec must be present.
|
||||
assert!(
|
||||
stdout.contains("FORM-A SPECIFICATION"),
|
||||
"prompt missing FORM-A SPECIFICATION section"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user