---
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)
