34 "variation-any-attribute-unresolved": "Route the product's selection attributes with wooSelectionAttributes (rp-source-wordpress/lib/woo-selection-attributes.js) and routeSelections (rp-target-wix/lib/selection-route.js), as rp-import-codegen/SKILL.md describes: shared (\"Any\") attributes become mandatory modifiers and the product is written. Only a product the route refuses (SELECTION_ROUTE_CONTRADICTION) is held, with code selection-route-contradiction and a mapping-gaps.json entry."
111 "https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/create-product: media.itemsInfo.items[].url sets media from an external URL; passing brand.name adds a new brand while creating the product (BRAND_CREATION_FAILED fails the create). DUPLICATE_SLUG_ERROR makes slug a unique reconciliation key; SKU is not assumed unique.",
112 "LIVE 2026-09-01 (Update Product, see patchStoresProductMediaVerified): an accepted URL item is queued (UNKNOWN_MEDIA_TYPE + uploadId) and may be dropped after the 200; only a read with fields=MEDIA_ITEMS_INFO establishes ingestion. Create timing is not separately observed.",
113 "Brand by existing name links the existing brand (stores/brand pitfalls); normalization and broad idempotency are not established.",
114 "bulkCreateProductsWithInventory is a different operation with no reviewed contract and cannot share this proof.",
115 "Create Product returns product.variantsInfo.variants[] with server-assigned ids. product-import.js matches each source variant expectation to exactly one returned variant by its source-fixed projection and stores { sourceKey, variantId } in the verified product receipt (write-verification.js verifyProduct re-checks each variantId against the read-back); those are the child identities."
223 "summary": "VERIFIED LIVE: a source product carrying an EMPTY slug is not rejected -- Wix derives one from the product name instead. That derived slug competes in the same namespace as every slug the import supplies, so it can collide with another product's decoded source slug or with a second product whose name slugifies the same way, and the losing write comes back as a duplicate-slug conflict. Empty source slugs are not rare. Resolve them BEFORE the write -- mint an explicit, catalog-unique slug per affected product and record it against the source id -- so which product owns the name is decided by the mapping, not by write order. A conflict resolved by appending a numeric suffix, whether the platform did it or a repair pass did, is a DEVIATION from the source value and not a preserved slug: report it as an exception on the run's faithfulness ledger rather than counting it as correct."
224 },
225 {
226 "code": "media-source-resolution",
227 "severity": "blocker",
228 "summary": "media.itemsInfo.items[] takes ONE url per image and the destination copies exactly that file, generating every derivative from it with no way to raise quality afterwards -- so the url sent is that image's permanent ceiling. Never map a single named source image field: a source image reference usually carries several urls for the same picture at different sizes, and the field holding the original differs between routes of the SAME source. Resolve to the original, maximum-resolution candidate before building the item. Also: altText (1-1000 chars) and displayName (max 80, url-set items only) are writable per item and must not be flattened away; thumbnail is destination-generated output, never an input."
229 },
230 {
231 "code": "wordpress-percent-encoded-slug",
232 "severity": "blocker",
233 "summary": "product.slug is user-visible URL, not internal state -- and Wix slug sanitizing silently turns literal '%' characters into dashes rather than erroring. A WooCommerce source stores a non-Latin (e.g. Hebrew) post_name percent-encoded and its REST API (wc/v3) returns it undecoded; passing that raw value straight through as product.slug produces a garbled hex-with-dashes URL (e.g. '.../product-page/d7-90-d7-99-d7-a0-d7-98-d7-a0-d7-a1-d7-95') instead of the real slug. Nothing fails -- no 400 and no warning -- so only a check that compares the stored slug against the DECODED source value catches it; comparing against the raw source field or against what the writer sent calls it correct. Decode with decodeURIComponent BEFORE the slug coercion runs (decoding afterwards is too late: sanitizing has already consumed the '%'), guarded in a try/catch so a slug that was never percent-encoded passes through unchanged and a malformed escape falls back to the raw value instead of throwing. The decoded Unicode slug is then writable as-is: the standard coercion (`coerce: 'slug'`, i.e. toWixSlug) lowercases and keeps letters, combining marks and digits in any script, so do not replace a non-Latin slug with a transliterated or invented one. Confirm on the FIRST product that the decoded slug read back intact; missing it costs a repair pass over every product already created. Confirmed live on non-Latin WooCommerce catalogs. See rp-source-wordpress/SKILL.md and rp-import-codegen/SKILL.md's 'Never hand-write slug sanitizing' section."
234 },
235 {
236 "code": "seo-data-only-for-proven-overrides",
237 "severity": "blocker",
238 "summary": "Write product.seoData only for per-field SEO overrides the source proves (see rp-source-wordpress wordpress-seo product-seo-meta). Rendered Yoast output would turn an inherited pattern into a hard per-product override. [UNVERIFIED as of 2026-09-29] Create-time persistence of seoData on the create-with-inventory call has not been live-verified: the throwaway-site canary (create one product with inventory plus title and description seoData, read it back, observe both tags resolve TAG_SOURCE_ITEM, delete it) is still pending. Until it is recorded here, product-seo.js reads every product back after create and, if seoData was dropped, writes it with a revision-protected PATCH and verifies that. A PATCH of seoData with the saved revision is live-observed (2026-08-27, resolved TAG_SOURCE_ITEM). tags[] is replaced whole: send current non-import-owned tags back unchanged."
243 "summary": "An attribute that cannot change price or stock (WooCommerce: \"Any\" in every variation) becomes a mandatory connected modifier on one default variant, never an enumerated variant set; attributes that move price or stock become variants within 6 options / 1,000 variants. A tracked stock pool is never copied onto several variants. Route with lib/selection-route.js routeSelections from canonical selectionAttributes (source-neutral); write customizations first (planSelectionCustomizations + createStoresCustomization) and attach with connectedModifierFromCustomization. Modifiers carry no price of their own here (defaultAddedPrice is docs-only and not used). Live read-back of TEXT_CHOICES modifiers on a product is still unverified (needs a live canary)."
244 }
245 ],
246 "mappingGuidance": [
247 "Create categories, brands, tags, and media references before products when the approved mapping uses them; assign brands by crosswalked brand.id, never by name (see stores/brand). Tag references are per product: resolve each used tag ID through the dependency gate. An excluded tag population uses its reviewed census disposition and exact dependent-field omission rule; absence of a tag writer never authorizes silent loss. Keep omitted tag behavior visible for the customer to fix or dismiss. Media ids are per product too: resolve each media.itemsInfo.items[].id through the dependency gate (resolveReference), so a product never waits for the whole media task or for any image. Place each resolved id at the next free index; an image that is not ready, held or failed resolves to nothing (dependencyFailureActions omit) and the product is written without it, recorded in execution/dependency-readiness.json omittedReferences. The media patch pass (omittedReady, then patchStoresProductMediaVerified, then recordOmissionsResolved) adds each image once it lands. Media sent as items[].url need no media task at all.",
248 "Preserve recurring offers natively via product.subscriptionDetails.subscriptions[] when the source exposes clear cadence data; live create coverage was verified on July 26, 2026, while patch/update coverage remains unverified.",
249 "Plan the slug for the WHOLE catalog before writing the first product, not per record. Two source shapes decide the target slug and neither is visible one record at a time: a percent-encoded non-Latin slug must be tolerantly percent-decoded first and then kept as the decoded Unicode value (see wordpress-percent-encoded-slug), and an empty source slug must be filled in rather than left for the platform to derive (see empty-source-slug-derives-and-can-collide). Build the source-slug -> planned-target-slug map up front, assert it is unique across the catalog after coercion, and carry the ORIGINAL source slug beside the planned one so a later URL-preservation report can tell a faithful slug from a deviated one.",
250 "Product images: pass every candidate url the source record offers to the source adapter's image resolver and use its result, never a hardcoded field such as src. Carry altText and displayName through to the media item. When the resolver reports that only a downscaled crop existed, record those images in the run's faithfulness ledger rather than importing them silently.",
251 "Product SEO: build seoData with buildProduct(canonical, { seoPlan }) from product-seo.js planProductSeo; never map rendered source SEO into seoTitle/seoDescription for an evidence-contract source."
252 ],
253 "setupRequirements": [
254 "Wix Stores app",
255 "Product data extension schema when source-only product fields are preserved natively.",
256 "Product data extension schema (FQDN wix.stores.catalog.*.product, provision-extended-fields step) before any writer emits product extendedFields values."