Seed the dev store's collections from a production snapshot

The AFK half of the wayfinder ticket "Seed the dev store from the live
catalogue". Products, metafields and images still come from the CSV export —
the CSV re-hosts images for free — but collection membership cannot: the
Collection column is import-only, takes one collection per product, and this
catalogue runs 1.6 memberships per product across 16 collections.

- scripts/seed-dev-store.mjs (`npm run seed:dev-store`): creates each missing
  collection with its members in one call and publishes it. Uses
  CollectionCreateInput.sources because collectionAddProducts is deprecated at
  2026-10, and publishes because collectionCreate leaves a collection
  unpublished — invisible to the Storefront API the dev store exists to serve.
  Idempotent, never deletes, reports handles it cannot find (which is how a
  partial CSV import shows up), and is a dry run unless given --apply.
- assets/dev-store-seed.json: the baseline the seed is checked against and the
  membership map it consumes — 61 products with option values, weights and
  product types, 16 collections with 100 memberships, and the 10 metafield
  definitions that must exist before the import.
- The ticket now records what the probes settled: the option shape is readable
  from the Storefront API (all 61 use Option1 name=Title; 22 are `Default
  Title`, 39 carry real values, so the import must not normalise), and the
  repo's production credentials are Storefront-scoped only — `products`,
  `collections` and `productsCount` are Access denied without read_products —
  so the ticket's "Admin API script using the credentials the repo already
  mints" was never available. A dev-store custom app is required instead.
This commit is contained in:
tanshu committed 2026-10-16 10:11:32 +05:30
1 parent de0553d0d4
commit 1bc1d5f753
4 files changed
+1354 -2

No files matched your search

@@ -1,7 +1,7 @@
# Seed the dev store from the live catalogue
`Type:` task (HITL)
`Status:` open
`Status:` claimed
Part of [the map](../map.md). Empty the frontier from the top.
@@ -42,3 +42,146 @@ Facts later tickets depend on: which products exist in the dev store, whether th
## Answer
<!-- filled on resolution -->
## 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.