use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateDefinition {
pub id: String,
pub version: String,
pub extends: Option<String>,
pub metadata: TemplateMetadata,
pub input_schema: serde_json::Value,
pub output_schema: OutputSchema,
pub validation: ValidationRules,
pub prompt_template: String,
#[cfg(feature = "quality-proxy")]
pub quality_enforcement: Option<QualityEnforcement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateMetadata {
pub provider: String,
pub description: String,
pub parameters: HashMap<String, serde_json::Value>,
pub author: Option<String>,
#[cfg(feature = "todo-validation")]
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
#[cfg(feature = "todo-validation")]
pub modified_at: Option<chrono::DateTime<chrono::Utc>>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputSchema {
pub format: String,
pub structure: String,
pub schema: Option<serde_json::Value>,
pub example: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationRules {
pub deterministic_only: bool,
pub required_fields: Vec<String>,
pub optional_fields: Vec<String>,
pub quality_gates: Option<QualityGateRules>,
pub structure_rules: Option<StructureRules>,
pub custom_validators: Vec<String>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateRules {
pub max_complexity_per_task: Option<u8>,
pub require_time_estimates: bool,
pub require_specific_actions: bool,
pub min_task_detail_chars: Option<usize>,
pub max_task_detail_chars: Option<usize>,
pub custom_rules: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructureRules {
pub max_items: Option<usize>,
pub min_items: Option<usize>,
pub require_dependency_graph: bool,
pub prevent_circular_dependencies: bool,
pub required_elements: Vec<String>,
pub forbidden_elements: Vec<String>,
}
#[cfg(feature = "quality-proxy")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityEnforcement {
pub pmat_config: PmatConfig,
pub auto_refactor: bool,
pub mode: QualityMode,
pub thresholds: HashMap<String, f64>,
}
#[cfg(feature = "quality-proxy")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmatConfig {
pub mode: String,
pub max_complexity: u32,
pub allow_satd: bool,
pub require_docs: bool,
pub auto_format: bool,
pub custom_settings: HashMap<String, serde_json::Value>,
}
#[cfg(feature = "quality-proxy")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QualityMode {
Strict,
Advisory,
AutoFix,
Disabled,
}
impl TemplateDefinition {
pub fn new<S: Into<String>>(id: S, version: S, prompt_template: S) -> Self {
Self {
id: id.into(),
version: version.into(),
extends: None,
metadata: TemplateMetadata::default(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
output_schema: OutputSchema::default(),
validation: ValidationRules::default(),
prompt_template: prompt_template.into(),
#[cfg(feature = "quality-proxy")]
quality_enforcement: None,
}
}
pub fn is_deterministic(&self) -> bool {
if self.metadata.provider == "deterministic" {
return true;
}
self.metadata
.parameters
.get("temperature")
.and_then(|v| v.as_f64())
.map(|t| t == 0.0)
.unwrap_or(false)
}
pub fn get_parameter<T>(&self, key: &str) -> Option<T>
where
T: serde::de::DeserializeOwned,
{
self.metadata
.parameters
.get(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn set_parameter<T>(&mut self, key: String, value: T) -> crate::Result<()>
where
T: Serialize,
{
let json_value = serde_json::to_value(value)?;
self.metadata.parameters.insert(key, json_value);
Ok(())
}
pub fn validate(&self) -> crate::Result<()> {
if self.id.is_empty() {
return Err(crate::error::TemplateError::InvalidDefinition {
reason: "Template ID cannot be empty".to_string(),
}
.into());
}
if self.version.is_empty() {
return Err(crate::error::TemplateError::InvalidDefinition {
reason: "Template version cannot be empty".to_string(),
}
.into());
}
if self.prompt_template.is_empty() {
return Err(crate::error::TemplateError::InvalidDefinition {
reason: "Prompt template cannot be empty".to_string(),
}
.into());
}
if !self.input_schema.is_object() {
return Err(crate::error::TemplateError::InvalidDefinition {
reason: "Input schema must be a JSON object".to_string(),
}
.into());
}
if self.validation.deterministic_only && !self.is_deterministic() {
return Err(crate::error::TemplateError::InvalidDefinition {
reason: "Template marked as deterministic_only but provider/parameters are non-deterministic".to_string(),
}.into());
}
Ok(())
}
pub fn get_all_tags(&self) -> Vec<String> {
let mut tags = self.metadata.tags.clone();
if self.is_deterministic() {
tags.push("deterministic".to_string());
}
if self.validation.deterministic_only {
tags.push("strict".to_string());
}
#[cfg(feature = "quality-proxy")]
if self.quality_enforcement.is_some() {
tags.push("quality-enforced".to_string());
}
tags.sort();
tags.dedup();
tags
}
}
impl Default for TemplateMetadata {
fn default() -> Self {
Self {
provider: "deterministic".to_string(),
description: "Template generated by PDMT".to_string(),
parameters: {
let mut params = HashMap::new();
params.insert("temperature".to_string(), serde_json::json!(0.0));
params
},
author: None,
#[cfg(feature = "todo-validation")]
created_at: Some(chrono::Utc::now()),
#[cfg(feature = "todo-validation")]
modified_at: Some(chrono::Utc::now()),
tags: Vec::new(),
}
}
}
impl Default for OutputSchema {
fn default() -> Self {
Self {
format: "yaml".to_string(),
structure: "Generated content structure".to_string(),
schema: None,
example: None,
}
}
}
impl Default for ValidationRules {
fn default() -> Self {
Self {
deterministic_only: true,
required_fields: Vec::new(),
optional_fields: Vec::new(),
quality_gates: None,
structure_rules: None,
custom_validators: Vec::new(),
min_length: Some(10),
max_length: Some(10000),
}
}
}
impl Default for QualityGateRules {
fn default() -> Self {
Self {
max_complexity_per_task: Some(8),
require_time_estimates: true,
require_specific_actions: true,
min_task_detail_chars: Some(10),
max_task_detail_chars: Some(100),
custom_rules: HashMap::new(),
}
}
}
impl Default for StructureRules {
fn default() -> Self {
Self {
max_items: Some(50),
min_items: Some(1),
require_dependency_graph: true,
prevent_circular_dependencies: true,
required_elements: Vec::new(),
forbidden_elements: Vec::new(),
}
}
}
#[cfg(feature = "quality-proxy")]
impl Default for QualityEnforcement {
fn default() -> Self {
Self {
pmat_config: PmatConfig::default(),
auto_refactor: false,
mode: QualityMode::Strict,
thresholds: HashMap::new(),
}
}
}
#[cfg(feature = "quality-proxy")]
impl Default for PmatConfig {
fn default() -> Self {
Self {
mode: "strict".to_string(),
max_complexity: 8,
allow_satd: false,
require_docs: true,
auto_format: true,
custom_settings: HashMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_definition_creation() {
let template =
TemplateDefinition::new("test_template", "1.0.0", "Generate content for {{input}}");
assert_eq!(template.id, "test_template");
assert_eq!(template.version, "1.0.0");
assert!(template.is_deterministic());
}
#[test]
fn test_template_validation() {
let mut template = TemplateDefinition::new("test", "1.0", "{{input}}");
assert!(template.validate().is_ok());
template.id = String::new();
assert!(template.validate().is_err());
}
#[test]
fn test_deterministic_detection() {
let mut template = TemplateDefinition::new("test", "1.0", "{{input}}");
assert!(template.is_deterministic());
template.metadata.provider = "anthropic".to_string();
template
.set_parameter("temperature".to_string(), 0.7)
.unwrap();
assert!(!template.is_deterministic());
template
.set_parameter("temperature".to_string(), 0.0)
.unwrap();
assert!(template.is_deterministic());
template.metadata.provider = "deterministic".to_string();
assert!(template.is_deterministic());
}
#[test]
fn test_parameter_management() {
let mut template = TemplateDefinition::new("test", "1.0", "{{input}}");
template
.set_parameter("max_tokens".to_string(), 100)
.unwrap();
let max_tokens: Option<i32> = template.get_parameter("max_tokens");
assert_eq!(max_tokens, Some(100));
let missing: Option<String> = template.get_parameter("missing");
assert_eq!(missing, None);
}
#[test]
fn test_template_tags() {
let mut template = TemplateDefinition::new("test", "1.0", "{{input}}");
template.metadata.tags.push("test".to_string());
template.validation.deterministic_only = true;
let all_tags = template.get_all_tags();
assert!(all_tags.contains(&"test".to_string()));
assert!(all_tags.contains(&"deterministic".to_string()));
assert!(all_tags.contains(&"strict".to_string()));
}
}