diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 3d79797..1b48d9e 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,7 +1,7 @@ # DocuVault Roadmap: v0.3 Reimagining Cloud Storage integration **Status:** Proposed -**Phases:** 12-16 +**Phases:** 12-16 plus inserted 12.1, 14.1, and 14.2 **Requirements:** 36 **Defined:** 2026-06-17 @@ -26,6 +26,8 @@ Before any phase is marked complete: | 12 | 6/6 | Complete | 2026-06-21 | | 13 | 11/11 | Complete | 2026-06-23 | | 14 | 9/9 | Complete | 2026-06-23 | +| 14.1 | Cloud/Local File Parity Hardening | Make cloud open, view, download, analyze, and analyzed-file display match local-file behavior end to end | CLOUD-02, ANALYZE-01..07, CACHE-03..05 | +| 14.2 | Cross-Codebase Review and Cleanup | Cross-reference backend/frontend code, remove duplication and dead code, consolidate shared paths, and fix inefficiencies without behavior changes | Cross-cutting quality gates | | 15 | Unified Smart Search | Search local and analyzed cloud documents by exact text or semantic ideas without downloading files during queries | SEARCH-01..07 | | 16 | Change Tracking and Reliability | Detect external provider changes, mark stale indexes, remove deleted items, and harden refresh behavior | SYNC-02..04 | @@ -134,11 +136,54 @@ Plans: 4. Cloud bytes are cached only for active opening, preview, or analysis work and are evicted by configurable limits without interrupting pinned active jobs. 5. Cache ownership, cache-key versioning, cancellation, duplicate-job, failure-retry, and eviction behavior have dedicated tests. +### Phase 14.1: Cloud/Local File Parity Hardening (INSERTED) + +**Goal:** Cloud files opened, viewed, downloaded, analyzed, and displayed in results use the same user-facing behavior as local files, while preserving provider ownership, cache boundaries, authorization, and no-provider-mutation guarantees. +**Requirements:** CLOUD-02, ANALYZE-01, ANALYZE-02, ANALYZE-03, ANALYZE-04, ANALYZE-05, ANALYZE-06, ANALYZE-07, CACHE-03, CACHE-04, CACHE-05 +**Depends on:** Phase 14 +**Plans:** 5 plans +**Execution waves:** Wave 1: 14.1-01 (RED tests); Wave 2: 14.1-02 after 01 (backend detail/force/retry); Wave 3: 14.1-03 after 01-02 (shared detail surface + cloud route/view); Wave 4: 14.1-04 after 01-03 (browser row parity + cloud row navigation); Wave 5: 14.1-05 after 01-04 (gates, security, docs, version, commit). + +Plans: + +- [ ] 14.1-01-PLAN.md — RED backend + frontend parity tests (cloud detail endpoint, force re-analyze, single-item retry, route + row parity). +- [ ] 14.1-02-PLAN.md — Backend cloud detail endpoint (CloudItemDetailOut + resolve_owned_cloud_item_detail), force re-analyze flag, single-item retry-job creation. +- [ ] 14.1-03-PLAN.md — Shared DocumentDetailSurface extraction, DocumentView refactor (Re-analyze copy), CloudDetailView + cloud-file-detail route + getCloudItemDetail. +- [ ] 14.1-04-PLAN.md — Browse row topics/analysis_status/stale, StorageBrowser row parity via store translator, cloud row click → cloud detail route (no auto preview/download). +- [ ] 14.1-05-PLAN.md — Full test suites, security gate, dependency/secret audits, CLAUDE.md/README updates, version bump, atomic commit. + +**Success Criteria:** + +1. Cloud and local file rows/cards expose equivalent open, view, download, analyze, retry, and reanalyze states through `StorageBrowser.vue`. +2. Analyzed cloud files surface extracted text, topics, analysis status, stale/current state, and retry/reanalyze affordances wherever equivalent local file data appears. +3. Authorized cloud open, preview, and download never expose provider URLs or credentials and hydrate bytes only through the existing cache lifecycle. +4. Unsupported or provider-limited cloud actions use typed responses and shared UI states instead of creating local/cloud UX forks. +5. End-to-end parity tests cover local versus cloud workflows for open, preview/download fallback, analyze, status display, retry, and ownership negatives. + +### Phase 14.2: Cross-Codebase Review and Cleanup (INSERTED) + +**Goal:** Cross-reference the full codebase, remove duplicate logic, consolidate shared helpers/components/services, improve inefficient paths, delete dead code, and preserve behavior. +**Requirements:** Cross-cutting quality gates +**Depends on:** Phase 14.1 +**Plans:** 0 plans + +Plans: + +- [ ] TBD (run /gsd-plan-phase 14.2 to break down) + +**Success Criteria:** + +1. Backend routers, services, providers, and tasks are audited for duplicated helper logic, raw inline orchestration, repeated parsing/formatting, and inefficient query/cache paths. +2. Frontend views, components, stores, and utilities are audited for duplicated browser logic, formatters, provider styling, tree behavior, and local/cloud branching. +3. Shared module maps and non-negotiable rules in `AGENTS.md` are updated for any newly centralized helpers or components. +4. Unused files, dead imports, stale tests, obsolete planning references, and unreachable code are removed in the same cleanup work. +5. Full backend, frontend, security, audit, dependency, secret-scan, and rendered UI gates pass after behavior-preserving cleanup. + ### Phase 15: Unified Smart Search **Goal:** One search experience finds local and analyzed cloud documents by keywords, sentences, or semantic ideas and opens cloud results through on-demand hydration. -**Depends on:** Phase 14 +**Depends on:** Phase 14.2 **Requirements:** SEARCH-01, SEARCH-02, SEARCH-03, SEARCH-04, SEARCH-05, SEARCH-06, SEARCH-07 **Success Criteria:** diff --git a/.planning/STATE.md b/.planning/STATE.md index 91f63e6..769499f 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ milestone_name: Reimagining Cloud Storage integration current_phase: 14.1 current_phase_name: cloud-local-file-parity-hardening status: not_planned -stopped_at: Phase 14.1 context gathered -last_updated: "2026-06-26T17:23:14.358Z" +stopped_at: Phase 14.1 UI-SPEC approved +last_updated: "2026-06-26T18:51:13.000Z" last_activity: 2026-06-26 -last_activity_desc: Inserted Phase 14.1 cloud/local parity hardening and Phase 14.2 cross-codebase review and cleanup +last_activity_desc: Approved Phase 14.1 UI design contract progress: total_phases: 8 completed_phases: 4 @@ -20,15 +20,15 @@ progress: # Project State **Project:** DocuVault -**Status:** Phase 14.1 pending planning — Cloud/Local File Parity Hardening +**Status:** Phase 14.1 UI-SPEC approved; pending planning — Cloud/Local File Parity Hardening **Last Updated:** 2026-06-26 ## Current Position Phase: 14.1 (cloud-local-file-parity-hardening) — NOT PLANNED Plan: 0 of 0 -Status: Phase 14 complete. Next: plan inserted Phase 14.1. -Last activity: 2026-06-26 — Inserted Phase 14.1 cloud/local parity hardening and Phase 14.2 cross-codebase review and cleanup +Status: Phase 14 complete. Phase 14.1 UI-SPEC approved. Next: plan inserted Phase 14.1. +Last activity: 2026-06-26 — Approved Phase 14.1 UI design contract ## Phase Status @@ -116,16 +116,16 @@ None. ## Session Continuity -**Stopped at:** Phase 14.1 context gathered +**Stopped at:** Phase 14.1 UI-SPEC approved _Updated at each phase transition._ | Field | Value | |---|---| -| Last session | 2026-06-26T17:23:14.347Z | +| Last session | 2026-06-26T18:51:13.000Z | | Next action | Plan 14.1 | | Pending decisions | None | -| Resume file | .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md | +| Resume file | .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md | ## Decisions diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-01-PLAN.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-01-PLAN.md new file mode 100644 index 0000000..fe2a85c --- /dev/null +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-01-PLAN.md @@ -0,0 +1,189 @@ +--- +phase: 14.1-cloud-local-file-parity-hardening +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - backend/tests/test_cloud_detail_parity.py + - backend/tests/test_cloud_reanalyze_force.py + - frontend/src/views/__tests__/CloudDetailParity.test.js + - frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js +autonomous: true +requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, ANALYZE-07, CACHE-03, CACHE-05] +must_haves: + truths: + - "A failing backend test asserts a cloud detail endpoint returns extracted_text, analysis_status, topics, and source metadata for the owner" + - "A failing backend test asserts the cloud detail response excludes credentials_enc, object_key, and raw provider URLs" + - "A failing backend test asserts a foreign user gets 404 and an admin is blocked from the cloud detail endpoint" + - "A failing backend test asserts force re-analyze enqueues an already-current item while default enqueue still skips it" + - "A failing frontend test asserts a cloud detail route renders the same core sections as local document detail" + - "A failing frontend test asserts cloud and local file rows both navigate to a detail view and both show topic badges and analysis status in the same slot" + artifacts: + - path: "backend/tests/test_cloud_detail_parity.py" + provides: "RED backend tests for cloud detail endpoint fields, auth/cache boundaries, and route-level parity" + min_lines: 80 + - path: "backend/tests/test_cloud_reanalyze_force.py" + provides: "RED backend tests for force re-analyze and single-item retry job creation" + min_lines: 60 + - path: "frontend/src/views/__tests__/CloudDetailParity.test.js" + provides: "RED frontend tests for cloud detail route + paired local/cloud detail section parity" + min_lines: 60 + - path: "frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js" + provides: "RED frontend tests for paired local/cloud row parity (topic/status/action slots, Re-analyze copy)" + min_lines: 60 + key_links: + - from: "backend/tests/test_cloud_detail_parity.py" + to: "backend/api/cloud/operations.py" + via: "httpx AsyncClient GET against the new cloud detail route" + pattern: "/detail|/item" + - from: "frontend/src/views/__tests__/CloudDetailParity.test.js" + to: "frontend/src/router/index.js" + via: "router resolves a named cloud detail route" + pattern: "cloud-file-detail" +--- + + +Create the RED (failing) test suites that pin down Phase 14.1 parity behavior before any implementation exists: an owner-scoped cloud detail endpoint with analysis fields and strict allowlist, force re-analyze + single-item retry semantics, a cloud detail route, and paired local/cloud parity for browser rows and detail surfaces. + +Purpose: Lock the contract first so backend (Plan 02) and frontend (Plan 03/04) implementations have an executable target. Per CLAUDE.md Testing Protocol, every feature requires tests; these are written first so the Nyquist `` gates in later plans are real. +Output: Two backend test files and two frontend test files that fail (or are skipped-pending) against the current codebase because the detail endpoint, route, force flag, and shared detail surface do not exist yet. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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 +@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md +@CLAUDE.md +@backend/tests/test_cloud_security.py +@backend/tests/test_cloud_analysis_contract.py +@frontend/src/views/__tests__/CloudFolderView.test.js +@frontend/src/views/__tests__/FileManagerView.test.js +@frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js + + + + + + Task 1: RED backend tests — cloud detail endpoint contract + force re-analyze + single-item retry + + - backend/tests/test_cloud_security.py (owner/admin/no-leak fixture + assertion patterns to reuse) + - backend/tests/test_cloud_analysis_contract.py (analysis enqueue/idempotency fixture patterns) + - backend/api/cloud/schemas.py (CloudItemOut, AnalysisEnqueueRequest — fields that DO and do NOT exist today) + - backend/api/cloud/operations.py (existing open/preview/download route shapes and path patterns) + - backend/services/cloud_analysis.py (enqueue_analysis_job and retry_job_item current signatures) + - backend/db/models.py (CloudItem.extracted_text/analysis_status, CloudItemTopic, Topic) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-05, D-07, D-08, D-11, D-12, D-18) + + + - Test: owner GET of the cloud detail endpoint returns 200 with extracted_text, analysis_status, semantic_index_status, topics (list of topic names), provider, location/parent metadata, and capability/unsupported reasons. (D-05) + - Test: cloud detail response JSON contains no key matching credentials_enc, object_key, version_key, password, or a raw http(s) provider URL. (D-18, T-14-02) + - Test: a different user GET of another user's cloud item detail returns 404; an admin token GET returns 403 (get_regular_user). (D-18) + - Test: stale item detail still returns prior extracted_text and topics with analysis_status reflecting stale. (D-07) + - Test: default enqueue of an already-indexed/current item yields already_current_count >= 1 and queued_count 0; the same enqueue with force=true yields queued_count >= 1 for that item. (D-11, ANALYZE-06) + - Test: forced re-analyze does not mutate the provider (no rename/move/delete adapter call) and routes byte work through the existing analysis path. (ANALYZE-07) + - Test: retry for a failed item with no surviving active job creates a single-item retry job (or returns a typed result that yields one queued item). (D-12, ANALYZE-05) + + + Per D-20, these parity/security tests use mocked provider contracts for determinism (no live provider calls); preserve opt-in live tests only where existing patterns already support them — do not add new live-provider dependencies. Create backend/tests/test_cloud_detail_parity.py and backend/tests/test_cloud_reanalyze_force.py using the async httpx.AsyncClient + real-PostgreSQL fixtures already used in test_cloud_security.py and test_cloud_analysis_contract.py. Reuse existing fixtures for authenticated owner client, a second non-owner user, and an admin user; do not invent a new auth harness. Target the cloud detail endpoint at the path the implementation will add — use GET /api/cloud/connections/{connection_id}/items/{item_id:path}/detail (the path Plan 02 implements); assert against response_model CloudItemDetailOut field names: extracted_text, analysis_status, semantic_index_status, topics, provider, display_name, parent_ref/location, modified_at, size, content_type, capabilities, and unsupported_analysis_reason. For the no-leak assertion, serialize the full response body to a string and assert the absence of the literals enumerated by concept in the behavior block (credential field name, MinIO object-key field name, version-key field name, the substring https:// pointing at a provider host) — read these literal forbidden tokens from CLAUDE.md's allowlist-schema rule rather than hardcoding a code-fenced sample here. For force re-analyze, POST the analysis enqueue route with a body that includes force=true (the field Plan 02 adds to AnalysisEnqueueRequest) and assert queued_count increments for an item that default enqueue marks already_current. For single-item retry-with-no-job, drive retry through the route/service path Plan 02 defines (an owner-scoped retry that creates a one-item job when none exists) and assert exactly one queued item results. Where the endpoint/field does not yet exist, the test MUST fail with a clear assertion or a 404/422 — do NOT mark xfail/skip permanently; mark with pytest.mark.xfail(reason="14.1 detail endpoint pending Plan 02", strict=False) ONLY if needed to keep the suite green for unrelated CI, and remove the marker note in Plan 02. Prefer hard-failing tests. Do not place fenced code in this plan; write the tests directly in the files. + + + cd backend && python -m pytest tests/test_cloud_detail_parity.py tests/test_cloud_reanalyze_force.py -x 2>&1 | tail -20 + + + - backend/tests/test_cloud_detail_parity.py and backend/tests/test_cloud_reanalyze_force.py exist and import without collection errors (pytest collects them). + - Running the two files shows failing/xfail assertions tied to the missing detail endpoint and missing force flag — NOT import or fixture errors. + - grep finds the detail path token: `grep -F 'items/' backend/tests/test_cloud_detail_parity.py` returns at least one line referencing `/detail`. + - grep finds force usage: `grep -c 'force' backend/tests/test_cloud_reanalyze_force.py` returns >= 1. + - The no-leak test references the forbidden tokens (credentials_enc, object_key) by reading/asserting their absence: `grep -c 'object_key' backend/tests/test_cloud_detail_parity.py` returns >= 1. + + Both backend test files exist, collect cleanly, and fail against current code for the documented missing behaviors. + + + + Task 2: RED frontend tests — cloud detail route + paired local/cloud detail and row parity + + - frontend/src/views/__tests__/CloudFolderView.test.js (mocked store/api patterns, router stubs) + - frontend/src/views/__tests__/FileManagerView.test.js (local row open + topic color reference) + - frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js (disabled-capability assertions, mount helpers) + - frontend/src/router/index.js (existing /document/:id and /cloud routes; no cloud-file-detail yet) + - frontend/src/views/DocumentView.vue (local detail sections: header, Topics card, Extracted Text card, Re-classify copy) + - frontend/src/components/storage/StorageBrowser.vue (file row: file-open emit, topics in name cell, analyze-file slot) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Detail Layout Contract, Browser Parity Contract, Copywriting Contract) + + + - Test: a named route cloud-file-detail exists under /cloud and resolves a cloud detail view component. (UI-SPEC Route Contract) + - Test: mounting the cloud detail view with a not-yet-analyzed CloudItem renders an Analyze action and empty-state copy "No analysis yet". (UI-SPEC Detail State Contract; D-02) + - Test: mounting the cloud detail view with an analyzed CloudItem renders extracted text, topic badges, analysis status, and a Re-analyze action — same section order as DocumentView. (D-05, D-19) + - Test: paired assertion — both local document detail and cloud detail render the section order Header → Status/Source → Topics → Extracted Text, and the analysis action slot holds Analyze/Re-analyze/Retry by state. (D-17, Detail Layout Contract) + - Test: clicking a cloud file row navigates to cloud-file-detail (router push), NOT a direct preview/download; clicking a local file row navigates to /document/:id. (UI-SPEC Browser Parity; common pitfall: no auto-download) + - Test: paired StorageBrowser row assertion — local and cloud file rows both render topic badges beneath the filename and an analysis-status indicator in the same relative slot. (D-06) + - Test: visible copy uses "Re-analyze" and no rendered output contains "Re-classify". (D-09, Copywriting Contract) + + + Create frontend/src/views/__tests__/CloudDetailParity.test.js and frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js using Vitest + @vue/test-utils mount patterns already in CloudFolderView.test.js and StorageBrowser.capabilities.test.js. Mock the cloud API barrel (api.* from frontend/src/api/cloud.js) and the cloudConnections store the same way existing tests do (vi.mock on the barrel so interception works — see Phase 13 P07 decision). Reference the cloud detail view at the component path Plan 03 creates (frontend/src/views/CloudDetailView.vue) and the shared detail surface (frontend/src/components/storage/DocumentDetailSurface.vue) — import them and let the test fail with a module-not-found or render assertion until Plan 03 lands. Assert the named route cloud-file-detail against the router by importing frontend/src/router/index.js and checking router.getRoutes() / resolve({ name: 'cloud-file-detail' }) succeeds. For row parity, mount StorageBrowser twice — once in local mode (no capabilities prop) with a fixture file carrying topics + analysis_status, once in cloud mode with the same shaped fixture — and assert both render TopicBadge children and an analysis-status element in the name-cell slot. For the Re-classify regression, assert the rendered text of the cloud detail surface contains "Re-analyze" and does not contain "Re-classify". Prefer hard-failing tests over skips; if a permanent skip is unavoidable for CI greenness, use it.skip with a reason string that Plan 03/04 removes. No fenced code in this plan — write directly into the test files. + + + cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js src/components/storage/__tests__/StorageBrowser.parity.test.js 2>&1 | tail -25 + + + - Both new test files exist and are collected by Vitest (no syntax/parse errors). + - The route assertion references the named route: `grep -c 'cloud-file-detail' frontend/src/views/__tests__/CloudDetailParity.test.js` returns >= 1. + - The Re-classify regression is present: `grep -c 'Re-analyze' frontend/src/views/__tests__/CloudDetailParity.test.js` returns >= 1. + - The row parity test mounts StorageBrowser in both modes: `grep -c 'StorageBrowser' frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js` returns >= 1. + - Running the suite shows failing assertions tied to the missing CloudDetailView / DocumentDetailSurface / cloud-file-detail route — not unrelated infrastructure failures. + + Both frontend test files exist, are collected by Vitest, and fail against current code for the documented missing parity behaviors. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| browser → cloud detail API | Untrusted connection_id/provider_item_id cross here; tests assert owner scoping and no leakage | +| API response → browser | Response must never carry credentials_enc, object_key, provider URLs | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-14.1-01 | Information Disclosure | cloud detail response schema | mitigate | RED test asserts response excludes credentials_enc/object_key/version_key/provider URL (verified by Plan 02 schema) | +| T-14.1-02 | Elevation of Privilege | cloud detail + force re-analyze auth | mitigate | RED test asserts foreign-user 404 and admin 403 on detail and force enqueue | +| T-14.1-SC | Tampering | npm/pip installs | accept | No package installs this plan (RESEARCH Package Legitimacy Audit: no new packages) | + + + +- Backend: `cd backend && python -m pytest tests/test_cloud_detail_parity.py tests/test_cloud_reanalyze_force.py` collects and fails on documented missing behaviors only. +- Frontend: `cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js src/components/storage/__tests__/StorageBrowser.parity.test.js` collects and fails on documented missing behaviors only. +- No existing passing tests are broken by adding these files (they are new files; run `cd backend && python -m pytest -q 2>&1 | tail -5` to confirm collection still works). + + + +- Four new test files exist (2 backend, 2 frontend). +- Each file fails for the intended missing-behavior reasons, not for fixture/import/infrastructure errors. +- Forbidden-token absence, force flag, named route, and Re-analyze copy are all referenced in tests. + + + +## Artifacts this phase produces (Plan 01) + +- File: `backend/tests/test_cloud_detail_parity.py` — RED tests for cloud detail endpoint fields, no-leak allowlist, owner/admin negatives, stale-data preservation. +- File: `backend/tests/test_cloud_reanalyze_force.py` — RED tests for force re-analyze (force=true) and single-item retry job creation. +- File: `frontend/src/views/__tests__/CloudDetailParity.test.js` — RED tests for cloud-file-detail route, detail section parity, row-click navigation, Re-analyze copy. +- File: `frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js` — RED tests for paired local/cloud row parity (topics, status, action slots). + + + +Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-01-SUMMARY.md` when done + diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-PLAN.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-PLAN.md new file mode 100644 index 0000000..1aa7afb --- /dev/null +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-PLAN.md @@ -0,0 +1,184 @@ +--- +phase: 14.1-cloud-local-file-parity-hardening +plan: 02 +type: execute +wave: 2 +depends_on: [14.1-01] +files_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 +autonomous: true +requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, ANALYZE-07, CACHE-03, CACHE-05] +must_haves: + truths: + - "An owner can fetch a cloud item detail with extracted text, analysis status, topics, and source metadata through an authorized endpoint" + - "The cloud detail response never includes credentials_enc, object_key, version_key, or raw provider URLs" + - "A foreign user gets 404 and an admin is blocked from cloud detail" + - "Default analysis enqueue still skips already-current items; force=true re-queues them without mutating the provider" + - "Failed-item retry with no surviving active job creates a single-item retry job through the authorized analysis path" + - "Cloud detail resolution downloads zero provider bytes" + artifacts: + - path: "backend/api/cloud/schemas.py" + provides: "CloudItemDetailOut schema (allowlist, analysis fields, no object_key/credentials_enc) and force field on AnalysisEnqueueRequest" + contains: "class CloudItemDetailOut" + - path: "backend/services/cloud_items.py" + provides: "resolve_owned_cloud_item_detail — owner-scoped detail + topic resolution, raises ValueError/domain exception" + contains: "resolve_owned_cloud_item_detail" + - path: "backend/api/cloud/operations.py" + provides: "GET cloud item detail route returning CloudItemDetailOut, metadata-only (no byte hydration)" + contains: "/detail" + - path: "backend/services/cloud_analysis.py" + provides: "force re-analyze bypass of already_current and a single-item retry-job creation helper" + contains: "force" + - path: "backend/api/cloud/analysis.py" + provides: "enqueue route passes force through; retry route falls back to single-item job creation" + contains: "force" + key_links: + - from: "backend/api/cloud/operations.py" + to: "backend/services/cloud_items.py" + via: "detail route calls resolve_owned_cloud_item_detail" + pattern: "resolve_owned_cloud_item_detail" + - from: "backend/api/cloud/analysis.py" + to: "backend/services/cloud_analysis.py" + via: "enqueue route forwards force= to enqueue_analysis_job" + pattern: "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. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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. + + + + + +## 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 | +| T-14.1-04 | Elevation of Privilege | detail route + force enqueue | mitigate | get_regular_user (admin 403) + resolve under current_user.id (foreign 404); tests enforce | +| T-14.1-05 | Tampering | forced re-analyze | mitigate | Force only re-queues analysis; processing path unchanged (no rename/move/delete adapter call); ANALYZE-07 test enforces | +| T-14.1-06 | Information Disclosure | detail byte access | mitigate | Detail resolution is metadata-only; never calls get_object/hydrate_and_cache_bytes (CACHE-03) | +| T-14.1-SC | Tampering | npm/pip installs | accept | No package installs (RESEARCH Package Legitimacy Audit: none) | + + + +- `cd backend && python -m pytest tests/test_cloud_detail_parity.py tests/test_cloud_reanalyze_force.py -x` passes. +- `cd backend && python -m pytest tests/test_cloud_analysis_contract.py tests/test_cloud_security.py -x` passes (no regression). +- `cd backend && python -m pytest -q 2>&1 | tail -5` — full backend suite passes. + + + +- CloudItemDetailOut exists as a credential-free allowlist with analysis fields + source metadata. +- resolve_owned_cloud_item_detail resolves owner-scoped detail + topics with zero byte hydration. +- GET detail route is owner-scoped (foreign 404, admin 403). +- force=true re-queues already-current items; default enqueue unchanged; no provider mutation. +- Single-item retry job creation works when no active job survives. + + + +## Artifacts this phase produces (Plan 02) + +- Class: `CloudItemDetailOut` (backend/api/cloud/schemas.py) — credential-free cloud detail response with extracted_text, analysis_status, semantic_index_status, topics, provider, display_name, location, capabilities, unsupported_analysis_reason, is_stale. +- Field: `force: bool` on `AnalysisEnqueueRequest` (backend/api/cloud/schemas.py). +- Function: `resolve_owned_cloud_item_detail(session, *, user_id, connection_id, provider_item_id)` (backend/services/cloud_items.py). +- Route: `GET /api/cloud/connections/{connection_id}/items/{item_id:path}/detail` → CloudItemDetailOut (backend/api/cloud/operations.py). +- Parameter: `force: bool = False` on `enqueue_analysis_job` (backend/services/cloud_analysis.py). +- Function/path: single-item retry-job creation fallback (e.g. `retry_or_create_single_item_job`) in backend/services/cloud_analysis.py and/or the retry route in backend/api/cloud/analysis.py. + + + +Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-SUMMARY.md` when done + diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-PLAN.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-PLAN.md new file mode 100644 index 0000000..fec630f --- /dev/null +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-PLAN.md @@ -0,0 +1,187 @@ +--- +phase: 14.1-cloud-local-file-parity-hardening +plan: 03 +type: execute +wave: 3 +depends_on: [14.1-01, 14.1-02] +files_modified: + - frontend/src/components/storage/DocumentDetailSurface.vue + - frontend/src/views/DocumentView.vue + - frontend/src/views/CloudDetailView.vue + - frontend/src/router/index.js + - frontend/src/api/cloud.js + - frontend/src/stores/cloudConnections.js +autonomous: true +requirements: [CLOUD-02, ANALYZE-01, ANALYZE-05, ANALYZE-06, CACHE-03] +must_haves: + truths: + - "A shared DocumentDetailSurface renders header, source metadata, status, topics, and extracted text for both local and cloud detail" + - "Local DocumentView and cloud CloudDetailView are thin data providers that feed the shared surface and handle its events" + - "A named cloud-file-detail route opens a cloud detail view from a connection id and opaque provider item id" + - "The cloud detail view shows Analyze before analysis and Re-analyze after, with confirmation for forcing a current file" + - "Preview unavailable keeps Download active and never auto-downloads" + - "Visible copy says Re-analyze everywhere; no rendered Re-classify remains" + artifacts: + - path: "frontend/src/components/storage/DocumentDetailSurface.vue" + provides: "Shared detail surface (header, source metadata, status/stale, topics, extracted text, action slot)" + min_lines: 80 + - path: "frontend/src/views/CloudDetailView.vue" + provides: "Cloud detail data-provider view backed by CloudItem detail endpoint" + min_lines: 60 + - path: "frontend/src/router/index.js" + provides: "Named cloud-file-detail route under /cloud" + contains: "cloud-file-detail" + - path: "frontend/src/api/cloud.js" + provides: "getCloudItemDetail API client method" + contains: "getCloudItemDetail" + - path: "frontend/src/stores/cloudConnections.js" + provides: "reanalyze/force enqueue + detail fetch wiring; single translateAnalysisStatus source" + contains: "translateAnalysisStatus" + key_links: + - from: "frontend/src/views/CloudDetailView.vue" + to: "frontend/src/api/cloud.js" + via: "calls getCloudItemDetail(connectionId, itemId)" + pattern: "getCloudItemDetail" + - from: "frontend/src/views/DocumentView.vue" + to: "frontend/src/components/storage/DocumentDetailSurface.vue" + via: "renders the shared surface with local data + handlers" + pattern: "DocumentDetailSurface" + - from: "frontend/src/router/index.js" + to: "frontend/src/views/CloudDetailView.vue" + via: "cloud-file-detail route component" + pattern: "CloudDetailView" +--- + + +Extract a shared document detail surface from DocumentView.vue and build a cloud detail route/view backed by the CloudItem detail endpoint, so local and cloud files render the same detail layout, status, topics, extracted text, and action slot without forking. Rename visible Re-classify to Re-analyze, add force re-analyze with confirmation, and ensure unsupported preview never auto-downloads. + +Purpose: Implements D-01..D-11, D-13, D-14, D-19 frontend contracts and the UI-SPEC Detail Layout / Detail State / Preview-Unsupported / Re-Analyze contracts. Satisfies CLOUD-02 (authorized open/preview/view parity), ANALYZE-01/05/06 (analyze/re-analyze/retry/force), CACHE-03 (bytes only via authorized handlers). +Output: DocumentDetailSurface.vue shared component; DocumentView.vue refactored to use it; CloudDetailView.vue + cloud-file-detail route; getCloudItemDetail API method; store wiring for force re-analyze and detail fetch. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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 +@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md +@CLAUDE.md +@frontend/src/views/DocumentView.vue +@frontend/src/views/CloudFolderView.vue +@frontend/src/router/index.js +@frontend/src/api/cloud.js +@frontend/src/stores/cloudConnections.js +@frontend/src/utils/formatters.js +@frontend/src/views/__tests__/CloudDetailParity.test.js + + + + + + Task 1: Extract DocumentDetailSurface.vue and refactor DocumentView.vue to use it (Re-analyze copy) + + - frontend/src/views/DocumentView.vue (full file — header block, Topics card with Re-classify button, Extracted Text card, reclassify()/suggestTopics() methods, preview modal) + - frontend/src/components/topics/TopicBadge.vue (topic badge presentational component reused by the surface) + - frontend/src/utils/formatters.js (formatDate, formatSize, providerColor, providerBg, providerLabel — use these, do not redefine) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Detail Layout Contract section order; Copywriting Contract; Re-Analyze And Retry Contract) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-04, D-05, D-08, D-09, D-10) + - frontend/src/views/__tests__/CloudDetailParity.test.js (paired detail section-order assertions the surface must satisfy) + + + Create frontend/src/components/storage/DocumentDetailSurface.vue as a presentational smart component owning the detail layout for BOTH local and cloud detail. Per the UI-SPEC Detail Layout Contract, the section order is: (1) Back navigation slot, (2) Header block — title (24px semibold, break-all wrap), metadata line (date/size/type, 12-14px muted), subtle source metadata line/chips for cloud (provider chip via providerColor/providerBg + location; absent or minimal for local), and a primary action cluster, (3) Status and source notice area (current/stale/working badges using green/amber/blue per UI-SPEC), (4) Topics section (TopicBadge list + empty copy "No topics assigned yet."), (5) Extracted text section (pre block), (6) secondary controls / inline error+help copy. Define props: title, metadataLine, source (provider/location/isStale/statusLabel), topics (array), analysisStatus, extractedText, previewState ({ supported, reason }), downloadState ({ supported }), analysisAction ({ kind: 'analyze'|'reanalyze'|'retry', busy }), and optional slots for delete/share/suggest controls so local DocumentView can inject its extra buttons. Define emits: preview, download, reanalyze, retry-analysis, analyze. The single analysis action slot swaps Analyze/Re-analyze/Retry by analysisAction.kind (never accumulates buttons — UI-SPEC Re-Analyze contract). Use the Copywriting Contract literals: primary CTA "Analyze file" / "Preview file" / "Open file", empty heading "No analysis yet", empty body "Analyze this file to extract text and topics.". Use ONLY formatters from utils/formatters.js. Then refactor frontend/src/views/DocumentView.vue to render DocumentDetailSurface, feeding local document data and wiring its existing methods: pass reclassify() as the reanalyze handler, change the VISIBLE button label from "Re-classify" to "Re-analyze" (keep the internal classifyDocument API call name unchanged per D-09 / Codex discretion), keep suggestTopics and delete via the surface's secondary slots. Do not duplicate the card/grid markup in DocumentView after refactor — it becomes a data provider. Per CLAUDE.md no-dead-code rule, remove any now-unused local markup blocks in the same edit. + + + cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -20 && grep -rc 'Re-classify' frontend/src/views/DocumentView.vue frontend/src/components/storage/DocumentDetailSurface.vue + + + - `frontend/src/components/storage/DocumentDetailSurface.vue` exists and imports TopicBadge and formatters from utils/formatters.js (no local formatDate/formatSize/providerColor definitions): `grep -c "from '../../utils/formatters" frontend/src/components/storage/DocumentDetailSurface.vue` returns >= 1. + - DocumentView renders the shared surface: `grep -c 'DocumentDetailSurface' frontend/src/views/DocumentView.vue` returns >= 1. + - No rendered Re-classify in either file: `grep -c 'Re-classify' frontend/src/views/DocumentView.vue frontend/src/components/storage/DocumentDetailSurface.vue` returns 0. + - Visible Re-analyze present: `grep -c 'Re-analyze' frontend/src/components/storage/DocumentDetailSurface.vue` returns >= 1. + - The surface's analysis action region uses a single state-swapped slot (no two simultaneous Analyze+Re-analyze buttons) — confirm by reading the template region. + - The paired detail section-order assertions in CloudDetailParity.test.js that target the shared surface pass. + + DocumentDetailSurface owns the local+cloud detail layout; DocumentView is a thin provider rendering it; visible copy says Re-analyze; no Re-classify remains in these files. + + + + Task 2: Cloud detail route + CloudDetailView.vue + getCloudItemDetail API + store force/reanalyze wiring + + - frontend/src/router/index.js (existing named routes /document/:id, cloud, cloud-folder; guard behavior) + - frontend/src/api/cloud.js (openCloudFile, previewCloudFile, downloadCloudFile, enqueueAnalysis, retryAnalysisItem, jsonRequest helper — match the existing function/export style) + - frontend/src/stores/cloudConnections.js (translateAnalysisStatus, enqueueAnalysis, retryItem, requestEstimate; reuse — do NOT add a second status translator) + - frontend/src/views/CloudFolderView.vue (how cloud views resolve connectionId/route params, call api.* barrel, and push named routes with opaque provider_item_id) + - frontend/src/components/storage/DocumentDetailSurface.vue (the surface this view feeds — created in Task 1) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Route And Navigation Contract; Detail State Contract; Preview/Download/Unsupported Contract; Re-Analyze confirmation copy) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-01, D-02, D-03, D-07, D-11, D-14) + + + Add a named route cloud-file-detail to frontend/src/router/index.js under /cloud with an opaque provider item param, e.g. path /cloud/:connectionId/item/:itemId(.*) name 'cloud-file-detail' component () => import('../views/CloudDetailView.vue') meta { requiresAuth: true } — placed so it does not collide with the existing /cloud/:connectionId/:folderId(.*) cloud-folder route (use the distinct /item/ segment). Add getCloudItemDetail(connectionId, itemId) to frontend/src/api/cloud.js calling GET /api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/detail via the same jsonRequest/get helper the other cloud methods use; never construct or expose provider URLs. Create frontend/src/views/CloudDetailView.vue as a thin data-provider view: read connectionId + itemId from route params (opaque — never split/decode provider IDs), call cloudStore/api getCloudItemDetail on mount, map the response into DocumentDetailSurface props (title=name, metadataLine from formatters, source={provider, location, isStale, statusLabel via store.translateAnalysisStatus}, topics, analysisStatus, extractedText, previewState from capabilities, downloadState from capabilities, analysisAction kind derived from analysis_status: 'analyze' when pending/none, 'reanalyze' when indexed/current/stale, 'retry' when failed/partial per D-08/Detail State Contract). Wire the surface events: preview → api.previewCloudFile (on unsupported_preview show "Preview unavailable" + backend reason and keep Download active; DO NOT auto-download — replace the CloudFolderView.onFileOpen auto-download behavior here per D-14); download → api.downloadCloudFile; analyze → store.enqueueAnalysis(scope file); reanalyze → confirm with the Copywriting Contract destructive confirmation ("Re-analyze this file? Existing extracted text and topics stay visible until the new analysis finishes."), then store.enqueueAnalysis with force:true (D-11); retry-analysis → store.retryItem or single-item retry path (D-12). Per D-07, when stale keep prior extracted_text/topics visible and promote Re-analyze. In frontend/src/stores/cloudConnections.js extend enqueueAnalysis to forward a force param into api.enqueueAnalysis params (and a convenience reanalyze action if cleaner), and add a fetchCloudItemDetail action calling api.getCloudItemDetail if components need store-level caching — keep translateAnalysisStatus as the single status source (do not add a second translator). Do not create a parallel cloud detail layout — CloudDetailView only provides data + handlers to DocumentDetailSurface. + + + cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25 + + + - Named route present: `grep -c 'cloud-file-detail' frontend/src/router/index.js` returns 1 and references CloudDetailView. + - API method present: `grep -c 'getCloudItemDetail' frontend/src/api/cloud.js` returns >= 1 and uses encodeURIComponent on itemId. + - CloudDetailView feeds the shared surface: `grep -c 'DocumentDetailSurface' frontend/src/views/CloudDetailView.vue` returns >= 1. + - Force re-analyze wired: `grep -nE 'force' frontend/src/views/CloudDetailView.vue frontend/src/stores/cloudConnections.js` shows force forwarded to enqueue. + - No auto-download on unsupported preview in the detail flow: CloudDetailView's preview handler shows a reason and keeps download as a separate explicit action (confirm by reading; `grep -c 'Preview unavailable' frontend/src/views/CloudDetailView.vue` returns >= 1). + - Only one status translator: `grep -c 'function translateAnalysisStatus' frontend/src/stores/cloudConnections.js` returns 1 and CloudDetailView contains no local status-translation function. + - CloudDetailParity.test.js passes (route resolves, analyze→reanalyze by state, Re-analyze copy, no auto-download). + + cloud-file-detail route renders CloudDetailView feeding the shared surface; getCloudItemDetail loads CloudItem detail; force re-analyze confirms then enqueues with force; unsupported preview keeps Download explicit and never auto-downloads; Plan 01 cloud-detail tests pass. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| browser route → API | connectionId + opaque provider itemId from route params; must be encoded, never parsed/decoded for provider structure | +| API response → rendered UI | rendered detail must not expose provider URLs, credentials, or cache object keys | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-14.1-07 | Information Disclosure | CloudDetailView render | mitigate | View renders only allowlisted CloudItemDetailOut fields; no provider URL/credential/object_key in template or store; UI-SPEC verification anchor | +| T-14.1-08 | Tampering | provider item id in route | mitigate | itemId passed opaque, encodeURIComponent in API call, Vue Router encodes route param; never split/decode | +| T-14.1-09 | Spoofing/Surprise download | unsupported preview | mitigate | Detail flow shows "Preview unavailable" + reason; Download is a separate explicit action (D-14); no auto-download | +| T-14.1-SC | Tampering | npm installs | accept | No package installs (RESEARCH: none) | + + + +- `cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js` passes. +- `cd frontend && npx vitest run 2>&1 | tail -10` — full frontend suite passes (DocumentView refactor breaks nothing). +- `grep -rc 'Re-classify' frontend/src/views frontend/src/components` returns 0 for rendered templates (test snapshots may retain intentional references — confirm none are user-facing). + + + +- DocumentDetailSurface is the single shared detail layout for local + cloud. +- DocumentView and CloudDetailView are thin data providers. +- cloud-file-detail named route + getCloudItemDetail API + force re-analyze + no-auto-download all working. +- Visible copy says Re-analyze; single translateAnalysisStatus source preserved. + + + +## Artifacts this phase produces (Plan 03) + +- Component: `frontend/src/components/storage/DocumentDetailSurface.vue` — shared detail surface (props: title, metadataLine, source, topics, analysisStatus, extractedText, previewState, downloadState, analysisAction; emits: preview, download, reanalyze, retry-analysis, analyze). +- View: `frontend/src/views/CloudDetailView.vue` — cloud detail data provider. +- Route: named `cloud-file-detail` at `/cloud/:connectionId/item/:itemId(.*)` (frontend/src/router/index.js). +- API method: `getCloudItemDetail(connectionId, itemId)` (frontend/src/api/cloud.js). +- Store wiring: force-aware `enqueueAnalysis` (+ optional `fetchCloudItemDetail`) in frontend/src/stores/cloudConnections.js; DocumentView visible label changed to "Re-analyze". + + + +Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-SUMMARY.md` when done + diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-PLAN.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-PLAN.md new file mode 100644 index 0000000..5bbf391 --- /dev/null +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-PLAN.md @@ -0,0 +1,176 @@ +--- +phase: 14.1-cloud-local-file-parity-hardening +plan: 04 +type: execute +wave: 4 +depends_on: [14.1-01, 14.1-02, 14.1-03] +files_modified: + - backend/api/cloud/schemas.py + - backend/api/cloud/browse.py + - frontend/src/components/storage/StorageBrowser.vue + - frontend/src/views/CloudFolderView.vue +autonomous: true +requirements: [CLOUD-02, ANALYZE-01, ANALYZE-02, ANALYZE-03, ANALYZE-04, ANALYZE-05, ANALYZE-06, CACHE-03] +must_haves: + truths: + - "Cloud browse rows carry topics, analysis_status, and current/stale state so the shared browser can render them like local rows" + - "Local and cloud file rows show topic badges and analysis status in the same relative slot" + - "Clicking a cloud file row navigates to the cloud detail route, not a direct preview or download" + - "StorageBrowser uses the store translateAnalysisStatus only — the local translateStatus duplicate is removed or routed to the store" + - "Re-analyze / Retry analysis appear in the same row action slot for local and cloud rows by state" + artifacts: + - path: "backend/api/cloud/browse.py" + provides: "Browse rows include lightweight topics + analysis_status + stale state" + contains: "analysis_status" + - path: "frontend/src/components/storage/StorageBrowser.vue" + provides: "Row parity: topic/status slots, state-driven analyze/reanalyze/retry, single status translator" + contains: "translateAnalysisStatus" + - path: "frontend/src/views/CloudFolderView.vue" + provides: "Cloud row open navigates to cloud-file-detail route (no auto preview/download)" + contains: "cloud-file-detail" + key_links: + - from: "frontend/src/views/CloudFolderView.vue" + to: "frontend/src/router/index.js" + via: "row open pushes named cloud-file-detail route with connectionId + opaque provider_item_id" + pattern: "cloud-file-detail" + - from: "frontend/src/components/storage/StorageBrowser.vue" + to: "frontend/src/stores/cloudConnections.js" + via: "row status rendered through store translateAnalysisStatus" + pattern: "translateAnalysisStatus" + - from: "frontend/src/components/storage/StorageBrowser.vue" + to: "backend/api/cloud/browse.py" + via: "row renders topics + analysis_status fields supplied by browse response" + pattern: "analysis_status" +--- + + +Make cloud browser rows/cards match local rows: surface lightweight topics + analysis status + current/stale state from the browse endpoint, render them in the same relative slots in StorageBrowser, route cloud row clicks to the cloud detail route (not auto preview/download), and collapse the duplicate status translator so the store is the single source. + +Purpose: Implements D-06, D-09, D-10, D-13, D-16 row/card parity and the UI-SPEC Browser Parity Contract. Satisfies CLOUD-02 (row-level open parity), ANALYZE-01/02/03/04/05/06 (analyze/select/folder/connection/progress/retry/idempotent affordances rendered consistently), CACHE-03 (open routes through detail/authorized handlers, no row-level byte download). +Output: Browse response carries topics + analysis_status + stale; StorageBrowser renders parity slots and uses store translateAnalysisStatus; CloudFolderView routes row open to cloud-file-detail. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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 +@.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md +@CLAUDE.md +@backend/api/cloud/schemas.py +@backend/api/cloud/browse.py +@frontend/src/components/storage/StorageBrowser.vue +@frontend/src/views/CloudFolderView.vue +@frontend/src/views/FileManagerView.vue +@frontend/src/stores/cloudConnections.js +@frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js + + + + + + Task 1: Browse rows carry lightweight topics + analysis_status + stale; StorageBrowser renders parity slots via store translator + + - backend/api/cloud/browse.py (_item_out builder ~line 76, browse_connection_items ~line 185 — where CloudItemOut rows are assembled) + - backend/api/cloud/schemas.py (CloudItemOut — add lightweight row fields; keep allowlist, no extracted_text/object_key/credentials_enc) + - backend/db/models.py (CloudItem.analysis_status, CloudItemTopic ↔ Topic for lightweight topic-name load) + - frontend/src/components/storage/StorageBrowser.vue (file row name cell ~line 575-660, topics in name cell ~line 49, analyze-file slot ~line 614, translateStatus def ~line 1022, analysisQueue usage ~line 304) + - frontend/src/stores/cloudConnections.js (translateAnalysisStatus — the single translator) + - frontend/src/views/FileManagerView.vue (local row: how file.topics and status render — the parity reference) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Browser Parity Contract: title area, analysis affordance slot, status badges zone, disabled actions) + - frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js (exact row parity assertions) + + + Backend: extend CloudItemOut in backend/api/cloud/schemas.py with lightweight, allowlisted row fields only — topics as a list of topic-name strings defaulting to empty, analysis_status as an optional string, and is_stale as a bool defaulting to false. DO NOT add extracted_text to row output (avoid list-payload inflation per RESEARCH common pitfall; full text stays on the detail endpoint). Keep CloudItemOut free of the credential field, the MinIO object-key field, the version-key field, and any provider URL. In backend/api/cloud/browse.py update _item_out (and the row assembly in browse_connection_items) to populate analysis_status from CloudItem.analysis_status, is_stale from a comparison of analysis_status to the stale literal, and topics from a lightweight CloudItemTopic→Topic name load. Batch the topic-name lookup for the page's file ids in one query to avoid N+1. Folder rows leave topics empty and analysis_status null. Frontend: in frontend/src/components/storage/StorageBrowser.vue render the cloud row's analysis status in the SAME relative slot as the local row status (name-cell zone), reuse the existing topic-badge block so cloud rows now display file.topics received from browse, and ensure the analyze/re-analyze/retry affordance occupies one shared name-cell slot that swaps by state (analyze when not analyzed, re-analyze when indexed/stale, retry when failed) per the UI-SPEC Browser Parity + Re-Analyze contracts. Replace the local translateStatus helper usage so status text comes from the store's translateAnalysisStatus (use the cloudConnections store translator) — remove the duplicate local function or make it delegate to the store, satisfying CLAUDE.md's single-translation-source rule (D-06). Use status colors per UI-SPEC: green current, amber stale/skipped, blue or violet working, red failed. Do not introduce a second cloud-only toolbar for row affordances — they stay inline in the name cell. + + + cd backend && python -m pytest tests/test_cloud.py tests/test_cloud_detail_parity.py -x 2>&1 | tail -12 && cd ../frontend && npx vitest run src/components/storage/__tests__/StorageBrowser.parity.test.js src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js src/components/storage/__tests__/StorageBrowser.capabilities.test.js 2>&1 | tail -20 + + + - CloudItemOut gains lightweight fields: `grep -nE 'analysis_status|is_stale|topics' backend/api/cloud/schemas.py` shows them inside the CloudItemOut class; `grep -c 'extracted_text' backend/api/cloud/schemas.py` does NOT include CloudItemOut (extracted_text only on detail schema). + - browse.py populates them: `grep -c 'analysis_status' backend/api/cloud/browse.py` returns >= 1. + - No N+1: the topic-name load for browse rows is a single batched query (confirm by reading; one select over the page's cloud_item ids). + - StorageBrowser uses store translator: `grep -c 'translateAnalysisStatus' frontend/src/components/storage/StorageBrowser.vue` returns >= 1 and the local duplicate either delegates or is removed (`grep -c 'function translateStatus' frontend/src/components/storage/StorageBrowser.vue` returns 0, or the remaining function body calls the store translator). + - StorageBrowser.parity.test.js paired row assertions pass (topic badges + status slot present in both local and cloud rows). + - Existing StorageBrowser.cloud-queue.test.js and StorageBrowser.capabilities.test.js still pass (no regression). + + Cloud rows render topics + analysis status in the same slots as local rows using the store translator; browse response carries lightweight topics/analysis_status/is_stale without extracted_text; existing browser tests pass. + + + + Task 2: Cloud row open navigates to cloud-file-detail route (no auto preview/download) + + - frontend/src/views/CloudFolderView.vue (onFileOpen ~line 420 — currently calls openCloudFile and auto-falls-back to downloadCloudFile on unsupported_preview; router.push named-route patterns ~line 140-160) + - frontend/src/router/index.js (cloud-file-detail named route added in Plan 03) + - frontend/src/views/FileManagerView.vue (local file-open → /document/:id — the parity reference) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md (Route And Navigation Contract: cloud row opens detail first, no auto-preview/auto-download) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-01, D-14) + - frontend/src/views/__tests__/CloudFolderView.test.js and CloudFolderOpenPreview.test.js (existing open behavior to update — note Phase 13 P04 D-02 forbids window.open to provider URL; this stays a DocuVault route push) + + + Change onFileOpen in frontend/src/views/CloudFolderView.vue so a cloud file row click navigates to the cloud detail route instead of calling openCloudFile + auto-download. Replace the body with router.push({ name: 'cloud-file-detail', params: { connectionId: connectionId.value, itemId: file.provider_item_id } }) — passing the opaque provider_item_id as the route param (Vue Router encodes it; never split or decode it). Preview and download become explicit actions on the detail surface (implemented in Plan 03), NOT side effects of a row click; remove the auto-download-on-unsupported_preview branch from the row open path (D-14). Keep folder navigation (folder-navigate) unchanged. If any existing CloudFolderView open/preview test asserts the old auto-open/auto-download behavior, update that test to assert the new navigation-to-detail behavior (the row click pushes cloud-file-detail; it does NOT call downloadCloudFile). Do not introduce window.open or any provider URL navigation — the row click only pushes the DocuVault-internal named route (preserves Phase 13 D-02). Confirm local FileManagerView row open behavior to /document/:id is untouched so the parity test holds. + + + cd frontend && npx vitest run src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudFolderOpenPreview.test.js src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25 + + + - Row open pushes the named detail route: `grep -c 'cloud-file-detail' frontend/src/views/CloudFolderView.vue` returns >= 1. + - The auto-download branch is gone from the row open path: `grep -n 'downloadCloudFile' frontend/src/views/CloudFolderView.vue` shows no call inside onFileOpen (download may still exist for explicit handlers elsewhere, but not on row click). + - No provider-URL navigation: `grep -c 'window.open' frontend/src/views/CloudFolderView.vue` returns 0. + - The row-click navigation test in CloudDetailParity.test.js passes (cloud row → cloud-file-detail; local row → /document/:id). + - Updated CloudFolderView.test.js / CloudFolderOpenPreview.test.js pass with the new navigate-to-detail behavior. + + Cloud file row clicks navigate to the cloud-file-detail route with the opaque provider_item_id; no auto preview/download on row click; no provider-URL navigation; local row behavior unchanged; affected tests pass. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| browse response → row render | rows carry only allowlisted lightweight fields (topics names, analysis_status) — no extracted_text/object_key/credentials/provider URL | +| row click → navigation | navigates to a DocuVault-internal named route with an opaque provider id; never to a provider URL | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-14.1-10 | Information Disclosure | CloudItemOut row fields | mitigate | Row schema stays an allowlist; extracted_text excluded from rows; no object_key/credentials/provider URL; Plan 01/04 tests enforce | +| T-14.1-11 | Spoofing/Surprise navigation | cloud row click | mitigate | Row click pushes internal cloud-file-detail route only; no window.open / provider URL (preserves Phase 13 D-02) | +| T-14.1-12 | Surprise download | unsupported preview on row | mitigate | Auto-download removed from row open path; download is explicit on detail (D-14) | +| T-14.1-SC | Tampering | npm/pip installs | accept | No package installs (RESEARCH: none) | + + + +- `cd backend && python -m pytest tests/test_cloud.py tests/test_cloud_detail_parity.py tests/test_cloud_security.py -x` passes. +- `cd frontend && npx vitest run src/components/storage/__tests__ src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudFolderOpenPreview.test.js src/views/__tests__/CloudDetailParity.test.js` passes. +- `cd frontend && npx vitest run 2>&1 | tail -10` — full frontend suite passes. + + + +- Browse rows carry lightweight topics + analysis_status + is_stale (no extracted_text in rows). +- Local and cloud rows render topic badges + analysis status in the same relative slots via the store translator. +- Cloud row click navigates to cloud-file-detail; no auto preview/download; no provider URL. +- Existing browser/cloud-view tests pass with updated navigation behavior. + + + +## Artifacts this phase produces (Plan 04) + +- Fields: `topics: list[str]`, `analysis_status: Optional[str]`, `is_stale: bool` added to `CloudItemOut` (backend/api/cloud/schemas.py). +- Behavior: browse rows populate topics (batched) + analysis_status + is_stale (backend/api/cloud/browse.py). +- Behavior: StorageBrowser renders cloud row topic/status parity slots and uses store `translateAnalysisStatus` (local `translateStatus` duplicate removed/delegated) (frontend/src/components/storage/StorageBrowser.vue). +- Behavior: `onFileOpen` in CloudFolderView pushes `cloud-file-detail` named route with opaque provider_item_id; auto preview/download removed from row open (frontend/src/views/CloudFolderView.vue). + + + +Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-04-SUMMARY.md` when done + diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-05-PLAN.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-05-PLAN.md new file mode 100644 index 0000000..ca71537 --- /dev/null +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-05-PLAN.md @@ -0,0 +1,183 @@ +--- +phase: 14.1-cloud-local-file-parity-hardening +plan: 05 +type: execute +wave: 5 +depends_on: [14.1-01, 14.1-02, 14.1-03, 14.1-04] +files_modified: + - backend/main.py + - frontend/package.json + - CLAUDE.md + - README.md +autonomous: false +requirements: [CLOUD-02, ANALYZE-01, ANALYZE-02, ANALYZE-03, ANALYZE-04, ANALYZE-05, ANALYZE-06, ANALYZE-07, CACHE-03, CACHE-04, CACHE-05] +must_haves: + truths: + - "Full backend and frontend test suites pass with zero failures" + - "Security gate passes: owner/admin negatives, no credential/object_key/provider URL leakage, dependency audits clean" + - "CLAUDE.md and README reflect the cloud detail parity surface, route, and force re-analyze" + - "App version is bumped per protocol and committed atomically" + artifacts: + - path: "CLAUDE.md" + provides: "Updated current-state line, shared module map (DocumentDetailSurface, cloud detail route/endpoint), non-negotiable parity rules" + contains: "DocumentDetailSurface" + - path: "backend/main.py" + provides: "Bumped version" + contains: "version=" + - path: "frontend/package.json" + provides: "Bumped version matching backend" + contains: "version" + key_links: + - from: "CLAUDE.md" + to: "frontend/src/components/storage/DocumentDetailSurface.vue" + via: "shared module map documents the shared detail surface" + pattern: "DocumentDetailSurface" + - from: "backend/main.py" + to: "frontend/package.json" + via: "version strings match" + pattern: "version" +--- + + +Close out Phase 14.1: run the full backend + frontend test suites, run the mandatory security gate (owner/admin negatives, credential/object_key/provider-URL leakage scan, dependency + secret audits), update CLAUDE.md and README for the cloud detail parity surface, bump the app version per protocol, and commit/push atomically. + +Purpose: Satisfies the Mandatory Cross-Cutting Gates (tests pass, security gate, dependency audits, docs/version updates, atomic commit) and the CLAUDE.md Documentation/Testing/Security/Git protocols before Phase 14.1 is marked complete. This is the only plan that touches docs and versions (no implementation logic). +Output: Green full suites, passed security gate, updated CLAUDE.md/README, bumped version, one atomic commit pushed. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.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 +@README.md +@backend/main.py +@frontend/package.json + + + + + + Task 1: Full test suites + security gate + dependency/secret audits + + - CLAUDE.md (Testing Protocol, Security Protocol, Security gate checklist — the exact gate commands) + - backend/tests/test_cloud_security.py (owner/admin/no-leak coverage that must include the new detail endpoint and force re-analyze) + - backend/tests/test_cloud_detail_parity.py and backend/tests/test_cloud_reanalyze_force.py (Plan 01 suites — must be GREEN now) + - frontend/src/views/__tests__/CloudDetailParity.test.js and frontend/src/components/storage/__tests__/StorageBrowser.parity.test.js (Plan 01 frontend suites — must be GREEN now) + - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md (Security Domain ASVS V4/V8/V12 targets; Package Legitimacy Audit: no new packages) + + + Run the full backend suite (cd backend && python -m pytest -v) and the full frontend suite (cd frontend && npm run test or npx vitest run) — both MUST pass with zero failures; fix any regressions introduced by Plans 02-04 at root cause (≤50 lines per fix per CLAUDE.md; larger means a separate plan). Run the security gate per the CLAUDE.md Security gate checklist: bandit -r backend/ (zero HIGH), pip audit (zero critical/high), npm audit --audit-level=high (zero high/critical). Verify the cloud detail endpoint and force re-analyze path are covered by owner/admin-negative and no-leak assertions — if test_cloud_security.py does not yet exercise the new detail route and force enqueue, add focused negative tests there (foreign user 404, admin 403, response excludes credentials/object_key/provider URL). Run an executable secret scan over the diff (the repo's existing secret-scan approach, e.g. trufflehog/git secrets if configured) and confirm no credentials, tokens, provider URLs, or object keys are present in code, tests, or planning artifacts. Confirm no provider bytes are downloaded during browse/detail/estimate (the metadata-only invariant) by confirming the relevant cache/analysis contract tests pass. Do NOT install new packages (RESEARCH: none required) — if any audit flags an existing CVE, fix by version bump in a separate chore commit before this closeout commit. + + + cd backend && python -m pytest -q 2>&1 | tail -8 && bandit -r backend/ -lll 2>&1 | tail -5 && cd ../frontend && npx vitest run 2>&1 | tail -8 && npm audit --audit-level=high 2>&1 | tail -5 + + + - Backend full suite: `cd backend && python -m pytest -q` reports 0 failed. + - Frontend full suite: `cd frontend && npx vitest run` reports 0 failed. + - bandit -r backend/ reports 0 HIGH severity findings. + - npm audit --audit-level=high reports 0 high/critical vulnerabilities. + - test_cloud_security.py (or test_cloud_detail_parity.py) includes the detail-endpoint and force-enqueue owner/admin/no-leak negatives: `grep -nE 'detail|force' backend/tests/test_cloud_security.py backend/tests/test_cloud_detail_parity.py` shows coverage. + - No secrets/object keys/provider URLs in the diff (secret scan clean). + + Full backend + frontend suites pass; bandit/pip/npm audits clean; detail + force re-analyze covered by owner/admin/no-leak negatives; secret scan clean. + + + + Checkpoint: Human verification of cloud/local file parity + Human verifies the cloud detail view, browser row parity, force re-analyze confirmation, and unsupported-preview-keeps-download behavior against a running app. See what-built and how-to-verify below. + + Cloud/local file parity: a cloud detail view (route cloud-file-detail) backed by CloudItem, shared DocumentDetailSurface used by local + cloud detail, browser row parity (topics + analysis status + Re-analyze/Retry slots), force re-analyze with confirmation, and unsupported-preview-keeps-download-explicit behavior. + + + 1. Start the app: `docker compose up` (or backend `uvicorn main:app --reload` + frontend `npm run dev`). + 2. Log in, open /cloud, navigate into a connected provider folder. + 3. Click a cloud file row — confirm it opens a DETAIL view (URL contains /cloud/.../item/...), NOT an immediate preview or download. + 4. On an un-analyzed cloud file: confirm the detail shows "No analysis yet" + an "Analyze file" action; trigger Analyze and watch status progress. + 5. On an analyzed cloud file: confirm extracted text, topic badges, analysis status, and a "Re-analyze" action appear (NO "Re-classify" text anywhere). + 6. Click Re-analyze on a current file: confirm a confirmation dialog appears ("Re-analyze this file? Existing extracted text and topics stay visible…") and proceeding re-runs analysis without losing prior text/topics. + 7. On a file whose preview is unsupported: confirm "Preview unavailable" + reason shows and Download stays active and does NOT auto-download. + 8. Open a LOCAL document at /document/:id and confirm the detail layout (header, topics, extracted text, Re-analyze) matches the cloud detail layout. + 9. In the browser grid, confirm cloud rows show topic badges + analysis status in the same place as local rows. + + Type "approved" or describe any parity/behavior issues to fix + + + + Task 2: Update CLAUDE.md + README, bump version, atomic commit + push + + - CLAUDE.md (Current state line in GSD Workflow section; shared module map tables backend + frontend; Documentation Protocol; Git Protocol; version bump rule) + - README.md (Features section, env var table, version reference) + - backend/main.py (FastAPI version="0.4.0") + - frontend/package.json ("version": "0.4.0") + - .planning/ROADMAP.md (Phase 14.1 plan checklist to tick) + + + Update CLAUDE.md: change the "Current state" line in the GSD Workflow section to record Phase 14.1 complete (cloud/local file parity: cloud detail route + view, shared DocumentDetailSurface, browser row parity, force re-analyze, unsupported-preview keeps download explicit). Add to the FRONTEND shared module map a row for frontend/src/components/storage/DocumentDetailSurface.vue (shared detail surface used by DocumentView and CloudDetailView) and note CloudDetailView.vue + the cloud-file-detail route as thin data providers. Add to the BACKEND shared module map the CloudItemDetailOut schema and resolve_owned_cloud_item_detail (owner-scoped cloud detail + topics, metadata-only), the GET .../items/{id}/detail route, and the force flag on enqueue. Add non-negotiable rules: visible copy says "Re-analyze" not "Re-classify"; cloud row click opens the cloud detail route (no auto preview/download); cloud detail resolution is metadata-only (no byte hydration); CloudItemDetailOut/CloudItemOut remain allowlists excluding object_key/credentials_enc/provider URL. Update README.md if any user-facing feature/route/behavior changed (add cloud file detail view + Re-analyze to the Features section; no new env vars expected). Bump the version: per CLAUDE.md protocol, Phase 14.1 is an inserted hardening phase shipping user-facing changes — bump the PATCH segment in backend/main.py (version="0.4.0" → "0.4.1") and frontend/package.json ("0.4.0" → "0.4.1") so both match. Tick the Phase 14.1 plan checkboxes in ROADMAP.md. Stage explicitly and commit atomically per CLAUDE.md Git Protocol: `git add backend/ frontend/ CLAUDE.md README.md .planning/` then commit `feat(14.1): cloud/local file parity — shared detail surface, cloud detail route, force re-analyze`, then push. Do NOT use git add -A. + + + cd backend && grep -c 'version="0.4.1"' main.py && cd ../frontend && grep -c '"version": "0.4.1"' package.json && cd .. && grep -c 'DocumentDetailSurface' CLAUDE.md && grep -c 'Phase 14.1' CLAUDE.md + + + - backend/main.py version is "0.4.1": `grep -c 'version="0.4.1"' backend/main.py` returns 1. + - frontend/package.json version is "0.4.1": `grep -c '"version": "0.4.1"' frontend/package.json` returns 1. + - CLAUDE.md documents the shared surface and phase completion: `grep -c 'DocumentDetailSurface' CLAUDE.md` >= 1 and the Current state line references Phase 14.1. + - CLAUDE.md adds the cloud detail backend symbols: `grep -cE 'CloudItemDetailOut|resolve_owned_cloud_item_detail' CLAUDE.md` >= 1. + - ROADMAP Phase 14.1 plans are checked off and a single atomic commit exists: `git log --oneline -1` shows the feat(14.1) message. + - The commit is pushed (git status shows clean / ahead-by-0 after push). + + CLAUDE.md + README updated, both versions bumped to 0.4.1, ROADMAP ticked, one atomic feat(14.1) commit created and pushed. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| diff → repo | committed code/tests/docs must contain no secrets, tokens, provider URLs, or object keys | +| dependency tree → app | audits must show no high/critical CVEs | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-14.1-13 | Information Disclosure | committed artifacts | mitigate | Secret scan over diff; no credentials/object_key/provider URL in code/tests/planning | +| T-14.1-14 | Tampering / Vulnerable deps | dependency tree | mitigate | bandit + pip audit + npm audit gate; no new packages added | +| T-14.1-15 | Elevation of Privilege | detail + force endpoints (regression) | mitigate | Security gate re-runs owner/admin negatives on the new surfaces | +| T-14.1-SC | Tampering | npm/pip installs | accept | No package installs this phase (RESEARCH Package Legitimacy Audit: none) | + + + +- `cd backend && python -m pytest -q` → 0 failed; `cd frontend && npx vitest run` → 0 failed. +- `bandit -r backend/ -lll`, `pip audit`, `npm audit --audit-level=high` → clean. +- Versions match at 0.4.1; CLAUDE.md + README updated; ROADMAP ticked; atomic commit pushed. +- Human checkpoint approved for visual/interaction parity. + + + +- All gates green (tests, security, audits, secret scan). +- Docs + versions updated and committed atomically; pushed. +- Human verification of cloud/local parity approved. + + + +## Artifacts this phase produces (Plan 05) + +- Version bump: backend/main.py and frontend/package.json → 0.4.1. +- Docs: CLAUDE.md updated (current-state line, shared module maps for DocumentDetailSurface + CloudItemDetailOut/resolve_owned_cloud_item_detail + cloud detail route, new non-negotiable parity rules); README Features section updated. +- ROADMAP: Phase 14.1 plan checkboxes ticked. +- One atomic `feat(14.1)` commit, pushed. + + + +Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-05-SUMMARY.md` when done + diff --git a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md index a6ff014..fc1d2d0 100644 --- a/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md +++ b/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-UI-SPEC.md @@ -1,10 +1,11 @@ --- phase: 14.1 slug: cloud-local-file-parity-hardening -status: draft +status: approved shadcn_initialized: false preset: none created: 2026-06-26 +reviewed_at: 2026-06-26T18:51:13Z --- # Phase 14.1 — UI Design Contract @@ -39,7 +40,7 @@ Declared values (must be multiples of 4): | 2xl | 48px | Major section separation | | 3xl | 64px | Reserved for full-page spacing only | -Exceptions: `36px` minimum dense icon-button targets inside `StorageBrowser.vue` rows; `44px` minimum for modal and confirmation actions; sticky browser headers keep `12px` vertical padding with `16px` mobile and `24px` desktop horizontal padding. +Dense row icon buttons use `32px` targets inside `StorageBrowser.vue` rows. Sticky browser headers use `16px` vertical padding with `16px` mobile and `24px` desktop horizontal padding. Modal, footer, confirmation, and primary actions use `48px` minimum targets. --- @@ -73,9 +74,9 @@ Warning and stale states use the existing amber family (`amber-50/200/500/800`), | Element | Copy | |---------|------| -| Primary CTA | `Analyze file` when no analysis exists; otherwise the first content action is `Preview` or `Open` and analysis becomes `Re-analyze` | +| Primary CTA | `Analyze file` when no analysis exists; otherwise the first content action is `Preview file` or `Open file` and analysis becomes `Re-analyze` | | Empty state heading | `No analysis yet` | -| Empty state body | `Analyze this file to extract text and topics.` Follow with a subtle source note for cloud files: `Preview, download, and analysis still run through DocuVault.` | +| Empty state body | `Analyze this file to extract text and topics.` Follow with a subtle source note for cloud files: `Preview file, download, and analysis still run through DocuVault.` | | Error state | `This file could not be fully analyzed.` Follow with the next step: `Retry analysis or re-analyze to refresh the result.` | | Destructive confirmation | `Re-analyze this file? Existing extracted text and topics stay visible until the new analysis finishes.` | @@ -109,7 +110,7 @@ Visible copy rule: user-facing text must say `Re-analyze` everywhere. `Re-classi 2. Cloud rows open a dedicated detail route, named `cloud-file-detail` or an equivalent explicit named route under `/cloud/:connectionId/...`. 3. Cloud route params use `connectionId` plus opaque provider item identity. Vue Router handles encoding; frontend code must not split or infer structure from provider IDs. 4. Clicking a cloud row opens detail first. It does not auto-preview and does not auto-download. -5. Preview/Open and Download are explicit actions from the detail header and any mirrored row action slots. +5. `Preview file` / `Open file` and Download are explicit actions from the detail header and any mirrored row action slots. --- @@ -139,6 +140,8 @@ The shared detail surface uses this section order for both local and cloud files 5. Extracted text section 6. Secondary controls and inline error/help copy +The filename plus primary action cluster is the first visual anchor on detail screens. + Header rules: - Display title uses `24px` semibold with wrapping allowed for long filenames. @@ -149,7 +152,7 @@ Header rules: Card rules: - Detail sections stay white with `border-gray-200` and `rounded-xl`. -- Internal section padding is `20-24px`. +- Internal section padding is `24px`. - Section headings use `18px` semibold for top-level detail sections and `14px` semibold for compact labels. --- @@ -169,10 +172,10 @@ Card rules: ## Preview, Download, And Unsupported-State Contract -1. Preview and Download stay adjacent in the detail header action cluster. -2. If preview is supported, `Preview` or `Open` is the first content action. -3. If preview is unsupported but download is supported, Preview remains visible but disabled, with helper copy `Preview unavailable` plus the backend reason. Download remains active. -4. Preview never triggers an automatic download. +1. `Preview file` and Download stay adjacent in the detail header action cluster; when inline opening is the correct capability, the label becomes `Open file`. +2. If preview is supported, `Preview file` or `Open file` is the first content action. +3. If preview is unsupported but download is supported, `Preview file` remains visible but disabled, with helper copy `Preview unavailable` plus the backend reason. Download remains active. +4. `Preview file` never triggers an automatic download. 5. No cloud UI may expose provider URLs, provider hostnames, raw tokens, or cache object keys. 6. Any content action copy that differs between local and cloud must be justified by ownership or capability, not by layout convenience. @@ -190,11 +193,12 @@ Card rules: ## Accessibility And Feedback Contract -1. Dense row icon buttons may use `36px` targets; modal, footer, and primary actions use `44px` minimum targets. -2. Disabled actions must remain keyboard-focusable only when they can surface an explanatory reason; otherwise they are non-interactive and accompanied by inline explanatory text. -3. Status banners use semantic color plus icon plus text; color alone is insufficient. -4. Focus indicators use the indigo ring pattern already present in the app. -5. Long filenames, provider locations, and error reasons must wrap instead of truncating inside the detail header and status areas. +1. Dense row icon buttons use `32px` targets; modal, footer, and primary actions use `48px` minimum targets. +2. Dense icon-only row actions require a tooltip and a visible text fallback anywhere hover is unavailable or the action collapses into a menu. +3. Disabled actions must remain keyboard-focusable only when they can surface an explanatory reason; otherwise they are non-interactive and accompanied by inline explanatory text. +4. Status banners use semantic color plus icon plus text; color alone is insufficient. +5. Focus indicators use the indigo ring pattern already present in the app. +6. Long filenames, provider locations, and error reasons must wrap instead of truncating inside the detail header and status areas. --- @@ -205,7 +209,7 @@ The implementation must be testable against these UI outcomes: - Paired local/cloud browser assertions verify identical row title structure, topic placement, status placement, and analysis-action slots. - Paired local/cloud detail assertions verify identical section order, action order, and stale/partial/failed rendering rules. - Cloud row click opens detail instead of immediate preview/download. -- Unsupported preview keeps Download active and never auto-downloads from the Preview trigger. +- Unsupported preview keeps Download active and never auto-downloads from the `Preview file` trigger. - No rendered output or API-fed UI state leaks provider URLs, credentials, or cache object keys. - Visible UI contains `Re-analyze` and no surviving `Re-classify` copy. @@ -213,11 +217,11 @@ The implementation must be testable against these UI outcomes: ## Checker Sign-Off -- [ ] Dimension 1 Copywriting: PASS -- [ ] Dimension 2 Visuals: PASS -- [ ] Dimension 3 Color: PASS -- [ ] Dimension 4 Typography: PASS -- [ ] Dimension 5 Spacing: PASS -- [ ] Dimension 6 Registry Safety: PASS +- [x] Dimension 1 Copywriting: PASS +- [x] Dimension 2 Visuals: PASS +- [x] Dimension 3 Color: PASS +- [x] Dimension 4 Typography: PASS +- [x] Dimension 5 Spacing: PASS +- [x] Dimension 6 Registry Safety: PASS -**Approval:** pending +**Approval:** approved