Skip to content

Capture and restore nu_state/paths.json in snapshots - #84

Merged
tonythethompson merged 9 commits into
masterfrom
cursor/snapshot-paths-sidecar-2d25
Aug 4, 2026
Merged

Capture and restore nu_state/paths.json in snapshots#84
tonythethompson merged 9 commits into
masterfrom
cursor/snapshot-paths-sidecar-2d25

Conversation

@tonythethompson

@tonythethompson tonythethompson commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

numan init --refresh rewrites nu_state/paths.json, but snapshots previously only stored lockfile / autoload / imports plus optional nu_identity (hash + version). Rollback restored the activation graph, not the path cache, so refresh-then-rollback with no active packages could leave paths on the post-refresh Nu.

This PR adds an additive paths sidecar to every new snapshot and restores it during rollback, including the empty-lockfile case.

Changes

  • Snapshot schema (src/state/snapshot.rs): optional SidecarDigests.paths_sha256 + SnapshotPaths::{Absent, Present(NuPaths)} sidecar written on every create_snapshot. Legacy snapshots without the file still load (paths: None).
  • Rollback (src/state/rollback.rs): journaled PathsCommitted step after imports. Present → NuPaths::save; Absent → delete paths.json; legacy None → leave live cache alone. Keeps the existing Nu-identity precondition for Present managed autoload (checked against the live cache before restore).
  • Init (src/cmd/init.rs): init --refresh takes a PreMutation / Init snapshot before rewriting lockfile identity or paths.json.
  • Docs: docs/snapshots-and-rollback.md + AGENTS.md (capture set, restore order, no-active-packages behavior, nu_identity vs paths sidecar).
  • Inspect: shows paths digest / captured Nu cache summary.

Non-goals

  • Does not move Nu binaries or rewrite shell PATH.
  • Absolute paths in the sidecar remain root-specific (same as autoload).
  • Additive optional fields only; no schema version bump.

Test plan

  • Snapshot with paths present → load verifies digest
  • New snapshot with paths absent records Absent
  • Legacy snapshot without paths sidecar still loads
  • Rollback with empty lockfile restores mutated paths.json
  • Rollback when snapshot had Absent deletes paths
  • Legacy rollback leaves live paths alone
  • Refresh-then-rollback round-trip restores pre-refresh paths
  • cargo fmt --check
  • cargo clippy -- -D warnings
  • cargo test (full suite green; one pre-existing flaky nu::paths PATH fall-through test observed under parallel load, passes on retry / isolation)
Open in Web Open in Cursor 

Review in cubic

PreMutation snapshots now always record the Nu path cache (or Absent),
and rollback restores it even with an empty lockfile. init --refresh
snapshots before rewriting paths so refresh-then-rollback is consistent.

Co-authored-by: Anthony Thompson <github@trackdub.com>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 392c42b8-4f6e-4ffa-ae19-df25abb07169

📥 Commits

Reviewing files that changed from the base of the PR and between 25d47ca and 6b02a6d.

📒 Files selected for processing (2)
  • AGENTS.md
  • src/cmd/snapshot.rs
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • tonythethompson/QuickShell (manual)
  • tonythethompson/numan (manual)
  • tonythethompson/dependency-chain-substrate (manual)
📜 Recent review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Greptile Review
  • GitHub Check: Real-Nu acceptance (windows-latest)
  • GitHub Check: Analyze (rust)
🧰 Additional context used
📓 Path-based instructions (9)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Use Serena's semantic, symbol-aware tools as the primary tools for reading and editing code; use built-in Read, Glob, Grep, and Edit only under the stated exceptions.
Before editing a code file, inspect its symbol overview, read the specific symbols being changed, and edit them with Serena's symbol-aware tools.
Understand the existing code before changing it and make the smallest change that satisfies the request; avoid unrelated cleanup, premature abstractions, unnecessary error handling, feature flags, and compatibility shims.
Prefer editing existing files over creating new ones, and never create Markdown or README files unless explicitly requested.
For exploratory questions, provide a 2–3 sentence recommendation with the main tradeoff and do not implement until the user agrees.
For UI or frontend changes that cannot be tested in a browser, explicitly state that browser testing was not performed rather than claiming success.
Address security issues when discovered, including injection, XSS, SQL injection, path traversal, and secret leaks.
Pause and obtain confirmation before destructive, hard-to-reverse, externally visible, or third-party-upload actions, including deleting files or branches, dropping tables, force-pushing, modifying CI/CD, posting externally, or uploading content.
When blocked, investigate the root cause instead of bypassing it with --no-verify, --force, or deletion; investigate unfamiliar files, branches, and configuration before deleting them.
Only commit when explicitly asked; do not proactively update git configuration or push changes.
Do not skip Git hooks unless explicitly asked; if a pre-commit hook fails, fix the issue, re-stage, and create a new commit rather than amending.
Stage files by name rather than using git add -A or git add ., to avoid accidentally including secrets or large binaries.
Use a HEREDOC for commit messages, and add co-author attribution only when the user explicitly requests the exact trailer.
Do not force-pus...

