> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
- Produces `ProductArchiveStore(root: Path)` with `upsert_product`、`create_version`、`add_image`、`add_wordcloud_archive`、`get_product`、`list_products`、`mark_pending_cleanup`、`restore_product`、`due_product_cleanups`、`purge_product`。
-`upsert_product` creates `prod_<uuidhex>` for `manual`; `external` requires non-empty `external_product_id` and conflicts on `(source, external_product_id)`.
- [ ]**Step 1: Write the failing persistence and idempotency tests**
Expected: FAIL because `product_archive_store` and its contracts do not exist.
- [ ]**Step 3: Write the minimal store and schema**
Add Pydantic response/request models in `schemas.py`. Create SQLite tables `products`, `product_versions`, `product_images`, `product_wordcloud_archives`, and `cleanup_records`, with indexes on `name`, `(source, external_product_id)`, `product_id`, and `purge_after`. Configure connections with WAL, 5-second busy timeout and `sqlite3.Row`, matching `MetadataStore`.
Reject blank names, external inputs without ID, unknown IDs, and archive rows whose version does not exist. Do not physically delete a product in this task.
- Produces `validate_word_locations_db(db_path: Path) -> None` and `copy_word_locations_snapshot(source: Path, destination: Path) -> str`; the return is a SHA-256 checksum.
Add tests for no layer list (visible by default), absent elements (not returned), missing `job_id` (not returned), and a non-SQLite source file (raises `ValueError` and leaves no destination file).
Validate that `word_locations` exists with `sqlite3`. Copy through `<destination>.tmp`, calculate SHA-256 in chunks, reopen the temporary copy, then atomically `replace()` it.
Expected: FAIL because product service and routes do not exist.
- [ ]**Step 3: Write archive transaction and authenticated routes**
Save the preview as `design-preview.png` only after checking PNG magic bytes and decoding it with Pillow. Require source job status `success` and a current DB artifact before copying. Create the version, image row (`kind="design_preview"`, `is_cover=1`) and every archive row only after its file exists. On any error, remove the incomplete version directory and transaction rows.
Parse `document_json` with `json.loads`; call `_require_orders_auth(request)` for every product read/write route. Resolve asset metadata with `_read_asset_meta(_asset_dir(asset_id))`; never accept a client job ID. Return 201 for version creation, 400 for malformed JSON/file, 404 for absent product, and 409 for a pending-cleanup product.
- [ ]**Step 4: Run API and existing regression tests**
A temporary candidate is a successful job containing a DB artifact whose metadata `created_at` is at least 30 days old and whose job ID is not present in `product_wordcloud_archives`. At 23 days add it to `reminder_job_ids`. `apply()` must rerun `preview()`, delete workspace through `storage.remove_job_dir`, then delete metadata. It must delete product files only after `purge_after <= now`; missing directories are idempotent success.
Update `storage_summary()` and `storage_metrics.py` to report archive-protected job IDs separately. Ordinary asset references only extend the temporary window; they are not permanent protection.
Start a daemon scheduler that runs `preview()` once on application startup and every 24 hours. It calls `apply()` only when `CLEANUP_APPLY_ENABLED=true`. Logs may contain counts, job IDs and byte totals but no person names from location data.
- [ ]**Step 5: Run focused tests and Compose validation**
-`createDesignPreviewBlob` uses `serializeDocument(document, stickers, { includeBackground: true })`, draws the resulting SVG into a canvas at document dimensions, and returns a PNG Blob.
Expected: FAIL because the new modules do not exist.
- [ ]**Step 3: Write the typed client and preview converter**
Use `apiUrl` and `ensureOk` from `frontend/src/lib/api.ts`. `archiveProductVersion` appends `JSON.stringify(document)` as `document_json` and a `File` named `design-preview.png` as `preview`; it never appends job IDs. Pass the current order-admin token via the existing Bearer-header convention.
Create the preview from the existing `serializeDocument` exporter, which already includes visible layers, text, shapes, rotations and embedded sticker assets. Load its SVG Blob into `Image`, draw once to `HTMLCanvasElement`, reject empty/non-PNG output, and revoke the object URL in success and failure paths.
Expected: FAIL because the dialog and action text do not exist.
- [ ]**Step 3: Write dialog states and exact user copy**
Use four explicit local states: `loadingProducts`, `creatingProduct`, `archiving`, and `error`. Let users search existing products by visible name/SKU or choose “新建产品”; manual creation requires name and accepts optional SKU/规格. The displayed source count is a preview only; server scanning remains authoritative.
Use this confirmation copy:
```text
已检测到 N 份画布词云,将全部归档。
当前完整设计将保存为产品预览图,后续可替换为实景图。
```
When zero sources are detected, use:
```text
当前画布未检测到可归档词云。可以建立产品,但该版本会标记为“无词云归档数据”。
```
- [ ]**Step 4: Wire CanvasStudio action and success state**
Place `加入产品列表` next to existing `添加词云` in the CanvasStudio navbar. On success close the dialog and show:
```text
已归档至产品《{name}》· {count} 份词云位置数据已长期保存
```
Do not show a raw product ID or job ID. Pass `normalizedDocument` to keep hidden-layer treatment identical to the exported preview.
- [ ]**Step 5: Add style using existing tokens**
Create a wide modal with left 4:3 contain preview and right product selector/metadata. Reuse `--bg-panel`, `--border`, `--accent`, `--success`, `--warn`, `--font-main`, and `--font-mono`. Add classes `product-status-archived`, `product-status-empty`, and `product-status-pending-cleanup`.
Expected: FAIL because the page and route do not exist.
- [ ]**Step 3: Write product list and detail flow**
Each list row has a left `design_preview` thumbnail, center name plus optional SKU/规格, and right human-readable archive status/count. With no cover, render a neutral `暂无预览图` frame. Detail shows preview, version timestamp, wordcloud count, and a collapsed “系统信息” section with copyable product ID.
For `pending_cleanup`, render `待清理 · 将于 YYYY/MM/DD 删除` plus “恢复产品” and “立即删除”. The latter only sends soft delete; physical deletion remains CleanupService responsibility.
- [ ]**Step 4: Wire navigation and authorization**
Add `产品档案` to the home actions alongside `生产订单` and `查找`. Do not replace OrdersPage: WCD job status remains a separate production-task view. Product page uses the same stored order-admin token and existing login presentation when no token is available.
Document that `--apply` is permitted only after `CLEANUP_APPLY_ENABLED=true` and a dry-run candidate list has been reviewed. Document product recovery before `purge_after`.
- [ ]**Step 4: Run complete local verification**
Run: `cd backend && pytest -q`
Run: `node --test frontend/tests/*.test.mjs`
Run: `cd frontend && npm run build`
Run: `docker compose config`
Run: `docker compose up -d --build`
Run: `docker compose ps`
Expected: tests and frontend build PASS; Compose reports healthy services. Manually create a wordcloud, insert it into a visible layer, add it to a manual product, confirm preview/card rendering, hide a second wordcloud layer, and verify only the visible source is archived.
- [ ]**Step 5: Inspect a dry-run without deleting data**
- Visible canvas-only scanning, hidden-layer exclusion, deleted/non-inserted exclusion and deduplication: Tasks 2 and 8.
- Product main record, stable external ID, manual products and name-first presentation: Tasks 1, 3 and 7.
- Independent word-location snapshots and source-job traceability: Task 3.
- Default full-design preview and future multi-source image model: Tasks 1, 3, 5, 6 and 7.
- 30-day temporary cleanup, day-23 reminder, product soft-delete grace period and dry-run: Tasks 4 and 8.
- Existing Docker persistence gap: Task 4.
- Admin authorization and server-side archive decisions: Tasks 3 and 7.
- Local Docker validation required by repository instructions: Task 8.
### Placeholder scan
The plan names every module, public interface, test file, command, persistent directory and cleanup state. It contains no deferred implementation markers or unspecified error-handling steps.
### Type consistency
- Task 1 defines ProductArchiveStore, ProductInput and response records used by Tasks 3 and 4.
- Task 2 defines WordcloudSource and snapshot helpers consumed by Task 3.
- Task 3 defines routes consumed by Tasks 5 and 7.
- Task 4 depends only on ProductArchiveStore archive-source lookup and existing Storage/MetadataStore.
- Task 5 defines the frontend client and preview creator consumed by Tasks 6 and 7.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.