Skip to main content

aida_core/
models.rs

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/// Represents the status of a requirement
11#[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/// Represents the priority of a requirement
35#[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/// Represents the type of a requirement
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
54pub enum RequirementType {
55    // Traditional requirements types
56    Functional,
57    NonFunctional,
58    System,
59    User,
60    ChangeRequest,
61    Bug,
62    // Agile types
63    Epic,
64    Story,
65    Task,
66    Spike,
67    Sprint, // Time-boxed iteration for work planning
68    // Organizational types (stateless)
69    Folder,
70    // Meta type for database configuration (prompts, skills, etc.)
71    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/// Represents the subtype for Meta requirements
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
96pub enum MetaSubtype {
97    /// AI prompts (evaluate, improve, etc.)
98    Prompt,
99    /// Skill definitions
100    Skill,
101    /// Slash commands
102    Command,
103    /// Other templates
104    Template,
105    /// Database configuration
106    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    /// Parse a meta subtype from a string
123    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    /// Get all meta subtypes
135    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/// Represents a relationship type between requirements
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
148pub enum RelationshipType {
149    /// Parent-child relationship (this is parent of target)
150    Parent,
151    /// Child-parent relationship (this is child of target)
152    Child,
153    /// Duplicate relationship
154    Duplicate,
155    /// Verification relationship (this verifies target)
156    Verifies,
157    /// Verified-by relationship (this is verified by target)
158    VerifiedBy,
159    /// General reference relationship
160    References,
161    /// Custom relationship type with user-defined name
162    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    /// Parse a relationship type from a string
181    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    /// Get the inverse relationship type (if applicable)
194    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    /// Get the canonical name for this relationship type
207    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// ============================================================================
221// Custom Type Definition System
222// ============================================================================
223
224/// Field type for custom fields
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
226#[serde(rename_all = "lowercase")]
227pub enum CustomFieldType {
228    /// Single-line text input
229    Text,
230    /// Multi-line text input
231    TextArea,
232    /// Selection from predefined options
233    Select,
234    /// Boolean checkbox
235    Boolean,
236    /// Date value
237    Date,
238    /// Reference to a user ($USER-XXX)
239    User,
240    /// Reference to another requirement (SPEC-XXX)
241    Requirement,
242    /// Numeric value
243    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/// Definition of a custom field for a requirement type
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
269pub struct CustomFieldDefinition {
270    /// Field name (used as key in custom_fields map)
271    pub name: String,
272
273    /// Display label for the field
274    pub label: String,
275
276    /// Field type
277    #[serde(default)]
278    pub field_type: CustomFieldType,
279
280    /// Whether this field is required
281    #[serde(default)]
282    pub required: bool,
283
284    /// Options for Select field type
285    #[serde(default, skip_serializing_if = "Vec::is_empty")]
286    pub options: Vec<String>,
287
288    /// Default value (as string, converted based on field_type)
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub default_value: Option<String>,
291
292    /// Help text / description
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub description: Option<String>,
295
296    /// Display order (lower = first)
297    #[serde(default)]
298    pub order: i32,
299}
300
301impl CustomFieldDefinition {
302    /// Creates a new text field definition
303    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    /// Creates a new select field definition
317    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    /// Creates a new user reference field definition
331    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    /// Creates a new text area (multiline text) field definition
345    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    /// Creates a new number field definition
359    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    /// Sets the field as required
373    pub fn required(mut self) -> Self {
374        self.required = true;
375        self
376    }
377
378    /// Sets the description
379    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
380        self.description = Some(desc.into());
381        self
382    }
383
384    /// Sets the display order
385    pub fn with_order(mut self, order: i32) -> Self {
386        self.order = order;
387        self
388    }
389
390    /// Sets a default value
391    pub fn with_default(mut self, value: impl Into<String>) -> Self {
392        self.default_value = Some(value.into());
393        self
394    }
395}
396
397/// Definition of a custom requirement type with its specific statuses and fields
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
399pub struct CustomTypeDefinition {
400    /// Internal name/key for the type (e.g., "ChangeRequest")
401    pub name: String,
402
403    /// Display label (e.g., "Change Request")
404    pub display_name: String,
405
406    /// Description of this type
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub description: Option<String>,
409
410    /// Preferred ID prefix for this type (e.g., "CR")
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub prefix: Option<String>,
413
414    /// Custom statuses for this type (if empty, uses default statuses)
415    #[serde(default, skip_serializing_if = "Vec::is_empty")]
416    pub statuses: Vec<String>,
417
418    /// Custom priorities for this type (if empty, uses default priorities)
419    #[serde(default, skip_serializing_if = "Vec::is_empty")]
420    pub priorities: Vec<String>,
421
422    /// Additional custom fields for this type
423    #[serde(default, skip_serializing_if = "Vec::is_empty")]
424    pub custom_fields: Vec<CustomFieldDefinition>,
425
426    /// Whether this is a built-in type (cannot be deleted)
427    #[serde(default)]
428    pub built_in: bool,
429
430    /// Color for visual distinction (hex color code)
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub color: Option<String>,
433
434    /// Whether this type is stateless (no status/priority tracking)
435    /// Stateless types are used for organizational purposes (e.g., Folders)
436    /// They are excluded from status metrics and reports by default
437    #[serde(default)]
438    pub stateless: bool,
439}
440
441impl CustomTypeDefinition {
442    /// Creates a new custom type definition
443    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    /// Creates a built-in type definition
459    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    /// Creates a built-in stateless type definition (no status/priority tracking)
475    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    /// Sets the prefix
491    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
492        self.prefix = Some(prefix.into());
493        self
494    }
495
496    /// Sets custom statuses
497    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    /// Sets custom priorities
503    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    /// Adds a custom field
509    pub fn with_field(mut self, field: CustomFieldDefinition) -> Self {
510        self.custom_fields.push(field);
511        self
512    }
513
514    /// Sets the description
515    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
516        self.description = Some(desc.into());
517        self
518    }
519
520    /// Sets the color
521    pub fn with_color(mut self, color: impl Into<String>) -> Self {
522        self.color = Some(color.into());
523        self
524    }
525
526    /// Marks this type as stateless (no status/priority tracking)
527    pub fn as_stateless(mut self) -> Self {
528        self.stateless = true;
529        self
530    }
531
532    /// Gets the statuses for this type, falling back to defaults if none specified
533    pub fn get_statuses(&self) -> Vec<String> {
534        if self.statuses.is_empty() {
535            // Default statuses
536            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    /// Gets the priorities for this type, falling back to defaults if none specified
548    pub fn get_priorities(&self) -> Vec<String> {
549        if self.priorities.is_empty() {
550            // Default priorities
551            vec!["High".to_string(), "Medium".to_string(), "Low".to_string()]
552        } else {
553            self.priorities.clone()
554        }
555    }
556}
557
558/// Returns the default type definitions
559pub 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        // Bug tracking
619        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        // Agile types
662        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        // trace:FR-0309 | ai:claude:high
683        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        // Sprint type for time-boxed iterations
745        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        // Stateless organizational types
776        CustomTypeDefinition::built_in_stateless("Folder", "Folder")
777            .with_prefix("FLD")
778            .with_description("Organizational container for grouping related requirements")
779            .with_color("#6b7280"),
780        // Meta type for database configuration
781        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// ============================================================================
789// Relationship Definition System
790// ============================================================================
791
792/// Cardinality constraints for relationships
793#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
794pub enum Cardinality {
795    /// One source to one target (1:1)
796    OneToOne,
797    /// One source to many targets (1:N)
798    OneToMany,
799    /// Many sources to one target (N:1)
800    ManyToOne,
801    /// Many sources to many targets (N:N) - default
802    #[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    /// Parse cardinality from string
819    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/// Defines a relationship type and its constraints
830#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
831pub struct RelationshipDefinition {
832    /// Unique identifier for this relationship type (lowercase, no spaces)
833    pub name: String,
834
835    /// Human-readable display name
836    pub display_name: String,
837
838    /// Description of what this relationship means
839    #[serde(default)]
840    pub description: String,
841
842    /// The inverse relationship name (if any)
843    /// e.g., "parent" has inverse "child"
844    #[serde(default)]
845    pub inverse: Option<String>,
846
847    /// Whether this relationship is symmetric (A->B implies B->A with same type)
848    /// e.g., "duplicate" is symmetric
849    #[serde(default)]
850    pub symmetric: bool,
851
852    /// Cardinality constraints
853    #[serde(default)]
854    pub cardinality: Cardinality,
855
856    /// Source type constraints (which requirement types can be the source)
857    /// Empty means all types allowed
858    #[serde(default)]
859    pub source_types: Vec<String>,
860
861    /// Target type constraints (which requirement types can be the target)
862    /// Empty means all types allowed
863    #[serde(default)]
864    pub target_types: Vec<String>,
865
866    /// Whether this is a built-in relationship (cannot be deleted)
867    #[serde(default)]
868    pub built_in: bool,
869
870    /// Color for visualization (optional, hex format e.g., "#ff6b6b")
871    #[serde(default)]
872    pub color: Option<String>,
873
874    /// Icon/symbol for the relationship (optional)
875    #[serde(default)]
876    pub icon: Option<String>,
877}
878
879impl RelationshipDefinition {
880    /// Create a new relationship definition
881    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    /// Create a built-in relationship definition
898    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    /// Set the inverse relationship
915    pub fn with_inverse(mut self, inverse: &str) -> Self {
916        self.inverse = Some(inverse.to_lowercase());
917        self
918    }
919
920    /// Set as symmetric
921    pub fn with_symmetric(mut self, symmetric: bool) -> Self {
922        self.symmetric = symmetric;
923        self
924    }
925
926    /// Set the cardinality
927    pub fn with_cardinality(mut self, cardinality: Cardinality) -> Self {
928        self.cardinality = cardinality;
929        self
930    }
931
932    /// Set source type constraints
933    pub fn with_source_types(mut self, types: Vec<String>) -> Self {
934        self.source_types = types;
935        self
936    }
937
938    /// Set target type constraints
939    pub fn with_target_types(mut self, types: Vec<String>) -> Self {
940        self.target_types = types;
941        self
942    }
943
944    /// Set the color
945    pub fn with_color(mut self, color: &str) -> Self {
946        self.color = Some(color.to_string());
947        self
948    }
949
950    /// Get the default built-in relationship definitions
951    pub fn defaults() -> Vec<RelationshipDefinition> {
952        vec![
953            // Requirement-to-requirement relationships
954            RelationshipDefinition::built_in("parent", "Parent", "Hierarchical parent requirement")
955                .with_inverse("child")
956                .with_cardinality(Cardinality::OneToMany), // A parent can have many children
957            RelationshipDefinition::built_in("child", "Child", "Hierarchical child requirement")
958                .with_inverse("parent")
959                .with_cardinality(Cardinality::ManyToOne), // Many children can share one parent
960            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            // User-to-requirement relationships
1000            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            // Sprint planning relationships
1029            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) // Each item in one Sprint at a time
1036            .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    /// Check if a source requirement type is allowed
1051    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    /// Check if a target requirement type is allowed
1062    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/// Result of validating a relationship
1074#[derive(Debug, Clone, TS)]
1075pub struct RelationshipValidation {
1076    /// Whether the relationship is valid
1077    pub valid: bool,
1078    /// Error messages (if invalid)
1079    pub errors: Vec<String>,
1080    /// Warning messages (valid but may have issues)
1081    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// ============================================================================
1117// Configurable ID System
1118// ============================================================================
1119
1120/// ID format style for requirement identifiers
1121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
1122pub enum IdFormat {
1123    /// Single-level format: PREFIX-NNN (e.g., AUTH-001, FR-002)
1124    /// Features and types share the same namespace
1125    #[default]
1126    SingleLevel,
1127    /// Two-level format: FEATURE-TYPE-NNN (e.g., AUTH-FR-001)
1128    /// Hierarchical with feature prefix, type prefix, and number
1129    TwoLevel,
1130}
1131
1132/// Numbering strategy for requirement IDs
1133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
1134pub enum NumberingStrategy {
1135    /// Global sequential numbering across all prefixes
1136    /// e.g., AUTH-001, FR-002, PAY-003
1137    #[default]
1138    Global,
1139    /// Per-prefix numbering (each prefix has its own counter)
1140    /// e.g., AUTH-001, FR-001, PAY-001
1141    PerPrefix,
1142    /// Per feature+type combination (only for TwoLevel format)
1143    /// e.g., AUTH-FR-001, AUTH-FR-002, AUTH-NFR-001
1144    PerFeatureType,
1145}
1146
1147/// Configuration for a requirement type with its prefix
1148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1149pub struct RequirementTypeDefinition {
1150    /// Display name for the type (e.g., "Functional")
1151    pub name: String,
1152    /// Prefix used in IDs (e.g., "FR")
1153    pub prefix: String,
1154    /// Optional description
1155    #[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
1169/// Default requirement types with prefixes
1170fn 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/// Configuration for a feature with its prefix
1197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1198pub struct FeatureDefinition {
1199    /// Sequential number for ordering
1200    pub number: u32,
1201    /// Display name for the feature
1202    pub name: String,
1203    /// Prefix used in IDs (e.g., "AUTH" for Authentication)
1204    pub prefix: String,
1205    /// Optional description
1206    #[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/// ID system configuration
1227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1228pub struct IdConfiguration {
1229    /// Format style for IDs
1230    #[serde(default)]
1231    pub format: IdFormat,
1232    /// Numbering strategy
1233    #[serde(default)]
1234    pub numbering: NumberingStrategy,
1235    /// Number of digits for the numeric portion (default 3 = 001)
1236    #[serde(default = "default_id_digits")]
1237    pub digits: u8,
1238    /// Configured requirement types
1239    #[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    /// Get all reserved prefixes (type prefixes that cannot be used as feature prefixes)
1260    pub fn reserved_prefixes(&self) -> Vec<String> {
1261        self.requirement_types
1262            .iter()
1263            .map(|t| t.prefix.clone())
1264            .collect()
1265    }
1266
1267    /// Check if a prefix is reserved (used by a requirement type)
1268    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    /// Get a requirement type definition by name
1274    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    /// Get a requirement type definition by prefix
1282    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    /// Format a number with the configured digit width
1288    pub fn format_number(&self, num: u32) -> String {
1289        format!("{:0>width$}", num, width = self.digits as usize)
1290    }
1291}
1292
1293// ============================================================================
1294// Original structures continue below
1295// ============================================================================
1296
1297/// Result of validating ID configuration changes
1298#[derive(Debug, Clone)]
1299pub struct IdConfigValidation {
1300    /// Whether the change is valid
1301    pub valid: bool,
1302    /// Error message if invalid
1303    pub error: Option<String>,
1304    /// Warning message (change is valid but has implications)
1305    pub warning: Option<String>,
1306    /// Whether migration is possible
1307    pub can_migrate: bool,
1308    /// Number of requirements that would be affected by migration
1309    pub affected_count: usize,
1310}
1311
1312/// Represents a relationship between two requirements
1313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1314pub struct Relationship {
1315    /// The type of relationship
1316    pub rel_type: RelationshipType,
1317    /// The target requirement ID
1318    pub target_id: Uuid,
1319    /// When this relationship was created
1320    #[serde(default, skip_serializing_if = "Option::is_none")]
1321    pub created_at: Option<DateTime<Utc>>,
1322    /// Who created this relationship
1323    #[serde(default, skip_serializing_if = "Option::is_none")]
1324    pub created_by: Option<String>,
1325}
1326
1327/// Represents a field change in a requirement's history
1328#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1329pub struct FieldChange {
1330    /// Name of the field that changed
1331    pub field_name: String,
1332
1333    /// Value before the change
1334    pub old_value: String,
1335
1336    /// Value after the change
1337    pub new_value: String,
1338}
1339
1340/// Represents a history entry for a requirement update
1341#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1342pub struct HistoryEntry {
1343    /// Unique identifier for this history entry
1344    pub id: Uuid,
1345
1346    /// Who made the change
1347    pub author: String,
1348
1349    /// When the change was made
1350    pub timestamp: DateTime<Utc>,
1351
1352    /// List of field changes in this update
1353    pub changes: Vec<FieldChange>,
1354}
1355
1356impl HistoryEntry {
1357    /// Creates a new history entry
1358    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/// A snapshot of a requirement at a specific point in time (for baselines)
1369/// This is a full copy of the requirement state, not a reference
1370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
1371pub struct RequirementSnapshot {
1372    /// The original requirement's UUID (for linking back)
1373    pub original_id: Uuid,
1374
1375    /// Spec ID at the time of snapshot
1376    pub spec_id: Option<String>,
1377
1378    /// Title at snapshot time
1379    pub title: String,
1380
1381    /// Description at snapshot time
1382    pub description: String,
1383
1384    /// Status at snapshot time
1385    pub status: RequirementStatus,
1386
1387    /// Priority at snapshot time
1388    pub priority: RequirementPriority,
1389
1390    /// Owner at snapshot time
1391    pub owner: String,
1392
1393    /// Feature at snapshot time
1394    pub feature: String,
1395
1396    /// Type at snapshot time
1397    pub req_type: RequirementType,
1398
1399    /// Tags at snapshot time
1400    pub tags: HashSet<String>,
1401
1402    /// Relationships at snapshot time (storing IDs, not full objects)
1403    pub relationships: Vec<Relationship>,
1404
1405    /// Custom status at snapshot time
1406    #[serde(default, skip_serializing_if = "Option::is_none")]
1407    pub custom_status: Option<String>,
1408
1409    /// Custom priority at snapshot time
1410    #[serde(default, skip_serializing_if = "Option::is_none")]
1411    pub custom_priority: Option<String>,
1412
1413    /// Custom fields at snapshot time
1414    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
1415    pub custom_fields: std::collections::HashMap<String, String>,
1416}
1417
1418impl RequirementSnapshot {
1419    /// Creates a snapshot from a requirement
1420    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/// Represents a baseline - a named snapshot of requirements at a point in time
1441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
1442pub struct Baseline {
1443    /// Unique identifier for this baseline
1444    pub id: Uuid,
1445
1446    /// Human-readable name (e.g., "Release 1.0", "Sprint 5 End")
1447    pub name: String,
1448
1449    /// Optional description of what this baseline represents
1450    #[serde(default, skip_serializing_if = "Option::is_none")]
1451    pub description: Option<String>,
1452
1453    /// When the baseline was created
1454    pub created_at: DateTime<Utc>,
1455
1456    /// Who created the baseline
1457    pub created_by: String,
1458
1459    /// Git tag associated with this baseline (for YAML backend)
1460    /// Format: "baseline-{name-slug}" or custom
1461    #[serde(default, skip_serializing_if = "Option::is_none")]
1462    pub git_tag: Option<String>,
1463
1464    /// Full snapshots of all requirements at baseline time
1465    /// For SQL backends, this is always populated
1466    /// For YAML backend, this may be empty if git_tag is used (can reconstruct from git)
1467    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1468    pub requirements: Vec<RequirementSnapshot>,
1469
1470    /// Whether this baseline is locked (cannot be modified or deleted)
1471    #[serde(default)]
1472    pub locked: bool,
1473}
1474
1475impl Baseline {
1476    /// Creates a new baseline with snapshots of the given requirements
1477    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) // Don't include archived requirements in baselines
1486            .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    /// Creates a baseline name slug suitable for git tags
1502    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    /// Gets the git tag name for this baseline
1513    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/// Summary of changes between two baselines or baseline and current state
1521#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1522pub struct BaselineComparison {
1523    /// Requirements added (in target but not in source)
1524    pub added: Vec<Uuid>,
1525
1526    /// Requirements removed (in source but not in target)
1527    pub removed: Vec<Uuid>,
1528
1529    /// Requirements modified (exist in both but changed)
1530    pub modified: Vec<BaselineRequirementDiff>,
1531
1532    /// Requirements unchanged
1533    pub unchanged: Vec<Uuid>,
1534}
1535
1536/// Represents changes to a single requirement between baselines
1537#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1538pub struct BaselineRequirementDiff {
1539    /// The requirement's UUID
1540    pub id: Uuid,
1541
1542    /// Spec ID (for display)
1543    pub spec_id: Option<String>,
1544
1545    /// List of changed fields
1546    pub changes: Vec<FieldChange>,
1547}
1548
1549/// Represents a reaction emoji definition
1550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1551pub struct ReactionDefinition {
1552    /// Unique identifier/key for the reaction (e.g., "resolved", "rejected")
1553    pub name: String,
1554
1555    /// The emoji character to display
1556    pub emoji: String,
1557
1558    /// Human-readable label for the reaction
1559    pub label: String,
1560
1561    /// Optional description of when to use this reaction
1562    #[serde(skip_serializing_if = "Option::is_none")]
1563    pub description: Option<String>,
1564
1565    /// Whether this is a built-in reaction (cannot be deleted)
1566    #[serde(default)]
1567    pub built_in: bool,
1568}
1569
1570impl ReactionDefinition {
1571    /// Creates a new reaction definition
1572    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    /// Creates a built-in reaction definition
1587    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
1603/// Returns the default set of reaction definitions
1604pub 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/// Represents a reaction on a comment
1631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1632pub struct CommentReaction {
1633    /// The reaction type (references ReactionDefinition.name)
1634    pub reaction: String,
1635
1636    /// Who added this reaction
1637    pub author: String,
1638
1639    /// When the reaction was added
1640    pub added_at: DateTime<Utc>,
1641}
1642
1643impl CommentReaction {
1644    /// Creates a new reaction
1645    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/// How a URL should be opened when clicked
1655#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, TS)]
1656#[serde(rename_all = "snake_case")]
1657pub enum UrlOpenMode {
1658    /// Open in embedded iframe preview (default)
1659    #[default]
1660    Preview,
1661    /// Open in a new browser tab/window
1662    NewTab,
1663}
1664
1665/// Represents an external URL link attached to a requirement
1666#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1667pub struct UrlLink {
1668    /// Unique identifier for the link
1669    pub id: Uuid,
1670
1671    /// The URL
1672    pub url: String,
1673
1674    /// Display title/label for the link
1675    pub title: String,
1676
1677    /// Optional description
1678    #[serde(default, skip_serializing_if = "Option::is_none")]
1679    pub description: Option<String>,
1680
1681    /// How the URL should be opened (preview iframe or new tab)
1682    #[serde(default)]
1683    pub open_mode: UrlOpenMode,
1684
1685    /// When the link was added
1686    pub added_at: DateTime<Utc>,
1687
1688    /// Who added the link
1689    pub added_by: String,
1690
1691    /// Last time the URL was verified as accessible
1692    #[serde(default, skip_serializing_if = "Option::is_none")]
1693    pub last_verified: Option<DateTime<Utc>>,
1694
1695    /// Whether the last verification succeeded
1696    #[serde(default, skip_serializing_if = "Option::is_none")]
1697    pub last_verified_ok: Option<bool>,
1698}
1699
1700impl UrlLink {
1701    /// Creates a new URL link
1702    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    /// Creates a new URL link with description
1721    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1722        self.description = Some(description.into());
1723        self
1724    }
1725
1726    /// Sets the open mode for the URL link
1727    pub fn with_open_mode(mut self, mode: UrlOpenMode) -> Self {
1728        self.open_mode = mode;
1729        self
1730    }
1731}
1732
1733/// Represents a file attachment on a requirement
1734#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1735pub struct Attachment {
1736    /// Unique identifier for the attachment
1737    pub id: Uuid,
1738
1739    /// Original filename
1740    pub filename: String,
1741
1742    /// Relative path to the stored file (e.g., "attachments/FR-0042/document.pdf")
1743    pub stored_path: String,
1744
1745    /// Optional MIME type
1746    #[serde(default, skip_serializing_if = "Option::is_none")]
1747    pub mime_type: Option<String>,
1748
1749    /// File size in bytes
1750    pub size_bytes: u64,
1751
1752    /// When the attachment was added
1753    pub added_at: DateTime<Utc>,
1754
1755    /// Who added the attachment (user handle)
1756    #[serde(default, skip_serializing_if = "Option::is_none")]
1757    pub added_by: Option<String>,
1758
1759    /// Optional description of the attachment
1760    #[serde(default, skip_serializing_if = "Option::is_none")]
1761    pub description: Option<String>,
1762}
1763
1764impl Attachment {
1765    /// Creates a new attachment
1766    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    /// Sets the MIME type
1785    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    /// Sets the description
1791    pub fn with_description(mut self, description: impl Into<String>) -> Self {
1792        self.description = Some(description.into());
1793        self
1794    }
1795
1796    /// Formats the file size as a human-readable string
1797    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// trace:REQ-0243 | ai:claude:high
1812/// Represents the type of artifact being traced
1813#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
1814pub enum ArtifactType {
1815    /// Source code file
1816    SourceCode,
1817    /// Test code file
1818    TestCode,
1819    /// Configuration file
1820    Config,
1821    /// Documentation file
1822    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    /// Parse an artifact type from a string
1838    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// trace:REQ-0243 | ai:claude:high
1850/// Represents a trace link between a requirement and a code artifact
1851/// This enables bidirectional traceability: requirement -> code and code -> requirement
1852#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1853pub struct TraceLink {
1854    /// Unique identifier for the trace link
1855    pub id: Uuid,
1856
1857    /// The type of artifact (source, test, config, doc)
1858    pub artifact_type: ArtifactType,
1859
1860    /// Path to the file containing the artifact (relative to project root)
1861    pub file_path: String,
1862
1863    /// Optional symbol name (function, struct, module, etc.)
1864    #[serde(default, skip_serializing_if = "Option::is_none")]
1865    pub symbol: Option<String>,
1866
1867    /// Optional line range (start, end) where the implementation exists
1868    #[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    /// Optional notes about this trace link
1875    #[serde(default, skip_serializing_if = "Option::is_none")]
1876    pub notes: Option<String>,
1877
1878    /// When this trace link was created
1879    pub created_at: DateTime<Utc>,
1880
1881    /// Who created this trace link
1882    #[serde(default, skip_serializing_if = "Option::is_none")]
1883    pub created_by: Option<String>,
1884
1885    /// Git commit hash where this trace was identified (optional)
1886    #[serde(default, skip_serializing_if = "Option::is_none")]
1887    pub commit_hash: Option<String>,
1888}
1889
1890impl TraceLink {
1891    /// Creates a new trace link
1892    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    /// Sets the symbol name
1908    pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
1909        self.symbol = Some(symbol.into());
1910        self
1911    }
1912
1913    /// Sets the line range
1914    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    /// Sets notes
1921    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
1922        self.notes = Some(notes.into());
1923        self
1924    }
1925
1926    /// Sets the creator
1927    pub fn with_created_by(mut self, author: impl Into<String>) -> Self {
1928        self.created_by = Some(author.into());
1929        self
1930    }
1931
1932    /// Sets the commit hash
1933    pub fn with_commit(mut self, hash: impl Into<String>) -> Self {
1934        self.commit_hash = Some(hash.into());
1935        self
1936    }
1937
1938    /// Returns the line range as a tuple if both start and end are set
1939    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// trace:STORY-0323 | ai:claude
1948/// Represents a link between an AIDA requirement and a GitLab issue
1949/// Used to track traceability between specs and implementation work
1950#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1951pub struct GitLabIssueLink {
1952    /// Unique identifier for the link
1953    pub id: Uuid,
1954
1955    /// GitLab issue IID (project-scoped issue number)
1956    pub issue_iid: u64,
1957
1958    /// GitLab project ID (optional - uses default from config if not set)
1959    #[serde(default, skip_serializing_if = "Option::is_none")]
1960    pub project_id: Option<u64>,
1961
1962    /// Display title for the issue (cached from GitLab)
1963    pub issue_title: String,
1964
1965    /// Type of link between requirement and issue
1966    #[serde(default)]
1967    pub link_type: GitLabLinkType,
1968
1969    /// Optional notes about this link
1970    #[serde(default, skip_serializing_if = "Option::is_none")]
1971    pub notes: Option<String>,
1972
1973    /// When this link was created
1974    pub created_at: DateTime<Utc>,
1975
1976    /// Who created this link
1977    #[serde(default, skip_serializing_if = "Option::is_none")]
1978    pub created_by: Option<String>,
1979
1980    /// Last time the issue data was synced from GitLab
1981    #[serde(default, skip_serializing_if = "Option::is_none")]
1982    pub last_synced: Option<DateTime<Utc>>,
1983
1984    /// GitLab issue state when last synced (open/closed)
1985    #[serde(default, skip_serializing_if = "Option::is_none")]
1986    pub issue_state: Option<String>,
1987}
1988
1989impl GitLabIssueLink {
1990    /// Creates a new GitLab issue link
1991    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    /// Sets the project ID
2007    pub fn with_project(mut self, project_id: u64) -> Self {
2008        self.project_id = Some(project_id);
2009        self
2010    }
2011
2012    /// Sets the link type
2013    pub fn with_link_type(mut self, link_type: GitLabLinkType) -> Self {
2014        self.link_type = link_type;
2015        self
2016    }
2017
2018    /// Sets the creator
2019    pub fn with_creator(mut self, creator: impl Into<String>) -> Self {
2020        self.created_by = Some(creator.into());
2021        self
2022    }
2023
2024    /// Sets notes
2025    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
2026        self.notes = Some(notes.into());
2027        self
2028    }
2029
2030    /// Update sync metadata from GitLab issue
2031    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    /// Returns a display string like "GL-123"
2038    pub fn display_id(&self) -> String {
2039        format!("GL-{}", self.issue_iid)
2040    }
2041}
2042
2043// trace:STORY-0323 | ai:claude
2044/// Type of link between AIDA requirement and GitLab issue
2045#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2046pub enum GitLabLinkType {
2047    /// Requirement is implemented by the GitLab issue
2048    #[default]
2049    ImplementedBy,
2050    /// Requirement traces to the GitLab issue (general traceability)
2051    TracesTo,
2052    /// GitLab issue is a bug related to this requirement
2053    RelatedBug,
2054    /// GitLab issue is a follow-up task
2055    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// trace:STORY-0325 | ai:claude
2070/// How a GitLab link was originally created
2071#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2072pub enum LinkOrigin {
2073    /// Issue was created from AIDA via "Create GitLab Issue"
2074    #[default]
2075    CreatedFromAida,
2076    /// Issue was imported from GitLab (future feature)
2077    ImportedFromGitLab,
2078    /// User manually linked an existing issue
2079    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// trace:STORY-0325 | ai:claude
2093/// Current sync status between AIDA and GitLab
2094#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
2095pub enum SyncStatus {
2096    /// Content matches between AIDA and GitLab
2097    #[default]
2098    InSync,
2099    /// AIDA requirement has changed since last sync
2100    AidaModified,
2101    /// GitLab issue has changed since last sync
2102    GitLabModified,
2103    /// Both AIDA and GitLab have changed (conflict)
2104    Conflict,
2105    /// A sync error occurred
2106    Error,
2107    /// Linked but not actively syncing
2108    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// trace:STORY-0325 | ai:claude
2125/// Tracks sync state between an AIDA requirement and a GitLab issue
2126#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2127pub struct GitLabSyncState {
2128    /// AIDA requirement UUID
2129    pub requirement_id: Uuid,
2130
2131    /// AIDA spec-id (for display)
2132    pub spec_id: String,
2133
2134    /// GitLab project ID
2135    pub gitlab_project_id: u64,
2136
2137    /// GitLab issue IID (project-scoped issue number)
2138    pub gitlab_issue_iid: u64,
2139
2140    /// GitLab issue global ID (for API operations)
2141    pub gitlab_issue_id: u64,
2142
2143    /// When the link was created
2144    pub linked_at: DateTime<Utc>,
2145
2146    /// Last successful sync timestamp
2147    pub last_sync: DateTime<Utc>,
2148
2149    /// Hash of AIDA content at last sync
2150    pub aida_content_hash: String,
2151
2152    /// Hash of GitLab content at last sync
2153    pub gitlab_content_hash: String,
2154
2155    /// How the link was created
2156    pub link_origin: LinkOrigin,
2157
2158    /// Current sync status
2159    pub sync_status: SyncStatus,
2160
2161    /// Last sync error message (if any)
2162    #[serde(default, skip_serializing_if = "Option::is_none")]
2163    pub last_error: Option<String>,
2164}
2165
2166impl GitLabSyncState {
2167    /// Create a new sync state for a newly created link
2168    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    /// Update the sync state after a successful sync
2194    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    /// Mark the sync state as having an error
2203    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    /// Calculate content hash for an AIDA requirement
2209    /// Includes: title, description, status, priority, owner
2210    /// Excludes: timestamps, comments, history (too volatile)
2211    pub fn hash_requirement(req: &Requirement) -> String {
2212        use sha2::{Digest, Sha256};
2213        let mut hasher = Sha256::new();
2214
2215        // Include stable content fields
2216        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        // Include tags (sorted for consistency)
2224        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    /// Calculate content hash for a GitLab issue
2234    /// Includes: title, description, state, labels, assignees
2235    /// Excludes: timestamps, comment count, vote count (too volatile)
2236    #[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        // Include stable content fields
2242        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        // Include labels (sorted for consistency)
2249        let mut labels = issue.labels.clone();
2250        labels.sort();
2251        for label in labels {
2252            hasher.update(label.as_bytes());
2253        }
2254
2255        // Include assignees (sorted by id for consistency)
2256        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// trace:EPIC-0246 | ai:claude:high
2267/// Confidence level for AI-generated implementation
2268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS)]
2269pub enum ConfidenceLevel {
2270    /// >80% AI-generated
2271    High,
2272    /// 40-80% AI with modifications
2273    Medium,
2274    /// <40% AI, mostly human
2275    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    /// Parse a confidence level from a string
2290    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// trace:EPIC-0246 | ai:claude:high
2301/// Tracks implementation metadata for a requirement
2302/// Stores information about how and when a requirement was implemented
2303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)]
2304pub struct ImplementationInfo {
2305    /// Whether the requirement has been implemented
2306    pub implemented: bool,
2307
2308    /// Summary of the implementation
2309    #[serde(default, skip_serializing_if = "Option::is_none")]
2310    pub summary: Option<String>,
2311
2312    /// When the last AI agent run occurred for this requirement
2313    #[serde(default, skip_serializing_if = "Option::is_none")]
2314    pub last_agent_run: Option<DateTime<Utc>>,
2315
2316    /// Risk notes identified during implementation
2317    #[serde(default, skip_serializing_if = "Option::is_none")]
2318    pub risk_notes: Option<String>,
2319
2320    /// Notes about test coverage
2321    #[serde(default, skip_serializing_if = "Option::is_none")]
2322    pub test_coverage_notes: Option<String>,
2323
2324    /// The AI tool used for implementation (e.g., "claude", "copilot")
2325    #[serde(default, skip_serializing_if = "Option::is_none")]
2326    pub source_tool: Option<String>,
2327
2328    /// Confidence level of the AI-generated implementation
2329    #[serde(default, skip_serializing_if = "Option::is_none")]
2330    pub confidence: Option<ConfidenceLevel>,
2331
2332    /// When the implementation was completed
2333    #[serde(default, skip_serializing_if = "Option::is_none")]
2334    pub implemented_at: Option<DateTime<Utc>>,
2335
2336    /// Who performed the implementation
2337    #[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    /// Creates a new ImplementationInfo with implemented=false
2359    pub fn new() -> Self {
2360        Self::default()
2361    }
2362
2363    /// Creates a new ImplementationInfo marked as implemented
2364    pub fn implemented() -> Self {
2365        Self {
2366            implemented: true,
2367            implemented_at: Some(Utc::now()),
2368            ..Self::default()
2369        }
2370    }
2371
2372    /// Sets the implementation summary
2373    pub fn with_summary(mut self, summary: impl Into<String>) -> Self {
2374        self.summary = Some(summary.into());
2375        self
2376    }
2377
2378    /// Sets the source tool
2379    pub fn with_source_tool(mut self, tool: impl Into<String>) -> Self {
2380        self.source_tool = Some(tool.into());
2381        self
2382    }
2383
2384    /// Sets the confidence level
2385    pub fn with_confidence(mut self, confidence: ConfidenceLevel) -> Self {
2386        self.confidence = Some(confidence);
2387        self
2388    }
2389
2390    /// Sets the implementer
2391    pub fn with_implemented_by(mut self, author: impl Into<String>) -> Self {
2392        self.implemented_by = Some(author.into());
2393        self
2394    }
2395
2396    /// Records an agent run
2397    pub fn record_agent_run(&mut self) {
2398        self.last_agent_run = Some(Utc::now());
2399    }
2400}
2401
2402/// Parsed trace comment from source code
2403/// Format: `// trace:<SPEC-ID> - <title> | ai:<tool>:<confidence> | impl:<date> | by:<user>`
2404#[derive(Debug, Clone, PartialEq, Eq, TS)]
2405pub struct TraceComment {
2406    /// The requirement ID (e.g., FR-0042)
2407    pub spec_id: String,
2408    /// Brief title from the requirement (optional)
2409    pub title: Option<String>,
2410    /// AI tool used (e.g., "claude")
2411    pub ai_tool: Option<String>,
2412    /// Confidence level
2413    pub confidence: Option<ConfidenceLevel>,
2414    /// Implementation date
2415    pub impl_date: Option<String>,
2416    /// Implementer username
2417    pub implemented_by: Option<String>,
2418}
2419
2420impl TraceComment {
2421    /// Parse a trace comment from a line of source code
2422    /// Supports both old format: `// trace:FR-0042 | ai:claude:high`
2423    /// And new format: `// trace:FR-0042 - Title | ai:claude:high | impl:2025-12-10 | by:joe`
2424    pub fn parse(line: &str) -> Option<Self> {
2425        // Strip comment prefix and find trace:
2426        let line = line.trim();
2427        let trace_start = line.find("trace:")?;
2428        let content = &line[trace_start + 6..];
2429
2430        // Split by pipe to get segments
2431        let segments: Vec<&str> = content.split('|').map(|s| s.trim()).collect();
2432        if segments.is_empty() {
2433            return None;
2434        }
2435
2436        // First segment: SPEC-ID optionally followed by " - title"
2437        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        // Parse remaining segments
2456        for segment in segments.iter().skip(1) {
2457            let segment = segment.trim();
2458            if segment.starts_with("ai:") {
2459                // Format: ai:claude:high or ai:claude
2460                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    /// Format as a trace comment string (without the comment prefix)
2478    pub fn format(&self) -> String {
2479        let mut parts = Vec::new();
2480
2481        // SPEC-ID with optional title
2482        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        // AI tool and confidence
2490        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        // Implementation date
2500        if let Some(ref date) = self.impl_date {
2501            parts.push(format!("impl:{}", date));
2502        }
2503
2504        // Implementer
2505        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/// Represents a comment on a requirement with threading support
2514#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
2515pub struct Comment {
2516    /// Unique identifier for the comment
2517    pub id: Uuid,
2518
2519    /// Author of the comment
2520    pub author: String,
2521
2522    /// Content of the comment
2523    pub content: String,
2524
2525    /// When the comment was created
2526    pub created_at: DateTime<Utc>,
2527
2528    /// When the comment was last modified
2529    pub modified_at: DateTime<Utc>,
2530
2531    /// Parent comment ID (None for top-level comments)
2532    #[serde(skip_serializing_if = "Option::is_none")]
2533    pub parent_id: Option<Uuid>,
2534
2535    /// Nested replies to this comment
2536    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2537    pub replies: Vec<Comment>,
2538
2539    /// Reactions on this comment
2540    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2541    pub reactions: Vec<CommentReaction>,
2542}
2543
2544impl Comment {
2545    /// Creates a new top-level comment
2546    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    /// Creates a new reply to an existing comment
2561    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    /// Adds a reaction to this comment
2576    /// Returns true if reaction was added, false if user already has this reaction
2577    pub fn add_reaction(&mut self, reaction: &str, author: &str) -> bool {
2578        // Check if user already has this reaction
2579        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    /// Removes a reaction from this comment
2591    /// Returns true if reaction was removed, false if not found
2592    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    /// Toggles a reaction (adds if not present, removes if present)
2600    /// Returns true if reaction is now present, false if removed
2601    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    /// Gets counts of each reaction type
2611    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    /// Checks if a user has a specific reaction
2620    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    /// Adds a reply to this comment
2627    pub fn add_reply(&mut self, reply: Comment) {
2628        self.replies.push(reply);
2629    }
2630
2631    /// Finds a comment by ID in this comment tree
2632    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    /// Updates the modified timestamp
2645    pub fn touch(&mut self) {
2646        self.modified_at = Utc::now();
2647    }
2648
2649    /// Recursively removes a reply from comment tree
2650    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/// Represents a user in the system
2665#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2666pub struct User {
2667    /// Unique identifier for the user
2668    pub id: Uuid,
2669
2670    /// Human-friendly spec ID (e.g., "$USER-001")
2671    #[serde(skip_serializing_if = "Option::is_none")]
2672    pub spec_id: Option<String>,
2673
2674    /// User's full name
2675    pub name: String,
2676
2677    /// User's email address
2678    pub email: String,
2679
2680    /// User's handle for @mentions (without the @)
2681    pub handle: String,
2682
2683    /// Hashed PIN for simple authentication (SHA-256 hash stored as hex string)
2684    /// This is a basic authentication mechanism for web clients.
2685    /// For production use, consider a proper auth system (OAuth, JWT, etc.)
2686    #[serde(default, skip_serializing_if = "Option::is_none")]
2687    pub pin_hash: Option<String>,
2688
2689    /// When the user was created
2690    pub created_at: DateTime<Utc>,
2691
2692    /// Whether the user is archived
2693    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2694    pub archived: bool,
2695
2696    /// Version number for optimistic locking (SQLite only)
2697    #[serde(skip)]
2698    pub version: i64,
2699}
2700
2701impl User {
2702    /// Creates a new user (without spec_id - use RequirementsStore::add_user for auto-generated ID)
2703    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    /// Creates a new user with a spec_id
2718    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    /// Returns display name: spec_id if available, otherwise name
2733    pub fn display_id(&self) -> &str {
2734        self.spec_id.as_deref().unwrap_or(&self.name)
2735    }
2736
2737    /// Set the user's PIN (stores SHA-256 hash)
2738    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    /// Verify a PIN against the stored hash
2747    /// Returns true if the PIN matches, false otherwise
2748    /// Returns false if no PIN is set
2749    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    /// Check if the user has a PIN set
2763    pub fn has_pin(&self) -> bool {
2764        self.pin_hash.is_some()
2765    }
2766
2767    /// Clear the user's PIN
2768    pub fn clear_pin(&mut self) {
2769        self.pin_hash = None;
2770    }
2771}
2772
2773/// Represents a team in the system
2774/// Teams can contain users (members) and can be nested (parent/child teams)
2775#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2776pub struct Team {
2777    /// Unique identifier for the team
2778    pub id: Uuid,
2779
2780    /// Human-friendly spec ID (e.g., "$TEAM-001")
2781    #[serde(skip_serializing_if = "Option::is_none")]
2782    pub spec_id: Option<String>,
2783
2784    /// Team name
2785    pub name: String,
2786
2787    /// Team description (optional)
2788    #[serde(default, skip_serializing_if = "String::is_empty")]
2789    pub description: String,
2790
2791    /// Parent team ID for nested teams (None if top-level team)
2792    #[serde(skip_serializing_if = "Option::is_none")]
2793    pub parent_team_id: Option<Uuid>,
2794
2795    /// User IDs of team members
2796    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2797    pub member_ids: Vec<Uuid>,
2798
2799    /// When the team was created
2800    pub created_at: DateTime<Utc>,
2801
2802    /// When the team was last modified
2803    #[serde(skip_serializing_if = "Option::is_none")]
2804    pub modified_at: Option<DateTime<Utc>>,
2805
2806    /// Whether the team is archived
2807    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2808    pub archived: bool,
2809}
2810
2811impl Team {
2812    /// Creates a new team (without spec_id - use RequirementsStore::add_team_with_id for auto-generated ID)
2813    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    /// Creates a new team with a spec_id
2828    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    /// Returns display name: spec_id if available, otherwise name
2848    pub fn display_id(&self) -> &str {
2849        self.spec_id.as_deref().unwrap_or(&self.name)
2850    }
2851
2852    /// Adds a member to the team
2853    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    /// Removes a member from the team
2861    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    /// Checks if a user is a member of this team
2872    pub fn has_member(&self, user_id: &Uuid) -> bool {
2873        self.member_ids.contains(user_id)
2874    }
2875
2876    /// Returns the number of members
2877    pub fn member_count(&self) -> usize {
2878        self.member_ids.len()
2879    }
2880}
2881
2882/// Represents a single requirement in the system
2883#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2884pub struct Requirement {
2885    /// Unique identifier for the requirement (UUID)
2886    pub id: Uuid,
2887
2888    /// Human-friendly specification ID (e.g., "SPEC-001")
2889    #[serde(skip_serializing_if = "Option::is_none")]
2890    pub spec_id: Option<String>,
2891
2892    /// Short agreed ID assigned at merge-to-trunk (e.g., "FR-423").
2893    /// Only populated in distributed mode after the merge gate runs.
2894    /// In centralized mode, spec_id is already the short form so this is unused.
2895    /// Both spec_id and agreed_id permanently resolve to the same UUID.
2896    #[serde(default, skip_serializing_if = "Option::is_none")]
2897    pub agreed_id: Option<String>,
2898
2899    /// Optional prefix override for the spec_id (e.g., "SEC" for security requirements)
2900    /// If set, uses this prefix instead of deriving from feature/type
2901    /// Must be uppercase letters only (A-Z)
2902    #[serde(default, skip_serializing_if = "Option::is_none")]
2903    pub prefix_override: Option<String>,
2904
2905    /// Short title describing the requirement
2906    pub title: String,
2907
2908    /// Detailed description of the requirement
2909    pub description: String,
2910
2911    /// Current status of the requirement
2912    pub status: RequirementStatus,
2913
2914    /// Priority level of the requirement
2915    pub priority: RequirementPriority,
2916
2917    /// Person responsible for the requirement
2918    pub owner: String,
2919
2920    /// The feature this requirement belongs to
2921    pub feature: String,
2922
2923    /// When the requirement was created
2924    pub created_at: DateTime<Utc>,
2925
2926    /// Who created this requirement
2927    #[serde(default, skip_serializing_if = "Option::is_none")]
2928    pub created_by: Option<String>,
2929
2930    /// When the requirement was last modified
2931    pub modified_at: DateTime<Utc>,
2932
2933    /// Type of the requirement
2934    pub req_type: RequirementType,
2935
2936    /// Subtype for Meta requirements (prompts, skills, commands, etc.)
2937    #[serde(default, skip_serializing_if = "Option::is_none")]
2938    pub meta_subtype: Option<MetaSubtype>,
2939
2940    /// IDs of requirements this requirement depends on
2941    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2942    pub dependencies: Vec<Uuid>,
2943
2944    /// Tags for categorizing the requirement
2945    #[serde(default, skip_serializing_if = "HashSet::is_empty")]
2946    pub tags: HashSet<String>,
2947
2948    /// Weight/effort estimate for the requirement (e.g., story points)
2949    /// Optional - only shown in UI when set
2950    #[serde(default, skip_serializing_if = "Option::is_none")]
2951    pub weight: Option<f32>,
2952
2953    /// Relationships to other requirements
2954    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2955    pub relationships: Vec<Relationship>,
2956
2957    /// Comments on this requirement (threaded)
2958    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2959    pub comments: Vec<Comment>,
2960
2961    /// History of changes to this requirement
2962    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2963    pub history: Vec<HistoryEntry>,
2964
2965    /// Whether this requirement is archived
2966    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2967    pub archived: bool,
2968
2969    /// Custom status string (for types with custom statuses)
2970    /// If set, this takes precedence over the `status` enum field
2971    #[serde(default, skip_serializing_if = "Option::is_none")]
2972    pub custom_status: Option<String>,
2973
2974    /// Custom priority string (for types with custom priorities)
2975    /// If set, this takes precedence over the `priority` enum field
2976    #[serde(default, skip_serializing_if = "Option::is_none")]
2977    pub custom_priority: Option<String>,
2978
2979    /// Custom field values (key = field name, value = field value as string)
2980    #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
2981    pub custom_fields: std::collections::HashMap<String, String>,
2982
2983    /// External URL links attached to this requirement
2984    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2985    pub urls: Vec<UrlLink>,
2986
2987    /// File attachments on this requirement
2988    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2989    pub attachments: Vec<Attachment>,
2990
2991    // trace:REQ-0243 | ai:claude:high
2992    /// Trace links to code artifacts implementing this requirement
2993    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2994    pub trace_links: Vec<TraceLink>,
2995
2996    // trace:STORY-0323 | ai:claude
2997    /// Links to GitLab issues related to this requirement
2998    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2999    pub gitlab_issues: Vec<GitLabIssueLink>,
3000
3001    // trace:EPIC-0246 | ai:claude:high
3002    /// Implementation metadata for this requirement
3003    #[serde(default, skip_serializing_if = "Option::is_none")]
3004    pub implementation_info: Option<ImplementationInfo>,
3005
3006    /// Cached AI evaluation results
3007    /// Automatically populated by background evaluator when requirement changes
3008    #[serde(default, skip_serializing_if = "Option::is_none")]
3009    pub ai_evaluation: Option<StoredAiEvaluation>,
3010
3011    /// Version number for optimistic locking (SQLite only)
3012    /// Incremented on each update, used to detect concurrent modifications
3013    #[serde(skip)]
3014    pub version: i64,
3015}
3016
3017impl Requirement {
3018    /// Creates a new requirement with the specified title and description
3019    pub fn new(title: String, description: String) -> Self {
3020        let now = Utc::now();
3021
3022        // Get default feature name from environment variable
3023        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, // Will be assigned when added to store
3029            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    /// Gets the best display ID: agreed_id if available, then spec_id, then UUID.
3063    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    /// Check if this requirement matches a given ID string.
3072    /// Matches against spec_id, agreed_id, or UUID.
3073    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    /// Gets the effective status string, preferring custom_status if set
3080    pub fn effective_status(&self) -> String {
3081        self.custom_status
3082            .clone()
3083            .unwrap_or_else(|| self.status.to_string())
3084    }
3085
3086    /// Sets the status from a string, using custom_status for non-standard values
3087    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                // Custom status - keep enum at Draft but store custom value
3107                self.custom_status = Some(other.to_string());
3108            }
3109        }
3110    }
3111
3112    /// Gets the effective priority string, preferring custom_priority if set
3113    pub fn effective_priority(&self) -> String {
3114        self.custom_priority
3115            .clone()
3116            .unwrap_or_else(|| self.priority.to_string())
3117    }
3118
3119    /// Sets the priority from a string, using custom_priority for non-standard values
3120    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                // Custom priority - keep enum at Medium but store custom value
3136                self.custom_priority = Some(other.to_string());
3137            }
3138        }
3139    }
3140
3141    /// Gets a custom field value
3142    pub fn get_custom_field(&self, name: &str) -> Option<&String> {
3143        self.custom_fields.get(name)
3144    }
3145
3146    /// Sets a custom field value
3147    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    /// Removes a custom field
3152    pub fn remove_custom_field(&mut self, name: &str) -> Option<String> {
3153        self.custom_fields.remove(name)
3154    }
3155
3156    /// Validates and normalizes a prefix string
3157    /// Returns Some(normalized_prefix) if valid, None if invalid
3158    /// Valid prefixes contain only uppercase letters A-Z
3159    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    /// Sets the prefix override with validation
3173    /// Returns Ok if valid or empty, Err with message if invalid
3174    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    /// Records a change to the requirement history
3190    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    /// Helper to create a field change
3199    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    /// Adds a top-level comment to this requirement
3208    pub fn add_comment(&mut self, comment: Comment) {
3209        self.comments.push(comment);
3210        self.modified_at = Utc::now();
3211    }
3212
3213    /// Adds a reply to an existing comment
3214    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    /// Finds a comment by ID (returns mutable reference)
3231    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    /// Deletes a comment by ID
3244    pub fn delete_comment(&mut self, comment_id: &Uuid) -> anyhow::Result<()> {
3245        // Try to find and remove from top-level
3246        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        // Search in nested replies
3253        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    /// Compute a hash of the requirement content used for AI evaluation staleness detection
3264    /// The hash includes title, description, and type - fields that affect evaluation
3265    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    /// Check if AI evaluation is needed (never evaluated or stale)
3277    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// ============================================================================
3286// AI Prompt Configuration
3287// ============================================================================
3288
3289/// Configuration for a single AI prompt action (evaluation, duplicates, etc.)
3290#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3291pub struct AiActionPromptConfig {
3292    /// Custom template to replace the default prompt entirely.
3293    /// Use placeholders: {project_context}, {req_context}, {related_context}, {all_reqs}
3294    #[serde(default, skip_serializing_if = "Option::is_none")]
3295    pub custom_template: Option<String>,
3296
3297    /// Additional instructions appended to the default prompt.
3298    /// Used when custom_template is None.
3299    #[serde(default, skip_serializing_if = "String::is_empty")]
3300    pub additional_instructions: String,
3301}
3302
3303/// Per-type AI prompt customization
3304#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3305pub struct AiTypePromptConfig {
3306    /// The requirement type this config applies to (e.g., "Functional", "Epic", "Story")
3307    pub type_name: String,
3308
3309    /// Extra instructions for evaluation prompts for this type
3310    #[serde(default, skip_serializing_if = "String::is_empty")]
3311    pub evaluation_extra: String,
3312
3313    /// Extra instructions for improve description prompts for this type
3314    #[serde(default, skip_serializing_if = "String::is_empty")]
3315    pub improve_extra: String,
3316
3317    /// Extra instructions for generate children prompts for this type
3318    #[serde(default, skip_serializing_if = "String::is_empty")]
3319    pub generate_children_extra: String,
3320}
3321
3322/// Complete AI prompt configuration for a project
3323#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
3324pub struct AiPromptConfig {
3325    /// Global context prepended to ALL AI prompts.
3326    /// Use this to describe your project's methodology, terminology, or special rules.
3327    #[serde(default, skip_serializing_if = "String::is_empty")]
3328    pub global_context: String,
3329
3330    /// Configuration for the evaluation action
3331    #[serde(default, skip_serializing_if = "is_default_action_config")]
3332    pub evaluation: AiActionPromptConfig,
3333
3334    /// Configuration for the find duplicates action
3335    #[serde(default, skip_serializing_if = "is_default_action_config")]
3336    pub duplicates: AiActionPromptConfig,
3337
3338    /// Configuration for the suggest relationships action
3339    #[serde(default, skip_serializing_if = "is_default_action_config")]
3340    pub relationships: AiActionPromptConfig,
3341
3342    /// Configuration for the improve description action
3343    #[serde(default, skip_serializing_if = "is_default_action_config")]
3344    pub improve: AiActionPromptConfig,
3345
3346    /// Configuration for the generate children action
3347    #[serde(default, skip_serializing_if = "is_default_action_config")]
3348    pub generate_children: AiActionPromptConfig,
3349
3350    /// Per-type customization
3351    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3352    pub type_prompts: Vec<AiTypePromptConfig>,
3353}
3354
3355/// Helper function for skip_serializing_if
3356fn is_default_action_config(config: &AiActionPromptConfig) -> bool {
3357    config.custom_template.is_none() && config.additional_instructions.is_empty()
3358}
3359
3360impl AiPromptConfig {
3361    /// Get type-specific extra instructions for evaluation
3362    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    /// Get type-specific extra instructions for improve
3371    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    /// Get type-specific extra instructions for generate children
3380    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/// Collection of all requirements
3390#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3391pub struct RequirementsStore {
3392    /// Database name (displayed in window title prefix)
3393    #[serde(default)]
3394    pub name: String,
3395
3396    /// Database title (one-liner, displayed in window title)
3397    #[serde(default)]
3398    pub title: String,
3399
3400    /// Database description (multi-line)
3401    #[serde(default)]
3402    pub description: String,
3403
3404    pub requirements: Vec<Requirement>,
3405
3406    /// Users in the system
3407    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3408    pub users: Vec<User>,
3409
3410    /// Teams in the system
3411    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3412    pub teams: Vec<Team>,
3413
3414    /// ID system configuration
3415    #[serde(default)]
3416    pub id_config: IdConfiguration,
3417
3418    /// Defined features with their prefixes
3419    #[serde(default)]
3420    pub features: Vec<FeatureDefinition>,
3421
3422    /// Counter for feature numbers (used when creating new features)
3423    #[serde(default = "default_next_feature_number")]
3424    pub next_feature_number: u32,
3425
3426    /// Global counter for requirement IDs (used with Global numbering strategy)
3427    #[serde(default = "default_next_spec_number")]
3428    pub next_spec_number: u32,
3429
3430    /// Per-prefix counters for requirement IDs (used with PerPrefix numbering)
3431    /// Key is the prefix (e.g., "FR", "AUTH"), value is the next number
3432    #[serde(default)]
3433    pub prefix_counters: std::collections::HashMap<String, u32>,
3434
3435    /// Relationship type definitions with constraints
3436    #[serde(default = "RelationshipDefinition::defaults")]
3437    pub relationship_definitions: Vec<RelationshipDefinition>,
3438
3439    /// Reaction definitions for comments
3440    #[serde(default = "default_reaction_definitions")]
3441    pub reaction_definitions: Vec<ReactionDefinition>,
3442
3443    /// Counter for meta-type IDs (users, views, etc.) - maps prefix to next number
3444    /// e.g., "$USER" -> 1 means next user will be $USER-001
3445    #[serde(default)]
3446    pub meta_counters: std::collections::HashMap<String, u32>,
3447
3448    /// Custom type definitions with their statuses and fields
3449    #[serde(default = "default_type_definitions")]
3450    pub type_definitions: Vec<CustomTypeDefinition>,
3451
3452    /// List of allowed/known ID prefixes for the project
3453    /// These are collected from usage and can be managed by admins
3454    #[serde(default)]
3455    pub allowed_prefixes: Vec<String>,
3456
3457    /// Whether to restrict prefix selection to only allowed_prefixes
3458    /// When false, users can enter any valid prefix (which gets added to allowed_prefixes)
3459    /// When true, users must select from the allowed_prefixes list
3460    #[serde(default)]
3461    pub restrict_prefixes: bool,
3462
3463    /// AI prompt configuration for customizing AI behavior
3464    #[serde(default, skip_serializing_if = "is_default_ai_prompt_config")]
3465    pub ai_prompts: AiPromptConfig,
3466
3467    /// Baselines - named snapshots of requirements at specific points in time
3468    #[serde(default, skip_serializing_if = "Vec::is_empty")]
3469    pub baselines: Vec<Baseline>,
3470
3471    /// Store version for detecting external modifications (SQLite only)
3472    /// Incremented on each save, used to detect if store was modified externally
3473    #[serde(skip)]
3474    pub store_version: i64,
3475
3476    /// Migration marker - if set, indicates this YAML was migrated to another format
3477    /// Contains the path to the migrated database (e.g., "requirements.db")
3478    /// When this is set, opening the YAML should warn/redirect to the migrated database
3479    #[serde(default, skip_serializing_if = "Option::is_none")]
3480    pub migrated_to: Option<String>,
3481
3482    /// Optional external dispenser for ID generation (distributed mode).
3483    /// When set, ID generation delegates to this dispenser instead of using
3484    /// the internal next_spec_number / prefix_counters fields.
3485    /// Skipped in serialization — the dispenser has its own persistence.
3486    #[serde(skip)]
3487    #[ts(skip)]
3488    pub dispenser: Option<DispenserHandle>,
3489}
3490
3491/// Wrapper for Arc<dyn Dispenser> that implements Debug and Clone.
3492/// This avoids requiring Debug on the Dispenser trait itself.
3493#[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
3509/// Helper function for skip_serializing_if on AiPromptConfig
3510fn 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
3520/// Default value for next_feature_number
3521fn default_next_feature_number() -> u32 {
3522    1
3523}
3524
3525/// Default value for next_spec_number
3526fn default_next_spec_number() -> u32 {
3527    1
3528}
3529
3530/// Meta-type prefixes for special object types
3531pub 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    /// Creates an empty requirements store
3538    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    /// Gets the type definition for a requirement type
3566    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    /// Gets the available statuses for a requirement type
3586    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    /// Gets the available priorities for a requirement type
3600    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    /// Gets the custom field definitions for a requirement type
3607    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    /// Checks if a requirement type is stateless (no status/priority tracking)
3621    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    // ========================================================================
3628    // Sprint Planning Methods
3629    // ========================================================================
3630
3631    /// Get all Sprints, sorted by sprint_number custom field (if available)
3632    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        // Sort by sprint_number if available, otherwise by created_at
3640        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    /// Get items assigned to a specific Sprint via sprint_assignment relationship
3658    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    /// Get the Sprint that a requirement is assigned to (if any)
3671    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    /// Get backlog items (requirements without Sprint assignment, excluding Sprints and Folders)
3680    pub fn get_backlog(&self) -> Vec<&Requirement> {
3681        self.requirements
3682            .iter()
3683            .filter(|r| {
3684                // Exclude Sprint, Folder, and Meta types
3685                r.req_type != RequirementType::Sprint
3686                    && r.req_type != RequirementType::Folder
3687                    && r.req_type != RequirementType::Meta
3688                    // Has no sprint_assignment relationship
3689                    && !r.relationships.iter().any(|rel| {
3690                        rel.rel_type == RelationshipType::Custom("sprint_assignment".to_string())
3691                    })
3692            })
3693            .collect()
3694    }
3695
3696    /// Assign a requirement to a Sprint
3697    /// This removes any existing sprint assignment and creates a new one
3698    pub fn assign_to_sprint(&mut self, req_id: Uuid, sprint_id: Uuid, username: &str) {
3699        // First, remove any existing sprint assignment
3700        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            // Add the new sprint assignment
3706            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            // Update modified timestamp
3714            req.modified_at = Utc::now();
3715
3716            // Add to history
3717            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        // Also add the inverse relationship (sprint_contains) to the sprint
3737        if let Some(sprint) = self.requirements.iter_mut().find(|r| r.id == sprint_id) {
3738            // Check if this relationship already exists
3739            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    /// Remove a requirement from its Sprint assignment
3754    pub fn remove_from_sprint(&mut self, req_id: Uuid, username: &str) {
3755        // Get the current sprint assignment for history
3756        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            // Remove from requirement
3772            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                // Add to history
3779                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            // Remove inverse relationship from sprint
3791            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    /// Gets all unique prefixes currently in use from requirements
3801    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                // Extract prefix from spec_id (e.g., "SEC-001" -> "SEC")
3807                if let Some(prefix) = spec_id.split('-').next() {
3808                    // Skip meta-type prefixes like $USER, $VIEW
3809                    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    /// Gets all allowed prefixes (combines allowed_prefixes with used prefixes)
3822    pub fn get_all_prefixes(&self) -> Vec<String> {
3823        let mut prefixes: std::collections::HashSet<String> = std::collections::HashSet::new();
3824
3825        // Add explicitly allowed prefixes
3826        for p in &self.allowed_prefixes {
3827            prefixes.insert(p.clone());
3828        }
3829
3830        // Add prefixes currently in use
3831        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    /// Adds a prefix to the allowed list if not already present
3841    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    /// Removes a prefix from the allowed list
3850    pub fn remove_allowed_prefix(&mut self, prefix: &str) {
3851        self.allowed_prefixes.retain(|p| p != prefix);
3852    }
3853
3854    /// Checks if a prefix is allowed (always true if restrict_prefixes is false)
3855    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    /// Generates the next meta-type ID for a given prefix (e.g., "$USER" -> "$USER-001")
3865    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    /// Adds a requirement to the store
3878    pub fn add_requirement(&mut self, req: Requirement) {
3879        self.requirements.push(req);
3880    }
3881
3882    /// Adds a user to the store (legacy - no spec_id)
3883    pub fn add_user(&mut self, user: User) {
3884        self.users.push(user);
3885    }
3886
3887    /// Adds a user with auto-generated $USER-XXX spec_id
3888    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    /// Finds a user by spec_id (e.g., "$USER-001")
3896    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    /// Finds a user by spec_id (mutable)
3903    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    /// Finds a user by UUID
3910    pub fn find_user_by_id(&self, id: &Uuid) -> Option<&User> {
3911        self.users.iter().find(|u| u.id == *id)
3912    }
3913
3914    /// Migrates existing users without spec_id to have $USER-XXX IDs
3915    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    /// Gets a mutable reference to a user by ID
3930    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    /// Removes a user by ID
3935    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    // ==================== Team Management ====================
3945
3946    /// Adds a team to the store (legacy - no spec_id)
3947    pub fn add_team(&mut self, team: Team) {
3948        self.teams.push(team);
3949    }
3950
3951    /// Adds a team with auto-generated $TEAM-XXX spec_id
3952    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    /// Finds a team by spec_id (e.g., "$TEAM-001")
3965    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    /// Finds a team by spec_id (mutable)
3972    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    /// Finds a team by UUID
3979    pub fn find_team_by_id(&self, id: &Uuid) -> Option<&Team> {
3980        self.teams.iter().find(|t| t.id == *id)
3981    }
3982
3983    /// Gets a mutable reference to a team by ID
3984    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    /// Removes a team by ID
3989    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    /// Gets child teams for a given parent team
3999    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    /// Gets top-level teams (teams without a parent)
4007    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    /// Gets all teams a user belongs to
4015    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    /// Checks if setting a parent team would create a circular reference
4023    pub fn would_create_team_cycle(&self, team_id: &Uuid, proposed_parent_id: &Uuid) -> bool {
4024        // If team_id equals proposed_parent_id, it's a direct cycle
4025        if team_id == proposed_parent_id {
4026            return true;
4027        }
4028
4029        // Walk up the parent chain from proposed_parent_id
4030        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    /// Migrates existing teams without spec_id to have $TEAM-XXX IDs
4041    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    // ==================== End Team Management ====================
4056
4057    /// Gets a requirement by ID
4058    pub fn get_requirement_by_id(&self, id: &Uuid) -> Option<&Requirement> {
4059        self.requirements.iter().find(|r| r.id == *id)
4060    }
4061
4062    /// Gets a mutable reference to a requirement by ID
4063    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    /// Gets the next feature number and increments the counter
4068    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    /// Formats a feature with number prefix
4075    pub fn format_feature_with_number(&self, feature_name: &str) -> String {
4076        format!("{}-{}", self.next_feature_number, feature_name)
4077    }
4078
4079    /// Gets all unique feature names
4080    pub fn get_feature_names(&self) -> Vec<String> {
4081        let mut feature_names = Vec::new();
4082
4083        for req in &self.requirements {
4084            // Skip feature if it's already in the list
4085            if feature_names.contains(&req.feature) {
4086                continue;
4087            }
4088
4089            feature_names.push(req.feature.clone());
4090        }
4091
4092        // Sort features by their prefix number if they have one
4093        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 both have prefix numbers, compare them numerically
4098            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            // Otherwise, lexicographical comparison
4107            a.cmp(b)
4108        });
4109
4110        feature_names
4111    }
4112
4113    /// Updates an existing feature name
4114    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    /// Migrate existing features to use numbered prefixes
4123    pub fn migrate_features(&mut self) {
4124        // First, collect all unique features
4125        let mut unique_features: Vec<String> = Vec::new();
4126
4127        for req in &self.requirements {
4128            // Skip if already has a number prefix (format: "1-Feature")
4129            if req.feature.contains('-') {
4130                if let Some((prefix, _)) = req.feature.split_once('-') {
4131                    if prefix.parse::<u32>().is_ok() {
4132                        continue; // Already has a number prefix
4133                    }
4134                }
4135            }
4136
4137            if !unique_features.contains(&req.feature) {
4138                unique_features.push(req.feature.clone());
4139            }
4140        }
4141
4142        // Assign numbers to each unique feature
4143        for feature in unique_features {
4144            let number = self.get_next_feature_number();
4145            let new_name = format!("{}-{}", number, feature);
4146
4147            // Update all requirements with this feature
4148            self.update_feature_name(&feature, &new_name);
4149        }
4150    }
4151
4152    /// Gets a requirement by SPEC-ID
4153    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    /// Gets a mutable reference to a requirement by SPEC-ID
4160    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    /// Assigns SPEC-IDs to requirements that don't have them
4167    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    /// Gets the next SPEC-ID that would be assigned
4177    pub fn peek_next_spec_id(&self) -> String {
4178        format!("SPEC-{:03}", self.next_spec_number)
4179    }
4180
4181    /// Validates that all SPEC-IDs are unique
4182    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    /// Repairs duplicate SPEC-IDs by assigning new unique IDs to duplicates
4198    /// Keeps the first occurrence of each SPEC-ID, reassigns duplicates
4199    /// Returns the number of duplicates that were repaired
4200    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        // First pass: find all duplicates (indices and their spec_id prefixes)
4206        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                    // This is a duplicate - extract the prefix
4210                    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        // Log duplicates found (for CLI output)
4221        eprintln!(
4222            "Found {} duplicate SPEC-ID(s), automatically repairing...",
4223            duplicates.len()
4224        );
4225
4226        // Second pass: assign new unique IDs to duplicates
4227        // We need to collect these changes because we're modifying self
4228        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        // Apply the repairs
4237        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                // Add the new ID to seen set so we don't create new duplicates
4243                seen.insert(new_id.clone());
4244            }
4245        }
4246
4247        repairs.len()
4248    }
4249
4250    /// Extract the prefix from a spec_id (e.g., "FR-0042" -> "FR", "AUTH-REQ-001" -> "AUTH-REQ")
4251    fn extract_prefix_from_spec_id(spec_id: &str) -> String {
4252        // Find the last '-' followed by digits
4253        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 everything before the last dash as the prefix
4257                return spec_id[..last_dash_pos].to_string();
4258            }
4259        }
4260        // Fallback: use the whole thing as-is or default to "REQ"
4261        if spec_id.is_empty() {
4262            "REQ".to_string()
4263        } else {
4264            spec_id.to_string()
4265        }
4266    }
4267
4268    /// Adds a requirement and assigns it a SPEC-ID (legacy method for backward compatibility)
4269    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    /// Migrates type definitions by adding any missing built-in types
4278    /// Returns true if any types were added
4279    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            // Only add built-in types that are missing
4285            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    /// Migrates id_config.requirement_types by adding any missing built-in types
4301    /// This ensures CLI type list shows all built-in types
4302    /// Returns true if any types were added
4303    // trace:FR-0309 | ai:claude:high
4304    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    // ========================================================================
4323    // New ID System Methods
4324    // ========================================================================
4325
4326    /// Add a new feature definition
4327    /// Returns error if the prefix is reserved or already in use
4328    pub fn add_feature(&mut self, name: &str, prefix: &str) -> anyhow::Result<FeatureDefinition> {
4329        let prefix_upper = prefix.to_uppercase();
4330
4331        // Check if prefix is reserved by a requirement type
4332        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        // Check if prefix is already used by another feature
4344        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    /// Get a feature by name
4358    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    /// Get a feature by prefix
4366    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    /// Get the next counter value for a given prefix.
4372    /// If a dispenser is set, delegates to it. Otherwise uses internal counters.
4373    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                // Fallback to internal counter if dispenser fails
4377                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    /// Get the next global sequence number.
4392    /// If a dispenser is set, delegates to it (using "GLOBAL" as the type key).
4393    /// Otherwise uses internal next_spec_number counter.
4394    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    /// Generate a new requirement ID based on configuration
4408    /// - feature_prefix: Optional feature prefix (e.g., "AUTH")
4409    /// - type_prefix: Optional type prefix (e.g., "FR")
4410    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                // Use either feature or type prefix, type takes precedence
4420                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 we have a dispenser in distributed mode, use its formatting
4433                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()); // GEN = General
4445                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                        // Per feature prefix only
4453                        self.get_next_counter_for_prefix(&feat)
4454                    }
4455                    NumberingStrategy::PerFeatureType => {
4456                        // Per feature+type combination
4457                        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    /// Add a requirement with the new ID system
4474    /// If spec_id is already set, uses that; otherwise generates one
4475    /// If prefix_override is set on the requirement, uses that prefix instead of feature/type
4476    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            // Check if requirement has a prefix override
4484            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    /// Generate a requirement ID using an explicit prefix override
4494    /// Uses SingleLevel format with the override prefix, respects numbering strategy
4495    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                // Treat the override prefix as its own counter
4503                self.get_next_counter_for_prefix(&prefix_upper)
4504            }
4505        };
4506
4507        // If we have a dispenser in distributed mode, use its formatting
4508        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    /// Get the type prefix for a RequirementType enum value
4523    /// Falls back to built-in defaults if the type is not in the database
4524    // trace:BUG-0308 | ai:claude:high
4525    pub fn get_type_prefix(&self, req_type: &RequirementType) -> Option<String> {
4526        // Map enum to type name and fallback prefix
4527        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        // Try database first, fall back to built-in prefix
4543        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    /// Generate a new spec_id for a requirement with a new prefix override
4550    /// Returns Ok(new_spec_id) if successful, Err if the new ID would conflict
4551    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        // Generate the new ID
4559        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        // Check if this ID is already taken by another requirement
4566        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    /// Check if a spec_id is available (not used by any requirement, or only by the given UUID)
4582    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    /// Update a requirement's spec_id when its type changes
4589    /// Replaces the type prefix portion while keeping the number
4590    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        // Parse the current spec_id to extract the number
4599        // Formats: "PREFIX-NNN" (SingleLevel) or "FEATURE-TYPE-NNN" (TwoLevel)
4600        let parts: Vec<&str> = spec_id.split('-').collect();
4601
4602        match self.id_config.format {
4603            IdFormat::SingleLevel => {
4604                // Format: PREFIX-NNN
4605                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                // Format: FEATURE-TYPE-NNN
4614                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    /// Migrate all existing SPEC-XXX IDs to the new format
4626    /// This will regenerate all IDs based on the current configuration
4627    /// Requirements with prefix_override will use their override prefix
4628    pub fn migrate_to_new_id_format(&mut self) {
4629        // Reset counters
4630        self.next_spec_number = 1;
4631        self.prefix_counters.clear();
4632
4633        // Clear all spec_ids first
4634        for req in &mut self.requirements {
4635            req.spec_id = None;
4636        }
4637
4638        // Collect data needed for ID generation (to avoid borrow issues)
4639        let req_data: Vec<(usize, Option<String>, Option<String>, Option<String>)> = self
4640            .requirements
4641            .iter()
4642            .enumerate()
4643            .map(|(i, req)| {
4644                // Check for prefix_override first
4645                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        // Now assign new IDs
4672        for (i, prefix_override, feature_prefix, type_prefix) in req_data {
4673            let new_id = if let Some(ref override_prefix) = prefix_override {
4674                // Use the override prefix
4675                self.generate_requirement_id_with_override(override_prefix)
4676            } else {
4677                // Use standard feature/type prefix logic
4678                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    /// Validate proposed changes to ID configuration
4685    /// Returns validation result with error/warning messages
4686    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        // Check if anything actually changed
4701        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        // Find the maximum number of digits currently in use
4711        let max_digits_in_use = self.get_max_digits_in_use();
4712
4713        // Validate digit reduction
4714        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        // Check format change constraints
4725        if format_changed {
4726            // For format changes, we require Global numbering for safe migration
4727            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            // Count affected requirements
4741            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            // For numbering/digit changes only, count affected
4755            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    /// Get the maximum number of digits currently used in requirement IDs
4773    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                // Extract the numeric portion from the ID
4779                // Formats: "PREFIX-NNN" or "FEATURE-TYPE-NNN"
4780                let parts: Vec<&str> = spec_id.split('-').collect();
4781                if let Some(last) = parts.last() {
4782                    // Check if it's numeric
4783                    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    /// Migrate requirement IDs to new format/numbering/digits configuration
4797    /// Returns the number of requirements migrated
4798    pub fn migrate_ids_to_config(
4799        &mut self,
4800        new_format: IdFormat,
4801        new_numbering: NumberingStrategy,
4802        new_digits: u8,
4803    ) -> usize {
4804        // Update the configuration first
4805        self.id_config.format = new_format;
4806        self.id_config.numbering = new_numbering;
4807        self.id_config.digits = new_digits;
4808
4809        // Reset counters for fresh numbering
4810        self.next_spec_number = 1;
4811        self.prefix_counters.clear();
4812
4813        // Collect requirement data for migration (to avoid borrow issues)
4814        let req_data: Vec<(usize, Option<String>, Option<String>, Option<String>)> = self
4815            .requirements
4816            .iter()
4817            .enumerate()
4818            .map(|(i, req)| {
4819                // Check for prefix_override first
4820                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        // Generate new IDs for all requirements
4849        for (i, prefix_override, feature_prefix, type_prefix) in req_data {
4850            let new_id = if let Some(ref override_prefix) = prefix_override {
4851                // Use the override prefix
4852                self.generate_requirement_id_with_override(override_prefix)
4853            } else {
4854                // Use standard feature/type prefix logic
4855                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    /// Add a new requirement type definition
4865    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        // Check if prefix is already used
4874        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        // Check if it conflicts with a feature prefix
4882        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    /// Add a relationship between two requirements
4897    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    /// Add a relationship between two requirements with optional creator info
4908    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        // Validate both requirements exist
4917        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        // Don't allow self-relationships
4925        if source_id == target_id {
4926            anyhow::bail!("Cannot create relationship to self");
4927        }
4928
4929        // Add the relationship to source
4930        let source_req = self
4931            .get_requirement_by_id_mut(source_id)
4932            .ok_or_else(|| anyhow::anyhow!("Source requirement not found"))?;
4933
4934        // Check if relationship already exists
4935        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        // Add inverse relationship if bidirectional and inverse exists
4956        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                // Only add if it doesn't already exist
4963                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    /// Set a unique relationship, removing any existing relationship of the same type first
4982    /// This is useful for Parent relationships where a requirement can only have one parent
4983    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    /// Set a unique relationship with creator info, removing any existing relationship of the same type first
4994    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        // Validate both requirements exist
5003        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        // Don't allow self-relationships
5011        if source_id == target_id {
5012            anyhow::bail!("Cannot create relationship to self");
5013        }
5014
5015        // Remove any existing relationships of this type from the source
5016        // For Parent relationships, this ensures a child can only have one parent
5017        {
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            // Find and remove existing relationships of this type
5023            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            // Remove inverse relationships from old targets
5033            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        // Now add the new relationship
5047        self.add_relationship_with_creator(
5048            source_id,
5049            rel_type,
5050            target_id,
5051            bidirectional,
5052            created_by,
5053        )
5054    }
5055
5056    /// Remove a relationship between two requirements
5057    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        // Remove relationship from source
5065        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        // Remove inverse relationship if bidirectional
5079        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    /// Get all relationships for a requirement
5093    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    /// Get all relationships of a specific type for a requirement
5105    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    // ========================================================================
5118    // Relationship Definition Management
5119    // ========================================================================
5120
5121    /// Get a relationship definition by name
5122    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    /// Get a relationship definition for a RelationshipType
5130    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    /// Get all relationship definitions
5138    pub fn get_relationship_definitions(&self) -> &[RelationshipDefinition] {
5139        &self.relationship_definitions
5140    }
5141
5142    /// Add a new relationship definition
5143    pub fn add_relationship_definition(
5144        &mut self,
5145        definition: RelationshipDefinition,
5146    ) -> anyhow::Result<()> {
5147        let name_lower = definition.name.to_lowercase();
5148
5149        // Check if name already exists
5150        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 it has an inverse, verify the inverse exists or will be created
5159        if let Some(ref inverse) = definition.inverse {
5160            let inverse_lower = inverse.to_lowercase();
5161            // Only warn if the inverse doesn't exist - it might be added later
5162            if !self
5163                .relationship_definitions
5164                .iter()
5165                .any(|d| d.name == inverse_lower)
5166            {
5167                // This is okay - the inverse might be defined later
5168            }
5169        }
5170
5171        self.relationship_definitions.push(RelationshipDefinition {
5172            name: name_lower,
5173            ..definition
5174        });
5175        Ok(())
5176    }
5177
5178    /// Update an existing relationship definition
5179    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        // Can't change built_in status
5193        if def.built_in {
5194            // Allow updates to non-critical fields for built-ins
5195            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            // Don't change: name, inverse, symmetric, cardinality, built_in
5202        } else {
5203            *def = RelationshipDefinition {
5204                name: name_lower,
5205                built_in: false,
5206                ..definition
5207            };
5208        }
5209
5210        Ok(())
5211    }
5212
5213    /// Remove a relationship definition (only non-built-in)
5214    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    /// Ensure built-in relationship definitions exist (call after loading)
5236    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    /// Validate a proposed relationship
5250    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        // Check self-reference
5259        if source_id == target_id {
5260            return RelationshipValidation::error("Cannot create relationship to self");
5261        }
5262
5263        // Get source and target requirements
5264        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        // Check if relationship already exists
5274        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        // Get the relationship definition
5286        let definition = match self.get_definition_for_type(rel_type) {
5287            Some(d) => d,
5288            None => {
5289                // Custom relationship without definition - allow but warn
5290                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        // Check source type constraint
5299        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        // Check target type constraint
5307        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        // Check cardinality constraints
5315        match definition.cardinality {
5316            Cardinality::OneToOne => {
5317                // Source can only have one outgoing relationship of this type
5318                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                // Target can only have one incoming relationship of this type
5330                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                // Source can only have one outgoing relationship of this type
5346                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                // Target can only have one incoming relationship of this type
5360                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                // No cardinality constraints
5376            }
5377        }
5378
5379        // Check for cycles in hierarchical relationships (parent/child)
5380        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    /// Check if adding a relationship would create a cycle
5390    fn would_create_cycle(
5391        &self,
5392        source_id: &Uuid,
5393        target_id: &Uuid,
5394        rel_type: &RelationshipType,
5395    ) -> bool {
5396        // For parent relationships, check if target is already an ancestor of source
5397        // For child relationships, check if target is already a descendant of source
5398        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; // Found a cycle
5412            }
5413            if visited.contains(&current) {
5414                continue;
5415            }
5416            visited.insert(current);
5417
5418            // Follow the relationship chain
5419            if let Some(req) = self.get_requirement_by_id(&current) {
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    /// Get the inverse relationship type from definitions
5432    pub fn get_inverse_type(&self, rel_type: &RelationshipType) -> Option<RelationshipType> {
5433        // First check built-in inverse
5434        if let Some(inverse) = rel_type.inverse() {
5435            return Some(inverse);
5436        }
5437
5438        // Then check definition
5439        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    // =========================================================================
5452    // Baseline Operations
5453    // =========================================================================
5454
5455    /// Creates a new baseline from current requirements
5456    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    /// Gets a baseline by ID
5468    pub fn get_baseline(&self, id: &Uuid) -> Option<&Baseline> {
5469        self.baselines.iter().find(|b| &b.id == id)
5470    }
5471
5472    /// Gets a baseline by name
5473    pub fn get_baseline_by_name(&self, name: &str) -> Option<&Baseline> {
5474        self.baselines.iter().find(|b| b.name == name)
5475    }
5476
5477    /// Deletes a baseline by ID (if not locked)
5478    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    /// Compares current requirements against a baseline
5489    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    /// Compares two baselines
5495    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    /// Helper: compare snapshots to current requirements
5509    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        // Find added (in current but not in baseline)
5528        for id in current_map.keys() {
5529            if !snapshot_map.contains_key(id) {
5530                comparison.added.push(*id);
5531            }
5532        }
5533
5534        // Find removed (in baseline but not in current)
5535        for id in snapshot_map.keys() {
5536            if !current_map.contains_key(id) {
5537                comparison.removed.push(*id);
5538            }
5539        }
5540
5541        // Find modified and unchanged
5542        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    /// Helper: compare two sets of snapshots
5561    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        // Find added (in target but not in source)
5576        for id in target_map.keys() {
5577            if !source_map.contains_key(id) {
5578                comparison.added.push(*id);
5579            }
5580        }
5581
5582        // Find removed (in source but not in target)
5583        for id in source_map.keys() {
5584            if !target_map.contains_key(id) {
5585                comparison.removed.push(*id);
5586            }
5587        }
5588
5589        // Find modified and unchanged
5590        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    /// Helper: diff a snapshot against current requirement
5609    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    /// Helper: diff two snapshots
5669    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        // Manually add without SPEC-IDs
5776        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; // Start at 2 since SPEC-001 exists
5800
5801        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()); // Duplicate!
5828
5829        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()); // Duplicate!
5848        let mut req3 = Requirement::new("R3".into(), "D3".into());
5849        req3.spec_id = Some("FR-0002".into()); // Unique
5850
5851        store.requirements.push(req1);
5852        store.requirements.push(req2);
5853        store.requirements.push(req3);
5854
5855        // Verify duplicates exist
5856        assert!(store.validate_unique_spec_ids().is_err());
5857
5858        // Repair duplicates
5859        let repaired = store.repair_duplicate_spec_ids();
5860        assert_eq!(repaired, 1);
5861
5862        // Verify no more duplicates
5863        assert!(store.validate_unique_spec_ids().is_ok());
5864
5865        // First requirement should keep its original ID
5866        assert_eq!(store.requirements[0].spec_id.as_deref(), Some("FR-0001"));
5867
5868        // Second requirement should have a new ID with same prefix
5869        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        // Third requirement should be unchanged
5874        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        // Edge cases
5896        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        // Add parent relationship
5926        let result = store.add_relationship(&id1, RelationshipType::Parent, &id2, false);
5927        assert!(result.is_ok());
5928
5929        // Verify relationship was added
5930        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        // Add bidirectional parent-child relationship
5952        let result = store.add_relationship(&id1, RelationshipType::Parent, &id2, true);
5953        assert!(result.is_ok());
5954
5955        // Verify forward relationship
5956        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        // Verify inverse relationship
5964        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        // Try to add self-relationship
5982        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        // Add relationship
6003        store
6004            .add_relationship(&id1, RelationshipType::Parent, &id2, false)
6005            .unwrap();
6006
6007        // Try to add duplicate
6008        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        // Remove relationship
6029        let result = store.remove_relationship(&id1, &RelationshipType::Parent, &id2, false);
6030        assert!(result.is_ok());
6031
6032        // Verify it was removed
6033        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        // Remove bidirectional relationship
6053        let result = store.remove_relationship(&id1, &RelationshipType::Parent, &id2, true);
6054        assert!(result.is_ok());
6055
6056        // Verify both sides were removed
6057        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        // Test custom type
6089        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// =========================================================================
6124// Queue Entry (STORY-0366)
6125// =========================================================================
6126
6127/// Represents an entry in a user's personal work queue
6128// trace:STORY-0366 | ai:claude
6129#[derive(Debug, Clone, Serialize, Deserialize)]
6130pub struct QueueEntry {
6131    /// The user whose queue this entry belongs to
6132    pub user_id: String,
6133    /// The requirement in the queue
6134    pub requirement_id: Uuid,
6135    /// Position in the queue (lower = higher priority)
6136    pub position: i64,
6137    /// Who added this entry (may differ from user_id for assigned items)
6138    pub added_by: String,
6139    /// Optional note explaining why this was queued
6140    #[serde(skip_serializing_if = "Option::is_none")]
6141    pub note: Option<String>,
6142    /// When the entry was added
6143    pub added_at: DateTime<Utc>,
6144}