Files:

  • AGENTS.md
  • src/cmd/snapshot.rs
!**/.env,!**/credentials.json,!**/*.pem

📄 CodeRabbit inference engine (CLAUDE.md)

Do not commit files that appear to contain secrets, including .env, credentials.json, and PEM files; warn before doing so even if explicitly requested.

Files:

  • AGENTS.md
  • src/cmd/snapshot.rs
**/*.{rs,md}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Match existing naming, module layout, and documentation level in the file being edited; update AGENTS.md, docs/, or command help when structure, conventions, or user-visible behavior changes.

Tests must cover failure modes, not only successful execution.

Files:

  • AGENTS.md
  • src/cmd/snapshot.rs
**/*.{rs,md,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use the repository's established serialization and module conventions rather than introducing unrelated refactors.

Files:

  • AGENTS.md
  • src/cmd/snapshot.rs
**/*.md

📄 CodeRabbit inference engine (REVIEW.md)

Update documentation and AGENTS.md when project structure or conventions change.

Files:

  • AGENTS.md
**/*.{js,jsx,ts,tsx,py,java,go,rs,rb,php,c,cpp,h,hpp,cs,swift,kt,kts}

📄 CodeRabbit inference engine (CLAUDE.md)

Add comments only when the WHY is non-obvious; do not narrate what the code does, reference the current task, or reference the PR in comments.

Files:

  • src/cmd/snapshot.rs
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use the Rust 2021 edition.
Use anyhow::Result with .context(...) in application code; use thiserror for library error types that callers match on.
Use clap derive macros for CLI definitions.
Use serde with serde_json or toml for serialization.
Function parameters must use &Path, not &PathBuf.
Library code must not panic; error paths should return anyhow::Result with context where appropriate.
Add or update tests for behavior changes, including relevant failure paths.
New mutating code paths must acquire the mutation lock via acquire_mutation_lock(root) and snapshot the lockfile before writes.
Lockfile, journal, and state-file JSON writes must use write_json_atomic.
numan install must write only to $NUMAN_ROOT; it must not invoke Nu or register plugins/autoloads.
Only activate and deactivate may modify Nu integration state.
Treat the lockfile as the authoritative source of truth; derived projections such as autoload state must not be authoritative.
Install payloads under versioned, content-addressed paths and never overwrite them in place.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass plugin paths through environment variables only; do not use runtime interpolation in Nu program strings.

**/*.rs: All CI gates must pass: cargo test, cargo clippy -- -D warnings, and cargo fmt --check.
Every mutating command—including install, remove, update, gc, and future nupm import—must call acquire_mutation_lock(root).
Lockfiles, journals, and state files must use write_json_atomic; partial writes are not allowed.
Pending activation, autoload, and lifecycle journals must be stored under $NUMAN_ROOT/state/.
Module autoload identity must match all four fields: Nu executable hash, Nu version, vendor autoload directory, and managed file path; the lockfile module_activation value is authoritative.
Never overwrite foreign autoload files; respect OWNERSHIP_MARKER.
Pass paths to Nu only throu...

Files:

  • src/cmd/snapshot.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run and keep cargo fmt/rustfmt clean, and ensure cargo clippy -- -D warnings passes.

Maintain compatibility with the declared MSRV of Rust 1.88.

Files:

  • src/cmd/snapshot.rs
