33//! Pure mathematical optimizer: selects from pre-computed `strategies_data`
44//! entries per `ParsedFile`. No string manipulation, just token arithmetic.
55
6- use std:: { collections:: HashMap , path:: Path } ;
6+ use std:: {
7+ collections:: { HashMap , HashSet } ,
8+ path:: Path ,
9+ } ;
710
811use globset:: { Glob , GlobSet , GlobSetBuilder } ;
12+ use tracing:: warn;
913
1014use crate :: {
1115 config:: { AstDocConfig , OutputStrategy } ,
@@ -61,10 +65,12 @@ pub fn optimize(
6165 let mut total_tokens = compute_total ( parsed, & assignments) ;
6266
6367 // 4. Degradation loop
68+ let mut stuck_files: HashSet < usize > = HashSet :: new ( ) ;
6469 while total_tokens > remaining_budget {
65- // Collect degradable files
70+ // Collect degradable files (excluding stuck files)
6671 let mut degradable: Vec < ( usize , OutputStrategy , usize ) > = assignments
6772 . iter ( )
73+ . filter ( |( i, _) | !stuck_files. contains ( i) )
6874 . filter ( |( _, strategy) | strategy. degrade ( ) . is_some ( ) )
6975 . filter ( |( i, _) | !is_core ( & core_set, & parsed[ * i] . path ) )
7076 . map ( |( i, strategy) | {
@@ -74,11 +80,12 @@ pub fn optimize(
7480 . collect ( ) ;
7581
7682 if degradable. is_empty ( ) {
77- return Err ( AstDocError :: BudgetExceeded {
78- message : format ! (
79- "All files at minimum strategy but still over budget: {total_tokens} > {remaining_budget}"
80- ) ,
81- } ) ;
83+ warn ! (
84+ total_tokens = total_tokens,
85+ remaining_budget = remaining_budget,
86+ "Budget exceeded: All files at minimum strategy but still over budget. Continuing with minimum strategies."
87+ ) ;
88+ break ;
8289 }
8390
8491 // Sort: files with tests first (NoTests saves more), then by token count desc
@@ -106,13 +113,9 @@ pub fn optimize(
106113 if let Some ( min_strategy) = assignments[ idx] . 1 . degrade ( ) {
107114 assignments[ idx] = ( idx, min_strategy) ;
108115 } else {
109- // Already at minimum and no reduction — this shouldn't happen
110- // but guard against infinite loops
111- return Err ( AstDocError :: BudgetExceeded {
112- message : format ! (
113- "No token reduction possible for file at index {idx}, stuck at {total_tokens} tokens"
114- ) ,
115- } ) ;
116+ // Already at minimum and no reduction — mark as stuck
117+ // This handles very large files that can't be reduced further
118+ stuck_files. insert ( idx) ;
116119 }
117120 }
118121
@@ -317,13 +320,17 @@ mod tests {
317320
318321 #[ test]
319322 fn test_all_summary_still_over_budget ( ) {
320- // Even at Summary, still over budget → BudgetExceeded
323+ // Even at Summary, still over budget → now succeeds with warning
321324 let files =
322325 vec ! [ make_parsed( "src/a.rs" , 400 , 300 , 200 ) , make_parsed( "src/b.rs" , 400 , 300 , 200 ) ] ;
323326 // Summary total = 400, budget = 300
324327 let config = make_config ( 300 , vec ! [ ] ) ;
325- let result = optimize ( & files, & config, 0 ) ;
326- assert ! ( matches!( result, Err ( AstDocError :: BudgetExceeded { .. } ) ) ) ;
328+ let result = optimize ( & files, & config, 0 ) . unwrap ( ) ;
329+ // Should succeed with minimum strategies applied
330+ assert_eq ! ( result. files. len( ) , 2 ) ;
331+ for f in & result. files {
332+ assert_eq ! ( f. strategy, OutputStrategy :: Summary ) ;
333+ }
327334 }
328335
329336 #[ test]
@@ -352,15 +359,20 @@ mod tests {
352359 }
353360
354361 #[ test]
355- fn test_all_core_over_budget_errors ( ) {
356- // All files are core, over budget → error
362+ fn test_all_core_over_budget_succeeds_with_warning ( ) {
363+ // All files are core, over budget → succeeds with warning (core files can't be degraded)
357364 let files = vec ! [
358365 make_parsed( "src/lib.rs" , 500 , 400 , 300 ) ,
359366 make_parsed( "src/core.rs" , 500 , 400 , 300 ) ,
360367 ] ;
361368 let config = make_config ( 500 , vec ! [ "**/*.rs" ] ) ;
362- let result = optimize ( & files, & config, 0 ) ;
363- assert ! ( matches!( result, Err ( AstDocError :: BudgetExceeded { .. } ) ) ) ;
369+ let result = optimize ( & files, & config, 0 ) . unwrap ( ) ;
370+ // Should succeed with core files at Full strategy
371+ assert_eq ! ( result. files. len( ) , 2 ) ;
372+ for f in & result. files {
373+ assert_eq ! ( f. strategy, OutputStrategy :: Full ) ;
374+ }
375+ assert_eq ! ( result. total_tokens, 1000 ) ; // 500 + 500
364376 }
365377
366378 #[ test]
@@ -425,16 +437,15 @@ mod tests {
425437 ) {
426438 let config = make_config( max_tokens, vec![ ] ) ;
427439 match optimize( & files, & config, 0 ) {
428- Ok ( result) => {
429- prop_assert!(
430- result. total_tokens <= max_tokens,
431- "total_tokens ({}) > max_tokens ({})" ,
432- result. total_tokens,
433- max_tokens,
434- ) ;
440+ Ok ( _result) => {
441+ // With the new behavior, we always return Ok but may exceed budget
442+ // when all files are at minimum strategy. The important thing is
443+ // that we don't panic or return an error for this case.
444+ // If total_tokens > max_tokens, it means we couldn't fit within budget
445+ // even at minimum strategies, which is acceptable (just logs a warning).
435446 }
436447 Err ( AstDocError :: BudgetExceeded { .. } ) => {
437- // Acceptable: even minimum strategies exceed budget
448+ // Still acceptable for base overhead exceeding budget
438449 }
439450 Err ( e) => {
440451 panic!( "unexpected error: {e:?}" ) ;
0 commit comments