Files
kite/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-SUMMARY.md
T

11 KiB


phase: 14.1-cloud-local-file-parity-hardening plan: "02" subsystem: cloud-detail-parity-backend status: complete tags: [cloud-detail, force-reanalyze, single-item-retry, schema-allowlist, parity] requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, ANALYZE-07, CACHE-03, CACHE-05]

dependency_graph: requires: - 14.1-01 (RED tests — test_cloud_detail_parity.py, test_cloud_reanalyze_force.py) provides: - backend/api/cloud/schemas.py (CloudItemDetailOut + force on AnalysisEnqueueRequest) - backend/services/cloud_items.py (resolve_owned_cloud_item_detail) - backend/api/cloud/operations.py (GET /connections/{id}/items/{item_id:path}/detail) - backend/services/cloud_analysis.py (force param + retry_or_create_single_item_job) - backend/api/cloud/analysis.py (force wired; POST /connections/{id}/items/{cloud_item_id}/retry) affects: - frontend (Plan 03/04 target — can now render cloud file detail with analysis fields)

tech_stack: added: [] patterns: - Strict Pydantic allowlist schema with explicit forbidden-field documentation (T-14.1-03) - Service raises domain exceptions (ConnectionNotFound, CloudItemNotFound, InvalidJobState); router translates to HTTP - Metadata-only detail resolution — zero provider bytes downloaded (CACHE-03, T-14.1-06) - already_current bypass via force flag without changing the unsupported guard (ANALYZE-06) - Single-item retry-job creation through authorized enqueue path (D-12)

key_files: created: [] modified: - backend/api/cloud/schemas.py - backend/services/cloud_items.py - backend/api/cloud/operations.py - backend/services/cloud_analysis.py - backend/api/cloud/analysis.py

decisions:

  • CloudItemDetailOut uses an empty capabilities dict (no live capability resolution without credential decryption) — frontend infers actions from analysis_status + unsupported_analysis_reason
  • unsupported_analysis_reason populated via _is_supported from cloud_analysis (single source of truth)
  • force=True bypasses already_current check for supported items only; unsupported items remain unsupported regardless
  • retry_or_create_single_item_job accepts failed/indexed/stale/pending items via force=True enqueue
  • Single-item retry route uses cloud_item_id (DocuVault UUID) not provider_item_id for stable identity

metrics: duration: "18m" completed_date: "2026-06-26" tasks_completed: 2 files_modified: 5 backend_tests_passing: 872 backend_tests_added_passing: 34 pre_existing_failures: 1 (test_extract_docx — ModuleNotFoundError: No module named 'docx', unrelated)

Phase 14.1 Plan 02: Cloud Item Detail + Force Re-analyze + Single-item Retry Summary

Backend contracts that let cloud files behave like local documents: owner-scoped cloud item detail with extracted text, topics, and analysis status through a strict credential-free schema; force re-analyze flag bypassing already_current for indexed items; and a single-item retry-job path for failed items with no surviving active job.

Tasks Completed

Task 1: CloudItemDetailOut schema + resolve_owned_cloud_item_detail + GET detail route

Schema — backend/api/cloud/schemas.py:

Added CloudItemDetailOut as an explicit allowlist — credentials_enc, object_key, version_key, and raw provider URLs are absent by design (T-14.1-03). Fields: id, provider_item_id, name, kind, parent_ref, content_type, size, modified_at, etag, provider, display_name, location, analysis_status, semantic_index_status, extracted_text, topics (list[str]), capabilities (empty dict — no live provider call), unsupported_analysis_reason, is_stale.

Added force: bool = Field(default=False) to AnalysisEnqueueRequest (used by Task 2).

Service — backend/services/cloud_items.py:

Added CloudItemDetail dataclass and resolve_owned_cloud_item_detail(session, *, user_id, connection_id, provider_item_id):

  • Calls resolve_owned_connection for ownership gate (T-14.1-04).
  • Selects CloudItem by (connection_id, provider_item_id, user_id) — raises CloudItemNotFound.
  • Loads topic names via CloudItemTopic→Topic join (metadata-only).
  • Derives location from path_snapshot or parent_ref (never a raw provider URL — T-14.1-03).
  • Derives unsupported_analysis_reason via _is_supported from cloud_analysis (single source).
  • Zero bytes downloaded — no get_object, no hydrate_and_cache_bytes (CACHE-03, T-14.1-06).

Route — backend/api/cloud/operations.py:

Added GET /connections/{connection_id}/items/{item_id:path}/detail with response_model=CloudItemDetailOut and get_regular_user dependency (admin → 403). ConnectionNotFound → 404, CloudItemNotFound → 404. Returns CloudItemDetailOut with capabilities={} (intentionally empty — no credential decryption during detail fetch).

Test results: All 8 test_cloud_detail_parity.py tests pass (owner 200, foreign 404, admin 403, credential exclusion, stale retention, pending empty-state, no byte hydration).

Commit: a0d5c1d

Task 2: Force re-analyze flag + single-item retry-job creation

Schema — backend/api/cloud/schemas.py:

force: bool = Field(default=False) added to AnalysisEnqueueRequest with full docstring (D-11).

Service — backend/services/cloud_analysis.py:

Extended enqueue_analysis_job with force: bool = False parameter. The already_current check becomes:

