1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct Constraints {
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub style: Option<StyleConstraint>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub mutation: Option<MutationConstraint>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub behavior: Option<BehaviorModifier>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub quality: Option<QualityGate>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub deprecation: Option<DeprecationInfo>,
27
28 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub references: Vec<Reference>,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub directive: Option<String>,
34
35 #[serde(default, skip_serializing_if = "is_false")]
37 pub auto_generated: bool,
38}
39
40fn is_false(b: &bool) -> bool {
41 !*b
42}
43
44impl Constraints {
45 pub fn merge(&self, other: &Constraints) -> Constraints {
48 Constraints {
49 style: other.style.clone().or_else(|| self.style.clone()),
50 mutation: other.mutation.clone().or_else(|| self.mutation.clone()),
51 behavior: other.behavior.clone().or_else(|| self.behavior.clone()),
52 quality: other.quality.clone().or_else(|| self.quality.clone()),
53 deprecation: other
54 .deprecation
55 .clone()
56 .or_else(|| self.deprecation.clone()),
57 references: {
58 let mut refs = self.references.clone();
59 refs.extend(other.references.clone());
60 refs
61 },
62 directive: other.directive.clone().or_else(|| self.directive.clone()),
64 auto_generated: other.directive.is_some() && other.auto_generated
65 || other.directive.is_none() && self.auto_generated,
66 }
67 }
68
69 pub fn can_modify(&self, operation: &str) -> ModifyPermission {
71 if let Some(mutation) = &self.mutation {
72 match mutation.level {
73 LockLevel::Frozen => {
74 return ModifyPermission::Denied {
75 reason: "Code is frozen and cannot be modified".to_string(),
76 };
77 }
78 LockLevel::Restricted => {
79 if let Some(allowed) = &mutation.allowed_operations {
80 if !allowed.iter().any(|op| op == operation) {
81 return ModifyPermission::Denied {
82 reason: format!(
83 "Operation '{}' not allowed. Allowed: {:?}",
84 operation, allowed
85 ),
86 };
87 }
88 }
89 return ModifyPermission::RequiresApproval {
90 reason: "Code is restricted".to_string(),
91 };
92 }
93 LockLevel::ApprovalRequired => {
94 return ModifyPermission::RequiresApproval {
95 reason: mutation.reason.clone().unwrap_or_default(),
96 };
97 }
98 _ => {}
99 }
100 }
101
102 ModifyPermission::Allowed
103 }
104
105 pub fn get_requirements(&self) -> Vec<String> {
107 let mut reqs = Vec::new();
108
109 if let Some(mutation) = &self.mutation {
110 if mutation.requires_tests {
111 reqs.push("tests".to_string());
112 }
113 if mutation.requires_docs {
114 reqs.push("documentation".to_string());
115 }
116 if mutation.requires_approval {
117 reqs.push("approval".to_string());
118 }
119 }
120
121 if let Some(quality) = &self.quality {
122 if quality.tests_required {
123 reqs.push("tests".to_string());
124 }
125 if quality.security_review {
126 reqs.push("security-review".to_string());
127 }
128 }
129
130 reqs.sort();
131 reqs.dedup();
132 reqs
133 }
134}
135
136#[derive(Debug, Clone)]
138pub enum ModifyPermission {
139 Allowed,
140 RequiresApproval { reason: String },
141 Denied { reason: String },
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct StyleConstraint {
147 pub guide: String,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub reference: Option<String>,
153
154 #[serde(default)]
156 pub rules: Vec<String>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub linter: Option<String>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct MutationConstraint {
166 #[serde(default)]
168 pub level: LockLevel,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub reason: Option<String>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub contact: Option<String>,
177
178 #[serde(default)]
180 pub requires_approval: bool,
181
182 #[serde(default)]
184 pub requires_tests: bool,
185
186 #[serde(default)]
188 pub requires_docs: bool,
189
190 #[serde(skip_serializing_if = "Option::is_none")]
192 pub max_lines_changed: Option<usize>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 pub allowed_operations: Option<Vec<String>>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub forbidden_operations: Option<Vec<String>>,
201}
202
203#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "kebab-case")]
206pub enum LockLevel {
207 Frozen,
209 Restricted,
211 ApprovalRequired,
213 TestsRequired,
215 DocsRequired,
217 ReviewRequired,
219 #[default]
221 Normal,
222 Experimental,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct BehaviorModifier {
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub approach: Option<Approach>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub priority: Option<Priority>,
236
237 #[serde(default)]
239 pub explain: bool,
240
241 #[serde(default)]
243 pub step_by_step: bool,
244
245 #[serde(default)]
247 pub verify: bool,
248
249 #[serde(default)]
251 pub ask_first: bool,
252}
253
254#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
256#[serde(rename_all = "lowercase")]
257pub enum Approach {
258 Conservative,
259 Aggressive,
260 Minimal,
261 Comprehensive,
262}
263
264#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
266#[serde(rename_all = "lowercase")]
267pub enum Priority {
268 Correctness,
269 Performance,
270 Readability,
271 Security,
272 Compatibility,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct QualityGate {
278 #[serde(default)]
279 pub tests_required: bool,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub min_coverage: Option<f64>,
283
284 #[serde(default)]
285 pub security_review: bool,
286
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub max_complexity: Option<u32>,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub accessibility: Option<String>,
292
293 #[serde(default)]
294 pub browser_support: Vec<String>,
295
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub performance_budget: Option<PerformanceBudget>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct PerformanceBudget {
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub max_time_ms: Option<u64>,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub max_memory_mb: Option<u64>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct DeprecationInfo {
313 #[serde(default)]
314 pub deprecated: bool,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub since: Option<String>,
318
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub removal_version: Option<String>,
321
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub replacement: Option<String>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub migration_guide: Option<String>,
327
328 #[serde(default)]
329 pub action: DeprecationAction,
330}
331
332#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
334#[serde(rename_all = "kebab-case")]
335pub enum DeprecationAction {
336 #[default]
337 Warn,
338 SuggestMigration,
339 AutoMigrate,
340 Block,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct Reference {
346 pub url: String,
347
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub description: Option<String>,
350
351 #[serde(default)]
353 pub fetch: bool,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct HackMarker {
359 pub id: String,
360
361 #[serde(rename = "type")]
362 pub hack_type: HackType,
363
364 pub file: String,
365
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub line: Option<usize>,
368
369 pub created_at: DateTime<Utc>,
370
371 #[serde(skip_serializing_if = "Option::is_none")]
372 pub author: Option<String>,
373
374 pub reason: String,
375
376 #[serde(skip_serializing_if = "Option::is_none")]
377 pub ticket: Option<String>,
378
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub expires: Option<DateTime<Utc>>,
381
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub original_code: Option<String>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub revert_instructions: Option<String>,
387}
388
389impl HackMarker {
390 pub fn is_expired(&self) -> bool {
391 self.expires.map(|e| e < Utc::now()).unwrap_or(false)
392 }
393}
394
395#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
397#[serde(rename_all = "lowercase")]
398pub enum HackType {
399 Hack,
400 Workaround,
401 Debug,
402 Experiment,
403 Temporary,
404 TestOnly,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct DebugSession {
410 pub id: String,
411 pub started_at: DateTime<Utc>,
412 pub problem: String,
413
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub hypothesis: Option<String>,
416
417 #[serde(default)]
418 pub attempts: Vec<DebugAttempt>,
419
420 #[serde(default)]
421 pub status: DebugStatus,
422
423 #[serde(skip_serializing_if = "Option::is_none")]
424 pub resolution: Option<String>,
425
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub resolved_at: Option<DateTime<Utc>>,
428}
429
430impl DebugSession {
431 pub fn new(id: impl Into<String>, problem: impl Into<String>) -> Self {
432 Self {
433 id: id.into(),
434 started_at: Utc::now(),
435 problem: problem.into(),
436 hypothesis: None,
437 attempts: Vec::new(),
438 status: DebugStatus::Active,
439 resolution: None,
440 resolved_at: None,
441 }
442 }
443
444 pub fn add_attempt(
445 &mut self,
446 hypothesis: impl Into<String>,
447 change: impl Into<String>,
448 ) -> usize {
449 let attempt_id = self.attempts.len() + 1;
450 self.attempts.push(DebugAttempt {
451 attempt_id,
452 timestamp: Utc::now(),
453 hypothesis: hypothesis.into(),
454 change: change.into(),
455 files_modified: Vec::new(),
456 diff: None,
457 result: DebugResult::Unknown,
458 observations: None,
459 keep: false,
460 reverted: false,
461 });
462 attempt_id
463 }
464
465 pub fn record_result(
466 &mut self,
467 attempt_id: usize,
468 result: DebugResult,
469 observations: Option<String>,
470 ) {
471 if let Some(attempt) = self
472 .attempts
473 .iter_mut()
474 .find(|a| a.attempt_id == attempt_id)
475 {
476 attempt.result = result;
477 attempt.observations = observations;
478
479 if result == DebugResult::Success {
480 attempt.keep = true;
481 }
482 }
483 }
484
485 pub fn revert_attempt(&mut self, attempt_id: usize) -> Option<&DebugAttempt> {
486 if let Some(attempt) = self
487 .attempts
488 .iter_mut()
489 .find(|a| a.attempt_id == attempt_id)
490 {
491 attempt.reverted = true;
492 attempt.keep = false;
493 return Some(attempt);
494 }
495 None
496 }
497
498 pub fn resolve(&mut self, resolution: impl Into<String>) {
499 self.status = DebugStatus::Resolved;
500 self.resolution = Some(resolution.into());
501 self.resolved_at = Some(Utc::now());
502 }
503
504 pub fn get_kept_attempts(&self) -> Vec<&DebugAttempt> {
505 self.attempts
506 .iter()
507 .filter(|a| a.keep && !a.reverted)
508 .collect()
509 }
510
511 pub fn get_reverted_attempts(&self) -> Vec<&DebugAttempt> {
512 self.attempts.iter().filter(|a| a.reverted).collect()
513 }
514}
515
516#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "lowercase")]
519pub enum DebugStatus {
520 #[default]
521 Active,
522 Paused,
523 Resolved,
524 Abandoned,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct DebugAttempt {
530 pub attempt_id: usize,
531 pub timestamp: DateTime<Utc>,
532 pub hypothesis: String,
533 pub change: String,
534
535 #[serde(default)]
536 pub files_modified: Vec<String>,
537
538 #[serde(skip_serializing_if = "Option::is_none")]
539 pub diff: Option<String>,
540
541 #[serde(default)]
542 pub result: DebugResult,
543
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub observations: Option<String>,
546
547 #[serde(default)]
548 pub keep: bool,
549
550 #[serde(default)]
551 pub reverted: bool,
552}
553
554#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(rename_all = "lowercase")]
557pub enum DebugResult {
558 Success,
559 Failure,
560 Partial,
561 #[default]
562 Unknown,
563}
564
565#[derive(Debug, Clone, Default, Serialize, Deserialize)]
567pub struct ConstraintIndex {
568 #[serde(default)]
570 pub by_file: HashMap<String, Constraints>,
571
572 #[serde(default)]
574 pub hacks: Vec<HackMarker>,
575
576 #[serde(default)]
578 pub debug_sessions: Vec<DebugSession>,
579
580 #[serde(default)]
582 pub by_lock_level: HashMap<String, Vec<String>>,
583}
584
585impl ConstraintIndex {
586 pub fn get_effective(&self, file: &str, project_defaults: &Constraints) -> Constraints {
588 let file_constraints = self.by_file.get(file).cloned().unwrap_or_default();
589 project_defaults.merge(&file_constraints)
590 }
591
592 pub fn get_expired_hacks(&self) -> Vec<&HackMarker> {
594 self.hacks.iter().filter(|h| h.is_expired()).collect()
595 }
596
597 pub fn get_active_debug_sessions(&self) -> Vec<&DebugSession> {
599 self.debug_sessions
600 .iter()
601 .filter(|s| s.status == DebugStatus::Active)
602 .collect()
603 }
604
605 pub fn get_frozen_files(&self) -> Vec<&str> {
607 self.by_lock_level
608 .get("frozen")
609 .map(|v| v.iter().map(|s| s.as_str()).collect())
610 .unwrap_or_default()
611 }
612
613 pub fn get_restricted_files(&self) -> Vec<&str> {
615 self.by_lock_level
616 .get("restricted")
617 .map(|v| v.iter().map(|s| s.as_str()).collect())
618 .unwrap_or_default()
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn test_constraint_merge() {
628 let base = Constraints {
629 style: Some(StyleConstraint {
630 guide: "google-typescript".to_string(),
631 reference: None,
632 rules: vec![],
633 linter: None,
634 }),
635 mutation: None,
636 behavior: None,
637 quality: None,
638 deprecation: None,
639 references: vec![],
640 directive: Some("Base directive".to_string()),
641 auto_generated: false,
642 };
643
644 let override_constraints = Constraints {
645 style: None,
646 mutation: Some(MutationConstraint {
647 level: LockLevel::Restricted,
648 reason: Some("Security".to_string()),
649 contact: None,
650 requires_approval: true,
651 requires_tests: false,
652 requires_docs: false,
653 max_lines_changed: None,
654 allowed_operations: None,
655 forbidden_operations: None,
656 }),
657 behavior: None,
658 quality: None,
659 deprecation: None,
660 references: vec![],
661 directive: Some("Override directive".to_string()),
662 auto_generated: false,
663 };
664
665 let merged = base.merge(&override_constraints);
666
667 assert!(merged.style.is_some());
669 assert_eq!(merged.style.unwrap().guide, "google-typescript");
670
671 assert!(merged.mutation.is_some());
673 assert_eq!(merged.mutation.unwrap().level, LockLevel::Restricted);
674 }
675
676 #[test]
677 fn test_debug_session() {
678 let mut session = DebugSession::new("test-123", "Something is broken");
679
680 let attempt1 = session.add_attempt("Maybe it's X", "Changed X");
681 session.record_result(
682 attempt1,
683 DebugResult::Failure,
684 Some("Nope, not X".to_string()),
685 );
686
687 let attempt2 = session.add_attempt("Maybe it's Y", "Changed Y");
688 session.record_result(
689 attempt2,
690 DebugResult::Success,
691 Some("Yes, Y was the issue!".to_string()),
692 );
693
694 session.revert_attempt(attempt1);
696
697 assert_eq!(session.get_kept_attempts().len(), 1);
698 assert_eq!(session.get_reverted_attempts().len(), 1);
699
700 session.resolve("Fixed by changing Y");
701 assert_eq!(session.status, DebugStatus::Resolved);
702 }
703}