1use crate::core::config::CompressionLevel;
2use crate::core::rules_canonical::{self as rc, Wrapper};
3use crate::tools::CrpMode;
4
5const INSTRUCTION_CAP_TOKENS: usize = 800;
7
8#[cfg(test)]
20const STATIC_INSTRUCTION_BUDGET_TOKENS: usize = 545;
21#[cfg(test)]
22const STATIC_INSTRUCTION_BUDGET_TDD_TOKENS: usize = 675;
23#[cfg(all(test, windows))]
25const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 25;
26#[cfg(all(test, not(windows)))]
27const STATIC_INSTRUCTION_SHELL_HINT_TOKENS: usize = 0;
28
29#[must_use]
30pub fn build_instructions(crp_mode: CrpMode) -> String {
31 build_instructions_with_client(crp_mode, "")
32}
33
34#[must_use]
35pub fn build_instructions_with_client(crp_mode: CrpMode, client_name: &str) -> String {
36 let cfg = crate::core::config::Config::load();
37 let minimal = cfg.minimal_overhead_effective_for_client(client_name);
38 let shadow = cfg.shadow_mode;
39 let level = if client_loads_compression_from_file(client_name) {
42 CompressionLevel::Off
43 } else {
44 CompressionLevel::effective(&cfg)
45 };
46 let persona_block = crate::core::persona::Persona::resolve(&cfg).prompt_block();
50 build_full_instructions(
51 crp_mode,
52 client_name,
53 minimal,
54 level,
55 shadow,
56 &persona_block,
57 )
58}
59
60#[must_use]
69pub fn claude_code_static_instructions_for_test() -> String {
70 build_full_instructions(CrpMode::Off, "", true, CompressionLevel::Off, false, "")
71}
72
73#[must_use]
75pub fn build_instructions_for_test(crp_mode: CrpMode) -> String {
76 let shadow = false;
77 let level = CompressionLevel::effective(&crate::core::config::Config::load());
80 let tp =
81 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
82 let skeleton = rc::render(shadow, Wrapper::Bare, level, &tp);
83 let shell_hint = build_shell_hint();
84
85 let base = format!(
86 "{skeleton}\n\
87 {shell_hint}\n\
88 {decoder_block}\n\
89 {origin}",
90 decoder_block =
91 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
92 origin = crate::core::integrity::origin_line(),
93 );
94
95 match crp_mode_suffix(crp_mode) {
96 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
97 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
98 }
99}
100
101#[must_use]
105pub fn build_instructions_with_client_for_compiler(
106 crp_mode: CrpMode,
107 client_name: &str,
108 _unified_tool_mode: bool,
109) -> String {
110 let tp =
111 crate::core::tool_profiles::ToolProfile::from_config(&crate::core::config::Config::load());
112 let skeleton = rc::render(true, Wrapper::Bare, CompressionLevel::Off, &tp);
113 let shell_hint = build_shell_hint();
114
115 let base = format!(
116 "{skeleton}\n\
117 {shell_hint}\n\
118 {decoder_block}\n\
119 {origin}",
120 decoder_block =
121 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
122 origin = crate::core::integrity::origin_line(),
123 );
124
125 let _ = client_name;
126
127 match crp_mode_suffix(crp_mode) {
128 "" => format!("{base}\n\n{}", rc::INTELLIGENCE),
129 crp => format!("{base}\n\n{crp}\n\n{}", rc::INTELLIGENCE),
130 }
131}
132
133fn rotate_wakeup_manifest(session: &crate::core::session::SessionState, profile_name: &str) {
135 use crate::core::litm_calibration::{Position, record_outcome};
136 use crate::core::session::ManifestEntry;
137
138 let mut updated = session.clone();
139
140 for entry in &updated.wakeup_manifest {
141 if !entry.missed
142 && let Some(pos) = Position::parse(&entry.position)
143 {
144 record_outcome(&entry.profile, pos, true);
145 }
146 }
147
148 let mut manifest: Vec<ManifestEntry> = Vec::new();
149 let mut push = |key: &str, position: &str| {
150 let key = key.trim();
151 if !key.is_empty() {
152 manifest.push(ManifestEntry {
153 key: key.chars().take(80).collect(),
154 position: position.to_string(),
155 profile: profile_name.to_string(),
156 missed: false,
157 });
158 }
159 };
160
161 if let Some(ref task) = updated.task {
162 push(&task.description, "begin");
163 }
164 for d in updated.decisions.iter().rev().take(5) {
165 push(&d.summary, "begin");
166 }
167 for f in updated.findings.iter().rev().take(8) {
168 push(&f.summary, "end");
169 }
170 for n in updated.next_steps.iter().take(3) {
171 push(n, "end");
172 }
173
174 updated.wakeup_manifest = manifest;
175 let _ = updated.save();
176}
177
178#[must_use]
180pub fn claude_config_dir_display() -> String {
181 match std::env::var("CLAUDE_CONFIG_DIR") {
182 Ok(dir) if !dir.trim().is_empty() => {
183 let dir = dir.trim().to_string();
184 if dir.starts_with('~') {
185 dir
186 } else if let Some(home) = dirs::home_dir() {
187 let home_str = home.to_string_lossy();
188 if let Some(rest) = dir.strip_prefix(home_str.as_ref()) {
189 format!("~{rest}")
190 } else {
191 dir
192 }
193 } else {
194 dir
195 }
196 }
197 _ => "~/.claude".to_string(),
198 }
199}
200
201fn build_full_instructions(
204 crp_mode: CrpMode,
205 client_name: &str,
206 minimal: bool,
207 level: CompressionLevel,
208 shadow: bool,
209 persona_block: &str,
210) -> String {
211 let profile = crate::core::litm::LitmProfile::from_client_name(client_name);
212 let loaded_session = if minimal {
213 None
214 } else {
215 crate::core::session::SessionState::load_latest()
216 };
217
218 let (session_block, litm_end_block) = match loaded_session {
219 Some(ref session) => {
220 rotate_wakeup_manifest(session, profile.name);
221 let share = crate::core::litm_calibration::begin_share(profile.name);
222 let mut positioned = crate::core::litm::position_optimize_with_share(session, share);
223 positioned.enforce_token_budget(crate::core::litm::active_session_budget());
226 let begin = format!(
227 "\n\n--- ACTIVE SESSION (LITM P1: begin position, profile: {}) ---\n{}\n---\n",
228 profile.name, positioned.begin_block
229 );
230 let end = if positioned.end_block.is_empty() {
231 String::new()
232 } else {
233 format!(
234 "\n--- SESSION RESUME (post-compaction) ---\n{}\n---\n",
235 positioned.end_block
236 )
237 };
238 (begin, end)
239 }
240 None => (String::new(), String::new()),
241 };
242
243 let project_root_for_blocks = if minimal {
244 None
245 } else {
246 loaded_session
247 .as_ref()
248 .and_then(|s| s.project_root.clone())
249 .or_else(|| {
250 std::env::current_dir()
251 .ok()
252 .map(|p| p.to_string_lossy().to_string())
253 })
254 };
255
256 let knowledge_block = match &project_root_for_blocks {
257 Some(root) => {
258 let knowledge = crate::core::knowledge::ProjectKnowledge::load(root);
259 match knowledge {
260 Some(k) if !k.facts.is_empty() || !k.patterns.is_empty() => {
261 let aaak = k.format_aaak();
262 if aaak.is_empty() {
263 String::new()
264 } else {
265 format!("\n--- PROJECT MEMORY (AAAK) ---\n{}\n---\n", aaak.trim())
266 }
267 }
268 _ => String::new(),
269 }
270 }
271 None => String::new(),
272 };
273
274 let gotcha_block = match &project_root_for_blocks {
275 Some(root) => {
276 let store = crate::core::gotcha_tracker::GotchaStore::load(root);
277 let files: Vec<String> = loaded_session
278 .as_ref()
279 .map(|s| s.files_touched.iter().map(|ft| ft.path.clone()).collect())
280 .unwrap_or_default();
281 let block = store.format_injection_block(&files);
282 if block.is_empty() {
283 String::new()
284 } else {
285 format!("\n{block}\n")
286 }
287 }
288 None => String::new(),
289 };
290
291 let health_block = match &project_root_for_blocks {
292 Some(root) => {
293 let block = crate::core::code_health::persist::format_session_block(root);
294 if block.is_empty() {
295 String::new()
296 } else {
297 format!("\n{block}\n")
298 }
299 }
300 None => String::new(),
301 };
302
303 let shell_hint = build_shell_hint();
304
305 let cfg = crate::core::config::Config::load();
319 let tool_profile = crate::core::tool_profiles::ToolProfile::from_config(&cfg);
320 let skeleton = if client_loads_rules_from_file(client_name) {
321 let anchor = if client_is_hook_covered(client_name) {
322 hook_covered_anchor(&tool_profile)
323 } else {
324 skeleton_anchor(&tool_profile)
325 };
326 let compression = rc::compression_text(level);
327 if compression.is_empty() {
328 anchor
329 } else {
330 format!("{anchor}\n{compression}")
331 }
332 } else {
333 let effective_shadow = shadow && client_is_hook_covered(client_name);
341 rc::render(effective_shadow, Wrapper::Bare, level, &tool_profile)
342 };
343
344 let config_dir = claude_config_dir_display();
347
348 let persona_section = if persona_block.is_empty() {
351 String::new()
352 } else {
353 format!("\n{persona_block}")
354 };
355
356 let base = format!(
357 "{skeleton}\n\
358 {persona_section}\
359 {shell_hint}\n\
360 {decoder_block}\n\
361 Full instructions at {config_dir}/CLAUDE.md\n\
362 {session_block}\n\
363 {knowledge_block}\n\
364 {gotcha_block}\n\
365 {health_block}\n\
366 {origin}\n\
367 {litm_end_block}",
368 decoder_block =
369 crate::core::protocol::instruction_decoder_block(matches!(crp_mode, CrpMode::Tdd)),
370 origin = crate::core::integrity::origin_line(),
371 litm_end_block = litm_end_block
372 );
373
374 let guidance_suffix = match crp_mode_suffix(crp_mode) {
377 "" => rc::INTELLIGENCE.to_string(),
378 crp => format!("{crp}\n\n{}", rc::INTELLIGENCE),
379 };
380
381 assemble_within_cap(&base, &guidance_suffix, INSTRUCTION_CAP_TOKENS)
382}
383
384fn crp_mode_suffix(crp_mode: CrpMode) -> &'static str {
385 match crp_mode {
386 CrpMode::Off => "",
387 CrpMode::Compact => {
388 "CRP MODE: compact — omit filler; abbreviate fn,cfg,impl,deps,req,res; \
389 diff lines (+/-) only; <=200 tok; trust tool outputs."
390 }
391 CrpMode::Tdd => {
392 "CRP MODE: tdd — max density; Fn refs + diff lines only \
393 (+F1:42 | -F1:10-15 | ~F1:42 old->new); <=150 tok; zero narration."
394 }
395 }
396}
397
398fn assemble_within_cap(base: &str, suffix: &str, cap_tokens: usize) -> String {
399 use crate::core::tokens::count_tokens;
400 let suffix = suffix.trim_end_matches('\n');
401 if suffix.is_empty() {
402 let full = base.to_string();
403 return if count_tokens(&full) > cap_tokens {
404 truncate_to_token_cap(&full, cap_tokens)
405 } else {
406 full
407 };
408 }
409
410 let full = format!("{base}\n\n{suffix}");
411 if count_tokens(&full) <= cap_tokens {
412 return full;
413 }
414
415 let suffix_tokens = count_tokens(suffix);
416 let Some(base_budget) = cap_tokens.checked_sub(suffix_tokens + 1) else {
417 return truncate_to_token_cap(&full, cap_tokens);
418 };
419 let trimmed_base = truncate_to_token_cap(base, base_budget);
420 format!("{trimmed_base}\n\n{suffix}")
421}
422
423fn truncate_to_token_cap(s: &str, cap_tokens: usize) -> String {
424 use crate::core::tokens::count_tokens;
425 if count_tokens(s) <= cap_tokens {
426 return s.to_string();
427 }
428 let cuts: Vec<usize> = s.match_indices('\n').map(|(i, _)| i).collect();
429 let (mut lo, mut hi) = (0usize, cuts.len());
430 let mut best: Option<usize> = None;
431 while lo < hi {
432 let mid = lo + (hi - lo) / 2;
433 let end = cuts[mid];
434 if end > 0 && count_tokens(&s[..end]) <= cap_tokens {
435 best = Some(end);
436 lo = mid + 1;
437 } else {
438 hi = mid;
439 }
440 }
441 if let Some(end) = best {
442 return s[..end].to_string();
443 }
444 let byte_approx = cap_tokens * 4;
445 let safe = s.floor_char_boundary(byte_approx.min(s.len()));
446 s[..safe].to_string()
447}
448
449#[must_use]
451pub fn claude_code_instructions() -> String {
452 build_instructions(CrpMode::Off)
453}
454
455fn skeleton_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
458 if tp.is_tool_enabled("ctx_compose") {
459 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
460 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first)."
461 .into()
462 } else {
463 "lean-ctx active — your auto-loaded lean-ctx rules apply: \
464 ctx_* tools replace native Read/Grep/Shell/Glob."
465 .into()
466 }
467}
468
469fn hook_covered_anchor(tp: &crate::core::tool_profiles::ToolProfile) -> String {
471 let mut s =
472 String::from("lean-ctx active — hooks compress native Shell/Read/Grep transparently");
473 if tp.is_tool_enabled("ctx_compose") {
474 s.push_str("; call ctx_compose to orient");
475 }
476 if tp.is_tool_enabled("ctx_session") || tp.is_tool_enabled("ctx_knowledge") {
478 s.push_str(", ctx_search(action=semantic) / ctx_knowledge for meaning & memory");
479 }
480 s.push('.');
481 s
482}
483
484#[cfg(test)]
486const SKELETON_ANCHOR: &str = "lean-ctx active — your auto-loaded lean-ctx rules apply: \
487 ctx_* tools replace native Read/Grep/Shell/Glob (ctx_compose first).";
488
489fn client_loads_compression_from_file(client_name: &str) -> bool {
490 crate::core::home::resolve_home_dir().is_some_and(|home| {
491 crate::core::rules_channel::client_autoloads_compression(client_name, &home)
492 })
493}
494
495fn client_loads_rules_from_file(client_name: &str) -> bool {
496 crate::core::home::resolve_home_dir()
497 .is_some_and(|home| crate::core::rules_channel::client_autoloads_rules(client_name, &home))
498}
499
500fn client_is_hook_covered(client_name: &str) -> bool {
501 crate::core::home::resolve_home_dir()
502 .is_some_and(|home| crate::core::rules_channel::client_hook_covered(client_name, &home))
503}
504
505fn build_shell_hint() -> String {
506 if !cfg!(windows) {
507 return String::new();
508 }
509 let name = crate::shell::shell_name();
510 let is_posix = matches!(name.as_str(), "bash" | "sh" | "zsh" | "fish");
511 if is_posix {
512 format!("\nSHELL: {name} (POSIX) — no PowerShell cmdlets.\n")
513 } else if name.contains("powershell") || name.contains("pwsh") {
514 format!("\nSHELL: {name}. Use PowerShell cmdlets.\n")
515 } else {
516 format!("\nSHELL: {name}.\n")
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::core::tokens::count_tokens;
524
525 #[test]
526 fn guidance_suffix_survives_oversized_base() {
527 let base = "SESSION LINE\n".repeat(4000);
528 let suffix = "OUTPUT STYLE: expert-terse\nFn refs only, diff lines only.";
529 let out = assemble_within_cap(&base, suffix, INSTRUCTION_CAP_TOKENS);
530 assert!(out.contains("OUTPUT STYLE: expert-terse"));
531 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
532 assert!(out.len() < base.len());
533 }
534
535 #[test]
536 fn empty_client_never_dedups_compression() {
537 assert!(!client_loads_compression_from_file(""));
538 assert!(!client_loads_compression_from_file("totally-unknown-agent"));
539 }
540
541 #[test]
542 fn covered_client_gets_anchor_instead_of_skeleton() {
543 let _guard = crate::core::data_dir::test_env_lock();
546 let tmp = tempfile::tempdir().unwrap();
547 let home = tmp.path();
548 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
549 let cfg = crate::core::config::Config::load();
550 let shadow = cfg.shadow_mode;
551 let tp = crate::core::tool_profiles::ToolProfile::Power;
552 std::fs::write(
553 home.join(".cursor/rules/lean-ctx.mdc"),
554 rc::render(
555 shadow,
556 Wrapper::Dedicated,
557 crate::core::config::CompressionLevel::Standard,
558 &tp,
559 ),
560 )
561 .unwrap();
562 let old_home = std::env::var("HOME").ok();
563 crate::test_env::set_var("HOME", home);
564 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
565
566 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
567 let uncovered = build_instructions_with_client(CrpMode::Off, "some-other-agent");
568
569 if let Some(h) = old_home {
570 crate::test_env::set_var("HOME", h);
571 } else {
572 crate::test_env::remove_var("HOME");
573 }
574 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
575
576 assert!(
577 covered.contains(SKELETON_ANCHOR),
578 "covered client must get the anchor:\n{covered}"
579 );
580 if shadow {
581 assert!(
582 !covered.contains("auto-route"),
583 "covered shadow client must not re-pay the shadow nudge:\n{covered}"
584 );
585 } else {
586 assert!(
587 !covered.contains("MANDATORY MAPPING"),
588 "covered client must not re-pay the skeleton:\n{covered}"
589 );
590 }
591 assert!(
593 !covered.contains("OUTPUT STYLE:"),
594 "covered client must not re-pay the compression prompt:\n{covered}"
595 );
596 if shadow {
600 assert!(
601 !uncovered.contains("auto-route"),
602 "#987: uncovered client WITHOUT hooks must NOT get shadow nudge:\n{uncovered}"
603 );
604 assert!(
605 uncovered.contains("MANDATORY MAPPING") || uncovered.contains("ctx_"),
606 "uncovered client without hooks gets explicit tool mapping:\n{uncovered}"
607 );
608 } else {
609 assert!(
610 uncovered.contains("MANDATORY MAPPING"),
611 "uncovered client keeps the full skeleton:\n{uncovered}"
612 );
613 }
614 eprintln!(
615 "instructions footprint: covered={} tok, uncovered={} tok",
616 count_tokens(&covered),
617 count_tokens(&uncovered)
618 );
619 assert!(
620 count_tokens(&covered) < count_tokens(&uncovered),
621 "anchor path must be strictly cheaper"
622 );
623 }
624
625 #[test]
626 fn hook_covered_client_gets_hook_aware_anchor() {
627 let _guard = crate::core::data_dir::test_env_lock();
631 let tmp = tempfile::tempdir().unwrap();
632 let home = tmp.path();
633 std::fs::create_dir_all(home.join(".cursor/rules")).unwrap();
634 let tp = crate::core::tool_profiles::ToolProfile::Power;
635 std::fs::write(
636 home.join(".cursor/rules/lean-ctx.mdc"),
637 rc::render(
638 false,
639 Wrapper::HookCovered,
640 crate::core::config::CompressionLevel::Off,
641 &tp,
642 ),
643 )
644 .unwrap();
645 std::fs::write(
646 home.join(".cursor/hooks.json"),
647 r#"{"version":1,"hooks":{"preToolUse":[
648 {"matcher":"Shell","command":"/usr/local/bin/lean-ctx hook rewrite"},
649 {"matcher":"Read|Grep","command":"/usr/local/bin/lean-ctx hook redirect"}
650 ]}}"#,
651 )
652 .unwrap();
653 let old_home = std::env::var("HOME").ok();
654 crate::test_env::set_var("HOME", home);
655 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
656
657 let covered = build_instructions_with_client(CrpMode::Off, "cursor");
658
659 if let Some(h) = old_home {
660 crate::test_env::set_var("HOME", h);
661 } else {
662 crate::test_env::remove_var("HOME");
663 }
664 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
665
666 assert!(
667 covered.contains("hooks compress native Shell/Read/Grep"),
668 "hook-covered client must get the hook-aware anchor:\n{covered}"
669 );
670 assert!(
671 !covered.contains("ctx_* tools replace native")
672 && !covered.contains("MANDATORY MAPPING"),
673 "hook-covered client must not carry the replace-native wording:\n{covered}"
674 );
675 }
676
677 #[test]
678 fn non_coding_persona_block_lands_in_instructions() {
679 let _guard = crate::core::data_dir::test_env_lock();
680 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
681
682 crate::test_env::set_var("LEAN_CTX_PERSONA", "research");
683 let research = build_instructions_with_client(CrpMode::Off, "");
684
685 crate::test_env::set_var("LEAN_CTX_PERSONA", "coding");
686 let coding = build_instructions_with_client(CrpMode::Off, "");
687
688 crate::test_env::remove_var("LEAN_CTX_PERSONA");
689 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
690
691 assert!(
692 research.contains("PERSONA: research"),
693 "research persona must announce its domain block:\n{research}"
694 );
695 assert!(
696 research.contains("INTENTS: explore, summarize, compare, cite, synthesize"),
697 "research persona must carry its intent vocabulary:\n{research}"
698 );
699 assert!(
700 !coding.contains("PERSONA:"),
701 "the coding default must keep the instructions byte-stable (#498):\n{coding}"
702 );
703 }
704
705 #[test]
706 fn under_cap_keeps_everything() {
707 let base = "tool mapping block";
708 let suffix = "OUTPUT STYLE: dense";
709 let out = assemble_within_cap(base, suffix, INSTRUCTION_CAP_TOKENS);
710 assert!(out.contains(base));
711 assert!(out.contains(suffix));
712 }
713
714 #[test]
715 fn empty_suffix_caps_base_only() {
716 let base = "x\n".repeat(4000);
717 let out = assemble_within_cap(&base, "", INSTRUCTION_CAP_TOKENS);
718 assert!(count_tokens(&out) <= INSTRUCTION_CAP_TOKENS);
719 }
720
721 #[cfg(windows)]
722 #[test]
723 fn shell_hint_stays_within_its_budget() {
724 let hint = build_shell_hint();
725 let tokens = count_tokens(&hint);
726 assert!(
727 tokens <= STATIC_INSTRUCTION_SHELL_HINT_TOKENS,
728 "shell hint = {tokens} tok, budget {STATIC_INSTRUCTION_SHELL_HINT_TOKENS}: {hint}"
729 );
730 }
731
732 #[test]
733 fn minimal_overhead_instructions_stay_within_budget() {
734 const MINIMAL_INSTRUCTION_BUDGET_TOKENS: usize =
735 STATIC_INSTRUCTION_BUDGET_TDD_TOKENS + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
736 let _iso = crate::core::data_dir::isolated_data_dir();
737 crate::test_env::set_var("LEAN_CTX_MINIMAL", "1");
738 let out = build_instructions(CrpMode::Compact);
739 crate::test_env::remove_var("LEAN_CTX_MINIMAL");
740 let tokens = count_tokens(&out);
741 assert!(
742 tokens <= MINIMAL_INSTRUCTION_BUDGET_TOKENS,
743 "minimal-overhead instructions = {tokens} tok, budget {MINIMAL_INSTRUCTION_BUDGET_TOKENS}\n---\n{out}\n---"
744 );
745 }
746
747 #[test]
748 fn static_skeleton_stays_within_budget() {
749 let _iso = crate::core::data_dir::isolated_data_dir();
750 crate::test_env::set_var("LEAN_CTX_COMPRESSION", "off");
753 for (mode, base_budget) in [
754 (CrpMode::Off, STATIC_INSTRUCTION_BUDGET_TOKENS),
755 (CrpMode::Compact, STATIC_INSTRUCTION_BUDGET_TOKENS),
756 (CrpMode::Tdd, STATIC_INSTRUCTION_BUDGET_TDD_TOKENS),
757 ] {
758 let budget = base_budget + STATIC_INSTRUCTION_SHELL_HINT_TOKENS;
759 let out = build_instructions_for_test(mode);
760 let tokens = count_tokens(&out);
761 assert!(
762 tokens <= budget,
763 "static instructions for {mode:?} = {tokens} tok, budget {budget}\n---\n{out}\n---"
764 );
765 }
766 crate::test_env::remove_var("LEAN_CTX_COMPRESSION");
767 }
768}