--- phase: "12" plan: "01" subsystem: cloud-resource-foundation tags: [cloud, metadata, capability, schema, migration, service] dependency_graph: requires: [] provides: - backend/storage/cloud_base.py — CloudCapability, CloudResource, CloudListing, CloudResourceAdapter - backend/migrations/versions/0006_cloud_resource_foundation.py — cloud_items, cloud_item_topics, cloud_folder_states tables - backend/db/models.py — CloudItem, CloudItemTopic, CloudFolderState ORM models - backend/services/cloud_items.py — reconcile_cloud_listing, upsert_cloud_item, resolve_owned_connection affects: - backend/db/models.py — extended with 3 new ORM classes tech_stack: added: - SQLAlchemy 2.0 async ORM models for cloud metadata - Alembic migration 0006 with cloud_items, cloud_item_topics, cloud_folder_states patterns: - Frozen dataclass value types for immutable capability/resource contracts - Composite (connection_id, provider_item_id) uniqueness for stable item identity - Soft-deletion reconciliation gated on CloudListing.complete=True - Domain exceptions (ConnectionNotFound, CloudItemNotFound) — no HTTPException in service key_files: created: - backend/storage/cloud_base.py - backend/migrations/versions/0006_cloud_resource_foundation.py - backend/services/cloud_items.py - backend/tests/test_cloud_capabilities.py - backend/tests/test_cloud_items.py modified: - backend/db/models.py decisions: - Frozen dataclasses for CloudCapability/CloudResource/CloudListing enforce immutability at Python level - parent_ref='' (empty string) represents connection root in cloud_folder_states to allow unique constraint - provider_size never referenced in quota service — D-18 compliance by design - No object_key field on CloudItem — provider bytes are not mirrored in Phase 12 - display_name_override column added to cloud_connections for user-customized names without uniqueness changes metrics: duration: "~15 minutes" completed: "2026-06-18" tasks_completed: 3 tasks_total: 3 files_created: 5 files_modified: 1 tests_added: 46 --- # Phase 12 Plan 01: Cloud Resource Foundation Summary **One-liner:** Provider-neutral CloudResourceAdapter contract, Alembic migration 0006 with durable owner-scoped cloud_items/cloud_folder_states tables, and idempotent reconciliation service with stable UUID identity across provider rename/move. ## What Was Built ### Task 1: Normalized cloud resource capabilities (0a7273b) `backend/storage/cloud_base.py` defines the Phase 12 read-only contract: - 9 action keys (browse, open, preview, upload, create_folder, rename, move, delete, change_tracking) - 3 capability states (supported, unsupported, temporarily_unavailable) - 6 reason codes (provider_unsupported, insufficient_scope, read_only, reauth_required, offline, item_restricted) - Frozen dataclasses: `CloudCapability`, `CloudResource`, `CloudListing` - Abstract `CloudResourceAdapter` with `list_folder`, `get_capabilities`, `merge_item_capabilities` - No mutation methods in Phase 12 interface — Phase 13 boundary enforced 29 unit tests covering vocabulary, validation, merge behavior, and a fake adapter proving no mutation methods exist. ### Task 2: Durable owner-scoped metadata schema (718fb2c) Migration `0006_cloud_resource_foundation.py` adds: - `cloud_items`: UUID PK, user_id + connection_id ownership FKs with CASCADE, unique (connection_id, provider_item_id), no object_key or retained-byte field - `cloud_item_topics`: association between CloudItem and Topic without requiring a Document row - `cloud_folder_states`: per-connection/parent-ref freshness row; parent_ref='' for root enables unique constraint - `cloud_connections`: `display_name_override` column for user-defined same-provider disambiguation ORM models `CloudItem`, `CloudItemTopic`, `CloudFolderState` added to `backend/db/models.py`. ### Task 3: Owner-scoped reconciliation service (718fb2c) `backend/services/cloud_items.py` implements: - `resolve_owned_connection(session, connection_id, user_id)` — raises `ConnectionNotFound` for cross-owner access - `list_cloud_children(session, user_id, connection_id, parent_ref)` — composite owner+connection scope - `upsert_cloud_item(session, user_id, resource)` — preserves CloudItem UUID across rename/move - `reconcile_cloud_listing(session, ...)` — soft-deletes missing items only when `CloudListing.complete=True` - `get_or_create_folder_state` / `update_folder_state` — idempotent, controlled error_code/message 17 tests covering rename/move stable identity, complete removal, incomplete-listing retention, owner isolation, and idempotency. ## Verification - `pytest -q tests/test_cloud_capabilities.py tests/test_cloud_items.py` — 46 passed - `rg "HTTPException" backend/services/cloud_items.py` — no matches - No Phase 12 task writes provider file bytes or MinIO objects ## Deviations from Plan None — plan executed exactly as written. Service module (`cloud_items.py`) was created during Task 2 preparation since tests required the import; it was committed in the Task 2 commit which also covered the Task 3 deliverable. ## Threat Flags None. All T-12-01 through T-12-09 mitigations addressed: - T-12-01: Composite owner+connection predicates enforced in every service function - T-12-02: Connection UUID in uniqueness constraint and ownership checks - T-12-06: Read-only interface verified by fake adapter test - T-12-07: Deletion gated on `complete=True` in `reconcile_cloud_listing` - T-12-09: `provider_size` never touches quota service — confirmed by `test_model_quota_unchanged_after_item_upsert` ## Self-Check: PASSED Files exist: - backend/storage/cloud_base.py — FOUND - backend/migrations/versions/0006_cloud_resource_foundation.py — FOUND - backend/services/cloud_items.py — FOUND - backend/tests/test_cloud_capabilities.py — FOUND - backend/tests/test_cloud_items.py — FOUND Commits: - 0a7273b — feat(12-01): normalized cloud resource capability contract and unit tests - 718fb2c — feat(12-01): durable owner-scoped cloud metadata schema (migration 0006 + models)