Skip to content

Commit fa52f22

Browse files
committed
refactor(scheduler): update optimizer to log warnings on budget exceedance
1 parent ce33cbb commit fa52f22

4 files changed

Lines changed: 56 additions & 36 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 1b13f4315c8046b31b368706360e4310fb1b663eab2c6f8ee4bbb73f08b3b083 # shrinks to files = [ParsedFile { path: "src/a.rs", language: Rust, source: "", strategies_data: {Full: StrategyData { content: "", token_count: 1692 }, NoTests: StrategyData { content: "", token_count: 1692 }, Summary: StrategyData { content: "", token_count: 1692 }} }], max_tokens = 1324

crates/ast-doc-core/src/scheduler/optimizer.rs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
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

811
use globset::{Glob, GlobSet, GlobSetBuilder};
12+
use tracing::warn;
913

1014
use 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:?}");

crates/ast-doc-core/tests/bdd.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -653,13 +653,16 @@ fn then_no_txt_files(world: &mut AstDocWorld) {
653653
assert!(!result.output.contains("notes.txt"), "output should not contain notes.txt");
654654
}
655655

656-
#[then(expr = "ast-doc should report a budget exceeded error")]
657-
fn then_budget_error(world: &mut AstDocWorld) {
656+
#[then(expr = "ast-doc should succeed with a budget warning")]
657+
fn then_budget_warning(world: &mut AstDocWorld) {
658+
// With the new behavior, budget exceeded is now a warning, not an error.
659+
// The pipeline should succeed even when budget cannot be met.
658660
assert!(
659-
world.error.as_ref().expect("should have an error").contains("Budget exceeded"),
660-
"error should be BudgetExceeded, got: {:?}",
661+
world.pipeline_result.is_some(),
662+
"pipeline should succeed even when budget is exceeded, got error: {:?}",
661663
world.error
662664
);
665+
// The output may exceed the budget, but the pipeline completes successfully.
663666
}
664667

665668
#[then(expr = "the error message should suggest increasing --max-tokens or using --no-git")]

features/ast-doc.feature

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ Feature: ast-doc llms.txt Generation
4545
When I run ast-doc with exclude pattern "*.txt"
4646
Then text files should not appear in the output
4747

48-
Scenario: Report error when budget is insufficient
48+
Scenario: Succeed with warning when budget is insufficient
4949
Given a project directory with source files totalling 50000 tokens
5050
And a git diff totalling 5000 tokens
5151
When I run ast-doc with max-tokens set to 1000
52-
Then ast-doc should report a budget exceeded error
53-
And the error message should suggest increasing --max-tokens or using --no-git
52+
Then ast-doc should succeed with a budget warning

0 commit comments

Comments
 (0)