# Phase 1 metaobject schemas

**Zebra Skimmers redesign · data layer specification · v0.1**

Companion document to the [Zebra Redesign Plan](https://zebra-redesign-plan.pages.dev/). This spec defines the three Shopify metaobject types that ship before any theme code is written, plus the metafields that bind them to products and the theme. The intent is to lock the data layer in Phase 1 so Phase 2 component work can build against stable shapes.

---

## 00 · Scope and principles

Three schemas in this document:

1. **`product_document`** — files attached to products (datasheets, manuals, drawings, certifications, SDS). Replaces the current page-scrape inheritance that gives every product 46 PDFs whether they're relevant or not. The 51 unique documents in the catalog need explicit attachments, not transitive ones.

2. **`product_relationship` + `fits_products`** — curated and data-driven relationships between products. Replaces the empty `related_products` array on every current product and the prose-only compatibility descriptions. Two complementary mechanisms: parent-curated for editorial pairings, child-declared for compatibility at variant granularity.

3. **`ai_policy` + `ai_crawler_policy`** — admin-editable AI metadata and crawler rules. Captures Zebra's current bespoke meta-tag schema, adds the standards-based controls that crawlers actually honor, and makes both admin-editable without theme code changes.

**Naming conventions.** Metaobject types are `snake_case` singular. Metafield namespaces are `zs` for Zebra-specific keys that won't make sense in the sellable Theme Store version, and `theme` for keys that should ship as part of any installation of this theme. Field keys are `snake_case`. Choice values are `snake_case`. Where Shopify's metaobject editor labels a field, the label is title-cased; the underlying key stays snake_case.

**A note on the sellable theme.** Everything in this document needs to work both for Zebra and for any future Theme Store merchant. The schemas use generic field names — `document_type`, `relationship_type`, `industry` — rather than industrial-specific ones. Choice values are extensible. Where Zebra needs a vertical-specific choice that wouldn't make sense for a coffee retailer (e.g., `sds` as a `document_type`), it's still defined here because removing it doesn't hurt the coffee retailer and adding it later means migrating real data.

---

## 01 · `product_document`

Files attached to products, with type and metadata that drives both PDP rendering and search/filtering.

### Definition

| Property | Value |
|---|---|
| **Type** | `product_document` |
| **Display name** | Product document |
| **Description** | A file resource attached to one or more products — datasheet, manual, drawing, certification, SDS, etc. |
| **Access** | Storefront: read · Admin: read/write |

### Fields

| Key | Type | Required | Validations / Notes |
|---|---|---|---|
| `title` | `single_line_text_field` | yes | Display name. e.g., "ZVA8 Sidewinder — Tube Oil Skimmer Manual" |
| `document_type` | `single_line_text_field` | yes | Choices enforced via validation regex or list. See **Document types** below. |
| `file` | `file_reference` | one of `file` / `external_url` | PDF, STEP, DXF, ZIP. Native Shopify file upload. |
| `external_url` | `url` | one of `file` / `external_url` | Fallback for Cloudinary-hosted or supplier-hosted documents. The current Zebra PDFs live on `res.cloudinary.com` and `cdn.shopify.com` — both are valid sources during migration. |
| `language` | `single_line_text_field` | no | BCP-47 locale code. Default `en`. Choices: `en`, `es`, `de`, `fr`, `it`, `pt`, `ja`, `zh`. |
| `version` | `single_line_text_field` | no | Free text. e.g., `v3.1`, `rev 2.0`. |
| `revision_date` | `date` | no | Last revision of the document itself, not the metaobject entry. |
| `valid_until` | `date` | no | For certifications, calibration certs, regulatory documents with expiry. |
| `file_size_bytes` | `number_integer` | no | Auto-derived from `file_reference` when set; manually entered for `external_url`. The theme uses this to render "PDF · 2.4 MB" without fetching the file. |
| `description` | `multi_line_text_field` | no | One-line blurb shown under the filename in the document list. |
| `audience` | `list.single_line_text_field` | no | Choices: `buyer`, `installer`, `operator`, `maintenance`, `compliance`. Used by the PDP to optionally group documents by who needs them. |
| `thumbnail` | `file_reference` (image) | no | First-page preview for the document card. Optional but improves UX. |

### Document types

Enum values for `document_type`, chosen so they cover both Zebra's current 12 patterns and the broader manufacturing vertical:

```
datasheet          — Marketing technical sheet (what Zebra calls "Profile Sheet")
manual             — Installation / operation / service manual
install_guide      — Standalone installation instructions
parts_diagram      — Exploded view, parts list
repair_kit         — Repair kit contents / instructions
specification      — Formal spec document distinct from datasheet
drawing_2d         — Mechanical drawing (PDF/DWG)
drawing_3d         — CAD model (STEP, IGES, STL)
sds                — Safety Data Sheet (chemicals, lubricants)
certification      — CE, UL, ISO, calibration certs
warranty           — Warranty document
white_paper        — Long-form technical / application paper
catalog            — Multi-product catalog or brochure
software           — Firmware, drivers, configuration files
other              — Escape hatch; should be rare
```

### Product binding

Documents are attached to products via a metafield:

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `documents` |
| **Type** | `list.metaobject_reference[product_document]` |
| **Validations** | Linked type must be `product_document` |
| **Access** | Storefront: read |
| **Description** | Files attached to this product. Order is preserved and meaningful — first entries surface highest in the documentation section. |

Same metafield exists on the variant level for variant-specific documents (e.g., the 220V European model has its own install guide):

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `documents` |
| **Owner type** | `variant` |
| **Type** | `list.metaobject_reference[product_document]` |

When the PDP renders the documentation section, it merges product-level and variant-level documents, deduplicating by metaobject ID, with variant-specific documents surfacing first.

### Shop-level documents

A small set of documents apply to the whole catalog — the Zebra Skimmers Product Catalog PDF, the company SDS index, general warranty terms. These attach via a shop metafield instead of a product metafield:

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `shop_documents` |
| **Owner type** | `shop` |
| **Type** | `list.metaobject_reference[product_document]` |

The PDP can optionally surface relevant shop documents in a "Company resources" sub-section.

### Example entries (from real Zebra catalog)

```yaml
- title: "ZVA8 Sidewinder — Tube Oil Skimmer Manual"
  document_type: manual
  external_url: "https://res.cloudinary.com/zebra-skimmers-corp/.../tube-skimmer-manual.pdf"
  language: en
  audience: [installer, operator]
  description: "Installation, operation, and maintenance for all ZVA8 Sidewinder variants."

- title: "ZVA8 Sidewinder — Datasheet"
  document_type: datasheet
  file_reference: /* uploaded to Shopify Files */
  language: en
  version: "v3.1"
  audience: [buyer]

- title: "Tube Replacement Instructions"
  document_type: install_guide
  external_url: "https://res.cloudinary.com/.../tube-replacement-instructions.pdf"
  audience: [maintenance]
  description: "How to replace the collection tube on a ZVA8 Sidewinder. Applies to all reach lengths."

- title: "Odor Control Tablets — SDS"
  document_type: sds
  language: en
  audience: [compliance]
  valid_until: 2027-03-15
```

The same `product_document` entry attaches to multiple products. The Zebra Product Catalog attaches at the shop level once; the Tube Oil Skimmer Manual attaches to every ZVA8 product but as a single metaobject reference, not as 14 duplicate PDFs.

### Migration path from current data

The AI Search pipeline at `zebra-product-images/product_data/products_full.json` already has 51 unique documents with their `name`, `url`, `local_path`, `size`. A migration script POSTs `metaobjectCreate` mutations for each unique document (keyed by URL), then updates each product's `theme.documents` metafield with the curated subset (currently each product has all 46 — the human cleanup is filtering to the actually-relevant ones, which probably averages 4-8 per product).

---

## 02 · `product_relationship` + `fits_products`

Two parallel mechanisms for product-to-product relationships. They solve different problems and both ship in v1.

### A. `product_relationship` metaobject — curated

For editorial pairings. Parent declares "these are the related products" with optional metadata. Examples: "Required accessory", "Often bought together", "Replacement part", "Upgrade path".

#### Definition

| Property | Value |
|---|---|
| **Type** | `product_relationship` |
| **Display name** | Product relationship |
| **Description** | A curated link from a parent product to a related product or variant, with semantic type. |

#### Fields

| Key | Type | Required | Notes |
|---|---|---|---|
| `target_product` | `product_reference` | yes | The related product. |
| `target_variant` | `variant_reference` | no | Specific variant of the target if the relationship only applies to one. |
| `relationship_type` | `single_line_text_field` | yes | See **Relationship types** below. |
| `display_label` | `single_line_text_field` | no | Override for the section heading. Defaults are derived from `relationship_type`. |
| `notes` | `multi_line_text_field` | no | Editorial copy: "Required for installation on Doosan-style machines." |
| `display_priority` | `number_integer` | no | Sort order within a group. Lower numbers first. Default 100. |

#### Relationship types

```
accessory                 — Optional add-on (mounts, adapters, accessories)
required_accessory        — Must be purchased separately for product to function
replacement_part          — Spare part for this product
upgrade                   — A premium version of this product
kit_component             — Part of this product as sold (informational)
frequently_bought_together — Marketing-driven pairing
similar_product           — Alternative product
predecessor               — Older version this product replaces
```

#### Product binding

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `related_products` |
| **Type** | `list.metaobject_reference[product_relationship]` |

### B. `fits_products` — reverse-lookup compatibility

For data-driven compatibility at variant granularity. Child product declares which parent variants it fits. The PDP queries products with `fits_products` containing the current variant when rendering the parts and accessories section.

This is the variant-level version of the existing QR parts system pattern. It scales better than curated relationships because adding a new compatible part doesn't require editing every compatible parent.

#### Metafield definition

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `fits_products` |
| **Owner type** | `product` |
| **Type** | `list.variant_reference` |
| **Access** | Storefront: read |
| **Description** | Variants this product is compatible with. Used by parent PDPs to filter the parts and accessories section by selected variant. |

When the PDP renders the parts and accessories section, it issues a Storefront API query along the lines of:

```graphql
products(query: "metafield:theme.fits_products contains gid://shopify/ProductVariant/<current_variant_id>") {
  edges { node { id title handle priceRange ... } }
}
```

The result is the list of parts and accessories that fit the currently-selected variant. Client-side JS updates this list when the variant picker changes — same pattern as the existing QR parts system, but driven by metafield data instead of compatible_with strings.

#### Why variant_reference and not product_reference

The compatibility data is more honest at the variant level. A "Replacement 115 VAC power supply" fits 115 VAC variants of multiple products, not all variants. A "Magnetic base mount" fits all variants of one product. The variant_reference list handles both — for the magnetic base, the metafield lists every variant of the parent product; for the power supply, it lists only the 115 VAC variants. The same metaobject schema serves both cases.

#### How the two mechanisms combine

The PDP renders the parts and accessories section in two passes:

1. **Curated pass.** Read `theme.related_products` from the current product. Render the metaobject entries in their declared order, grouped by `relationship_type` ("Required accessories" first, then "Optional accessories", then "Replacement parts").

2. **Compatibility pass.** Query products with `theme.fits_products` containing the current variant. Render any that aren't already in the curated list, grouped by category or sorted alphabetically.

Both passes feed the same UI. The merchant can curate when they want editorial control, or let compatibility data drive the section when they don't. Most products will use both: a short curated list of the "right answers" plus a longer compatibility-driven list of "everything that technically fits."

### Example entries (from real Zebra catalog)

```yaml
# Parent: ZVA8-08 (Sidewinder Tube Skimmer)
# Curated relationships on the parent:

- target_product: handle:zva-tube-skimmer-base-plate  # ZXB8
  relationship_type: required_accessory
  display_label: "Mounting"
  notes: "Required for side-clamp installation. Choose magnetic base or LockJaw variants below for other mounting styles."

- target_product: handle:bgx2-lockjaw-mount  # BGX2
  target_variant: <BGX2 variant>
  relationship_type: accessory
  notes: "Adds positioning flexibility. Requires BGX2.ZVA adapter."

- target_product: handle:zva-to-bgx2-adapter  # BGX2.ZVA
  relationship_type: required_accessory
  notes: "Required only when using BGX2 LockJaw mount."
  display_priority: 90

# Child: ZT8-08 (Tube Skimmer Replacement Tubes)
# Compatibility declared on the child:
fits_products:
  - <ZVA8-08 variant id>
  - <ZVA8-11 variant id>
  - <ZVA8-14 variant id>
  - <ZVA8-17 variant id>
  # ... all standard reach variants
  # NOT ZVA8-11E (European), which uses ZT8-08.E

# Child: ZVA8LSWITCH (Tube Skimmer Replacement Switch)
fits_products:
  - <every ZVA8 variant>
```

### Migration path from current data

The AI Search pipeline's `transform_for_ai_search.py` already extracts SKU references from product descriptions (the "Cross-Product Linking" feature). The extracted references become candidate `product_relationship` entries — a manual review pass classifies each as `accessory`, `required_accessory`, or `replacement_part` and confirms the target. For `fits_products`, the existing `compatible_with` metafield from the QR parts system migrates directly: any product with values in `compatible_with` becomes a `fits_products` entry pointing at the matching variants.

---

## 03 · `ai_policy` + `ai_crawler_policy`

Admin-editable AI metadata and crawler rules. The current Zebra implementation hardcodes meta tags in theme Liquid; the new version drives them from metaobject data so the merchant can update policy without code changes.

### A. `ai_policy` metaobject — content metadata

Single-entry metaobject. Holds the shop-level AI content metadata that gets rendered into `<head>` on every page.

#### Definition

| Property | Value |
|---|---|
| **Type** | `ai_policy` |
| **Display name** | AI policy |
| **Description** | Shop-level AI content metadata. Drives the AI meta tags in `<head>` and the `/llms.txt` document. |
| **Capabilities** | Single entry (set max instances to 1 in the metaobject definition) |

#### Fields

| Key | Type | Required | Choices / Notes |
|---|---|---|---|
| `content_license` | `single_line_text_field` | yes | `all-rights-reserved`, `allowed-with-attribution`, `allowed-no-attribution`, `allowed-non-commercial`. Renders as `<meta name="ai-content-license">`. |
| `training_allowed` | `boolean` | yes | Renders as `<meta name="ai-training-allowed">`. When false, also flips the relevant robots directives. |
| `attribution_required` | `boolean` | no | Used in `llms.txt` rendering. |
| `description` | `multi_line_text_field` | yes | The `ai-description` content. Long form — 1-3 sentences. |
| `summary_short` | `single_line_text_field` | no | One-line version for `llms.txt` blockquote. |
| `keywords` | `list.single_line_text_field` | no | Comma-joined for `ai-keywords` meta. |
| `product_type` | `single_line_text_field` | no | The `ai-product-type` content. e.g., `industrial-equipment`. |
| `industry` | `list.single_line_text_field` | no | Comma-joined for `ai-industry`. e.g., `[manufacturing, CNC machining, metalworking]`. |
| `llms_url` | `url` | no | Absolute URL pointing at the published `/llms.txt`. Used both for the `llms-txt` meta tag and for the `ai-info` link relation. |
| `ai_info_url` | `url` | no | URL of the human-readable AI policy page (currently `/pages/ai-info`). Distinct from `llms_url` because they may serve different audiences. |
| `structured_data_formats` | `list.single_line_text_field` | no | Choices: `json-ld`, `open-graph`, `twitter-cards`, `microdata`. Informational; doesn't drive behavior. |
| `max_image_preview` | `single_line_text_field` | no | Choices: `none`, `standard`, `large`. Drives `<meta name="robots" content="max-image-preview:...">`. Default `large` for product-image-driven catalogs. |
| `last_reviewed_date` | `date` | no | Auto-updates when the metaobject is saved. Powers `ai-content-freshness`. |

The snippet `snippets/ai-meta-tags.liquid` reads this metaobject once and renders the full set of meta tags. Today's `ai-content-freshness` becomes `{{ ai_policy.last_reviewed_date | date: '%Y-%m-%d' }}` instead of being hardcoded.

### B. `ai_crawler_policy` metaobject — per-crawler rules

Multi-entry metaobject. Each entry is one User-Agent rule that ends up in `robots.txt`.

#### Definition

| Property | Value |
|---|---|
| **Type** | `ai_crawler_policy` |
| **Display name** | AI crawler policy |
| **Description** | Per-User-Agent crawl rules. Each entry becomes a stanza in `robots.txt`. |

#### Fields

| Key | Type | Required | Notes |
|---|---|---|---|
| `user_agent` | `single_line_text_field` | yes | e.g., `GPTBot`, `ClaudeBot`, `Google-Extended`, `PerplexityBot`, `anthropic-ai`, `Bytespider`, `CCBot`. Free text so new crawlers can be added without a deploy. |
| `display_name` | `single_line_text_field` | no | Human-readable name for the admin UI. e.g., "OpenAI / GPTBot". |
| `enabled` | `boolean` | yes | Default true. When false, the stanza renders as `User-agent: X / Disallow: /`. |
| `allow_paths` | `list.single_line_text_field` | no | Default `["/"]`. Use empty list with `enabled: false` to fully block. |
| `disallow_paths` | `list.single_line_text_field` | no | Always-disallowed paths regardless of `enabled` state. Default `["/admin", "/cart", "/checkout", "/account", "/orders"]`. |
| `crawl_delay` | `number_integer` | no | Seconds between requests. Most crawlers ignore this but it's part of the standard. |
| `notes` | `single_line_text_field` | no | Admin-only documentation. e.g., "Anthropic's web crawler — used for Claude training corpus refresh." |

The metafield owner is shop, not product:

| Property | Value |
|---|---|
| **Namespace** | `theme` |
| **Key** | `ai_crawler_policies` |
| **Owner type** | `shop` |
| **Type** | `list.metaobject_reference[ai_crawler_policy]` |

`templates/robots.txt.liquid` iterates this list and renders the actual robots.txt:

```liquid
{%- for policy in shop.metafields.theme.ai_crawler_policies.value -%}
User-agent: {{ policy.user_agent }}
{%- if policy.enabled -%}
  {%- for path in policy.allow_paths.value -%}
Allow: {{ path }}
  {%- endfor -%}
{%- else -%}
Disallow: /
{%- endif -%}
{%- for path in policy.disallow_paths.value -%}
Disallow: {{ path }}
{%- endfor -%}
{%- if policy.crawl_delay -%}
Crawl-delay: {{ policy.crawl_delay }}
{%- endif %}

{% endfor -%}

# Sitemaps
Sitemap: {{ shop.url }}/sitemap.xml
```

### Default entries (seeded on theme install)

A theme install creates these entries by default, all set to `enabled: true` with `allow_paths: ["/"]` and the standard `disallow_paths`:

```
- GPTBot              (OpenAI)
- ClaudeBot           (Anthropic)
- anthropic-ai        (Anthropic, legacy)
- Google-Extended     (Google Gemini)
- PerplexityBot       (Perplexity)
- Applebot-Extended   (Apple Intelligence)
- Bytespider          (ByteDance)
- CCBot               (Common Crawl)
- Meta-ExternalAgent  (Meta AI)
- cohere-ai           (Cohere)
- diffbot             (Diffbot)
- Omgilibot           (Webz.io)
```

Allow-by-default matches Zebra's current posture (`allowed-with-attribution`) and the recommended default for the sellable theme. Merchants who want to block all AI crawlers can flip each `enabled` to false in the metaobject editor without touching code.

### The `/llms.txt` document

The emerging convention (proposed by Answer.AI in 2024) is a markdown file at `/llms.txt` describing the site to language models. Shopify doesn't serve arbitrary root-level text files, so the path is:

1. A Cloudflare Worker on `zebraskimmers.com` (CF is already in front) serves `/llms.txt` by either:
   - Rewriting to `/pages/llms-txt` (a regular Shopify page) and stripping HTML to leave markdown, or
   - Reading from KV storage and serving directly.

2. The content of `/llms.txt` is rendered server-side from `ai_policy` + `ai_crawler_policy` + the product catalog. Format:

```markdown
# Zebra Skimmers

> {{ ai_policy.summary_short }}

## Products

- [Tube oil skimmers](/collections/tube-skimmers): {{ collection.description_short }}
- [Belt oil skimmers](/collections/belt-skimmers): ...
- [Coalescers](/collections/coalescers): ...
- [Dazzle automation](/collections/dazzle): ...

## Documentation

- [Datasheets](/pages/datasheets)
- [Installation guides](/pages/install-guides)
- [SDS index](/pages/sds)

## Optional

- [Training library](/pages/learn)
- [AI policy](/pages/ai-info)
```

The companion `/llms-full.txt` (longer form) is generated by concatenating all product descriptions. Both files regenerate on a cron via a Worker scheduled trigger reading from the Storefront API.

### Migration path from current data

The existing hardcoded meta tags in theme Liquid map cleanly to the new fields:

| Existing meta tag | New source |
|---|---|
| `ai-content-license` | `ai_policy.content_license` |
| `ai-training-allowed` | `ai_policy.training_allowed` |
| `ai-description` | `ai_policy.description` |
| `ai-keywords` | `ai_policy.keywords` |
| `ai-product-type` | `ai_policy.product_type` |
| `ai-industry` | `ai_policy.industry` |
| `ai-content-freshness` | `ai_policy.last_reviewed_date` |
| `llms-txt` (link rel) | `ai_policy.llms_url` |
| `link rel="ai-info"` | `ai_policy.ai_info_url` |
| `ai-structured-data` | `ai_policy.structured_data_formats` |

A one-time seed script reads the current values from the hardcoded Liquid and creates the `ai_policy` metaobject entry. After that, the merchant edits in the admin without touching theme code.

---

## 04 · Theme consumption

The three schemas above feed the following theme primitives. None of these need to be designed in this document — they're noted here so the data layer specification has a clear interface contract.

| Schema | Consumed by |
|---|---|
| `product_document` | PDP `product-documentation` section · printable spec sheet generator · sitemap of downloadable resources · footer / shop documents block · search results document-type filter |
| `product_relationship` | PDP `product-related-accessories` section (curated portion) · cart upsell suggestions · email order-confirmation accessory recommendations |
| `fits_products` | PDP `product-related-accessories` section (compatibility portion) · variant-aware filtering when variant picker changes · global parts finder UI · cart "you might need" prompts |
| `ai_policy` | `snippets/ai-meta-tags.liquid` in `<head>` · `robots.txt.liquid` template · `/llms.txt` Cloudflare Worker · structured-data JSON-LD enrichment |
| `ai_crawler_policy` | `robots.txt.liquid` template |

---

## 05 · Open questions

1. **Spec data schema.** Specifications are the biggest data gap (`specifications: dict[0]` on every product in the scraped data). A `product_spec_group` + `product_spec_row` pair of metaobjects is the obvious shape — `spec_group` has `label, sort_order`; `spec_row` has `label, value_text, value_number, unit, sort_order, spec_group_reference`. This deserves its own schema document in Phase 1 because the spec data drives the comparison view, the printable spec sheet, and the spec-filter facets on collection pages. Suggested deliverable: a parallel `phase-1-spec-schema.md` once this document is reviewed.

2. **Document audience grouping in the UI.** The `audience` field on `product_document` is optional. Two design options for the PDP documentation section: (a) flat list ordered by the metafield's array order, (b) grouped by audience with collapsible groups. Recommend (a) for v1 simplicity; (b) is a small addition later if buyer feedback warrants it.

3. **Variant-level documents.** The schema supports variant-scoped documents but Zebra doesn't currently have any in the scraped data. The 220V European variants probably have their own install guides; the Doosan-style variant probably has its own dimension drawings. Worth a data hygiene pass during migration to identify which documents are actually variant-specific.

4. **`fits_products` performance at scale.** A Storefront API metafield query is fine for the current ~120 products but worth load-testing with realistic part counts (a single tube skimmer parent has 14 variants and ~8 compatible parts each = manageable). If query latency becomes an issue, the alternative is a build-time JSON of variant-to-parts mappings served from theme assets or R2.

5. **`product_video` metaobject (not in this document).** The AI Search data already structures videos cleanly (`title, platform, video_id, watch_url, embed_url, thumbnail_url`). A `product_video` metaobject with a corresponding `theme.videos` product metafield is a small addition that would let videos live in the same admin data layer rather than being scraped from third-party embeds. Suggested for Phase 2 alongside the PDP video section.

---

## 06 · Phase 1 deliverable list

1. ✅ This document reviewed and approved
2. Spec schema document (Open question #1) drafted and approved
3. Metaobject definitions created in Zebra's Shopify store via Admin API (`metaobjectDefinitionCreate` mutations)
4. Metafield definitions created (`metafieldDefinitionCreate` for each product / variant / shop binding)
5. Seed script for `ai_policy` and `ai_crawler_policy` defaults
6. Migration script: `product_data/products_full.json` → `product_document` entries (51 unique documents)
7. Migration script: scraped SKU references → candidate `product_relationship` entries (human review pass)
8. Migration script: existing `compatible_with` parts → `fits_products` metafield values
9. Catalog audit deliverable: every product/variant in a buyable state (no DRAFT, no ARCHIVED, no `custom.hidden: true` on solo variants, no $0 price)

---

*Companion to the Zebra Redesign Plan · v0.1 · {{ date }}*
