1use crate::Severity;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RuleCategory {
5 Structure,
6 References,
7 Context,
8 Security,
9 Commands,
10 Consistency,
11}
12
13impl std::fmt::Display for RuleCategory {
14 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 write!(
16 formatter,
17 "{}",
18 match self {
19 Self::Structure => "structure",
20 Self::References => "references",
21 Self::Context => "context",
22 Self::Security => "security",
23 Self::Commands => "commands",
24 Self::Consistency => "consistency",
25 }
26 )
27 }
28}
29
30#[derive(Debug, Clone, Copy)]
31pub struct RuleDefinition {
32 pub id: &'static str,
33 pub name: &'static str,
34 pub description: &'static str,
35 pub default_severity: Severity,
36 pub category: RuleCategory,
37 pub why: &'static str,
38 pub bad_example: &'static str,
39 pub good_example: &'static str,
40}
41
42macro_rules! rule {
43 ($id:literal, $name:literal, $description:literal, $severity:expr, $category:expr, $why:literal, $bad:literal, $good:literal) => {
44 RuleDefinition {
45 id: $id,
46 name: $name,
47 description: $description,
48 default_severity: $severity,
49 category: $category,
50 why: $why,
51 bad_example: $bad,
52 good_example: $good,
53 }
54 };
55}
56
57pub static RULES: &[RuleDefinition] = &[
58 rule!(
59 "AGL001",
60 "invalid-syntax",
61 "Configuration syntax is malformed.",
62 Severity::Error,
63 RuleCategory::Structure,
64 "Malformed configuration cannot be loaded reliably by an agent.",
65 "{ \"mcpServers\":",
66 "{ \"mcpServers\": {} }"
67 ),
68 rule!(
69 "AGL002",
70 "invalid-frontmatter",
71 "Markdown frontmatter is malformed or uses unsupported fields.",
72 Severity::Error,
73 RuleCategory::Structure,
74 "Broken metadata can silently disable scoped instructions and skills.",
75 "---\ndescription: [\n---",
76 "---\ndescription: Backend rules\n---"
77 ),
78 rule!(
79 "AGL003",
80 "duplicate-key",
81 "A configuration object contains a duplicate key.",
82 Severity::Error,
83 RuleCategory::Structure,
84 "Parsers disagree about which duplicate value wins.",
85 "{ \"command\": \"a\", \"command\": \"b\" }",
86 "{ \"command\": \"a\" }"
87 ),
88 rule!(
89 "AGL101",
90 "invalid-mcp",
91 "An MCP configuration or server definition is invalid.",
92 Severity::Error,
93 RuleCategory::Structure,
94 "Invalid MCP servers fail at agent startup or tool invocation.",
95 "{ \"mcpServers\": { \"fs\": {} } }",
96 "{ \"mcpServers\": { \"fs\": { \"command\": \"npx\", \"args\": [] } } }"
97 ),
98 rule!(
99 "AGL102",
100 "unknown-field",
101 "A configuration contains an obviously unsupported field.",
102 Severity::Warning,
103 RuleCategory::Structure,
104 "Misspelled fields are usually ignored without an error.",
105 "commandd: npx",
106 "command: npx"
107 ),
108 rule!(
109 "AGL103",
110 "invalid-url",
111 "An MCP URL is malformed or uses an unsafe scheme.",
112 Severity::Error,
113 RuleCategory::Structure,
114 "Malformed endpoints cannot connect; non-HTTP schemes are unexpected here.",
115 "url: ht!tp://host",
116 "url: https://mcp.example.com"
117 ),
118 rule!(
119 "AGL104",
120 "missing-executable",
121 "A referenced executable is unavailable.",
122 Severity::Error,
123 RuleCategory::Structure,
124 "The configured command will fail immediately.",
125 "command: npxx",
126 "command: npx"
127 ),
128 rule!(
129 "AGL105",
130 "undocumented-environment",
131 "An MCP environment variable is not documented in an environment template.",
132 Severity::Warning,
133 RuleCategory::Structure,
134 "Undocumented variables make MCP setup difficult to reproduce.",
135 "env: { API_TOKEN: ${API_TOKEN} } with no .env.example",
136 "Document API_TOKEN= in .env.example."
137 ),
138 rule!(
139 "AGL201",
140 "invalid-glob",
141 "An instruction glob is malformed.",
142 Severity::Error,
143 RuleCategory::References,
144 "A malformed glob can prevent instructions from applying.",
145 "globs: '[src/**'",
146 "globs: 'src/**'"
147 ),
148 rule!(
149 "AGL202",
150 "missing-path",
151 "A repository path referenced by instructions does not exist.",
152 Severity::Warning,
153 RuleCategory::References,
154 "Agents waste time or make wrong assumptions when guidance names stale paths.",
155 "Read `src/removed/auth.ts` first.",
156 "Read `src/auth.ts` first."
157 ),
158 rule!(
159 "AGL203",
160 "missing-script",
161 "A referenced script file does not exist.",
162 Severity::Error,
163 RuleCategory::References,
164 "Required validation or deployment commands cannot run.",
165 "Run scripts/check.sh",
166 "Run scripts/verify.sh"
167 ),
168 rule!(
169 "AGL301",
170 "large-context",
171 "An instruction file consumes excessive context.",
172 Severity::Warning,
173 RuleCategory::Context,
174 "Large instruction files reduce context available for useful work.",
175 "An instruction file over 6,000 approximate tokens",
176 "Keep durable instructions concise and scoped."
177 ),
178 rule!(
179 "AGL302",
180 "duplicate-instruction",
181 "Substantially duplicate instructions appear in multiple files.",
182 Severity::Warning,
183 RuleCategory::Context,
184 "Repeated instructions consume context and drift independently.",
185 "Repeat the same test requirement in AGENTS.md and CLAUDE.md.",
186 "Keep shared guidance in AGENTS.md and link to it."
187 ),
188 rule!(
189 "AGL303",
190 "verbose-block",
191 "A single instruction block is excessively verbose.",
192 Severity::Info,
193 RuleCategory::Context,
194 "Dense blocks are harder for people and agents to follow.",
195 "A paragraph longer than 1,200 characters",
196 "Split instructions into concise, actionable sections."
197 ),
198 rule!(
199 "AGL401",
200 "hardcoded-secret",
201 "A likely credential is embedded in agent configuration.",
202 Severity::Error,
203 RuleCategory::Security,
204 "Agent configuration is commonly committed and broadly read.",
205 "OPENAI_API_KEY=sk-proj-example-secret",
206 "OPENAI_API_KEY=${OPENAI_API_KEY}"
207 ),
208 rule!(
209 "AGL501",
210 "destructive-command",
211 "An instruction contains a destructive shell command.",
212 Severity::Warning,
213 RuleCategory::Commands,
214 "An agent may run the command without understanding unrecoverable effects.",
215 "git clean -fdx",
216 "Review untracked files, then remove explicit paths."
217 ),
218 rule!(
219 "AGL502",
220 "unknown-command",
221 "A shell command is misspelled or unavailable.",
222 Severity::Warning,
223 RuleCategory::Commands,
224 "An unavailable command makes the documented workflow fail.",
225 "nmp test",
226 "npm test"
227 ),
228 rule!(
229 "AGL503",
230 "missing-package-script",
231 "A package-manager command references a missing script.",
232 Severity::Error,
233 RuleCategory::Commands,
234 "The command cannot run against the repository package metadata.",
235 "npm run chek",
236 "npm run check"
237 ),
238 rule!(
239 "AGL601",
240 "package-manager-conflict",
241 "Instructions conflict with the repository package manager.",
242 Severity::Warning,
243 RuleCategory::Consistency,
244 "Mixing package managers changes lockfiles and dependency resolution.",
245 "Use npm in a pnpm repository.",
246 "Use pnpm in a pnpm repository."
247 ),
248 rule!(
249 "AGL602",
250 "contradictory-instruction",
251 "Two high-confidence instructions contradict each other.",
252 Severity::Warning,
253 RuleCategory::Consistency,
254 "Conflicting guidance makes agent behavior unpredictable.",
255 "Always use npm. Never use npm.",
256 "Always use pnpm."
257 ),
258 rule!(
259 "AGL603",
260 "tooling-mismatch",
261 "Instructions require tooling unsupported by repository metadata.",
262 Severity::Warning,
263 RuleCategory::Consistency,
264 "Stale commands distract agents and fail in clean environments.",
265 "Run pytest in a repository with no Python project.",
266 "Run cargo test in a Rust repository."
267 ),
268];
269
270pub fn definitions() -> &'static [RuleDefinition] {
271 RULES
272}
273
274pub fn definition(id: &str) -> Option<&'static RuleDefinition> {
275 RULES.iter().find(|rule| rule.id.eq_ignore_ascii_case(id))
276}
277
278pub fn default_severity(id: &str) -> Severity {
279 definition(id).map_or(Severity::Warning, |rule| rule.default_severity)
280}