@sadrmanesh/commerce-agent-skills 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/bin/commerce-agent-skills.mjs +368 -0
  5. package/package.json +52 -4
  6. package/rules/b2b-pricing.md +11 -0
  7. package/rules/catalog-import-and-migration.md +11 -0
  8. package/rules/checkout-ux-and-conversion.md +12 -0
  9. package/rules/conversion-event-tracking.md +11 -0
  10. package/rules/ecommerce-seo-structured-data.md +11 -0
  11. package/rules/fraud-and-risk-basics.md +11 -0
  12. package/rules/google-merchant-product-feeds.md +11 -0
  13. package/rules/headless-nextjs-storefront.md +10 -0
  14. package/rules/inventory-and-multi-warehouse.md +10 -0
  15. package/rules/iranian-payment-gateways.md +10 -0
  16. package/rules/marketplace-search-and-ranking.md +10 -0
  17. package/rules/mcp-store-tooling.md +11 -0
  18. package/rules/multi-vendor-payouts.md +11 -0
  19. package/rules/order-and-webhook-reliability.md +12 -0
  20. package/rules/payment-gateway-integration.md +12 -0
  21. package/rules/product-catalog-modeling.md +11 -0
  22. package/rules/product-description-guardrails.md +10 -0
  23. package/rules/returns-and-refunds.md +10 -0
  24. package/rules/rtl-and-persian-localization.md +11 -0
  25. package/rules/shopify-app-development.md +10 -0
  26. package/rules/storefront-accessibility.md +11 -0
  27. package/rules/storefront-performance.md +10 -0
  28. package/rules/stripe-payments.md +10 -0
  29. package/rules/subscription-billing.md +10 -0
  30. package/rules/tax-and-shipping.md +10 -0
  31. package/rules/vector-product-search.md +10 -0
  32. package/rules/woocommerce-plugin-development.md +10 -0
  33. package/rules/woocommerce-rest-api.md +10 -0
  34. package/rules/wordpress-plugin-security.md +10 -0
  35. package/scripts/lib/frontmatter.mjs +218 -0
  36. package/skills/b2b-pricing/SKILL.md +55 -0
  37. package/skills/b2b-pricing/evals/examples/net-terms.md +3 -0
  38. package/skills/b2b-pricing/evals/examples/tiered-wholesale.md +3 -0
  39. package/skills/b2b-pricing/evals/tasks.json +19 -0
  40. package/skills/b2b-pricing/references/price-lists.md +46 -0
  41. package/skills/b2b-pricing/scripts/resolve-price.mjs +93 -0
  42. package/skills/catalog-import-and-migration/SKILL.md +58 -0
  43. package/skills/catalog-import-and-migration/evals/examples/platform-migration-urls.md +3 -0
  44. package/skills/catalog-import-and-migration/evals/examples/rerunnable-import.md +3 -0
  45. package/skills/catalog-import-and-migration/evals/tasks.json +20 -0
  46. package/skills/catalog-import-and-migration/references/field-mapping.md +43 -0
  47. package/skills/catalog-import-and-migration/scripts/check-import-rows.mjs +75 -0
  48. package/skills/catalog-order.txt +29 -0
  49. package/skills/checkout-ux-and-conversion/SKILL.md +86 -0
  50. package/skills/checkout-ux-and-conversion/evals/examples/guest-total.md +3 -0
  51. package/skills/checkout-ux-and-conversion/evals/examples/single-submit.md +3 -0
  52. package/skills/checkout-ux-and-conversion/evals/tasks.json +30 -0
  53. package/skills/checkout-ux-and-conversion/references/checkout-review.md +41 -0
  54. package/skills/conversion-event-tracking/SKILL.md +64 -0
  55. package/skills/conversion-event-tracking/evals/examples/purchase-once.md +3 -0
  56. package/skills/conversion-event-tracking/evals/examples/variant-item-id.md +3 -0
  57. package/skills/conversion-event-tracking/evals/tasks.json +30 -0
  58. package/skills/conversion-event-tracking/references/events.md +36 -0
  59. package/skills/conversion-event-tracking/scripts/check-event.mjs +79 -0
  60. package/skills/ecommerce-seo-structured-data/SKILL.md +84 -0
  61. package/skills/ecommerce-seo-structured-data/assets/example-product.jsonld +15 -0
  62. package/skills/ecommerce-seo-structured-data/evals/examples/extend-woocommerce-schema.md +8 -0
  63. package/skills/ecommerce-seo-structured-data/evals/examples/variant-offer.md +7 -0
  64. package/skills/ecommerce-seo-structured-data/evals/tasks.json +29 -0
  65. package/skills/ecommerce-seo-structured-data/references/product-jsonld.md +83 -0
  66. package/skills/ecommerce-seo-structured-data/scripts/check-product-jsonld.mjs +211 -0
  67. package/skills/fraud-and-risk-basics/SKILL.md +66 -0
  68. package/skills/fraud-and-risk-basics/evals/examples/review-queue.md +3 -0
  69. package/skills/fraud-and-risk-basics/evals/examples/wait-for-3ds.md +3 -0
  70. package/skills/fraud-and-risk-basics/evals/tasks.json +30 -0
  71. package/skills/google-merchant-product-feeds/SKILL.md +70 -0
  72. package/skills/google-merchant-product-feeds/assets/example-item.json +11 -0
  73. package/skills/google-merchant-product-feeds/evals/examples/generated-title.md +3 -0
  74. package/skills/google-merchant-product-feeds/evals/examples/stable-variant-id.md +3 -0
  75. package/skills/google-merchant-product-feeds/evals/tasks.json +30 -0
  76. package/skills/google-merchant-product-feeds/references/attributes.md +39 -0
  77. package/skills/google-merchant-product-feeds/scripts/check-feed-item.mjs +92 -0
  78. package/skills/headless-nextjs-storefront/SKILL.md +69 -0
  79. package/skills/headless-nextjs-storefront/evals/examples/revalidate-stock.md +7 -0
  80. package/skills/headless-nextjs-storefront/evals/examples/server-price.md +3 -0
  81. package/skills/headless-nextjs-storefront/evals/tasks.json +30 -0
  82. package/skills/headless-nextjs-storefront/references/cache-and-cart.md +39 -0
  83. package/skills/inventory-and-multi-warehouse/SKILL.md +61 -0
  84. package/skills/inventory-and-multi-warehouse/evals/examples/reserve-last-unit.md +3 -0
  85. package/skills/inventory-and-multi-warehouse/evals/examples/split-order.md +3 -0
  86. package/skills/inventory-and-multi-warehouse/evals/tasks.json +20 -0
  87. package/skills/inventory-and-multi-warehouse/references/platforms.md +40 -0
  88. package/skills/inventory-and-multi-warehouse/scripts/allocate-stock.mjs +72 -0
  89. package/skills/iranian-payment-gateways/SKILL.md +64 -0
  90. package/skills/iranian-payment-gateways/evals/examples/toman-store.md +3 -0
  91. package/skills/iranian-payment-gateways/evals/examples/zarinpal-callback.md +3 -0
  92. package/skills/iranian-payment-gateways/evals/tasks.json +20 -0
  93. package/skills/iranian-payment-gateways/references/zarinpal-v4.md +44 -0
  94. package/skills/iranian-payment-gateways/scripts/check-verify.mjs +62 -0
  95. package/skills/marketplace-search-and-ranking/SKILL.md +93 -0
  96. package/skills/marketplace-search-and-ranking/evals/examples/index-variant.md +3 -0
  97. package/skills/marketplace-search-and-ranking/evals/examples/normalize-query.md +7 -0
  98. package/skills/marketplace-search-and-ranking/evals/tasks.json +29 -0
  99. package/skills/marketplace-search-and-ranking/references/document-and-ranking.md +56 -0
  100. package/skills/marketplace-search-and-ranking/scripts/normalize-search-text.mjs +40 -0
  101. package/skills/mcp-store-tooling/SKILL.md +49 -0
  102. package/skills/mcp-store-tooling/evals/examples/refund-tool-design.md +3 -0
  103. package/skills/mcp-store-tooling/evals/examples/remote-server-auth.md +3 -0
  104. package/skills/mcp-store-tooling/evals/tasks.json +20 -0
  105. package/skills/mcp-store-tooling/references/mcp-notes.md +67 -0
  106. package/skills/mcp-store-tooling/scripts/check-tool-definitions.mjs +69 -0
  107. package/skills/multi-vendor-payouts/SKILL.md +71 -0
  108. package/skills/multi-vendor-payouts/evals/examples/retry-payout.md +3 -0
  109. package/skills/multi-vendor-payouts/evals/examples/split-paid-order.md +3 -0
  110. package/skills/multi-vendor-payouts/evals/tasks.json +30 -0
  111. package/skills/multi-vendor-payouts/references/ledger.md +35 -0
  112. package/skills/order-and-webhook-reliability/SKILL.md +99 -0
  113. package/skills/order-and-webhook-reliability/evals/examples/dedupe-resource-version.md +3 -0
  114. package/skills/order-and-webhook-reliability/evals/examples/verify-raw-body.md +3 -0
  115. package/skills/order-and-webhook-reliability/evals/tasks.json +29 -0
  116. package/skills/order-and-webhook-reliability/references/woocommerce-webhooks.md +58 -0
  117. package/skills/order-and-webhook-reliability/scripts/verify-hmac-signature.mjs +78 -0
  118. package/skills/payment-gateway-integration/SKILL.md +90 -0
  119. package/skills/payment-gateway-integration/evals/examples/confirmed-capture.md +9 -0
  120. package/skills/payment-gateway-integration/evals/examples/replayed-webhook.md +3 -0
  121. package/skills/payment-gateway-integration/evals/tasks.json +30 -0
  122. package/skills/payment-gateway-integration/references/woocommerce-gateway.md +67 -0
  123. package/skills/product-catalog-modeling/SKILL.md +91 -0
  124. package/skills/product-catalog-modeling/assets/example-catalog.json +45 -0
  125. package/skills/product-catalog-modeling/evals/examples/create-variation.md +10 -0
  126. package/skills/product-catalog-modeling/evals/examples/sync-parent.md +7 -0
  127. package/skills/product-catalog-modeling/evals/tasks.json +29 -0
  128. package/skills/product-catalog-modeling/references/variation-model.md +71 -0
  129. package/skills/product-catalog-modeling/scripts/validate-catalog.mjs +234 -0
  130. package/skills/product-description-guardrails/SKILL.md +84 -0
  131. package/skills/product-description-guardrails/evals/examples/facts-only.md +3 -0
  132. package/skills/product-description-guardrails/evals/examples/label-generated.md +3 -0
  133. package/skills/product-description-guardrails/evals/tasks.json +30 -0
  134. package/skills/product-description-guardrails/references/claims.md +24 -0
  135. package/skills/product-description-guardrails/scripts/check-description.mjs +61 -0
  136. package/skills/returns-and-refunds/SKILL.md +55 -0
  137. package/skills/returns-and-refunds/evals/examples/refund-after-inspection.md +3 -0
  138. package/skills/returns-and-refunds/evals/examples/woocommerce-partial-refund.md +3 -0
  139. package/skills/returns-and-refunds/evals/tasks.json +20 -0
  140. package/skills/returns-and-refunds/references/rma-states.md +37 -0
  141. package/skills/rtl-and-persian-localization/SKILL.md +81 -0
  142. package/skills/rtl-and-persian-localization/evals/examples/document-direction.md +7 -0
  143. package/skills/rtl-and-persian-localization/evals/examples/rial-display.md +7 -0
  144. package/skills/rtl-and-persian-localization/evals/tasks.json +30 -0
  145. package/skills/rtl-and-persian-localization/references/persian-text.md +51 -0
  146. package/skills/rtl-and-persian-localization/scripts/normalize-persian.mjs +62 -0
  147. package/skills/shopify-app-development/SKILL.md +73 -0
  148. package/skills/shopify-app-development/evals/examples/dedupe-delivery.md +3 -0
  149. package/skills/shopify-app-development/evals/examples/verify-webhook.md +3 -0
  150. package/skills/shopify-app-development/evals/tasks.json +29 -0
  151. package/skills/shopify-app-development/references/auth-and-webhooks.md +44 -0
  152. package/skills/storefront-accessibility/SKILL.md +59 -0
  153. package/skills/storefront-accessibility/evals/examples/add-to-cart-announcement.md +3 -0
  154. package/skills/storefront-accessibility/evals/examples/checkout-errors.md +3 -0
  155. package/skills/storefront-accessibility/evals/tasks.json +20 -0
  156. package/skills/storefront-accessibility/references/wcag-checklist.md +41 -0
  157. package/skills/storefront-performance/SKILL.md +59 -0
  158. package/skills/storefront-performance/evals/examples/cache-without-leaking-carts.md +3 -0
  159. package/skills/storefront-performance/evals/examples/slow-product-lcp.md +3 -0
  160. package/skills/storefront-performance/evals/tasks.json +20 -0
  161. package/skills/storefront-performance/references/web-vitals.md +55 -0
  162. package/skills/storefront-performance/scripts/check-web-vitals.mjs +49 -0
  163. package/skills/stripe-payments/SKILL.md +72 -0
  164. package/skills/stripe-payments/evals/examples/checkout-session-delayed.md +3 -0
  165. package/skills/stripe-payments/evals/examples/mark-paid-from-webhook.md +5 -0
  166. package/skills/stripe-payments/evals/examples/partial-refund.md +3 -0
  167. package/skills/stripe-payments/evals/tasks.json +27 -0
  168. package/skills/stripe-payments/references/stripe-objects.md +54 -0
  169. package/skills/stripe-payments/scripts/verify-stripe-signature.mjs +62 -0
  170. package/skills/subscription-billing/SKILL.md +76 -0
  171. package/skills/subscription-billing/evals/examples/cancel-at-period-end.md +3 -0
  172. package/skills/subscription-billing/evals/examples/one-renewal.md +3 -0
  173. package/skills/subscription-billing/evals/tasks.json +30 -0
  174. package/skills/subscription-billing/references/states.md +27 -0
  175. package/skills/tax-and-shipping/SKILL.md +61 -0
  176. package/skills/tax-and-shipping/evals/examples/store-tax-lines.md +3 -0
  177. package/skills/tax-and-shipping/evals/examples/unknown-zone.md +3 -0
  178. package/skills/tax-and-shipping/evals/tasks.json +30 -0
  179. package/skills/tax-and-shipping/references/woocommerce.md +22 -0
  180. package/skills/vector-product-search/SKILL.md +66 -0
  181. package/skills/vector-product-search/evals/examples/reembed-on-title.md +3 -0
  182. package/skills/vector-product-search/evals/examples/sku-before-vector.md +3 -0
  183. package/skills/vector-product-search/evals/tasks.json +30 -0
  184. package/skills/vector-product-search/references/index.md +40 -0
  185. package/skills/woocommerce-plugin-development/SKILL.md +84 -0
  186. package/skills/woocommerce-plugin-development/assets/main-plugin-file.php +28 -0
  187. package/skills/woocommerce-plugin-development/evals/examples/declare-hpos.md +9 -0
  188. package/skills/woocommerce-plugin-development/evals/examples/save-order-meta.md +9 -0
  189. package/skills/woocommerce-plugin-development/evals/tasks.json +32 -0
  190. package/skills/woocommerce-plugin-development/references/hooks.md +46 -0
  191. package/skills/woocommerce-plugin-development/references/hpos.md +84 -0
  192. package/skills/woocommerce-rest-api/SKILL.md +90 -0
  193. package/skills/woocommerce-rest-api/evals/examples/https-basic-auth.md +8 -0
  194. package/skills/woocommerce-rest-api/evals/examples/idempotent-order-create.md +7 -0
  195. package/skills/woocommerce-rest-api/evals/tasks.json +30 -0
  196. package/skills/woocommerce-rest-api/references/endpoints.md +66 -0
  197. package/skills/wordpress-plugin-security/SKILL.md +121 -0
  198. package/skills/wordpress-plugin-security/evals/examples/admin-form-nonce.md +9 -0
  199. package/skills/wordpress-plugin-security/evals/examples/private-rest-route.md +9 -0
  200. package/skills/wordpress-plugin-security/evals/tasks.json +31 -0
  201. package/skills/wordpress-plugin-security/references/review-checklist.md +31 -0
