feat(6243): move_path carries a moved path's snippets with its shapes
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 2m12s
CI & Build / Build & push image (push) Successful in 31s

Snippet locations are keyed by path too, so a snippet recorded under a
moved directory reads `missing` at its next check. The tool is now
move_path(project_id, from_path, to_path). It moves the ledger rows as
before, and re-paths the project's snippet locations under from_path
through update_snippet, so each body and its indexed mirror stay one
record. A snippet the caller can only read is listed in
snippets_read_only. The tool refuses only when nothing at all is recorded
under from_path. The path test is the one snippets already uses
(_path_touches), and the plugin version is minted for the skill edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 15:07:32 -04:00
co-authored by Claude Opus 5.5
parent 47b52197aa
commit 979efe6662
8 changed files with 114 additions and 36 deletions
+3 -8
View File
@@ -170,10 +170,6 @@ async def sync_repo_shapes(
await session.commit()
def _under(path: str, prefix: str) -> bool:
return path == prefix or path.startswith(prefix + "/")
async def move_shapes(
user_id: int, project_id: int, from_path: str, to_path: str,
) -> dict:
@@ -193,12 +189,13 @@ async def move_shapes(
from sqlalchemy import or_
from grimoire.services import access
from grimoire.services.snippets import _path_touches
old = (from_path or "").strip().strip("/")
new = (to_path or "").strip().strip("/")
if not old or not new:
raise ValueError("from_path and to_path are both required")
if _under(old, new) or _under(new, old):
if _path_touches(old, new) or _path_touches(new, old):
raise ValueError(f"`{old}` and `{new}` overlap; move to a path outside the source")
if not await access.can_write_project(user_id, project_id):
raise ValueError(f"project {project_id} not found or no write access")
@@ -213,10 +210,8 @@ async def move_shapes(
# LIKE reads `_` as a wildcard; the prefix test is the real filter.
sources = [
r for r in (await session.execute(beneath(old))).scalars().all()
if _under(r.path, old)
if _path_touches(old, r.path)
]
if not sources:
raise ValueError(f"project {project_id} has no shapes under `{old}`")
targets = {
(r.repo_key, r.path, r.symbol, r.kind): r
for r in (await session.execute(beneath(new))).scalars().all()