1use regex::Regex;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct FileGuardrails {
15 #[serde(default, skip_serializing_if = "GuardrailConstraints::is_empty")]
17 pub constraints: GuardrailConstraints,
18
19 #[serde(default, skip_serializing_if = "AIBehavior::is_empty")]
21 pub ai_behavior: AIBehavior,
22
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
25 pub temporary: Vec<TemporaryMarker>,
26
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub attempts: Vec<Attempt>,
30
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub checkpoints: Vec<Checkpoint>,
34
35 #[serde(default, skip_serializing_if = "ReviewRequirements::is_empty")]
37 pub review: ReviewRequirements,
38
39 #[serde(default, skip_serializing_if = "QualityMarkers::is_empty")]
41 pub quality: QualityMarkers,
42}
43
44impl FileGuardrails {
45 pub fn is_empty(&self) -> bool {
46 self.constraints.is_empty()
47 && self.ai_behavior.is_empty()
48 && self.temporary.is_empty()
49 && self.attempts.is_empty()
50 && self.checkpoints.is_empty()
51 && self.review.is_empty()
52 && self.quality.is_empty()
53 }
54}
55
56#[derive(Debug, Clone, Default, Serialize, Deserialize)]
62pub struct GuardrailConstraints {
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub style: Option<StyleGuide>,
66
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub frameworks: Vec<FrameworkRequirement>,
70
71 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub compat: Vec<String>,
74
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub requires: Vec<String>,
78
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub forbids: Vec<String>,
82
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub patterns: Vec<String>,
86
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub lint: Vec<String>,
90
91 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub test_required: Vec<String>,
94}
95
96impl GuardrailConstraints {
97 pub fn is_empty(&self) -> bool {
98 self.style.is_none()
99 && self.frameworks.is_empty()
100 && self.compat.is_empty()
101 && self.requires.is_empty()
102 && self.forbids.is_empty()
103 && self.patterns.is_empty()
104 && self.lint.is_empty()
105 && self.test_required.is_empty()
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct StyleGuide {
112 pub name: String,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub url: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct FrameworkRequirement {
120 pub name: String,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub version: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub docs_url: Option<String>,
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
133pub struct AIBehavior {
134 #[serde(default)]
136 pub readonly: bool,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub readonly_reason: Option<String>,
141
142 #[serde(default, skip_serializing_if = "Vec::is_empty")]
144 pub careful: Vec<String>,
145
146 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub ask_before: Vec<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub context: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub approach: Option<String>,
157
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub references: Vec<String>,
161}
162
163impl AIBehavior {
164 pub fn is_empty(&self) -> bool {
165 !self.readonly
166 && self.readonly_reason.is_none()
167 && self.careful.is_empty()
168 && self.ask_before.is_empty()
169 && self.context.is_none()
170 && self.approach.is_none()
171 && self.references.is_empty()
172 }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct TemporaryMarker {
182 pub kind: TemporaryKind,
184
185 pub description: String,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub lines: Option<[usize; 2]>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub until: Option<String>,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "lowercase")]
200pub enum TemporaryKind {
201 Hack,
202 Experiment,
203 Debug,
204 Wip,
205 Temporary,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct Attempt {
215 pub id: String,
217
218 #[serde(skip_serializing_if = "Option::is_none")]
220 pub for_issue: Option<String>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub description: Option<String>,
225
226 pub status: AttemptStatus,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub failure_reason: Option<String>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub lines: Option<[usize; 2]>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub original: Option<String>,
240
241 #[serde(default, skip_serializing_if = "Vec::is_empty")]
243 pub revert_if: Vec<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub change_reason: Option<String>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub timestamp: Option<String>,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "lowercase")]
257pub enum AttemptStatus {
258 Active,
259 Testing,
260 Failed,
261 Verified,
262 Reverted,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct Checkpoint {
268 pub name: String,
270
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub hash: Option<String>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub description: Option<String>,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub timestamp: Option<String>,
282}
283
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
290pub struct ReviewRequirements {
291 #[serde(default, skip_serializing_if = "Vec::is_empty")]
293 pub required: Vec<String>,
294
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub reviewers: Vec<String>,
298
299 #[serde(default, skip_serializing_if = "Vec::is_empty")]
301 pub ai_generated: Vec<AIGeneratedMarker>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub human_verified: Option<HumanVerification>,
306}
307
308impl ReviewRequirements {
309 pub fn is_empty(&self) -> bool {
310 self.required.is_empty()
311 && self.reviewers.is_empty()
312 && self.ai_generated.is_empty()
313 && self.human_verified.is_none()
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct AIGeneratedMarker {
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub model: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub date: Option<String>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub prompt: Option<String>,
326 #[serde(default)]
327 pub needs_review: bool,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub lines: Option<[usize; 2]>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct HumanVerification {
335 pub verified: bool,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub by: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub date: Option<String>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub notes: Option<String>,
342}
343
344#[derive(Debug, Clone, Default, Serialize, Deserialize)]
350pub struct QualityMarkers {
351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
353 pub tech_debt: Vec<TechDebtItem>,
354
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
357 pub complexity: Vec<ComplexityMarker>,
358
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub smells: Vec<String>,
362
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub coverage: Option<String>,
366}
367
368impl QualityMarkers {
369 pub fn is_empty(&self) -> bool {
370 self.tech_debt.is_empty()
371 && self.complexity.is_empty()
372 && self.smells.is_empty()
373 && self.coverage.is_none()
374 }
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct TechDebtItem {
380 pub description: String,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub priority: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub effort: Option<String>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
389pub struct ComplexityMarker {
390 pub level: String,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub reason: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub metric: Option<String>,
395}
396
397pub struct GuardrailParser {
403 patterns: GuardrailPatterns,
404}
405
406struct GuardrailPatterns {
407 style: Regex,
408 framework: Regex,
409 requires: Regex,
410 forbids: Regex,
411 ai_readonly: Regex,
412 ai_careful: Regex,
413 ai_ask: Regex,
414 ai_context: Regex,
415 ai_reference: Regex,
416 hack: Regex,
417 attempt_start: Regex,
418 checkpoint: Regex,
419 review_required: Regex,
420 tech_debt: Regex,
421 test_required: Regex,
422}
423
424impl GuardrailParser {
425 pub fn new() -> Self {
426 Self {
427 patterns: GuardrailPatterns {
428 style: Regex::new(r"@acp:style\s+(\S+)(?:\s+(\S+))?").unwrap(),
429 framework: Regex::new(r"@acp:framework\s+(\S+)(?:@(\S+))?(?:\s+(\S+))?").unwrap(),
430 requires: Regex::new(r"@acp:requires\s+(.+)").unwrap(),
431 forbids: Regex::new(r"@acp:forbids\s+(.+)").unwrap(),
432 ai_readonly: Regex::new(r"@acp:ai-readonly(?:\s+reason:(.+))?").unwrap(),
433 ai_careful: Regex::new(r"@acp:ai-careful\s+(.+)").unwrap(),
434 ai_ask: Regex::new(r"@acp:ai-ask\s+(.+)").unwrap(),
435 ai_context: Regex::new(r"@acp:ai-context\s+(.+)").unwrap(),
436 ai_reference: Regex::new(r"@acp:ai-reference\s+(.+)").unwrap(),
437 hack: Regex::new(r"@acp:hack\s+(.+)").unwrap(),
438 attempt_start: Regex::new(
439 r"@acp:attempt-start\s+id:(\S+)(?:\s+for:(\S+))?(?:\s+description:(.+))?",
440 )
441 .unwrap(),
442 checkpoint: Regex::new(r"@acp:checkpoint\s+name:(\S+)(?:\s+hash:(\S+))?").unwrap(),
443 review_required: Regex::new(r"@acp:review-required\s+(.+)").unwrap(),
444 tech_debt: Regex::new(r"@acp:tech-debt\s+(.+)").unwrap(),
445 test_required: Regex::new(r"@acp:test-required\s+(.+)").unwrap(),
446 },
447 }
448 }
449
450 pub fn parse(&self, content: &str) -> FileGuardrails {
452 let mut guardrails = FileGuardrails::default();
453
454 for (line_num, line) in content.lines().enumerate() {
455 self.parse_line(line, line_num + 1, &mut guardrails);
456 }
457
458 guardrails
459 }
460
461 fn parse_line(&self, line: &str, _line_num: usize, g: &mut FileGuardrails) {
462 if let Some(cap) = self.patterns.style.captures(line) {
464 g.constraints.style = Some(StyleGuide {
465 name: cap.get(1).unwrap().as_str().to_string(),
466 url: cap.get(2).map(|m| m.as_str().to_string()),
467 });
468 }
469
470 if let Some(cap) = self.patterns.framework.captures(line) {
472 g.constraints.frameworks.push(FrameworkRequirement {
473 name: cap.get(1).unwrap().as_str().to_string(),
474 version: cap.get(2).map(|m| m.as_str().to_string()),
475 docs_url: cap.get(3).map(|m| m.as_str().to_string()),
476 });
477 }
478
479 if let Some(cap) = self.patterns.requires.captures(line) {
481 let items: Vec<_> = cap
482 .get(1)
483 .unwrap()
484 .as_str()
485 .split(',')
486 .map(|s| s.trim().to_string())
487 .collect();
488 g.constraints.requires.extend(items);
489 }
490
491 if let Some(cap) = self.patterns.forbids.captures(line) {
493 let items: Vec<_> = cap
494 .get(1)
495 .unwrap()
496 .as_str()
497 .split(',')
498 .map(|s| s.trim().to_string())
499 .collect();
500 g.constraints.forbids.extend(items);
501 }
502
503 if let Some(cap) = self.patterns.ai_readonly.captures(line) {
505 g.ai_behavior.readonly = true;
506 g.ai_behavior.readonly_reason = cap.get(1).map(|m| m.as_str().to_string());
507 }
508
509 if let Some(cap) = self.patterns.ai_careful.captures(line) {
511 g.ai_behavior
512 .careful
513 .push(cap.get(1).unwrap().as_str().to_string());
514 }
515
516 if let Some(cap) = self.patterns.ai_ask.captures(line) {
518 g.ai_behavior
519 .ask_before
520 .push(cap.get(1).unwrap().as_str().to_string());
521 }
522
523 if let Some(cap) = self.patterns.ai_context.captures(line) {
525 let ctx = cap.get(1).unwrap().as_str().to_string();
526 if let Some(existing) = &mut g.ai_behavior.context {
527 existing.push('\n');
528 existing.push_str(&ctx);
529 } else {
530 g.ai_behavior.context = Some(ctx);
531 }
532 }
533
534 if let Some(cap) = self.patterns.ai_reference.captures(line) {
536 g.ai_behavior
537 .references
538 .push(cap.get(1).unwrap().as_str().to_string());
539 }
540
541 if let Some(cap) = self.patterns.hack.captures(line) {
543 g.temporary.push(TemporaryMarker {
544 kind: TemporaryKind::Hack,
545 description: cap.get(1).unwrap().as_str().to_string(),
546 lines: None,
547 until: None,
548 });
549 }
550
551 if let Some(cap) = self.patterns.attempt_start.captures(line) {
553 g.attempts.push(Attempt {
554 id: cap.get(1).unwrap().as_str().to_string(),
555 for_issue: cap.get(2).map(|m| m.as_str().to_string()),
556 description: cap.get(3).map(|m| m.as_str().to_string()),
557 status: AttemptStatus::Active,
558 failure_reason: None,
559 lines: None,
560 original: None,
561 revert_if: vec![],
562 change_reason: None,
563 timestamp: None,
564 });
565 }
566
567 if let Some(cap) = self.patterns.checkpoint.captures(line) {
569 g.checkpoints.push(Checkpoint {
570 name: cap.get(1).unwrap().as_str().to_string(),
571 hash: cap.get(2).map(|m| m.as_str().to_string()),
572 description: None,
573 timestamp: None,
574 });
575 }
576
577 if let Some(cap) = self.patterns.review_required.captures(line) {
579 let items: Vec<_> = cap
580 .get(1)
581 .unwrap()
582 .as_str()
583 .split(',')
584 .map(|s| s.trim().to_string())
585 .collect();
586 g.review.required.extend(items);
587 }
588
589 if let Some(cap) = self.patterns.tech_debt.captures(line) {
591 g.quality.tech_debt.push(TechDebtItem {
592 description: cap.get(1).unwrap().as_str().to_string(),
593 priority: None,
594 effort: None,
595 });
596 }
597
598 if let Some(cap) = self.patterns.test_required.captures(line) {
600 let items: Vec<_> = cap
601 .get(1)
602 .unwrap()
603 .as_str()
604 .split(',')
605 .map(|s| s.trim().to_string())
606 .collect();
607 g.constraints.test_required.extend(items);
608 }
609 }
610}
611
612impl Default for GuardrailParser {
613 fn default() -> Self {
614 Self::new()
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn test_parse_readonly() {
624 let parser = GuardrailParser::new();
625 let content = "// @acp:ai-readonly reason:security-audited\nfunction secure() {}";
626 let guardrails = parser.parse(content);
627
628 assert!(guardrails.ai_behavior.readonly);
629 assert_eq!(
630 guardrails.ai_behavior.readonly_reason,
631 Some("security-audited".to_string())
632 );
633 }
634
635 #[test]
636 fn test_parse_forbids() {
637 let parser = GuardrailParser::new();
638 let content = "// @acp:forbids eval, Function, inline-styles";
639 let guardrails = parser.parse(content);
640
641 assert_eq!(guardrails.constraints.forbids.len(), 3);
642 assert!(guardrails.constraints.forbids.contains(&"eval".to_string()));
643 }
644}