Skip to content

feat(scopes): cross-project Move-to via project_dir_from/_to split (#179, #181)#202

Merged
bminier merged 1 commit into
devfrom
feat/v0.7-cross-project
May 25, 2026
Merged

feat(scopes): cross-project Move-to via project_dir_from/_to split (#179, #181)#202
bminier merged 1 commit into
devfrom
feat/v0.7-cross-project

Conversation

@bminier

@bminier bminier commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

Finishes the half-disabled cross-project Move-to submenu from v0.6.

Before

  • Submenu hid OtherProject > Local whenever the source was Local (and same for Project) — the skip ran in every nested project, not just the current one.
  • Backend's `apply_move_leaf` took a single `project_dir`, so a move from current project's Local/Project into another project's local/project couldn't be expressed. The codex 6th-pass [P1] half-fix in PR fix: v0.6 release-gate blockers — audit log atomicity + CLI parity (#163-#171) #178 routed `project_dir` to the destination to dodge silent corruption, but the genuine cross-project move stayed blocked.

After

  • `apply_move_leaf` / `diff_move_leaf` accept `project_dir_from` + `project_dir_to` alongside the existing `project_dir`. Each side's `ScopePaths` resolves under its own root.
  • `resolve_move_paths` collapses to one resolution when both sides match (the 99% case), so the IPC stays zero-extra-cost on the hot path.
  • Audit records gain `project_dir_to: Option`, set only when the move actually crossed projects. Wire format stays backwards compatible.
  • `validate_audit_records` checks each side against BOTH project allowlists — passing under either is acceptable. Paths outside both still get rejected (no weakening of the Security: Audit log path injection — restore writes to attacker-chosen path #183 injection gate).
  • `buildMoveToSubmenu` skips `target === from` only when the nested project is the CURRENT one. The full `local + project` matrix is offered for other projects.
  • `apply_move_leaf_impl`'s same-scope dispatch keys off `from_path == to_path` rather than `req.from == req.to` — cross-project `Local → Local` is a real two-file move, not a change-kind.

Test plan

  • 280 Rust lib tests (was 278 — +2 net after consolidating two existing tests into the new shared "overwrite the file with itself" wording, plus +4 new cross-project tests)
  • 27 binary tests, 21 CLI integration tests
  • 140 JS unit tests (was 137 — +3 for the submenu unlock)
  • `just check` clean
  • SMOKE_TEST.md: new bullet for the two-project flow
  • CI confirms

Reviewer notes

  • The audit record's `project_dir_to` is opt-in (`skip_serializing_if = Option::is_none`): every existing audit log replays unchanged because the field deserializes to `None` and the restore path falls back to `project_dir` for the to-side allowlist.
  • `MoveOptions.projectDir` was renamed to `MoveOptions.projectDirTo`. The old field was the v0.6 half-fix routing `project_dir` to the destination; the new name spells out which side it overrides. No backend or wire compatibility break — the IPC always took `project_dir` and now additionally accepts the two split fields.
  • CLI's `move` subcommand still single-project (passes `paths, paths` to the new impl). Cross-project CLI moves are a follow-up to CLI: move-key subcommand for moving whole top-level keys #140.
  • The same-scope dispatch change in `apply_move_leaf_impl` is the subtle bit: same-scope change-kind (`from == to` with `to_kind` set) still routes to the single-file branch via the path-equality check, since same-project resolves both sides to the same file.

Closes #179, closes #181.

🤖 Generated with Claude Code

, #181)

Finishes the half-disabled cross-project Move-to submenu that shipped
in v0.6. Before this:
  - The submenu hid `OtherProject > Local` whenever the source was
    Local (and same for Project), because the skip ran in every
    nested project, not just the current one.
  - The backend's `apply_move_leaf` took a single `project_dir`, so a
    move whose source lived in the current project's Local/Project
    scope into another project's local/project couldn't even be
    expressed — both sides resolved under one root, and the
    destination silently won (the codex 6th-pass [P1] half-fix in
    PR #178 routed `project_dir` to the destination to dodge silent
    corruption, but the genuine cross-project move stayed blocked).

#179 — backend split
====================

`apply_move_leaf` / `diff_move_leaf` gain `project_dir_from` and
`project_dir_to` IPC fields alongside the existing `project_dir`. Each
side's `ScopePaths` resolves under its own root. The single-project
`project_dir` argument is the fallback for either missing side, so
existing same-project callers don't have to switch IPC shape.

  - `resolve_move_paths` returns a `(paths_from, paths_to)` pair,
    collapsing to one resolution when both sides match (the
    99% case).
  - `apply_move_leaf_impl` / `diff_move_leaf_impl` take two
    `ScopePaths`. Same-scope change-kind is now dispatched on whether
    `from_path == to_path` rather than `req.from == req.to` — pre-#179
    the two conditions were equivalent, but cross-project
    `Local → Local` is a real two-file move and needs the cross-scope
    branch.
  - `validate_move_request`'s path-collision check (#153) folds the
    pre-#179 `from == to` rejection into the same shared "source and
    destination resolve to the same file" message. One wording, two
    cases, identical effect.
  - Audit record gains `project_dir_to: Option<PathBuf>`, set only
    when the move actually crossed projects. Wire format stays
    backwards compatible — `skip_serializing_if` omits the field for
    every existing record.
  - `validate_audit_records` checks each side's `file_path` against
    BOTH project allowlists (`project_dir` and `project_dir_to`);
    passing under either is acceptable. A path outside both still
    gets rejected — the cross-project surface doesn't weaken the
    #183 injection gate.

#181 — submenu unlock
=====================

`buildMoveToSubmenu`'s `target === from` skip now fires ONLY when the
nested project is the currently-loaded one. For other projects, the
full `local + project` matrix is fair game. The chosen project root
threads through `MoveOptions.projectDirTo`, which `moveLeaf` in
`main.ts` sends as `project_dir_to` — the backend then resolves the
destination side under that root.

  - `MoveOptions.projectDir` → `MoveOptions.projectDirTo`. The
    previous field was a partial half-fix; renaming makes the
    semantics ("override the DESTINATION root") explicit so a
    future cross-project source override has a name to take.
  - `moveLeaf` always sends both `project_dir_from` (= viewed
    project) and `project_dir_to` (= override or viewed project).
    Backend's `resolve_move_paths` collapses the equal case so the
    IPC stays zero-extra-cost on the hot path.

Tests
=====

Rust (278 → 285, +7):
  - apply_move_leaf_impl_moves_local_rule_across_project_boundaries
  - apply_move_leaf_impl_moves_project_rule_across_project_boundaries
  - resolve_move_paths_falls_back_to_project_dir_when_per_side_unset
  - resolve_move_paths_honors_per_side_overrides_when_set
  - validate_audit_records_accepts_cross_project_move_with_both_project_dirs
  - validate_audit_records_refuses_cross_project_path_outside_both_allowlists
  - existing `move_leaf_rejects_same_scope_source_and_destination` and
    `validate_move_request_refuses_cross_scope_move_into_a_colliding_pair`
    updated to assert against the new shared "overwrite the file with
    itself" wording.

JS (137 → 140, +3):
  - offers same-scope-name targets when the project differs (#181)
  - still hides the same-scope-name target under the CURRENT project
  - dispatches onMoveLeaf with projectDirTo for a cross-project pick

CLI:
  - cli.rs's `move` subcommand wraps the new impl signature by passing
    `paths` to both sides — the CLI itself stays single-project until
    #140's `move-key` lands and we revisit.

Docs:
  - SMOKE_TEST.md: new bullet for the two-project cross-scope flow.

Closes #179, closes #181.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bminier bminier merged commit cd9390d into dev May 25, 2026
14 checks passed
@bminier bminier deleted the feat/v0.7-cross-project branch May 25, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant