SLICE-6: export & the round-trip lock — canonical serializer + streamed export (SD-0002 §7.2) #29

Merged
ben.stull merged 13 commits from worktree-slice-6-export-roundtrip into main 2026-06-12 05:23:24 +00:00
Owner

SLICE-6 — Export & the round-trip lock (SD-0002 §7.2)

Completes PUC-9 (export the catalog) and PUC-10 (the round-trip is visibly lossless) of SD-0002. Anchor: #13.

What landed

  • Canonical serializer (serialize.py) — one codec, two directions: the CatalogProduct snapshot → canonical CSV, the inverse of codec/validate, in the §6.5.1 row grammar (product fields + option names on row 0, one variant per row, images interleaved, image-only rows, no Option*Value for no-option products).
  • GET /api/products/export?status=all|active|draft|archived — streamed text/csv (StreamingResponse), storefront-scoped, read-only; 409 empty_catalog (raised eagerly, no partial body); 422 on a bad status. repo.export_catalog (status-filtered, handle-sorted snapshot) + service.export_catalog (stream + TEL-3 catalog_exported).
  • Frontend — the Export action on the Products page is now a status-filter menu (native <details> disclosure; download links per status); disabled-with-note when the catalog is empty. exportUrl / EXPORT_STATUSES / isExportEnabled helpers.
  • PUC-10 no-op preview reachable: re-importing an unmodified export previews all-unchanged with the import action disabled.

DoD

  • BUC-3/4 acceptance.
  • INV-12 property testdiff(catalog, import(export(catalog))) = ∅ over 200 generated text-field catalogs + decimal/non-sequential-position coverage.
  • E2E e2e_export_download + e2e_roundtrip_noop green (6/6 total).
  • TEL-3 emitting; DOC-1 (OPERATIONS) + DOC-4 (products-domain) updated.
  • version 0.6.0.

Note

The final code review caught a real INV-12 bug the green tests missed: Variant Position (a CatalogVariant attribute, not a fields{} entry) was emitted as an empty cell, re-importing as "reset to file order" — so a non-sequential stored position round-tripped to a spurious update. Fixed (emit it explicitly, like images); property generator strengthened to non-sequential positions + a targeted regression test.

Gates: check.sh green (lint-imports, 166 backend pytest, frontend build, 14 vitest); e2e.sh 6/6 green.

🤖 Generated with Claude Code

## SLICE-6 — Export & the round-trip lock (SD-0002 §7.2) Completes **PUC-9** (export the catalog) and **PUC-10** (the round-trip is visibly lossless) of [SD-0002](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-content/src/branch/main/specs/SD-0002-products-bulk-csv-import-export.md). Anchor: #13. ### What landed - **Canonical serializer** (`serialize.py`) — one codec, two directions: the `CatalogProduct` snapshot → canonical CSV, the inverse of codec/validate, in the §6.5.1 row grammar (product fields + option names on row 0, one variant per row, images interleaved, image-only rows, no `Option*Value` for no-option products). - **`GET /api/products/export?status=all|active|draft|archived`** — streamed `text/csv` (`StreamingResponse`), storefront-scoped, read-only; `409 empty_catalog` (raised eagerly, no partial body); `422` on a bad status. `repo.export_catalog` (status-filtered, handle-sorted snapshot) + `service.export_catalog` (stream + **TEL-3 `catalog_exported`**). - **Frontend** — the Export action on the Products page is now a status-filter menu (native `<details>` disclosure; download links per status); disabled-with-note when the catalog is empty. `exportUrl` / `EXPORT_STATUSES` / `isExportEnabled` helpers. - **PUC-10 no-op preview** reachable: re-importing an unmodified export previews all-unchanged with the import action disabled. ### DoD - ✅ BUC-3/4 acceptance. - ✅ **INV-12 property test** — `diff(catalog, import(export(catalog))) = ∅` over 200 generated text-field catalogs + decimal/non-sequential-position coverage. - ✅ E2E `e2e_export_download` + `e2e_roundtrip_noop` green (6/6 total). - ✅ TEL-3 emitting; DOC-1 (OPERATIONS) + DOC-4 (products-domain) updated. - ✅ version 0.6.0. ### Note The final code review caught a real INV-12 bug the green tests missed: `Variant Position` (a `CatalogVariant` attribute, not a `fields{}` entry) was emitted as an empty cell, re-importing as "reset to file order" — so a non-sequential stored position round-tripped to a spurious update. Fixed (emit it explicitly, like images); property generator strengthened to non-sequential positions + a targeted regression test. Gates: `check.sh` green (lint-imports, 166 backend pytest, frontend build, 14 vitest); `e2e.sh` 6/6 green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ben.stull added 13 commits 2026-06-12 05:23:13 +00:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(products): serialize Variant Position explicitly — INV-12 round-trip bug
ci / check (push) Has been cancelled
ci / check (pull_request) Has been cancelled
af72299a57
A stored variant position is a CatalogVariant attribute, not a fields{} entry,
so the serializer emitted an empty Variant Position cell — which re-imports as
'reset to file order'. A non-sequential stored position (a merchant can import
explicit positions) then round-tripped to a spurious update, violating INV-12.
Emit str(variant.position) explicitly (like _write_image). The property-test
generator now assigns non-sequential positions to lock the regression, plus a
targeted test. Also wires isExportEnabled into ProductsPage (was dead code).
Both caught by the final code review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ben.stull merged commit 385df8d728 into main 2026-06-12 05:23:24 +00:00
Sign in to join this conversation.