- Add hydrate_and_cache_bytes to services/cloud_cache.py (Plan 06 cache lifecycle)
- Cache-hit path: pin existing entry, read from MinIO, release pin in finally
- Cache-miss path: fetch from provider, store in MinIO, create/reactivate entry, pin during response, release pin in finally
- CacheQuotaExceeded and MinIO failures are non-fatal (bytes returned from provider path)
- evict_lru_entries run after pin release to stay within user cache limit
- Update preview_cloud_file and download_cloud_file in operations.py to call hydrate_and_cache_bytes
- Version key computed from CloudItem metadata (no provider call required for key resolution)
- Shared _make_minio_cache_wrapper helper eliminates duplicate adapter code
- Falls through to direct provider fetch when no CloudItem row found
- Add _make_minio_cache_wrapper helper to operations.py (DRY, shared by preview and download)
- Add 5 new tests to test_cloud_cache.py: cache-miss fetches provider, cache-hit skips provider, pin released on hit, pin released on miss, user isolation
- Add 3 new security tests to test_cloud_security.py: preview response excludes object_key, download response excludes object_key, foreign-user IDOR via download blocked
- Implement services/cloud_analysis_processing.py with full pipeline:
queued -> downloading -> extracting -> classifying -> indexed
- Stale guard: version key comparison fires before provider byte fetch (T-14-13)
- Cache pin lifecycle: pin acquired before bytes used, released in finally block (T-14-12)
- No provider mutation methods called: adapter only used via get_object (T-14-03)
- No local Document row created: classification targets CloudItem directly
- Cooperative cancellation check at job/item level before each stage
- ProcessingResult dataclass with status, cache_entry_id, error_code fields
- Add 4 contract tests: status_transitions, stale_detection, pin_release, no_mutations
- cloud_analysis_versioning.py: compute_version_key with version>etag>fingerprint
precedence (D-19); compute_fingerprint helper; content_hash is optional post-
hydration only (D-20); raises ValueError only, no HTTPException
- cloud_analysis_settings.py: get_or_create_user_analysis_settings (upsert on
first access), update_user_analysis_settings with enum validation and tier-capped
cache limit, build_default_settings for API responses; raises ValueError only
- cloud_cache.py: Phase 14 durable cache functions added alongside existing Phase 12
in-memory TTL cache — create_cache_entry, list_cache_entries (owner-scoped),
evict_lru_entries (LRU, skips pin_count>0 and active_job_count>0), increment/
decrement_quota_for_cache (atomic UPDATE pattern); re-exports compute_version_key
from cloud_analysis_versioning so tests have one import site
Implemented write_audit_log calls for open_cloud_file, create_cloud_folder,
and rename_cloud_item in api/cloud/operations.py. Removed xfail markers from
test_cloud_audit.py. All 11 audit tests pass; 360 cloud tests pass total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move endpoint (D-07, D-08, D-09, T-13-27, T-13-28, T-13-29):
- Descendant destination detection via cloud_items ancestor-chain walk (D-09)
- Stale provider result marks source folder non-fresh, returns typed stale body (D-07)
- Success: upsert_cloud_item with new parent_ref before returning
- Success: update_folder_state for both source and destination folders
- Success: write_audit_log 'cloud.item_moved' with metadata-only payload
Delete endpoint (D-10, D-11, T-13-29):
- Pre-fetch item kind for D-10 folder-vs-file disclosure
- Success: update_folder_state for parent folder before returning
- Success: write_audit_log 'cloud.item_deleted' with delete_kind metadata
- Response carries is_folder/item_kind for frontend disclosure
- Failed deletes never emit audit rows
Promote xfail markers in test_cloud_audit.py for move and delete audit rows
- create_cloud_folder: bounded collision retry (up to 5 attempts) with keep_both_name counter suffix (D-05, D-06)
- create_cloud_folder: stale precondition marks parent folder non-fresh and returns typed stale result (D-07)
- rename_cloud_item: stale precondition marks parent folder non-fresh via folder state update (D-07)
- create_cloud_folder: reconcile-before-return upserts new folder in cloud_items and invalidates parent (T-13-26)
- rename_cloud_item: reconcile-before-return upserts renamed item and invalidates parent folder (T-13-26)
- Import upsert_cloud_item, update_folder_state, keep_both_name, CloudResource from service modules
- Test 1: collision auto-retry with bounded window (D-05, D-06)
- Test 2: stale guard updates folder state and returns typed stale kind (D-07)
- Test 3: create-folder and rename success upserts CloudItem and invalidates parent folder
- Test 4: failed rename does not mutate CloudItem rows
- Write cloud.file_uploaded audit row in same transaction as reconciliation
- Audit payload is metadata-only: filename, size_bytes, connection_id, provider_item_id, parent_ref
- No provider URLs, tokens, bytes, or document text in audit metadata (T-13-02)
- Non-success paths (conflict, offline, reauth_required) bypass audit write (T-13-21)
- Import get_client_ip from deps.utils and write_audit_log from services.audit
- Remove xfail marker from test_upload_success_writes_metadata_only_audit_row
- Add mock adapter so test isolates audit write logic without real provider I/O
- Test expects cloud.file_uploaded audit row with metadata-only payload (T-13-20)
- Upsert uploaded item into cloud_items for stable row identity and navigation
- Invalidate parent folder state (warning/upload_mutated) so next browse re-fetches
- Reconcile-before-return: session committed before response returned
- Failed/conflict uploads bypass reconciliation path (T-13-19)
- test_upload_success_returns_typed_uploaded_body: kind/provider_item_id/name/size
- test_upload_provider_retryable_error_returns_offline_kind: typed offline body
- test_upload_provider_conflict_returns_conflict_kind: provider-detected collision
- test_upload_reauth_result_is_typed_and_not_500: CONN-02 credential failure path
- test_upload_queue_control_semantics_are_backend_authored: all 4 queue fields
- test_upload_keep_both_name_format: route can use keep_both_name() from service
- test_upload_foreign_user_blocked: IDOR protection on upload route
- 7 new tests pass; full suite 741 passed
- connections.py: Update Google Drive OAuth to broader `drive` scope (D-17)
instead of `drive.file` so authorized users can operate on all existing Drive
items, not just files created by this app. Both initiation and callback flows
updated to keep scopes consistent.
- schemas.py: Add Phase 13 typed response schemas — ConnectionHealthOut, ReconnectOut,
ContentResultOut, MutationResultOut, and request schemas CreateFolderRequest,
RenameItemRequest, MoveItemRequest. All credential-free (T-13-14, CONN-03).
- api/cloud.js: Add centralized frontend helpers for getConnectionHealth,
reconnectCloudConnection, testCloudConnection (D-12, D-13), plus authorized
cloud content and mutation helpers: openCloudFile, previewCloudFile,
createCloudFolder, renameCloudItem, moveCloudItem, deleteCloudItem, uploadCloudFile.
All reconnect/health/test tests pass (14/14). Security suite passes (19/19).
- backend/tests/test_nextcloud_live.py: three read-only live tests behind
live_nextcloud marker (adapter root metadata, sanitized diagnostic, connection-ID
browse as testuser@docuvault.example with IDOR/admin negatives)
- backend/pytest.ini: register live_nextcloud marker; addopts excludes it from
ordinary runs so CI never contacts Nextcloud without explicit selection
- 12.1-VALIDATION.md: live test contract, commands, skip behaviour, sanitized
7-of-10 probe result, and threat references T-12.1-16 through T-12.1-20
- Manifest status: unconfirmed — Task 2 exact-name gate pending owner decision
Normalized cloud frontend for DocuVault v0.2.5:
- Cloud UI classifies items by kind=folder/file (is_dir removed from all active paths)
- Folder navigation uses provider_item_id (opaque provider ref) — DocuVault id kept for row identity
- Server freshness (refresh_state, last_refreshed_at) mapped verbatim — no browser-clock evidence
- Breadcrumb lineage built from explicit visited nodes — never split(provider_item_id)
- CloudFolderTreeItem: expandable/icon/navigate/loadChildren all use kind and provider_item_id
- Version bump 0.2.4 → 0.2.5
- SECURITY.md: T-12.1-11 through T-12.1-15 closed
- 369 tests pass, npm audit clean, production build clean