@@ -0,0 +1,67 @@
1
+ # WooCommerce payment gateway
2
+
3
+ Extend `WC_Payment_Gateway` and register it on `woocommerce_payment_gateways`.
4
+
5
+ ```php
6
+ class Example_Gateway extends WC_Payment_Gateway {
7
+ public function __construct() {
8
+ $this->id = 'example';
9
+ $this->method_title = __( 'Example', 'example-gateway' );
10
+ $this->method_description = __( 'Pay with Example.', 'example-gateway' );
11
+ $this->has_fields = false;
12
+ $this->supports = array( 'products' );
13
+ $this->init_form_fields();
14
+ $this->init_settings();
15
+ add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
16
+ }
17
+
18
+ public function process_payment( $order_id ) {
19
+ $order = wc_get_order( $order_id );
20
+ if ( ! $order ) {
21
+ wc_add_notice( __( 'Order not found.', 'example-gateway' ), 'error' );
22
+ return array( 'result' => 'failure' );
23
+ }
24
+
25
+ // Create or reuse the provider payment with an idempotency key.
26
+ // Do not mark the order paid here unless the provider response is a confirmed capture.
27
+ // A hosted payment page uses the provider URL. An offline method uses get_return_url().
28
+ $redirect_url = $this->get_return_url( $order );
29
+
30
+ return array(
31
+ 'result' => 'success',
32
+ 'redirect' => $redirect_url,
33
+ );
34
+ }
35
+
36
+ public function process_refund( $order_id, $amount = null, $reason = '' ) {
37
+ // Add 'refunds' to $supports only after this calls the provider and returns true on acceptance.
38
+ // On decline: return new WP_Error( 'example_refund_failed', __( 'The provider declined the refund.', 'example-gateway' ) );
39
+ return new WP_Error( 'example_refund_failed', __( 'Refunds are not implemented yet.', 'example-gateway' ) );
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Paid, held, and failed
45
+
46
+ - Confirmed capture: `$order->payment_complete( $transaction_id )`. That stores the transaction id, chooses `processing` or `completed` through `woocommerce_payment_complete_order_status`, and fires `woocommerce_payment_complete`.
47
+ - Awaiting a bank transfer or webhook: `$order->update_status( 'on-hold', $note )`. Do not call `payment_complete()` for money you do not have.
48
+ - Decline: `$order->update_status( 'failed', $note )` and `wc_add_notice()`.
49
+ - Do not call `update_status( 'processing' )` for a successful capture. That skips the payment-complete path.
50
+
51
+ `payment_complete()` is not a substitute for an idempotency check. Guard it with the stored transaction id or `$order->is_paid()` so a replay does not treat the order as newly paid. `is_paid()` is true for statuses in `woocommerce_order_is_paid_statuses`, which defaults to `processing` and `completed`.
52
+
53
+ Empty the cart with `WC()->cart->empty_cart()` only after the shop accepts the attempt (redirect created, or payment confirmed), matching the policy in `SKILL.md`. Use `$this->get_return_url( $order )` when you return the shopper to the site. Do not call `wp_redirect()` inside `process_payment()`.
54
+
55
+ ## Blocks checkout
56
+
57
+ A classic gateway is not automatically offered in the Checkout block. Register a payment method integration that extends `Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType` on `woocommerce_blocks_payment_method_type_registration`, and declare `cart_checkout_blocks` compatibility from the main plugin file. Until that exists, say so in the plugin and test classic checkout separately.
58
+
59
+ ## Webhooks
60
+
61
+ WooCommerce API callbacks under `/?wc-api=example` are not cookie-authenticated. Verify the provider signature there. The same rule applies to a custom REST route. Nonces do not apply.
62
+
63
+ Store the transaction id with `payment_complete()`. Do not call `add_post_meta( $order_id, '_transaction_id', ... )`. That misses HPOS.
64
+
65
+ ## Refunds
66
+
67
+ `process_refund()` must talk to the provider and return `true` only after the provider accepts the refund. Returning `true` without a provider call marks the WooCommerce refund as done while the money has not moved. Omit `refunds` from `$supports` until the method is real.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: product-catalog-modeling
3
+ description: "Model a sellable catalog of simple products, variants, attributes, SKUs, and stock, and check the model before importing it. Use when designing product data, variation matrices, inventory rules, or catalog imports."
4
+ license: MIT
5
+ compatibility: "The catalog checker requires Node.js 20 or newer. No network access."
6
+ metadata:
7
+ author: Naser Sadrmanesh
8
+ version: "0.3.0"
9
+ ---
10
+
11
+ # Product catalog modeling
12
+
13
+ Use this skill before writing import code or a product schema. The document checked by [`scripts/validate-catalog.mjs`](scripts/validate-catalog.mjs) is a planning model. It is not a WooCommerce export. Map it to WooCommerce with [the variation notes](references/variation-model.md). An example that passes the checker is [`assets/example-catalog.json`](assets/example-catalog.json).
14
+
15
+ ## Procedure
16
+
17
+ 1. Decide the purchasable unit. A simple product is purchased as itself. A variable product is purchased as one variant. A grouped product is a collection of other products. An external product is sold somewhere else.
18
+ 2. Give every purchasable unit one SKU. SKUs are unique across the catalog, including variants.
19
+ 3. Model variation attributes as a fixed set of options. Every variant picks exactly one option for every variation attribute. Two variants cannot share a combination.
20
+ 4. Put the price and the stock record on the purchasable unit. A variable parent does not have its own price or SKU in this model.
21
+ 5. Run `node scripts/validate-catalog.mjs --file catalog.json` from this skill directory and fix every reported error before importing.
22
+
23
+ ## Document shape
24
+
25
+ ```json
26
+ {
27
+ "products": [
28
+ {
29
+ "id": "shirt",
30
+ "type": "variable",
31
+ "sku": null,
32
+ "name": "Cotton shirt",
33
+ "attributes": [
34
+ { "code": "color", "name": "Color", "options": ["blue", "red"], "variation": true }
35
+ ],
36
+ "variants": [
37
+ {
38
+ "id": "shirt-blue",
39
+ "sku": "SHIRT-BLUE",
40
+ "attributes": { "color": "blue" },
41
+ "price": "19.00",
42
+ "currency": "USD",
43
+ "stock": { "managed": true, "quantity": 4, "backorders": "no", "status": "instock" }
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ Rules the checker enforces:
52
+
53
+ - `type` is `simple`, `variable`, `external`, or `grouped`.
54
+ - `price` is a major-unit string with two decimal places, such as `"19.00"`. `currency` is an ISO 4217 code. Map to the store's decimal setting at the boundary. Do not send this string through a binary float.
55
+ - Simple and external products require `sku`, `price`, and `currency`. External products also require `productUrl`.
56
+ - Variable products require `sku: null`, no parent `price`, at least one attribute with `"variation": true`, and at least one variant. Each variant sets exactly those attributes.
57
+ - Grouped products list `children` ids and do not carry a price.
58
+ - `stock.managed: true` requires an integer `quantity` of zero or more. `stock.managed: false` requires `quantity: null`. `backorders` is `no`, `notify`, or `yes`.
59
+ - Optional `stock.status` is `instock`, `outofstock`, or `onbackorder`. When quantity is managed, status should agree with quantity and backorders. The checker does not infer status for you.
60
+
61
+ ## Inventory rules
62
+
63
+ - Managed stock is a quantity you decrement on paid orders and restore on cancel or refund, using the platform's stock API once.
64
+ - Unmanaged stock is a status you set by hand (`instock` or `outofstock`). Do not also store a quantity.
65
+ - Backorders mean you accept orders at zero. `notify` shows the shopper that the item is on backorder. `yes` allows it quietly. `no` stops the sale at zero.
66
+ - A variable parent's availability is the rollup of its variants. Do not maintain a second parent quantity that can disagree with the variants.
67
+ - Reservation during checkout, when the platform holds stock for unpaid orders, is part of available-to-sell. Do not subtract that hold a second time in your importer.
68
+
69
+ ## Attributes
70
+
71
+ - A variation attribute changes the purchasable unit (size, color). It belongs in `attributes` with `variation: true` and on each variant.
72
+ - A descriptive attribute (material, care) is not a variant axis. Store it with `variation: false` or as content. Do not create a variant per descriptive value.
73
+ - Keep option values stable. Translate the display label, not the option code. The search skill depends on stable facet ids.
74
+
75
+ ## Gotchas
76
+
77
+ - A parent price range is derived from variants. Writing a parent price and different variant prices hides which amount will be charged.
78
+ - Duplicate SKUs across a simple product and a variant make stock and fulfillment ambiguous.
79
+ - Cartesian explosions are real. Three attributes with ten options are a thousand variants. Split the product or drop an axis before generating them.
80
+ - Currency is a property of the price, not of the shopper's browser. A multi-currency display converts from the stored currency. It does not create a second SKU.
81
+
82
+ ## Verification
83
+
84
+ - The checker exits 0 on the catalog file.
85
+ - Every SKU resolves to one purchasable unit.
86
+ - A variant combination that is not for sale is absent, not stored with a blank price.
87
+ - After import, the storefront price and stock for one variant match that variant's record, not the parent.
88
+
89
+ ## Evaluation
90
+
91
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,45 @@
1
+ {
2
+ "products": [
3
+ {
4
+ "id": "shirt",
5
+ "type": "variable",
6
+ "sku": null,
7
+ "name": "Cotton shirt",
8
+ "attributes": [
9
+ {
10
+ "code": "color",
11
+ "name": "Color",
12
+ "options": ["blue", "red"],
13
+ "variation": true
14
+ }
15
+ ],
16
+ "variants": [
17
+ {
18
+ "id": "shirt-blue",
19
+ "sku": "SHIRT-BLUE",
20
+ "attributes": { "color": "blue" },
21
+ "price": "19.00",
22
+ "currency": "USD",
23
+ "stock": { "managed": true, "quantity": 5, "backorders": "no", "status": "instock" }
24
+ },
25
+ {
26
+ "id": "shirt-red",
27
+ "sku": "SHIRT-RED",
28
+ "attributes": { "color": "red" },
29
+ "price": "21.00",
30
+ "currency": "USD",
31
+ "stock": { "managed": true, "quantity": 0, "backorders": "no", "status": "outofstock" }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "id": "gift-card",
37
+ "type": "simple",
38
+ "sku": "GIFT-25",
39
+ "name": "Gift card",
40
+ "price": "25.00",
41
+ "currency": "USD",
42
+ "stock": { "managed": false, "quantity": null, "backorders": "no", "status": "instock" }
43
+ }
44
+ ]
45
+ }
@@ -0,0 +1,10 @@
1
+ Create the purchasable variation and give it the SKU.
2
+
3
+ ```php
4
+ $variation = new WC_Product_Variation();
5
+ $variation->set_parent_id( $product->get_id() );
6
+ $variation->set_attributes( array( 'pa_color' => 'blue' ) );
7
+ $variation->set_sku( 'SHIRT-BLUE' );
8
+ $variation->set_regular_price( '19.00' );
9
+ $variation->save();
10
+ ```
@@ -0,0 +1,7 @@
1
+ Sync the variable parent after the variations exist.
2
+
3
+ ```php
4
+ WC_Product_Variable::sync( $product->get_id() );
5
+ ```
6
+
7
+ The parent record does not carry the variation SKU or the charged price.
@@ -0,0 +1,29 @@
1
+ {
2
+ "tasks": [
3
+ {
4
+ "id": "create-variation",
5
+ "task": "Create a WooCommerce variation with its own SKU and price under a variable parent that has no SKU.",
6
+ "expects": {
7
+ "must_include": [
8
+ "WC_Product_Variation",
9
+ "set_sku"
10
+ ],
11
+ "must_not_include": [
12
+ "update_post_meta"
13
+ ]
14
+ }
15
+ },
16
+ {
17
+ "id": "sync-parent",
18
+ "task": "After writing variations, refresh the parent price range and stock status from the children.",
19
+ "expects": {
20
+ "must_include": [
21
+ "WC_Product_Variable::sync"
22
+ ],
23
+ "must_not_include": [
24
+ "set_sku( 'PARENT' )"
25
+ ]
26
+ }
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,71 @@
1
+ # Mapping the catalog model to WooCommerce
2
+
3
+ WooCommerce product types match this model: `simple`, `variable`, `external`, and `grouped`.
4
+
5
+ ## Global attributes
6
+
7
+ Create a global attribute once, then attach terms. The taxonomy name is `pa_` plus the attribute slug. Variation attribute values stored on a `WC_Product_Variation` are term slugs, not translated labels.
8
+
9
+ ```php
10
+ $product = new WC_Product_Variable();
11
+ $product->set_name( 'Cotton shirt' );
12
+ $product->set_status( 'publish' );
13
+
14
+ $attribute = new WC_Product_Attribute();
15
+ $attribute->set_id( wc_attribute_taxonomy_id_by_name( 'pa_color' ) );
16
+ $attribute->set_name( 'pa_color' );
17
+ $attribute->set_options( array( $blue_term_id, $red_term_id ) );
18
+ $attribute->set_visible( true );
19
+ $attribute->set_variation( true );
20
+ $product->set_attributes( array( $attribute ) );
21
+ $product->save();
22
+
23
+ $variation = new WC_Product_Variation();
24
+ $variation->set_parent_id( $product->get_id() );
25
+ $variation->set_attributes( array( 'pa_color' => 'blue' ) );
26
+ $variation->set_sku( 'SHIRT-BLUE' );
27
+ $variation->set_regular_price( '19.00' );
28
+ $variation->set_manage_stock( true );
29
+ $variation->set_stock_quantity( 4 );
30
+ $variation->set_backorders( 'no' );
31
+ $variation->save();
32
+
33
+ WC_Product_Variable::sync( $product->get_id() );
34
+ ```
35
+
36
+ Call `WC_Product_Variable::sync()` after variant writes so the parent price range and stock status match the children. Skip it and the catalog can show the parent as out of stock while a variant has quantity.
37
+
38
+ Local attributes (not a `pa_` taxonomy) are valid for a single product. Prefer global attributes when the same facet must be shared across products and search.
39
+
40
+ ## Simple products
41
+
42
+ ```php
43
+ $product = new WC_Product_Simple();
44
+ $product->set_name( 'Gift card' );
45
+ $product->set_sku( 'GIFT-25' );
46
+ $product->set_regular_price( '25.00' );
47
+ $product->set_manage_stock( false );
48
+ $product->set_stock_status( 'instock' );
49
+ $product->save();
50
+ ```
51
+
52
+ `set_regular_price()` takes a decimal string. Sale price is `set_sale_price()`. The active price follows WooCommerce sale schedule rules. Do not overwrite `_price` meta by hand.
53
+
54
+ ## SKU lookup
55
+
56
+ `wc_get_product_id_by_sku()` resolves both parent products and variations. Uniqueness is enforced unless `wc_product_has_unique_sku` is filtered. Do not filter it to allow duplicates unless the shop has a separate identity for the sellable unit.
57
+
58
+ ## Stock
59
+
60
+ - `set_manage_stock( true )` plus `set_stock_quantity()` is managed stock.
61
+ - `set_stock_status()` is the manual status used when stock is not managed, and the cached status when it is.
62
+ - Reducing stock for an order is `wc_reduce_stock_levels()` / `wc_maybe_reduce_stock_levels()`, which no-ops when `_order_stock_reduced` is already set. Call it from payment completion only if core has not already done so.
63
+ - `woocommerce_hold_stock_minutes` (default 60) holds stock on unpaid pending orders. Importers and availability displays should use WooCommerce's availability helpers rather than subtracting `stock_quantity` a second time.
64
+
65
+ ## Grouped and external
66
+
67
+ Grouped products store child ids. The price is the child's price. External products store a product URL and an optional button label. They are not stock-managed inside the shop.
68
+
69
+ ## Imports
70
+
71
+ Resolve SKU to id before update. Create when absent. Wrap a large import in batches and call `WC_Product_Variable::sync()` per parent after its variants, not once for the whole catalog at the start. Delete a variant that left the model. Leaving it published sells a combination the model no longer allows.
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+
4
+ const TYPES = new Set(["simple", "variable", "external", "grouped"]);
5
+ const BACKORDERS = new Set(["no", "notify", "yes"]);
6
+ const STOCK_STATUS = new Set(["instock", "outofstock", "onbackorder"]);
7
+
8
+ export function validateCatalog(document) {
9
+ const errors = [];
10
+ if (!document || !Array.isArray(document.products)) {
11
+ return ["Catalog root must be an object with a products array."];
12
+ }
13
+
14
+ const productIds = new Set();
15
+ const skus = new Set();
16
+
17
+ for (const [index, product] of document.products.entries()) {
18
+ const label = `products[${index}]`;
19
+ if (!product || typeof product !== "object") {
20
+ errors.push(`${label} must be an object`);
21
+ continue;
22
+ }
23
+ if (!isId(product.id)) {
24
+ errors.push(`${label}.id must be a non-empty string`);
25
+ } else if (productIds.has(product.id)) {
26
+ errors.push(`${label}.id duplicates ${product.id}`);
27
+ } else {
28
+ productIds.add(product.id);
29
+ }
30
+ if (!TYPES.has(product.type)) {
31
+ errors.push(`${label}.type must be simple, variable, external, or grouped`);
32
+ continue;
33
+ }
34
+ if (typeof product.name !== "string" || product.name.trim() === "") {
35
+ errors.push(`${label}.name is required`);
36
+ }
37
+
38
+ if (product.type === "simple") {
39
+ requireSku(product.sku, skus, `${label}.sku`, errors);
40
+ requirePrice(product, label, errors);
41
+ checkStock(product.stock, `${label}.stock`, errors, false);
42
+ reject(product.variants, `${label}.variants`, "simple products have no variants", errors);
43
+ } else if (product.type === "external") {
44
+ requireSku(product.sku, skus, `${label}.sku`, errors);
45
+ requirePrice(product, label, errors);
46
+ if (typeof product.productUrl !== "string" || product.productUrl.trim() === "") {
47
+ errors.push(`${label}.productUrl is required for external products`);
48
+ }
49
+ reject(product.variants, `${label}.variants`, "external products have no variants", errors);
50
+ } else if (product.type === "grouped") {
51
+ if (!Array.isArray(product.children) || product.children.length === 0) {
52
+ errors.push(`${label}.children must list at least one product id`);
53
+ }
54
+ if (product.price !== undefined) {
55
+ errors.push(`${label}.price belongs on the child products, not the grouped parent`);
56
+ }
57
+ reject(product.variants, `${label}.variants`, "grouped products have no variants", errors);
58
+ } else {
59
+ checkVariable(product, label, skus, errors);
60
+ }
61
+ }
62
+
63
+ const known = new Set(document.products.filter((product) => product && isId(product.id)).map((product) => product.id));
64
+ for (const [index, product] of document.products.entries()) {
65
+ if (!product || product.type !== "grouped" || !Array.isArray(product.children)) {
66
+ continue;
67
+ }
68
+ for (const child of product.children) {
69
+ if (!known.has(child) || child === product.id) {
70
+ errors.push(`products[${index}].children references unknown id ${child}`);
71
+ }
72
+ }
73
+ }
74
+
75
+ return errors;
76
+ }
77
+
78
+ function checkVariable(product, label, skus, errors) {
79
+ if (product.sku !== null) {
80
+ errors.push(`${label}.sku must be null; put SKUs on variants`);
81
+ }
82
+ if (product.price !== undefined) {
83
+ errors.push(`${label}.price must live on each variant so the purchasable price is explicit`);
84
+ }
85
+ const attributes = Array.isArray(product.attributes) ? product.attributes : [];
86
+ const variationAttributes = attributes.filter((attribute) => attribute && attribute.variation === true);
87
+ if (variationAttributes.length === 0) {
88
+ errors.push(`${label} needs at least one attribute with variation: true`);
89
+ }
90
+ const optionsByCode = new Map();
91
+ const codes = new Set();
92
+ for (const [index, attribute] of attributes.entries()) {
93
+ const attributeLabel = `${label}.attributes[${index}]`;
94
+ if (!attribute || !isId(attribute.code)) {
95
+ errors.push(`${attributeLabel}.code is required`);
96
+ continue;
97
+ }
98
+ if (codes.has(attribute.code)) {
99
+ errors.push(`${attributeLabel}.code duplicates ${attribute.code}`);
100
+ }
101
+ codes.add(attribute.code);
102
+ if (!Array.isArray(attribute.options) || attribute.options.length === 0 || attribute.options.some((option) => typeof option !== "string" || option === "")) {
103
+ errors.push(`${attributeLabel}.options must be a non-empty list of strings`);
104
+ } else if (new Set(attribute.options).size !== attribute.options.length) {
105
+ errors.push(`${attributeLabel}.options contains a duplicate`);
106
+ }
107
+ optionsByCode.set(attribute.code, new Set(attribute.options ?? []));
108
+ }
109
+
110
+ if (!Array.isArray(product.variants) || product.variants.length === 0) {
111
+ errors.push(`${label}.variants must contain at least one variant`);
112
+ return;
113
+ }
114
+
115
+ const combinations = new Set();
116
+ const variantIds = new Set();
117
+ for (const [index, variant] of product.variants.entries()) {
118
+ const variantLabel = `${label}.variants[${index}]`;
119
+ if (!variant || typeof variant !== "object") {
120
+ errors.push(`${variantLabel} must be an object`);
121
+ continue;
122
+ }
123
+ if (!isId(variant.id)) {
124
+ errors.push(`${variantLabel}.id is required`);
125
+ } else if (variantIds.has(variant.id)) {
126
+ errors.push(`${variantLabel}.id duplicates ${variant.id}`);
127
+ } else {
128
+ variantIds.add(variant.id);
129
+ }
130
+ requireSku(variant.sku, skus, `${variantLabel}.sku`, errors);
131
+ requirePrice(variant, variantLabel, errors);
132
+ checkStock(variant.stock, `${variantLabel}.stock`, errors, true);
133
+ const chosen = variant.attributes;
134
+ if (!chosen || typeof chosen !== "object" || Array.isArray(chosen)) {
135
+ errors.push(`${variantLabel}.attributes must map attribute codes to option values`);
136
+ continue;
137
+ }
138
+ const keys = Object.keys(chosen).sort();
139
+ const expected = variationAttributes.map((attribute) => attribute.code).filter(Boolean).sort();
140
+ if (keys.join("|") !== expected.join("|")) {
141
+ errors.push(`${variantLabel}.attributes must set exactly the variation attributes (${expected.join(", ")})`);
142
+ }
143
+ for (const key of keys) {
144
+ const allowed = optionsByCode.get(key);
145
+ if (!allowed || !allowed.has(chosen[key])) {
146
+ errors.push(`${variantLabel}.attributes.${key} value ${JSON.stringify(chosen[key])} is not an option`);
147
+ }
148
+ }
149
+ const signature = expected.map((key) => `${key}=${chosen[key]}`).join("&");
150
+ if (combinations.has(signature)) {
151
+ errors.push(`${variantLabel} repeats combination ${signature}`);
152
+ }
153
+ combinations.add(signature);
154
+ }
155
+ }
156
+
157
+ function requireSku(sku, skus, label, errors) {
158
+ if (typeof sku !== "string" || sku.trim() === "") {
159
+ errors.push(`${label} is required`);
160
+ return;
161
+ }
162
+ if (skus.has(sku)) {
163
+ errors.push(`${label} duplicates SKU ${sku}`);
164
+ return;
165
+ }
166
+ skus.add(sku);
167
+ }
168
+
169
+ function requirePrice(record, label, errors) {
170
+ if (typeof record.price !== "string" || !/^\d+\.\d{2}$/.test(record.price)) {
171
+ errors.push(`${label}.price must be a decimal string with two places, such as "19.00"`);
172
+ }
173
+ if (typeof record.currency !== "string" || !/^[A-Z]{3}$/.test(record.currency)) {
174
+ errors.push(`${label}.currency must be a 3-letter ISO 4217 code`);
175
+ }
176
+ }
177
+
178
+ function checkStock(stock, label, errors, required) {
179
+ if (stock === undefined) {
180
+ if (required) {
181
+ errors.push(`${label} is required`);
182
+ }
183
+ return;
184
+ }
185
+ if (!stock || typeof stock !== "object") {
186
+ errors.push(`${label} must be an object`);
187
+ return;
188
+ }
189
+ if (typeof stock.managed !== "boolean") {
190
+ errors.push(`${label}.managed must be true or false`);
191
+ }
192
+ if (stock.managed === true) {
193
+ if (!Number.isInteger(stock.quantity) || stock.quantity < 0) {
194
+ errors.push(`${label}.quantity must be an integer of zero or more when stock is managed`);
195
+ }
196
+ } else if (stock.quantity !== null && stock.quantity !== undefined) {
197
+ errors.push(`${label}.quantity must be null when stock is not managed`);
198
+ }
199
+ if (!BACKORDERS.has(stock.backorders)) {
200
+ errors.push(`${label}.backorders must be no, notify, or yes`);
201
+ }
202
+ if (stock.status !== undefined && !STOCK_STATUS.has(stock.status)) {
203
+ errors.push(`${label}.status must be instock, outofstock, or onbackorder`);
204
+ }
205
+ }
206
+
207
+ function reject(value, label, message, errors) {
208
+ if (value !== undefined && !(Array.isArray(value) && value.length === 0)) {
209
+ errors.push(`${label}: ${message}`);
210
+ }
211
+ }
212
+
213
+ function isId(value) {
214
+ return typeof value === "string" && value.trim() !== "";
215
+ }
216
+
217
+ const isCli = process.argv[1] && process.argv[1].endsWith("validate-catalog.mjs");
218
+ if (isCli) {
219
+ const fileFlag = process.argv.indexOf("--file");
220
+ const file = fileFlag === -1 ? undefined : process.argv[fileFlag + 1];
221
+ if (!file) {
222
+ console.error("Usage: node validate-catalog.mjs --file catalog.json");
223
+ process.exit(2);
224
+ }
225
+ const document = JSON.parse(await readFile(file, "utf8"));
226
+ const errors = validateCatalog(document);
227
+ if (errors.length > 0) {
228
+ for (const error of errors) {
229
+ console.error(`ERROR ${error}`);
230
+ }
231
+ process.exit(1);
232
+ }
233
+ console.log("Catalog is valid.");
234
+ }
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: product-description-guardrails
3
+ description: "Generate product titles and descriptions only from catalog facts, and block prices, certifications, and medical claims that are not in the record. Use when writing or reviewing generated product copy, Merchant Center text, or locale-specific descriptions."
4
+ license: MIT
5
+ compatibility: "The description checker requires Node.js 20 or newer. No network access."
6
+ metadata:
7
+ author: Naser Sadrmanesh
8
+ version: "0.3.0"
9
+ ---
10
+
11
+ # Product description guardrails
12
+
13
+ Use this skill when copy is generated from a catalog record. The feed skill decides which Merchant Center attribute receives that copy. Search text normalization is the search skill. This skill does not claim the copy is true. It keeps the copy inside the facts you passed in.
14
+
15
+ ## Procedure
16
+
17
+ 1. Pass a source record: name, price, currency, locale, and the facts that may be stated (materials, dimensions, color, care, certifications).
18
+ 2. Write the description from those facts only. If a fact is absent, omit it. Do not fill the gap with a typical value for that category.
19
+ 3. Either omit the price or state the source price and currency. Do not round it into a different amount.
20
+ 4. Refuse medical, safety, and guarantee claims that are not in the source. The checker encodes a short refuse-list, not a legal opinion.
21
+ 5. Match the locale. A `fa_IR` description is Persian. Do not mix a generated English paragraph into that field.
22
+ 6. Run `node scripts/check-description.mjs --description copy.txt --source source.json`.
23
+
24
+ The refuse-list and the Merchant Center labeling rule are in [the claim notes](references/claims.md). Worked examples and checks are in [evals/tasks.json](evals/tasks.json).
25
+
26
+ ## Source record
27
+
28
+ ```json
29
+ {
30
+ "name": "Cotton shirt",
31
+ "price": "19.00",
32
+ "currency": "USD",
33
+ "locale": "en",
34
+ "facts": ["short sleeve", "cotton", "blue"],
35
+ "allowedClaims": []
36
+ }
37
+ ```
38
+
39
+ `allowedClaims` is empty unless the merchant has supplied the exact claim that may be repeated, such as a certification number that is already in `facts`.
40
+
41
+ ## What may be written
42
+
43
+ - The product name and the facts, in the locale's language.
44
+ - Care or sizing text that appears in `facts`.
45
+ - The price only as `{price} {currency}`.
46
+
47
+ ## What may not be written
48
+
49
+ - A material, dimension, country of origin, or certification that is not in `facts`.
50
+ - "Cures", "clinically proven", "guaranteed to", "100% effective", or "risk-free", unless that phrase is listed in `allowedClaims`.
51
+ - Another product's name, a competitor, or "free shipping".
52
+ - A sale price the source does not contain.
53
+ - Stars or review counts that are not in the source.
54
+
55
+ ## Merchant Center
56
+
57
+ Google's product data specification says generated titles and descriptions use `structured_title` and `structured_description` with `digital_source_type` set to `trained_algorithmic_media`. Text that was not generated uses `title` and `description`. The feed skill checks lengths: 150 characters for a title and 5000 for a description.
58
+
59
+ ## Checker limits
60
+
61
+ `scripts/check-description.mjs` checks:
62
+
63
+ - non-empty text at most 5000 characters
64
+ - any `{amount} {CURRENCY}` in the text matches `price` and `currency`
65
+ - the refuse-list phrases above, unless present in `allowedClaims`
66
+
67
+ It does not detect a made-up material that lacks those phrases. Review the facts list. A passing check is not a statement that the description is accurate.
68
+
69
+ ## Gotchas
70
+
71
+ - Translating a fact is allowed. Adding a fact during translation is not. "Cotton" may become the Persian word for cotton. It may not gain "organic".
72
+ - A marketing model will add "perfect for" and a use the catalog never stated. Keep the sentence tied to a fact or delete it.
73
+ - Putting the price in Persian digits in the description is a display choice. The checker looks for ASCII `{amount} {CURRENCY}`. Do not also embed a different ASCII amount.
74
+
75
+ ## Verification
76
+
77
+ - The checker fails a description that says `15.00 USD` when the source price is `19.00`.
78
+ - The checker fails "clinically proven" when `allowedClaims` is empty.
79
+ - A `fa_IR` request does not return an English-only description.
80
+ - Generated Merchant Center text is labeled `trained_algorithmic_media`.
81
+
82
+ ## Evaluation
83
+
84
+ Worked examples and the checks they must satisfy are in [evals/tasks.json](evals/tasks.json).
@@ -0,0 +1,3 @@
1
+ Describe the shirt from the source record.
2
+
3
+ The Cotton shirt is short sleeve, cotton, and blue. The price is 19.00 USD. No certification is mentioned because the source has none.
@@ -0,0 +1,3 @@
1
+ Send the generated paragraph as `structured_description`.
2
+
3
+ Set `digital_source_type` to `trained_algorithmic_media`. Keep the words inside the supplied facts.