enqueue route forwards force= to enqueue_analysis_job
force=
Add the backend surface that makes cloud files behave like local documents: an owner-scoped cloud item detail endpoint that returns extracted text, analysis status, topics, and subtle source metadata through a strict credential-free schema; a force re-analyze flag that lets a user re-queue an already-current cloud item; and a single-item retry-job creation path so a failed item can be retried even when no active job survives.
Purpose: Implements D-01..D-08, D-11, D-12, D-15, D-16 backend contracts so the frontend (Plan 03/04) can render parity without forking. Satisfies CLOUD-02 (authorized open/preview/view), ANALYZE-05 (retry), ANALYZE-06 (idempotency with explicit force override), ANALYZE-07 (no provider mutation), CACHE-03/CACHE-05 (bytes only via cache, owner-scoped).
Output: CloudItemDetailOut schema, resolve_owned_cloud_item_detail service helper, GET detail route, force-aware enqueue, single-item retry-job path — all covered by Plan 01 tests turning GREEN.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md
@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md
@CLAUDE.md
@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
@backend/db/models.py
@backend/tests/test_cloud_detail_parity.py
@backend/tests/test_cloud_reanalyze_force.py
Task 1: CloudItemDetailOut schema + resolve_owned_cloud_item_detail service + GET detail route
- backend/api/cloud/schemas.py (CloudItemOut, CloudCapabilityOut, CacheStatusOut allowlist style — model the new schema on these)
- backend/services/cloud_items.py (resolve_owned_connection, list_cloud_children, ListingResult — reuse owner-scoping pattern; service raises ValueError/domain exception, never HTTPException)
- backend/api/cloud/operations.py (open/preview/download routes, _resolve_and_get_adapter, get_regular_user dependency, parse_uuid usage, JSONResponse vs HTTPException convention)
- backend/db/models.py (CloudItem fields: extracted_text, analysis_status, semantic_index_status, provider_size, content_type, modified_at, parent_ref, path_snapshot; CloudItemTopic ↔ Topic join; CloudConnection.provider/display_name)
- backend/tests/test_cloud_detail_parity.py (the exact field names and forbidden tokens the route must satisfy)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-04, D-05, D-07, D-08, D-15, D-16, D-18)
In backend/api/cloud/schemas.py add class CloudItemDetailOut(BaseModel) as an explicit allowlist modeled on CloudItemOut + CacheStatusOut. Fields (and ONLY these — no credentials_enc, object_key, version_key, provider URL, token, fingerprint): id (DocuVault UUID str), provider_item_id, name, kind, parent_ref, content_type, size, modified_at, etag, provider (connection provider string), display_name (connection display name), location (human path_snapshot or parent_ref, no provider URL), analysis_status, semantic_index_status, extracted_text (Optional[str]), topics (list[str] of topic names), capabilities (dict[str, CloudCapabilityOut]), unsupported_analysis_reason (Optional[str], populated from capabilities when analyze is unsupported per D-16), is_stale (bool derived from analysis_status == "stale" per D-07). Document at class top that object_key/credentials_enc are absent by design (mirror the T-14-02 docstring pattern). In backend/services/cloud_items.py add async def resolve_owned_cloud_item_detail(session, *, user_id, connection_id, provider_item_id) that: resolves the connection via resolve_owned_connection (owner check), selects the CloudItem by (connection_id, provider_item_id) scoped to user_id, raises a domain exception / ValueError when not found (router maps to 404), loads topic names via a CloudItemTopic→Topic join, and returns a plain dataclass/dict the router maps into CloudItemDetailOut. This helper performs metadata-only DB reads — it MUST NOT call adapter.get_object or hydrate_and_cache_bytes (T-14-04 / CACHE-03). In backend/api/cloud/operations.py register GET /connections/{connection_id}/items/{item_id:path}/detail with response_model=CloudItemDetailOut and dependency get_regular_user (admin blocked); parse connection_id with parse_uuid; call resolve_owned_cloud_item_detail; on the domain not-found exception raise HTTPException(404) (router layer translates, per CLAUDE.md service-vs-router rule); never decrypt or expose credentials. Do not add a parallel detail router file — keep it on the existing operations router so it shares the /api/cloud prefix.
cd backend && python -m pytest tests/test_cloud_detail_parity.py -x 2>&1 | tail -20
- `grep -c 'class CloudItemDetailOut' backend/api/cloud/schemas.py` returns 1.
- CloudItemDetailOut has no forbidden fields: `grep -E 'object_key|credentials_enc|version_key' backend/api/cloud/schemas.py | grep -A0 -i detail` is empty within the class body (manual confirm the class block excludes them).
- `grep -c 'resolve_owned_cloud_item_detail' backend/services/cloud_items.py` returns >= 1 (definition present).
- The detail route exists: `grep -F '/detail' backend/api/cloud/operations.py` returns a route line with `response_model=CloudItemDetailOut`.
- The detail service does not hydrate bytes: `grep -n 'get_object\|hydrate_and_cache_bytes' backend/services/cloud_items.py` shows no new call inside resolve_owned_cloud_item_detail.
- test_cloud_detail_parity.py passes: owner gets fields, foreign user 404, admin 403, response has no forbidden tokens, stale preserves extracted_text/topics.
Cloud detail endpoint returns analysis fields + source metadata for the owner through a credential-free schema, blocks foreign user/admin, and downloads no bytes; Plan 01 detail tests pass.
Task 2: Force re-analyze flag + single-item retry-job creation
- backend/services/cloud_analysis.py (enqueue_analysis_job signature, already_current branch ~line 546-660, retry_job_item ~line 1049, _check_already_current, EnqueueResult)
- backend/api/cloud/schemas.py (AnalysisEnqueueRequest — add force; AnalysisEnqueueOut)
- backend/api/cloud/analysis.py (enqueue_job route ~line 137, retry route, _build_job_out, AnalysisControlOut)
- backend/tests/test_cloud_reanalyze_force.py (exact force=true and single-item retry assertions)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-11, D-12)
- .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md (Forced Re-Analyze Pattern section)
Add force: bool = Field(default=False) to AnalysisEnqueueRequest in backend/api/cloud/schemas.py (default preserves existing idempotency). Extend enqueue_analysis_job in backend/services/cloud_analysis.py to accept force: bool = False; when force is True, skip the already_current branch so supported items are created as queued job items even if their version_key matches an indexed item — the already_current check becomes (already_current and not force). Forced enqueue must still: validate owner/connection/item and unsupported type (unsupported items stay unsupported), compute version_key/fingerprint normally, and create queued items only — it MUST NOT call any provider mutation (no rename/move/delete) and MUST NOT bypass the cache lifecycle; processing remains process_cloud_analysis_item + hydrate_and_cache_bytes (ANALYZE-07, CACHE-03). Wire force through the enqueue_job route in backend/api/cloud/analysis.py: pass body.force into enqueue_analysis_job(...). For D-12, add an owner-scoped single-item retry path: when a failed item cannot be retried within an existing job (no surviving active job, or retry_job_item raises AnalysisJobNotFound for that cloud_item_id), create a one-item analysis job for that cloud_item_id via the existing enqueue_analysis_job(scope="file", provider_item_ids=[provider_item_id], force=True) so a fresh queued item is produced through the authorized path. Implement this fallback either in the retry route handler (catch the not-found/invalid-state domain exception and create the single-item job) or as a thin service helper (e.g. retry_or_create_single_item_job) in cloud_analysis.py — choose the service helper if both the route and tests need it. Keep all aggregate counters consistent and return the existing typed result schemas (AnalysisControlOut / AnalysisEnqueueOut). Do not raise HTTPException from the service layer — raise domain exceptions and translate in the route.
cd backend && python -m pytest tests/test_cloud_reanalyze_force.py tests/test_cloud_analysis_contract.py -x 2>&1 | tail -20
- `grep -c 'force' backend/api/cloud/schemas.py` shows force added to AnalysisEnqueueRequest (>= 1 occurrence near the class).
- enqueue_analysis_job accepts force: `grep -n 'def enqueue_analysis_job' backend/services/cloud_analysis.py` and the signature/body reference force.
- The already_current branch respects force: `grep -n 'already_current and not force\|not force' backend/services/cloud_analysis.py` returns >= 1.
- The enqueue route forwards force: `grep -F 'force=' backend/api/cloud/analysis.py` returns >= 1 (or body.force passed positionally is visible).
- Single-item retry fallback exists: `grep -nE 'retry_or_create_single_item_job|scope="file"|single' backend/services/cloud_analysis.py backend/api/cloud/analysis.py` shows the fallback path.
- test_cloud_reanalyze_force.py passes: default skips already-current, force re-queues, no provider mutation, single-item retry yields one queued item. test_cloud_analysis_contract.py still passes (no regression in default idempotency).
force=true re-queues already-current items without provider mutation, default enqueue is unchanged, and failed items can be retried via a single-item job when no active job exists; Plan 01 force/retry tests pass with no idempotency regression.
<threat_model>
Trust Boundaries
Boundary
Description
browser → GET detail
connection_id/provider_item_id untrusted; must resolve under current_user.id only
browser → POST enqueue (force)
force is a user-driven flag; must not enable cross-user enqueue or provider mutation
service → DB
metadata-only reads; no byte hydration during detail
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-14.1-03
Information Disclosure
CloudItemDetailOut
mitigate
Allowlist schema excludes credentials_enc/object_key/version_key/provider URL; Plan 01 no-leak test enforces