@@ -154,15 +154,235 @@ pub fn save_to_context(target_path: &Path, threat_model: &str) {
154154 . expect ( "Failed to save threat model to context" ) ;
155155}
156156
157- /// Load architecture summary from AnalysisContext or regenerate via CodebaseUnderstanding .
157+ /// Generate architecture summary by statically inspecting the codebase .
158158#[ cfg_attr( test, visibility:: make( pub ) ) ]
159- pub fn load_or_generate_architecture ( _target_path : & Path , context : & AnalysisContext ) -> String {
159+ pub fn generate_architecture_static ( target_path : & Path ) -> String {
160+ let mut summary = String :: new ( ) ;
161+
162+ // Get project type
163+ let project_type = detect_project_type ( target_path) ;
164+ summary. push_str ( "=== ARCHITECTURAL SUMMARY ===\n " ) ;
165+ summary. push_str ( & format ! ( "Project type: {}\n " , project_type) ) ;
166+
167+ // Index project files
168+ let file_index = crate :: indexer:: FileIndex :: index_project (
169+ target_path. to_str ( ) . unwrap_or ( "." ) ,
170+ & [
171+ "rust" . to_string ( ) ,
172+ "typescript" . to_string ( ) ,
173+ "javascript" . to_string ( ) ,
174+ "python" . to_string ( ) ,
175+ ] ,
176+ 1024 * 1024 , // 1MB max file size
177+ & [
178+ "target/" . to_string ( ) ,
179+ "node_modules/" . to_string ( ) ,
180+ ".git/" . to_string ( ) ,
181+ ] ,
182+ ) ;
183+
184+ let file_count = file_index. as_ref ( ) . map ( |i| i. files . len ( ) ) . unwrap_or ( 0 ) ;
185+ summary. push_str ( & format ! ( "Total files: {}\n \n " , file_count) ) ;
186+
187+ // Detect components by scanning file contents
188+ let ( has_http, has_db, has_filesys, has_auth) = detect_components ( target_path, file_index) ;
189+
190+ summary. push_str ( "Components detected:\n " ) ;
191+ if has_http {
192+ summary. push_str ( "- HTTP API: yes\n " ) ;
193+ } else {
194+ summary. push_str ( "- No web framework\n " ) ;
195+ }
196+ if has_db {
197+ summary. push_str ( "- database: yes\n " ) ;
198+ } else {
199+ summary. push_str ( "- No database\n " ) ;
200+ }
201+ if has_filesys {
202+ summary. push_str ( "- file system: yes\n " ) ;
203+ } else {
204+ summary. push_str ( "- No file system\n " ) ;
205+ }
206+ if has_auth {
207+ summary. push_str ( "- Authentication: yes\n " ) ;
208+ } else {
209+ summary. push_str ( "- No auth\n " ) ;
210+ }
211+ summary. push ( '\n' ) ;
212+
213+ // Entry points
214+ summary. push_str ( "Entry points:\n " ) ;
215+ if has_http {
216+ summary. push_str ( "- HTTP endpoints\n " ) ;
217+ }
218+ if file_count > 0 {
219+ summary. push_str ( "- Source code entry (main.rs / index.js / etc.)\n " ) ;
220+ }
221+
222+ // Data stores
223+ summary. push_str ( "\n Data stores:\n " ) ;
224+ if !has_db {
225+ summary. push_str ( "- None detected\n " ) ;
226+ }
227+
228+ summary
229+ }
230+
231+ /// Detect components by scanning indexed files for keywords.
232+ fn detect_components (
233+ target_path : & Path ,
234+ file_index : Result < crate :: indexer:: FileIndex , std:: io:: Error > ,
235+ ) -> ( bool , bool , bool , bool ) {
236+ let mut has_http = false ;
237+ let mut has_db = false ;
238+ let mut has_filesys = false ;
239+ let mut has_auth = false ;
240+
241+ let http_keywords = [
242+ "HTTP" ,
243+ "endpoint" ,
244+ "router" ,
245+ "axum" ,
246+ "actix" ,
247+ "warp" ,
248+ "rocket" ,
249+ "tower" ,
250+ "express" ,
251+ "flask" ,
252+ "django" ,
253+ "fastapi" ,
254+ "spring" ,
255+ "gin" ,
256+ "echo" ,
257+ "http::" ,
258+ "actix_web" ,
259+ "axum::" ,
260+ ] ;
261+ let db_keywords = [
262+ "sqlite" ,
263+ "postgres" ,
264+ "mysql" ,
265+ "mongodb" ,
266+ "redis" ,
267+ "database" ,
268+ "data store" ,
269+ "Repository" ,
270+ "Entity" ,
271+ "migration" ,
272+ "sqlx" ,
273+ "diesel" ,
274+ "orm" ,
275+ "prisma" ,
276+ ] ;
277+ let fs_keywords = [
278+ "fs::read" ,
279+ "fs::write" ,
280+ "File::open" ,
281+ "File::create" ,
282+ "tempfile" ,
283+ "file upload" ,
284+ "filesystem" ,
285+ "std::fs" ,
286+ "read_to_string" ,
287+ ] ;
288+ let auth_keywords = [
289+ "auth" ,
290+ "session" ,
291+ "token" ,
292+ "jwt" ,
293+ "password" ,
294+ "credential" ,
295+ "oauth" ,
296+ "bearer" ,
297+ "authentication" ,
298+ "authorization" ,
299+ ] ;
300+
301+ // Get files from index or fallback to scanning common source files
302+ let files_to_scan = match file_index {
303+ Ok ( index) => index. files . into_iter ( ) . take ( 100 ) . collect ( ) ,
304+ Err ( _) => {
305+ // Fallback: scan common source files directly
306+ let mut files = Vec :: new ( ) ;
307+ let src_path = target_path. join ( "src" ) ;
308+ if src_path. exists ( ) {
309+ if let Ok ( entries) = std:: fs:: read_dir ( & src_path) {
310+ for entry in entries. flatten ( ) . take ( 100 ) {
311+ if entry. path ( ) . extension ( ) . and_then ( |e| e. to_str ( ) ) == Some ( "rs" ) {
312+ files. push ( crate :: indexer:: FileInfo {
313+ path : entry. path ( ) ,
314+ size : 0 ,
315+ language : "rust" . to_string ( ) ,
316+ hash : None ,
317+ } ) ;
318+ }
319+ }
320+ }
321+ }
322+ files
323+ }
324+ } ;
325+
326+ for file_info in files_to_scan {
327+ if let Ok ( content) = std:: fs:: read_to_string ( & file_info. path ) {
328+ let lower = content. to_lowercase ( ) ;
329+
330+ if !has_http
331+ && http_keywords
332+ . iter ( )
333+ . any ( |k| lower. contains ( & k. to_lowercase ( ) ) )
334+ {
335+ has_http = true ;
336+ }
337+ if !has_db
338+ && db_keywords
339+ . iter ( )
340+ . any ( |k| lower. contains ( & k. to_lowercase ( ) ) )
341+ {
342+ has_db = true ;
343+ }
344+ if !has_filesys
345+ && fs_keywords
346+ . iter ( )
347+ . any ( |k| lower. contains ( & k. to_lowercase ( ) ) )
348+ {
349+ has_filesys = true ;
350+ }
351+ if !has_auth
352+ && auth_keywords
353+ . iter ( )
354+ . any ( |k| lower. contains ( & k. to_lowercase ( ) ) )
355+ {
356+ has_auth = true ;
357+ }
358+
359+ // Early exit if all detected
360+ if has_http && has_db && has_filesys && has_auth {
361+ break ;
362+ }
363+ }
364+ }
365+
366+ ( has_http, has_db, has_filesys, has_auth)
367+ }
368+
369+ /// Load architecture summary from AnalysisContext or regenerate via static codebase analysis.
370+ #[ cfg_attr( test, visibility:: make( pub ) ) ]
371+ pub fn load_or_generate_architecture ( target_path : & Path , context : & AnalysisContext ) -> String {
160372 if !context. architecture_summary . is_empty ( ) {
161373 tracing:: debug!( "Using existing architecture summary from context" ) ;
162374 context. architecture_summary . clone ( )
163375 } else {
164- tracing:: warn!( "No architecture summary in context, using empty architecture" ) ;
165- "No architecture summary available" . to_string ( )
376+ tracing:: info!( "Generating architecture summary via static analysis" ) ;
377+ let generated = generate_architecture_static ( target_path) ;
378+ // Persist for reuse by later phases
379+ let mut ctx =
380+ AnalysisContext :: load ( target_path) . unwrap_or_else ( |_| AnalysisContext :: default ( ) ) ;
381+ ctx. architecture_summary = generated. clone ( ) ;
382+ if let Err ( e) = ctx. save ( target_path) {
383+ tracing:: warn!( "Failed to persist architecture summary: {e}" ) ;
384+ }
385+ generated
166386 }
167387}
168388
@@ -289,8 +509,17 @@ mod tests {
289509 use tempfile:: tempdir;
290510 let tmp = tempdir ( ) . unwrap ( ) ;
291511
512+ // Create a simple Rust file for detection
513+ let src_dir = tmp. path ( ) . join ( "src" ) ;
514+ std:: fs:: create_dir_all ( & src_dir) . unwrap ( ) ;
515+ std:: fs:: write ( src_dir. join ( "main.rs" ) , "fn main() {}" ) . unwrap ( ) ;
516+
292517 let ctx = AnalysisContext :: default ( ) ;
293518 let arch = load_or_generate_architecture ( tmp. path ( ) , & ctx) ;
294- assert_eq ! ( arch, "No architecture summary available" ) ;
519+
520+ // Should generate architecture summary, not return placeholder
521+ assert ! ( arch. contains( "ARCHITECTURAL SUMMARY" ) ) ;
522+ assert ! ( arch. contains( "Project type" ) ) ;
523+ assert_ne ! ( arch, "No architecture summary available" ) ;
295524 }
296525}
0 commit comments