docs(14.1): create phase plan — cloud/local file parity hardening

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-26 21:13:14 +02:00
co-authored by Claude Sonnet 4.6
parent 4873a22f9b
commit f50cce61cb
8 changed files with 1002 additions and 34 deletions
@@ -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"
---
<objective>
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.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Extract DocumentDetailSurface.vue and refactor DocumentView.vue to use it (Re-analyze copy)</name>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Cloud detail route + CloudDetailView.vue + getCloudItemDetail API + store force/reanalyze wiring</name>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>cd frontend && npx vitest run src/views/__tests__/CloudDetailParity.test.js 2>&1 | tail -25</automated>
</verify>
<acceptance_criteria>
- 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).
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
<threat_model>
## 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) |
</threat_model>
<verification>
- `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).
</verification>
<success_criteria>
- 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.
</success_criteria>
<artifacts_produced>
## 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".
</artifacts_produced>
<output>
Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-03-SUMMARY.md` when done
</output>