ail: merge-prose subcommand + PROSE_ROUNDTRIP.md (iter 20d)

Closes family 20: prose-edit cycle is end-to-end. ail merge-prose
<original.ail.json> <edited.prose.txt> composes a shaped LLM prompt
to stdout. User pipes to their LLM (Claude/etc.), gets new .ail.json,
runs ail check, saves.

No built-in API client — AILang stays a compiler + tooling, the LLM
mediator is supplied externally. PROSE_ROUNDTRIP.md documents the
6-step cycle, the prompt contract (preserve modes/effects/tail
flags from original), failure modes, and the design choice to omit
an API client.

Family 20 now closes: 20a renderer + 20b polish + 20d mediator
shipped; 20c rolled into 20a. Three deferred polishes (let-inlining,
print sugar, doc-wrap widow control) wait for corpus signal.
This commit is contained in:
2026-05-08 18:22:46 +02:00
parent 9cf0e3e81c
commit 9c09dfbc8d
4 changed files with 510 additions and 0 deletions
+48
View File
@@ -2372,3 +2372,51 @@ fn alloc_rc_partial_drop_at_app_let_close_does_not_leak_unmoved_fields() {
`partial_drop_<m>_<T>(p, mask)`"
);
}
/// Iter 20d: `ail merge-prose` reads two files and prints a prompt
/// that frames the prose-round-trip task for an external LLM. Smoke
/// test: writes two temp inputs, invokes the binary, asserts exit 0
/// and that the captured stdout carries the role-statement landmark
/// plus both inputs verbatim.
#[test]
fn merge_prose_prints_framed_prompt() {
let tmp = std::env::temp_dir().join(format!(
"ailang_e2e_merge_prose_{}",
std::process::id()
));
std::fs::create_dir_all(&tmp).unwrap();
let orig_path = tmp.join("orig.ail.json");
let prose_path = tmp.join("edited.prose.txt");
let orig_bytes =
b"{\"schema\":\"ailang/v0\",\"name\":\"foo\",\"imports\":[],\"defs\":[]}";
let prose_bytes = b"// module foo\n\nfn bar() -> Int { 42 }\n";
std::fs::write(&orig_path, orig_bytes).unwrap();
std::fs::write(&prose_path, prose_bytes).unwrap();
let output = Command::new(ail_bin())
.args(["merge-prose"])
.arg(&orig_path)
.arg(&prose_path)
.output()
.expect("ail merge-prose failed to run");
assert!(
output.status.success(),
"ail merge-prose exited non-zero: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("stdout utf8");
assert!(!stdout.is_empty(), "empty stdout from merge-prose");
assert!(
stdout.contains("integrating prose edits"),
"prompt missing role-statement landmark; got:\n{stdout}"
);
// Both inputs must round-trip into the prompt verbatim.
assert!(
stdout.contains("ailang/v0"),
"prompt missing original .ail.json content"
);
assert!(
stdout.contains("fn bar() -> Int { 42 }"),
"prompt missing edited prose content"
);
}