requiem/domain/
tree.rs

1//! New in-memory tree structure for requirements with decomposed storage
2//!
3//! The [`Tree`] knows nothing about the filesystem or the directory structure.
4//! It stores requirements in a decomposed format for better maintainability and
5//! performance.
6
7use std::{
8    collections::{BTreeMap, HashMap},
9    num::NonZeroUsize,
10};
11
12use petgraph::graphmap::DiGraphMap;
13use tracing::instrument;
14use uuid::Uuid;
15
16use crate::{
17    domain::{
18        hrid::KindString,
19        requirement::{LoadError, Parent},
20        requirement_data::RequirementData,
21        requirement_view::RequirementView,
22        Hrid,
23    },
24    Requirement,
25};
26
27/// Data stored on each edge in the dependency graph.
28///
29/// Each edge represents a parent-child relationship, pointing from child to
30/// parent. The edge stores the parent's HRID and the expected fingerprint for
31/// change detection.
32#[derive(Debug, Clone, PartialEq, Eq)]
33struct EdgeData {
34    /// The HRID of the parent requirement at the time the link was created.
35    /// This can become stale if the parent's HRID is changed.
36    parent_hrid: Hrid,
37
38    /// The fingerprint of the parent requirement at the time the link was
39    /// created. Used to detect if the parent has been modified since the
40    /// link was established.
41    fingerprint: String,
42}
43
44/// An in-memory representation of the set of requirements with decomposed
45/// storage.
46///
47/// Requirements are stored as separate components:
48/// - Content data: `HashMap<Uuid, RequirementData>`
49/// - HRIDs: `HashMap<Uuid, Hrid>` (separate to allow O(1) lookup)
50/// - HRID lookup: `BTreeMap<Hrid, Uuid>`
51/// - Relationships: `DiGraphMap<Uuid, EdgeData>` (edges are child→parent,
52///   `EdgeData` contains parent info)
53#[derive(Debug)]
54pub struct Tree {
55    /// Requirements data, keyed by UUID.
56    requirements: HashMap<Uuid, RequirementData>,
57
58    /// HRID for each requirement, keyed by UUID.
59    /// Stored separately from `RequirementData` for efficient updates.
60    hrids: HashMap<Uuid, Hrid>,
61
62    /// Forward lookup map from HRID to UUID.
63    /// `BTreeMap` for Hrid range lookups.
64    hrid_to_uuid: BTreeMap<Hrid, Uuid>,
65
66    /// Dependency graph. Nodes are UUIDs, edges point from child to parent.
67    /// Edge data contains parent HRID and fingerprint for change detection.
68    /// This is the sole source of truth for parent relationships.
69    graph: DiGraphMap<Uuid, EdgeData>,
70}
71
72/// Result of linking two requirements together.
73#[derive(Debug)]
74pub struct LinkOutcome {
75    /// UUID of the child requirement.
76    pub child_uuid: Uuid,
77    /// HRID of the child requirement.
78    pub child_hrid: Hrid,
79    /// UUID of the parent requirement.
80    pub parent_uuid: Uuid,
81    /// HRID of the parent requirement.
82    pub parent_hrid: Hrid,
83    /// Whether the relationship already existed prior to linking.
84    pub already_linked: bool,
85}
86
87impl Default for Tree {
88    fn default() -> Self {
89        Self {
90            requirements: HashMap::new(),
91            hrids: HashMap::new(),
92            hrid_to_uuid: BTreeMap::new(),
93            graph: DiGraphMap::new(),
94        }
95    }
96}
97
98impl Tree {
99    /// Creates a new tree with pre-allocated capacity for the given number of
100    /// requirements.
101    #[must_use]
102    pub fn with_capacity(capacity: usize) -> Self {
103        Self {
104            requirements: HashMap::with_capacity(capacity),
105            hrids: HashMap::with_capacity(capacity),
106            hrid_to_uuid: BTreeMap::new(),
107            graph: DiGraphMap::with_capacity(capacity, capacity * 2),
108        }
109    }
110
111    /// Inserts a requirement into the tree.
112    ///
113    /// # Panics
114    ///
115    /// Panics if a requirement with the same UUID already exists.
116    pub fn insert(&mut self, requirement: Requirement) {
117        let uuid = requirement.metadata.uuid;
118        assert!(
119            !self.requirements.contains_key(&uuid),
120            "Duplicate requirement UUID: {uuid}"
121        );
122
123        let hrid = requirement.metadata.hrid.clone();
124
125        // Add node to graph (if it doesn't already exist from being referenced as a
126        // parent)
127        self.graph.add_node(uuid);
128
129        // Add edges for parent relationships
130        // Note: add_edge() will automatically create parent nodes if they don't exist
131        // yet
132        for (parent_uuid, parent_info) in &requirement.metadata.parents {
133            let edge_data = EdgeData {
134                parent_hrid: parent_info.hrid.clone(),
135                fingerprint: parent_info.fingerprint.clone(),
136            };
137            self.graph.add_edge(uuid, *parent_uuid, edge_data);
138        }
139
140        // Store HRID
141        self.hrids.insert(uuid, hrid.clone());
142        self.hrid_to_uuid.insert(hrid, uuid);
143
144        // Store decomposed data
145        let data = RequirementData::from(requirement);
146        self.requirements.insert(uuid, data);
147    }
148
149    /// Retrieves just the HRID for a requirement by UUID.
150    ///
151    /// This is more efficient than `requirement()` when only the HRID is
152    /// needed.
153    #[must_use]
154    pub fn hrid(&self, uuid: Uuid) -> Option<&Hrid> {
155        self.hrids.get(&uuid)
156    }
157
158    /// Retrieves a requirement by UUID as an owned Requirement.
159    ///
160    /// This is more efficient than calling `requirement().to_requirement()`
161    /// when you need an owned Requirement, as it avoids creating the
162    /// intermediate view.
163    #[must_use]
164    pub fn get_requirement(&self, uuid: Uuid) -> Option<Requirement> {
165        use std::collections::HashMap;
166
167        let data = self.requirements.get(&uuid)?;
168        let hrid = self.hrids.get(&uuid)?;
169
170        // Reconstruct parents from graph edges
171        let parents: HashMap<Uuid, Parent> = self
172            .graph
173            .edges(uuid)
174            .map(|(_, parent_uuid, edge_data)| {
175                (
176                    parent_uuid,
177                    Parent {
178                        hrid: edge_data.parent_hrid.clone(),
179                        fingerprint: edge_data.fingerprint.clone(),
180                    },
181                )
182            })
183            .collect();
184
185        Some(Requirement {
186            content: crate::domain::requirement::Content {
187                title: data.title.clone(),
188                body: data.body.clone(),
189                tags: data.tags.clone(),
190            },
191            metadata: crate::domain::requirement::Metadata {
192                uuid,
193                hrid: hrid.clone(),
194                created: data.created,
195                parents,
196            },
197        })
198    }
199
200    /// Retrieves a requirement by UUID as a borrowed view.
201    ///
202    /// Note: Since UUID is passed by value, we need to find a way to get a
203    /// reference to it. The UUID is stored as a key in the requirements
204    /// `HashMap`.
205    #[must_use]
206    pub fn requirement(&self, uuid: Uuid) -> Option<RequirementView<'_>> {
207        let data = self.requirements.get(&uuid)?;
208        let hrid = self.hrids.get(&uuid)?;
209
210        // Reconstruct parent data from graph edges
211        // Since RequirementView owns the parent data, we can collect directly into Vec
212        let parents: Vec<(Uuid, Parent)> = self
213            .graph
214            .edges(uuid)
215            .map(|(_, parent_uuid, edge_data)| {
216                (
217                    parent_uuid,
218                    Parent {
219                        hrid: edge_data.parent_hrid.clone(),
220                        fingerprint: edge_data.fingerprint.clone(),
221                    },
222                )
223            })
224            .collect();
225
226        // Get a reference to the UUID from the requirements HashMap key
227        // This is safe because we know it exists (we just got data from it)
228        let uuid_ref = self.requirements.get_key_value(&uuid)?.0;
229
230        Some(RequirementView {
231            uuid: uuid_ref,
232            hrid,
233            created: &data.created,
234            title: &data.title,
235            body: &data.body,
236            tags: &data.tags,
237            parents,
238        })
239    }
240
241    /// Returns the next available index for a requirement of the given kind.
242    ///
243    /// This method uses a range query on the `hrid_to_uuid` `BTreeMap` to find
244    /// the maximum ID for the given kind. Time complexity is O(log n) where n
245    /// is the total number of requirements.
246    ///
247    /// The input `kind` will be normalized to uppercase.
248    ///
249    /// # Panics
250    ///
251    /// Panics if the provided kind is invalid (empty or contains non-alphabetic
252    /// characters).
253    #[must_use]
254    pub fn next_index(&self, kind: &KindString) -> NonZeroUsize {
255        // Construct range bounds for this kind
256        // Start: kind with ID 1 (MIN), End: kind with ID MAX
257        let start =
258            crate::domain::Hrid::new_with_namespace(Vec::new(), kind.clone(), NonZeroUsize::MIN);
259        let end =
260            crate::domain::Hrid::new_with_namespace(Vec::new(), kind.clone(), NonZeroUsize::MAX);
261
262        // Use range query to find all HRIDs of this kind, then get the last one
263        self.hrid_to_uuid
264            .range(start..=end)
265            .next_back()
266            .map_or(NonZeroUsize::MIN, |(hrid, _)| {
267                hrid.id().checked_add(1).expect("requirement ID overflow!")
268            })
269    }
270
271    /// Returns an iterator over all requirements in the tree as borrowed views.
272    pub fn iter(&self) -> impl Iterator<Item = RequirementView<'_>> + '_ {
273        self.requirements.iter().filter_map(move |(uuid, data)| {
274            let hrid = self.hrids.get(uuid)?;
275
276            let parents: Vec<(Uuid, Parent)> = self
277                .graph
278                .edges(*uuid)
279                .map(|(_, parent_uuid, edge_data)| {
280                    (
281                        parent_uuid,
282                        Parent {
283                            hrid: edge_data.parent_hrid.clone(),
284                            fingerprint: edge_data.fingerprint.clone(),
285                        },
286                    )
287                })
288                .collect();
289
290            Some(RequirementView {
291                uuid,
292                hrid,
293                created: &data.created,
294                title: &data.title,
295                body: &data.body,
296                tags: &data.tags,
297                parents,
298            })
299        })
300    }
301
302    /// Finds a requirement by its human-readable identifier.
303    #[must_use]
304    pub fn find_by_hrid(&self, hrid: &Hrid) -> Option<RequirementView<'_>> {
305        let uuid = self.hrid_to_uuid.get(hrid)?;
306        self.requirement(*uuid)
307    }
308
309    /// Link two requirements identified by their HRIDs.
310    ///
311    /// # Errors
312    ///
313    /// Returns [`LoadError::NotFound`] when either HRID does not exist in the
314    /// tree.
315    pub fn link_requirement(
316        &mut self,
317        child: &Hrid,
318        parent: &Hrid,
319    ) -> Result<LinkOutcome, LoadError> {
320        let (child_uuid, child_hrid) = {
321            let view = self.find_by_hrid(child).ok_or(LoadError::NotFound)?;
322            (*view.uuid, view.hrid.clone())
323        };
324
325        let (parent_uuid, parent_hrid, parent_fingerprint) = {
326            let view = self.find_by_hrid(parent).ok_or(LoadError::NotFound)?;
327            (*view.uuid, view.hrid.clone(), view.fingerprint())
328        };
329
330        let already_linked = self
331            .parents(child_uuid)
332            .into_iter()
333            .any(|(uuid, _)| uuid == parent_uuid);
334
335        self.upsert_parent_link(child_uuid, parent_uuid, parent_fingerprint);
336
337        Ok(LinkOutcome {
338            child_uuid,
339            child_hrid,
340            parent_uuid,
341            parent_hrid,
342            already_linked,
343        })
344    }
345
346    /// Get all children of a requirement.
347    #[must_use]
348    pub fn children(&self, uuid: Uuid) -> Vec<Uuid> {
349        if !self.graph.contains_node(uuid) {
350            return Vec::new();
351        }
352
353        // Incoming edges are from children
354        self.graph
355            .neighbors_directed(uuid, petgraph::Direction::Incoming)
356            .collect()
357    }
358
359    /// Get all parents of a requirement with their fingerprints.
360    #[must_use]
361    pub fn parents(&self, uuid: Uuid) -> Vec<(Uuid, String)> {
362        if !self.graph.contains_node(uuid) {
363            return Vec::new();
364        }
365
366        // Outgoing edges are to parents
367        self.graph
368            .edges(uuid)
369            .map(|(_, parent_uuid, edge_data)| (parent_uuid, edge_data.fingerprint.clone()))
370            .collect()
371    }
372
373    /// Insert or update a parent link for the given child UUID.
374    ///
375    /// Returns `true` if an existing link was replaced, or `false` if a new
376    /// link was created.
377    ///
378    /// # Panics
379    ///
380    /// Panics if either the child or parent UUID does not exist in the tree.
381    pub fn upsert_parent_link(
382        &mut self,
383        child_uuid: Uuid,
384        parent_uuid: Uuid,
385        fingerprint: String,
386    ) -> bool {
387        assert!(
388            self.requirements.contains_key(&child_uuid),
389            "Child requirement {child_uuid} not found in tree"
390        );
391        assert!(
392            self.requirements.contains_key(&parent_uuid),
393            "Parent requirement {parent_uuid} not found in tree"
394        );
395
396        // Ensure both nodes exist in the graph; GraphMap::add_node is idempotent.
397        self.graph.add_node(child_uuid);
398        self.graph.add_node(parent_uuid);
399
400        let parent_hrid = self
401            .hrids
402            .get(&parent_uuid)
403            .unwrap_or_else(|| panic!("Parent HRID for {parent_uuid} not found"));
404
405        let edge = EdgeData {
406            parent_hrid: parent_hrid.clone(),
407            fingerprint,
408        };
409
410        self.graph.add_edge(child_uuid, parent_uuid, edge).is_some()
411    }
412
413    /// Read all the requirements and update any incorrect parent HRIDs.
414    /// Returns an iterator of UUIDs whose parents were updated.
415    ///
416    /// # Panics
417    ///
418    /// Panics if a requirement references a parent UUID that doesn't exist in
419    /// the tree, or if a requirement is its own parent.
420    #[instrument(skip(self))]
421    pub fn update_hrids(&mut self) -> impl Iterator<Item = Uuid> + '_ {
422        use std::collections::HashSet;
423
424        let mut updated_uuids = HashSet::new();
425
426        // Collect all edges that need updating (store edge identifiers, not cloned
427        // HRIDs)
428        let mut edges_to_update = Vec::new();
429
430        for child_uuid in self.graph.nodes() {
431            for (_, parent_uuid, edge_data) in self.graph.edges(child_uuid) {
432                // Get the current HRID of the parent
433                let current_parent_hrid = self
434                    .hrids
435                    .get(&parent_uuid)
436                    .unwrap_or_else(|| panic!("Parent requirement {parent_uuid} not found!"));
437
438                // Check if the stored HRID is outdated
439                if &edge_data.parent_hrid != current_parent_hrid {
440                    edges_to_update.push((child_uuid, parent_uuid));
441                    updated_uuids.insert(child_uuid);
442                }
443            }
444        }
445
446        // Apply the updates to EdgeData only
447        for (child_uuid, parent_uuid) in edges_to_update {
448            // Look up the HRID again (HashMap lookup is O(1) and cheaper than cloning
449            // earlier)
450            let current_parent_hrid = self.hrids.get(&parent_uuid).unwrap();
451            if let Some(edge_data) = self.graph.edge_weight_mut(child_uuid, parent_uuid) {
452                // Update EdgeData (the sole source of truth)
453                edge_data.parent_hrid = current_parent_hrid.clone();
454            }
455        }
456
457        updated_uuids.into_iter()
458    }
459
460    /// Find all suspect links in the requirement graph.
461    ///
462    /// A link is suspect when the fingerprint stored in the edge data
463    /// does not match the current fingerprint of the parent requirement.
464    ///
465    /// # Panics
466    ///
467    /// Panics if a child UUID in the graph doesn't have a corresponding HRID.
468    #[must_use]
469    pub fn suspect_links(&self) -> Vec<SuspectLink> {
470        use crate::domain::requirement::ContentRef;
471
472        let mut suspect = Vec::new();
473
474        for child_uuid in self.graph.nodes() {
475            let child_hrid = self.hrids.get(&child_uuid).unwrap();
476
477            for (_, parent_uuid, edge_data) in self.graph.edges(child_uuid) {
478                // Access RequirementData directly to avoid full RequirementView construction
479                let Some(parent_data) = self.requirements.get(&parent_uuid) else {
480                    continue;
481                };
482
483                // Calculate fingerprint directly from RequirementData
484                let current_fingerprint = ContentRef {
485                    title: &parent_data.title,
486                    body: &parent_data.body,
487                    tags: &parent_data.tags,
488                }
489                .fingerprint();
490
491                if edge_data.fingerprint != current_fingerprint {
492                    suspect.push(SuspectLink {
493                        child_uuid,
494                        child_hrid: child_hrid.clone(),
495                        parent_uuid,
496                        parent_hrid: edge_data.parent_hrid.clone(),
497                        stored_fingerprint: edge_data.fingerprint.clone(),
498                        current_fingerprint,
499                    });
500                }
501            }
502        }
503
504        suspect
505    }
506
507    /// Update the fingerprint for a specific parent link.
508    ///
509    /// Returns `true` if the fingerprint was updated.
510    ///
511    /// # Panics
512    ///
513    /// Panics if the child or parent requirement is not found.
514    pub fn accept_suspect_link(&mut self, child_uuid: Uuid, parent_uuid: Uuid) -> bool {
515        let parent = self
516            .requirement(parent_uuid)
517            .unwrap_or_else(|| panic!("Parent requirement {parent_uuid} not found!"));
518        let current_fingerprint = parent.fingerprint();
519
520        // Check if child and parent exist in graph
521        assert!(
522            self.graph.contains_node(child_uuid),
523            "Child requirement {child_uuid} not found!"
524        );
525        assert!(
526            self.graph.contains_node(parent_uuid),
527            "Parent requirement {parent_uuid} not found!"
528        );
529
530        // Find and update the edge
531        if let Some(edge_data) = self.graph.edge_weight_mut(child_uuid, parent_uuid) {
532            if edge_data.fingerprint == current_fingerprint {
533                return false; // Already up to date
534            }
535
536            // Update EdgeData (the sole source of truth)
537            edge_data.fingerprint.clone_from(&current_fingerprint);
538
539            true
540        } else {
541            panic!("Parent link {parent_uuid} not found in child {child_uuid}");
542        }
543    }
544
545    /// Update all suspect fingerprints in the tree.
546    pub fn accept_all_suspect_links(&mut self) -> Vec<(Uuid, Uuid)> {
547        let suspect = self.suspect_links();
548        let mut updated = Vec::new();
549
550        for link in suspect {
551            if self.accept_suspect_link(link.child_uuid, link.parent_uuid) {
552                updated.push((link.child_uuid, link.parent_uuid));
553            }
554        }
555
556        updated
557    }
558}
559
560/// A suspect link in the requirement graph.
561#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct SuspectLink {
563    /// The UUID of the child requirement.
564    pub child_uuid: Uuid,
565    /// The HRID of the child requirement.
566    pub child_hrid: Hrid,
567    /// The UUID of the parent requirement.
568    pub parent_uuid: Uuid,
569    /// The HRID of the parent requirement.
570    pub parent_hrid: Hrid,
571    /// The fingerprint stored in the child's parent reference.
572    pub stored_fingerprint: String,
573    /// The current fingerprint of the parent requirement.
574    pub current_fingerprint: String,
575}