thndrs_lib/core/context/
mod.rs1pub mod export;
7pub mod instructions;
8
9pub use instructions::{
10 InstructionDiagnostic, InstructionInventory, InstructionSelection, InstructionSeverity, InstructionSnapshot,
11 discover_instructions, select_instructions,
12};
13
14use std::fs;
15use std::path::{Path, PathBuf};
16use std::process::Command;
17
18use std::collections::hash_map::DefaultHasher;
19use std::hash::{Hash, Hasher};
20
21fn hash_content(content: &str) -> u64 {
22 let mut hasher = DefaultHasher::new();
23 content.hash(&mut hasher);
24 hasher.finish()
25}
26
27pub fn scope_depth(scope: &str) -> usize {
28 if scope == "." || scope.is_empty() { 0 } else { scope.matches('/').count() + 1 }
29}
30
31pub const AGENTS_MD_SIZE_CAP: usize = 32_768;
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ContextSource {
39 pub path: PathBuf,
41 pub scope: String,
43 pub content: String,
45 pub content_hash: u64,
47 pub truncated: bool,
49 pub byte_count: usize,
51}
52
53impl ContextSource {
54 pub fn summary(&self) -> String {
56 let path_display = self.path.display().to_string();
57 match self.truncated {
58 true => format!("loaded {path_display} (truncated, {} bytes)", self.byte_count),
59 false => format!("loaded {path_display}"),
60 }
61 }
62}
63
64pub fn discover_workspace_root(cwd: &Path) -> PathBuf {
73 match Command::new("git")
74 .args(["rev-parse", "--show-toplevel"])
75 .current_dir(cwd)
76 .output()
77 {
78 Ok(output) if output.status.success() => {
79 let stdout = String::from_utf8_lossy(&output.stdout);
80 let root = stdout.trim();
81 let root = if root.is_empty() { cwd.to_path_buf() } else { PathBuf::from(root) };
82 root.canonicalize().unwrap_or(root)
83 }
84 _ => cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf()),
85 }
86}
87
88pub fn load_agents_md(workspace_root: &Path) -> Option<ContextSource> {
97 let path = workspace_root.join("AGENTS.md");
98 let metadata = fs::metadata(&path).ok()?;
99 let byte_count = metadata.len() as usize;
100 let content = fs::read_to_string(&path).ok()?;
101 let content_hash = hash_content(&content);
102
103 let (content, truncated) = if byte_count > AGENTS_MD_SIZE_CAP {
104 let mut capped = content.into_bytes();
105 capped.truncate(AGENTS_MD_SIZE_CAP);
106 (
107 trim_to_char_boundary(&String::from_utf8_lossy(&capped), AGENTS_MD_SIZE_CAP),
108 true,
109 )
110 } else {
111 (content, false)
112 };
113
114 Some(ContextSource { path, scope: String::from("."), content, content_hash, truncated, byte_count })
115}
116
117pub fn trim_to_char_boundary(s: &str, max_bytes: usize) -> String {
119 if s.len() <= max_bytes {
120 return s.to_string();
121 }
122 let mut end = max_bytes;
123 while end > 0 && !s.is_char_boundary(end) {
124 end -= 1;
125 }
126 s[..end].to_string()
127}