--- phase: "13" plan: "08" subsystem: cloud-operations status: complete tags: - cloud - create-folder - rename - collision-retry - stale-guard - reconciliation - tdd dependency_graph: requires: - "13-03 (MutableCloudResourceAdapter + mutable provider implementations)" - "13-04 (operations.py route layer + typed result vocabulary)" - "13-06 (upload reconcile-before-return pattern)" provides: - "Bounded collision retry for create-folder (D-05, D-06)" - "Stale guard with folder state refresh for create-folder and rename (D-07)" - "Reconcile-before-return for create-folder and rename success (T-13-26)" - "10 new behavioral tests in test_cloud_mutations.py" affects: - "13-09 through 13-11 (move, delete, audit plans)" tech_stack: added: - "Bounded collision retry loop in create_cloud_folder route (D-06)" - "_CREATE_FOLDER_MAX_RETRIES = 5 constant" - "upsert_cloud_item called from create_cloud_folder success path" - "upsert_cloud_item called from rename_cloud_item success path" - "update_folder_state called with warning/create_folder_mutated on create-folder success" - "update_folder_state called with warning/rename_mutated on rename success" - "update_folder_state called with warning/stale_listing on stale create-folder or rename" - "keep_both_name imported from services.cloud_operations for counter suffix generation" patterns: - "Reconcile-before-return: upsert + folder invalidation before success response" - "Bounded retry: up to 5 collision suffix attempts before returning conflict" - "Stale-guard: folder state invalidation on stale result before returning typed stale body" - "Non-success bypass: conflict/offline/reauth/stale never reach reconciliation upsert" key_files: modified: - path: "backend/api/cloud/operations.py" change: "create_cloud_folder: bounded retry + stale guard + reconcile-before-return; rename_cloud_item: stale guard + reconcile-before-return; new imports" - path: "backend/tests/test_cloud_mutations.py" change: "Added 10 behavioral tests (RED then GREEN) for Plan 08" decisions: - "Rename collision surfaced to user (they chose the name explicitly) — no auto-retry for rename" - "Create-folder retry uses keep_both_name counter suffix: 'Projects (1)', 'Projects (2)', etc." - "Stale guard for both operations uses update_folder_state with warning/stale_listing error_code" - "Reconciliation pattern follows Plan 06 upload pattern: upsert + folder state + session.commit in one atomic transaction" - "rename reconcile resolves parent_ref from existing CloudItem (if present) to invalidate correct folder" metrics: duration: "~20 minutes" completed: "2026-06-22" tasks_completed: 2 tasks_planned: 2 files_changed: 2 files_created: 0 tests_added: 10 tests_passing: 755 --- # Phase 13 Plan 08: Create-Folder and Rename Collision, Stale Guard, and Reconciliation Summary **One-liner:** Bounded collision retry (D-05/D-06), stale-guard with folder refresh (D-07), and reconcile-before-return reconciliation (T-13-26) for create-folder and rename mutations. ## Tasks Completed | Task | Name | Commit | Key Files | |------|------|--------|-----------| | 1 (RED) | Add failing collision retry, stale guard, and provider normalization tests | 9ad9946 | test_cloud_mutations.py | | 1/2 (GREEN) | Implement bounded retry, stale guard, and reconcile-before-return | aaa63c1 | api/cloud/operations.py | ## What Was Built ### Task 1: Collision Retry, Stale Guard, Provider Normalization (GREEN) **`create_cloud_folder` in `backend/api/cloud/operations.py`:** Added a bounded retry loop (up to `_CREATE_FOLDER_MAX_RETRIES = 5` attempts) around the adapter call: ```python for attempt in range(_CREATE_FOLDER_MAX_RETRIES + 1): result = await adapter.create_folder(parent_ref=..., name=candidate_name, ...) if kind == MUT_KIND_FOLDER: # reconcile and return if kind == MUT_KIND_CONFLICT and attempt < _CREATE_FOLDER_MAX_RETRIES: candidate_name = keep_both_name(body.name, attempt + 1) # 'Projects (1)', etc. continue if kind == MUT_KIND_STALE: # update_folder_state with warning/stale_listing and return stale body break ``` - D-05/D-06: Collision auto-suffixes `Projects (1)`, `Projects (2)`, etc. via `keep_both_name`; retries up to 5 times before surfacing conflict to client - D-07: Stale precondition calls `update_folder_state(warning, stale_listing)` before returning typed `{kind: 'stale'}` **`rename_cloud_item` in `backend/api/cloud/operations.py`:** - D-07: Stale result triggers `update_folder_state(warning, stale_listing)` using the existing CloudItem's `parent_ref` before returning typed `{kind: 'stale'}` - Rename collision is surfaced directly (user chose the name — no auto-retry) ### Task 2: Reconcile Create-Folder and Rename Success (GREEN) Both `create_cloud_folder` and `rename_cloud_item` now follow the reconcile-before-return pattern established by Plan 06 (upload): **create_cloud_folder success path:** ```python resource = CloudResource(provider_item_id=..., kind="folder", ...) await upsert_cloud_item(session, user_id=..., resource=resource) await update_folder_state(session, ..., refresh_state="warning", error_code="create_folder_mutated") await session.commit() ``` **rename_cloud_item success path:** ```python # Resolve existing item for parent_ref, kind, content_type, size existing_item = await session.execute(select(CloudItem).where(...)) resource = CloudResource(provider_item_id=..., name=resolved_name, ...) await upsert_cloud_item(session, user_id=..., resource=resource) await update_folder_state(session, ..., refresh_state="warning", error_code="rename_mutated") await session.commit() ``` Key design decisions: - `upsert_cloud_item` uses `provider_item_id` as stable identity key — existing rows are updated in place, preserving the DocuVault UUID - `update_folder_state` invalidates the parent folder (not the item itself) so the next browse triggers a provider re-list - Non-success paths (conflict, offline, reauth, stale) never reach the upsert/folder-state calls — preserving T-13-26 (no phantom metadata mutations on failure) ## Deviations from Plan ### Auto-fixed Issues None. The plan was executed exactly as written. **Scope note:** The plan listed `backend/services/cloud_operations.py` and `backend/services/cloud_items.py` as files modified. In practice, neither was modified — the existing `upsert_cloud_item`, `update_folder_state`, and `keep_both_name` helpers were sufficient. The route layer in `operations.py` consumed them directly via imports, consistent with the CLAUDE.md shared module map. ## Test Results ``` 755 passed, 17 skipped, 4 deselected, 12 xfailed ``` - `test_cloud_mutations.py`: 10 new tests, all pass - `test_cloud_backends.py`: unchanged, all pass - `test_cloud_provider_contract.py`: unchanged, all pass - Full backend suite: no regressions ## Known Stubs None. Create-folder and rename reconciliation are fully functional. The reconcile-before-return pattern is complete for create-folder and rename (analogous to Plan 06 for upload). ## Threat Flags No new security surfaces introduced. T-13-24, T-13-25, T-13-26 are mitigated: - **T-13-24 (collision handling):** Bounded retry loop enforces D-06; tests verify retry behavior and collision exhaustion. - **T-13-25 (stale mutation safety):** Stale guards stop mutations, refresh affected folder state, require retry — never forcing a stale create-folder or rename. - **T-13-26 (identity reconciliation):** Successful create-folder and rename route through `upsert_cloud_item` before success is returned. ## Self-Check: PASSED - FOUND: `backend/api/cloud/operations.py` with bounded retry, stale guards, and reconcile-before-return for both create_cloud_folder and rename_cloud_item - FOUND: `backend/tests/test_cloud_mutations.py` with 10 new behavioral tests - FOUND: `.planning/phases/13-virtual-local-cloud-operations/13-08-SUMMARY.md` - FOUND commit: `9ad9946` (Task 1 RED) - FOUND commit: `aaa63c1` (Task 1/2 GREEN) - Full suite: 755 passed, 17 skipped, 4 deselected, 12 xfailed ## Self-Check: PASSED (verified) - FOUND: `backend/api/cloud/operations.py` - FOUND: `backend/tests/test_cloud_mutations.py` - FOUND: `.planning/phases/13-virtual-local-cloud-operations/13-08-SUMMARY.md` - FOUND commit: `9ad9946` (RED tests) - FOUND commit: `aaa63c1` (GREEN implementation) - FOUND commit: `3e9355f` (docs/state)