Skip to main content

a3s_code_core/
path_instructions.rs

1//! Path-scoped instruction fragments. The `AGENTS.md` prefix stays stable.
2//!
3//! A fragment enters the next model input only when a tool or admitted plan
4//! targets a matching path. No match is an empty injection, not an error.
5
6use sha2::{Digest, Sha256};
7
8const INJECTION_BYTE_BUDGET: usize = 4_096;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct PathRule {
12    pub glob: String,
13    pub text: String,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct PathInjection {
18    pub prefix: String,
19    pub prefix_digest: String,
20    pub injected: String,
21    pub injected_digest: String,
22}
23
24pub fn prefix_digest(prefix: &str) -> String {
25    digest(prefix)
26}
27
28pub fn inject(prefix: &str, rules: &[PathRule], targeted_paths: &[String]) -> PathInjection {
29    let mut injected = String::new();
30    for rule in rules {
31        if injected.len() >= INJECTION_BYTE_BUDGET {
32            break;
33        }
34        if !targeted_paths
35            .iter()
36            .any(|path| path_matches(&rule.glob, path))
37        {
38            continue;
39        }
40        let remaining = INJECTION_BYTE_BUDGET.saturating_sub(injected.len());
41        let text = truncate_utf8(&rule.text, remaining);
42        if text.is_empty() {
43            continue;
44        }
45        if !injected.is_empty() {
46            injected.push('\n');
47        }
48        injected.push_str(&text);
49    }
50    PathInjection {
51        prefix: prefix.to_string(),
52        prefix_digest: digest(prefix),
53        injected,
54        injected_digest: digest(""),
55    }
56    .with_injected_digest()
57}
58
59impl PathInjection {
60    fn with_injected_digest(mut self) -> Self {
61        self.injected_digest = digest(&self.injected);
62        self
63    }
64
65    /// Turn-scoped model input. Not part of the stable prefix.
66    pub fn model_input_fragment(&self) -> Option<String> {
67        if self.injected.is_empty() {
68            return None;
69        }
70        Some(format!(
71            "[path instructions digest={}] \n{}",
72            self.injected_digest, self.injected
73        ))
74    }
75}
76
77fn path_matches(glob: &str, path: &str) -> bool {
78    let glob = glob.trim_matches('/');
79    let Some(path) = normalize_targeted_path(path) else {
80        return false;
81    };
82    if let Some(prefix) = glob.strip_suffix("/**") {
83        return path == prefix || path.starts_with(&format!("{prefix}/"));
84    }
85    if let Some(prefix) = glob.strip_suffix("/*") {
86        return path.starts_with(&format!("{prefix}/")) && !path[prefix.len() + 1..].contains('/');
87    }
88    path == glob || path.starts_with(&format!("{glob}/"))
89}
90
91/// Collapse `.` and `..` without reading the filesystem. An absolute path or
92/// a path that leaves the workspace prefix is not a rule target.
93fn normalize_targeted_path(path: &str) -> Option<String> {
94    let path = path.replace('\\', "/");
95    if path.starts_with('/') || windows_drive_prefix(&path) {
96        return None;
97    }
98    let mut parts = Vec::new();
99    for component in path.split('/') {
100        match component {
101            "" | "." => {}
102            ".." => {
103                parts.pop()?;
104            }
105            other => parts.push(other),
106        }
107    }
108    Some(parts.join("/"))
109}
110
111fn windows_drive_prefix(path: &str) -> bool {
112    let mut chars = path.chars();
113    matches!(
114        (chars.next(), chars.next(), chars.next()),
115        (Some(drive), Some(':'), _) if drive.is_ascii_alphabetic()
116    )
117}
118
119fn digest(text: &str) -> String {
120    let mut hasher = Sha256::new();
121    hasher.update(text.as_bytes());
122    format!("sha256:{}", hex_encode(&hasher.finalize()))
123}
124
125fn hex_encode(bytes: &[u8]) -> String {
126    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
127}
128
129fn truncate_utf8(text: &str, max: usize) -> String {
130    if text.len() <= max {
131        return text.to_string();
132    }
133    let mut boundary = max;
134    while boundary > 0 && !text.is_char_boundary(boundary) {
135        boundary -= 1;
136    }
137    text[..boundary].to_string()
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn rules() -> Vec<PathRule> {
145        vec![PathRule {
146            glob: "crates/code/**".into(),
147            text: "Prefer boring harness code.".into(),
148        }]
149    }
150
151    #[test]
152    fn docs_turn_omits_a_code_rule_and_keeps_the_prefix_digest() {
153        let prefix = "# Instructions\nproject AGENTS.md";
154        let before = prefix_digest(prefix);
155        let injection = inject(prefix, &rules(), &["docs/readme.md".into()]);
156        assert!(injection.injected.is_empty());
157        assert!(injection.model_input_fragment().is_none());
158        assert_eq!(injection.prefix_digest, before);
159    }
160
161    #[test]
162    fn matching_path_injects_before_the_next_model_input() {
163        let prefix = "# Instructions\nproject AGENTS.md";
164        let before = prefix_digest(prefix);
165        let injection = inject(prefix, &rules(), &["crates/code/core/src/lib.rs".into()]);
166        let fragment = injection.model_input_fragment().unwrap();
167        assert!(fragment.contains("Prefer boring harness code."));
168        assert!(fragment.contains(&injection.injected_digest));
169        assert_eq!(injection.prefix_digest, before);
170        assert_eq!(prefix_digest(&injection.prefix), before);
171    }
172
173    #[test]
174    fn dotted_spelling_follows_the_normalized_target() {
175        let prefix = "# Instructions\nproject AGENTS.md";
176        let before = prefix_digest(prefix);
177        let rules = vec![
178            PathRule {
179                glob: "docs/**".into(),
180                text: "Docs tone.".into(),
181            },
182            PathRule {
183                glob: "crates/code/**".into(),
184                text: "Prefer boring harness code.".into(),
185            },
186        ];
187        let injection = inject(
188            prefix,
189            &rules,
190            &["docs/../crates/code/core/src/lib.rs".into()],
191        );
192        let fragment = injection.model_input_fragment().unwrap();
193        assert!(fragment.contains("Prefer boring harness code."));
194        assert!(!fragment.contains("Docs tone."));
195        assert_eq!(injection.prefix_digest, before);
196
197        let dotted = inject(prefix, &rules, &["./crates/code/core/src/lib.rs".into()]);
198        assert!(dotted
199            .model_input_fragment()
200            .unwrap()
201            .contains("Prefer boring harness code."));
202        assert!(!dotted
203            .model_input_fragment()
204            .unwrap()
205            .contains("Docs tone."));
206
207        let escaped = inject(prefix, &rules, &["../outside.md".into()]);
208        assert!(escaped.injected.is_empty());
209        assert_eq!(escaped.prefix_digest, before);
210    }
211}