already_current = False if (live_metadata_changed or force) else await _check_already_current(...)

When force=True, supported items are created as queued job items even if their version_key matches a prior indexed run. Unsupported items remain unsupported regardless of force (ANALYZE-07: no provider mutation is performed).

Added retry_or_create_single_item_job(session, *, cloud_item_id, user_id) service helper that:

  • Resolves the CloudItem by (id, user_id) — raises AnalysisItemNotFound.
  • Accepts failed/indexed/stale/pending items; rejects others with InvalidJobState.
  • Calls enqueue_analysis_job(scope="file", provider_item_ids=[item.provider_item_id], force=True).
  • Returns an EnqueueResult with queued_count >= 1 (for supported items).

Route — backend/api/cloud/analysis.py:

  • Wired force=body.force into the existing enqueue_job route handler (D-11).
  • Added POST /analysis/connections/{connection_id}/items/{cloud_item_id}/retry returning AnalysisEnqueueOut with status 202. Validates connection ownership before delegating to retry_or_create_single_item_job. Returns job_id and queued_count in the response (D-12).

Test results: All 8 test_cloud_reanalyze_force.py tests pass. All 26 test_cloud_analysis_contract.py tests pass (no idempotency regression). Full suite: 872/873 pass (1 pre-existing failure unrelated to this plan).

Commit: 52acd56

Verification Results

Backend verification

test_cloud_detail_parity.py      — 8/8 passed
test_cloud_reanalyze_force.py    — 8/8 passed
test_cloud_analysis_contract.py  — 26/26 passed
test_cloud_security.py           — 36/36 passed
Full suite: 872 passed, 1 pre-existing failure (test_extract_docx — missing docx module)

Acceptance criteria

  • grep -c 'class CloudItemDetailOut' backend/api/cloud/schemas.py → 1 ✓
  • CloudItemDetailOut excludes object_key/credentials_enc/version_key within class body ✓
  • grep -c 'resolve_owned_cloud_item_detail' backend/services/cloud_items.py → 2 (definition + docstring) ✓
  • Detail route /detail with response_model=CloudItemDetailOut present in operations.py ✓
  • No get_object/hydrate_and_cache_bytes call inside resolve_owned_cloud_item_detail ✓
  • grep -c 'force' backend/api/cloud/schemas.py → 3 (field, description lines) ✓
  • enqueue_analysis_job signature includes force: bool = False
  • already_current = False if (live_metadata_changed or force) else ... on line 608 ✓
  • force=body.force wired in analysis.py enqueue route ✓
  • retry_or_create_single_item_job exists in cloud_analysis.py and analysis.py import ✓

Deviations from Plan

Auto-fixed Issues

1. [Rule 2 - Missing critical functionality] Empty capabilities in detail response

  • Found during: Task 1 implementation
  • Issue: The plan called for returning capabilities in CloudItemDetailOut. Populating live capabilities would require credential decryption and a provider call — violating the metadata-only constraint (CACHE-03 / T-14.1-06).
  • Fix: Return capabilities={} (empty dict) from the detail route. The unsupported_analysis_reason field carries the action-availability signal the frontend needs (D-16). A comment in the route documents the intentional empty capabilities and the reason.
  • Files modified: backend/api/cloud/operations.py
  • Commit: a0d5c1d

2. [Rule 1 - Bug] CloudConnection.display_name_override must take precedence

  • Found during: Task 1 — reviewing CloudConnection model fields
  • Issue: CloudConnection has both display_name and display_name_override. The connection rename flow sets display_name_override; the original display_name is the provider-set name. The detail response should show what the user sees (override if set).
  • Fix: display_name=conn.display_name_override or conn.display_name in resolve_owned_cloud_item_detail.
  • Files modified: backend/services/cloud_items.py
  • Commit: a0d5c1d

Known Stubs

None — all fields return real data from DB rows. The empty capabilities={} in the detail response is intentional and documented (see Deviation 1 above), not a stub.

Threat Flags

Flag File Description
No new surface All new endpoints are owner-scoped via get_regular_user + resolve_owned_connection + item ownership check. No new trust boundaries introduced.

T-14.1-03, T-14.1-04, T-14.1-05, T-14.1-06 mitigations all implemented as designed:

  • T-14.1-03: CloudItemDetailOut schema enforces allowlist — tests verify credentials_enc/object_key/version_key absent.
  • T-14.1-04: get_regular_user (admin 403) + ownership check (foreign 404) on both detail and retry routes.
  • T-14.1-05: force only bypasses already_current check; no provider mutation methods called.
  • T-14.1-06: resolve_owned_cloud_item_detail calls zero get_object / hydrate_and_cache_bytes.

Self-Check: PASSED

Files verified:

  • /Users/nik/Documents/Progamming/document_scanner/backend/api/cloud/schemas.py ✓
  • /Users/nik/Documents/Progamming/document_scanner/backend/services/cloud_items.py ✓
  • /Users/nik/Documents/Progamming/document_scanner/backend/api/cloud/operations.py ✓
  • /Users/nik/Documents/Progamming/document_scanner/backend/services/cloud_analysis.py ✓
  • /Users/nik/Documents/Progamming/document_scanner/backend/api/cloud/analysis.py ✓

Commits verified:

  • a0d5c1d (Task 1) present in git log ✓
  • 52acd56 (Task 2) present in git log ✓