ito_core/templates/
guidance.rs1use ito_templates::ITO_END_MARKER;
2use std::path::Path;
3
4use super::WorkflowError;
5
6const ITO_INTERNAL_COMMENT_START: &str = "<!-- ITO:INTERNAL:START -->";
7const ITO_INTERNAL_COMMENT_END: &str = "<!-- ITO:INTERNAL:END -->";
8
9pub fn load_user_guidance(ito_path: &Path) -> Result<Option<String>, WorkflowError> {
13 let path = ito_path.join("user-prompts").join("guidance.md");
14 if path.exists() {
15 return load_guidance_file(&path);
16 }
17 let path = ito_path.join("user-guidance.md");
18 load_guidance_file(&path)
19}
20
21pub fn load_user_guidance_for_artifact(
23 ito_path: &Path,
24 artifact_id: &str,
25) -> Result<Option<String>, WorkflowError> {
26 if !is_safe_artifact_id(artifact_id) {
27 return Err(WorkflowError::InvalidArtifactId(artifact_id.to_string()));
28 }
29 let path = ito_path
30 .join("user-prompts")
31 .join(format!("{artifact_id}.md"));
32 load_guidance_file(&path)
33}
34
35pub fn load_composed_user_guidance(
43 ito_path: &Path,
44 artifact_id: &str,
45) -> Result<Option<String>, WorkflowError> {
46 let scoped = load_user_guidance_for_artifact(ito_path, artifact_id)?;
47 let shared = load_user_guidance(ito_path)?;
48
49 match (scoped, shared) {
50 (None, None) => Ok(None),
51 (Some(scoped), None) => Ok(Some(scoped)),
52 (None, Some(shared)) => Ok(Some(shared)),
53 (Some(scoped), Some(shared)) => Ok(Some(format!(
54 "## Scoped Guidance ({artifact_id})\n\n{scoped}\n\n## Shared Guidance\n\n{shared}"
55 ))),
56 }
57}
58
59fn load_guidance_file(path: &Path) -> Result<Option<String>, WorkflowError> {
69 if !path.exists() {
70 return Ok(None);
71 }
72
73 let content = ito_common::io::read_to_string_std(path)?;
74 let content = content.replace("\r\n", "\n");
75 let content = match content.find(ITO_END_MARKER) {
76 Some(i) => &content[i + ITO_END_MARKER.len()..],
77 None => content.as_str(),
78 };
79 let content = strip_ito_internal_comment_blocks(content);
80 let content = content.trim();
81 if content.is_empty() {
82 return Ok(None);
83 }
84
85 Ok(Some(content.to_string()))
86}
87
88fn strip_ito_internal_comment_blocks(content: &str) -> String {
89 let mut out = String::new();
90 let mut in_block = false;
91
92 for line in content.lines() {
93 let trimmed = line.trim();
94
95 if in_block {
96 if trimmed == ITO_INTERNAL_COMMENT_END {
97 in_block = false;
98 }
99 continue;
100 }
101
102 if trimmed == ITO_INTERNAL_COMMENT_START {
103 in_block = true;
104 continue;
105 }
106
107 out.push_str(line);
108 out.push('\n');
109 }
110
111 out
112}
113
114fn is_safe_artifact_id(artifact_id: &str) -> bool {
115 if artifact_id.is_empty() || artifact_id.contains("..") {
116 return false;
117 }
118
119 for c in artifact_id.chars() {
120 if !(c.is_ascii_alphanumeric() || c == '-' || c == '_') {
121 return false;
122 }
123 }
124
125 true
126}
127
128#[cfg(test)]
129mod tests {
130 use super::strip_ito_internal_comment_blocks;
131
132 #[test]
133 fn strip_ito_internal_comment_blocks_removes_internal_template_guidance() {
134 let contents = r#"
135Keep this.
136<!-- ITO:INTERNAL:START -->
137## Your Guidance
138(placeholder)
139<!-- ITO:INTERNAL:END -->
140Keep this too.
141"#;
142
143 let stripped = strip_ito_internal_comment_blocks(contents);
144 assert!(stripped.contains("Keep this."));
145 assert!(stripped.contains("Keep this too."));
146 assert!(!stripped.contains("## Your Guidance"));
147 assert!(!stripped.contains("(placeholder)"));
148 }
149}