# Claude Code Context

## Project Overview

`feed-manager` is a standalone PHP project for ingesting ecommerce product data and generating Google Merchant Center-compatible XML feeds.

The first client implementation is `rooflights`, with a variant-level export flow from Shopify to a local XML file.

This file is the single source of truth for `feed-manager` conventions.

## CLAUDE Hierarchy (Feed Manager)

- Follow the nearest `CLAUDE.md` in this project.
- Subdirectory `CLAUDE.md` files provide local constraints and inherit this file by default.
- Current scoped files:
  - `feed-manager/src/CLAUDE.md`
  - `feed-manager/web/CLAUDE.md`
  - `feed-manager/bin/CLAUDE.md`
  - `feed-manager/migrations/CLAUDE.md`
  - `feed-manager/tests/CLAUDE.md`
  - `feed-manager/clients/CLAUDE.md`

## Scope and Structure

- Shared, reusable code lives in project-level source folders under `feed-manager/`.
- Client-specific code/config/output lives under `feed-manager/clients/<client-name>/`.
- Initial client path: `feed-manager/clients/rooflights/`.
- Each new client gets its own subfolder under `feed-manager/clients/`.

## Initial V1 Scope

- Pull Shopify variant data and persist source/enriched feed state in MySQL.
- Apply field-level policy routing:
  - auto-pass-through fields
  - approval-required fields
- Support product/batch approvals with optional field-level review.
- Export Google Merchant-compatible XML from persisted enriched state.
- Record sync runs and audit log events.
- Support scheduled sync execution via cron/script-level interval controls.

## Tech Stack

- PHP (application/runtime)
- MySQL (required for persistence, approvals, and audit trails)

## Shopify API Constraints

- The GraphQL query in `ShopifyVariantFeedSource` fetches 7 variant-level metafield aliases and `selectedOptions` per variant. This makes each page expensive.
- **Page size must stay at 50 or below.** At 100, Shopify returns 503 errors (query cost exceeds limits). The default is set in `fetchAllVariants()`.
- CLI sync should be run with `-d memory_limit=512M` — the expanded variant payloads exceed the default 128M limit.

## Authentication and Authorization

- Authentication (login/session/password flows) is provided by `delight-im/auth` and backed by the `users` and related auth tables.
- Application authorization is defined by `fm_user_roles` (`admin`, `reviewer`, `viewer`) and should be treated as the source of truth for Feed Manager permissions.
- `users.roles_mask` exists for the auth library but is not the primary permission model for Feed Manager screens/workflows.

## Project Paths

- Project root: `feed-manager/`
- Client root: `feed-manager/clients/`
- First client: `feed-manager/clients/rooflights/`

## Feed Enrichment Architecture

The data flow is: Shopify → `VariantToGoogleFeedItemMapper::map()` → `source_feed_json` + `enriched_feed_json` (DB) → `PersistedFeedExportService` → XML.

### Where changes go

- **Client config** (`clients/<client>/config.php`): All client-specific rules live under the `feed.mapping` key. This includes:
  - `variant_metafields` / `product_metafields` — which Shopify metafields to fetch (drives the GraphQL query dynamically via `ShopifyVariantFeedSource::buildQuery()`)
  - `title_strip_patterns` / `description_strip_patterns` — regex patterns applied during mapping
  - `attributes` — ordered source chains for each GMC attribute (`variant_metafield` → `selected_option` fallback)
  - `product_type_override_attribute` — which resolved attribute overrides Shopify's `productType`
  - `mpn_metafield_key` / `seo_hidden_metafield_key` — metafield keys for MPN and SEO hidden
  - `normalize_size` — whether to apply `Nmm x Nmm` spacing normalisation
- **Mapper** (`VariantToGoogleFeedItemMapper`): Generic field mapping engine. Reads the `mappingConfig` array — no hardcoded client-specific option names, metafield keys, or strip patterns. New clients need only a `config.php`.
- **GPC config** (`feed.google_product_category`): Separate from mapping config. Maps resolved `product_type` → GPC ID via `by_product_type` lookup with a `default` fallback.
- **No new DB tables** for enrichment. The `enriched_feed_json` column is a flexible JSON blob — new fields are added to it naturally. The field policy system (`FeedFieldPolicy`) controls which changes auto-apply vs need human approval.

### Metafield alias convention

GraphQL metafield aliases use the `meta_{configKey}` prefix (e.g. `meta_colour`, `meta_seo_hidden`). The mapper reads variant data via `$variant['meta_colour']['value']`. This is driven by the config — no aliases are hardcoded in the GraphQL query.

### Re-seeding after mapper changes

When mapper logic changes significantly (new fields, sanitisation improvements), the cleanest approach pre-launch is a **one-time re-seed**: wipe variant state for the client, then re-sync. This avoids 4,000+ pending approval items for fields like `description` that default to `approval_required`.

## Automation Run Paths (Authoritative)

All run artifacts for this project must stay inside project scope:

- Specs: `feed-manager/_assets/automation/runs/specs/`
- Plans: `feed-manager/_assets/automation/runs/plans/`

Never write `feed-manager` specs/plans to repo-root `_assets/automation/runs/` or to another project's runs directory.

## Spec and Plan Conventions

- Spec location pattern:
  `feed-manager/_assets/automation/runs/specs/<lifecycle>/<YYYYMMDD-HHMMSS>-<slug>/spec.md`
- Plan location pattern:
  `feed-manager/_assets/automation/runs/plans/YYYY-MM-DD-<feature-name>/plan.md`
- Keep lifecycle folders project-scoped (`draft`, `approved`, `in-progress`, `completed`).
