fix: validate state integrity after loading state.json#18
fix: validate state integrity after loading state.json#18don-petry wants to merge 4 commits intooneirosoft:mainfrom
Conversation
Add a validate() method to DaggerState that checks referential integrity after deserialization: version compatibility, duplicate node IDs, duplicate active branch names, and dangling parent references. This catches corrupted state early with clear error messages instead of causing confusing failures or infinite loops downstream. Refs: oneirosoft#10 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a DaggerState::validate() pass to catch corrupted state.json immediately after deserialization, improving correctness and error clarity for state integrity issues (referential integrity, duplicates, version mismatch).
Changes:
- Added
DaggerState::validate()to enforce basic state invariants (version, unique node IDs, unique active branch names, parent references exist). - Invoked
validate()fromload_state()so invalid state fails fast during store initialization/load. - Added unit tests for the new validation behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/core/store/types.rs |
Implements DaggerState::validate() and adds unit tests for version/duplicate/parent-reference validation cases. |
src/core/store/state.rs |
Calls state.validate() immediately after JSON deserialization in load_state(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/core/store/types.rs
Outdated
| if let ParentRef::Branch { node_id } = &node.parent { | ||
| if !all_ids.contains(node_id) { | ||
| return Err(io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| format!( | ||
| "branch '{}' references non-existent parent node {}", | ||
| node.branch_name, node_id | ||
| ), | ||
| )); | ||
| } | ||
| } |
There was a problem hiding this comment.
validate() only checks that a ParentRef::Branch { node_id } exists in all_ids, but all_ids includes archived nodes. Elsewhere (e.g., DaggerState::find_branch_by_id used by BranchGraph::lineage / branch_depth) parent lookups ignore archived nodes, so an active node can reference an archived parent and still pass validation while graph traversal treats the parent as missing. Consider tightening validation to reject parent refs that point to archived nodes (at least when the child is active), or consistently use find_any_branch_by_id for parent traversal if archived parents are meant to be allowed.
There was a problem hiding this comment.
Addressed in the latest push.
There was a problem hiding this comment.
This is intentionally allowed. An active node referencing an archived parent is a valid intermediate state that dgr sync is designed to repair. We previously had this check and removed it (see commit 00ad7b0) because it caused false validation failures during normal workflows. The comment at lines 92-93 documents this design decision, and the test validates_active_node_with_archived_parent_is_allowed (line 847) explicitly covers this case.
| let state = DaggerState { | ||
| version: 1, | ||
| nodes: vec![parent, child], | ||
| }; | ||
|
|
There was a problem hiding this comment.
The new validation tests hardcode version: 1. Using DAGGER_STATE_VERSION instead will keep these tests correct if/when the state version is bumped and avoids having to update multiple literals.
There was a problem hiding this comment.
Addressed in the latest push.
There was a problem hiding this comment.
Fixed — all tests now use the DAGGER_STATE_VERSION constant instead of hardcoded 1. The only literal version number remaining is 999 in the validates_version_mismatch test, which intentionally uses an invalid version to test error handling.
| for node in &self.nodes { | ||
| if !seen_ids.insert(node.id) { | ||
| return Err(io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| format!("duplicate node id {}", node.id), | ||
| )); | ||
| } | ||
|
|
||
| if !node.archived && !seen_names.insert(&node.branch_name) { | ||
| return Err(io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| format!("duplicate active branch name '{}'", node.branch_name), | ||
| )); | ||
| } | ||
|
|
||
| if let ParentRef::Branch { node_id } = &node.parent { | ||
| if !all_ids.contains(node_id) { | ||
| return Err(io::Error::new( | ||
| io::ErrorKind::InvalidData, | ||
| format!( | ||
| "branch '{}' references non-existent parent node {}", | ||
| node.branch_name, node_id | ||
| ), | ||
| )); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
validate() doesn’t currently guard against self-parenting or cyclic parent chains. A corrupted state.json containing a cycle (including a node whose parent is itself) can still pass this validation but will cause infinite loops in BranchGraph::lineage / branch_depth (they follow ParentRef::Branch without a visited set). Consider adding cycle detection (or at minimum a self-parent check) as part of validation so load-time validation better protects against hangs when the state file is corrupted.
There was a problem hiding this comment.
Addressed in the latest push.
There was a problem hiding this comment.
Added a self-parenting check in validate() (lines 76-81) — if a node's ParentRef::Branch { node_id } points to its own id, validation returns an error. The test validates_self_parent_reference (line 832) covers this case.
Full cycle detection (A→B→C→A) is already handled by PR #17 (merged), which detects cycles during graph traversal at restack time, so we only need the simple self-referential guard here in the state integrity check.
…DAGGER_STATE_VERSION constant Address Copilot review comments on PR oneirosoft#18: - Add self-referencing parent cycle detection in validate() - Add check that active nodes don't reference archived parents - Replace hardcoded version: 1 with DAGGER_STATE_VERSION in tests - Add tests for self-parent reference and active-node-with-archived-parent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Active branches referencing archived parents is a valid intermediate state that the sync command is designed to repair. Rejecting this in validation prevents sync from loading the state to perform the repair, causing two integration tests to panic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Why?
A corrupted or manually edited
state.jsoncan causedgrto panic, silently produce wrong results, or corrupt data further. Early validation with clear error messages makes debugging straightforward and prevents cascading failures.Summary
validate()method toDaggerStatethat checks referential integrity after deserializingstate.json: version compatibility, duplicate node IDs, duplicate active branch names, self-referential parents, and dangling parent referencesvalidate()inload_stateimmediately after deserialization so corrupted state is caught early with clear error messagesdgr syncrepairsRefs: #10 (item 4 — state validation)
Test plan
cargo test --lockedpasses with all new validation testsstate.json(e.g. duplicate a node ID) and verifydgrreports a clear error🤖 Generated with Claude Code