Skip to main content

pecto_core/
context_export.rs

1use crate::model::ProjectSpec;
2
3/// Generate a compact, LLM-optimized context string from a ProjectSpec.
4/// Designed to fit in ~4K tokens while capturing the essential behavior.
5pub fn to_context(spec: &ProjectSpec) -> String {
6    let mut out = String::new();
7
8    // Header
9    out.push_str(&format!(
10        "Project: {} ({} files, {} capabilities)\n",
11        spec.name,
12        spec.files_analyzed,
13        spec.capabilities.len()
14    ));
15
16    // Domains summary
17    if !spec.domains.is_empty() {
18        let domain_names: Vec<&str> = spec.domains.iter().map(|d| d.name.as_str()).collect();
19        out.push_str(&format!("Domains: {}\n", domain_names.join(", ")));
20    }
21
22    // Dependencies summary
23    if !spec.dependencies.is_empty() {
24        out.push_str(&format!(
25            "Dependencies: {} edges\n",
26            spec.dependencies.len()
27        ));
28    }
29
30    out.push('\n');
31
32    // Group by domain if available, otherwise flat list
33    if !spec.domains.is_empty() {
34        for domain in &spec.domains {
35            out.push_str(&format!("## {}\n", domain.name));
36
37            for cap_name in &domain.capabilities {
38                if let Some(cap) = spec.capabilities.iter().find(|c| &c.name == cap_name) {
39                    format_capability(&mut out, cap);
40                }
41            }
42
43            if !domain.external_dependencies.is_empty() {
44                out.push_str(&format!(
45                    "  Depends on: {}\n",
46                    domain.external_dependencies.join(", ")
47                ));
48            }
49            out.push('\n');
50        }
51
52        // Capabilities not in any domain
53        let domain_caps: std::collections::HashSet<&str> = spec
54            .domains
55            .iter()
56            .flat_map(|d| d.capabilities.iter().map(|c| c.as_str()))
57            .collect();
58
59        let orphans: Vec<_> = spec
60            .capabilities
61            .iter()
62            .filter(|c| !domain_caps.contains(c.name.as_str()))
63            .collect();
64
65        if !orphans.is_empty() {
66            out.push_str("## Other\n");
67            for cap in orphans {
68                format_capability(&mut out, cap);
69            }
70            out.push('\n');
71        }
72    } else {
73        for cap in &spec.capabilities {
74            format_capability(&mut out, cap);
75        }
76    }
77
78    // Key dependencies
79    if !spec.dependencies.is_empty() {
80        out.push_str("## Dependencies\n");
81        for dep in &spec.dependencies {
82            out.push_str(&format!("  {} → {} ({:?})\n", dep.from, dep.to, dep.kind));
83        }
84    }
85
86    out
87}
88
89fn format_capability(out: &mut String, cap: &crate::model::Capability) {
90    if !cap.endpoints.is_empty() {
91        for ep in &cap.endpoints {
92            out.push_str(&format!("  {:?} {}", ep.method, ep.path));
93            if let Some(sec) = &ep.security
94                && sec.authentication.is_some()
95            {
96                out.push_str(" [auth]");
97            }
98            out.push('\n');
99
100            // Validation summary
101            if !ep.validation.is_empty() {
102                let fields: Vec<&str> = ep.validation.iter().map(|v| v.field.as_str()).collect();
103                out.push_str(&format!("    validates: {}\n", fields.join(", ")));
104            }
105
106            // Behaviors beyond success
107            for b in &ep.behaviors {
108                if b.name != "success" {
109                    out.push_str(&format!("    {} → {}\n", b.name, b.returns.status));
110                }
111            }
112        }
113    }
114
115    if !cap.entities.is_empty() {
116        for entity in &cap.entities {
117            let field_names: Vec<&str> = entity.fields.iter().map(|f| f.name.as_str()).collect();
118            out.push_str(&format!(
119                "  Entity {} (table: {}) [{}]\n",
120                entity.name,
121                entity.table,
122                field_names.join(", ")
123            ));
124        }
125    }
126
127    if !cap.operations.is_empty() {
128        for op in &cap.operations {
129            out.push_str(&format!("  {}", op.name));
130            if let Some(tx) = &op.transaction {
131                out.push_str(&format!(" [tx:{}]", tx));
132            }
133            let effects: Vec<String> = op
134                .behaviors
135                .iter()
136                .flat_map(|b| &b.side_effects)
137                .map(format_side_effect)
138                .collect();
139            if !effects.is_empty() {
140                out.push_str(&format!(" → {}", effects.join(", ")));
141            }
142            out.push('\n');
143        }
144    }
145
146    if !cap.scheduled_tasks.is_empty() {
147        for task in &cap.scheduled_tasks {
148            out.push_str(&format!("  Scheduled: {} ({})\n", task.name, task.schedule));
149        }
150    }
151}
152
153fn format_side_effect(effect: &crate::model::SideEffect) -> String {
154    match effect {
155        crate::model::SideEffect::DbInsert { table } => format!("insert:{}", table),
156        crate::model::SideEffect::DbUpdate { description } => format!("update:{}", description),
157        crate::model::SideEffect::Event { name } => format!("event:{}", name),
158        crate::model::SideEffect::ServiceCall { target } => format!("call:{}", target),
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::model::*;
166
167    #[test]
168    fn test_context_export_basic() {
169        let mut spec = ProjectSpec::new("my-app");
170        spec.files_analyzed = 50;
171
172        let mut cap = Capability::new("user-controller", "UserController.java");
173        cap.endpoints.push(Endpoint {
174            method: HttpMethod::Get,
175            path: "/api/users".to_string(),
176            input: None,
177            validation: Vec::new(),
178            behaviors: vec![Behavior {
179                name: "success".to_string(),
180                condition: None,
181                returns: ResponseSpec {
182                    status: 200,
183                    body: None,
184                },
185                side_effects: Vec::new(),
186            }],
187            security: None,
188        });
189        spec.capabilities.push(cap);
190
191        let ctx = to_context(&spec);
192        assert!(ctx.contains("my-app"));
193        assert!(ctx.contains("50 files"));
194        // HttpMethod uses Debug format (Get, Post, etc.)
195        assert!(
196            ctx.contains("/api/users"),
197            "Should contain endpoint path. Got: {}",
198            ctx
199        );
200    }
201
202    #[test]
203    fn test_context_export_with_domains() {
204        let mut spec = ProjectSpec::new("test");
205        spec.files_analyzed = 10;
206
207        spec.capabilities
208            .push(Capability::new("user-service", "UserService.java"));
209        spec.domains.push(Domain {
210            name: "user".to_string(),
211            capabilities: vec!["user-service".to_string()],
212            external_dependencies: vec![],
213        });
214
215        let ctx = to_context(&spec);
216        assert!(ctx.contains("Domains: user"));
217        assert!(ctx.contains("## user"));
218    }
219}