pub mod hash;
use std::path::Path;
use crate::error::{Error, Result};
use crate::models::PromptsConfig;
#[derive(Debug, Clone)]
pub struct PromptBundle {
pub id: String,
pub policy_hash: String,
pub system: String,
pub classification: String,
pub escalation: String,
pub mediation_style: String,
pub message_templates: String,
}
pub fn load_bundle(config: &PromptsConfig) -> Result<PromptBundle> {
let system = read_file(&config.system_instructions_path, "system_instructions_path")?;
let classification = read_file(
&config.classification_policy_path,
"classification_policy_path",
)?;
let escalation = read_file(&config.escalation_policy_path, "escalation_policy_path")?;
let mediation_style = read_file(&config.mediation_style_path, "mediation_style_path")?;
let message_templates = read_file(&config.message_templates_path, "message_templates_path")?;
let policy_hash = hash::policy_hash(
&system,
&classification,
&escalation,
&mediation_style,
&message_templates,
);
Ok(PromptBundle {
id: "phase3-default".to_string(),
policy_hash,
system,
classification,
escalation,
mediation_style,
message_templates,
})
}
fn read_file(path: &str, field: &str) -> Result<String> {
std::fs::read_to_string(Path::new(path)).map_err(|e| {
Error::PromptBundleLoad(format!(
"failed to read prompt bundle file for `{field}` at {path}: {e}"
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write_tmp(contents: &str) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(contents.as_bytes()).unwrap();
f
}
#[test]
fn load_bundle_errors_on_missing_path() {
let cfg = PromptsConfig {
system_instructions_path: "/no/such/system.md".into(),
classification_policy_path: "/no/such/classification.md".into(),
escalation_policy_path: "/no/such/escalation.md".into(),
mediation_style_path: "/no/such/style.md".into(),
message_templates_path: "/no/such/templates.md".into(),
};
let err = load_bundle(&cfg).unwrap_err();
assert!(matches!(err, Error::PromptBundleLoad(_)));
}
#[test]
fn load_bundle_happy_path() {
let a = write_tmp("system body\n");
let b = write_tmp("classification body\n");
let c = write_tmp("escalation body\n");
let d = write_tmp("style body\n");
let e = write_tmp("templates body\n");
let cfg = PromptsConfig {
system_instructions_path: a.path().to_string_lossy().into_owned(),
classification_policy_path: b.path().to_string_lossy().into_owned(),
escalation_policy_path: c.path().to_string_lossy().into_owned(),
mediation_style_path: d.path().to_string_lossy().into_owned(),
message_templates_path: e.path().to_string_lossy().into_owned(),
};
let bundle = load_bundle(&cfg).unwrap();
assert_eq!(bundle.id, "phase3-default");
assert_eq!(bundle.system, "system body\n");
assert_eq!(bundle.policy_hash.len(), 64);
assert!(bundle.policy_hash.chars().all(|c| c.is_ascii_hexdigit()));
}
}