Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

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}