**/*.{rs,nu}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.{rs,nu}: Real-Nu acceptance tests must be marked #[ignore] and should be run when changes affect activation or nupm import; unit tests must not spawn real nu and should use injectable seams such as FakeCandidateRunner or registrars.
The nupm integration must be read-only toward NUPM_HOME, must not execute build.nu, and must not perform bidirectional synchronization.

Unit tests must use FakeCandidateRunner or injectable registrars and must not spawn a real nu process.

Files:

  • src/cmd/snapshot.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: tonythethompson/numan

Timestamp: 2026-08-04T13:13:21.887Z
Learning: Use `anyhow::Result` for application code and `thiserror` for library errors that callers match on; add context with `.context(...)` or `?`, and never panic in library code.
Learnt from: CR
Repo: tonythethompson/numan

Timestamp: 2026-08-04T13:13:21.887Z
Learning: Use test-first development, run the full test suite before completion, update AGENTS.md when structure or conventions change, and follow the documented review checklist in `REVIEW.md`.
🔍 Remote MCP DeepWiki, GitHub Copilot

Relevant review context

  • DeepWiki confirms rollback is journaled and ordered: lockfile → autoload → autoload state → imports → path cache → completion. Payload integrity, root stability, and Nu identity validation are preconditions. Legacy snapshots must leave live paths unchanged.
  • The current diff implements digest-authoritative path loading: a declared digest requires paths.json; missing or corrupt sidecars fail loading, while absent digests produce legacy behavior. It also distinguishes Absent from Present(NuPaths).
  • Existing review concerns about corrupt caches, duplicate refresh snapshots, and unverified legacy sidecars are marked resolved/outdated; the current diff includes regression tests for these cases.
  • CI currently reports success for Clippy, MSRV, Ubuntu/macOS tests, formatting, packaging, and CodeFactor. Windows tests, Windows acceptance, Rust analysis, and Greptile review were still in progress at retrieval time.
🔇 Additional comments (4)
AGENTS.md (2)

159-159: 🗄️ Data Integrity & Integration

No change needed. src/cmd/init.rs acquires the mutation lock, creates the PreMutation Init snapshot before updating the lockfile and nu_state/paths.json, and includes tests for no-active-package, lock contention, and snapshot-failure fail-closed behavior.


159-159: 📐 Maintainability & Code Quality

No change needed.

src/cmd/snapshot.rs (2)

288-356: LGTM!


111-113: 🎯 Functional Correctness

No change needed. snapshot inspect already prints distinct output for Present, Absent, and legacy None SnapshotPaths.


📝 Walkthrough

Walkthrough

Snapshots now capture and verify the Nu path cache, recording present or absent state. Rollback restores, deletes, or preserves the cache based on snapshot metadata through a new lifecycle stage. Inspection output, lifecycle journaling, documentation, refresh comments, and related tests reflect this change. The architecture rule excludes doctor repair from required pre-mutation snapshots.

Changes

Nu paths snapshot and rollback

Layer / File(s) Summary
Capture and load Nu paths
src/state/snapshot.rs, docs/snapshots-and-rollback.md
SidecarDigests adds paths_sha256. New SnapshotPaths enum distinguishes absent and present cache states. Snapshot carries optional captured paths. Snapshot creation captures the sidecar and includes its digest. Snapshot loading verifies the sidecar or loads with no paths data for legacy compatibility. Tests cover present, absent, corrupt, missing, and legacy scenarios. Documentation describes snapshot scope, captured data, storage costs, and restore behavior.
Journaled paths restoration
src/state/rollback.rs, src/state/lifecycle_journal.rs, docs/snapshots-and-rollback.md
LifecycleStage::PathsCommitted tracks path-cache restoration. Rollback validates live Nu identity before restoration. When paths are present, rollback creates nu_state and restores the cache. When paths are absent, rollback deletes paths.json. Legacy snapshots leave the live cache unchanged. The lifecycle journal records PathsCommitted. Documentation clarifies Nu identity validation uses the live cache, states the restore order, and records path-cache restoration. Tests cover empty-lockfile restoration, absent-path deletion, refresh rollback, and legacy behavior.
Snapshot paths inspection
src/cmd/snapshot.rs
Inspection prints the stored paths digest and reports captured Nu path-cache state: Nu version, executable hash or path, plugin registry path, absence at capture time, or legacy snapshots without capture.
Mutation snapshot policy and refresh setup
AGENTS.md, src/cmd/init.rs
Architecture rule removes doctor repair from operations requiring pre-mutation snapshots. Refresh comments explicitly state that refresh writes are serialized and snapshots capture pre-refresh state. Plugin test setup creates the plugin payload before the lockfile fixture.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • tonythethompson/numan#79: The main PR removes doctor repair from required pre-mutation snapshots, directly refining the architectural decision added in PR #79. Both modify AGENTS.md, docs/snapshots-and-rollback.md, src/cmd/init.rs, and src/state/snapshot.rs.
  • tonythethompson/numan#83: Both PRs adjust snapshot requirements and lifecycle handling for doctor repair operations.

Suggested reviewers: greptile-apps

🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: capturing and restoring nu_state/paths.json in snapshots.
Description check ✅ Passed The description directly explains the snapshot schema, rollback behavior, refresh handling, documentation, and tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Pipeline Stage Enum Ordering ✅ Passed The custom check references SessionWorkflowStage (audio pipeline: Foundation/MediaLoaded/etc), which does not exist in this codebase. The PR modifies LifecycleStage for snapshot/rollback functional...
Gpu/Cpu Runtime Boundary ✅ Passed The PR changes only Rust, Markdown, and test files; no inference/requirements/main.py/C# files are modified, so the GPU/CPU boundary check is not applicable.
Managed Host Restart Safety ✅ Passed The custom check targets container/managed host restart safety. This PR modifies snapshot capture and rollback for a Nushell package manager (numan). No ManagedVenvHostManager, ContainerizedService...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/snapshot-paths-sidecar-2d25
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch cursor/snapshot-paths-sidecar-2d25

Warning

Review ran into problems

🔥 Problems

Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. Analyzed tonythethompson/QuickShell, tonythethompson/numan, tonythethompson/dependency-chain-substrate, skipped Trackdubllc/Trackdub.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tonythethompson
tonythethompson marked this pull request as ready for review August 3, 2026 02:32
Copilot AI review requested due to automatic review settings August 3, 2026 02:32

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @tonythethompson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Capture and restore nu_state/paths.json in snapshots

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Persist Nu path-cache (nu_state/paths.json) as a snapshot sidecar with digest validation.
• Restore (or delete) paths.json during rollback, including empty-lockfile/no-activation cases.
• Take a pre-mutation snapshot for init --refresh, and document/inspect the new behavior.
Diagram

graph TD
  A["numan init --refresh"] --> B["create_snapshot()"] --> C[("state/snapshots/<id>")]
  D["rollback_to_snapshot()"] --> E["load_snapshot()"] --> C
  D --> J[("PendingLifecycle journal")] --> F["restore paths sidecar"] --> G[("nu_state/paths.json")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Derive `paths.json` during rollback instead of snapshotting it
  • ➕ Avoids storing absolute-path cache content in snapshots
  • ➕ Potentially more resilient across roots if derivation were root-agnostic
  • ➖ Rollback would no longer be an exact restore of prior state
  • ➖ Derivation would require re-running Nu discovery/refresh logic and could be flaky or version-dependent
2. Always delete `nu_state/paths.json` on rollback
  • ➕ Very simple implementation
  • ➕ Avoids restoring stale cache entries
  • ➖ Breaks the goal of returning to a known-good prior environment
  • ➖ Regresses UX by forcing re-init/recompute even when the snapshot had valid paths
3. Store only Nu identity (hash/version) and re-run `init --refresh` on rollback
  • ➕ Smaller snapshot footprint than a full cache sidecar
  • ➕ Keeps cache generation centralized in init
  • ➖ Still mutates state by recomputation instead of restoring it
  • ➖ Does not handle the 'snapshot had no cache' (Absent) semantics cleanly

Recommendation: The chosen approach (additive paths.json sidecar with explicit Present/Absent and legacy None) best preserves rollback’s 'exact restore' contract while remaining backward compatible. The explicit Absent marker is particularly important to correctly handle uninitialized roots and the empty-lockfile refresh-then-rollback gap.

Files changed (7) +404 / -19

Enhancement (3) +180 / -2
snapshot.rsShow paths digest and captured Nu cache summary in 'snapshot inspect' +21/-0

Show paths digest and captured Nu cache summary in 'snapshot inspect'

• Extends inspect output to print the paths sidecar digest and a human-readable summary of captured paths state: Present (with version/hash) vs Absent vs legacy not captured.

src/cmd/snapshot.rs

lifecycle_journal.rsAdd rollback stage for committing Nu paths cache +2/-0

Add rollback stage for committing Nu paths cache

• Introduces 'LifecycleStage::PathsCommitted' to journal the new rollback step that restores or clears 'nu_state/paths.json'.

src/state/lifecycle_journal.rs

snapshot.rsAdd 'SnapshotPaths' sidecar and digest for 'nu_state/paths.json' +157/-2

Add 'SnapshotPaths' sidecar and digest for 'nu_state/paths.json'

• Extends snapshot schema with 'SidecarDigests.paths_sha256' and a 'SnapshotPaths' enum (Present/Absent), written for all new snapshots. Updates snapshot loading to verify the digest when present and treat missing sidecar as legacy 'None' so rollback can avoid mutating live paths.

src/state/snapshot.rs

Bug fix (2) +188 / -5
init.rsTake PreMutation snapshot before 'init --refresh' rewrites paths/identity +16/-0

Take PreMutation snapshot before 'init --refresh' rewrites paths/identity

• Adds an early 'create_snapshot(...PreMutation, Init...)' so refresh-then-rollback can restore the previous Nu path cache even when there are no active packages. Minor test setup adjustments ensure required payload directories/files exist.

src/cmd/init.rs

rollback.rsRestore/delete 'nu_state/paths.json' during rollback with journaling and legacy skip +172/-5

Restore/delete 'nu_state/paths.json' during rollback with journaling and legacy skip

• Adds a journaled commit step after imports to restore 'paths.json' when the snapshot recorded Present, delete it when Absent, and leave live paths untouched for legacy 'None'. Adds tests covering empty-lockfile restore, Absent deletion, refresh-then-rollback, and legacy no-op behavior.

src/state/rollback.rs

Documentation (2) +36 / -12
AGENTS.mdDocument snapshots capturing 'nu_state/paths.json' and legacy restore behavior +3/-3

Document snapshots capturing 'nu_state/paths.json' and legacy restore behavior

• Updates the internal architecture notes to include paths-cache capture in snapshots and clarifies rollback semantics for legacy snapshots that lack the sidecar.

AGENTS.md

snapshots-and-rollback.mdExpand snapshot/rollback docs to include Nu path cache sidecar and restore order +33/-9

Expand snapshot/rollback docs to include Nu path cache sidecar and restore order

• Documents that snapshots capture 'nu_state/paths.json', how legacy snapshots behave, and that rollback restores paths as a final journaled step (including empty-lockfile cases). Clarifies non-goals like not moving Nu binaries or shell PATH.

docs/snapshots-and-rollback.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Numan’s snapshot/rollback system to also capture and restore the Nu path cache (nu_state/paths.json), closing the gap where numan init --refresh could rewrite the cache and rollback would not restore it (notably when the lockfile is empty / no active packages).

Changes:

  • Add a paths.json snapshot sidecar (SnapshotPaths::{Absent, Present(NuPaths)}) with a digest recorded in the snapshot manifest, while keeping legacy snapshots loadable.
  • Restore/clear nu_state/paths.json during rollback as a journaled stage (PathsCommitted) after imports.
  • Update numan snapshot inspect output and documentation to reflect paths capture/restore behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/state/snapshot.rs Adds SnapshotPaths sidecar capture + digesting + legacy-aware load behavior.
src/state/rollback.rs Restores/clears nu_state/paths.json as an additional journaled rollback commit step.
src/state/lifecycle_journal.rs Adds LifecycleStage::PathsCommitted for rollback journaling.
src/cmd/snapshot.rs Prints paths sidecar digest and a summary of captured Nu paths during inspect.
src/cmd/init.rs Adds snapshotting around init --refresh (but currently duplicates the snapshot).
docs/snapshots-and-rollback.md Documents paths sidecar scope, legacy behavior, and restore order.
AGENTS.md Updates architecture notes to include paths capture/restore semantics.
Suppressed comments (1)

src/state/snapshot.rs:628

  • capture_paths_sidecar currently maps all NuPaths::load failures to SnapshotPaths::Absent. This incorrectly treats parse/IO errors as an "uninitialized root" and can lead to snapshots/rollbacks that delete a real-but-corrupt path cache. Only return Absent when nu_state/paths.json is missing; otherwise return an error so the user can repair/re-init.
fn capture_paths_sidecar(root: &Path) -> SnapshotPaths {
    match NuPaths::load(root) {
        Ok(paths) => SnapshotPaths::Present(paths),
        Err(_) => SnapshotPaths::Absent,
    }

Comment thread src/state/snapshot.rs Outdated
Comment thread src/cmd/init.rs Outdated
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR closes the numan init --refresh → rollback gap by capturing nu_state/paths.json in every new snapshot and restoring or deleting it during rollback. A SnapshotPaths::Absent / SnapshotPaths::Present sidecar is written at snapshot-creation time (with a SHA-256 digest in the manifest), a new PathsCommitted journal stage is added to make the restore step crash-recoverable, and legacy snapshots that pre-date the sidecar are handled with a None path that leaves the live cache untouched on rollback.

  • Snapshot schema (snapshot.rs): capture_paths_sidecar correctly fails hard on a corrupt-but-present paths.json rather than silently recording Absent; load_snapshot refuses to load a snapshot whose manifest declares a paths_sha256 but whose sidecar file is missing.
  • Rollback (rollback.rs): Commit 5 handles all three cases idempotently (Present → NuPaths::save, Absent → delete, legacy None → no-op); nu_state/ is created before paths.save in the Present branch; the Nu-identity precondition reads the live cache before any restore runs.
  • Init (init.rs): A single pre-mutation snapshot is taken inside execute_refresh before any file mutations; the test refresh_updates_plugin_activation_identity contains a minor duplicated payload-setup block introduced by this PR.

Confidence Score: 5/5

Safe to merge; the paths sidecar is additive and all three rollback cases are idempotent and crash-recoverable.

The core changes are well-bounded: an optional sidecar written atomically at snapshot time, a single new journaled commit step in rollback, and a pre-existing pattern for digest verification. The capture_paths_sidecar function correctly propagates errors for corrupt-but-present files rather than silently degrading to Absent. Legacy snapshot compatibility is exercised by dedicated tests. The only nit is a duplicated payload-setup block in one test helper, which has no effect on correctness.

Files Needing Attention: No files require special attention; the test duplication in src/cmd/init.rs is cosmetic only.

Important Files Changed

Filename Overview
src/state/snapshot.rs Adds SnapshotPaths enum (Absent/Present), paths_sha256 to SidecarDigests, and paths field to Snapshot; capture_paths_sidecar correctly errors on a corrupt-but-present paths.json (not silent Absent) and all load/verify logic is consistent with the existing digest pattern.
src/state/rollback.rs Adds journaled Commit 5 for SnapshotPaths; all three cases (None/legacy, Absent, Present) are handled correctly; crash-resume idempotency is preserved; nu_state directory is created before paths.save in the Present branch.
src/cmd/init.rs Single create_snapshot call correctly placed before any mutation in execute_refresh; test refresh_updates_plugin_activation_identity creates and writes the payload directory twice due to new lines shadowing the pre-existing setup.
src/state/lifecycle_journal.rs Adds PathsCommitted variant between ImportsCommitted and Completed; serializes cleanly with rename_all = "snake_case"; no other state machines consume this enum so no match exhaustiveness regressions.
src/cmd/snapshot.rs Inspect command correctly displays all three path states (Present, Absent, legacy None); digest line added alongside existing autoload/imports lines.

Reviews (6): Last reviewed commit: "Merge branch 'master' into cursor/snapsh..." | Re-trigger Greptile

Comment thread src/cmd/init.rs Outdated
Comment thread src/state/snapshot.rs Outdated
Comment thread src/cmd/init.rs Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 20 rules
✅ REVIEW.md

Grey Divider


Remediation recommended

1. Paths load errors masked ✓ Resolved 🐞 Bug ≡ Correctness
Description
capture_paths_sidecar() converts any NuPaths::load() error into SnapshotPaths::Absent, so a
present-but-corrupt nu_state/paths.json is silently recorded as “uninitialized”. A later rollback
to that snapshot can delete/clear the live cache instead of refusing with a clear error, losing the
pre-mutation paths state.
Code

src/state/snapshot.rs[R624-628]

+fn capture_paths_sidecar(root: &Path) -> SnapshotPaths {
+    match NuPaths::load(root) {
+        Ok(paths) => SnapshotPaths::Present(paths),
+        Err(_) => SnapshotPaths::Absent,
+    }
Relevance

●●● Strong

They commonly prefer explicit error paths/tests over silently swallowing state corruption; masking
load errors likely changed.

PR-#37

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
NuPaths::load() returns an error both when paths.json is missing and when parsing fails; the new
snapshot capture code maps all such errors to Absent, which changes semantics from “invalid cache
is an error” to “invalid cache is treated as not present”.

src/state/snapshot.rs[624-629]
src/nu/paths.rs[68-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`capture_paths_sidecar()` currently treats *all* failures from `NuPaths::load()` as `SnapshotPaths::Absent`. This conflates "paths cache not present" with "paths cache present but unreadable" (parse error, IO error), which can cause snapshots to record an incorrect pre-mutation state and rollbacks to delete the live cache.

### Issue Context
`NuPaths::load()` fails both when the file is missing and when JSON parsing fails. Only the former should map to `Absent`; the latter should fail snapshot creation (or at minimum surface an error) so rollback doesn’t silently lose state.

### Fix Focus Areas
- src/state/snapshot.rs[624-629]
- src/nu/paths.rs[68-78]

### Suggested fix
- Change `capture_paths_sidecar` to check `root.join("nu_state/paths.json")` existence explicitly.
 - If missing: return `SnapshotPaths::Absent`.
 - If present: attempt to load; on failure, return `Err` (propagate) so `create_snapshot()` fails loudly with context.
- Update callers accordingly (likely making `capture_paths_sidecar` return `Result<SnapshotPaths>` and using `?`).
- Add/adjust a test that creates an invalid `nu_state/paths.json` and asserts `create_snapshot()` fails with a useful message (instead of recording `Absent`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Legacy paths sidecar trusted ✓ Resolved 🐞 Bug ☼ Reliability
Description
load_snapshot() loads paths.json whenever the file exists, even if
manifest.sidecar_digests.paths_sha256 is None, and then skips verification. This allows a
legacy/partially-migrated snapshot (digest missing but file present) to overwrite/delete the live
nu_state/paths.json during rollback, violating the intended “legacy snapshots leave live paths
untouched” behavior.
Code

src/state/snapshot.rs[R324-329]

+    let paths_path = dir.join("paths.json");
+    let paths = if paths_path.exists() {
+        let paths: SnapshotPaths = read_json(&paths_path)?;
+        if let Some(expected) = manifest.sidecar_digests.paths_sha256.as_deref() {
+            verify_digest(&paths, expected, "paths")?;
+        }
Relevance

●● Moderate

Seems to violate stated legacy behavior, but no close precedent on digest-missing sidecars; could be
debated.

PR-#37

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The snapshot loader returns Some(paths) when the sidecar file exists, but integrity verification
is conditional on paths_sha256 being present; rollback then mutates live paths.json whenever
snapshot.paths is Some(...).

src/state/snapshot.rs[322-333]
src/state/snapshot.rs[89-101]
src/state/rollback.rs[234-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`load_snapshot()` currently decides whether paths were captured based solely on whether `<snapshot_dir>/paths.json` exists, and only verifies the digest if `paths_sha256` is present in the manifest.

### Issue Context
For true legacy snapshots, `paths_sha256` is absent (`None`). If a `paths.json` file is present anyway (manual tampering, partial migration, or corruption), `load_snapshot()` will return `Some(paths)` without integrity verification, and rollback will apply it.

### Fix Focus Areas
- src/state/snapshot.rs[322-333]
- src/state/snapshot.rs[89-101]
- src/state/rollback.rs[234-257]

### Suggested fix
- Treat `manifest.sidecar_digests.paths_sha256 == None` as authoritative “not captured”.
 - Option A (safer): if digest is `None`, set `paths = None` regardless of file existence.
 - Option B (strict): if digest is `None` but `paths.json` exists, fail `load_snapshot()` with an error explaining the snapshot is inconsistent.
- Add a regression test that clears `paths_sha256` but leaves `paths.json` present, and asserts the snapshot loads as legacy (`paths == None`) or errors (depending on chosen option).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Init refresh snapshots twice ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
execute_refresh() now calls create_snapshot(...Init...) twice under the mutation lock before any
refresh writes, producing redundant PreMutation/Init snapshots on every numan init --refresh. This
bloats snapshot history and increases snapshot-rooted GC retention without improving rollback
recovery.
Code

src/cmd/init.rs[R202-205]

+    // Capture pre-refresh state (including paths.json) before any mutation so
+    // rollback can restore the prior Nu path cache even with an empty lockfile.
+    create_snapshot(
+        root,
Relevance

●●● Strong

Team has accepted avoiding unnecessary snapshots/locks in non-mutating paths; redundant snapshots
likely removed.

PR-#67

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The refresh path contains two create_snapshot calls with the same reason/trigger prior to
lockfile/autoload/paths mutations, so the newly-added call is redundant.

src/cmd/init.rs[168-223]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`numan init --refresh` currently takes two snapshots with the same reason/trigger in the same refresh flow.

### Issue Context
Within `execute_refresh`, there is already a pre-mutation snapshot taken immediately after acquiring the mutation lock. A second, newly-added pre-refresh snapshot is taken later but still before any on-disk mutations, so it captures effectively the same state.

### Fix Focus Areas
- src/cmd/init.rs[168-212]

### Suggested fix
- Remove one of the two `create_snapshot(... SnapshotTrigger::Init ...)` calls.
- Keep/adjust the surrounding comment so it documents the single intended snapshot point (before any lockfile/autoload-state/paths writes).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/state/snapshot.rs Outdated
Comment thread src/cmd/init.rs Outdated
Comment thread src/state/snapshot.rs Outdated
- Differentiate missing and invalid paths sidecars
- Ignore unverified legacy paths sidecars
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo Fixer

✅ Committed (3) · ☑ Fixed (3)

Grey Divider

Commits pushed directly to this PR — no separate fix PR opened.

Process — 3 fixed
  • ☑ Fixed: Paths load errors masked
  • ☑ Fixed: Legacy paths sidecar trusted
  • ☑ Fixed: Init refresh snapshots twice

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 522cc06f57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/state/snapshot.rs
Comment thread src/state/snapshot.rs Outdated
Comment thread src/cmd/init.rs Outdated
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 3, 2026
Remove `let mut lockfile = lockfile;` in execute_refresh so the existing
mutable binding is used through activation refresh. Clears unused-mut and
redundant_locals under `cargo clippy -- -D warnings`.

Co-authored-by: Anthony Thompson <github@trackdub.com>
@cursor
cursor Bot dismissed stale reviews from greptile-apps[bot] and coderabbitai[bot] via 4a1724b August 3, 2026 04:06
capture_paths_sidecar already returns Absent only when paths.json is
missing and propagates other load errors. Add regression coverage and
refuse load when a paths digest is present but the sidecar file is gone.

Co-authored-by: Anthony Thompson <github@trackdub.com>
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 3, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 3, 2026
@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Re: Codex P2 on capture_paths_sidecar treating all NuPaths::load failures as Absent.

Already addressed on this branch:

  • 324bea6capture_paths_sidecar returns Absent only when nu_state/paths.json is missing; otherwise propagates load/parse/IO errors via Result.
  • 9bd2a27 — regression test create_snapshot_fails_when_existing_paths_json_is_corrupt locks that behavior.

Current shape:

fn capture_paths_sidecar(root: &Path) -> Result<SnapshotPaths> {
    let paths_path = root.join("nu_state/paths.json");
    if !paths_path.exists() {
        return Ok(SnapshotPaths::Absent);
    }
    let paths = NuPaths::load(root).with_context(|| { /* ... */ })?;
    Ok(SnapshotPaths::Present(paths))
}

No further code change needed for this finding.

@tonythethompson
tonythethompson enabled auto-merge (squash) August 4, 2026 13:13
@tonythethompson
tonythethompson merged commit c69a5a6 into master Aug 4, 2026
20 checks passed
@tonythethompson
tonythethompson deleted the cursor/snapshot-paths-sidecar-2d25 branch August 4, 2026 13:16
@linear-code

linear-code Bot commented Aug 4, 2026

Copy link
Copy Markdown

NUM-53

tonythethompson added a commit that referenced this pull request Aug 4, 2026
Combine master #83/#84 with PR 82 doctor/migrate hardening. Prefer master bootstrap short-circuit skeleton, keep short-circuit PreMutation snapshot, and fold validate_reconcile with layout guards.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants