eidetica/entry/mod.rs
1//!
2//! Defines the fundamental data unit (`Entry`) and related types.
3//!
4//! An `Entry` is the core, content-addressable building block of the database,
5//! representing a snapshot of data in the main tree and potentially multiple named subtrees.
6//! This module also defines the `ID` type and `RawData` type.
7
8mod builder;
9pub mod errors;
10pub mod id;
11
12#[cfg(test)]
13mod tests;
14
15use std::sync::OnceLock;
16
17use serde::{Deserialize, Serialize};
18
19pub use builder::EntryBuilder;
20pub use errors::EntryError;
21pub use id::ID;
22
23use crate::{Result, auth::types::AuthInfo, constants::ROOT, store::StoreError};
24
25use id::IdError;
26
27/// Opaque payload bytes embedded in an `Entry`.
28///
29/// Each `Store` owns the format of its own payload (JSON, CBOR, raw binary, etc.);
30/// the entry layer treats `RawData` as a byte string and does not interpret it.
31///
32/// Encoded as a CBOR byte string (major type 2) in DAG-CBOR for IPLD compatibility.
33pub type RawData = Vec<u8>;
34
35/// Helper to check if tree height is zero for serde skip_serializing_if
36fn is_zero(h: &u64) -> bool {
37 *h == 0
38}
39
40/// Internal representation of the main tree node within an `Entry`.
41#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
42pub(super) struct TreeNode {
43 /// The ID of the root `Entry` of the tree this node belongs to.
44 /// `None` for root entries (they are their own root).
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub root: Option<ID>,
47 /// IDs of the parent `Entry`s in the main tree history.
48 /// The vector is kept sorted in CID order (`ID`'s `Ord`, not lexicographic string order).
49 pub parents: Vec<ID>,
50 /// Serialized metadata associated with this `Entry` in the main tree.
51 /// This data is metadata about this specific entry only and is not merged with other entries.
52 ///
53 /// Metadata is used to improve the efficiency of certain operations and for experimentation.
54 ///
55 /// Metadata is optional and may not be present in all entries. Future versions
56 /// may extend metadata to include additional information.
57 #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")]
58 pub metadata: Option<RawData>,
59 /// Height of this entry in the tree DAG (longest path from root).
60 /// Root entries have height 0, children have max(parent heights) + 1.
61 #[serde(rename = "h", default, skip_serializing_if = "is_zero")]
62 pub height: u64,
63}
64
65/// Internal representation of a named subtree node within an `Entry`.
66#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
67pub(super) struct SubTreeNode {
68 /// The name of the subtree, analogous to a table name.
69 /// Subtrees are _named_, and not identified by an ID.
70 pub name: String,
71 /// IDs of the parent `Entry`s specific to this subtree's history.
72 /// The vector is kept sorted in CID order (`ID`'s `Ord`, not lexicographic string order).
73 pub parents: Vec<ID>,
74 /// Serialized data specific to this `Entry` within this named subtree.
75 ///
76 /// `None` indicates that this Entry participates in the subtree but makes no data changes.
77 /// This is used when there is information needed for this subtree found somewhere else (e.g. the `_index`)
78 ///
79 /// `Some(data)` contains the actual serialized data for this subtree.
80 #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")]
81 pub data: Option<RawData>,
82 /// Height of this entry in the subtree DAG.
83 ///
84 /// `None` means the subtree inherits the tree's height (not serialized).
85 /// `Some(h)` is an independent height for subtrees with their own strategy.
86 #[serde(rename = "h", default, skip_serializing_if = "Option::is_none")]
87 pub height: Option<u64>,
88}
89
90/// The fundamental unit of data in Eidetica, representing a finalized, immutable Database Entry.
91///
92/// An `Entry` represents a snapshot of data within a `Database` and potentially one or more named `Store`s.
93/// It is content-addressable, meaning its `ID` is a cryptographic hash of its contents.
94/// Entries form a Merkle-DAG (Directed Acyclic Graph) structure through parent references.
95///
96/// # Authentication
97///
98/// Each entry contains authentication information (`auth`, an [`AuthInfo`]) with:
99/// - `signature`: Base64-encoded cryptographic signature (optional, allows unsigned entry creation)
100/// - `key`: Authentication key reference path, either:
101/// - A direct key ID defined in this tree's `_settings.auth`
102/// - A delegation path as an ordered list of `{"key": "delegated_tree_1", "tips": ["A", "B"]}`
103/// where the last element must contain only a `"key"` field
104///
105/// # Immutability
106///
107/// `Entry` instances are designed to be immutable once created. To create or modify entries,
108/// use the `EntryBuilder` struct, which provides a mutable API for constructing entries.
109/// Once an entry is built, its content cannot be changed, and its ID is deterministic
110/// based on its content.
111///
112/// # Example
113///
114/// ```
115/// # use eidetica::Entry;
116///
117/// // Create a new root entry (standalone entry that starts a new DAG)
118/// let entry = Entry::root_builder()
119/// .set_subtree_data("users", br#"{"user1":"data"}"#.to_vec())
120/// .build()
121/// .expect("Entry should build successfully");
122///
123/// // Access entry data
124/// let id = entry.id(); // Calculate content-addressable ID
125/// let user_data = entry.data("users").unwrap();
126/// ```
127///
128/// # Builders
129///
130/// To create an `Entry`, use the associated `EntryBuilder`.
131/// The preferred way to get an `EntryBuilder` is via the static methods
132/// `Entry::builder()` for regular entries or `Entry::root_builder()` for new top-level tree roots.
133///
134/// ```
135/// # use eidetica::entry::{Entry, ID, RawData};
136/// # let root_id = ID::from_bytes("some_root_id");
137/// # let data: RawData = b"{}".to_vec();
138/// // For a regular entry:
139/// let builder = Entry::builder(root_id);
140///
141/// // For a new top-level tree root:
142/// let root_builder = Entry::root_builder();
143/// ```
144/// The current entry format version.
145/// v0 indicates this is an unstable protocol subject to breaking changes.
146pub const ENTRY_VERSION: u8 = 0;
147
148/// Helper to check if version is default (0) for serde skip_serializing_if
149fn is_v0(v: &u8) -> bool {
150 *v == 0
151}
152
153/// Validates the entry version during deserialization.
154fn validate_entry_version<'de, D>(deserializer: D) -> std::result::Result<u8, D::Error>
155where
156 D: serde::Deserializer<'de>,
157{
158 let version = u8::deserialize(deserializer)?;
159 if version != ENTRY_VERSION {
160 return Err(serde::de::Error::custom(format!(
161 "unsupported Entry version {version}; only version {ENTRY_VERSION} is supported"
162 )));
163 }
164 Ok(version)
165}
166
167/// Lazily-computed cache of an `Entry`'s content-addressable ID.
168///
169/// The ID is a pure function of the entry's DAG-CBOR encoding, so it can be computed
170/// once and reused. The cache is derived state and deliberately invisible to everything
171/// that observes an entry's content: it is skipped by serde (so it never reaches the wire
172/// format or the hash input), ignored by equality, and hidden from `Debug`.
173///
174/// A clone carries the cached value, which is sound because an `Entry` exposes no in-place
175/// mutation: content changes go through [`Entry::with_auth`], which consumes the entry and
176/// returns a new one with a fresh cache.
177#[derive(Clone, Default)]
178struct IdCache(OnceLock<ID>);
179
180impl IdCache {
181 /// Return the cached ID, computing and storing it on first use.
182 fn get_or_init(&self, compute: impl FnOnce() -> ID) -> &ID {
183 self.0.get_or_init(compute)
184 }
185}
186
187impl PartialEq for IdCache {
188 fn eq(&self, _other: &Self) -> bool {
189 true
190 }
191}
192
193impl Eq for IdCache {}
194
195impl std::fmt::Debug for IdCache {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.write_str("IdCache")
198 }
199}
200
201#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
202pub struct Entry {
203 /// Protocol version for this entry format.
204 /// Used to verify that we support reading this entry.
205 #[serde(
206 rename = "_v",
207 default,
208 skip_serializing_if = "is_v0",
209 deserialize_with = "validate_entry_version"
210 )]
211 version: u8,
212 /// The main tree node data, including the root ID, parents in the main tree, and associated data.
213 ///
214 /// Scoped to this module so that a built entry's content cannot be mutated from
215 /// elsewhere in the crate, which is what makes [`IdCache`] sound.
216 pub(in crate::entry) tree: TreeNode,
217 /// A collection of named subtrees this entry contains data for.
218 /// The vector is kept sorted alphabetically by subtree name during the build process.
219 ///
220 /// Scoped for the same reason as [`Entry::tree`].
221 pub(in crate::entry) subtrees: Vec<SubTreeNode>,
222 /// Authentication information for this entry: the signature and the hint used to
223 /// locate the key that produced it.
224 ///
225 /// Private so that changes go through [`Entry::with_auth`], which rebuilds the entry
226 /// rather than mutating it in place. Serialized as `sig` to keep the wire format —
227 /// and therefore every existing entry ID — unchanged.
228 #[serde(rename = "sig")]
229 auth: AuthInfo,
230 /// Memoized content-addressable ID. Derived state; see [`IdCache`].
231 #[serde(skip)]
232 id_cache: IdCache,
233}
234
235impl Entry {
236 /// Creates a new `EntryBuilder` for an entry associated with a specific tree root.
237 /// This is a convenience method and preferred over calling `EntryBuilder::new()` directly.
238 ///
239 /// # Arguments
240 /// * `root` - The `ID` of the root `Entry` of the tree this entry will belong to.
241 pub fn builder(root: ID) -> EntryBuilder {
242 EntryBuilder::new(root)
243 }
244
245 /// Creates a new `EntryBuilder` for a top-level (root) entry for a new tree.
246 /// This is a convenience method and preferred over calling `EntryBuilder::new_top_level()` directly.
247 ///
248 /// Root entries have an empty string as their `root` ID and include a special ROOT subtree marker.
249 /// This method is typically used when creating a new tree.
250 pub fn root_builder() -> EntryBuilder {
251 EntryBuilder::new_top_level()
252 }
253
254 /// Get the content-addressable ID of the entry.
255 ///
256 /// The ID is the CID of the DAG-CBOR serialized representation of the Entry.
257 ///
258 /// The result is memoized: the encoding and hash are computed on the first call and
259 /// reused afterwards. Callers on hot paths (sorting, DAG traversal, map keys) can
260 /// therefore call this freely instead of threading an ID alongside the entry.
261 pub fn id(&self) -> ID {
262 self.id_ref().clone()
263 }
264
265 /// Borrow the content-addressable ID of the entry.
266 ///
267 /// The same value [`Entry::id`] returns, without copying it out of the memo. An [`ID`]
268 /// carries a fixed-size digest buffer, so comparison-heavy callers — sorting a slice of
269 /// entries, scanning one for a match — avoid a copy per access by borrowing instead.
270 pub fn id_ref(&self) -> &ID {
271 self.id_cache.get_or_init(|| {
272 let bytes = self
273 .to_dagcbor()
274 .expect("Failed to serialize entry to DAG-CBOR for ID");
275 ID::from_dagcbor_bytes(bytes)
276 })
277 }
278
279 /// Get the authentication information attached to this entry.
280 pub fn auth(&self) -> &AuthInfo {
281 &self.auth
282 }
283
284 /// Return this entry with its authentication information updated by `update`.
285 ///
286 /// Signature material is part of an entry's content, so the returned entry has a
287 /// different ID from the receiver. Consuming `self` and returning a new value is what
288 /// keeps that honest: an `Entry` is never mutated in place, so a memoized ID can never
289 /// outlive the content it was computed from.
290 ///
291 /// ```
292 /// # use eidetica::entry::Entry;
293 /// # fn main() -> eidetica::Result<()> {
294 /// let entry = Entry::root_builder().build()?;
295 /// let signed = entry.with_auth(|auth| auth.signature = Some("c2lnbmF0dXJl".to_string()));
296 /// # Ok(())
297 /// # }
298 /// ```
299 pub fn with_auth(mut self, update: impl FnOnce(&mut AuthInfo)) -> Self {
300 update(&mut self.auth);
301 self.id_cache = IdCache::default();
302 self
303 }
304
305 /// Get the ID of the root `Entry` of the tree this entry belongs to.
306 /// Returns `None` for root entries.
307 pub fn root(&self) -> Option<ID> {
308 self.tree.root.clone()
309 }
310
311 /// Check if this entry is a root entry (contains the ROOT marker and has no parents).
312 ///
313 /// Root entries are the top-level entries in the database and are distinguished by:
314 /// 1. Containing a subtree with the ROOT marker
315 /// 2. Having no parent entries (they are true tree roots)
316 ///
317 /// This ensures that root entries are actual starting points of trees in the DAG.
318 pub fn is_root(&self) -> bool {
319 // FIXME: better identification of root entries
320 self.subtrees.iter().any(|node| node.name == ROOT)
321 && self.tree.parents.is_empty()
322 && self.tree.root.is_none()
323 }
324
325 /// Check if this entry contains data for a specific named subtree.
326 pub fn in_subtree(&self, subtree_name: impl AsRef<str>) -> bool {
327 self.subtrees
328 .iter()
329 .any(|node| node.name == subtree_name.as_ref())
330 }
331
332 /// Check if this entry belongs to a specific tree, identified by its root ID.
333 pub fn in_tree(&self, tree_id: &ID) -> bool {
334 // Entries that are roots exist in both trees
335 self.tree.root.as_ref() == Some(tree_id) || self.id() == *tree_id
336 }
337
338 /// Get the names of all subtrees this entry contains data for.
339 /// The names are returned in alphabetical order.
340 pub fn subtrees(&self) -> Vec<String> {
341 self.subtrees
342 .iter()
343 .map(|subtree| subtree.name.clone())
344 .collect()
345 }
346
347 /// Get the metadata associated with this entry's tree node.
348 ///
349 /// Metadata is optional information attached to an entry that is not part of the
350 /// main data model and is not merged between entries.
351 pub fn metadata(&self) -> Option<&RawData> {
352 self.tree.metadata.as_ref()
353 }
354
355 /// Get the `RawData` for a specific named subtree within this entry.
356 ///
357 /// Returns an error if the subtree is not found or if the subtree exists but has no data (`None`).
358 pub fn data(&self, subtree_name: impl AsRef<str>) -> Result<&RawData> {
359 self.subtrees
360 .iter()
361 .find(|node| node.name == subtree_name.as_ref())
362 .and_then(|node| node.data.as_ref())
363 .ok_or_else(|| {
364 StoreError::KeyNotFound {
365 store: "entry".to_string(),
366 key: subtree_name.as_ref().to_string(),
367 }
368 .into()
369 })
370 }
371
372 /// Get the IDs of the parent entries in the main tree history.
373 /// The parent IDs are returned in CID order (`ID`'s `Ord`, not lexicographic string order).
374 pub fn parents(&self) -> Result<Vec<ID>> {
375 Ok(self.tree.parents.clone())
376 }
377
378 /// Get the IDs of the parent entries specific to a named subtree's history.
379 /// The parent IDs are returned in CID order (`ID`'s `Ord`, not lexicographic string order).
380 pub fn subtree_parents(&self, subtree_name: impl AsRef<str>) -> Result<Vec<ID>> {
381 self.subtrees
382 .iter()
383 .find(|node| node.name == subtree_name.as_ref())
384 .map(|node| node.parents.clone())
385 .ok_or_else(|| {
386 StoreError::KeyNotFound {
387 store: "entry".to_string(),
388 key: subtree_name.as_ref().to_string(),
389 }
390 .into()
391 })
392 }
393
394 /// Get the height of this entry in the main tree DAG.
395 pub fn height(&self) -> u64 {
396 self.tree.height
397 }
398
399 /// Get the height of this entry in a specific subtree's DAG.
400 ///
401 /// If the subtree has an explicit height (`Some(h)`), that value is returned.
402 /// If the subtree height is `None`, it inherits from the main tree height.
403 ///
404 /// This allows subtrees to either track independent heights (for subtrees
405 /// with their own height strategy) or share the tree's height (default).
406 pub fn subtree_height(&self, subtree_name: impl AsRef<str>) -> Result<u64> {
407 self.subtrees
408 .iter()
409 .find(|node| node.name == subtree_name.as_ref())
410 .map(|node| node.height.unwrap_or_else(|| self.height()))
411 .ok_or_else(|| {
412 StoreError::KeyNotFound {
413 store: "entry".to_string(),
414 key: subtree_name.as_ref().to_string(),
415 }
416 .into()
417 })
418 }
419
420 /// Create a canonical representation of this entry for signing purposes.
421 ///
422 /// This creates a copy of the entry with the signature field removed from auth,
423 /// which is necessary for signature generation and verification.
424 /// The returned entry has deterministic field ordering for consistent signatures.
425 pub fn canonical_for_signing(&self) -> Self {
426 self.clone().with_auth(|auth| auth.signature = None)
427 }
428
429 /// Create canonical bytes for signing or ID generation.
430 ///
431 /// This method serializes the entry to DAG-CBOR with deterministic field ordering.
432 /// For signing purposes, call `canonical_for_signing()` first.
433 pub fn canonical_bytes(&self) -> Result<Vec<u8>> {
434 self.to_dagcbor()
435 }
436
437 /// Create canonical bytes for signing (convenience method).
438 ///
439 /// This combines `canonical_for_signing()` and `canonical_bytes()` for convenience.
440 pub fn signing_bytes(&self) -> Result<Vec<u8>> {
441 self.canonical_for_signing().canonical_bytes()
442 }
443
444 /// Serialize this entry to DAG-CBOR bytes.
445 ///
446 /// Returns the canonical DAG-CBOR encoding for CID computation.
447 pub fn to_dagcbor(&self) -> Result<Vec<u8>> {
448 serde_ipld_dagcbor::to_vec(self).map_err(|e| {
449 EntryError::SerializationFailed {
450 context: format!("DAG-CBOR serialization failed: {e}"),
451 }
452 .into()
453 })
454 }
455
456 /// Validate the structural integrity of this entry.
457 ///
458 /// This method performs lightweight structural validation that can be done
459 /// without access to the backend database. It checks for obvious structural
460 /// issues while deferring complex DAG relationship validation to the transaction
461 /// and backend layers where full database access is available.
462 ///
463 /// # Validation Rules
464 ///
465 /// ## Critical Main Tree Parent Validation (Prevents "No Common Ancestor" Errors)
466 /// - **Root entries** (containing "_root" subtree): May have empty parents
467 /// - **Non-root entries**: MUST have at least one parent - **HARD REQUIREMENT**
468 /// - **Empty parent IDs**: Always rejected as invalid
469 ///
470 /// This strict enforcement prevents orphaned entries that cause sync failures.
471 ///
472 /// ## Subtree Parent Relationships
473 /// - For root entries: Subtrees may have empty parents (they establish the subtree roots)
474 /// - For non-root entries: Empty subtree parents require deeper validation:
475 /// - Could be legitimate (first entry in a new subtree)
476 /// - Could indicate broken relationships (needs DAG traversal to verify)
477 ///
478 /// ## Multi-Layer Validation System
479 /// Complex validation happens at multiple layers:
480 /// 1. **Entry Layer** (this method): Structural validation, main tree parent enforcement
481 /// 2. **Transaction Layer**: Parent discovery, subtree parent validation with DAG access
482 /// 3. **Backend Storage**: Final validation gate before persistence
483 /// 4. **Sync Operations**: Validation of entries received from peers
484 ///
485 /// # Special Cases
486 /// - The "_root" marker subtree has special handling and skips validation
487 /// - The "_settings" subtree follows standard validation rules
488 /// - Empty subtree parents are logged but deferred to transaction layer
489 ///
490 /// # Returns
491 ///
492 /// - `Ok(())` if the entry is structurally valid
493 /// - `Err(InstanceError::EntryValidationFailed)` if validation fails with specific reason
494 ///
495 /// # Examples
496 ///
497 /// ```rust,no_run
498 /// # use eidetica::Entry;
499 /// # let entry: Entry = unimplemented!();
500 /// // Validate an entry before storage or sync
501 /// match entry.validate() {
502 /// Ok(()) => {
503 /// // Entry is valid, safe to store/sync
504 /// println!("Entry is valid");
505 /// }
506 /// Err(e) => {
507 /// // Entry is invalid, reject it
508 /// eprintln!("Invalid entry: {}", e);
509 /// }
510 /// }
511 /// ```
512 /// Validates that an ID contains a valid CID.
513 fn validate_id_format(id: &ID, context: &str) -> Result<()> {
514 if id.as_cid().is_none() {
515 return Err(
516 IdError::InvalidFormat(format!("Invalid ID in {context}: ID is empty")).into(),
517 );
518 }
519 Ok(())
520 }
521
522 pub fn validate(&self) -> Result<()> {
523 use crate::constants::{ROOT, SETTINGS};
524 use crate::instance::errors::InstanceError;
525
526 // CRITICAL VALIDATION: Root entries (with _root marker) cannot have parents
527 // This enforces that root entries are true starting points of trees
528 let has_root_marker = self.subtrees.iter().any(|node| node.name == ROOT);
529 if has_root_marker && !self.tree.parents.is_empty() {
530 return Err(InstanceError::EntryValidationFailed {
531 reason: format!(
532 "Entry {} has _root marker but also has parents. Root entries cannot have parent relationships as they are the starting points of trees.",
533 self.id()
534 ),
535 }.into());
536 }
537
538 // Check if this is a root entry (will be true only if has ROOT marker AND no parents AND no root)
539 let is_root_entry =
540 has_root_marker && self.tree.parents.is_empty() && self.tree.root.is_none();
541
542 // Validate root ID format (when present)
543 if let Some(root_id) = &self.tree.root {
544 Self::validate_id_format(root_id, "tree root ID")?;
545 }
546
547 // Validate each subtree
548 for subtree_node in &self.subtrees {
549 let subtree_name = &subtree_node.name;
550 let subtree_parents = &subtree_node.parents;
551
552 // Empty string is not allowed as a subtree name
553 if subtree_name.is_empty() {
554 return Err(InstanceError::EntryValidationFailed {
555 reason: format!(
556 "Entry {} has a subtree with empty name. Store names must be non-empty.",
557 self.id()
558 ),
559 }
560 .into());
561 }
562
563 // Skip validation for the special "_root" marker subtree
564 if subtree_name == ROOT {
565 continue;
566 }
567
568 // For non-root entries with empty subtree parents, this is only valid if:
569 // 1. The entry has no main parents (making it a legitimate subtree root), OR
570 // 2. The subtree is genuinely being established for the first time within the tree
571 //
572 // Note: We can't perform deep validation here without access to the backend,
573 // so we defer complex validation to transaction/backend layers where full
574 // DAG traversal is possible. This basic validation catches obvious structural errors.
575 if !is_root_entry && subtree_parents.is_empty() {
576 // This is a lightweight structural check - more comprehensive validation
577 // happens in transaction/backend layers with full DAG access
578 tracing::debug!(
579 entry_id = %self.id(),
580 subtree = subtree_name,
581 "Entry has empty subtree parents - will be validated in transaction layer"
582 );
583 }
584
585 // Special validation for the critical "_settings" subtree
586 // Note: Settings subtree follows the same rules as other subtrees - empty parents
587 // are valid for the first entry in the subtree. Comprehensive validation happens
588 // in transaction/backend layers with full DAG access.
589 if subtree_name == SETTINGS && !is_root_entry && subtree_parents.is_empty() {
590 tracing::debug!(
591 entry_id = %self.id(),
592 "Settings subtree has empty parents - will be validated in transaction layer"
593 );
594 }
595
596 // Validate that subtree parents are not empty strings and have valid format
597 for parent_id in subtree_parents {
598 if parent_id.is_empty() {
599 return Err(InstanceError::EntryValidationFailed {
600 reason: format!(
601 "Entry {} has subtree '{}' with empty parent ID. Parent IDs must be non-empty valid entry IDs.",
602 self.id(),
603 subtree_name
604 ),
605 }.into());
606 }
607 // Validate parent ID format
608 Self::validate_id_format(
609 parent_id,
610 &format!("subtree '{subtree_name}' parent ID"),
611 )?;
612 }
613 }
614
615 // Enforce main tree parent requirements
616 if !is_root_entry {
617 let main_parents = self.tree.parents.clone();
618 if main_parents.is_empty() {
619 // This is a HARD FAILURE - reject the entry completely
620 // Empty main tree parents create orphaned nodes that break LCA calculations
621 return Err(InstanceError::EntryValidationFailed {
622 reason: format!(
623 "Non-root entry {} has empty main tree parents. All non-root entries must have valid parent relationships in the main tree.",
624 self.id()
625 ),
626 }.into());
627 }
628
629 // Validate that main parents are not empty strings and have valid format
630 for parent_id in &main_parents {
631 if parent_id.is_empty() {
632 return Err(InstanceError::EntryValidationFailed {
633 reason: format!(
634 "Entry {} has empty parent ID in main tree. Parent IDs must be non-empty valid entry IDs.",
635 self.id()
636 ),
637 }.into());
638 }
639 // Validate parent ID format
640 Self::validate_id_format(parent_id, "main tree parent ID")?;
641 }
642 }
643
644 Ok(())
645 }
646}