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(¤t_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}