docs(04-review-download-security): create phase 4 plan
5 plans across 5 waves: Wave 0 TDD stubs, utilities + OutputBlock, ReviewStep implementation, App.tsx wiring, human verify checkpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
---
|
||||
phase: 04-review-download-security
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [04-01]
|
||||
files_modified:
|
||||
- src/utils/downloadFile.ts
|
||||
- src/utils/downloadZip.ts
|
||||
- src/components/wizard/OutputBlock.tsx
|
||||
autonomous: true
|
||||
requirements: [DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, CONF-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "downloadFile(content, filename) creates a Blob and triggers an anchor click download"
|
||||
- "downloadZip(files, zipName) builds a JSZip archive and triggers a download"
|
||||
- "OutputBlock renders pre-formatted content, a copy button, and an optional download button"
|
||||
- "OutputBlock copy button calls clipboard.writeText with block content"
|
||||
artifacts:
|
||||
- path: "src/utils/downloadFile.ts"
|
||||
provides: "Pure download utility — Blob + createObjectURL + anchor click"
|
||||
exports: ["downloadFile"]
|
||||
- path: "src/utils/downloadZip.ts"
|
||||
provides: "ZIP bundle download utility using JSZip"
|
||||
exports: ["downloadZip"]
|
||||
- path: "src/components/wizard/OutputBlock.tsx"
|
||||
provides: "Reusable code block with copy and optional download buttons"
|
||||
exports: ["OutputBlock", "OutputBlockProps"]
|
||||
key_links:
|
||||
- from: "src/components/wizard/OutputBlock.tsx"
|
||||
to: "src/utils/downloadFile"
|
||||
via: "import { downloadFile }"
|
||||
pattern: "downloadFile"
|
||||
- from: "src/utils/downloadZip.ts"
|
||||
to: "jszip"
|
||||
via: "import JSZip from 'jszip'"
|
||||
pattern: "JSZip"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the three utility/component building blocks that ReviewStep will compose: the file download helper, the ZIP bundle helper (installs jszip), and the reusable OutputBlock component.
|
||||
|
||||
Purpose: These are pure, independently testable pieces. Isolating them here keeps ReviewStep.tsx focused on layout and state wiring, not download mechanics.
|
||||
Output: src/utils/downloadFile.ts, src/utils/downloadZip.ts, src/components/wizard/OutputBlock.tsx
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/04-review-download-security/04-RESEARCH.md
|
||||
@src/store/types.ts
|
||||
@src/components/wizard/ReviewStep.test.tsx
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts the executor must implement — the test file already imports these paths. -->
|
||||
|
||||
Expected by ReviewStep.test.tsx:
|
||||
```typescript
|
||||
// src/utils/downloadFile.ts
|
||||
export function downloadFile(content: string, filename: string): void
|
||||
|
||||
// src/utils/downloadZip.ts
|
||||
export async function downloadZip(
|
||||
files: { name: string; content: string }[],
|
||||
zipName: string
|
||||
): Promise<void>
|
||||
|
||||
// src/components/wizard/OutputBlock.tsx
|
||||
export interface OutputBlockProps {
|
||||
label: string;
|
||||
content: string;
|
||||
filename?: string; // if provided, show download button
|
||||
disabled?: boolean; // gates both copy and download (security gate)
|
||||
}
|
||||
export function OutputBlock(props: OutputBlockProps): JSX.Element
|
||||
```
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create downloadFile.ts and downloadZip.ts utilities</name>
|
||||
<files>src/utils/downloadFile.ts, src/utils/downloadZip.ts</files>
|
||||
<behavior>
|
||||
- downloadFile creates a Blob with text/plain;charset=utf-8 MIME type, creates an object URL, clicks a hidden anchor, then revokes the URL immediately after click
|
||||
- downloadZip accepts an array of { name, content } entries, adds each to a JSZip instance, generates a blob asynchronously, then triggers download using the same Blob/anchor pattern
|
||||
- Both utilities have no React dependency (pure browser functions)
|
||||
</behavior>
|
||||
<action>
|
||||
First, install jszip (the only new runtime dependency this phase):
|
||||
```bash
|
||||
npm install jszip
|
||||
```
|
||||
JSZip ships its own TypeScript declarations. Do NOT install @types/jszip (stale, 6 years old).
|
||||
|
||||
Create src/utils/downloadFile.ts:
|
||||
```typescript
|
||||
// src/utils/downloadFile.ts
|
||||
// Triggers a browser file download from a string.
|
||||
// Pattern: MDN URL.createObjectURL — https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static
|
||||
export function downloadFile(content: string, filename: string): void {
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
```
|
||||
|
||||
Create src/utils/downloadZip.ts:
|
||||
```typescript
|
||||
// src/utils/downloadZip.ts
|
||||
// Builds a ZIP archive from file entries and triggers download.
|
||||
// Pattern: JSZip official docs — https://stuk.github.io/jszip/documentation/examples/download-zip-file.html
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function downloadZip(
|
||||
files: { name: string; content: string }[],
|
||||
zipName: string
|
||||
): Promise<void> {
|
||||
const zip = new JSZip();
|
||||
for (const { name, content } of files) {
|
||||
zip.file(name, content);
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = zipName;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
```
|
||||
|
||||
Do NOT create a src/utils/index.ts barrel — ReviewStep will import directly from the specific utility files to keep the mock paths in tests predictable (the test file already uses '../../utils/downloadFile' and '../../utils/downloadZip').
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>Both utility files exist and TypeScript compilation succeeds with no errors. jszip appears in package.json dependencies.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Create OutputBlock.tsx reusable component</name>
|
||||
<files>src/components/wizard/OutputBlock.tsx</files>
|
||||
<behavior>
|
||||
- OutputBlock renders a labelled section with a pre/code block showing the content
|
||||
- A "Copy" button calls navigator.clipboard.writeText(content); button label changes to "Copied!" for 2 seconds then reverts
|
||||
- When filename prop is provided, a "Download" button calls downloadFile(content, filename)
|
||||
- When disabled prop is true, both Copy and Download buttons are disabled (HTML disabled attribute) — this is the SECU-01 gate applied per-block
|
||||
- Copy and Download buttons are always rendered (never conditionally hidden) — disabled state is visual only
|
||||
</behavior>
|
||||
<action>
|
||||
Create src/components/wizard/OutputBlock.tsx:
|
||||
|
||||
```typescript
|
||||
// src/components/wizard/OutputBlock.tsx
|
||||
// Reusable output block: pre-formatted content + copy button + optional download button.
|
||||
// disabled prop gates both actions (SECU-01 security acknowledgement gate).
|
||||
import { useState } from 'react';
|
||||
import { downloadFile } from '../../utils/downloadFile';
|
||||
|
||||
export interface OutputBlockProps {
|
||||
label: string;
|
||||
content: string;
|
||||
filename?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function OutputBlock({ label, content, filename, disabled = false }: OutputBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function handleCopy() {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-700">{label}</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={handleCopy}
|
||||
className="text-xs px-2 py-1 rounded border border-gray-300 disabled:opacity-40"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
{filename && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => downloadFile(content, filename)}
|
||||
className="text-xs px-2 py-1 rounded border border-gray-300 disabled:opacity-40"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<pre className="bg-gray-900 text-gray-100 rounded p-4 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
<code>{content}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Styling uses Tailwind classes consistent with the existing wizard components. The `disabled:opacity-40` class communicates the locked state visually without hiding buttons.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm test 2>&1 | tail -15</automated>
|
||||
</verify>
|
||||
<done>OutputBlock.tsx exists and compiles cleanly. npm test still shows ReviewStep.test.tsx as RED (component does not exist yet) and all other tests GREEN. No new TypeScript errors.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
`npm run build` passes. `npm test` shows: reducer.test.ts GREEN, ReviewStep.test.tsx still RED (ReviewStep not yet created — expected), all prior phase tests GREEN.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- src/utils/downloadFile.ts exports downloadFile function
|
||||
- src/utils/downloadZip.ts exports downloadZip async function
|
||||
- src/components/wizard/OutputBlock.tsx exports OutputBlock and OutputBlockProps
|
||||
- jszip listed in package.json dependencies
|
||||
- TypeScript compilation clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-review-download-security/04-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user