rojo/.cursor/plans/destructive_syncback_mode_acb8cb54.plan.md
2026-02-01 07:58:06 -06:00

5.5 KiB

name overview todos isProject
Destructive Syncback Mode Add a `--clean` flag to syncback that ignores existing file structure and middleware, treating all instances as new to ensure consistent 1:1 output regardless of existing filesystem state.
false

Destructive Syncback Mode

Root Cause Analysis

Your inconsistency stems from three preservation mechanisms in syncback:

1. File Name Preservation ([src/syncback/file_names.rs](src/syncback/file_names.rs) lines 19-34)

When an old instance exists, the existing filename is always preserved, even if the instance was renamed:

if let Some(old_inst) = old_inst {
    if let Some(source) = old_inst.metadata().relevant_paths.first() {
        source.file_name()  // Returns OLD filename, ignores new instance name
    }
}

2. Middleware Preservation ([src/syncback/mod.rs](src/syncback/mod.rs) lines 353-357)

When an old instance exists, its middleware format is preserved:

} else if let Some(old_middleware) = old_middleware {
    // Use old middleware, even if a clean sync would choose differently
    middleware = old_middleware;
}

This means a Part.rbxm stays as .rbxm instead of potentially becoming a Part/ directory with children.

3. Name-Based Child Matching ([src/snapshot_middleware/dir.rs](src/snapshot_middleware/dir.rs) lines 196-216)

Children are matched by name between old and new DOMs. If a child exists in both, it inherits the old structure:

if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) {
    // Existing child - pass old reference, preserving its structure
    children.push(snapshot.with_joined_path(*new_child_ref, Some(old_child.id()))?);
} else {
    // New child - gets fresh structure
    children.push(snapshot.with_joined_path(*new_child_ref, None)?);
}

Solution: Add --clean Flag

Add a --clean flag that makes syncback treat all instances as "new" by setting old references to None, ensuring consistent output regardless of existing filesystem state.

Changes Required

1. Add CLI Flag ([src/cli/syncback.rs](src/cli/syncback.rs))

Add --clean flag to the command definition:

/// If provided, syncback will ignore existing file structure and create
/// a fresh project layout. This ensures consistent output regardless of
/// existing filesystem state.
#[clap(long)]
pub clean: bool,

2. Pass Flag to syncback_loop ([src/cli/syncback.rs](src/cli/syncback.rs))

Modify the syncback_loop call to pass the clean flag:

let snapshot = syncback_loop(
    session_old.vfs(),
    &mut dom_old,
    dom_new,
    session_old.root_project(),
    self.clean,  // New parameter
)?;

3. Modify syncback_loop ([src/syncback/mod.rs](src/syncback/mod.rs))

Update function signature and initial snapshot:

pub fn syncback_loop(
    vfs: &Vfs,
    old_tree: &mut RojoTree,
    mut new_tree: WeakDom,
    project: &Project,
    clean_mode: bool,  // New parameter
) -> anyhow::Result<FsSnapshot>

When clean_mode is true, set the initial old reference to None:

let mut snapshots = vec![SyncbackSnapshot {
    data: syncback_data,
    old: if clean_mode { None } else { Some(old_tree.get_root_id()) },
    new: new_tree.root_ref(),
    path: project.file_location.clone(),
    middleware: Some(Middleware::Project),
}];

4. Track Removed Files in Clean Mode ([src/syncback/mod.rs](src/syncback/mod.rs))

In clean mode, we need to remove all existing files in the target paths. Add logic to collect existing paths for removal:

if clean_mode {
    // Collect all existing paths under the project tree for removal
    for ref_id in descendants(old_tree.inner(), old_tree.get_root_id()) {
        if let Some(inst) = old_tree.get_instance(ref_id) {
            for path in inst.metadata().relevant_paths.iter() {
                if path.is_dir() {
                    fs_snapshot.remove_dir(path);
                } else {
                    fs_snapshot.remove_file(path);
                }
            }
        }
    }
}

5. Update Child Snapshot Creation ([src/snapshot_middleware/dir.rs](src/snapshot_middleware/dir.rs))

The SyncbackSnapshot::with_joined_path method already handles old = None correctly - it will create a fresh child snapshot without old references.

Data Flow Diagram

flowchart TD
    subgraph currentBehavior [Current Behavior]
        A1[Syncback on Existing Files] --> B1{Old Instance Exists?}
        B1 -->|Yes| C1[Preserve Filename]
        B1 -->|Yes| D1[Preserve Middleware]
        B1 -->|Yes| E1[Match Children by Name]
        C1 --> F1[Inconsistent Structure]
        D1 --> F1
        E1 --> F1
    end
    
    subgraph cleanMode [Clean Mode]
        A2[Syncback with --clean] --> B2[Set old = None]
        B2 --> C2[Generate Fresh Filename]
        B2 --> D2[Choose Best Middleware]
        B2 --> E2[All Children Are New]
        C2 --> F2[Consistent 1:1 Structure]
        D2 --> F2
        E2 --> F2
    end

Usage

# Current behavior (preserves existing structure)
rojo syncback -i game.rbxl

# Destructive mode (ignores existing structure, creates fresh layout)
rojo syncback -i game.rbxl --clean

# Combine with other flags
rojo syncback -i game.rbxl --clean --list --dry-run

Alternative Consideration

If you also want to support project-level configuration (so you don't need to pass --clean every time), we could add a syncbackRules.cleanMode: true option in the project file. Let me know if you'd prefer that approach.