# Seed the dev store from the live catalogue `Type:` task (HITL) `Status:` claimed Part of [the map](../map.md). Empty the frontier from the top. ## Question The dev store `staging-iw0gviik.myshopify.com` is empty. How does it come to hold the live catalogue faithfully, so that Phase 1 Cart work is built against realistic data? The manual CSV route was chosen for this. Verification has now shown exactly where it holds and where it **cannot**: ### ✅ Products, and product metafields with preparation - Export: **Products → Export** → scope → *Plain CSV* or *CSV for Excel/Numbers* → **Export products**. One file. Note that an **All products export is emailed**, not downloaded. - Import: **Products → Import** → add file → *Overwrite products with matching handles* → **Import products**. - Product metafields **do** ride in the CSV as their own columns, named ` (product.metafields..)`. `custom.ingredients` as multi-line text and `custom.energy_kcal` as a number are both documented-supported types. - **Precondition:** the metafield **definitions must already exist in the destination store** (Settings → Custom data → Products). Shopify's docs only promise columns for *defined* metafields. What happens to an undefined metafield's value is undocumented — so create the definitions before importing, not after. - Hazards: with *Overwrite* on, a **blank metafield cell overwrites the destination value as blank**. Images are URLs only and Shopify re-hosts them at import, so production's products and images must stay publicly reachable until the import finishes. There is **no dry run**, imports cannot be cancelled, there is no import history, and the file cap is 15 MB. ### ❌ Collection membership — impossible by CSV The `Collection` column is **never exported**, and on import it is the only column you may add — and it admits **one collection per product**. Most of this catalogue belongs to three or four collections each: `mozimo-carnival-celebration-m` is in `best-sellers`, `gift-collection`, `diwali` and `hampers-boxes` at once. Sixteen collections and their multi-membership therefore need a **second mechanism**. This part re-opens the seeding decision, because the chosen route cannot finish the job alone. ### ⚠️ Variant option shape — read the real export Shopify's docs say a one-option product should carry `Default Title` in `Option1 value`, while the **official sample CSV represents its single-option product as `Option1 name = Title`, `Option1 value = Premium`** — which is exactly this catalogue's stored shape. The docs and their own sample disagree, and the documented hazards around *changing* option values (variants are deleted and recreated, so IDs change) make this worth confirming rather than assuming. Exporting from production settles it, and that export is needed anyway. ## To resolve 1. Export the full catalogue from production and record what `Option1 name` / `Option1 value` actually contain. 2. Create the metafield definitions in the dev store before importing. 3. Run a **small-batch import with *Overwrite* off** to surface per-line errors cheaply, then the full set. 4. Decide the second mechanism for collection membership — admin bulk-editor passes, or a small Admin API script using the credentials the repo already mints. Record which was chosen and why. 5. Record what actually landed: product count, collection count and membership, metafield coverage, anything rebuilt by hand. Facts later tickets depend on: which products exist in the dev store, whether their Variant GIDs differ from production's (they will), and whether the metafield values survived. ## Answer ## Comments ### AFK half worked — and the seeding decision re-opens further Progress against **To resolve**: #1 is settled without an export; #4's stated option is dead and needs a decision the agent cannot make alone. #### 1. Variant option shape — settled (To resolve #1) ✅ The Storefront API exposes `Product.options`, so the export is not needed to read the shape. All **61** products carry one option named `Title`. The *values* are not uniform: | `Option1 value` (= Variant title) | products | | --- | --- | | `Default Title` | 22 | | `100gm` | 17 | | `200gm` | 11 | | `60gm` | 5 | | `1` | 3 | | `250gm`, `150gm`, `12pc` | 1 each | So the sample CSV's shape (name `Title`, a real value) covers 39 of 61 and the docs' `Default Title` covers 22. The import must preserve each product's own value: changing an option value deletes and recreates the Variant, and the resulting Variant GIDs are what Phase 1 carts. **Do not normalise.** (`ADR-0002`'s claim that weight lives in the Variant title holds for 39 products, not all 61; 22 are `Default Title`.) #### 2. The repo's credentials cannot drive an Admin API seed (To resolve #4) ❌ The production app's granted scopes are **Storefront-only**: ``` unauthenticated_read_product_listings unauthenticated_read_product_inventory unauthenticated_read_product_tags unauthenticated_read_checkouts unauthenticated_write_checkouts ``` `products`, `productsCount` and `collections` all return `Access denied … Required access: 'read_products' access scope`. So *"a small Admin API script using the credentials the repo already mints"* is **not available as written**. Any Admin API mechanism needs a custom app installed in the destination store with `write_products`. Two consequences beyond this ticket: - Production's collection *structure* (manual vs smart, and any smart rules) is also unreadable with today's scopes, so a collection rebuild can only reproduce **today's membership as manual collections** — a real fidelity gap, not a bug. - The Phase 0 admin data fixes (61 × `productType`, 41 × weight) could be scripted rather than hand-edited *if* an Admin-scoped app is installed on production. #### 3. Metafield definitions to create first (To resolve #2) — 10 keys The Storefront API cannot enumerate metafields (`metafields` demands explicit `identifiers`), so this comes from `docs/shopify-catalog-inventory.md` §6, measured with an explicit identifier list. These are the populated keys, i.e. the columns the CSV will actually carry: | definition | type | populated | | --- | --- | --- | | `custom.ingredients` | `multi_line_text_field` | 20 | | `custom.energy_kcal`, `custom.protein_g`, `custom.total_fat_g`, `custom.saturated_fat_g`, `custom.carbohydrate_g`, `custom.sodium_mg` | `number_decimal` | 20 each | | `custom.sugar_g` | `number_decimal` | 18 | | `custom.added_sugar_g` | `number_decimal` | 17 | | `custom.tagline` | `single_line_text_field` | 1 | The `facts.*` namespace is empty catalogue-wide, so no `facts.*` definitions are needed for a faithful seed (ticket 09 decides whether the compat branch survives at all). #### 4. Collection membership is 16 collections / 100 memberships Recorded with the full product list, option values, weights and product types in [`assets/dev-store-seed.json`](../assets/dev-store-seed.json) — the baseline the seed is checked against, and the membership map whichever mechanism is chosen will consume. Nothing in the CSV path can carry this. The docs confirm the `Collection` column takes **one collection per product**, is never exported, and — per the overwrite rules — a *blank* non-required column overwrites the existing value as blank. Repeated imports with different `Collection` values would have to *accumulate* membership, which is undocumented and whose failure mode is destroying membership. Not a sound mechanism. #### 5. The second mechanism, at 2026-10 - `collectionAddProducts` is **deprecated**; the current path is `collectionUpdate` with `inclusion.selectionsToAdd`, or `collectionCreate` with `sources[].inclusion.selections`. Both need `write_products`. - **Trap:** `collectionCreate` leaves the collection **unpublished**, so without a `publishablePublish` the membership work is invisible to the Storefront API — the thing the dev store exists to serve. ### Decision: CSV for products, a dev-store app for collections ✅ Products, metafields and images stay on CSV — it re-hosts images for free, whereas an API seed would need staged uploads for 61 products. Collections go through a custom app installed in the dev store, driven by [`scripts/seed-dev-store.mjs`](../../../scripts/seed-dev-store.mjs), which creates each collection with its members in one call and publishes it. Chosen over hand-building because the membership map is a snapshot that will drift, and over an all-API seed because images would then need staging. It also makes the collection step re-runnable whenever production changes. **Fidelity gap, recorded rather than hidden:** production's collection *structure* is unreadable with today's scopes, so the rebuild reproduces today's membership as **manual** collections. If any of the 16 is a smart collection in production, the dev store will hold a manual copy of it. ### Runbook (HITL — the parts that need admin access) In the dev store `staging-iw0gviik.myshopify.com`: 1. **Create the custom app.** Settings → Apps and sales channels → Develop apps → Create an app → Configure Admin API scopes: `read_products`, `write_products`, `read_publications`, `write_publications` → Install app → copy the **Admin API access token** (`shpat_…`). `write_products` covers `collectionCreate`; `write_publications` and `read_publications` cover `publishablePublish` and the `publications` query. 2. **Create the metafield definitions** (Settings → Custom data → Products) *before* importing — the 10 keys and types in §3 above. Undefined metafields on import are undocumented, which is why this comes first. 3. **Export from production, then import.** Products → Export → *Plain CSV* → Export products (an *All products* export is emailed, not downloaded). Then Products → Import → *Overwrite products with matching handles*, small batch first. Leave `Option1 name`/`Option1 value` exactly as exported (§1). Images are URLs only: production's products must stay publicly reachable until the import finishes. 4. **Add the dev-store credentials to `.env`** (gitignored): `SHOPIFY_DEV_STORE_DOMAIN`, `SHOPIFY_DEV_ADMIN_TOKEN`. 5. **Run the collection seed** — dry run, then apply: `npm run seed:dev-store`, then `npm run seed:dev-store -- --apply`. It creates the 16 collections, adds the 100 memberships and publishes each one; existing handles are skipped and any product handle it cannot find is reported, which is how a partial CSV import shows up. 6. **Record what landed** (To resolve #5): product count, collection count and membership, metafield coverage, anything rebuilt by hand — and whether the Variant GIDs differ from [`assets/dev-store-seed.json`](../assets/dev-store-seed.json) (they will). **Unverified until step 5 runs:** the script's mutation shapes come from the 2026-10 Admin reference (it uses the `CollectionCreateInput.sources` form because `collectionAddProducts` is deprecated). Its auth, error and snapshot paths have been exercised against production; the writes have not, because no dev-store app exists yet. That is why the dry run is the default. Facts later tickets depend on once this is done: which products exist in the dev store, the dev store's own Variant GIDs, and whether the metafield values survived the import.