1use crate::ai::StoredAiEvaluation;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::env;
6use std::fmt;
7use ts_rs_forge::TS;
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
12pub enum RequirementStatus {
13 Draft,
14 Approved,
15 Planned,
16 InProgress,
17 Completed,
18 Rejected,
19}
20
21impl fmt::Display for RequirementStatus {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 RequirementStatus::Draft => write!(f, "Draft"),
25 RequirementStatus::Approved => write!(f, "Approved"),
26 RequirementStatus::Planned => write!(f, "Planned"),
27 RequirementStatus::InProgress => write!(f, "In Progress"),
28 RequirementStatus::Completed => write!(f, "Completed"),
29 RequirementStatus::Rejected => write!(f, "Rejected"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
36pub enum RequirementPriority {
37 High,
38 Medium,
39 Low,
40}
41
42impl fmt::Display for RequirementPriority {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 RequirementPriority::High => write!(f, "High"),
46 RequirementPriority::Medium => write!(f, "Medium"),
47 RequirementPriority::Low => write!(f, "Low"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
54pub enum RequirementType {
55 Functional,
57 NonFunctional,
58 System,
59 User,
60 ChangeRequest,
61 Bug,
62 Epic,
64 Story,
65 Task,
66 Spike,
67 Sprint, Folder,
70 Meta,
72}
73
74impl fmt::Display for RequirementType {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 RequirementType::Functional => write!(f, "Functional"),
78 RequirementType::NonFunctional => write!(f, "Non-Functional"),
79 RequirementType::System => write!(f, "System"),
80 RequirementType::User => write!(f, "User"),
81 RequirementType::ChangeRequest => write!(f, "Change Request"),
82 RequirementType::Bug => write!(f, "Bug"),
83 RequirementType::Epic => write!(f, "Epic"),
84 RequirementType::Story => write!(f, "Story"),
85 RequirementType::Task => write!(f, "Task"),
86 RequirementType::Spike => write!(f, "Spike"),
87 RequirementType::Sprint => write!(f, "Sprint"),
88 RequirementType::Folder => write!(f, "Folder"),
89 RequirementType::Meta => write!(f, "Meta"),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
96pub enum MetaSubtype {
97 Prompt,
99 Skill,
101 Command,
103 Template,
105 Config,
107}
108
109impl fmt::Display for MetaSubtype {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 match self {
112 MetaSubtype::Prompt => write!(f, "Prompt"),
113 MetaSubtype::Skill => write!(f, "Skill"),
114 MetaSubtype::Command => write!(f, "Command"),
115 MetaSubtype::Template => write!(f, "Template"),
116 MetaSubtype::Config => write!(f, "Config"),
117 }
118 }
119}
120
121impl MetaSubtype {
122 pub fn from_str(s: &str) -> Option<Self> {
124 match s.to_lowercase().as_str() {
125 "prompt" => Some(MetaSubtype::Prompt),
126 "skill" => Some(MetaSubtype::Skill),
127 "command" => Some(MetaSubtype::Command),
128 "template" => Some(MetaSubtype::Template),
129 "config" => Some(MetaSubtype::Config),
130 _ => None,
131 }
132 }
133
134 pub fn all() -> Vec<MetaSubtype> {
136 vec![
137 MetaSubtype::Prompt,
138 MetaSubtype::Skill,
139 MetaSubtype::Command,
140 MetaSubtype::Template,
141 MetaSubtype::Config,
142 ]
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
148pub enum RelationshipType {
149 Parent,
151 Child,
153 Duplicate,
155 Verifies,
157 VerifiedBy,
159 References,
161 Custom(String),
163}
164
165impl fmt::Display for RelationshipType {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 match self {
168 RelationshipType::Parent => write!(f, "parent"),
169 RelationshipType::Child => write!(f, "child"),
170 RelationshipType::Duplicate => write!(f, "duplicate"),
171 RelationshipType::Verifies => write!(f, "verifies"),
172 RelationshipType::VerifiedBy => write!(f, "verified-by"),
173 RelationshipType::References => write!(f, "references"),
174 RelationshipType::Custom(name) => write!(f, "{}", name),
175 }
176 }
177}
178
179impl RelationshipType {
180 pub fn from_str(s: &str) -> Self {
182 match s.to_lowercase().as_str() {
183 "parent" => RelationshipType::Parent,
184 "child" => RelationshipType::Child,
185 "duplicate" => RelationshipType::Duplicate,
186 "verifies" => RelationshipType::Verifies,
187 "verified-by" | "verified_by" | "verifiedby" => RelationshipType::VerifiedBy,
188 "references" => RelationshipType::References,
189 _ => RelationshipType::Custom(s.to_string()),
190 }
191 }
192
193 pub fn inverse(&self) -> Option<Self> {
195 match self {
196 RelationshipType::Parent => Some(RelationshipType::Child),
197 RelationshipType::Child => Some(RelationshipType::Parent),
198 RelationshipType::Verifies => Some(RelationshipType::VerifiedBy),
199 RelationshipType::VerifiedBy => Some(RelationshipType::Verifies),
200 RelationshipType::Duplicate => Some(RelationshipType::Duplicate),
201 RelationshipType::References => None,
202 RelationshipType::Custom(_) => None,
203 }
204 }
205
206 pub fn name(&self) -> String {
208 match self {
209 RelationshipType::Parent => "parent".to_string(),
210 RelationshipType::Child => "child".to_string(),
211 RelationshipType::Duplicate => "duplicate".to_string(),
212 RelationshipType::Verifies => "verifies".to_string(),
213 RelationshipType::VerifiedBy => "verified_by".to_string(),
214 RelationshipType::References => "references".to_string(),
215 RelationshipType::Custom(name) => name.clone(),
216 }
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
226#[serde(rename_all = "lowercase")]
227pub enum CustomFieldType {
228 Text,
230 TextArea,
232 Select,
234 Boolean,
236 Date,
238 User,
240 Requirement,
242 Number,
244}
245
246impl Default for CustomFieldType {
247 fn default() -> Self {
248 CustomFieldType::Text
249 }
250}
251
252impl fmt::Display for CustomFieldType {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 match self {
255 CustomFieldType::Text => write!(f, "Text"),
256 CustomFieldType::TextArea => write!(f, "Text Area"),
257 CustomFieldType::Select => write!(f, "Select"),
258 CustomFieldType::Boolean => write!(f, "Boolean"),
259 CustomFieldType::Date => write!(f, "Date"),
260 CustomFieldType::User => write!(f, "User Reference"),
261 CustomFieldType::Requirement => write!(f, "Requirement Reference"),
262 CustomFieldType::Number => write!(f, "Number"),
263 }
264 }
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
269pub struct CustomFieldDefinition {
270 pub name: String,
272
273 pub label: String,
275
276 #[serde(default)]
278 pub field_type: CustomFieldType,
279
280 #[serde(default)]
282 pub required: bool,
283
284 #[serde(default, skip_serializing_if = "Vec::is_empty")]
286 pub options: Vec<String>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub default_value: Option<String>,
291
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub description: Option<String>,
295
296 #[serde(default)]
298 pub order: i32,
299}
300
301impl CustomFieldDefinition {
302 pub fn text(name: impl Into<String>, label: impl Into<String>) -> Self {
304 Self {
305 name: name.into(),
306 label: label.into(),
307 field_type: CustomFieldType::Text,
308 required: false,
309 options: Vec::new(),
310 default_value: None,
311 description: None,
312 order: 0,
313 }
314 }
315
316 pub fn select(name: impl Into<String>, label: impl Into<String>, options: Vec<String>) -> Self {
318 Self {
319 name: name.into(),
320 label: label.into(),
321 field_type: CustomFieldType::Select,
322 required: false,
323 options,
324 default_value: None,
325 description: None,
326 order: 0,
327 }
328 }
329
330 pub fn user_ref(name: impl Into<String>, label: impl Into<String>) -> Self {
332 Self {
333 name: name.into(),
334 label: label.into(),
335 field_type: CustomFieldType::User,
336 required: false,
337 options: Vec::new(),
338 default_value: None,
339 description: None,
340 order: 0,
341 }
342 }
343
344 pub fn textarea(name: impl Into<String>, label: impl Into<String>) -> Self {
346 Self {
347 name: name.into(),
348 label: label.into(),
349 field_type: CustomFieldType::TextArea,
350 required: false,
351 options: Vec::new(),
352 default_value: None,
353 description: None,
354 order: 0,
355 }
356 }
357
358 pub fn number(name: impl Into<String>, label: impl Into<String>) -> Self {
360 Self {
361 name: name.into(),
362 label: label.into(),
363 field_type: CustomFieldType::Number,
364 required: false,
365 options: Vec::new(),
366 default_value: None,
367 description: None,
368 order: 0,
369 }
370 }
371
372 pub fn required(mut self) -> Self {
374 self.required = true;
375 self
376 }
377
378 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
380 self.description = Some(desc.into());
381 self
382 }
383
384 pub fn with_order(mut self, order: i32) -> Self {
386 self.order = order;
387 self
388 }
389
390 pub fn with_default(mut self, value: impl Into<String>) -> Self {
392 self.default_value = Some(value.into());
393 self
394 }
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
399pub struct CustomTypeDefinition {
400 pub name: String,
402
403 pub display_name: String,
405
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub description: Option<String>,
409
410 #[serde(skip_serializing_if = "Option::is_none")]
412 pub prefix: Option<String>,
413
414 #[serde(default, skip_serializing_if = "Vec::is_empty")]
416 pub statuses: Vec<String>,
417
418 #[serde(default, skip_serializing_if = "Vec::is_empty")]
420 pub priorities: Vec<String>,
421
422 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub custom_fields: Vec<CustomFieldDefinition>,
425
426 #[serde(default)]
428 pub built_in: bool,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub color: Option<String>,
433
434 #[serde(default)]
438 pub stateless: bool,
439}
440
441impl CustomTypeDefinition {
442 pub fn new(name: impl Into<String>, display_name: impl Into<String>) -> Self {
444 Self {
445 name: name.into(),
446 display_name: display_name.into(),
447 description: None,
448 prefix: None,
449 statuses: Vec::new(),
450 priorities: Vec::new(),
451 custom_fields: Vec::new(),
452 built_in: false,
453 color: None,
454 stateless: false,
455 }
456 }
457
458 pub fn built_in(name: impl Into<String>, display_name: impl Into<String>) -> Self {
460 Self {
461 name: name.into(),
462 display_name: display_name.into(),
463 description: None,
464 prefix: None,
465 statuses: Vec::new(),
466 priorities: Vec::new(),
467 custom_fields: Vec::new(),
468 built_in: true,
469 color: None,
470 stateless: false,
471 }
472 }
473
474 pub fn built_in_stateless(name: impl Into<String>, display_name: impl Into<String>) -> Self {
476 Self {
477 name: name.into(),
478 display_name: display_name.into(),
479 description: None,
480 prefix: None,
481 statuses: Vec::new(),
482 priorities: Vec::new(),
483 custom_fields: Vec::new(),
484 built_in: true,
485 color: None,
486 stateless: true,
487 }
488 }
489
490 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
492 self.prefix = Some(prefix.into());
493 self
494 }
495
496 pub fn with_statuses(mut self, statuses: Vec<&str>) -> Self {
498 self.statuses = statuses.into_iter().map(String::from).collect();
499 self
500 }
501
502 pub fn with_priorities(mut self, priorities: Vec<&str>) -> Self {
504 self.priorities = priorities.into_iter().map(String::from).collect();
505 self
506 }
507
508 pub fn with_field(mut self, field: CustomFieldDefinition) -> Self {
510 self.custom_fields.push(field);
511 self
512 }
513
514 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
516 self.description = Some(desc.into());
517 self
518 }
519
520 pub fn with_color(mut self, color: impl Into<String>) -> Self {
522 self.color = Some(color.into());
523 self
524 }
525
526 pub fn as_stateless(mut self) -> Self {
528 self.stateless = true;
529 self
530 }
531
532 pub fn get_statuses(&self) -> Vec<String> {
534 if self.statuses.is_empty() {
535 vec![
537 "Draft".to_string(),
538 "Approved".to_string(),
539 "Completed".to_string(),
540 "Rejected".to_string(),
541 ]
542 } else {
543 self.statuses.clone()
544 }
545 }
546
547 pub fn get_priorities(&self) -> Vec<String> {
549 if self.priorities.is_empty() {
550 vec!["High".to_string(), "Medium".to_string(), "Low".to_string()]
552 } else {
553 self.priorities.clone()
554 }
555 }
556}
557
558pub fn default_type_definitions() -> Vec<CustomTypeDefinition> {
560 vec![
561 CustomTypeDefinition::built_in("Functional", "Functional")
562 .with_prefix("FR")
563 .with_description("Functional requirements describing system behavior"),
564 CustomTypeDefinition::built_in("NonFunctional", "Non-Functional")
565 .with_prefix("NFR")
566 .with_description("Non-functional requirements (performance, security, etc.)"),
567 CustomTypeDefinition::built_in("System", "System")
568 .with_prefix("SYS")
569 .with_description("System-level requirements"),
570 CustomTypeDefinition::built_in("User", "User Story")
571 .with_prefix("US")
572 .with_description("User stories and user requirements"),
573 CustomTypeDefinition::built_in("ChangeRequest", "Change Request")
574 .with_prefix("CR")
575 .with_description("Change requests for existing functionality")
576 .with_statuses(vec![
577 "Draft",
578 "Submitted",
579 "Under Review",
580 "Approved",
581 "Rejected",
582 "In Progress",
583 "Implemented",
584 "Verified",
585 "Closed",
586 ])
587 .with_color("#9333ea")
588 .with_field(
589 CustomFieldDefinition::select(
590 "impact",
591 "Impact Level",
592 vec![
593 "Low".to_string(),
594 "Medium".to_string(),
595 "High".to_string(),
596 "Critical".to_string(),
597 ],
598 )
599 .with_description("Impact of this change on the system")
600 .with_order(1),
601 )
602 .with_field(
603 CustomFieldDefinition::user_ref("requested_by", "Requested By")
604 .with_description("User who requested this change")
605 .with_order(2),
606 )
607 .with_field(
608 CustomFieldDefinition::text("target_release", "Target Release")
609 .with_description("Target release version for this change")
610 .with_order(3),
611 )
612 .with_field(
613 CustomFieldDefinition::text("justification", "Justification")
614 .required()
615 .with_description("Business justification for the change")
616 .with_order(4),
617 ),
618 CustomTypeDefinition::built_in("Bug", "Bug")
620 .with_prefix("BUG")
621 .with_description("Bug reports and defects")
622 .with_statuses(vec![
623 "New",
624 "Confirmed",
625 "In Progress",
626 "Fixed",
627 "Verified",
628 "Closed",
629 "Won't Fix",
630 ])
631 .with_color("#dc2626")
632 .with_field(
633 CustomFieldDefinition::select(
634 "severity",
635 "Severity",
636 vec![
637 "Critical".to_string(),
638 "Major".to_string(),
639 "Minor".to_string(),
640 "Trivial".to_string(),
641 ],
642 )
643 .with_description("Severity of the bug")
644 .with_order(1),
645 )
646 .with_field(
647 CustomFieldDefinition::text("steps_to_reproduce", "Steps to Reproduce")
648 .with_description("Steps to reproduce the bug")
649 .with_order(2),
650 )
651 .with_field(
652 CustomFieldDefinition::text("expected_behavior", "Expected Behavior")
653 .with_description("What should happen")
654 .with_order(3),
655 )
656 .with_field(
657 CustomFieldDefinition::text("actual_behavior", "Actual Behavior")
658 .with_description("What actually happens")
659 .with_order(4),
660 ),
661 CustomTypeDefinition::built_in("Epic", "Epic")
663 .with_prefix("EPIC")
664 .with_description("Large feature or initiative spanning multiple stories")
665 .with_statuses(vec!["Draft", "Ready", "In Progress", "Done"])
666 .with_color("#7c3aed")
667 .with_field(
668 CustomFieldDefinition::text("business_value", "Business Value")
669 .with_description("Business value or benefit of this epic")
670 .with_order(1),
671 )
672 .with_field(
673 CustomFieldDefinition::text("target_release", "Target Release")
674 .with_description("Target release or milestone")
675 .with_order(2),
676 )
677 .with_field(
678 CustomFieldDefinition::number("story_points", "Story Points")
679 .with_description("Estimated story points")
680 .with_order(3),
681 ),
682 CustomTypeDefinition::built_in("Story", "Story")
684 .with_prefix("STORY")
685 .with_description("User story for agile development")
686 .with_statuses(vec!["Draft", "Ready", "In Progress", "In Review", "Done"])
687 .with_color("#10b981")
688 .with_field(
689 CustomFieldDefinition::textarea("acceptance_criteria", "Acceptance Criteria")
690 .with_description("Criteria that must be met for the story to be complete")
691 .with_order(1),
692 )
693 .with_field(
694 CustomFieldDefinition::number("story_points", "Story Points")
695 .with_description("Estimated story points")
696 .with_order(2),
697 )
698 .with_field(
699 CustomFieldDefinition::user_ref("assignee", "Assignee")
700 .with_description("Person assigned to this story")
701 .with_order(3),
702 ),
703 CustomTypeDefinition::built_in("Task", "Task")
704 .with_prefix("TASK")
705 .with_description("Implementation task or work item")
706 .with_statuses(vec!["To Do", "In Progress", "In Review", "Done"])
707 .with_color("#0891b2")
708 .with_field(
709 CustomFieldDefinition::number("estimate_hours", "Estimate (hours)")
710 .with_description("Estimated hours to complete")
711 .with_order(1),
712 )
713 .with_field(
714 CustomFieldDefinition::user_ref("assignee", "Assignee")
715 .with_description("Person assigned to this task")
716 .with_order(2),
717 ),
718 CustomTypeDefinition::built_in("Spike", "Spike")
719 .with_prefix("SPIKE")
720 .with_description("Research or investigation task with time-boxed exploration")
721 .with_statuses(vec!["Planned", "In Progress", "Completed"])
722 .with_color("#ca8a04")
723 .with_field(
724 CustomFieldDefinition::text("research_question", "Research Question")
725 .required()
726 .with_description("The question or problem to investigate")
727 .with_order(1),
728 )
729 .with_field(
730 CustomFieldDefinition::text("time_box", "Time Box")
731 .with_description("Time allocated for investigation (e.g., '2 days')")
732 .with_order(2),
733 )
734 .with_field(
735 CustomFieldDefinition::textarea("findings", "Findings")
736 .with_description("Results and conclusions from the investigation")
737 .with_order(3),
738 )
739 .with_field(
740 CustomFieldDefinition::text("recommendation", "Recommendation")
741 .with_description("Recommended next steps based on findings")
742 .with_order(4),
743 ),
744 CustomTypeDefinition::built_in("Sprint", "Sprint")
746 .with_prefix("SPRINT")
747 .with_description("Time-boxed iteration for work planning")
748 .with_statuses(vec!["Draft", "In Progress", "Completed", "Archived"])
749 .with_color("#7c3aed")
750 .with_field(
751 CustomFieldDefinition::number("sprint_number", "Sprint Number")
752 .with_description("Sequential sprint number")
753 .with_order(1),
754 )
755 .with_field(
756 CustomFieldDefinition::text("start_date", "Start Date")
757 .with_description("Sprint start date (YYYY-MM-DD)")
758 .with_order(2),
759 )
760 .with_field(
761 CustomFieldDefinition::text("end_date", "End Date")
762 .with_description("Sprint end date (YYYY-MM-DD)")
763 .with_order(3),
764 )
765 .with_field(
766 CustomFieldDefinition::text("sprint_goal", "Sprint Goal")
767 .with_description("Goal or theme for this sprint")
768 .with_order(4),
769 )
770 .with_field(
771 CustomFieldDefinition::number("velocity", "Planned Velocity")
772 .with_description("Planned story points for this sprint")
773 .with_order(5),
774 ),
775 CustomTypeDefinition::built_in_stateless("Folder", "Folder")
777 .with_prefix("FLD")
778 .with_description("Organizational container for grouping related requirements")
779 .with_color("#6b7280"),
780 CustomTypeDefinition::built_in_stateless("Meta", "Meta")
782 .with_prefix("META")
783 .with_description("Database configuration, prompts, skills, and templates")
784 .with_color("#8b5cf6"),
785 ]
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
794pub enum Cardinality {
795 OneToOne,
797 OneToMany,
799 ManyToOne,
801 #[default]
803 ManyToMany,
804}
805
806impl fmt::Display for Cardinality {
807 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
808 match self {
809 Cardinality::OneToOne => write!(f, "1:1"),
810 Cardinality::OneToMany => write!(f, "1:N"),
811 Cardinality::ManyToOne => write!(f, "N:1"),
812 Cardinality::ManyToMany => write!(f, "N:N"),
813 }
814 }
815}
816
817impl Cardinality {
818 pub fn from_str(s: &str) -> Self {
820 match s.to_lowercase().replace(" ", "").as_str() {
821 "1:1" | "one_to_one" | "onetoone" => Cardinality::OneToOne,
822 "1:n" | "one_to_many" | "onetomany" => Cardinality::OneToMany,
823 "n:1" | "many_to_one" | "manytoone" => Cardinality::ManyToOne,
824 _ => Cardinality::ManyToMany,
825 }
826 }
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
831pub struct RelationshipDefinition {
832 pub name: String,
834
835 pub display_name: String,
837
838 #[serde(default)]
840 pub description: String,
841
842 #[serde(default)]
845 pub inverse: Option<String>,
846
847 #[serde(default)]
850 pub symmetric: bool,
851
852 #[serde(default)]
854 pub cardinality: Cardinality,
855
856 #[serde(default)]
859 pub source_types: Vec<String>,
860
861 #[serde(default)]
864 pub target_types: Vec<String>,
865
866 #[serde(default)]
868 pub built_in: bool,
869
870 #[serde(default)]
872 pub color: Option<String>,
873
874 #[serde(default)]
876 pub icon: Option<String>,
877}
878
879impl RelationshipDefinition {
880 pub fn new(name: &str, display_name: &str) -> Self {
882 Self {
883 name: name.to_lowercase(),
884 display_name: display_name.to_string(),
885 description: String::new(),
886 inverse: None,
887 symmetric: false,
888 cardinality: Cardinality::ManyToMany,
889 source_types: Vec::new(),
890 target_types: Vec::new(),
891 built_in: false,
892 color: None,
893 icon: None,
894 }
895 }
896
897 pub fn built_in(name: &str, display_name: &str, description: &str) -> Self {
899 Self {
900 name: name.to_lowercase(),
901 display_name: display_name.to_string(),
902 description: description.to_string(),
903 inverse: None,
904 symmetric: false,
905 cardinality: Cardinality::ManyToMany,
906 source_types: Vec::new(),
907 target_types: Vec::new(),
908 built_in: true,
909 color: None,
910 icon: None,
911 }
912 }
913
914 pub fn with_inverse(mut self, inverse: &str) -> Self {
916 self.inverse = Some(inverse.to_lowercase());
917 self
918 }
919
920 pub fn with_symmetric(mut self, symmetric: bool) -> Self {
922 self.symmetric = symmetric;
923 self
924 }
925
926 pub fn with_cardinality(mut self, cardinality: Cardinality) -> Self {
928 self.cardinality = cardinality;
929 self
930 }
931
932 pub fn with_source_types(mut self, types: Vec<String>) -> Self {
934 self.source_types = types;
935 self
936 }
937
938 pub fn with_target_types(mut self, types: Vec<String>) -> Self {
940 self.target_types = types;
941 self
942 }
943
944 pub fn with_color(mut self, color: &str) -> Self {
946 self.color = Some(color.to_string());
947 self
948 }
949
950 pub fn defaults() -> Vec<RelationshipDefinition> {
952 vec![
953 RelationshipDefinition::built_in("parent", "Parent", "Hierarchical parent requirement")
955 .with_inverse("child")
956 .with_cardinality(Cardinality::OneToMany), RelationshipDefinition::built_in("child", "Child", "Hierarchical child requirement")
958 .with_inverse("parent")
959 .with_cardinality(Cardinality::ManyToOne), RelationshipDefinition::built_in(
961 "verifies",
962 "Verifies",
963 "Test or verification relationship",
964 )
965 .with_inverse("verified_by"),
966 RelationshipDefinition::built_in(
967 "verified_by",
968 "Verified By",
969 "Verified by a test requirement",
970 )
971 .with_inverse("verifies"),
972 RelationshipDefinition::built_in(
973 "duplicate",
974 "Duplicate",
975 "Marks requirements as duplicates",
976 )
977 .with_symmetric(true),
978 RelationshipDefinition::built_in("references", "References", "General reference link"),
979 RelationshipDefinition::built_in("depends_on", "Depends On", "Dependency relationship")
980 .with_inverse("dependency_of"),
981 RelationshipDefinition::built_in(
982 "dependency_of",
983 "Dependency Of",
984 "Inverse dependency relationship",
985 )
986 .with_inverse("depends_on"),
987 RelationshipDefinition::built_in(
988 "implements",
989 "Implements",
990 "Implementation relationship",
991 )
992 .with_inverse("implemented_by"),
993 RelationshipDefinition::built_in(
994 "implemented_by",
995 "Implemented By",
996 "Inverse implementation relationship",
997 )
998 .with_inverse("implements"),
999 RelationshipDefinition::built_in(
1001 "created_by",
1002 "Created By",
1003 "User who created the requirement",
1004 )
1005 .with_cardinality(Cardinality::ManyToOne)
1006 .with_color("#4a9eff"),
1007 RelationshipDefinition::built_in(
1008 "assigned_to",
1009 "Assigned To",
1010 "User assigned to work on this requirement",
1011 )
1012 .with_cardinality(Cardinality::ManyToOne)
1013 .with_color("#22c55e"),
1014 RelationshipDefinition::built_in(
1015 "tested_by",
1016 "Tested By",
1017 "User who tested/verified the requirement",
1018 )
1019 .with_cardinality(Cardinality::ManyToMany)
1020 .with_color("#f59e0b"),
1021 RelationshipDefinition::built_in(
1022 "closed_by",
1023 "Closed By",
1024 "User who closed/completed the requirement",
1025 )
1026 .with_cardinality(Cardinality::ManyToOne)
1027 .with_color("#ef4444"),
1028 RelationshipDefinition::built_in(
1030 "sprint_assignment",
1031 "Assigned to Sprint",
1032 "Assigns a requirement to a Sprint for work planning",
1033 )
1034 .with_inverse("sprint_contains")
1035 .with_cardinality(Cardinality::ManyToOne) .with_target_types(vec!["Sprint".to_string()])
1037 .with_color("#7c3aed"),
1038 RelationshipDefinition::built_in(
1039 "sprint_contains",
1040 "Sprint Contains",
1041 "Items assigned to this Sprint",
1042 )
1043 .with_inverse("sprint_assignment")
1044 .with_cardinality(Cardinality::OneToMany)
1045 .with_source_types(vec!["Sprint".to_string()])
1046 .with_color("#7c3aed"),
1047 ]
1048 }
1049
1050 pub fn allows_source_type(&self, req_type: &RequirementType) -> bool {
1052 if self.source_types.is_empty() {
1053 return true;
1054 }
1055 let type_str = req_type.to_string();
1056 self.source_types
1057 .iter()
1058 .any(|t| t.eq_ignore_ascii_case(&type_str))
1059 }
1060
1061 pub fn allows_target_type(&self, req_type: &RequirementType) -> bool {
1063 if self.target_types.is_empty() {
1064 return true;
1065 }
1066 let type_str = req_type.to_string();
1067 self.target_types
1068 .iter()
1069 .any(|t| t.eq_ignore_ascii_case(&type_str))
1070 }
1071}
1072
1073#[derive(Debug, Clone, TS)]
1075pub struct RelationshipValidation {
1076 pub valid: bool,
1078 pub errors: Vec<String>,
1080 pub warnings: Vec<String>,
1082}
1083
1084impl RelationshipValidation {
1085 pub fn ok() -> Self {
1086 Self {
1087 valid: true,
1088 errors: Vec::new(),
1089 warnings: Vec::new(),
1090 }
1091 }
1092
1093 pub fn error(msg: &str) -> Self {
1094 Self {
1095 valid: false,
1096 errors: vec![msg.to_string()],
1097 warnings: Vec::new(),
1098 }
1099 }
1100
1101 pub fn with_warning(mut self, msg: &str) -> Self {
1102 self.warnings.push(msg.to_string());
1103 self
1104 }
1105
1106 pub fn add_error(&mut self, msg: &str) {
1107 self.valid = false;
1108 self.errors.push(msg.to_string());
1109 }
1110
1111 pub fn add_warning(&mut self, msg: &str) {
1112 self.warnings.push(msg.to_string());
1113 }
1114}
1115
1116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
1122pub enum IdFormat {
1123 #[default]
1126 SingleLevel,
1127 TwoLevel,
1130}
1131
1132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
1134pub enum NumberingStrategy {
1135 #[default]
1138 Global,
1139 PerPrefix,
1142 PerFeatureType,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1149pub struct RequirementTypeDefinition {
1150 pub name: String,
1152 pub prefix: String,
1154 #[serde(default)]
1156 pub description: String,
1157}
1158
1159impl RequirementTypeDefinition {
1160 pub fn new(name: &str, prefix: &str, description: &str) -> Self {
1161 Self {
1162 name: name.to_string(),
1163 prefix: prefix.to_uppercase(),
1164 description: description.to_string(),
1165 }
1166 }
1167}
1168
1169fn default_requirement_types() -> Vec<RequirementTypeDefinition> {
1171 vec![
1172 RequirementTypeDefinition::new("Functional", "FR", "Functional requirements"),
1173 RequirementTypeDefinition::new(
1174 "Non-Functional",
1175 "NFR",
1176 "Non-functional requirements (performance, security, etc.)",
1177 ),
1178 RequirementTypeDefinition::new("System", "SR", "System-level requirements"),
1179 RequirementTypeDefinition::new("User", "UR", "User story requirements"),
1180 RequirementTypeDefinition::new(
1181 "Change Request",
1182 "CR",
1183 "Change requests for modifications to existing functionality",
1184 ),
1185 RequirementTypeDefinition::new("Bug", "BUG", "Bug reports and defects"),
1186 RequirementTypeDefinition::new("Epic", "EPIC", "Large features spanning multiple stories"),
1187 RequirementTypeDefinition::new("Story", "STORY", "User stories for agile development"),
1188 RequirementTypeDefinition::new("Task", "TASK", "Individual work items"),
1189 RequirementTypeDefinition::new("Spike", "SPIKE", "Research and investigation tasks"),
1190 RequirementTypeDefinition::new("Sprint", "SPRINT", "Sprint planning containers"),
1191 RequirementTypeDefinition::new("Folder", "FOLDER", "Organizational folders"),
1192 RequirementTypeDefinition::new("Meta", "META", "Database configuration and templates"),
1193 ]
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1198pub struct FeatureDefinition {
1199 pub number: u32,
1201 pub name: String,
1203 pub prefix: String,
1205 #[serde(default)]
1207 pub description: String,
1208}
1209
1210impl FeatureDefinition {
1211 pub fn new(number: u32, name: &str, prefix: &str) -> Self {
1212 Self {
1213 number,
1214 name: name.to_string(),
1215 prefix: prefix.to_uppercase(),
1216 description: String::new(),
1217 }
1218 }
1219
1220 pub fn with_description(mut self, description: &str) -> Self {
1221 self.description = description.to_string();
1222 self
1223 }
1224}
1225
1226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1228pub struct IdConfiguration {
1229 #[serde(default)]
1231 pub format: IdFormat,
1232 #[serde(default)]
1234 pub numbering: NumberingStrategy,
1235 #[serde(default = "default_id_digits")]
1237 pub digits: u8,
1238 #[serde(default = "default_requirement_types")]
1240 pub requirement_types: Vec<RequirementTypeDefinition>,
1241}
1242
1243fn default_id_digits() -> u8 {
1244 3
1245}
1246
1247impl Default for IdConfiguration {
1248 fn default() -> Self {
1249 Self {
1250 format: IdFormat::default(),
1251 numbering: NumberingStrategy::default(),
1252 digits: 3,
1253 requirement_types: default_requirement_types(),
1254 }
1255 }
1256}
1257
1258impl IdConfiguration {
1259 pub fn reserved_prefixes(&self) -> Vec<String> {
1261 self.requirement_types
1262 .iter()
1263 .map(|t| t.prefix.clone())
1264 .collect()
1265 }
1266
1267 pub fn is_prefix_reserved(&self, prefix: &str) -> bool {
1269 let upper = prefix.to_uppercase();
1270 self.requirement_types.iter().any(|t| t.prefix == upper)
1271 }
1272
1273 pub fn get_type_by_name(&self, name: &str) -> Option<&RequirementTypeDefinition> {
1275 let lower = name.to_lowercase();
1276 self.requirement_types
1277 .iter()
1278 .find(|t| t.name.to_lowercase() == lower)
1279 }
1280
1281 pub fn get_type_by_prefix(&self, prefix: &str) -> Option<&RequirementTypeDefinition> {
1283 let upper = prefix.to_uppercase();
1284 self.requirement_types.iter().find(|t| t.prefix == upper)
1285 }
1286
1287 pub fn format_number(&self, num: u32) -> String {
1289 format!("{:0>width$}", num, width = self.digits as usize)
1290 }
1291}
1292
1293#[derive(Debug, Clone)]
1299pub struct IdConfigValidation {
1300 pub valid: bool,
1302 pub error: Option<String>,
1304 pub warning: Option<String>,
1306 pub can_migrate: bool,
1308 pub affected_count: usize,
1310}
1311
1312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1314pub struct Relationship {
1315 pub rel_type: RelationshipType,
1317 pub target_id: Uuid,
1319 #[serde(default, skip_serializing_if = "Option::is_none")]
1321 pub created_at: Option<DateTime<Utc>>,
1322 #[serde(default, skip_serializing_if = "Option::is_none")]
1324 pub created_by: Option<String>,
1325}
1326
1327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1329pub struct FieldChange {
1330 pub field_name: String,
1332
1333 pub old_value: String,
1335
1336 pub new_value: String,
1338}
1339
1340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1342pub struct HistoryEntry {
1343 pub id: Uuid,
1345
1346 pub author: String,
1348
1349 pub timestamp: DateTime<Utc>,
1351
1352 pub changes: Vec<FieldChange>,
1354}
1355
1356impl HistoryEntry {
1357 pub fn new(author: String, changes: Vec<FieldChange>) -> Self {
1359 Self {
1360 id: Uuid::now_v7(),
1361 author,
1362 timestamp: Utc::now(),
1363 changes,
1364 }
1365 }
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
1371pub struct RequirementSnapshot {
1372 pub original_id: Uuid,
1374
1375 pub spec_id: Option<String>,
1377
1378 pub title: String,
1380
1381 pub description: String,
1383
1384 pub status: RequirementStatus,
1386
1387 pub priority: RequirementPriority,
1389
1390 pub owner: String,
1392
1393 pub feature: String,
1395
1396 pub req_type: RequirementType,
1398
1399 pub tags: HashSet<String>,
1401
1402 pub relationships: Vec<Relationship>,
1404
1405 #[serde(default, skip_serializing_if = "Option::is_none")]
1407 pub custom_status: Option<String>,
1408
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1411 pub custom_priority: Option<String>,
1412
1413 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
1415 pub custom_fields: std::collections::HashMap<String, String>,
1416}
1417
1418impl RequirementSnapshot {
1419 pub fn from_requirement(req: &Requirement) -> Self {
1421 Self {
1422 original_id: req.id,
1423 spec_id: req.spec_id.clone(),
1424 title: req.title.clone(),
1425 description: req.description.clone(),
1426 status: req.status.clone(),
1427 priority: req.priority.clone(),
1428 owner: req.owner.clone(),
1429 feature: req.feature.clone(),
1430 req_type: req.req_type.clone(),
1431 tags: req.tags.clone(),
1432 relationships: req.relationships.clone(),
1433 custom_status: req.custom_status.clone(),
1434 custom_priority: req.custom_priority.clone(),
1435 custom_fields: req.custom_fields.clone(),
1436 }
1437 }
1438}
1439
1440#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
1442pub struct Baseline {
1443 pub id: Uuid,
1445
1446 pub name: String,
1448
1449 #[serde(default, skip_serializing_if = "Option::is_none")]
1451 pub description: Option<String>,
1452
1453 pub created_at: DateTime<Utc>,
1455
1456 pub created_by: String,
1458
1459 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub git_tag: Option<String>,
1463
1464 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1468 pub requirements: Vec<RequirementSnapshot>,
1469
1470 #[serde(default)]
1472 pub locked: bool,
1473}
1474
1475impl Baseline {
1476 pub fn new(
1478 name: String,
1479 description: Option<String>,
1480 created_by: String,
1481 requirements: &[Requirement],
1482 ) -> Self {
1483 let snapshots: Vec<RequirementSnapshot> = requirements
1484 .iter()
1485 .filter(|r| !r.archived) .map(RequirementSnapshot::from_requirement)
1487 .collect();
1488
1489 Self {
1490 id: Uuid::now_v7(),
1491 name,
1492 description,
1493 created_at: Utc::now(),
1494 created_by,
1495 git_tag: None,
1496 requirements: snapshots,
1497 locked: false,
1498 }
1499 }
1500
1501 pub fn name_slug(&self) -> String {
1503 self.name
1504 .to_lowercase()
1505 .chars()
1506 .map(|c| if c.is_alphanumeric() { c } else { '-' })
1507 .collect::<String>()
1508 .trim_matches('-')
1509 .to_string()
1510 }
1511
1512 pub fn git_tag_name(&self) -> String {
1514 self.git_tag
1515 .clone()
1516 .unwrap_or_else(|| format!("baseline-{}", self.name_slug()))
1517 }
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1522pub struct BaselineComparison {
1523 pub added: Vec<Uuid>,
1525
1526 pub removed: Vec<Uuid>,
1528
1529 pub modified: Vec<BaselineRequirementDiff>,
1531
1532 pub unchanged: Vec<Uuid>,
1534}
1535
1536#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1538pub struct BaselineRequirementDiff {
1539 pub id: Uuid,
1541
1542 pub spec_id: Option<String>,
1544
1545 pub changes: Vec<FieldChange>,
1547}
1548
1549#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1551pub struct ReactionDefinition {
1552 pub name: String,
1554
1555 pub emoji: String,
1557
1558 pub label: String,
1560
1561 #[serde(skip_serializing_if = "Option::is_none")]
1563 pub description: Option<String>,
1564
1565 #[serde(default)]
1567 pub built_in: bool,
1568}
1569
1570impl ReactionDefinition {
1571 pub fn new(
1573 name: impl Into<String>,
1574 emoji: impl Into<String>,
1575 label: impl Into<String>,
1576 ) -> Self {
1577 Self {
1578 name: name.into(),
1579 emoji: emoji.into(),
1580 label: label.into(),
1581 description: None,
1582 built_in: false,
1583 }
1584 }
1585
1586 pub fn builtin(
1588 name: impl Into<String>,
1589 emoji: impl Into<String>,
1590 label: impl Into<String>,
1591 description: impl Into<String>,
1592 ) -> Self {
1593 Self {
1594 name: name.into(),
1595 emoji: emoji.into(),
1596 label: label.into(),
1597 description: Some(description.into()),
1598 built_in: true,
1599 }
1600 }
1601}
1602
1603pub fn default_reaction_definitions() -> Vec<ReactionDefinition> {
1605 vec![
1606 ReactionDefinition::builtin(
1607 "resolved",
1608 "✅",
1609 "Resolved",
1610 "Mark comment as resolved/addressed",
1611 ),
1612 ReactionDefinition::builtin(
1613 "rejected",
1614 "❌",
1615 "Rejected",
1616 "Mark comment as rejected/declined",
1617 ),
1618 ReactionDefinition::builtin("thumbs_up", "👍", "Thumbs Up", "Agree or approve"),
1619 ReactionDefinition::builtin("thumbs_down", "👎", "Thumbs Down", "Disagree or disapprove"),
1620 ReactionDefinition::builtin("question", "❓", "Question", "Needs clarification"),
1621 ReactionDefinition::builtin(
1622 "important",
1623 "⚠️",
1624 "Important",
1625 "Mark as important/attention needed",
1626 ),
1627 ]
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1632pub struct CommentReaction {
1633 pub reaction: String,
1635
1636 pub author: String,
1638
1639 pub added_at: DateTime<Utc>,
1641}
1642
1643impl CommentReaction {
1644 pub fn new(reaction: impl Into<String>, author: impl Into<String>) -> Self {
1646 Self {
1647 reaction: reaction.into(),
1648 author: author.into(),
1649 added_at: Utc::now(),
1650 }
1651 }
1652}
1653
1654#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
1656#[serde(rename_all = "snake_case")]
1657pub enum UrlOpenMode {
1658 #[default]
1660 Preview,
1661 NewTab,
1663}
1664
1665#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1667pub struct UrlLink {
1668 pub id: Uuid,
1670
1671 pub url: String,
1673
1674 pub title: String,
1676
1677 #[serde(default, skip_serializing_if = "Option::is_none")]
1679 pub description: Option<String>,
1680
1681 #[serde(default)]
1683 pub open_mode: UrlOpenMode,
1684
1685 pub added_at: DateTime<Utc>,
1687
1688 pub added_by: String,
1690
1691 #[serde(default, skip_serializing_if = "Option::is_none")]
1693 pub last_verified: Option<DateTime<Utc>>,
1694
1695 #[serde(default, skip_serializing_if = "Option::is_none")]
1697 pub last_verified_ok: Option<bool>,
1698}
1699
1700impl UrlLink {
1701 pub fn new(
1703 url: impl Into<String>,
1704 title: impl Into<String>,
1705 added_by: impl Into<String>,
1706 ) -> Self {
1707 Self {
1708 id: Uuid::now_v7(),
1709 url: url.into(),
1710 title: title.into(),
1711 description: None,
1712 open_mode: UrlOpenMode::default(),
1713 added_at: Utc::now(),
1714 added_by: added_by.into(),
1715 last_verified: None,
1716 last_verified_ok: None,
1717 }
1718 }
1719
1720 pub fn with_description(mut self, description: impl Into<String>) -> Self {
1722 self.description = Some(description.into());
1723 self
1724 }
1725
1726 pub fn with_open_mode(mut self, mode: UrlOpenMode) -> Self {
1728 self.open_mode = mode;
1729 self
1730 }
1731}
1732
1733#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1735pub struct Attachment {
1736 pub id: Uuid,
1738
1739 pub filename: String,
1741
1742 pub stored_path: String,
1744
1745 #[serde(default, skip_serializing_if = "Option::is_none")]
1747 pub mime_type: Option<String>,
1748
1749 pub size_bytes: u64,
1751
1752 pub added_at: DateTime<Utc>,
1754
1755 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub added_by: Option<String>,
1758
1759 #[serde(default, skip_serializing_if = "Option::is_none")]
1761 pub description: Option<String>,
1762}
1763
1764impl Attachment {
1765 pub fn new(
1767 filename: impl Into<String>,
1768 stored_path: impl Into<String>,
1769 size_bytes: u64,
1770 added_by: Option<String>,
1771 ) -> Self {
1772 Self {
1773 id: Uuid::now_v7(),
1774 filename: filename.into(),
1775 stored_path: stored_path.into(),
1776 mime_type: None,
1777 size_bytes,
1778 added_at: Utc::now(),
1779 added_by,
1780 description: None,
1781 }
1782 }
1783
1784 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
1786 self.mime_type = Some(mime_type.into());
1787 self
1788 }
1789
1790 pub fn with_description(mut self, description: impl Into<String>) -> Self {
1792 self.description = Some(description.into());
1793 self
1794 }
1795
1796 pub fn format_size(&self) -> String {
1798 let bytes = self.size_bytes;
1799 if bytes < 1024 {
1800 format!("{} B", bytes)
1801 } else if bytes < 1024 * 1024 {
1802 format!("{:.1} KB", bytes as f64 / 1024.0)
1803 } else if bytes < 1024 * 1024 * 1024 {
1804 format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
1805 } else {
1806 format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
1807 }
1808 }
1809}
1810
1811#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
1814pub enum ArtifactType {
1815 SourceCode,
1817 TestCode,
1819 Config,
1821 Doc,
1823}
1824
1825impl fmt::Display for ArtifactType {
1826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1827 match self {
1828 ArtifactType::SourceCode => write!(f, "source"),
1829 ArtifactType::TestCode => write!(f, "test"),
1830 ArtifactType::Config => write!(f, "config"),
1831 ArtifactType::Doc => write!(f, "doc"),
1832 }
1833 }
1834}
1835
1836impl ArtifactType {
1837 pub fn from_str(s: &str) -> Option<Self> {
1839 match s.to_lowercase().as_str() {
1840 "source" | "sourcecode" | "src" | "code" => Some(ArtifactType::SourceCode),
1841 "test" | "testcode" | "tests" => Some(ArtifactType::TestCode),
1842 "config" | "configuration" | "cfg" => Some(ArtifactType::Config),
1843 "doc" | "docs" | "documentation" => Some(ArtifactType::Doc),
1844 _ => None,
1845 }
1846 }
1847}
1848
1849#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1853pub struct TraceLink {
1854 pub id: Uuid,
1856
1857 pub artifact_type: ArtifactType,
1859
1860 pub file_path: String,
1862
1863 #[serde(default, skip_serializing_if = "Option::is_none")]
1865 pub symbol: Option<String>,
1866
1867 #[serde(default, skip_serializing_if = "Option::is_none")]
1869 pub line_start: Option<u32>,
1870
1871 #[serde(default, skip_serializing_if = "Option::is_none")]
1872 pub line_end: Option<u32>,
1873
1874 #[serde(default, skip_serializing_if = "Option::is_none")]
1876 pub notes: Option<String>,
1877
1878 pub created_at: DateTime<Utc>,
1880
1881 #[serde(default, skip_serializing_if = "Option::is_none")]
1883 pub created_by: Option<String>,
1884
1885 #[serde(default, skip_serializing_if = "Option::is_none")]
1887 pub commit_hash: Option<String>,
1888}
1889
1890impl TraceLink {
1891 pub fn new(artifact_type: ArtifactType, file_path: impl Into<String>) -> Self {
1893 Self {
1894 id: Uuid::now_v7(),
1895 artifact_type,
1896 file_path: file_path.into(),
1897 symbol: None,
1898 line_start: None,
1899 line_end: None,
1900 notes: None,
1901 created_at: Utc::now(),
1902 created_by: None,
1903 commit_hash: None,
1904 }
1905 }
1906
1907 pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
1909 self.symbol = Some(symbol.into());
1910 self
1911 }
1912
1913 pub fn with_lines(mut self, start: u32, end: u32) -> Self {
1915 self.line_start = Some(start);
1916 self.line_end = Some(end);
1917 self
1918 }
1919
1920 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
1922 self.notes = Some(notes.into());
1923 self
1924 }
1925
1926 pub fn with_created_by(mut self, author: impl Into<String>) -> Self {
1928 self.created_by = Some(author.into());
1929 self
1930 }
1931
1932 pub fn with_commit(mut self, hash: impl Into<String>) -> Self {
1934 self.commit_hash = Some(hash.into());
1935 self
1936 }
1937
1938 pub fn line_range(&self) -> Option<(u32, u32)> {
1940 match (self.line_start, self.line_end) {
1941 (Some(start), Some(end)) => Some((start, end)),
1942 _ => None,
1943 }
1944 }
1945}
1946
1947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1951pub struct GitLabIssueLink {
1952 pub id: Uuid,
1954
1955 pub issue_iid: u64,
1957
1958 #[serde(default, skip_serializing_if = "Option::is_none")]
1960 pub project_id: Option<u64>,
1961
1962 pub issue_title: String,
1964
1965 #[serde(default)]
1967 pub link_type: GitLabLinkType,
1968
1969 #[serde(default, skip_serializing_if = "Option::is_none")]
1971 pub notes: Option<String>,
1972
1973 pub created_at: DateTime<Utc>,
1975
1976 #[serde(default, skip_serializing_if = "Option::is_none")]
1978 pub created_by: Option<String>,
1979
1980 #[serde(default, skip_serializing_if = "Option::is_none")]
1982 pub last_synced: Option<DateTime<Utc>>,
1983
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1986 pub issue_state: Option<String>,
1987}
1988
1989impl GitLabIssueLink {
1990 pub fn new(issue_iid: u64, issue_title: impl Into<String>) -> Self {
1992 Self {
1993 id: Uuid::now_v7(),
1994 issue_iid,
1995 project_id: None,
1996 issue_title: issue_title.into(),
1997 link_type: GitLabLinkType::default(),
1998 notes: None,
1999 created_at: Utc::now(),
2000 created_by: None,
2001 last_synced: None,
2002 issue_state: None,
2003 }
2004 }
2005
2006 pub fn with_project(mut self, project_id: u64) -> Self {
2008 self.project_id = Some(project_id);
2009 self
2010 }
2011
2012 pub fn with_link_type(mut self, link_type: GitLabLinkType) -> Self {
2014 self.link_type = link_type;
2015 self
2016 }
2017
2018 pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
2020 self.created_by = Some(creator.into());
2021 self
2022 }
2023
2024 pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
2026 self.notes = Some(notes.into());
2027 self
2028 }
2029
2030 pub fn update_from_issue(&mut self, title: &str, state: &str) {
2032 self.issue_title = title.to_string();
2033 self.issue_state = Some(state.to_string());
2034 self.last_synced = Some(Utc::now());
2035 }
2036
2037 pub fn display_id(&self) -> String {
2039 format!("GL-{}", self.issue_iid)
2040 }
2041}
2042
2043#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2046pub enum GitLabLinkType {
2047 #[default]
2049 ImplementedBy,
2050 TracesTo,
2052 RelatedBug,
2054 FollowUp,
2056}
2057
2058impl fmt::Display for GitLabLinkType {
2059 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2060 match self {
2061 GitLabLinkType::ImplementedBy => write!(f, "Implemented by"),
2062 GitLabLinkType::TracesTo => write!(f, "Traces to"),
2063 GitLabLinkType::RelatedBug => write!(f, "Related bug"),
2064 GitLabLinkType::FollowUp => write!(f, "Follow-up"),
2065 }
2066 }
2067}
2068
2069#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2072pub enum LinkOrigin {
2073 #[default]
2075 CreatedFromAida,
2076 ImportedFromGitLab,
2078 ManualLink,
2080}
2081
2082impl fmt::Display for LinkOrigin {
2083 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2084 match self {
2085 LinkOrigin::CreatedFromAida => write!(f, "Created from AIDA"),
2086 LinkOrigin::ImportedFromGitLab => write!(f, "Imported from GitLab"),
2087 LinkOrigin::ManualLink => write!(f, "Manual link"),
2088 }
2089 }
2090}
2091
2092#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2095pub enum SyncStatus {
2096 #[default]
2098 InSync,
2099 AidaModified,
2101 GitLabModified,
2103 Conflict,
2105 Error,
2107 Untracked,
2109}
2110
2111impl fmt::Display for SyncStatus {
2112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2113 match self {
2114 SyncStatus::InSync => write!(f, "In Sync"),
2115 SyncStatus::AidaModified => write!(f, "AIDA Modified"),
2116 SyncStatus::GitLabModified => write!(f, "GitLab Modified"),
2117 SyncStatus::Conflict => write!(f, "Conflict"),
2118 SyncStatus::Error => write!(f, "Error"),
2119 SyncStatus::Untracked => write!(f, "Untracked"),
2120 }
2121 }
2122}
2123
2124#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2127pub struct GitLabSyncState {
2128 pub requirement_id: Uuid,
2130
2131 pub spec_id: String,
2133
2134 pub gitlab_project_id: u64,
2136
2137 pub gitlab_issue_iid: u64,
2139
2140 pub gitlab_issue_id: u64,
2142
2143 pub linked_at: DateTime<Utc>,
2145
2146 pub last_sync: DateTime<Utc>,
2148
2149 pub aida_content_hash: String,
2151
2152 pub gitlab_content_hash: String,
2154
2155 pub link_origin: LinkOrigin,
2157
2158 pub sync_status: SyncStatus,
2160
2161 #[serde(default, skip_serializing_if = "Option::is_none")]
2163 pub last_error: Option<String>,
2164}
2165
2166impl GitLabSyncState {
2167 pub fn new(
2169 requirement_id: Uuid,
2170 spec_id: impl Into<String>,
2171 gitlab_project_id: u64,
2172 gitlab_issue_iid: u64,
2173 gitlab_issue_id: u64,
2174 link_origin: LinkOrigin,
2175 ) -> Self {
2176 let now = Utc::now();
2177 Self {
2178 requirement_id,
2179 spec_id: spec_id.into(),
2180 gitlab_project_id,
2181 gitlab_issue_iid,
2182 gitlab_issue_id,
2183 linked_at: now,
2184 last_sync: now,
2185 aida_content_hash: String::new(),
2186 gitlab_content_hash: String::new(),
2187 link_origin,
2188 sync_status: SyncStatus::Untracked,
2189 last_error: None,
2190 }
2191 }
2192
2193 pub fn mark_synced(&mut self, aida_hash: String, gitlab_hash: String) {
2195 self.last_sync = Utc::now();
2196 self.aida_content_hash = aida_hash;
2197 self.gitlab_content_hash = gitlab_hash;
2198 self.sync_status = SyncStatus::InSync;
2199 self.last_error = None;
2200 }
2201
2202 pub fn mark_error(&mut self, error: impl Into<String>) {
2204 self.sync_status = SyncStatus::Error;
2205 self.last_error = Some(error.into());
2206 }
2207
2208 pub fn hash_requirement(req: &Requirement) -> String {
2212 use sha2::{Digest, Sha256};
2213 let mut hasher = Sha256::new();
2214
2215 hasher.update(req.title.as_bytes());
2217 hasher.update(req.description.as_bytes());
2218 hasher.update(req.status.to_string().as_bytes());
2219 hasher.update(req.priority.to_string().as_bytes());
2220 hasher.update(req.owner.as_bytes());
2221 hasher.update(req.req_type.to_string().as_bytes());
2222
2223 let mut tags: Vec<_> = req.tags.iter().collect();
2225 tags.sort();
2226 for tag in tags {
2227 hasher.update(tag.as_bytes());
2228 }
2229
2230 format!("{:x}", hasher.finalize())
2231 }
2232
2233 #[cfg(feature = "gitlab")]
2237 pub fn hash_gitlab_issue(issue: &crate::integrations::gitlab::GitLabIssue) -> String {
2238 use sha2::{Digest, Sha256};
2239 let mut hasher = Sha256::new();
2240
2241 hasher.update(issue.title.as_bytes());
2243 if let Some(desc) = &issue.description {
2244 hasher.update(desc.as_bytes());
2245 }
2246 hasher.update(format!("{:?}", issue.state).as_bytes());
2247
2248 let mut labels = issue.labels.clone();
2250 labels.sort();
2251 for label in labels {
2252 hasher.update(label.as_bytes());
2253 }
2254
2255 let mut assignee_ids: Vec<_> = issue.assignees.iter().map(|a| a.id).collect();
2257 assignee_ids.sort();
2258 for id in assignee_ids {
2259 hasher.update(id.to_string().as_bytes());
2260 }
2261
2262 format!("{:x}", hasher.finalize())
2263 }
2264}
2265
2266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
2269pub enum ConfidenceLevel {
2270 High,
2272 Medium,
2274 Low,
2276}
2277
2278impl fmt::Display for ConfidenceLevel {
2279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2280 match self {
2281 ConfidenceLevel::High => write!(f, "high"),
2282 ConfidenceLevel::Medium => write!(f, "med"),
2283 ConfidenceLevel::Low => write!(f, "low"),
2284 }
2285 }
2286}
2287
2288impl ConfidenceLevel {
2289 pub fn from_str(s: &str) -> Option<Self> {
2291 match s.to_lowercase().as_str() {
2292 "high" | "h" => Some(ConfidenceLevel::High),
2293 "medium" | "med" | "m" => Some(ConfidenceLevel::Medium),
2294 "low" | "l" => Some(ConfidenceLevel::Low),
2295 _ => None,
2296 }
2297 }
2298}
2299
2300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
2304pub struct ImplementationInfo {
2305 pub implemented: bool,
2307
2308 #[serde(default, skip_serializing_if = "Option::is_none")]
2310 pub summary: Option<String>,
2311
2312 #[serde(default, skip_serializing_if = "Option::is_none")]
2314 pub last_agent_run: Option<DateTime<Utc>>,
2315
2316 #[serde(default, skip_serializing_if = "Option::is_none")]
2318 pub risk_notes: Option<String>,
2319
2320 #[serde(default, skip_serializing_if = "Option::is_none")]
2322 pub test_coverage_notes: Option<String>,
2323
2324 #[serde(default, skip_serializing_if = "Option::is_none")]
2326 pub source_tool: Option<String>,
2327
2328 #[serde(default, skip_serializing_if = "Option::is_none")]
2330 pub confidence: Option<ConfidenceLevel>,
2331
2332 #[serde(default, skip_serializing_if = "Option::is_none")]
2334 pub implemented_at: Option<DateTime<Utc>>,
2335
2336 #[serde(default, skip_serializing_if = "Option::is_none")]
2338 pub implemented_by: Option<String>,
2339}
2340
2341impl Default for ImplementationInfo {
2342 fn default() -> Self {
2343 Self {
2344 implemented: false,
2345 summary: None,
2346 last_agent_run: None,
2347 risk_notes: None,
2348 test_coverage_notes: None,
2349 source_tool: None,
2350 confidence: None,
2351 implemented_at: None,
2352 implemented_by: None,
2353 }
2354 }
2355}
2356
2357impl ImplementationInfo {
2358 pub fn new() -> Self {
2360 Self::default()
2361 }
2362
2363 pub fn implemented() -> Self {
2365 Self {
2366 implemented: true,
2367 implemented_at: Some(Utc::now()),
2368 ..Self::default()
2369 }
2370 }
2371
2372 pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
2374 self.summary = Some(summary.into());
2375 self
2376 }
2377
2378 pub fn with_source_tool(mut self, tool: impl Into<String>) -> Self {
2380 self.source_tool = Some(tool.into());
2381 self
2382 }
2383
2384 pub fn with_confidence(mut self, confidence: ConfidenceLevel) -> Self {
2386 self.confidence = Some(confidence);
2387 self
2388 }
2389
2390 pub fn with_implemented_by(mut self, author: impl Into<String>) -> Self {
2392 self.implemented_by = Some(author.into());
2393 self
2394 }
2395
2396 pub fn record_agent_run(&mut self) {
2398 self.last_agent_run = Some(Utc::now());
2399 }
2400}
2401
2402#[derive(Debug, Clone, PartialEq, Eq, TS)]
2405pub struct TraceComment {
2406 pub spec_id: String,
2408 pub title: Option<String>,
2410 pub ai_tool: Option<String>,
2412 pub confidence: Option<ConfidenceLevel>,
2414 pub impl_date: Option<String>,
2416 pub implemented_by: Option<String>,
2418}
2419
2420impl TraceComment {
2421 pub fn parse(line: &str) -> Option<Self> {
2425 let line = line.trim();
2427 let trace_start = line.find("trace:")?;
2428 let content = &line[trace_start + 6..];
2429
2430 let segments: Vec<&str> = content.split('|').map(|s| s.trim()).collect();
2432 if segments.is_empty() {
2433 return None;
2434 }
2435
2436 let first = segments[0];
2438 let (spec_id, title) = if let Some(dash_pos) = first.find(" - ") {
2439 let id = first[..dash_pos].trim().to_string();
2440 let title = first[dash_pos + 3..].trim().to_string();
2441 (id, Some(title))
2442 } else {
2443 (first.trim().to_string(), None)
2444 };
2445
2446 let mut result = TraceComment {
2447 spec_id,
2448 title,
2449 ai_tool: None,
2450 confidence: None,
2451 impl_date: None,
2452 implemented_by: None,
2453 };
2454
2455 for segment in segments.iter().skip(1) {
2457 let segment = segment.trim();
2458 if segment.starts_with("ai:") {
2459 let parts: Vec<&str> = segment[3..].split(':').collect();
2461 if !parts.is_empty() {
2462 result.ai_tool = Some(parts[0].to_string());
2463 }
2464 if parts.len() > 1 {
2465 result.confidence = ConfidenceLevel::from_str(parts[1]);
2466 }
2467 } else if segment.starts_with("impl:") {
2468 result.impl_date = Some(segment[5..].trim().to_string());
2469 } else if segment.starts_with("by:") {
2470 result.implemented_by = Some(segment[3..].trim().to_string());
2471 }
2472 }
2473
2474 Some(result)
2475 }
2476
2477 pub fn format(&self) -> String {
2479 let mut parts = Vec::new();
2480
2481 let spec_part = if let Some(ref title) = self.title {
2483 format!("{} - {}", self.spec_id, title)
2484 } else {
2485 self.spec_id.clone()
2486 };
2487 parts.push(spec_part);
2488
2489 if let Some(ref tool) = self.ai_tool {
2491 let ai_part = if let Some(ref conf) = self.confidence {
2492 format!("ai:{}:{}", tool, conf)
2493 } else {
2494 format!("ai:{}", tool)
2495 };
2496 parts.push(ai_part);
2497 }
2498
2499 if let Some(ref date) = self.impl_date {
2501 parts.push(format!("impl:{}", date));
2502 }
2503
2504 if let Some(ref by) = self.implemented_by {
2506 parts.push(format!("by:{}", by));
2507 }
2508
2509 format!("trace:{}", parts.join(" | "))
2510 }
2511}
2512
2513#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
2515pub struct Comment {
2516 pub id: Uuid,
2518
2519 pub author: String,
2521
2522 pub content: String,
2524
2525 pub created_at: DateTime<Utc>,
2527
2528 pub modified_at: DateTime<Utc>,
2530
2531 #[serde(skip_serializing_if = "Option::is_none")]
2533 pub parent_id: Option<Uuid>,
2534
2535 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2537 pub replies: Vec<Comment>,
2538
2539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2541 pub reactions: Vec<CommentReaction>,
2542}
2543
2544impl Comment {
2545 pub fn new(author: String, content: String) -> Self {
2547 let now = Utc::now();
2548 Self {
2549 id: Uuid::now_v7(),
2550 author,
2551 content,
2552 created_at: now,
2553 modified_at: now,
2554 parent_id: None,
2555 replies: Vec::new(),
2556 reactions: Vec::new(),
2557 }
2558 }
2559
2560 pub fn new_reply(author: String, content: String, parent_id: Uuid) -> Self {
2562 let now = Utc::now();
2563 Self {
2564 id: Uuid::now_v7(),
2565 author,
2566 content,
2567 created_at: now,
2568 modified_at: now,
2569 parent_id: Some(parent_id),
2570 replies: Vec::new(),
2571 reactions: Vec::new(),
2572 }
2573 }
2574
2575 pub fn add_reaction(&mut self, reaction: &str, author: &str) -> bool {
2578 if self
2580 .reactions
2581 .iter()
2582 .any(|r| r.reaction == reaction && r.author == author)
2583 {
2584 return false;
2585 }
2586 self.reactions.push(CommentReaction::new(reaction, author));
2587 true
2588 }
2589
2590 pub fn remove_reaction(&mut self, reaction: &str, author: &str) -> bool {
2593 let initial_len = self.reactions.len();
2594 self.reactions
2595 .retain(|r| !(r.reaction == reaction && r.author == author));
2596 self.reactions.len() < initial_len
2597 }
2598
2599 pub fn toggle_reaction(&mut self, reaction: &str, author: &str) -> bool {
2602 if self.remove_reaction(reaction, author) {
2603 false
2604 } else {
2605 self.add_reaction(reaction, author);
2606 true
2607 }
2608 }
2609
2610 pub fn reaction_counts(&self) -> std::collections::HashMap<String, usize> {
2612 let mut counts = std::collections::HashMap::new();
2613 for r in &self.reactions {
2614 *counts.entry(r.reaction.clone()).or_insert(0) += 1;
2615 }
2616 counts
2617 }
2618
2619 pub fn has_reaction(&self, reaction: &str, author: &str) -> bool {
2621 self.reactions
2622 .iter()
2623 .any(|r| r.reaction == reaction && r.author == author)
2624 }
2625
2626 pub fn add_reply(&mut self, reply: Comment) {
2628 self.replies.push(reply);
2629 }
2630
2631 pub fn find_comment_mut(&mut self, id: &Uuid) -> Option<&mut Comment> {
2633 if &self.id == id {
2634 return Some(self);
2635 }
2636 for reply in &mut self.replies {
2637 if let Some(found) = reply.find_comment_mut(id) {
2638 return Some(found);
2639 }
2640 }
2641 None
2642 }
2643
2644 pub fn touch(&mut self) {
2646 self.modified_at = Utc::now();
2647 }
2648
2649 fn remove_reply_recursive(comment: &mut Comment, target_id: &Uuid) -> bool {
2651 if let Some(pos) = comment.replies.iter().position(|c| &c.id == target_id) {
2652 comment.replies.remove(pos);
2653 return true;
2654 }
2655 for reply in &mut comment.replies {
2656 if Comment::remove_reply_recursive(reply, target_id) {
2657 return true;
2658 }
2659 }
2660 false
2661 }
2662}
2663
2664#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2666pub struct User {
2667 pub id: Uuid,
2669
2670 #[serde(skip_serializing_if = "Option::is_none")]
2672 pub spec_id: Option<String>,
2673
2674 pub name: String,
2676
2677 pub email: String,
2679
2680 pub handle: String,
2682
2683 #[serde(default, skip_serializing_if = "Option::is_none")]
2687 pub pin_hash: Option<String>,
2688
2689 pub created_at: DateTime<Utc>,
2691
2692 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2694 pub archived: bool,
2695
2696 #[serde(skip)]
2698 pub version: i64,
2699}
2700
2701impl User {
2702 pub fn new(name: String, email: String, handle: String) -> Self {
2704 Self {
2705 id: Uuid::now_v7(),
2706 spec_id: None,
2707 name,
2708 email,
2709 handle,
2710 pin_hash: None,
2711 created_at: Utc::now(),
2712 archived: false,
2713 version: 1,
2714 }
2715 }
2716
2717 pub fn new_with_spec_id(name: String, email: String, handle: String, spec_id: String) -> Self {
2719 Self {
2720 id: Uuid::now_v7(),
2721 spec_id: Some(spec_id),
2722 name,
2723 email,
2724 handle,
2725 pin_hash: None,
2726 created_at: Utc::now(),
2727 archived: false,
2728 version: 1,
2729 }
2730 }
2731
2732 pub fn display_id(&self) -> &str {
2734 self.spec_id.as_deref().unwrap_or(&self.name)
2735 }
2736
2737 pub fn set_pin(&mut self, pin: &str) {
2739 use sha2::{Digest, Sha256};
2740 let mut hasher = Sha256::new();
2741 hasher.update(pin.as_bytes());
2742 let result = hasher.finalize();
2743 self.pin_hash = Some(format!("{:x}", result));
2744 }
2745
2746 pub fn verify_pin(&self, pin: &str) -> bool {
2750 if let Some(ref stored_hash) = self.pin_hash {
2751 use sha2::{Digest, Sha256};
2752 let mut hasher = Sha256::new();
2753 hasher.update(pin.as_bytes());
2754 let result = hasher.finalize();
2755 let input_hash = format!("{:x}", result);
2756 stored_hash == &input_hash
2757 } else {
2758 false
2759 }
2760 }
2761
2762 pub fn has_pin(&self) -> bool {
2764 self.pin_hash.is_some()
2765 }
2766
2767 pub fn clear_pin(&mut self) {
2769 self.pin_hash = None;
2770 }
2771}
2772
2773#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2776pub struct Team {
2777 pub id: Uuid,
2779
2780 #[serde(skip_serializing_if = "Option::is_none")]
2782 pub spec_id: Option<String>,
2783
2784 pub name: String,
2786
2787 #[serde(default, skip_serializing_if = "String::is_empty")]
2789 pub description: String,
2790
2791 #[serde(skip_serializing_if = "Option::is_none")]
2793 pub parent_team_id: Option<Uuid>,
2794
2795 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2797 pub member_ids: Vec<Uuid>,
2798
2799 pub created_at: DateTime<Utc>,
2801
2802 #[serde(skip_serializing_if = "Option::is_none")]
2804 pub modified_at: Option<DateTime<Utc>>,
2805
2806 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2808 pub archived: bool,
2809}
2810
2811impl Team {
2812 pub fn new(name: String, description: String, parent_team_id: Option<Uuid>) -> Self {
2814 Self {
2815 id: Uuid::now_v7(),
2816 spec_id: None,
2817 name,
2818 description,
2819 parent_team_id,
2820 member_ids: Vec::new(),
2821 created_at: Utc::now(),
2822 modified_at: None,
2823 archived: false,
2824 }
2825 }
2826
2827 pub fn new_with_spec_id(
2829 name: String,
2830 description: String,
2831 parent_team_id: Option<Uuid>,
2832 spec_id: String,
2833 ) -> Self {
2834 Self {
2835 id: Uuid::now_v7(),
2836 spec_id: Some(spec_id),
2837 name,
2838 description,
2839 parent_team_id,
2840 member_ids: Vec::new(),
2841 created_at: Utc::now(),
2842 modified_at: None,
2843 archived: false,
2844 }
2845 }
2846
2847 pub fn display_id(&self) -> &str {
2849 self.spec_id.as_deref().unwrap_or(&self.name)
2850 }
2851
2852 pub fn add_member(&mut self, user_id: Uuid) {
2854 if !self.member_ids.contains(&user_id) {
2855 self.member_ids.push(user_id);
2856 self.modified_at = Some(Utc::now());
2857 }
2858 }
2859
2860 pub fn remove_member(&mut self, user_id: &Uuid) -> bool {
2862 if let Some(pos) = self.member_ids.iter().position(|id| id == user_id) {
2863 self.member_ids.remove(pos);
2864 self.modified_at = Some(Utc::now());
2865 true
2866 } else {
2867 false
2868 }
2869 }
2870
2871 pub fn has_member(&self, user_id: &Uuid) -> bool {
2873 self.member_ids.contains(user_id)
2874 }
2875
2876 pub fn member_count(&self) -> usize {
2878 self.member_ids.len()
2879 }
2880}
2881
2882#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2884pub struct Requirement {
2885 pub id: Uuid,
2887
2888 #[serde(skip_serializing_if = "Option::is_none")]
2890 pub spec_id: Option<String>,
2891
2892 #[serde(default, skip_serializing_if = "Option::is_none")]
2897 pub agreed_id: Option<String>,
2898
2899 #[serde(default, skip_serializing_if = "Option::is_none")]
2903 pub prefix_override: Option<String>,
2904
2905 pub title: String,
2907
2908 pub description: String,
2910
2911 pub status: RequirementStatus,
2913
2914 pub priority: RequirementPriority,
2916
2917 pub owner: String,
2919
2920 pub feature: String,
2922
2923 pub created_at: DateTime<Utc>,
2925
2926 #[serde(default, skip_serializing_if = "Option::is_none")]
2928 pub created_by: Option<String>,
2929
2930 pub modified_at: DateTime<Utc>,
2932
2933 pub req_type: RequirementType,
2935
2936 #[serde(default, skip_serializing_if = "Option::is_none")]
2938 pub meta_subtype: Option<MetaSubtype>,
2939
2940 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2942 pub dependencies: Vec<Uuid>,
2943
2944 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
2946 pub tags: HashSet<String>,
2947
2948 #[serde(default, skip_serializing_if = "Option::is_none")]
2951 pub weight: Option<f32>,
2952
2953 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2955 pub relationships: Vec<Relationship>,
2956
2957 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2959 pub comments: Vec<Comment>,
2960
2961 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2963 pub history: Vec<HistoryEntry>,
2964
2965 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2967 pub archived: bool,
2968
2969 #[serde(default, skip_serializing_if = "Option::is_none")]
2972 pub custom_status: Option<String>,
2973
2974 #[serde(default, skip_serializing_if = "Option::is_none")]
2977 pub custom_priority: Option<String>,
2978
2979 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
2981 pub custom_fields: std::collections::HashMap<String, String>,
2982
2983 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2985 pub urls: Vec<UrlLink>,
2986
2987 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2989 pub attachments: Vec<Attachment>,
2990
2991 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2994 pub trace_links: Vec<TraceLink>,
2995
2996 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2999 pub gitlab_issues: Vec<GitLabIssueLink>,
3000
3001 #[serde(default, skip_serializing_if = "Option::is_none")]
3004 pub implementation_info: Option<ImplementationInfo>,
3005
3006 #[serde(default, skip_serializing_if = "Option::is_none")]
3009 pub ai_evaluation: Option<StoredAiEvaluation>,
3010
3011 #[serde(skip)]
3014 pub version: i64,
3015}
3016
3017impl Requirement {
3018 pub fn new(title: String, description: String) -> Self {
3020 let now = Utc::now();
3021
3022 let default_feature =
3024 env::var("REQ_FEATURE").unwrap_or_else(|_| String::from("Uncategorized"));
3025
3026 Self {
3027 id: Uuid::now_v7(),
3028 spec_id: None, agreed_id: None,
3030 prefix_override: None,
3031 title,
3032 description,
3033 status: RequirementStatus::Draft,
3034 priority: RequirementPriority::Medium,
3035 owner: String::new(),
3036 feature: default_feature,
3037 created_at: now,
3038 created_by: None,
3039 modified_at: now,
3040 req_type: RequirementType::Functional,
3041 meta_subtype: None,
3042 dependencies: Vec::new(),
3043 tags: HashSet::new(),
3044 weight: None,
3045 relationships: Vec::new(),
3046 comments: Vec::new(),
3047 history: Vec::new(),
3048 archived: false,
3049 custom_status: None,
3050 custom_priority: None,
3051 custom_fields: std::collections::HashMap::new(),
3052 urls: Vec::new(),
3053 attachments: Vec::new(),
3054 trace_links: Vec::new(),
3055 gitlab_issues: Vec::new(),
3056 implementation_info: None,
3057 version: 1,
3058 ai_evaluation: None,
3059 }
3060 }
3061
3062 pub fn display_id(&self) -> String {
3064 self.agreed_id
3065 .as_deref()
3066 .or(self.spec_id.as_deref())
3067 .unwrap_or_else(|| "?")
3068 .to_string()
3069 }
3070
3071 pub fn matches_id(&self, id: &str) -> bool {
3074 self.spec_id.as_deref() == Some(id)
3075 || self.agreed_id.as_deref() == Some(id)
3076 || self.id.to_string() == id
3077 }
3078
3079 pub fn effective_status(&self) -> String {
3081 self.custom_status
3082 .clone()
3083 .unwrap_or_else(|| self.status.to_string())
3084 }
3085
3086 pub fn set_status_from_str(&mut self, status_str: &str) {
3088 match status_str {
3089 "Draft" => {
3090 self.status = RequirementStatus::Draft;
3091 self.custom_status = None;
3092 }
3093 "Approved" => {
3094 self.status = RequirementStatus::Approved;
3095 self.custom_status = None;
3096 }
3097 "Completed" => {
3098 self.status = RequirementStatus::Completed;
3099 self.custom_status = None;
3100 }
3101 "Rejected" => {
3102 self.status = RequirementStatus::Rejected;
3103 self.custom_status = None;
3104 }
3105 other => {
3106 self.custom_status = Some(other.to_string());
3108 }
3109 }
3110 }
3111
3112 pub fn effective_priority(&self) -> String {
3114 self.custom_priority
3115 .clone()
3116 .unwrap_or_else(|| self.priority.to_string())
3117 }
3118
3119 pub fn set_priority_from_str(&mut self, priority_str: &str) {
3121 match priority_str {
3122 "High" => {
3123 self.priority = RequirementPriority::High;
3124 self.custom_priority = None;
3125 }
3126 "Medium" => {
3127 self.priority = RequirementPriority::Medium;
3128 self.custom_priority = None;
3129 }
3130 "Low" => {
3131 self.priority = RequirementPriority::Low;
3132 self.custom_priority = None;
3133 }
3134 other => {
3135 self.custom_priority = Some(other.to_string());
3137 }
3138 }
3139 }
3140
3141 pub fn get_custom_field(&self, name: &str) -> Option<&String> {
3143 self.custom_fields.get(name)
3144 }
3145
3146 pub fn set_custom_field(&mut self, name: impl Into<String>, value: impl Into<String>) {
3148 self.custom_fields.insert(name.into(), value.into());
3149 }
3150
3151 pub fn remove_custom_field(&mut self, name: &str) -> Option<String> {
3153 self.custom_fields.remove(name)
3154 }
3155
3156 pub fn validate_prefix(prefix: &str) -> Option<String> {
3160 let trimmed = prefix.trim();
3161 if trimmed.is_empty() {
3162 return None;
3163 }
3164 let upper = trimmed.to_uppercase();
3165 if upper.chars().all(|c| c.is_ascii_uppercase()) {
3166 Some(upper)
3167 } else {
3168 None
3169 }
3170 }
3171
3172 pub fn set_prefix_override(&mut self, prefix: &str) -> Result<(), String> {
3175 let trimmed = prefix.trim();
3176 if trimmed.is_empty() {
3177 self.prefix_override = None;
3178 return Ok(());
3179 }
3180 match Self::validate_prefix(trimmed) {
3181 Some(valid) => {
3182 self.prefix_override = Some(valid);
3183 Ok(())
3184 }
3185 None => Err("Prefix must contain only uppercase letters (A-Z)".to_string()),
3186 }
3187 }
3188
3189 pub fn record_change(&mut self, author: String, changes: Vec<FieldChange>) {
3191 if !changes.is_empty() {
3192 let entry = HistoryEntry::new(author, changes);
3193 self.history.push(entry);
3194 self.modified_at = Utc::now();
3195 }
3196 }
3197
3198 pub fn field_change(field_name: &str, old_value: String, new_value: String) -> FieldChange {
3200 FieldChange {
3201 field_name: field_name.to_string(),
3202 old_value,
3203 new_value,
3204 }
3205 }
3206
3207 pub fn add_comment(&mut self, comment: Comment) {
3209 self.comments.push(comment);
3210 self.modified_at = Utc::now();
3211 }
3212
3213 pub fn add_reply(&mut self, parent_id: Uuid, reply: Comment) -> anyhow::Result<()> {
3215 for comment in &mut self.comments {
3216 if comment.id == parent_id {
3217 comment.add_reply(reply);
3218 self.modified_at = Utc::now();
3219 return Ok(());
3220 }
3221 if let Some(found) = comment.find_comment_mut(&parent_id) {
3222 found.add_reply(reply);
3223 self.modified_at = Utc::now();
3224 return Ok(());
3225 }
3226 }
3227 anyhow::bail!("Parent comment not found")
3228 }
3229
3230 pub fn find_comment_mut(&mut self, comment_id: &Uuid) -> Option<&mut Comment> {
3232 for comment in &mut self.comments {
3233 if &comment.id == comment_id {
3234 return Some(comment);
3235 }
3236 if let Some(found) = comment.find_comment_mut(comment_id) {
3237 return Some(found);
3238 }
3239 }
3240 None
3241 }
3242
3243 pub fn delete_comment(&mut self, comment_id: &Uuid) -> anyhow::Result<()> {
3245 if let Some(pos) = self.comments.iter().position(|c| &c.id == comment_id) {
3247 self.comments.remove(pos);
3248 self.modified_at = Utc::now();
3249 return Ok(());
3250 }
3251
3252 for comment in &mut self.comments {
3254 if Comment::remove_reply_recursive(comment, comment_id) {
3255 self.modified_at = Utc::now();
3256 return Ok(());
3257 }
3258 }
3259
3260 anyhow::bail!("Comment not found")
3261 }
3262
3263 pub fn content_hash(&self) -> String {
3266 use std::collections::hash_map::DefaultHasher;
3267 use std::hash::{Hash, Hasher};
3268
3269 let mut hasher = DefaultHasher::new();
3270 self.title.hash(&mut hasher);
3271 self.description.hash(&mut hasher);
3272 self.req_type.to_string().hash(&mut hasher);
3273 format!("{:016x}", hasher.finish())
3274 }
3275
3276 pub fn needs_ai_evaluation(&self) -> bool {
3278 match &self.ai_evaluation {
3279 None => true,
3280 Some(eval) => eval.is_stale(&self.content_hash()),
3281 }
3282 }
3283}
3284
3285#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3291pub struct AiActionPromptConfig {
3292 #[serde(default, skip_serializing_if = "Option::is_none")]
3295 pub custom_template: Option<String>,
3296
3297 #[serde(default, skip_serializing_if = "String::is_empty")]
3300 pub additional_instructions: String,
3301}
3302
3303#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3305pub struct AiTypePromptConfig {
3306 pub type_name: String,
3308
3309 #[serde(default, skip_serializing_if = "String::is_empty")]
3311 pub evaluation_extra: String,
3312
3313 #[serde(default, skip_serializing_if = "String::is_empty")]
3315 pub improve_extra: String,
3316
3317 #[serde(default, skip_serializing_if = "String::is_empty")]
3319 pub generate_children_extra: String,
3320}
3321
3322#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3324pub struct AiPromptConfig {
3325 #[serde(default, skip_serializing_if = "String::is_empty")]
3328 pub global_context: String,
3329
3330 #[serde(default, skip_serializing_if = "is_default_action_config")]
3332 pub evaluation: AiActionPromptConfig,
3333
3334 #[serde(default, skip_serializing_if = "is_default_action_config")]
3336 pub duplicates: AiActionPromptConfig,
3337
3338 #[serde(default, skip_serializing_if = "is_default_action_config")]
3340 pub relationships: AiActionPromptConfig,
3341
3342 #[serde(default, skip_serializing_if = "is_default_action_config")]
3344 pub improve: AiActionPromptConfig,
3345
3346 #[serde(default, skip_serializing_if = "is_default_action_config")]
3348 pub generate_children: AiActionPromptConfig,
3349
3350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3352 pub type_prompts: Vec<AiTypePromptConfig>,
3353}
3354
3355fn is_default_action_config(config: &AiActionPromptConfig) -> bool {
3357 config.custom_template.is_none() && config.additional_instructions.is_empty()
3358}
3359
3360impl AiPromptConfig {
3361 pub fn get_type_evaluation_extra(&self, type_name: &str) -> Option<&str> {
3363 self.type_prompts
3364 .iter()
3365 .find(|t| t.type_name == type_name)
3366 .filter(|t| !t.evaluation_extra.is_empty())
3367 .map(|t| t.evaluation_extra.as_str())
3368 }
3369
3370 pub fn get_type_improve_extra(&self, type_name: &str) -> Option<&str> {
3372 self.type_prompts
3373 .iter()
3374 .find(|t| t.type_name == type_name)
3375 .filter(|t| !t.improve_extra.is_empty())
3376 .map(|t| t.improve_extra.as_str())
3377 }
3378
3379 pub fn get_type_generate_children_extra(&self, type_name: &str) -> Option<&str> {
3381 self.type_prompts
3382 .iter()
3383 .find(|t| t.type_name == type_name)
3384 .filter(|t| !t.generate_children_extra.is_empty())
3385 .map(|t| t.generate_children_extra.as_str())
3386 }
3387}
3388
3389#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3391pub struct RequirementsStore {
3392 #[serde(default)]
3394 pub name: String,
3395
3396 #[serde(default)]
3398 pub title: String,
3399
3400 #[serde(default)]
3402 pub description: String,
3403
3404 pub requirements: Vec<Requirement>,
3405
3406 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3408 pub users: Vec<User>,
3409
3410 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3412 pub teams: Vec<Team>,
3413
3414 #[serde(default)]
3416 pub id_config: IdConfiguration,
3417
3418 #[serde(default)]
3420 pub features: Vec<FeatureDefinition>,
3421
3422 #[serde(default = "default_next_feature_number")]
3424 pub next_feature_number: u32,
3425
3426 #[serde(default = "default_next_spec_number")]
3428 pub next_spec_number: u32,
3429
3430 #[serde(default)]
3433 pub prefix_counters: std::collections::HashMap<String, u32>,
3434
3435 #[serde(default = "RelationshipDefinition::defaults")]
3437 pub relationship_definitions: Vec<RelationshipDefinition>,
3438
3439 #[serde(default = "default_reaction_definitions")]
3441 pub reaction_definitions: Vec<ReactionDefinition>,
3442
3443 #[serde(default)]
3446 pub meta_counters: std::collections::HashMap<String, u32>,
3447
3448 #[serde(default = "default_type_definitions")]
3450 pub type_definitions: Vec<CustomTypeDefinition>,
3451
3452 #[serde(default)]
3455 pub allowed_prefixes: Vec<String>,
3456
3457 #[serde(default)]
3461 pub restrict_prefixes: bool,
3462
3463 #[serde(default, skip_serializing_if = "is_default_ai_prompt_config")]
3465 pub ai_prompts: AiPromptConfig,
3466
3467 #[serde(default, skip_serializing_if = "Vec::is_empty")]
3469 pub baselines: Vec<Baseline>,
3470
3471 #[serde(skip)]
3474 pub store_version: i64,
3475
3476 #[serde(default, skip_serializing_if = "Option::is_none")]
3480 pub migrated_to: Option<String>,
3481
3482 #[serde(skip)]
3487 #[ts(skip)]
3488 pub dispenser: Option<DispenserHandle>,
3489}
3490
3491#[derive(Clone)]
3494pub struct DispenserHandle(pub std::sync::Arc<dyn crate::dispenser::Dispenser>);
3495
3496impl std::fmt::Debug for DispenserHandle {
3497 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3498 write!(f, "DispenserHandle(<active>)")
3499 }
3500}
3501
3502impl std::ops::Deref for DispenserHandle {
3503 type Target = dyn crate::dispenser::Dispenser;
3504 fn deref(&self) -> &Self::Target {
3505 &*self.0
3506 }
3507}
3508
3509fn is_default_ai_prompt_config(config: &AiPromptConfig) -> bool {
3511 config.global_context.is_empty()
3512 && is_default_action_config(&config.evaluation)
3513 && is_default_action_config(&config.duplicates)
3514 && is_default_action_config(&config.relationships)
3515 && is_default_action_config(&config.improve)
3516 && is_default_action_config(&config.generate_children)
3517 && config.type_prompts.is_empty()
3518}
3519
3520fn default_next_feature_number() -> u32 {
3522 1
3523}
3524
3525fn default_next_spec_number() -> u32 {
3527 1
3528}
3529
3530pub const META_PREFIX_USER: &str = "$USER";
3532pub const META_PREFIX_VIEW: &str = "$VIEW";
3533pub const META_PREFIX_FEATURE: &str = "$FEAT";
3534pub const META_PREFIX_TEAM: &str = "$TEAM";
3535
3536impl RequirementsStore {
3537 pub fn new() -> Self {
3539 Self {
3540 name: String::new(),
3541 title: String::new(),
3542 description: String::new(),
3543 requirements: Vec::new(),
3544 users: Vec::new(),
3545 teams: Vec::new(),
3546 id_config: IdConfiguration::default(),
3547 features: Vec::new(),
3548 next_feature_number: 1,
3549 next_spec_number: 1,
3550 prefix_counters: std::collections::HashMap::new(),
3551 relationship_definitions: RelationshipDefinition::defaults(),
3552 reaction_definitions: default_reaction_definitions(),
3553 meta_counters: std::collections::HashMap::new(),
3554 type_definitions: default_type_definitions(),
3555 allowed_prefixes: Vec::new(),
3556 restrict_prefixes: false,
3557 ai_prompts: AiPromptConfig::default(),
3558 baselines: Vec::new(),
3559 store_version: 1,
3560 migrated_to: None,
3561 dispenser: None,
3562 }
3563 }
3564
3565 pub fn get_type_definition(&self, req_type: &RequirementType) -> Option<&CustomTypeDefinition> {
3567 let type_name = match req_type {
3568 RequirementType::Functional => "Functional",
3569 RequirementType::NonFunctional => "NonFunctional",
3570 RequirementType::System => "System",
3571 RequirementType::User => "User",
3572 RequirementType::ChangeRequest => "ChangeRequest",
3573 RequirementType::Bug => "Bug",
3574 RequirementType::Epic => "Epic",
3575 RequirementType::Story => "Story",
3576 RequirementType::Task => "Task",
3577 RequirementType::Spike => "Spike",
3578 RequirementType::Sprint => "Sprint",
3579 RequirementType::Folder => "Folder",
3580 RequirementType::Meta => "Meta",
3581 };
3582 self.type_definitions.iter().find(|td| td.name == type_name)
3583 }
3584
3585 pub fn get_statuses_for_type(&self, req_type: &RequirementType) -> Vec<String> {
3587 self.get_type_definition(req_type)
3588 .map(|td| td.get_statuses())
3589 .unwrap_or_else(|| {
3590 vec![
3591 "Draft".to_string(),
3592 "Approved".to_string(),
3593 "Completed".to_string(),
3594 "Rejected".to_string(),
3595 ]
3596 })
3597 }
3598
3599 pub fn get_priorities_for_type(&self, req_type: &RequirementType) -> Vec<String> {
3601 self.get_type_definition(req_type)
3602 .map(|td| td.get_priorities())
3603 .unwrap_or_else(|| vec!["High".to_string(), "Medium".to_string(), "Low".to_string()])
3604 }
3605
3606 pub fn get_custom_fields_for_type(
3608 &self,
3609 req_type: &RequirementType,
3610 ) -> Vec<CustomFieldDefinition> {
3611 self.get_type_definition(req_type)
3612 .map(|td| {
3613 let mut fields = td.custom_fields.clone();
3614 fields.sort_by_key(|f| f.order);
3615 fields
3616 })
3617 .unwrap_or_default()
3618 }
3619
3620 pub fn is_type_stateless(&self, req_type: &RequirementType) -> bool {
3622 self.get_type_definition(req_type)
3623 .map(|td| td.stateless)
3624 .unwrap_or(false)
3625 }
3626
3627 pub fn get_sprints(&self) -> Vec<&Requirement> {
3633 let mut sprints: Vec<&Requirement> = self
3634 .requirements
3635 .iter()
3636 .filter(|r| r.req_type == RequirementType::Sprint)
3637 .collect();
3638
3639 sprints.sort_by(|a, b| {
3641 let a_num = a
3642 .custom_fields
3643 .get("sprint_number")
3644 .and_then(|s| s.parse::<i32>().ok())
3645 .unwrap_or(i32::MAX);
3646 let b_num = b
3647 .custom_fields
3648 .get("sprint_number")
3649 .and_then(|s| s.parse::<i32>().ok())
3650 .unwrap_or(i32::MAX);
3651 a_num.cmp(&b_num)
3652 });
3653
3654 sprints
3655 }
3656
3657 pub fn get_sprint_items(&self, sprint_id: &Uuid) -> Vec<&Requirement> {
3659 self.requirements
3660 .iter()
3661 .filter(|r| {
3662 r.relationships.iter().any(|rel| {
3663 rel.rel_type == RelationshipType::Custom("sprint_assignment".to_string())
3664 && rel.target_id == *sprint_id
3665 })
3666 })
3667 .collect()
3668 }
3669
3670 pub fn get_requirement_sprint(&self, req_id: &Uuid) -> Option<&Requirement> {
3672 let req = self.get_requirement_by_id(req_id)?;
3673 let sprint_rel = req.relationships.iter().find(|rel| {
3674 rel.rel_type == RelationshipType::Custom("sprint_assignment".to_string())
3675 })?;
3676 self.get_requirement_by_id(&sprint_rel.target_id)
3677 }
3678
3679 pub fn get_backlog(&self) -> Vec<&Requirement> {
3681 self.requirements
3682 .iter()
3683 .filter(|r| {
3684 r.req_type != RequirementType::Sprint
3686 && r.req_type != RequirementType::Folder
3687 && r.req_type != RequirementType::Meta
3688 && !r.relationships.iter().any(|rel| {
3690 rel.rel_type == RelationshipType::Custom("sprint_assignment".to_string())
3691 })
3692 })
3693 .collect()
3694 }
3695
3696 pub fn assign_to_sprint(&mut self, req_id: Uuid, sprint_id: Uuid, username: &str) {
3699 if let Some(req) = self.requirements.iter_mut().find(|r| r.id == req_id) {
3701 req.relationships.retain(|rel| {
3702 rel.rel_type != RelationshipType::Custom("sprint_assignment".to_string())
3703 });
3704
3705 req.relationships.push(Relationship {
3707 rel_type: RelationshipType::Custom("sprint_assignment".to_string()),
3708 target_id: sprint_id,
3709 created_at: Some(Utc::now()),
3710 created_by: Some(username.to_string()),
3711 });
3712
3713 req.modified_at = Utc::now();
3715
3716 let sprint = self.requirements.iter().find(|r| r.id == sprint_id);
3718 let sprint_name = sprint
3719 .and_then(|s| s.spec_id.clone())
3720 .unwrap_or_else(|| sprint_id.to_string());
3721
3722 let history_entry = HistoryEntry::new(
3723 username.to_string(),
3724 vec![FieldChange {
3725 field_name: "sprint_assignment".to_string(),
3726 old_value: String::new(),
3727 new_value: sprint_name,
3728 }],
3729 );
3730
3731 if let Some(req) = self.requirements.iter_mut().find(|r| r.id == req_id) {
3732 req.history.push(history_entry);
3733 }
3734 }
3735
3736 if let Some(sprint) = self.requirements.iter_mut().find(|r| r.id == sprint_id) {
3738 if !sprint.relationships.iter().any(|rel| {
3740 rel.rel_type == RelationshipType::Custom("sprint_contains".to_string())
3741 && rel.target_id == req_id
3742 }) {
3743 sprint.relationships.push(Relationship {
3744 rel_type: RelationshipType::Custom("sprint_contains".to_string()),
3745 target_id: req_id,
3746 created_at: Some(Utc::now()),
3747 created_by: Some(username.to_string()),
3748 });
3749 }
3750 }
3751 }
3752
3753 pub fn remove_from_sprint(&mut self, req_id: Uuid, username: &str) {
3755 let current_sprint_id = self
3757 .requirements
3758 .iter()
3759 .find(|r| r.id == req_id)
3760 .and_then(|req| {
3761 req.relationships.iter().find_map(|rel| {
3762 if rel.rel_type == RelationshipType::Custom("sprint_assignment".to_string()) {
3763 Some(rel.target_id)
3764 } else {
3765 None
3766 }
3767 })
3768 });
3769
3770 if let Some(sprint_id) = current_sprint_id {
3771 if let Some(req) = self.requirements.iter_mut().find(|r| r.id == req_id) {
3773 req.relationships.retain(|rel| {
3774 rel.rel_type != RelationshipType::Custom("sprint_assignment".to_string())
3775 });
3776 req.modified_at = Utc::now();
3777
3778 let history_entry = HistoryEntry::new(
3780 username.to_string(),
3781 vec![FieldChange {
3782 field_name: "sprint_assignment".to_string(),
3783 old_value: sprint_id.to_string(),
3784 new_value: String::new(),
3785 }],
3786 );
3787 req.history.push(history_entry);
3788 }
3789
3790 if let Some(sprint) = self.requirements.iter_mut().find(|r| r.id == sprint_id) {
3792 sprint.relationships.retain(|rel| {
3793 !(rel.rel_type == RelationshipType::Custom("sprint_contains".to_string())
3794 && rel.target_id == req_id)
3795 });
3796 }
3797 }
3798 }
3799
3800 pub fn get_used_prefixes(&self) -> Vec<String> {
3802 let mut prefixes: std::collections::HashSet<String> = std::collections::HashSet::new();
3803
3804 for req in &self.requirements {
3805 if let Some(ref spec_id) = req.spec_id {
3806 if let Some(prefix) = spec_id.split('-').next() {
3808 if !prefix.starts_with('$') {
3810 prefixes.insert(prefix.to_string());
3811 }
3812 }
3813 }
3814 }
3815
3816 let mut result: Vec<String> = prefixes.into_iter().collect();
3817 result.sort();
3818 result
3819 }
3820
3821 pub fn get_all_prefixes(&self) -> Vec<String> {
3823 let mut prefixes: std::collections::HashSet<String> = std::collections::HashSet::new();
3824
3825 for p in &self.allowed_prefixes {
3827 prefixes.insert(p.clone());
3828 }
3829
3830 for p in self.get_used_prefixes() {
3832 prefixes.insert(p);
3833 }
3834
3835 let mut result: Vec<String> = prefixes.into_iter().collect();
3836 result.sort();
3837 result
3838 }
3839
3840 pub fn add_allowed_prefix(&mut self, prefix: &str) {
3842 let prefix = prefix.to_uppercase();
3843 if !self.allowed_prefixes.contains(&prefix) {
3844 self.allowed_prefixes.push(prefix);
3845 self.allowed_prefixes.sort();
3846 }
3847 }
3848
3849 pub fn remove_allowed_prefix(&mut self, prefix: &str) {
3851 self.allowed_prefixes.retain(|p| p != prefix);
3852 }
3853
3854 pub fn is_prefix_allowed(&self, prefix: &str) -> bool {
3856 if !self.restrict_prefixes {
3857 return true;
3858 }
3859 self.allowed_prefixes
3860 .iter()
3861 .any(|p| p.eq_ignore_ascii_case(prefix))
3862 }
3863
3864 pub fn next_meta_id(&mut self, prefix: &str) -> String {
3866 if let Some(ref dispenser) = self.dispenser {
3867 if let Ok(id) = dispenser.next_id(prefix) {
3868 return id;
3869 }
3870 }
3871 let counter = self.meta_counters.entry(prefix.to_string()).or_insert(1);
3872 let num = *counter;
3873 *counter += 1;
3874 format!("{}-{:03}", prefix, num)
3875 }
3876
3877 pub fn add_requirement(&mut self, req: Requirement) {
3879 self.requirements.push(req);
3880 }
3881
3882 pub fn add_user(&mut self, user: User) {
3884 self.users.push(user);
3885 }
3886
3887 pub fn add_user_with_id(&mut self, name: String, email: String, handle: String) -> String {
3889 let spec_id = self.next_meta_id(META_PREFIX_USER);
3890 let user = User::new_with_spec_id(name, email, handle, spec_id.clone());
3891 self.users.push(user);
3892 spec_id
3893 }
3894
3895 pub fn find_user_by_spec_id(&self, spec_id: &str) -> Option<&User> {
3897 self.users
3898 .iter()
3899 .find(|u| u.spec_id.as_deref() == Some(spec_id))
3900 }
3901
3902 pub fn find_user_by_spec_id_mut(&mut self, spec_id: &str) -> Option<&mut User> {
3904 self.users
3905 .iter_mut()
3906 .find(|u| u.spec_id.as_deref() == Some(spec_id))
3907 }
3908
3909 pub fn find_user_by_id(&self, id: &Uuid) -> Option<&User> {
3911 self.users.iter().find(|u| u.id == *id)
3912 }
3913
3914 pub fn migrate_users_to_spec_ids(&mut self) {
3916 for user in &mut self.users {
3917 if user.spec_id.is_none() {
3918 let counter = self
3919 .meta_counters
3920 .entry(META_PREFIX_USER.to_string())
3921 .or_insert(1);
3922 let spec_id = format!("{}-{:03}", META_PREFIX_USER, *counter);
3923 *counter += 1;
3924 user.spec_id = Some(spec_id);
3925 }
3926 }
3927 }
3928
3929 pub fn get_user_by_id_mut(&mut self, id: &Uuid) -> Option<&mut User> {
3931 self.users.iter_mut().find(|u| &u.id == id)
3932 }
3933
3934 pub fn remove_user(&mut self, id: &Uuid) -> bool {
3936 if let Some(pos) = self.users.iter().position(|u| &u.id == id) {
3937 self.users.remove(pos);
3938 true
3939 } else {
3940 false
3941 }
3942 }
3943
3944 pub fn add_team(&mut self, team: Team) {
3948 self.teams.push(team);
3949 }
3950
3951 pub fn add_team_with_id(
3953 &mut self,
3954 name: String,
3955 description: String,
3956 parent_team_id: Option<Uuid>,
3957 ) -> String {
3958 let spec_id = self.next_meta_id(META_PREFIX_TEAM);
3959 let team = Team::new_with_spec_id(name, description, parent_team_id, spec_id.clone());
3960 self.teams.push(team);
3961 spec_id
3962 }
3963
3964 pub fn find_team_by_spec_id(&self, spec_id: &str) -> Option<&Team> {
3966 self.teams
3967 .iter()
3968 .find(|t| t.spec_id.as_deref() == Some(spec_id))
3969 }
3970
3971 pub fn find_team_by_spec_id_mut(&mut self, spec_id: &str) -> Option<&mut Team> {
3973 self.teams
3974 .iter_mut()
3975 .find(|t| t.spec_id.as_deref() == Some(spec_id))
3976 }
3977
3978 pub fn find_team_by_id(&self, id: &Uuid) -> Option<&Team> {
3980 self.teams.iter().find(|t| t.id == *id)
3981 }
3982
3983 pub fn get_team_by_id_mut(&mut self, id: &Uuid) -> Option<&mut Team> {
3985 self.teams.iter_mut().find(|t| &t.id == id)
3986 }
3987
3988 pub fn remove_team(&mut self, id: &Uuid) -> bool {
3990 if let Some(pos) = self.teams.iter().position(|t| &t.id == id) {
3991 self.teams.remove(pos);
3992 true
3993 } else {
3994 false
3995 }
3996 }
3997
3998 pub fn get_child_teams(&self, parent_id: &Uuid) -> Vec<&Team> {
4000 self.teams
4001 .iter()
4002 .filter(|t| t.parent_team_id.as_ref() == Some(parent_id))
4003 .collect()
4004 }
4005
4006 pub fn get_root_teams(&self) -> Vec<&Team> {
4008 self.teams
4009 .iter()
4010 .filter(|t| t.parent_team_id.is_none())
4011 .collect()
4012 }
4013
4014 pub fn get_teams_for_user(&self, user_id: &Uuid) -> Vec<&Team> {
4016 self.teams
4017 .iter()
4018 .filter(|t| t.member_ids.contains(user_id))
4019 .collect()
4020 }
4021
4022 pub fn would_create_team_cycle(&self, team_id: &Uuid, proposed_parent_id: &Uuid) -> bool {
4024 if team_id == proposed_parent_id {
4026 return true;
4027 }
4028
4029 let mut current_id = Some(*proposed_parent_id);
4031 while let Some(id) = current_id {
4032 if &id == team_id {
4033 return true;
4034 }
4035 current_id = self.find_team_by_id(&id).and_then(|t| t.parent_team_id);
4036 }
4037 false
4038 }
4039
4040 pub fn migrate_teams_to_spec_ids(&mut self) {
4042 for team in &mut self.teams {
4043 if team.spec_id.is_none() {
4044 let counter = self
4045 .meta_counters
4046 .entry(META_PREFIX_TEAM.to_string())
4047 .or_insert(1);
4048 let spec_id = format!("{}-{:03}", META_PREFIX_TEAM, *counter);
4049 *counter += 1;
4050 team.spec_id = Some(spec_id);
4051 }
4052 }
4053 }
4054
4055 pub fn get_requirement_by_id(&self, id: &Uuid) -> Option<&Requirement> {
4059 self.requirements.iter().find(|r| r.id == *id)
4060 }
4061
4062 pub fn get_requirement_by_id_mut(&mut self, id: &Uuid) -> Option<&mut Requirement> {
4064 self.requirements.iter_mut().find(|r| r.id == *id)
4065 }
4066
4067 pub fn get_next_feature_number(&mut self) -> u32 {
4069 let current_number = self.next_feature_number;
4070 self.next_feature_number += 1;
4071 current_number
4072 }
4073
4074 pub fn format_feature_with_number(&self, feature_name: &str) -> String {
4076 format!("{}-{}", self.next_feature_number, feature_name)
4077 }
4078
4079 pub fn get_feature_names(&self) -> Vec<String> {
4081 let mut feature_names = Vec::new();
4082
4083 for req in &self.requirements {
4084 if feature_names.contains(&req.feature) {
4086 continue;
4087 }
4088
4089 feature_names.push(req.feature.clone());
4090 }
4091
4092 feature_names.sort_by(|a, b| {
4094 let a_parts: Vec<&str> = a.splitn(2, '-').collect();
4095 let b_parts: Vec<&str> = b.splitn(2, '-').collect();
4096
4097 if a_parts.len() > 1 && b_parts.len() > 1 {
4099 if let (Ok(a_num), Ok(b_num)) =
4100 (a_parts[0].parse::<u32>(), b_parts[0].parse::<u32>())
4101 {
4102 return a_num.cmp(&b_num);
4103 }
4104 }
4105
4106 a.cmp(b)
4108 });
4109
4110 feature_names
4111 }
4112
4113 pub fn update_feature_name(&mut self, old_name: &str, new_name: &str) {
4115 for req in &mut self.requirements {
4116 if req.feature == old_name {
4117 req.feature = new_name.to_string();
4118 }
4119 }
4120 }
4121
4122 pub fn migrate_features(&mut self) {
4124 let mut unique_features: Vec<String> = Vec::new();
4126
4127 for req in &self.requirements {
4128 if req.feature.contains('-') {
4130 if let Some((prefix, _)) = req.feature.split_once('-') {
4131 if prefix.parse::<u32>().is_ok() {
4132 continue; }
4134 }
4135 }
4136
4137 if !unique_features.contains(&req.feature) {
4138 unique_features.push(req.feature.clone());
4139 }
4140 }
4141
4142 for feature in unique_features {
4144 let number = self.get_next_feature_number();
4145 let new_name = format!("{}-{}", number, feature);
4146
4147 self.update_feature_name(&feature, &new_name);
4149 }
4150 }
4151
4152 pub fn get_requirement_by_spec_id(&self, spec_id: &str) -> Option<&Requirement> {
4154 self.requirements
4155 .iter()
4156 .find(|r| r.spec_id.as_ref().map(|s| s.as_str()) == Some(spec_id))
4157 }
4158
4159 pub fn get_requirement_by_spec_id_mut(&mut self, spec_id: &str) -> Option<&mut Requirement> {
4161 self.requirements
4162 .iter_mut()
4163 .find(|r| r.spec_id.as_ref().map(|s| s.as_str()) == Some(spec_id))
4164 }
4165
4166 pub fn assign_spec_ids(&mut self) {
4168 for req in &mut self.requirements {
4169 if req.spec_id.is_none() {
4170 req.spec_id = Some(format!("SPEC-{:03}", self.next_spec_number));
4171 self.next_spec_number += 1;
4172 }
4173 }
4174 }
4175
4176 pub fn peek_next_spec_id(&self) -> String {
4178 format!("SPEC-{:03}", self.next_spec_number)
4179 }
4180
4181 pub fn validate_unique_spec_ids(&self) -> anyhow::Result<()> {
4183 use std::collections::HashSet;
4184 let mut seen = HashSet::new();
4185
4186 for req in &self.requirements {
4187 if let Some(spec_id) = &req.spec_id {
4188 if !seen.insert(spec_id) {
4189 anyhow::bail!("Duplicate SPEC-ID found: {}", spec_id);
4190 }
4191 }
4192 }
4193
4194 Ok(())
4195 }
4196
4197 pub fn repair_duplicate_spec_ids(&mut self) -> usize {
4201 use std::collections::HashSet;
4202 let mut seen: HashSet<String> = HashSet::new();
4203 let mut duplicates: Vec<(usize, String)> = Vec::new();
4204
4205 for (idx, req) in self.requirements.iter().enumerate() {
4207 if let Some(spec_id) = &req.spec_id {
4208 if !seen.insert(spec_id.clone()) {
4209 let prefix = Self::extract_prefix_from_spec_id(spec_id);
4211 duplicates.push((idx, prefix));
4212 }
4213 }
4214 }
4215
4216 if duplicates.is_empty() {
4217 return 0;
4218 }
4219
4220 eprintln!(
4222 "Found {} duplicate SPEC-ID(s), automatically repairing...",
4223 duplicates.len()
4224 );
4225
4226 let repairs: Vec<(usize, String)> = duplicates
4229 .iter()
4230 .map(|(idx, prefix)| {
4231 let new_id = self.generate_requirement_id_with_override(prefix);
4232 (*idx, new_id)
4233 })
4234 .collect();
4235
4236 for (idx, new_id) in &repairs {
4238 if let Some(req) = self.requirements.get_mut(*idx) {
4239 let old_id = req.spec_id.clone().unwrap_or_default();
4240 eprintln!(" Repaired: {} -> {} ({})", old_id, new_id, req.title);
4241 req.spec_id = Some(new_id.clone());
4242 seen.insert(new_id.clone());
4244 }
4245 }
4246
4247 repairs.len()
4248 }
4249
4250 fn extract_prefix_from_spec_id(spec_id: &str) -> String {
4252 if let Some(last_dash_pos) = spec_id.rfind('-') {
4254 let after_dash = &spec_id[last_dash_pos + 1..];
4255 if after_dash.chars().all(|c| c.is_ascii_digit()) {
4256 return spec_id[..last_dash_pos].to_string();
4258 }
4259 }
4260 if spec_id.is_empty() {
4262 "REQ".to_string()
4263 } else {
4264 spec_id.to_string()
4265 }
4266 }
4267
4268 pub fn add_requirement_with_spec_id(&mut self, mut req: Requirement) {
4270 if req.spec_id.is_none() {
4271 req.spec_id = Some(format!("SPEC-{:03}", self.next_spec_number));
4272 self.next_spec_number += 1;
4273 }
4274 self.requirements.push(req);
4275 }
4276
4277 pub fn migrate_type_definitions(&mut self) -> bool {
4280 let defaults = default_type_definitions();
4281 let mut added = false;
4282
4283 for default_type in defaults {
4284 if default_type.built_in {
4286 let exists = self
4287 .type_definitions
4288 .iter()
4289 .any(|t| t.name == default_type.name);
4290 if !exists {
4291 self.type_definitions.push(default_type);
4292 added = true;
4293 }
4294 }
4295 }
4296
4297 added
4298 }
4299
4300 pub fn migrate_id_config_types(&mut self) -> bool {
4305 let defaults = default_requirement_types();
4306 let mut added = false;
4307
4308 for default_type in defaults {
4309 let exists = self.id_config.requirement_types.iter().any(|t| {
4310 t.name.to_lowercase() == default_type.name.to_lowercase()
4311 || t.prefix == default_type.prefix
4312 });
4313 if !exists {
4314 self.id_config.requirement_types.push(default_type);
4315 added = true;
4316 }
4317 }
4318
4319 added
4320 }
4321
4322 pub fn add_feature(&mut self, name: &str, prefix: &str) -> anyhow::Result<FeatureDefinition> {
4329 let prefix_upper = prefix.to_uppercase();
4330
4331 if self.id_config.is_prefix_reserved(&prefix_upper) {
4333 anyhow::bail!(
4334 "Prefix '{}' is reserved for requirement type '{}'",
4335 prefix_upper,
4336 self.id_config
4337 .get_type_by_prefix(&prefix_upper)
4338 .map(|t| t.name.as_str())
4339 .unwrap_or("unknown")
4340 );
4341 }
4342
4343 if self.features.iter().any(|f| f.prefix == prefix_upper) {
4345 anyhow::bail!(
4346 "Prefix '{}' is already used by another feature",
4347 prefix_upper
4348 );
4349 }
4350
4351 let feature = FeatureDefinition::new(self.next_feature_number, name, &prefix_upper);
4352 self.next_feature_number += 1;
4353 self.features.push(feature.clone());
4354 Ok(feature)
4355 }
4356
4357 pub fn get_feature_by_name(&self, name: &str) -> Option<&FeatureDefinition> {
4359 let lower = name.to_lowercase();
4360 self.features
4361 .iter()
4362 .find(|f| f.name.to_lowercase() == lower)
4363 }
4364
4365 pub fn get_feature_by_prefix(&self, prefix: &str) -> Option<&FeatureDefinition> {
4367 let upper = prefix.to_uppercase();
4368 self.features.iter().find(|f| f.prefix == upper)
4369 }
4370
4371 fn get_next_counter_for_prefix(&mut self, prefix: &str) -> u32 {
4374 if let Some(ref dispenser) = self.dispenser {
4375 return dispenser.next(prefix).unwrap_or_else(|_| {
4376 let upper = prefix.to_uppercase();
4378 let counter = self.prefix_counters.entry(upper).or_insert(1);
4379 let current = *counter;
4380 *counter += 1;
4381 current
4382 });
4383 }
4384 let upper = prefix.to_uppercase();
4385 let counter = self.prefix_counters.entry(upper).or_insert(1);
4386 let current = *counter;
4387 *counter += 1;
4388 current
4389 }
4390
4391 fn get_next_global_number(&mut self) -> u32 {
4395 if let Some(ref dispenser) = self.dispenser {
4396 return dispenser.next("GLOBAL").unwrap_or_else(|_| {
4397 let n = self.next_spec_number;
4398 self.next_spec_number += 1;
4399 n
4400 });
4401 }
4402 let n = self.next_spec_number;
4403 self.next_spec_number += 1;
4404 n
4405 }
4406
4407 pub fn generate_requirement_id(
4411 &mut self,
4412 feature_prefix: Option<&str>,
4413 type_prefix: Option<&str>,
4414 ) -> String {
4415 let digits = self.id_config.digits;
4416
4417 match self.id_config.format {
4418 IdFormat::SingleLevel => {
4419 let prefix = type_prefix
4421 .or(feature_prefix)
4422 .map(|s| s.to_uppercase())
4423 .unwrap_or_else(|| "REQ".to_string());
4424
4425 let number = match self.id_config.numbering {
4426 NumberingStrategy::Global => self.get_next_global_number(),
4427 NumberingStrategy::PerPrefix | NumberingStrategy::PerFeatureType => {
4428 self.get_next_counter_for_prefix(&prefix)
4429 }
4430 };
4431
4432 if let Some(ref dispenser) = self.dispenser {
4434 if let Ok(id) = dispenser.format_id(&prefix, number) {
4435 return id;
4436 }
4437 }
4438
4439 format!("{}-{:0>width$}", prefix, number, width = digits as usize)
4440 }
4441 IdFormat::TwoLevel => {
4442 let feat = feature_prefix
4443 .map(|s| s.to_uppercase())
4444 .unwrap_or_else(|| "GEN".to_string()); let typ = type_prefix
4446 .map(|s| s.to_uppercase())
4447 .unwrap_or_else(|| "REQ".to_string());
4448
4449 let number = match self.id_config.numbering {
4450 NumberingStrategy::Global => self.get_next_global_number(),
4451 NumberingStrategy::PerPrefix => {
4452 self.get_next_counter_for_prefix(&feat)
4454 }
4455 NumberingStrategy::PerFeatureType => {
4456 let combo_key = format!("{}-{}", feat, typ);
4458 self.get_next_counter_for_prefix(&combo_key)
4459 }
4460 };
4461
4462 format!(
4463 "{}-{}-{:0>width$}",
4464 feat,
4465 typ,
4466 number,
4467 width = digits as usize
4468 )
4469 }
4470 }
4471 }
4472
4473 pub fn add_requirement_with_id(
4477 &mut self,
4478 mut req: Requirement,
4479 feature_prefix: Option<&str>,
4480 type_prefix: Option<&str>,
4481 ) {
4482 if req.spec_id.is_none() {
4483 if let Some(ref override_prefix) = req.prefix_override {
4485 req.spec_id = Some(self.generate_requirement_id_with_override(override_prefix));
4486 } else {
4487 req.spec_id = Some(self.generate_requirement_id(feature_prefix, type_prefix));
4488 }
4489 }
4490 self.requirements.push(req);
4491 }
4492
4493 fn generate_requirement_id_with_override(&mut self, prefix: &str) -> String {
4496 let prefix_upper = prefix.to_uppercase();
4497 let digits = self.id_config.digits;
4498
4499 let number = match self.id_config.numbering {
4500 NumberingStrategy::Global => self.get_next_global_number(),
4501 NumberingStrategy::PerPrefix | NumberingStrategy::PerFeatureType => {
4502 self.get_next_counter_for_prefix(&prefix_upper)
4504 }
4505 };
4506
4507 if let Some(ref dispenser) = self.dispenser {
4509 if let Ok(id) = dispenser.format_id(&prefix_upper, number) {
4510 return id;
4511 }
4512 }
4513
4514 format!(
4515 "{}-{:0>width$}",
4516 prefix_upper,
4517 number,
4518 width = digits as usize
4519 )
4520 }
4521
4522 pub fn get_type_prefix(&self, req_type: &RequirementType) -> Option<String> {
4526 let (type_name, fallback_prefix) = match req_type {
4528 RequirementType::Functional => ("Functional", "FR"),
4529 RequirementType::NonFunctional => ("Non-Functional", "NFR"),
4530 RequirementType::System => ("System", "SR"),
4531 RequirementType::User => ("User", "UR"),
4532 RequirementType::ChangeRequest => ("Change Request", "CR"),
4533 RequirementType::Bug => ("Bug", "BUG"),
4534 RequirementType::Epic => ("Epic", "EPIC"),
4535 RequirementType::Story => ("Story", "STORY"),
4536 RequirementType::Task => ("Task", "TASK"),
4537 RequirementType::Spike => ("Spike", "SPIKE"),
4538 RequirementType::Sprint => ("Sprint", "SPRINT"),
4539 RequirementType::Folder => ("Folder", "FOLDER"),
4540 RequirementType::Meta => ("Meta", "META"),
4541 };
4542 self.id_config
4544 .get_type_by_name(type_name)
4545 .map(|t| t.prefix.clone())
4546 .or_else(|| Some(fallback_prefix.to_string()))
4547 }
4548
4549 pub fn regenerate_spec_id_for_prefix_change(
4552 &mut self,
4553 req_uuid: &Uuid,
4554 new_prefix: Option<&str>,
4555 feature_prefix: Option<&str>,
4556 type_prefix: Option<&str>,
4557 ) -> Result<String, String> {
4558 let new_spec_id = if let Some(prefix) = new_prefix {
4560 self.generate_requirement_id_with_override(prefix)
4561 } else {
4562 self.generate_requirement_id(feature_prefix, type_prefix)
4563 };
4564
4565 let conflicts = self
4567 .requirements
4568 .iter()
4569 .any(|r| r.id != *req_uuid && r.spec_id.as_deref() == Some(&new_spec_id));
4570
4571 if conflicts {
4572 Err(format!(
4573 "ID '{}' is already in use by another requirement",
4574 new_spec_id
4575 ))
4576 } else {
4577 Ok(new_spec_id)
4578 }
4579 }
4580
4581 pub fn is_spec_id_available(&self, spec_id: &str, exclude_uuid: Option<&Uuid>) -> bool {
4583 !self.requirements.iter().any(|r| {
4584 r.spec_id.as_deref() == Some(spec_id) && exclude_uuid.map_or(true, |uuid| r.id != *uuid)
4585 })
4586 }
4587
4588 pub fn update_spec_id_for_type_change(
4591 &self,
4592 current_spec_id: Option<&str>,
4593 new_type: &RequirementType,
4594 ) -> Option<String> {
4595 let spec_id = current_spec_id?;
4596 let new_prefix = self.get_type_prefix(new_type)?;
4597
4598 let parts: Vec<&str> = spec_id.split('-').collect();
4601
4602 match self.id_config.format {
4603 IdFormat::SingleLevel => {
4604 if parts.len() >= 2 {
4606 let number = parts.last()?;
4607 Some(format!("{}-{}", new_prefix, number))
4608 } else {
4609 None
4610 }
4611 }
4612 IdFormat::TwoLevel => {
4613 if parts.len() >= 3 {
4615 let feature = parts[0];
4616 let number = parts.last()?;
4617 Some(format!("{}-{}-{}", feature, new_prefix, number))
4618 } else {
4619 None
4620 }
4621 }
4622 }
4623 }
4624
4625 pub fn migrate_to_new_id_format(&mut self) {
4629 self.next_spec_number = 1;
4631 self.prefix_counters.clear();
4632
4633 for req in &mut self.requirements {
4635 req.spec_id = None;
4636 }
4637
4638 let req_data: Vec<(usize, Option<String>, Option<String>, Option<String>)> = self
4640 .requirements
4641 .iter()
4642 .enumerate()
4643 .map(|(i, req)| {
4644 let prefix_override = req.prefix_override.clone();
4646
4647 let feature_prefix = self
4648 .features
4649 .iter()
4650 .find(|f| req.feature.contains(&f.name))
4651 .map(|f| f.prefix.clone());
4652 let type_prefix = match req.req_type {
4653 RequirementType::Functional => Some("FR".to_string()),
4654 RequirementType::NonFunctional => Some("NFR".to_string()),
4655 RequirementType::System => Some("SR".to_string()),
4656 RequirementType::User => Some("UR".to_string()),
4657 RequirementType::ChangeRequest => Some("CR".to_string()),
4658 RequirementType::Bug => Some("BUG".to_string()),
4659 RequirementType::Epic => Some("EPIC".to_string()),
4660 RequirementType::Story => Some("STORY".to_string()),
4661 RequirementType::Task => Some("TASK".to_string()),
4662 RequirementType::Spike => Some("SPIKE".to_string()),
4663 RequirementType::Sprint => Some("SPRINT".to_string()),
4664 RequirementType::Folder => Some("FLD".to_string()),
4665 RequirementType::Meta => Some("META".to_string()),
4666 };
4667 (i, prefix_override, feature_prefix, type_prefix)
4668 })
4669 .collect();
4670
4671 for (i, prefix_override, feature_prefix, type_prefix) in req_data {
4673 let new_id = if let Some(ref override_prefix) = prefix_override {
4674 self.generate_requirement_id_with_override(override_prefix)
4676 } else {
4677 self.generate_requirement_id(feature_prefix.as_deref(), type_prefix.as_deref())
4679 };
4680 self.requirements[i].spec_id = Some(new_id);
4681 }
4682 }
4683
4684 pub fn validate_id_config_change(
4687 &self,
4688 new_format: &IdFormat,
4689 new_numbering: &NumberingStrategy,
4690 new_digits: u8,
4691 ) -> IdConfigValidation {
4692 let mut result = IdConfigValidation {
4693 valid: true,
4694 error: None,
4695 warning: None,
4696 can_migrate: true,
4697 affected_count: 0,
4698 };
4699
4700 let format_changed = &self.id_config.format != new_format;
4702 let numbering_changed = &self.id_config.numbering != new_numbering;
4703 let digits_changed = self.id_config.digits != new_digits;
4704
4705 if !format_changed && !numbering_changed && !digits_changed {
4706 result.can_migrate = false;
4707 return result;
4708 }
4709
4710 let max_digits_in_use = self.get_max_digits_in_use();
4712
4713 if new_digits < max_digits_in_use {
4715 result.valid = false;
4716 result.can_migrate = false;
4717 result.error = Some(format!(
4718 "Cannot reduce digits to {} - existing requirements use up to {} digits",
4719 new_digits, max_digits_in_use
4720 ));
4721 return result;
4722 }
4723
4724 if format_changed {
4726 if self.id_config.numbering != NumberingStrategy::Global
4728 && *new_numbering != NumberingStrategy::Global
4729 {
4730 result.valid = false;
4731 result.can_migrate = false;
4732 result.error = Some(
4733 "Format changes require Global numbering strategy. \
4734 Please switch to Global numbering first."
4735 .to_string(),
4736 );
4737 return result;
4738 }
4739
4740 result.affected_count = self
4742 .requirements
4743 .iter()
4744 .filter(|r| r.spec_id.is_some())
4745 .count();
4746
4747 if result.affected_count > 0 {
4748 result.warning = Some(format!(
4749 "{} requirement(s) will have their IDs updated to the new format.",
4750 result.affected_count
4751 ));
4752 }
4753 } else if numbering_changed || digits_changed {
4754 result.affected_count = self
4756 .requirements
4757 .iter()
4758 .filter(|r| r.spec_id.is_some())
4759 .count();
4760
4761 if digits_changed && result.affected_count > 0 {
4762 result.warning = Some(format!(
4763 "{} requirement(s) will have their ID numbers reformatted.",
4764 result.affected_count
4765 ));
4766 }
4767 }
4768
4769 result
4770 }
4771
4772 pub fn get_max_digits_in_use(&self) -> u8 {
4774 let mut max_digits: u8 = 0;
4775
4776 for req in &self.requirements {
4777 if let Some(spec_id) = &req.spec_id {
4778 let parts: Vec<&str> = spec_id.split('-').collect();
4781 if let Some(last) = parts.last() {
4782 if last.chars().all(|c| c.is_ascii_digit()) {
4784 let digits = last.len() as u8;
4785 if digits > max_digits {
4786 max_digits = digits;
4787 }
4788 }
4789 }
4790 }
4791 }
4792
4793 max_digits
4794 }
4795
4796 pub fn migrate_ids_to_config(
4799 &mut self,
4800 new_format: IdFormat,
4801 new_numbering: NumberingStrategy,
4802 new_digits: u8,
4803 ) -> usize {
4804 self.id_config.format = new_format;
4806 self.id_config.numbering = new_numbering;
4807 self.id_config.digits = new_digits;
4808
4809 self.next_spec_number = 1;
4811 self.prefix_counters.clear();
4812
4813 let req_data: Vec<(usize, Option<String>, Option<String>, Option<String>)> = self
4815 .requirements
4816 .iter()
4817 .enumerate()
4818 .map(|(i, req)| {
4819 let prefix_override = req.prefix_override.clone();
4821
4822 let feature_prefix = self
4823 .features
4824 .iter()
4825 .find(|f| req.feature.contains(&f.name))
4826 .map(|f| f.prefix.clone());
4827 let type_prefix = match req.req_type {
4828 RequirementType::Functional => Some("FR".to_string()),
4829 RequirementType::NonFunctional => Some("NFR".to_string()),
4830 RequirementType::System => Some("SR".to_string()),
4831 RequirementType::User => Some("UR".to_string()),
4832 RequirementType::ChangeRequest => Some("CR".to_string()),
4833 RequirementType::Bug => Some("BUG".to_string()),
4834 RequirementType::Epic => Some("EPIC".to_string()),
4835 RequirementType::Story => Some("STORY".to_string()),
4836 RequirementType::Task => Some("TASK".to_string()),
4837 RequirementType::Spike => Some("SPIKE".to_string()),
4838 RequirementType::Sprint => Some("SPRINT".to_string()),
4839 RequirementType::Folder => Some("FLD".to_string()),
4840 RequirementType::Meta => Some("META".to_string()),
4841 };
4842 (i, prefix_override, feature_prefix, type_prefix)
4843 })
4844 .collect();
4845
4846 let mut migrated_count = 0;
4847
4848 for (i, prefix_override, feature_prefix, type_prefix) in req_data {
4850 let new_id = if let Some(ref override_prefix) = prefix_override {
4851 self.generate_requirement_id_with_override(override_prefix)
4853 } else {
4854 self.generate_requirement_id(feature_prefix.as_deref(), type_prefix.as_deref())
4856 };
4857 self.requirements[i].spec_id = Some(new_id);
4858 migrated_count += 1;
4859 }
4860
4861 migrated_count
4862 }
4863
4864 pub fn add_requirement_type(
4866 &mut self,
4867 name: &str,
4868 prefix: &str,
4869 description: &str,
4870 ) -> anyhow::Result<()> {
4871 let prefix_upper = prefix.to_uppercase();
4872
4873 if self.id_config.get_type_by_prefix(&prefix_upper).is_some() {
4875 anyhow::bail!(
4876 "Prefix '{}' is already used by another requirement type",
4877 prefix_upper
4878 );
4879 }
4880
4881 if self.get_feature_by_prefix(&prefix_upper).is_some() {
4883 anyhow::bail!("Prefix '{}' is already used by a feature", prefix_upper);
4884 }
4885
4886 self.id_config
4887 .requirement_types
4888 .push(RequirementTypeDefinition::new(
4889 name,
4890 &prefix_upper,
4891 description,
4892 ));
4893 Ok(())
4894 }
4895
4896 pub fn add_relationship(
4898 &mut self,
4899 source_id: &Uuid,
4900 rel_type: RelationshipType,
4901 target_id: &Uuid,
4902 bidirectional: bool,
4903 ) -> anyhow::Result<()> {
4904 self.add_relationship_with_creator(source_id, rel_type, target_id, bidirectional, None)
4905 }
4906
4907 pub fn add_relationship_with_creator(
4909 &mut self,
4910 source_id: &Uuid,
4911 rel_type: RelationshipType,
4912 target_id: &Uuid,
4913 bidirectional: bool,
4914 created_by: Option<String>,
4915 ) -> anyhow::Result<()> {
4916 if !self.requirements.iter().any(|r| r.id == *source_id) {
4918 anyhow::bail!("Source requirement not found: {}", source_id);
4919 }
4920 if !self.requirements.iter().any(|r| r.id == *target_id) {
4921 anyhow::bail!("Target requirement not found: {}", target_id);
4922 }
4923
4924 if source_id == target_id {
4926 anyhow::bail!("Cannot create relationship to self");
4927 }
4928
4929 let source_req = self
4931 .get_requirement_by_id_mut(source_id)
4932 .ok_or_else(|| anyhow::anyhow!("Source requirement not found"))?;
4933
4934 if source_req
4936 .relationships
4937 .iter()
4938 .any(|r| r.target_id == *target_id && r.rel_type == rel_type)
4939 {
4940 anyhow::bail!(
4941 "Relationship '{}' to {} already exists",
4942 rel_type,
4943 target_id
4944 );
4945 }
4946
4947 let now = Utc::now();
4948 source_req.relationships.push(Relationship {
4949 rel_type: rel_type.clone(),
4950 target_id: *target_id,
4951 created_at: Some(now),
4952 created_by: created_by.clone(),
4953 });
4954
4955 if bidirectional {
4957 if let Some(inverse_type) = rel_type.inverse() {
4958 let target_req = self
4959 .get_requirement_by_id_mut(target_id)
4960 .ok_or_else(|| anyhow::anyhow!("Target requirement not found"))?;
4961
4962 if !target_req
4964 .relationships
4965 .iter()
4966 .any(|r| r.target_id == *source_id && r.rel_type == inverse_type)
4967 {
4968 target_req.relationships.push(Relationship {
4969 rel_type: inverse_type,
4970 target_id: *source_id,
4971 created_at: Some(now),
4972 created_by: created_by.clone(),
4973 });
4974 }
4975 }
4976 }
4977
4978 Ok(())
4979 }
4980
4981 pub fn set_relationship(
4984 &mut self,
4985 source_id: &Uuid,
4986 rel_type: RelationshipType,
4987 target_id: &Uuid,
4988 bidirectional: bool,
4989 ) -> anyhow::Result<()> {
4990 self.set_relationship_with_creator(source_id, rel_type, target_id, bidirectional, None)
4991 }
4992
4993 pub fn set_relationship_with_creator(
4995 &mut self,
4996 source_id: &Uuid,
4997 rel_type: RelationshipType,
4998 target_id: &Uuid,
4999 bidirectional: bool,
5000 created_by: Option<String>,
5001 ) -> anyhow::Result<()> {
5002 if !self.requirements.iter().any(|r| r.id == *source_id) {
5004 anyhow::bail!("Source requirement not found: {}", source_id);
5005 }
5006 if !self.requirements.iter().any(|r| r.id == *target_id) {
5007 anyhow::bail!("Target requirement not found: {}", target_id);
5008 }
5009
5010 if source_id == target_id {
5012 anyhow::bail!("Cannot create relationship to self");
5013 }
5014
5015 {
5018 let source_req = self
5019 .get_requirement_by_id_mut(source_id)
5020 .ok_or_else(|| anyhow::anyhow!("Source requirement not found"))?;
5021
5022 let old_targets: Vec<Uuid> = source_req
5024 .relationships
5025 .iter()
5026 .filter(|r| r.rel_type == rel_type)
5027 .map(|r| r.target_id)
5028 .collect();
5029
5030 source_req.relationships.retain(|r| r.rel_type != rel_type);
5031
5032 if bidirectional {
5034 if let Some(inverse_type) = rel_type.inverse() {
5035 for old_target in old_targets {
5036 if let Some(old_target_req) = self.get_requirement_by_id_mut(&old_target) {
5037 old_target_req.relationships.retain(|r| {
5038 !(r.target_id == *source_id && r.rel_type == inverse_type)
5039 });
5040 }
5041 }
5042 }
5043 }
5044 }
5045
5046 self.add_relationship_with_creator(
5048 source_id,
5049 rel_type,
5050 target_id,
5051 bidirectional,
5052 created_by,
5053 )
5054 }
5055
5056 pub fn remove_relationship(
5058 &mut self,
5059 source_id: &Uuid,
5060 rel_type: &RelationshipType,
5061 target_id: &Uuid,
5062 bidirectional: bool,
5063 ) -> anyhow::Result<()> {
5064 let source_req = self
5066 .get_requirement_by_id_mut(source_id)
5067 .ok_or_else(|| anyhow::anyhow!("Source requirement not found: {}", source_id))?;
5068
5069 let original_len = source_req.relationships.len();
5070 source_req
5071 .relationships
5072 .retain(|r| !(r.target_id == *target_id && r.rel_type == *rel_type));
5073
5074 if source_req.relationships.len() == original_len {
5075 anyhow::bail!("Relationship '{}' to {} not found", rel_type, target_id);
5076 }
5077
5078 if bidirectional {
5080 if let Some(inverse_type) = rel_type.inverse() {
5081 if let Some(target_req) = self.get_requirement_by_id_mut(target_id) {
5082 target_req
5083 .relationships
5084 .retain(|r| !(r.target_id == *source_id && r.rel_type == inverse_type));
5085 }
5086 }
5087 }
5088
5089 Ok(())
5090 }
5091
5092 pub fn get_relationships(&self, id: &Uuid) -> Vec<(RelationshipType, Uuid)> {
5094 self.get_requirement_by_id(id)
5095 .map(|req| {
5096 req.relationships
5097 .iter()
5098 .map(|r| (r.rel_type.clone(), r.target_id))
5099 .collect()
5100 })
5101 .unwrap_or_default()
5102 }
5103
5104 pub fn get_relationships_by_type(&self, id: &Uuid, rel_type: &RelationshipType) -> Vec<Uuid> {
5106 self.get_requirement_by_id(id)
5107 .map(|req| {
5108 req.relationships
5109 .iter()
5110 .filter(|r| r.rel_type == *rel_type)
5111 .map(|r| r.target_id)
5112 .collect()
5113 })
5114 .unwrap_or_default()
5115 }
5116
5117 pub fn get_relationship_definition(&self, name: &str) -> Option<&RelationshipDefinition> {
5123 let name_lower = name.to_lowercase();
5124 self.relationship_definitions
5125 .iter()
5126 .find(|d| d.name == name_lower)
5127 }
5128
5129 pub fn get_definition_for_type(
5131 &self,
5132 rel_type: &RelationshipType,
5133 ) -> Option<&RelationshipDefinition> {
5134 self.get_relationship_definition(&rel_type.name())
5135 }
5136
5137 pub fn get_relationship_definitions(&self) -> &[RelationshipDefinition] {
5139 &self.relationship_definitions
5140 }
5141
5142 pub fn add_relationship_definition(
5144 &mut self,
5145 definition: RelationshipDefinition,
5146 ) -> anyhow::Result<()> {
5147 let name_lower = definition.name.to_lowercase();
5148
5149 if self
5151 .relationship_definitions
5152 .iter()
5153 .any(|d| d.name == name_lower)
5154 {
5155 anyhow::bail!("Relationship definition '{}' already exists", name_lower);
5156 }
5157
5158 if let Some(ref inverse) = definition.inverse {
5160 let inverse_lower = inverse.to_lowercase();
5161 if !self
5163 .relationship_definitions
5164 .iter()
5165 .any(|d| d.name == inverse_lower)
5166 {
5167 }
5169 }
5170
5171 self.relationship_definitions.push(RelationshipDefinition {
5172 name: name_lower,
5173 ..definition
5174 });
5175 Ok(())
5176 }
5177
5178 pub fn update_relationship_definition(
5180 &mut self,
5181 name: &str,
5182 definition: RelationshipDefinition,
5183 ) -> anyhow::Result<()> {
5184 let name_lower = name.to_lowercase();
5185
5186 let def = self
5187 .relationship_definitions
5188 .iter_mut()
5189 .find(|d| d.name == name_lower)
5190 .ok_or_else(|| anyhow::anyhow!("Relationship definition '{}' not found", name_lower))?;
5191
5192 if def.built_in {
5194 def.display_name = definition.display_name;
5196 def.description = definition.description;
5197 def.color = definition.color;
5198 def.icon = definition.icon;
5199 def.source_types = definition.source_types;
5200 def.target_types = definition.target_types;
5201 } else {
5203 *def = RelationshipDefinition {
5204 name: name_lower,
5205 built_in: false,
5206 ..definition
5207 };
5208 }
5209
5210 Ok(())
5211 }
5212
5213 pub fn remove_relationship_definition(&mut self, name: &str) -> anyhow::Result<()> {
5215 let name_lower = name.to_lowercase();
5216
5217 let def = self
5218 .relationship_definitions
5219 .iter()
5220 .find(|d| d.name == name_lower)
5221 .ok_or_else(|| anyhow::anyhow!("Relationship definition '{}' not found", name_lower))?;
5222
5223 if def.built_in {
5224 anyhow::bail!(
5225 "Cannot remove built-in relationship definition '{}'",
5226 name_lower
5227 );
5228 }
5229
5230 self.relationship_definitions
5231 .retain(|d| d.name != name_lower);
5232 Ok(())
5233 }
5234
5235 pub fn ensure_builtin_relationships(&mut self) {
5237 let defaults = RelationshipDefinition::defaults();
5238 for default_def in defaults {
5239 if !self
5240 .relationship_definitions
5241 .iter()
5242 .any(|d| d.name == default_def.name)
5243 {
5244 self.relationship_definitions.push(default_def);
5245 }
5246 }
5247 }
5248
5249 pub fn validate_relationship(
5251 &self,
5252 source_id: &Uuid,
5253 rel_type: &RelationshipType,
5254 target_id: &Uuid,
5255 ) -> RelationshipValidation {
5256 let mut validation = RelationshipValidation::ok();
5257
5258 if source_id == target_id {
5260 return RelationshipValidation::error("Cannot create relationship to self");
5261 }
5262
5263 let source = match self.get_requirement_by_id(source_id) {
5265 Some(r) => r,
5266 None => return RelationshipValidation::error("Source requirement not found"),
5267 };
5268 let target = match self.get_requirement_by_id(target_id) {
5269 Some(r) => r,
5270 None => return RelationshipValidation::error("Target requirement not found"),
5271 };
5272
5273 if source
5275 .relationships
5276 .iter()
5277 .any(|r| r.target_id == *target_id && r.rel_type == *rel_type)
5278 {
5279 return RelationshipValidation::error(&format!(
5280 "Relationship '{}' to {} already exists",
5281 rel_type, target_id
5282 ));
5283 }
5284
5285 let definition = match self.get_definition_for_type(rel_type) {
5287 Some(d) => d,
5288 None => {
5289 validation.add_warning(&format!(
5291 "No definition found for relationship type '{}'. Consider creating one.",
5292 rel_type.name()
5293 ));
5294 return validation;
5295 }
5296 };
5297
5298 if !definition.allows_source_type(&source.req_type) {
5300 validation.add_error(&format!(
5301 "Source requirement type '{}' is not allowed for '{}' relationships. Allowed: {:?}",
5302 source.req_type, definition.display_name, definition.source_types
5303 ));
5304 }
5305
5306 if !definition.allows_target_type(&target.req_type) {
5308 validation.add_error(&format!(
5309 "Target requirement type '{}' is not allowed for '{}' relationships. Allowed: {:?}",
5310 target.req_type, definition.display_name, definition.target_types
5311 ));
5312 }
5313
5314 match definition.cardinality {
5316 Cardinality::OneToOne => {
5317 let existing_outgoing = source
5319 .relationships
5320 .iter()
5321 .filter(|r| r.rel_type == *rel_type)
5322 .count();
5323 if existing_outgoing > 0 {
5324 validation.add_warning(&format!(
5325 "Source already has a '{}' relationship (cardinality is 1:1)",
5326 definition.display_name
5327 ));
5328 }
5329 let existing_incoming = self
5331 .requirements
5332 .iter()
5333 .filter(|r| r.id != *source_id)
5334 .flat_map(|r| r.relationships.iter())
5335 .filter(|r| r.target_id == *target_id && r.rel_type == *rel_type)
5336 .count();
5337 if existing_incoming > 0 {
5338 validation.add_warning(&format!(
5339 "Target already has an incoming '{}' relationship (cardinality is 1:1)",
5340 definition.display_name
5341 ));
5342 }
5343 }
5344 Cardinality::ManyToOne => {
5345 let existing_outgoing = source
5347 .relationships
5348 .iter()
5349 .filter(|r| r.rel_type == *rel_type)
5350 .count();
5351 if existing_outgoing > 0 {
5352 validation.add_warning(&format!(
5353 "Source already has a '{}' relationship (cardinality is N:1, only one allowed per source)",
5354 definition.display_name
5355 ));
5356 }
5357 }
5358 Cardinality::OneToMany => {
5359 let existing_incoming = self
5361 .requirements
5362 .iter()
5363 .filter(|r| r.id != *source_id)
5364 .flat_map(|r| r.relationships.iter())
5365 .filter(|r| r.target_id == *target_id && r.rel_type == *rel_type)
5366 .count();
5367 if existing_incoming > 0 {
5368 validation.add_warning(&format!(
5369 "Target already has an incoming '{}' relationship (cardinality is 1:N)",
5370 definition.display_name
5371 ));
5372 }
5373 }
5374 Cardinality::ManyToMany => {
5375 }
5377 }
5378
5379 if rel_type.name() == "parent" || rel_type.name() == "child" {
5381 if self.would_create_cycle(source_id, target_id, rel_type) {
5382 validation.add_error("This relationship would create a cycle in the hierarchy");
5383 }
5384 }
5385
5386 validation
5387 }
5388
5389 fn would_create_cycle(
5391 &self,
5392 source_id: &Uuid,
5393 target_id: &Uuid,
5394 rel_type: &RelationshipType,
5395 ) -> bool {
5396 let check_type = if rel_type.name() == "parent" {
5399 RelationshipType::Parent
5400 } else if rel_type.name() == "child" {
5401 RelationshipType::Child
5402 } else {
5403 return false;
5404 };
5405
5406 let mut visited = std::collections::HashSet::new();
5407 let mut stack = vec![*target_id];
5408
5409 while let Some(current) = stack.pop() {
5410 if current == *source_id {
5411 return true; }
5413 if visited.contains(¤t) {
5414 continue;
5415 }
5416 visited.insert(current);
5417
5418 if let Some(req) = self.get_requirement_by_id(¤t) {
5420 for rel in &req.relationships {
5421 if rel.rel_type == check_type {
5422 stack.push(rel.target_id);
5423 }
5424 }
5425 }
5426 }
5427
5428 false
5429 }
5430
5431 pub fn get_inverse_type(&self, rel_type: &RelationshipType) -> Option<RelationshipType> {
5433 if let Some(inverse) = rel_type.inverse() {
5435 return Some(inverse);
5436 }
5437
5438 if let Some(def) = self.get_definition_for_type(rel_type) {
5440 if let Some(ref inverse_name) = def.inverse {
5441 return Some(RelationshipType::from_str(inverse_name));
5442 }
5443 if def.symmetric {
5444 return Some(rel_type.clone());
5445 }
5446 }
5447
5448 None
5449 }
5450
5451 pub fn create_baseline(
5457 &mut self,
5458 name: String,
5459 description: Option<String>,
5460 created_by: String,
5461 ) -> &Baseline {
5462 let baseline = Baseline::new(name, description, created_by, &self.requirements);
5463 self.baselines.push(baseline);
5464 self.baselines.last().unwrap()
5465 }
5466
5467 pub fn get_baseline(&self, id: &Uuid) -> Option<&Baseline> {
5469 self.baselines.iter().find(|b| &b.id == id)
5470 }
5471
5472 pub fn get_baseline_by_name(&self, name: &str) -> Option<&Baseline> {
5474 self.baselines.iter().find(|b| b.name == name)
5475 }
5476
5477 pub fn delete_baseline(&mut self, id: &Uuid) -> bool {
5479 if let Some(idx) = self.baselines.iter().position(|b| &b.id == id) {
5480 if !self.baselines[idx].locked {
5481 self.baselines.remove(idx);
5482 return true;
5483 }
5484 }
5485 false
5486 }
5487
5488 pub fn compare_with_baseline(&self, baseline_id: &Uuid) -> Option<BaselineComparison> {
5490 let baseline = self.get_baseline(baseline_id)?;
5491 Some(self.compare_snapshots_to_current(&baseline.requirements))
5492 }
5493
5494 pub fn compare_baselines(
5496 &self,
5497 source_id: &Uuid,
5498 target_id: &Uuid,
5499 ) -> Option<BaselineComparison> {
5500 let source = self.get_baseline(source_id)?;
5501 let target = self.get_baseline(target_id)?;
5502 Some(Self::compare_snapshot_sets(
5503 &source.requirements,
5504 &target.requirements,
5505 ))
5506 }
5507
5508 fn compare_snapshots_to_current(
5510 &self,
5511 snapshots: &[RequirementSnapshot],
5512 ) -> BaselineComparison {
5513 use std::collections::HashMap;
5514
5515 let snapshot_map: HashMap<Uuid, &RequirementSnapshot> =
5516 snapshots.iter().map(|s| (s.original_id, s)).collect();
5517
5518 let current_map: HashMap<Uuid, &Requirement> = self
5519 .requirements
5520 .iter()
5521 .filter(|r| !r.archived)
5522 .map(|r| (r.id, r))
5523 .collect();
5524
5525 let mut comparison = BaselineComparison::default();
5526
5527 for id in current_map.keys() {
5529 if !snapshot_map.contains_key(id) {
5530 comparison.added.push(*id);
5531 }
5532 }
5533
5534 for id in snapshot_map.keys() {
5536 if !current_map.contains_key(id) {
5537 comparison.removed.push(*id);
5538 }
5539 }
5540
5541 for (id, snapshot) in &snapshot_map {
5543 if let Some(current) = current_map.get(id) {
5544 let changes = Self::diff_snapshot_to_requirement(snapshot, current);
5545 if changes.is_empty() {
5546 comparison.unchanged.push(*id);
5547 } else {
5548 comparison.modified.push(BaselineRequirementDiff {
5549 id: *id,
5550 spec_id: current.spec_id.clone(),
5551 changes,
5552 });
5553 }
5554 }
5555 }
5556
5557 comparison
5558 }
5559
5560 fn compare_snapshot_sets(
5562 source: &[RequirementSnapshot],
5563 target: &[RequirementSnapshot],
5564 ) -> BaselineComparison {
5565 use std::collections::HashMap;
5566
5567 let source_map: HashMap<Uuid, &RequirementSnapshot> =
5568 source.iter().map(|s| (s.original_id, s)).collect();
5569
5570 let target_map: HashMap<Uuid, &RequirementSnapshot> =
5571 target.iter().map(|s| (s.original_id, s)).collect();
5572
5573 let mut comparison = BaselineComparison::default();
5574
5575 for id in target_map.keys() {
5577 if !source_map.contains_key(id) {
5578 comparison.added.push(*id);
5579 }
5580 }
5581
5582 for id in source_map.keys() {
5584 if !target_map.contains_key(id) {
5585 comparison.removed.push(*id);
5586 }
5587 }
5588
5589 for (id, source_snap) in &source_map {
5591 if let Some(target_snap) = target_map.get(id) {
5592 let changes = Self::diff_snapshots(source_snap, target_snap);
5593 if changes.is_empty() {
5594 comparison.unchanged.push(*id);
5595 } else {
5596 comparison.modified.push(BaselineRequirementDiff {
5597 id: *id,
5598 spec_id: target_snap.spec_id.clone(),
5599 changes,
5600 });
5601 }
5602 }
5603 }
5604
5605 comparison
5606 }
5607
5608 fn diff_snapshot_to_requirement(
5610 snapshot: &RequirementSnapshot,
5611 current: &Requirement,
5612 ) -> Vec<FieldChange> {
5613 let mut changes = Vec::new();
5614
5615 if snapshot.title != current.title {
5616 changes.push(FieldChange {
5617 field_name: "title".to_string(),
5618 old_value: snapshot.title.clone(),
5619 new_value: current.title.clone(),
5620 });
5621 }
5622 if snapshot.description != current.description {
5623 changes.push(FieldChange {
5624 field_name: "description".to_string(),
5625 old_value: snapshot.description.clone(),
5626 new_value: current.description.clone(),
5627 });
5628 }
5629 if snapshot.status != current.status {
5630 changes.push(FieldChange {
5631 field_name: "status".to_string(),
5632 old_value: snapshot.status.to_string(),
5633 new_value: current.status.to_string(),
5634 });
5635 }
5636 if snapshot.priority != current.priority {
5637 changes.push(FieldChange {
5638 field_name: "priority".to_string(),
5639 old_value: snapshot.priority.to_string(),
5640 new_value: current.priority.to_string(),
5641 });
5642 }
5643 if snapshot.owner != current.owner {
5644 changes.push(FieldChange {
5645 field_name: "owner".to_string(),
5646 old_value: snapshot.owner.clone(),
5647 new_value: current.owner.clone(),
5648 });
5649 }
5650 if snapshot.feature != current.feature {
5651 changes.push(FieldChange {
5652 field_name: "feature".to_string(),
5653 old_value: snapshot.feature.clone(),
5654 new_value: current.feature.clone(),
5655 });
5656 }
5657 if snapshot.req_type != current.req_type {
5658 changes.push(FieldChange {
5659 field_name: "type".to_string(),
5660 old_value: format!("{:?}", snapshot.req_type),
5661 new_value: format!("{:?}", current.req_type),
5662 });
5663 }
5664
5665 changes
5666 }
5667
5668 fn diff_snapshots(
5670 source: &RequirementSnapshot,
5671 target: &RequirementSnapshot,
5672 ) -> Vec<FieldChange> {
5673 let mut changes = Vec::new();
5674
5675 if source.title != target.title {
5676 changes.push(FieldChange {
5677 field_name: "title".to_string(),
5678 old_value: source.title.clone(),
5679 new_value: target.title.clone(),
5680 });
5681 }
5682 if source.description != target.description {
5683 changes.push(FieldChange {
5684 field_name: "description".to_string(),
5685 old_value: source.description.clone(),
5686 new_value: target.description.clone(),
5687 });
5688 }
5689 if source.status != target.status {
5690 changes.push(FieldChange {
5691 field_name: "status".to_string(),
5692 old_value: source.status.to_string(),
5693 new_value: target.status.to_string(),
5694 });
5695 }
5696 if source.priority != target.priority {
5697 changes.push(FieldChange {
5698 field_name: "priority".to_string(),
5699 old_value: source.priority.to_string(),
5700 new_value: target.priority.to_string(),
5701 });
5702 }
5703 if source.owner != target.owner {
5704 changes.push(FieldChange {
5705 field_name: "owner".to_string(),
5706 old_value: source.owner.clone(),
5707 new_value: target.owner.clone(),
5708 });
5709 }
5710 if source.feature != target.feature {
5711 changes.push(FieldChange {
5712 field_name: "feature".to_string(),
5713 old_value: source.feature.clone(),
5714 new_value: target.feature.clone(),
5715 });
5716 }
5717 if source.req_type != target.req_type {
5718 changes.push(FieldChange {
5719 field_name: "type".to_string(),
5720 old_value: format!("{:?}", source.req_type),
5721 new_value: format!("{:?}", target.req_type),
5722 });
5723 }
5724
5725 changes
5726 }
5727}
5728
5729impl Default for RequirementsStore {
5730 fn default() -> Self {
5731 Self::new()
5732 }
5733}
5734
5735#[cfg(test)]
5736mod tests {
5737 use super::*;
5738
5739 #[test]
5740 fn test_add_requirement_with_spec_id() {
5741 let mut store = RequirementsStore::new();
5742 let req = Requirement::new("Test".into(), "Description".into());
5743
5744 assert_eq!(store.next_spec_number, 1);
5745 assert!(req.spec_id.is_none());
5746
5747 store.add_requirement_with_spec_id(req);
5748
5749 assert_eq!(store.requirements.len(), 1);
5750 assert_eq!(store.requirements[0].spec_id, Some("SPEC-001".into()));
5751 assert_eq!(store.next_spec_number, 2);
5752 }
5753
5754 #[test]
5755 fn test_get_requirement_by_spec_id() {
5756 let mut store = RequirementsStore::new();
5757 let req = Requirement::new("Test".into(), "Description".into());
5758 store.add_requirement_with_spec_id(req);
5759
5760 let found = store.get_requirement_by_spec_id("SPEC-001");
5761 assert!(found.is_some());
5762 assert_eq!(found.unwrap().title, "Test");
5763
5764 let not_found = store.get_requirement_by_spec_id("SPEC-999");
5765 assert!(not_found.is_none());
5766 }
5767
5768 #[test]
5769 fn test_assign_spec_ids() {
5770 let mut store = RequirementsStore::new();
5771
5772 let mut req1 = Requirement::new("R1".into(), "D1".into());
5773 let mut req2 = Requirement::new("R2".into(), "D2".into());
5774
5775 store.requirements.push(req1);
5777 store.requirements.push(req2);
5778
5779 assert!(store.requirements[0].spec_id.is_none());
5780 assert!(store.requirements[1].spec_id.is_none());
5781
5782 store.assign_spec_ids();
5783
5784 assert_eq!(store.requirements[0].spec_id, Some("SPEC-001".into()));
5785 assert_eq!(store.requirements[1].spec_id, Some("SPEC-002".into()));
5786 assert_eq!(store.next_spec_number, 3);
5787 }
5788
5789 #[test]
5790 fn test_assign_spec_ids_skips_existing() {
5791 let mut store = RequirementsStore::new();
5792
5793 let mut req1 = Requirement::new("R1".into(), "D1".into());
5794 req1.spec_id = Some("SPEC-001".into());
5795 let mut req2 = Requirement::new("R2".into(), "D2".into());
5796
5797 store.requirements.push(req1);
5798 store.requirements.push(req2);
5799 store.next_spec_number = 2; store.assign_spec_ids();
5802
5803 assert_eq!(store.requirements[0].spec_id, Some("SPEC-001".into()));
5804 assert_eq!(store.requirements[1].spec_id, Some("SPEC-002".into()));
5805 assert_eq!(store.next_spec_number, 3);
5806 }
5807
5808 #[test]
5809 fn test_validate_unique_spec_ids_success() {
5810 let mut store = RequirementsStore::new();
5811 let req1 = Requirement::new("R1".into(), "D1".into());
5812 let req2 = Requirement::new("R2".into(), "D2".into());
5813
5814 store.add_requirement_with_spec_id(req1);
5815 store.add_requirement_with_spec_id(req2);
5816
5817 assert!(store.validate_unique_spec_ids().is_ok());
5818 }
5819
5820 #[test]
5821 fn test_validate_unique_spec_ids_duplicate() {
5822 let mut store = RequirementsStore::new();
5823
5824 let mut req1 = Requirement::new("R1".into(), "D1".into());
5825 req1.spec_id = Some("SPEC-001".into());
5826 let mut req2 = Requirement::new("R2".into(), "D2".into());
5827 req2.spec_id = Some("SPEC-001".into()); store.requirements.push(req1);
5830 store.requirements.push(req2);
5831
5832 let result = store.validate_unique_spec_ids();
5833 assert!(result.is_err());
5834 assert!(result
5835 .unwrap_err()
5836 .to_string()
5837 .contains("Duplicate SPEC-ID"));
5838 }
5839
5840 #[test]
5841 fn test_repair_duplicate_spec_ids() {
5842 let mut store = RequirementsStore::new();
5843
5844 let mut req1 = Requirement::new("R1".into(), "D1".into());
5845 req1.spec_id = Some("FR-0001".into());
5846 let mut req2 = Requirement::new("R2".into(), "D2".into());
5847 req2.spec_id = Some("FR-0001".into()); let mut req3 = Requirement::new("R3".into(), "D3".into());
5849 req3.spec_id = Some("FR-0002".into()); store.requirements.push(req1);
5852 store.requirements.push(req2);
5853 store.requirements.push(req3);
5854
5855 assert!(store.validate_unique_spec_ids().is_err());
5857
5858 let repaired = store.repair_duplicate_spec_ids();
5860 assert_eq!(repaired, 1);
5861
5862 assert!(store.validate_unique_spec_ids().is_ok());
5864
5865 assert_eq!(store.requirements[0].spec_id.as_deref(), Some("FR-0001"));
5867
5868 let new_id = store.requirements[1].spec_id.as_deref().unwrap();
5870 assert!(new_id.starts_with("FR-"));
5871 assert_ne!(new_id, "FR-0001");
5872
5873 assert_eq!(store.requirements[2].spec_id.as_deref(), Some("FR-0002"));
5875 }
5876
5877 #[test]
5878 fn test_extract_prefix_from_spec_id() {
5879 assert_eq!(
5880 RequirementsStore::extract_prefix_from_spec_id("FR-0042"),
5881 "FR"
5882 );
5883 assert_eq!(
5884 RequirementsStore::extract_prefix_from_spec_id("AUTH-REQ-001"),
5885 "AUTH-REQ"
5886 );
5887 assert_eq!(
5888 RequirementsStore::extract_prefix_from_spec_id("SPEC-123"),
5889 "SPEC"
5890 );
5891 assert_eq!(
5892 RequirementsStore::extract_prefix_from_spec_id("IMPL-0001"),
5893 "IMPL"
5894 );
5895 assert_eq!(RequirementsStore::extract_prefix_from_spec_id(""), "REQ");
5897 assert_eq!(
5898 RequirementsStore::extract_prefix_from_spec_id("no-numbers"),
5899 "no-numbers"
5900 );
5901 }
5902
5903 #[test]
5904 fn test_peek_next_spec_id() {
5905 let store = RequirementsStore::new();
5906 assert_eq!(store.peek_next_spec_id(), "SPEC-001");
5907
5908 let mut store2 = RequirementsStore::new();
5909 store2.next_spec_number = 42;
5910 assert_eq!(store2.peek_next_spec_id(), "SPEC-042");
5911 }
5912
5913 #[test]
5914 fn test_add_relationship() {
5915 let mut store = RequirementsStore::new();
5916 let req1 = Requirement::new("Req1".into(), "Description 1".into());
5917 let req2 = Requirement::new("Req2".into(), "Description 2".into());
5918
5919 let id1 = req1.id;
5920 let id2 = req2.id;
5921
5922 store.add_requirement_with_spec_id(req1);
5923 store.add_requirement_with_spec_id(req2);
5924
5925 let result = store.add_relationship(&id1, RelationshipType::Parent, &id2, false);
5927 assert!(result.is_ok());
5928
5929 let req1_updated = store.get_requirement_by_id(&id1).unwrap();
5931 assert_eq!(req1_updated.relationships.len(), 1);
5932 assert_eq!(
5933 req1_updated.relationships[0].rel_type,
5934 RelationshipType::Parent
5935 );
5936 assert_eq!(req1_updated.relationships[0].target_id, id2);
5937 }
5938
5939 #[test]
5940 fn test_add_relationship_bidirectional() {
5941 let mut store = RequirementsStore::new();
5942 let req1 = Requirement::new("Req1".into(), "Description 1".into());
5943 let req2 = Requirement::new("Req2".into(), "Description 2".into());
5944
5945 let id1 = req1.id;
5946 let id2 = req2.id;
5947
5948 store.add_requirement_with_spec_id(req1);
5949 store.add_requirement_with_spec_id(req2);
5950
5951 let result = store.add_relationship(&id1, RelationshipType::Parent, &id2, true);
5953 assert!(result.is_ok());
5954
5955 let req1_updated = store.get_requirement_by_id(&id1).unwrap();
5957 assert_eq!(req1_updated.relationships.len(), 1);
5958 assert_eq!(
5959 req1_updated.relationships[0].rel_type,
5960 RelationshipType::Parent
5961 );
5962
5963 let req2_updated = store.get_requirement_by_id(&id2).unwrap();
5965 assert_eq!(req2_updated.relationships.len(), 1);
5966 assert_eq!(
5967 req2_updated.relationships[0].rel_type,
5968 RelationshipType::Child
5969 );
5970 assert_eq!(req2_updated.relationships[0].target_id, id1);
5971 }
5972
5973 #[test]
5974 fn test_add_relationship_self_error() {
5975 let mut store = RequirementsStore::new();
5976 let req = Requirement::new("Req".into(), "Description".into());
5977 let id = req.id;
5978
5979 store.add_requirement_with_spec_id(req);
5980
5981 let result = store.add_relationship(&id, RelationshipType::Parent, &id, false);
5983 assert!(result.is_err());
5984 assert!(result
5985 .unwrap_err()
5986 .to_string()
5987 .contains("Cannot create relationship to self"));
5988 }
5989
5990 #[test]
5991 fn test_add_relationship_duplicate_error() {
5992 let mut store = RequirementsStore::new();
5993 let req1 = Requirement::new("Req1".into(), "Description 1".into());
5994 let req2 = Requirement::new("Req2".into(), "Description 2".into());
5995
5996 let id1 = req1.id;
5997 let id2 = req2.id;
5998
5999 store.add_requirement_with_spec_id(req1);
6000 store.add_requirement_with_spec_id(req2);
6001
6002 store
6004 .add_relationship(&id1, RelationshipType::Parent, &id2, false)
6005 .unwrap();
6006
6007 let result = store.add_relationship(&id1, RelationshipType::Parent, &id2, false);
6009 assert!(result.is_err());
6010 assert!(result.unwrap_err().to_string().contains("already exists"));
6011 }
6012
6013 #[test]
6014 fn test_remove_relationship() {
6015 let mut store = RequirementsStore::new();
6016 let req1 = Requirement::new("Req1".into(), "Description 1".into());
6017 let req2 = Requirement::new("Req2".into(), "Description 2".into());
6018
6019 let id1 = req1.id;
6020 let id2 = req2.id;
6021
6022 store.add_requirement_with_spec_id(req1);
6023 store.add_requirement_with_spec_id(req2);
6024 store
6025 .add_relationship(&id1, RelationshipType::Parent, &id2, false)
6026 .unwrap();
6027
6028 let result = store.remove_relationship(&id1, &RelationshipType::Parent, &id2, false);
6030 assert!(result.is_ok());
6031
6032 let req1_updated = store.get_requirement_by_id(&id1).unwrap();
6034 assert_eq!(req1_updated.relationships.len(), 0);
6035 }
6036
6037 #[test]
6038 fn test_remove_relationship_bidirectional() {
6039 let mut store = RequirementsStore::new();
6040 let req1 = Requirement::new("Req1".into(), "Description 1".into());
6041 let req2 = Requirement::new("Req2".into(), "Description 2".into());
6042
6043 let id1 = req1.id;
6044 let id2 = req2.id;
6045
6046 store.add_requirement_with_spec_id(req1);
6047 store.add_requirement_with_spec_id(req2);
6048 store
6049 .add_relationship(&id1, RelationshipType::Parent, &id2, true)
6050 .unwrap();
6051
6052 let result = store.remove_relationship(&id1, &RelationshipType::Parent, &id2, true);
6054 assert!(result.is_ok());
6055
6056 let req1_updated = store.get_requirement_by_id(&id1).unwrap();
6058 assert_eq!(req1_updated.relationships.len(), 0);
6059
6060 let req2_updated = store.get_requirement_by_id(&id2).unwrap();
6061 assert_eq!(req2_updated.relationships.len(), 0);
6062 }
6063
6064 #[test]
6065 fn test_relationship_type_from_str() {
6066 assert_eq!(
6067 RelationshipType::from_str("parent"),
6068 RelationshipType::Parent
6069 );
6070 assert_eq!(RelationshipType::from_str("child"), RelationshipType::Child);
6071 assert_eq!(
6072 RelationshipType::from_str("duplicate"),
6073 RelationshipType::Duplicate
6074 );
6075 assert_eq!(
6076 RelationshipType::from_str("verifies"),
6077 RelationshipType::Verifies
6078 );
6079 assert_eq!(
6080 RelationshipType::from_str("verified-by"),
6081 RelationshipType::VerifiedBy
6082 );
6083 assert_eq!(
6084 RelationshipType::from_str("references"),
6085 RelationshipType::References
6086 );
6087
6088 if let RelationshipType::Custom(name) = RelationshipType::from_str("implements") {
6090 assert_eq!(name, "implements");
6091 } else {
6092 panic!("Expected Custom variant");
6093 }
6094 }
6095
6096 #[test]
6097 fn test_relationship_type_inverse() {
6098 assert_eq!(
6099 RelationshipType::Parent.inverse(),
6100 Some(RelationshipType::Child)
6101 );
6102 assert_eq!(
6103 RelationshipType::Child.inverse(),
6104 Some(RelationshipType::Parent)
6105 );
6106 assert_eq!(
6107 RelationshipType::Verifies.inverse(),
6108 Some(RelationshipType::VerifiedBy)
6109 );
6110 assert_eq!(
6111 RelationshipType::VerifiedBy.inverse(),
6112 Some(RelationshipType::Verifies)
6113 );
6114 assert_eq!(
6115 RelationshipType::Duplicate.inverse(),
6116 Some(RelationshipType::Duplicate)
6117 );
6118 assert_eq!(RelationshipType::References.inverse(), None);
6119 assert_eq!(RelationshipType::Custom("test".to_string()).inverse(), None);
6120 }
6121}
6122
6123#[derive(Debug, Clone, Serialize, Deserialize)]
6130pub struct QueueEntry {
6131 pub user_id: String,
6133 pub requirement_id: Uuid,
6135 pub position: i64,
6137 pub added_by: String,
6139 #[serde(skip_serializing_if = "Option::is_none")]
6141 pub note: Option<String>,
6142 pub added_at: DateTime<Utc>,
6144}