agent_policy/model/
mod.rs1pub mod normalized;
4pub mod policy;
5pub mod targets;
6
7use crate::error::{Error, Result};
8use indexmap::IndexMap;
9use normalized::{Commands, Constraints, Paths, Policy, Project, Role};
10use policy::RawPolicy;
11use targets::OutputTargets;
12
13const VALID_TARGETS: &[&str] = &["agents-md", "claude-md", "cursor-rules"];
15
16pub fn normalize(raw: RawPolicy) -> Result<Policy> {
28 let mut roles: IndexMap<String, Role> = IndexMap::new();
30 if let Some(raw_roles) = raw.roles {
31 for (name, raw_role) in raw_roles {
32 validate_role_name(&name)?;
33 let editable = raw_role.editable.unwrap_or_default();
34 let forbidden = raw_role.forbidden.unwrap_or_default();
35 validate_globs(&editable)?;
36 validate_globs(&forbidden)?;
37 roles.insert(
38 name.clone(),
39 Role {
40 name,
41 editable,
42 forbidden,
43 },
44 );
45 }
46 }
47
48 let raw_paths = raw.paths.unwrap_or_default();
50 let editable = raw_paths.editable.unwrap_or_default();
51 let protected = raw_paths.protected.unwrap_or_default();
52 let generated = raw_paths.generated.unwrap_or_default();
53 validate_globs(&editable)?;
54 validate_globs(&protected)?;
55 validate_globs(&generated)?;
56
57 let raw_commands = raw.commands.unwrap_or_default();
58 let raw_constraints = raw.constraints.unwrap_or_default();
59
60 let enabled_targets: Vec<String> = raw.outputs.unwrap_or_else(|| vec!["agents-md".to_owned()]);
62
63 for id in &enabled_targets {
66 if !VALID_TARGETS.contains(&id.as_str()) {
67 return Err(Error::UnknownTarget { id: id.clone() });
68 }
69 }
70
71 let outputs = OutputTargets {
72 agents_md: enabled_targets.contains(&"agents-md".to_owned()),
73 claude_md: enabled_targets.contains(&"claude-md".to_owned()),
74 cursor_rules: enabled_targets.contains(&"cursor-rules".to_owned()),
75 };
76
77 if outputs.is_empty() {
78 return Err(Error::NoOutputs);
79 }
80
81 Ok(Policy {
82 project: Project {
83 name: raw.project.name,
84 summary: raw.project.summary,
85 },
86 commands: Commands {
87 install: raw_commands.install,
88 dev: raw_commands.dev,
89 lint: raw_commands.lint,
90 test: raw_commands.test,
91 build: raw_commands.build,
92 },
93 paths: Paths {
94 editable,
95 protected,
96 generated,
97 },
98 roles,
99 constraints: Constraints {
100 require_tests_for_code_changes: raw_constraints
101 .require_tests_for_code_changes
102 .unwrap_or(false),
103 forbid_secrets: raw_constraints.forbid_secrets.unwrap_or(false),
104 require_human_review_for_protected_paths: raw_constraints
105 .require_human_review_for_protected_paths
106 .unwrap_or(false),
107 },
108 outputs,
109 })
110}
111
112fn validate_role_name(name: &str) -> Result<()> {
113 let valid = !name.is_empty()
114 && name
115 .chars()
116 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_');
117 if valid {
118 Ok(())
119 } else {
120 Err(Error::InvalidRoleName {
121 name: name.to_owned(),
122 })
123 }
124}
125
126fn validate_globs(patterns: &[String]) -> Result<()> {
127 for pattern in patterns {
128 globset::GlobBuilder::new(pattern)
129 .build()
130 .map_err(|e| Error::Glob {
131 pattern: pattern.clone(),
132 source: e,
133 })?;
134 }
135 Ok(())
136}