Skip to main content

intent_implement/
context.rs

1//! Build rich context from a parsed intent AST for the LLM prompt.
2//!
3//! Assembles three pieces: the canonical spec source, the skeleton code,
4//! and a structured contracts summary (requires/ensures/invariants/edge_cases).
5
6use intent_codegen::{Language, format_ensures_item, format_expr, format_prop_value};
7use intent_parser::ast;
8
9/// All the context the LLM needs to generate a full implementation.
10pub struct PromptContext {
11    /// Canonical `.intent` source (re-formatted).
12    pub spec_source: String,
13    /// Skeleton code in the target language.
14    pub skeleton: String,
15    /// Human-readable contracts summary.
16    pub contracts: String,
17    /// Generated contract test harness (empty if no test blocks in spec).
18    pub test_harness: String,
19}
20
21/// Build the full prompt context from an AST and target language.
22pub fn build_context(file: &ast::File, lang: Language) -> PromptContext {
23    let spec_source = intent_render::format::format(file);
24    let skeleton = intent_codegen::generate(file, lang);
25    let contracts = build_contracts_summary(file);
26    let test_harness = intent_codegen::test_harness::generate(file, lang);
27
28    PromptContext {
29        spec_source,
30        skeleton,
31        contracts,
32        test_harness,
33    }
34}
35
36/// Walk the AST and produce a structured contracts summary.
37fn build_contracts_summary(file: &ast::File) -> String {
38    let mut out = String::new();
39
40    for item in &file.items {
41        match item {
42            ast::TopLevelItem::Action(action) => {
43                out.push_str(&format!("### Action: {}\n", action.name));
44
45                // Parameters
46                if !action.params.is_empty() {
47                    let params: Vec<String> = action
48                        .params
49                        .iter()
50                        .map(|p| format!("{} ({})", p.name, format_type(&p.ty)))
51                        .collect();
52                    out.push_str(&format!("Parameters: {}\n", params.join(", ")));
53                }
54
55                // Preconditions
56                if let Some(req) = &action.requires {
57                    out.push_str("\nPreconditions:\n");
58                    for cond in &req.conditions {
59                        out.push_str(&format!("  - {}\n", format_expr(cond)));
60                    }
61                }
62
63                // Postconditions
64                if let Some(ens) = &action.ensures {
65                    out.push_str("\nPostconditions:\n");
66                    for item in &ens.items {
67                        out.push_str(&format!("  - {}\n", format_ensures_item(item)));
68                    }
69                }
70
71                // Properties
72                if let Some(props) = &action.properties {
73                    out.push_str("\nProperties:\n");
74                    for entry in &props.entries {
75                        out.push_str(&format!(
76                            "  - {}: {}\n",
77                            entry.key,
78                            format_prop_value(&entry.value)
79                        ));
80                    }
81                }
82
83                out.push('\n');
84            }
85            ast::TopLevelItem::Invariant(inv) => {
86                out.push_str(&format!("### Invariant: {}\n", inv.name));
87                out.push_str(&format!("  {}\n\n", format_expr(&inv.body)));
88            }
89            ast::TopLevelItem::EdgeCases(ec) => {
90                out.push_str("### Edge Cases\n");
91                for rule in &ec.rules {
92                    let args: Vec<String> = rule
93                        .action
94                        .args
95                        .iter()
96                        .map(|a| match a {
97                            ast::CallArg::Named { key, value, .. } => {
98                                format!("{}: {}", key, format_expr(value))
99                            }
100                            ast::CallArg::Positional(e) => format_expr(e),
101                        })
102                        .collect();
103                    out.push_str(&format!(
104                        "  - when {} => {}({})\n",
105                        format_expr(&rule.condition),
106                        rule.action.name,
107                        args.join(", "),
108                    ));
109                }
110                out.push('\n');
111            }
112            ast::TopLevelItem::Entity(_) | ast::TopLevelItem::Test(_) => {
113                // Entities are fully covered by the skeleton code; tests are not relevant
114            }
115        }
116    }
117
118    out
119}
120
121/// Format a type for human-readable display.
122fn format_type(ty: &ast::TypeExpr) -> String {
123    intent_render::format_type(ty)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn parse(src: &str) -> ast::File {
131        intent_parser::parse_file(src).expect("parse failed")
132    }
133
134    #[test]
135    fn test_build_context_has_all_parts() {
136        let src = "module Test\n\nentity Foo {\n  id: UUID\n  name: String\n}\n\naction CreateFoo {\n  name: String\n\n  requires {\n    name != \"\"\n  }\n\n  ensures {\n    exists f: Foo => f.name == name\n  }\n}\n";
137        let ast = parse(src);
138        let ctx = build_context(&ast, Language::Rust);
139
140        assert!(ctx.spec_source.contains("module Test"));
141        assert!(ctx.skeleton.contains("struct Foo"));
142        assert!(ctx.contracts.contains("Action: CreateFoo"));
143        assert!(ctx.contracts.contains("name != \"\""));
144        // No test blocks in this spec, so harness is empty
145        assert!(ctx.test_harness.is_empty());
146    }
147
148    #[test]
149    fn test_build_context_includes_test_harness() {
150        let src = "module Test\n\nentity Foo {\n  id: UUID\n}\n\naction CreateFoo {\n  name: String\n}\n\ntest \"creates foo\" {\n  given { n = \"hello\" }\n  when CreateFoo { name: n }\n  then fails\n}\n";
151        let ast = parse(src);
152        let ctx = build_context(&ast, Language::Rust);
153
154        assert!(ctx.test_harness.contains("mod contract_tests"));
155        assert!(ctx.test_harness.contains("test_creates_foo"));
156    }
157
158    #[test]
159    fn test_contracts_summary_includes_invariant() {
160        let src = "module Test\n\nentity Acc {\n  balance: Int\n}\n\ninvariant NoNeg {\n  forall a: Acc => a.balance >= 0\n}\n";
161        let ast = parse(src);
162        let summary = build_contracts_summary(&ast);
163
164        assert!(summary.contains("Invariant: NoNeg"));
165        assert!(summary.contains("forall a: Acc => a.balance >= 0"));
166    }
167
168    #[test]
169    fn test_contracts_summary_includes_edge_cases() {
170        let src = "module Test\n\nentity Acc {\n  id: UUID\n}\n\nedge_cases {\n  when amount > 10000 => require_approval(level: \"manager\")\n}\n";
171        let ast = parse(src);
172        let summary = build_contracts_summary(&ast);
173
174        assert!(summary.contains("Edge Cases"));
175        assert!(summary.contains("require_approval"));
176    }
177
178    #[test]
179    fn test_contracts_summary_includes_properties() {
180        let src = "module Test\n\nentity X {\n  id: UUID\n}\n\naction DoThing {\n  x: X\n\n  properties {\n    idempotent: true\n    atomic: true\n  }\n}\n";
181        let ast = parse(src);
182        let summary = build_contracts_summary(&ast);
183
184        assert!(summary.contains("idempotent: true"));
185        assert!(summary.contains("atomic: true"));
186    }
187}