use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Promptgram {
pub id: String,
#[serde(default)]
pub parent_id: Option<String>,
pub version: usize,
pub sections: HashMap<String, PromptSection>,
pub metadata: PromptgramMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptSection {
pub name: String,
pub content: String,
#[serde(default)]
pub immutable: bool,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PromptgramMetadata {
pub created_at: i64,
pub modified_at: i64,
pub best_ndcg: f64,
pub run_count: usize,
pub description: String,
#[serde(default)]
pub lineage: Vec<String>,
}
pub mod sections {
pub const ROLE: &str = "Role";
pub const API_CONTRACT: &str = "API_contract";
pub const POLICY: &str = "Policy";
pub const HEURISTICS: &str = "Heuristics";
pub const CURRICULUM: &str = "Curriculum";
pub const OUTPUT_SCHEMA: &str = "Output_schema";
pub const STYLE: &str = "Style";
}
impl Promptgram {
pub fn new(id: impl Into<String>) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
Promptgram {
id: id.into(),
parent_id: None,
version: 1,
sections: HashMap::new(),
metadata: PromptgramMetadata {
created_at: now,
modified_at: now,
..Default::default()
},
}
}
pub fn with_section(mut self, name: &str, content: &str, immutable: bool) -> Self {
self.sections.insert(
name.to_string(),
PromptSection {
name: name.to_string(),
content: content.to_string(),
immutable,
tags: vec![],
},
);
self
}
pub fn get_section(&self, name: &str) -> Option<&PromptSection> {
self.sections.get(name)
}
pub fn render(&self) -> String {
let section_order = [
sections::ROLE,
sections::API_CONTRACT,
sections::POLICY,
sections::HEURISTICS,
sections::CURRICULUM,
sections::OUTPUT_SCHEMA,
sections::STYLE,
];
let mut output = String::new();
for section_name in §ion_order {
if let Some(section) = self.sections.get(*section_name) {
output.push_str(&format!("## {}\n\n", section.name));
output.push_str(§ion.content);
output.push_str("\n\n");
}
}
for (name, section) in &self.sections {
if !section_order.contains(&name.as_str()) {
output.push_str(&format!("## {}\n\n", section.name));
output.push_str(§ion.content);
output.push_str("\n\n");
}
}
output.trim().to_string()
}
pub fn fork(&self, new_id: impl Into<String>) -> Self {
let new_id = new_id.into();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let mut lineage = self.metadata.lineage.clone();
lineage.push(self.id.clone());
Promptgram {
id: new_id,
parent_id: Some(self.id.clone()),
version: 1,
sections: self.sections.clone(),
metadata: PromptgramMetadata {
created_at: now,
modified_at: now,
best_ndcg: 0.0,
run_count: 0,
description: format!("Fork of {}", self.id),
lineage,
},
}
}
pub fn apply_edit(&mut self, edit: &super::PromptEdit) -> Result<(), String> {
let section = self
.sections
.get_mut(&edit.section)
.ok_or_else(|| format!("Section '{}' not found", edit.section))?;
if section.immutable {
return Err(format!("Section '{}' is immutable", edit.section));
}
match edit.edit_type.as_str() {
"append" => {
section.content.push_str("\n\n");
section.content.push_str(&edit.content);
}
"replace" => {
let target = edit.target.as_deref().unwrap_or("");
if target.is_empty() {
section.content = edit.content.clone();
} else {
section.content = section.content.replace(target, &edit.content);
}
}
"delete" => {
let target = edit.target.as_deref().unwrap_or("");
section.content = section.content.replace(target, "");
}
_ => return Err(format!("Unknown edit type: {}", edit.edit_type)),
}
self.version += 1;
self.metadata.modified_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
Ok(())
}
pub fn load(path: impl AsRef<Path>) -> Result<Self, String> {
let content = std::fs::read_to_string(path.as_ref())
.map_err(|e| format!("Failed to read promptgram: {}", e))?;
toml::from_str(&content).map_err(|e| format!("Failed to parse promptgram: {}", e))
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), String> {
let content = toml::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize promptgram: {}", e))?;
std::fs::write(path.as_ref(), content)
.map_err(|e| format!("Failed to write promptgram: {}", e))
}
pub fn from_markdown(id: &str, content: &str) -> Self {
let mut promptgram = Self::new(id);
let mut current_section: Option<String> = None;
let mut current_content = String::new();
for line in content.lines() {
if line.starts_with("## ") {
if let Some(section_name) = current_section.take() {
let immutable = matches!(
section_name.as_str(),
"Role" | "API_contract" | "Output_schema"
);
promptgram =
promptgram.with_section(§ion_name, current_content.trim(), immutable);
current_content.clear();
}
current_section = Some(line[3..].trim().to_string());
} else if current_section.is_some() {
current_content.push_str(line);
current_content.push('\n');
}
}
if let Some(section_name) = current_section {
let immutable = matches!(
section_name.as_str(),
"Role" | "API_contract" | "Output_schema"
);
promptgram = promptgram.with_section(§ion_name, current_content.trim(), immutable);
}
promptgram
}
}
pub fn diff_prompts(a: &Promptgram, b: &Promptgram) -> Vec<PromptDiff> {
let mut diffs = Vec::new();
for (name, section_a) in &a.sections {
match b.sections.get(name) {
Some(section_b) => {
if section_a.content != section_b.content {
diffs.push(PromptDiff {
section: name.clone(),
diff_type: DiffType::Modified,
before: Some(section_a.content.clone()),
after: Some(section_b.content.clone()),
lines_added: count_lines(§ion_b.content)
.saturating_sub(count_lines(§ion_a.content)),
lines_removed: count_lines(§ion_a.content)
.saturating_sub(count_lines(§ion_b.content)),
});
}
}
None => {
diffs.push(PromptDiff {
section: name.clone(),
diff_type: DiffType::Removed,
before: Some(section_a.content.clone()),
after: None,
lines_added: 0,
lines_removed: count_lines(§ion_a.content),
});
}
}
}
for (name, section_b) in &b.sections {
if !a.sections.contains_key(name) {
diffs.push(PromptDiff {
section: name.clone(),
diff_type: DiffType::Added,
before: None,
after: Some(section_b.content.clone()),
lines_added: count_lines(§ion_b.content),
lines_removed: 0,
});
}
}
diffs
}
#[derive(Debug, Clone)]
pub struct PromptDiff {
pub section: String,
pub diff_type: DiffType,
pub before: Option<String>,
pub after: Option<String>,
pub lines_added: usize,
pub lines_removed: usize,
}
impl PromptDiff {
pub fn summary(&self) -> String {
match self.diff_type {
DiffType::Added => format!(
"[+{}] {} (+{} lines)",
self.section, "added", self.lines_added
),
DiffType::Removed => format!(
"[-{}] {} (-{} lines)",
self.section, "removed", self.lines_removed
),
DiffType::Modified => format!(
"[~{}] modified (+{}/-{})",
self.section, self.lines_added, self.lines_removed
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffType {
Added,
Removed,
Modified,
}
fn count_lines(s: &str) -> usize {
s.lines().count()
}
pub fn baseline_promptgram() -> Promptgram {
Promptgram::new("inner_v001")
.with_section(
sections::ROLE,
r#"You approximate the gradient in concept space.
Given failures and trajectory, propose parameter changes."#,
true,
)
.with_section(
sections::POLICY,
r#"Analyze trajectory state:
- Improving: continue direction
- Degrading: revert or reverse
- Plateaued: orthogonal move
Analyze failures:
- Missing signal vs overwhelming signal
- Parameter interactions"#,
false,
)
.with_section(
sections::HEURISTICS,
r#"- NDCG drop >5% = collapse signal
- Temporal and structural signals compete
- High boosts cause tunnel vision
- Low alpha localizes, high alpha globalizes
- Depth penalties break monorepos"#,
false,
)
.with_section(
sections::STYLE,
r#"Analytical. Specific. Reference concrete failures."#,
false,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_promptgram_render() {
let pg = Promptgram::new("test")
.with_section(sections::ROLE, "You are a test", true)
.with_section(sections::POLICY, "Do the thing", false);
let rendered = pg.render();
assert!(rendered.contains("## Role"));
assert!(rendered.contains("You are a test"));
assert!(rendered.contains("## Policy"));
}
#[test]
fn test_promptgram_fork() {
let parent = Promptgram::new("parent").with_section(sections::POLICY, "Original", false);
let child = parent.fork("child");
assert_eq!(child.parent_id, Some("parent".to_string()));
assert_eq!(child.metadata.lineage, vec!["parent"]);
assert!(child.get_section(sections::POLICY).is_some());
}
#[test]
fn test_promptgram_edit_immutable() {
let mut pg = Promptgram::new("test").with_section(sections::ROLE, "Original", true);
let edit = super::super::PromptEdit {
section: sections::ROLE.to_string(),
edit_type: "replace".to_string(),
target: Some(String::new()),
content: "Modified".to_string(),
rationale: "test".to_string(),
};
assert!(pg.apply_edit(&edit).is_err());
}
#[test]
fn test_from_markdown() {
let md = r#"## Role
You are a test optimizer.
## Policy
Do smart things.
Make good choices.
## Style
Be brief.
"#;
let pg = Promptgram::from_markdown("test", md);
assert!(pg.get_section("Role").is_some());
assert!(pg.get_section("Policy").is_some());
assert!(pg.get_section("Style").is_some());
}
}