@wix/agent-skills 1.33.1 → 1.34.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 (616) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/skills/wix-export/SKILL.md +83 -0
  4. package/skills/wix-harmony-editor/.publish-manifest.json +8 -0
  5. package/skills/wix-harmony-editor/COPY-SITE.md +494 -0
  6. package/skills/wix-harmony-editor/SKILL.md +317 -0
  7. package/skills/wix-harmony-editor/scripts/copy/build-description.mjs +249 -0
  8. package/skills/wix-harmony-editor/scripts/copy/check-description.mjs +225 -0
  9. package/skills/wix-harmony-editor/scripts/copy/check-mirror.mjs +89 -0
  10. package/skills/wix-harmony-editor/scripts/copy/compare-preview.mjs +76 -0
  11. package/skills/wix-harmony-editor/scripts/copy/deploy-description.mjs +212 -0
  12. package/skills/wix-harmony-editor/scripts/copy/desc-hash.mjs +44 -0
  13. package/skills/wix-harmony-editor/scripts/copy/editor-run.mjs +45 -0
  14. package/skills/wix-harmony-editor/scripts/copy/export-page.mjs +53 -0
  15. package/skills/wix-harmony-editor/scripts/copy/find-interactions.mjs +76 -0
  16. package/skills/wix-harmony-editor/scripts/copy/hide-scrollbars.mjs +16 -0
  17. package/skills/wix-harmony-editor/scripts/copy/import-media.mjs +150 -0
  18. package/skills/wix-harmony-editor/scripts/copy/link-url.mjs +27 -0
  19. package/skills/wix-harmony-editor/scripts/copy/lint-api.mjs +56 -0
  20. package/skills/wix-harmony-editor/scripts/copy/measure-source.mjs +211 -0
  21. package/skills/wix-harmony-editor/scripts/copy/merge-mirrors.mjs +29 -0
  22. package/skills/wix-harmony-editor/scripts/copy/mirror-site.mjs +92 -0
  23. package/skills/wix-harmony-editor/scripts/copy/mobile-stage.mjs +56 -0
  24. package/skills/wix-harmony-editor/scripts/copy/preview-widths.mjs +73 -0
  25. package/skills/wix-harmony-editor/scripts/copy/render-plate.mjs +67 -0
  26. package/skills/wix-harmony-editor/scripts/copy/serve-mirror.mjs +102 -0
  27. package/skills/wix-harmony-editor/scripts/copy/snapshot-capabilities.mjs +40 -0
  28. package/skills/wix-harmony-editor/scripts/copy/split-sections.mjs +191 -0
  29. package/skills/wix-harmony-editor/scripts/copy/upload-media-manager.mjs +61 -0
  30. package/skills/wix-harmony-editor/scripts/launch.sh +359 -0
  31. package/skills/wix-headless/SKILL.md +7 -2
  32. package/skills/wix-headless/references/CAPABILITIES.md +6 -1
  33. package/skills/wix-headless/references/DISCOVERY.md +2 -1
  34. package/skills/wix-headless/references/SDK_HANDOFF.md +6 -1
  35. package/skills/wix-headless/references/SEED.md +3 -2
  36. package/skills/wix-headless/references/SETUP.md +1 -0
  37. package/skills/wix-headless/references/inline-recipes/experience-donations.md +102 -0
  38. package/skills/wix-headless/references/inline-recipes/how-to-code-donations.md +146 -0
  39. package/skills/wix-headless/references/inline-recipes/setup-donations.md +15 -0
  40. package/skills/wix-headless/references/managed/CREATE.md +1 -1
  41. package/skills/wix-headless-kit/SKILL.md +110 -21
  42. package/skills/wix-headless-kit/guides/api-run.md +15 -13
  43. package/skills/wix-headless-kit/guides/existing-site.md +31 -0
  44. package/skills/wix-headless-kit/guides/migration.md +49 -17
  45. package/skills/wix-headless-kit/install/agents-md.mjs +1 -1
  46. package/skills/wix-headless-kit/install/context.mjs +61 -6
  47. package/skills/wix-headless-kit/install/pins.json +1 -1
  48. package/skills/wix-headless-kit/install/setup.mjs +107 -12
  49. package/skills/wix-headless-replatform/.publish-manifest.json +8 -0
  50. package/skills/wix-headless-replatform/SKILL.md +116 -0
  51. package/skills/wix-headless-replatform/agents/openai.yaml +6 -0
  52. package/skills/wix-headless-replatform/references/control-and-visual-fidelity-contract.md +43 -0
  53. package/skills/wix-headless-replatform/references/extraction-schema.md +187 -0
  54. package/skills/wix-headless-replatform/references/interaction-runtime-contract.md +137 -0
  55. package/skills/wix-headless-replatform/references/layout-blueprint-contract.md +41 -0
  56. package/skills/wix-headless-replatform/references/output-contract.md +93 -0
  57. package/skills/wix-headless-replatform/references/post-build-gap-contract.md +125 -0
  58. package/skills/wix-headless-replatform/references/qa-checklist.md +83 -0
  59. package/skills/wix-headless-replatform/references/safety.md +19 -0
  60. package/skills/wix-headless-replatform/references/ui-normalization-contract.md +70 -0
  61. package/skills/wix-headless-replatform/references/workflow.md +157 -0
  62. package/skills/wix-headless-replatform/registry/README.md +20 -0
  63. package/skills/wix-headless-replatform/registry/approvals/button/shadcn-default-ac60ef5.json +28 -0
  64. package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/button.tsx +54 -0
  65. package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/capabilities.json +60 -0
  66. package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/contract.md +48 -0
  67. package/skills/wix-headless-replatform/registry/licenses/shadcn-ui-ac60ef5-MIT.txt +21 -0
  68. package/skills/wix-headless-replatform/registry/registry.json +36 -0
  69. package/skills/wix-headless-replatform/resources/rp-facelift/SKILL.md +32 -0
  70. package/skills/wix-headless-replatform/resources/rp-project-context/SKILL.md +62 -0
  71. package/skills/wix-headless-replatform/resources/rp-qa-gap-loop/SKILL.md +64 -0
  72. package/skills/wix-headless-replatform/resources/rp-source-evidence/SKILL.md +42 -0
  73. package/skills/wix-headless-replatform/resources/rp-ui-implementation/SKILL.md +73 -0
  74. package/skills/wix-headless-replatform/schemas/build-plan.schema.json +10 -0
  75. package/skills/wix-headless-replatform/schemas/candidate.schema.json +19 -0
  76. package/skills/wix-headless-replatform/schemas/common.schema.json +33 -0
  77. package/skills/wix-headless-replatform/schemas/decision-patch.schema.json +10 -0
  78. package/skills/wix-headless-replatform/schemas/decision-request.schema.json +10 -0
  79. package/skills/wix-headless-replatform/schemas/extraction-gap.schema.json +28 -0
  80. package/skills/wix-headless-replatform/schemas/extraction-manifest.schema.json +34 -0
  81. package/skills/wix-headless-replatform/schemas/final-report.schema.json +10 -0
  82. package/skills/wix-headless-replatform/schemas/foundation.schema.json +11 -0
  83. package/skills/wix-headless-replatform/schemas/metadata.schema.json +11 -0
  84. package/skills/wix-headless-replatform/schemas/observation-packet.schema.json +17 -0
  85. package/skills/wix-headless-replatform/schemas/page-capture.schema.json +11 -0
  86. package/skills/wix-headless-replatform/schemas/page-resolution.schema.json +33 -0
  87. package/skills/wix-headless-replatform/schemas/shared-chrome.schema.json +11 -0
  88. package/skills/wix-headless-replatform/schemas/unit.schema.json +40 -0
  89. package/skills/wix-headless-replatform/scripts/assemble-extraction.mjs +844 -0
  90. package/skills/wix-headless-replatform/scripts/benchmark-extraction.mjs +163 -0
  91. package/skills/wix-headless-replatform/scripts/browser-extraction-preflight.mjs +42 -0
  92. package/skills/wix-headless-replatform/scripts/build-extracted-homepage.mjs +228 -0
  93. package/skills/wix-headless-replatform/scripts/clear-wix-starter.mjs +87 -0
  94. package/skills/wix-headless-replatform/scripts/create-wix-project.mjs +505 -0
  95. package/skills/wix-headless-replatform/scripts/discover-urls.mjs +142 -0
  96. package/skills/wix-headless-replatform/scripts/extract-assets.mjs +140 -0
  97. package/skills/wix-headless-replatform/scripts/extract-design-system.mjs +637 -0
  98. package/skills/wix-headless-replatform/scripts/extract-interactions.mjs +1903 -0
  99. package/skills/wix-headless-replatform/scripts/extract-page.mjs +2461 -0
  100. package/skills/wix-headless-replatform/scripts/extract-seo.mjs +25 -0
  101. package/skills/wix-headless-replatform/scripts/extract-tokens.mjs +63 -0
  102. package/skills/wix-headless-replatform/scripts/finalize-extraction-report.mjs +126 -0
  103. package/skills/wix-headless-replatform/scripts/finalize-gap-review.mjs +180 -0
  104. package/skills/wix-headless-replatform/scripts/finalize-gap-review.test.mjs +98 -0
  105. package/skills/wix-headless-replatform/scripts/finalize-migration-website.mjs +103 -0
  106. package/skills/wix-headless-replatform/scripts/generate-control-state-contract.mjs +176 -0
  107. package/skills/wix-headless-replatform/scripts/generate-design-md.mjs +261 -0
  108. package/skills/wix-headless-replatform/scripts/generate-extraction-validators.mjs +25 -0
  109. package/skills/wix-headless-replatform/scripts/generate-invoice-download-backend.mjs +342 -0
  110. package/skills/wix-headless-replatform/scripts/generate-layout-blueprint.mjs +140 -0
  111. package/skills/wix-headless-replatform/scripts/generate-repeater-cms.mjs +22 -0
  112. package/skills/wix-headless-replatform/scripts/generate-route-plan.mjs +143 -0
  113. package/skills/wix-headless-replatform/scripts/generate-scene-contract.mjs +288 -0
  114. package/skills/wix-headless-replatform/scripts/generate-ui-normalization.mjs +266 -0
  115. package/skills/wix-headless-replatform/scripts/generate-visual-assets.mjs +111 -0
  116. package/skills/wix-headless-replatform/scripts/lib/browser-tooling.mjs +570 -0
  117. package/skills/wix-headless-replatform/scripts/lib/common.mjs +329 -0
  118. package/skills/wix-headless-replatform/scripts/lib/component-capabilities.mjs +121 -0
  119. package/skills/wix-headless-replatform/scripts/lib/component-registry.mjs +282 -0
  120. package/skills/wix-headless-replatform/scripts/lib/content-boundary.mjs +61 -0
  121. package/skills/wix-headless-replatform/scripts/lib/extraction-contract.mjs +374 -0
  122. package/skills/wix-headless-replatform/scripts/lib/extraction-detectors.mjs +243 -0
  123. package/skills/wix-headless-replatform/scripts/lib/extraction-validators.generated.mjs +1139 -0
  124. package/skills/wix-headless-replatform/scripts/lib/font-contract.mjs +326 -0
  125. package/skills/wix-headless-replatform/scripts/lib/frontend-automation-state.mjs +184 -0
  126. package/skills/wix-headless-replatform/scripts/lib/gap-analysis.mjs +396 -0
  127. package/skills/wix-headless-replatform/scripts/lib/html-extract.mjs +168 -0
  128. package/skills/wix-headless-replatform/scripts/lib/interaction-bootstrap.mjs +10 -0
  129. package/skills/wix-headless-replatform/scripts/lib/interaction-runtime.mjs +194 -0
  130. package/skills/wix-headless-replatform/scripts/lib/interaction-timeline.mjs +348 -0
  131. package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/classifiers.mjs +141 -0
  132. package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/geometry.mjs +43 -0
  133. package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/vocabulary.mjs +18 -0
  134. package/skills/wix-headless-replatform/scripts/lib/ui-normalization.css +64 -0
  135. package/skills/wix-headless-replatform/scripts/optimize-svg.mjs +31 -0
  136. package/skills/wix-headless-replatform/scripts/post-build-gap.mjs +358 -0
  137. package/skills/wix-headless-replatform/scripts/provision-repeater-cms.mjs +25 -0
  138. package/skills/wix-headless-replatform/scripts/record-gap-decision.mjs +39 -0
  139. package/skills/wix-headless-replatform/scripts/replay-extraction.mjs +107 -0
  140. package/skills/wix-headless-replatform/scripts/resolve-home-page.mjs +96 -0
  141. package/skills/wix-headless-replatform/scripts/site-clone.mjs +658 -0
  142. package/skills/wix-headless-replatform/scripts/validate-font-contract.mjs +29 -0
  143. package/skills/wix-headless-replatform/scripts/verify-interactions.mjs +669 -0
  144. package/skills/wix-headless-replatform/scripts/visual-qa.mjs +370 -0
  145. package/skills/wix-headless-templates/donations/INSTRUCTIONS.md +37 -24
  146. package/skills/wix-headless-templates/donations/app/components/donations/DonateForm.tsx +49 -23
  147. package/skills/wix-headless-templates/donations/app/components/donations/DonationReceipt.tsx +6 -5
  148. package/skills/wix-headless-templates/donations/app/components/donations/GoalProgress.tsx +5 -5
  149. package/skills/wix-headless-templates/donations/app/wix/donations/campaigns.ts +4 -4
  150. package/skills/wix-headless-templates/donations/app/wix/donations/donate.ts +7 -3
  151. package/skills/wix-headless-templates/donations/app/wix/donations/donation-store.ts +2 -2
  152. package/skills/wix-headless-templates/donations/app/wix/donations/donations-core.ts +12 -9
  153. package/skills/wix-headless-templates/donations/project/src/components/donations/DonateForm.tsx +49 -23
  154. package/skills/wix-headless-templates/donations/project/src/components/donations/DonationReceipt.tsx +6 -5
  155. package/skills/wix-headless-templates/donations/project/src/components/donations/GoalProgress.tsx +5 -5
  156. package/skills/wix-headless-templates/donations/project/src/wix/donations/campaigns.ts +4 -4
  157. package/skills/wix-headless-templates/donations/project/src/wix/donations/donate.ts +7 -3
  158. package/skills/wix-headless-templates/donations/project/src/wix/donations/donation-store.ts +2 -2
  159. package/skills/wix-headless-templates/donations/project/src/wix/donations/donations-core.ts +12 -9
  160. package/skills/wix-headless-templates/donations/rest/campaigns.ts +4 -4
  161. package/skills/wix-headless-templates/donations/rest/donate.ts +7 -3
  162. package/skills/wix-headless-templates/donations/seed/SEED.md +12 -7
  163. package/skills/wix-headless-templates/donations/tests/donation-contract.test.mjs +71 -0
  164. package/skills/wix-headless-templates/donations/tests/donation-form.test.cjs +137 -0
  165. package/skills/wix-headless-templates/restaurants/INSTRUCTIONS.md +28 -14
  166. package/skills/wix-headless-templates/restaurants/app/components/restaurants/OrderCartDrawer.tsx +87 -5
  167. package/skills/wix-headless-templates/restaurants/app/hooks/restaurants/useOrderCart.ts +9 -1
  168. package/skills/wix-headless-templates/restaurants/app/wix/restaurants/order-store.ts +33 -4
  169. package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering-core.ts +100 -5
  170. package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering.ts +101 -15
  171. package/skills/wix-headless-templates/restaurants/app/wix/restaurants/types.ts +35 -1
  172. package/skills/wix-headless-templates/restaurants/project/src/components/restaurants/OrderCartDrawer.tsx +87 -5
  173. package/skills/wix-headless-templates/restaurants/project/src/hooks/restaurants/useOrderCart.ts +9 -1
  174. package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/order-store.ts +33 -4
  175. package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering-core.ts +100 -5
  176. package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering.ts +101 -15
  177. package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/types.ts +35 -1
  178. package/skills/wix-headless-templates/restaurants/rest/ordering.ts +91 -14
  179. package/skills/wix-import/SKILL.md +90 -0
  180. package/skills/wix-manage/SKILL.md +13 -3
  181. package/skills/wix-manage/references/dashboard-navigation/dashboard-navigation.md +2 -1
  182. package/skills/wix-manage/references/dashboard-navigation/routes.json +249 -0
  183. package/skills/wix-manage/references/donations/create-and-update-donation-campaigns.md +139 -0
  184. package/skills/wix-manage/references/events/events-dashboard-navigation.md +81 -0
  185. package/skills/wix-manage/references/sites/manage-oauth-apps.md +1 -0
  186. package/skills/wix-manage/references/sites/upload-static-site.md +127 -8
  187. package/skills/wix-replatform/.publish-manifest.json +5 -5
  188. package/skills/wix-replatform/CLI.md +162 -0
  189. package/skills/wix-replatform/README.md +52 -0
  190. package/skills/wix-replatform/SKILL.md +229 -684
  191. package/skills/wix-replatform/console/account.css +1 -0
  192. package/skills/wix-replatform/console/app.css +1 -0
  193. package/skills/wix-replatform/console/app.js +117 -0
  194. package/skills/wix-replatform/console/index.html +4 -0
  195. package/skills/wix-replatform/console/json.css +11 -0
  196. package/skills/wix-replatform/lib/agent-adapters/index.js +71 -0
  197. package/skills/wix-replatform/lib/app-page-routing.js +75 -0
  198. package/skills/wix-replatform/lib/artifact-freshness.js +11 -1
  199. package/skills/wix-replatform/lib/blocked-data-requests.js +427 -0
  200. package/skills/wix-replatform/lib/census-reconciliation.js +426 -0
  201. package/skills/wix-replatform/lib/completion-report.js +1393 -13
  202. package/skills/wix-replatform/lib/config-env.js +85 -11
  203. package/skills/wix-replatform/lib/confirmed-pictures.js +212 -0
  204. package/skills/wix-replatform/lib/dependency-readiness.js +423 -0
  205. package/skills/wix-replatform/lib/dependency-recovery.js +21 -0
  206. package/skills/wix-replatform/lib/destination-choice.js +163 -0
  207. package/skills/wix-replatform/lib/destination-handoff.js +62 -0
  208. package/skills/wix-replatform/lib/destination-http.js +81 -0
  209. package/skills/wix-replatform/lib/destination-route.js +98 -0
  210. package/skills/wix-replatform/lib/destination.js +919 -0
  211. package/skills/wix-replatform/lib/discovered-census.js +231 -0
  212. package/skills/wix-replatform/lib/discovered-set-status.js +255 -0
  213. package/skills/wix-replatform/lib/document-url-guard.js +203 -0
  214. package/skills/wix-replatform/lib/entity-qa-attestation.js +339 -0
  215. package/skills/wix-replatform/lib/entity-verification.js +18 -0
  216. package/skills/wix-replatform/lib/execution-capability.js +120 -0
  217. package/skills/wix-replatform/lib/execution-log.js +170 -0
  218. package/skills/wix-replatform/lib/execution-manifest.js +20 -2
  219. package/skills/wix-replatform/lib/execution-state.js +22 -0
  220. package/skills/wix-replatform/lib/expectations-ledger.js +96 -0
  221. package/skills/wix-replatform/lib/generated-plugin-handoff.js +140 -0
  222. package/skills/wix-replatform/lib/import-readiness-contract.js +23 -0
  223. package/skills/wix-replatform/lib/import-recovery.js +800 -6
  224. package/skills/wix-replatform/lib/imported-redirect-targets.js +608 -0
  225. package/skills/wix-replatform/lib/internal-link-reconciliation.js +166 -0
  226. package/skills/wix-replatform/lib/link-audit.js +173 -0
  227. package/skills/wix-replatform/lib/link-census.js +194 -0
  228. package/skills/wix-replatform/lib/link-plan.js +148 -0
  229. package/skills/wix-replatform/lib/link-verification.js +274 -0
  230. package/skills/wix-replatform/lib/link-write-journal.js +42 -0
  231. package/skills/wix-replatform/lib/local-state.js +869 -30
  232. package/skills/wix-replatform/lib/member-completion.js +658 -0
  233. package/skills/wix-replatform/lib/member-crosswalk.js +69 -0
  234. package/skills/wix-replatform/lib/member-outcome-registry.js +265 -0
  235. package/skills/wix-replatform/lib/migration-completion.js +311 -0
  236. package/skills/wix-replatform/lib/operator-identity.js +47 -0
  237. package/skills/wix-replatform/lib/orchestration-decisions.js +248 -0
  238. package/skills/wix-replatform/lib/orchestration-preflight.js +401 -19
  239. package/skills/wix-replatform/lib/orchestration-router.js +608 -32
  240. package/skills/wix-replatform/lib/orchestration-state.js +188 -12
  241. package/skills/wix-replatform/lib/order-reference-readiness.js +30 -0
  242. package/skills/wix-replatform/lib/payments-outcome-registry.js +200 -0
  243. package/skills/wix-replatform/lib/plugin-cms-fallback.js +618 -0
  244. package/skills/wix-replatform/lib/preservation-approval-stamp.js +54 -0
  245. package/skills/wix-replatform/lib/product-media-deferral.js +96 -0
  246. package/skills/wix-replatform/lib/progress-log.js +19 -3
  247. package/skills/wix-replatform/lib/read-back-approval.js +184 -0
  248. package/skills/wix-replatform/lib/read-back-comparison.js +136 -0
  249. package/skills/wix-replatform/lib/read-back-contract.js +243 -0
  250. package/skills/wix-replatform/lib/read-back-extract.js +118 -0
  251. package/skills/wix-replatform/lib/read-back-gate.js +734 -0
  252. package/skills/wix-replatform/lib/read-back-reads.js +94 -0
  253. package/skills/wix-replatform/lib/read-back-state.js +107 -0
  254. package/skills/wix-replatform/lib/read-back-summary.js +81 -0
  255. package/skills/wix-replatform/lib/rerun-conflicts.js +334 -0
  256. package/skills/wix-replatform/lib/run-controller.js +401 -0
  257. package/skills/wix-replatform/lib/seo-outcome-registry.js +45 -0
  258. package/skills/wix-replatform/lib/setup-verification.js +148 -0
  259. package/skills/wix-replatform/lib/source-access.js +231 -0
  260. package/skills/wix-replatform/lib/source-coverage.js +1130 -0
  261. package/skills/wix-replatform/lib/source-secrets.js +277 -0
  262. package/skills/wix-replatform/lib/structured-link-patch.js +59 -0
  263. package/skills/wix-replatform/lib/task-state.js +110 -0
  264. package/skills/wix-replatform/lib/url-preservation-report.js +319 -0
  265. package/skills/wix-replatform/lib/url-preservation-state.js +349 -1
  266. package/skills/wix-replatform/lib/url-redirect-plan.js +728 -0
  267. package/skills/wix-replatform/lib/url-verification.js +315 -0
  268. package/skills/wix-replatform/lib/user-action-effects.js +42 -0
  269. package/skills/wix-replatform/lib/user-actions.js +264 -0
  270. package/skills/wix-replatform/lib/website-handoff.js +1220 -0
  271. package/skills/wix-replatform/lib/wix-client-callers.js +54 -0
  272. package/skills/wix-replatform/lib/wix-dashboard-url.js +35 -0
  273. package/skills/wix-replatform/lib/write-verification-state.js +92 -0
  274. package/skills/wix-replatform/lib/write-verification.js +274 -0
  275. package/skills/wix-replatform/resources/rp-destination/SKILL.md +137 -0
  276. package/skills/wix-replatform/resources/rp-discovery/SKILL.md +189 -16
  277. package/skills/wix-replatform/resources/rp-discovery/scripts/promote-source-coverage.js +47 -0
  278. package/skills/wix-replatform/resources/rp-execute-import/SKILL.md +869 -29
  279. package/skills/wix-replatform/resources/rp-execute-import/scripts/shipping-estimate-audit.js +70 -0
  280. package/skills/wix-replatform/resources/rp-execute-setup/SKILL.md +195 -29
  281. package/skills/wix-replatform/resources/rp-execute-setup/scripts/execute-setup.js +442 -0
  282. package/skills/wix-replatform/resources/rp-execution-policy/SKILL.md +112 -0
  283. package/skills/wix-replatform/resources/rp-execution-policy/lib/coupon-currency-plan-report.js +143 -0
  284. package/skills/wix-replatform/resources/rp-execution-policy/lib/payments-report.js +244 -0
  285. package/skills/wix-replatform/resources/rp-import-codegen/SKILL.md +2082 -38
  286. package/skills/wix-replatform/resources/rp-mapper/SKILL.md +528 -11
  287. package/skills/wix-replatform/resources/rp-mapper/lib/structure-bridge-planning.js +303 -0
  288. package/skills/wix-replatform/resources/rp-mapper/scripts/coverage-reconcile.js +88 -0
  289. package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +6 -5
  290. package/skills/wix-replatform/resources/rp-preflight/SKILL.md +40 -0
  291. package/skills/wix-replatform/resources/rp-project-intake/SKILL.md +55 -0
  292. package/skills/wix-replatform/resources/rp-quick-runtime/SKILL.md +60 -0
  293. package/skills/wix-replatform/resources/rp-quick-runtime/lib/import-progress.js +89 -0
  294. package/skills/wix-replatform/resources/rp-quick-runtime/lib/limit-recovery.js +36 -0
  295. package/skills/wix-replatform/resources/rp-quick-shopify/SKILL.md +123 -0
  296. package/skills/wix-replatform/resources/rp-quick-shopify/quick-mode.json +124 -0
  297. package/skills/wix-replatform/resources/rp-quick-shopify/scripts/quick-mode.js +522 -0
  298. package/skills/wix-replatform/resources/rp-quick-woocommerce/SKILL.md +56 -0
  299. package/skills/wix-replatform/resources/rp-quick-woocommerce/quick-mode.json +91 -0
  300. package/skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js +255 -0
  301. package/skills/wix-replatform/resources/rp-setup-discovery/SKILL.md +80 -10
  302. package/skills/wix-replatform/resources/rp-setup-discovery/scripts/quick-setup-plan.js +44 -0
  303. package/skills/wix-replatform/resources/rp-source-csv/SKILL.md +3 -0
  304. package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fileset.js +0 -0
  305. package/skills/wix-replatform/resources/rp-source-csv/scripts/csv-discovery.js +21 -1
  306. package/skills/wix-replatform/resources/rp-source-inputs/SKILL.md +66 -0
  307. package/skills/wix-replatform/resources/rp-source-sfcc/SKILL.md +153 -0
  308. package/skills/wix-replatform/resources/rp-source-shopify/SKILL.md +394 -0
  309. package/skills/wix-replatform/resources/rp-source-shopify/lib/discovery-reads.js +200 -0
  310. package/skills/wix-replatform/resources/rp-source-shopify/lib/import-findings.js +34 -0
  311. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk-sqlite-worker.js +44 -0
  312. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk.js +1470 -0
  313. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-cli.js +446 -0
  314. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-gift-cards.js +474 -0
  315. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-protected-fields.js +879 -0
  316. package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-session.js +426 -0
  317. package/skills/wix-replatform/resources/rp-source-shopify/queries/articles.graphql +24 -0
  318. package/skills/wix-replatform/resources/rp-source-shopify/queries/blogs.graphql +18 -0
  319. package/skills/wix-replatform/resources/rp-source-shopify/queries/collections.graphql +40 -0
  320. package/skills/wix-replatform/resources/rp-source-shopify/queries/customers.graphql +76 -0
  321. package/skills/wix-replatform/resources/rp-source-shopify/queries/delivery-profiles.graphql +51 -0
  322. package/skills/wix-replatform/resources/rp-source-shopify/queries/discounts.graphql +35 -0
  323. package/skills/wix-replatform/resources/rp-source-shopify/queries/files.graphql +20 -0
  324. package/skills/wix-replatform/resources/rp-source-shopify/queries/gift-cards.graphql +18 -0
  325. package/skills/wix-replatform/resources/rp-source-shopify/queries/inventory.graphql +31 -0
  326. package/skills/wix-replatform/resources/rp-source-shopify/queries/locations.graphql +21 -0
  327. package/skills/wix-replatform/resources/rp-source-shopify/queries/menus.graphql +21 -0
  328. package/skills/wix-replatform/resources/rp-source-shopify/queries/metaobjects.graphql +30 -0
  329. package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-core.graphql +150 -0
  330. package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-fulfillments.graphql +36 -0
  331. package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-refunds.graphql +48 -0
  332. package/skills/wix-replatform/resources/rp-source-shopify/queries/pages.graphql +20 -0
  333. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-address.graphql +8 -0
  334. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-email.graphql +6 -0
  335. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-name.graphql +8 -0
  336. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-phone.graphql +8 -0
  337. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-address.graphql +12 -0
  338. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-email.graphql +6 -0
  339. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-name.graphql +13 -0
  340. package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-phone.graphql +13 -0
  341. package/skills/wix-replatform/resources/rp-source-shopify/queries/products.graphql +73 -0
  342. package/skills/wix-replatform/resources/rp-source-shopify/queries/url-redirects.graphql +13 -0
  343. package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-discovery.js +693 -0
  344. package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-extract.js +283 -0
  345. package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-gift-card-remedy.js +191 -0
  346. package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-session-check.js +115 -0
  347. package/skills/wix-replatform/resources/rp-source-shopify/source-schema.example.json +201 -0
  348. package/skills/wix-replatform/resources/rp-source-wordpress/PLUGIN-READ-PLANS.md +146 -0
  349. package/skills/wix-replatform/resources/rp-source-wordpress/SKILL.md +736 -22
  350. package/skills/wix-replatform/resources/rp-source-wordpress/lib/blocked-data-handlers.js +73 -0
  351. package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.js +115 -0
  352. package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.test-fixture.js +91 -0
  353. package/skills/wix-replatform/resources/rp-source-wordpress/lib/coupon-discovery.js +440 -0
  354. package/skills/wix-replatform/resources/rp-source-wordpress/lib/currency-gate.js +125 -0
  355. package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.js +252 -0
  356. package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.json +183 -0
  357. package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-summary.mjs +333 -0
  358. package/skills/wix-replatform/resources/rp-source-wordpress/lib/invoice-discovery.js +426 -0
  359. package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-disposition.js +400 -0
  360. package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-entity-read-plan.js +633 -0
  361. package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-knowledge.js +1127 -0
  362. package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.js +194 -0
  363. package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.json +100 -0
  364. package/skills/wix-replatform/resources/rp-source-wordpress/lib/sampled-ids-batch.js +157 -0
  365. package/skills/wix-replatform/resources/rp-source-wordpress/lib/secret-value-guard.js +234 -0
  366. package/skills/wix-replatform/resources/rp-source-wordpress/lib/store-currency.js +101 -0
  367. package/skills/wix-replatform/resources/rp-source-wordpress/lib/structure-bridge-reader.js +755 -0
  368. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.js +1074 -0
  369. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.test-fixture.js +149 -0
  370. package/skills/wix-replatform/resources/rp-source-wordpress/lib/woo-inventory.js +25 -0
  371. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-http.js +204 -9
  372. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-image-url.js +186 -0
  373. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-plugin-detect.js +1422 -0
  374. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-redirect-sources.js +343 -0
  375. package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-route-classifier.js +262 -26
  376. package/skills/wix-replatform/resources/rp-source-wordpress/lib/yoast-product-seo-evidence.js +272 -0
  377. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/README.md +373 -0
  378. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/advanced-custom-fields.json +101 -0
  379. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/back-in-stock-notifier-for-woocommerce.json +81 -0
  380. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/capabilities-pending-decision.json +128 -0
  381. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/cms-fallback-eligible.json +18 -0
  382. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/contact-form-7.json +104 -0
  383. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/facebook-for-woocommerce.json +44 -0
  384. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/fingerprint-aliases.json +49 -0
  385. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/free-gifts-for-woocommerce.json +94 -0
  386. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/index.json +863 -0
  387. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mailpoet.json +52 -0
  388. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/membership-for-woocommerce.json +161 -0
  389. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mwb-bookings-for-woocommerce.json +180 -0
  390. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/no-migration-needed.json +286 -0
  391. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pricing-deals-for-woocommerce.json +79 -0
  392. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/product-extras-for-woocommerce.json +120 -0
  393. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pw-woocommerce-gift-cards.json +128 -0
  394. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/requires-development.json +88 -0
  395. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seo-by-rank-math.json +80 -0
  396. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seriously-simple-podcasting.json +46 -0
  397. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/strong-testimonials.json +55 -0
  398. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/subscriptions-for-woocommerce.json +99 -0
  399. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/tablepress.json +55 -0
  400. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/the-events-calendar.json +160 -0
  401. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/ti-woocommerce-wishlist.json +81 -0
  402. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-checkout-field-editor-pro.json +60 -0
  403. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-discount-rules.json +74 -0
  404. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-product-bundle.json +75 -0
  405. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-gift-cards.json +60 -0
  406. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-other-payment-gateway.json +44 -0
  407. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-pdf-invoices-packing-slips.json +57 -0
  408. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-product-addon.json +141 -0
  409. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-progressive-discounts.json +54 -0
  410. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wordpress-seo.json +158 -0
  411. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpc-composite-products.json +97 -0
  412. package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpforms-lite.json +86 -0
  413. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/capture-store-settings.js +90 -0
  414. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge-validate.js +51 -0
  415. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge.js +103 -0
  416. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-discovery.js +1668 -33
  417. package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-plugin-inventory.js +273 -0
  418. package/skills/wix-replatform/resources/rp-source-wordpress/secret-value-patterns.v1.json +11 -0
  419. package/skills/wix-replatform/resources/rp-source-wordpress/sub-parts.json +13 -0
  420. package/skills/wix-replatform/resources/rp-target-wix/SKILL.md +446 -32
  421. package/skills/wix-replatform/resources/rp-target-wix/app-page-routing-capabilities.json +36 -0
  422. package/skills/wix-replatform/resources/rp-target-wix/domains/README.md +38 -2
  423. package/skills/wix-replatform/resources/rp-target-wix/domains/SERVICE-PLUGINS.md +322 -0
  424. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/docs-survey.json +70 -0
  425. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/domain.json +19 -9
  426. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/category.json +42 -8
  427. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/comment.json +54 -8
  428. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/post.json +66 -10
  429. package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/tag.json +38 -7
  430. package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/docs-survey.json +332 -0
  431. package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/domain.json +75 -0
  432. package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/booking.json +181 -0
  433. package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service-add-on.json +130 -0
  434. package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service.json +180 -0
  435. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/docs-survey.json +161 -0
  436. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/domain.json +15 -4
  437. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/collection.json +39 -7
  438. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/data-item.json +47 -8
  439. package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/reference.json +7 -1
  440. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/docs-survey.json +204 -0
  441. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/domain.json +17 -4
  442. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/contact.json +159 -15
  443. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/email-subscription.json +80 -12
  444. package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/label.json +48 -8
  445. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/docs-survey.json +306 -0
  446. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/domain.json +30 -7
  447. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/delivery-profile.json +171 -0
  448. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/discount-rule.json +182 -0
  449. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-applied-discount.json +129 -0
  450. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-invoice-document.json +192 -0
  451. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item-options.json +123 -0
  452. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item.json +60 -12
  453. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-merchant-note.json +112 -0
  454. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-payment.json +191 -0
  455. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-refund.json +140 -13
  456. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order.json +383 -18
  457. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/pickup-location.json +122 -0
  458. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/shipping-option.json +98 -0
  459. package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/wishlist-item.json +86 -0
  460. package/skills/wix-replatform/resources/rp-target-wix/domains/events/docs-survey.json +128 -0
  461. package/skills/wix-replatform/resources/rp-target-wix/domains/events/domain.json +38 -0
  462. package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/category.json +73 -0
  463. package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/event.json +107 -0
  464. package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/registration-form.json +89 -0
  465. package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/rsvp.json +118 -0
  466. package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/ticket-definition.json +93 -0
  467. package/skills/wix-replatform/resources/rp-target-wix/domains/forms/docs-survey.json +86 -0
  468. package/skills/wix-replatform/resources/rp-target-wix/domains/forms/domain.json +56 -0
  469. package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form-submission.json +121 -0
  470. package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form.json +134 -0
  471. package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/docs-survey.json +41 -0
  472. package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/domain.json +50 -0
  473. package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card-product.json +87 -0
  474. package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card.json +130 -0
  475. package/skills/wix-replatform/resources/rp-target-wix/domains/index.json +914 -24
  476. package/skills/wix-replatform/resources/rp-target-wix/domains/media/docs-survey.json +41 -0
  477. package/skills/wix-replatform/resources/rp-target-wix/domains/media/domain.json +14 -4
  478. package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-file.json +45 -8
  479. package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-folder.json +41 -7
  480. package/skills/wix-replatform/resources/rp-target-wix/domains/members/docs-survey.json +119 -0
  481. package/skills/wix-replatform/resources/rp-target-wix/domains/members/domain.json +13 -3
  482. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/badge.json +41 -7
  483. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/custom-field.json +82 -0
  484. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member-about.json +77 -0
  485. package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member.json +165 -13
  486. package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/docs-survey.json +41 -0
  487. package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/domain.json +46 -0
  488. package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan-order.json +137 -0
  489. package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan.json +133 -0
  490. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/docs-survey.json +7 -0
  491. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/domain.json +36 -0
  492. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/amazon-shop-connect.json +71 -0
  493. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/ebay-shop-connect.json +67 -0
  494. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/google-youtube-connect.json +72 -0
  495. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/meta-catalog-connect.json +74 -0
  496. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/pinterest-catalog-connect.json +69 -0
  497. package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/tiktok-catalog-connect.json +61 -0
  498. package/skills/wix-replatform/resources/rp-target-wix/domains/schema.json +28 -1
  499. package/skills/wix-replatform/resources/rp-target-wix/domains/seo/docs-survey.json +109 -0
  500. package/skills/wix-replatform/resources/rp-target-wix/domains/seo/domain.json +73 -0
  501. package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/item-seo-tags.json +148 -0
  502. package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/seo-pattern.json +91 -0
  503. package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/site-seo-tags.json +79 -0
  504. package/skills/wix-replatform/resources/rp-target-wix/domains/site/docs-survey.json +32 -0
  505. package/skills/wix-replatform/resources/rp-target-wix/domains/site/domain.json +49 -0
  506. package/skills/wix-replatform/resources/rp-target-wix/domains/site/entities/business-contact.json +103 -0
  507. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/docs-survey.json +203 -0
  508. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/domain.json +27 -10
  509. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/back-in-stock-request.json +119 -0
  510. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/brand.json +111 -0
  511. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/category.json +52 -8
  512. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/coupon.json +214 -10
  513. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/inventory-item.json +93 -13
  514. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-customization.json +111 -0
  515. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-tag.json +52 -10
  516. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product.json +145 -14
  517. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/promotion.json +84 -0
  518. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/review.json +178 -0
  519. package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/subscription-product.json +87 -0
  520. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/docs-survey.json +106 -0
  521. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/domain.json +64 -0
  522. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/manual-tax-mapping.json +119 -0
  523. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-group.json +94 -0
  524. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-region.json +113 -0
  525. package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-settings.json +65 -0
  526. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-author-attribution.js +165 -0
  527. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-client.js +60 -0
  528. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-duplicates.js +201 -0
  529. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-ledger.js +306 -0
  530. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-report.js +229 -0
  531. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-stage.js +55 -0
  532. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build.js +715 -0
  533. package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-media.js +567 -0
  534. package/skills/wix-replatform/resources/rp-target-wix/lib/bulk-results.js +73 -0
  535. package/skills/wix-replatform/resources/rp-target-wix/lib/contact-extended-fields.js +194 -0
  536. package/skills/wix-replatform/resources/rp-target-wix/lib/contact-import.js +133 -0
  537. package/skills/wix-replatform/resources/rp-target-wix/lib/contract-ledger.js +13 -4
  538. package/skills/wix-replatform/resources/rp-target-wix/lib/coupon-redemption-drift.js +491 -0
  539. package/skills/wix-replatform/resources/rp-target-wix/lib/customer-member.js +104 -0
  540. package/skills/wix-replatform/resources/rp-target-wix/lib/data-extension-schema.js +673 -0
  541. package/skills/wix-replatform/resources/rp-target-wix/lib/domain-knowledge.js +453 -2
  542. package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-balance-reconciliation.js +119 -0
  543. package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-build.js +566 -0
  544. package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-redemption-build.js +249 -0
  545. package/skills/wix-replatform/resources/rp-target-wix/lib/order-applied-discount-build.js +63 -0
  546. package/skills/wix-replatform/resources/rp-target-wix/lib/order-buyer.js +187 -0
  547. package/skills/wix-replatform/resources/rp-target-wix/lib/order-channel-build.js +64 -0
  548. package/skills/wix-replatform/resources/rp-target-wix/lib/order-history.js +1027 -0
  549. package/skills/wix-replatform/resources/rp-target-wix/lib/order-invoice-contract.js +90 -0
  550. package/skills/wix-replatform/resources/rp-target-wix/lib/order-payment-mapping.js +420 -0
  551. package/skills/wix-replatform/resources/rp-target-wix/lib/pricing-plan-definition.js +177 -0
  552. package/skills/wix-replatform/resources/rp-target-wix/lib/product-import.js +151 -0
  553. package/skills/wix-replatform/resources/rp-target-wix/lib/product-seo.js +723 -0
  554. package/skills/wix-replatform/resources/rp-target-wix/lib/seo-redirects.js +353 -0
  555. package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-build.js +1437 -0
  556. package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-option-stage.js +354 -0
  557. package/skills/wix-replatform/resources/rp-target-wix/lib/store-config-build.js +134 -0
  558. package/skills/wix-replatform/resources/rp-target-wix/lib/stores-category-attach.js +221 -0
  559. package/skills/wix-replatform/resources/rp-target-wix/lib/stores-inventory.js +248 -0
  560. package/skills/wix-replatform/resources/rp-target-wix/lib/stores-verification.js +86 -3
  561. package/skills/wix-replatform/resources/rp-target-wix/lib/tax-build.js +231 -0
  562. package/skills/wix-replatform/resources/rp-target-wix/lib/url-mapper-transport.js +28 -0
  563. package/skills/wix-replatform/resources/rp-target-wix/lib/value-utils.js +14 -0
  564. package/skills/wix-replatform/resources/rp-target-wix/lib/variant-correlation.js +34 -0
  565. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-build.js +166 -15
  566. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-target-spec.js +19 -2
  567. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +4079 -257
  568. package/skills/wix-replatform/resources/rp-target-wix/lib/write-verification.js +3 -0
  569. package/skills/wix-replatform/resources/rp-target-wix/scripts/docs-survey-sync.js +150 -0
  570. package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge-validate.js +40 -2
  571. package/skills/wix-replatform/resources/rp-target-wix/scripts/verify-stores.js +6 -1
  572. package/skills/wix-replatform/resources/rp-telemetry/SKILL.md +148 -8
  573. package/skills/wix-replatform/resources/rp-telemetry/lib/bi-sink.js +83 -15
  574. package/skills/wix-replatform/resources/rp-telemetry/lib/telemetry-recorder.js +1119 -215
  575. package/skills/wix-replatform/resources/rp-telemetry/lib/transcript-digest.js +318 -0
  576. package/skills/wix-replatform/resources/rp-telemetry/scripts/meter-from-audit.js +2 -2
  577. package/skills/wix-replatform/resources/rp-telemetry/scripts/rp-telemetry.js +106 -4
  578. package/skills/wix-replatform/resources/rp-website-continuation/SKILL.md +94 -0
  579. package/skills/wix-replatform/scripts/census-reconciliation.js +25 -0
  580. package/skills/wix-replatform/scripts/cms-fallback.js +89 -0
  581. package/skills/wix-replatform/scripts/discovered-census.js +26 -0
  582. package/skills/wix-replatform/scripts/discovered-set-status.js +33 -0
  583. package/skills/wix-replatform/scripts/entity-qa-attestation.js +130 -0
  584. package/skills/wix-replatform/scripts/execution-health-check.js +415 -0
  585. package/skills/wix-replatform/scripts/execution-manifest-validate.js +1 -1
  586. package/skills/wix-replatform/scripts/execution-state-prepare.js +1 -1
  587. package/skills/wix-replatform/scripts/expectations-ledger.js +44 -0
  588. package/skills/wix-replatform/scripts/import-contract-verify.js +183 -0
  589. package/skills/wix-replatform/scripts/internal-link-audit.js +90 -0
  590. package/skills/wix-replatform/scripts/item-seo-tags-verify.js +205 -0
  591. package/skills/wix-replatform/scripts/migration-completion.js +76 -0
  592. package/skills/wix-replatform/scripts/orchestration-preflight.js +21 -2
  593. package/skills/wix-replatform/scripts/orchestration-route.js +32 -1
  594. package/skills/wix-replatform/scripts/orchestration-state.js +146 -10
  595. package/skills/wix-replatform/scripts/read-back-gate.js +175 -0
  596. package/skills/wix-replatform/scripts/replatform-console.js +146 -0
  597. package/skills/wix-replatform/scripts/replatform.js +436 -0
  598. package/skills/wix-replatform/scripts/source-secrets.js +77 -0
  599. package/skills/wix-replatform/scripts/url-redirects.js +286 -0
  600. package/skills/wix-replatform/scripts/user-action.js +58 -0
  601. package/skills/wix-replatform/scripts/website-handoff-generate.js +64 -0
  602. package/skills/wix-replatform/scripts/wix-dashboard-url.js +54 -0
  603. package/skills/wix-replatform/scripts/wix-destination.js +143 -0
  604. package/skills/wix-replatform/scripts/wix-headless-scaffold.js +762 -0
  605. package/skills/wix-vibe-headless/SKILL.md +7 -3
  606. package/skills/wix-vibe-headless/install/deploy.cjs +61 -34
  607. package/skills/wix-vibe-headless/platforms/base44.md +1 -0
  608. package/skills/wix-vibe-headless/references/donations/INSTRUCTIONS.md +88 -0
  609. package/skills/wix-vibe-headless/references/donations/app/rest/wix-donations.js +112 -0
  610. package/skills/wix-vibe-headless/references/donations/seed/SEED.md +11 -0
  611. package/skills/wix-vibe-headless/references/donations/tests/donations.test.mjs +258 -0
  612. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +5 -2
  613. package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +4 -1
  614. package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +67 -3
  615. package/skills/wix-vibe-headless/references/shared/app/rest/wix-client.js +4 -3
  616. package/skills/wix-replatform/resources/rp-target-wix/SPEC-product-reviews-and-blog-authorship.md +0 -330
@@ -23,6 +23,97 @@ This skill turns the schema, mapping, and setup decisions into implementation fi
23
23
  Prefer the machine-readable artifacts above. Markdown review files are secondary renderings
24
24
  for humans, not the primary codegen contract.
25
25
 
26
+ ## Dependency readiness contract
27
+
28
+ Copy `lib/dependency-readiness.js` and its shared dependencies into the generated runtime.
29
+ Bind the execution manifest to the approved mapping plan with `mappingPlanDigest`.
30
+ Each import task declares canonical `entityRefs`, the domain knowledge's `dependencyRefs`,
31
+ and matching `dependencyEdges`. Every edge has exactly one producer: an import task,
32
+ all bound setup step IDs, or an approved census disposition. Run `validateManifestForProject(projectDir)` after writing the candidate manifest; it loads the real census adapter and calls `bindDependencyProducers`. Validate the complete graph;
33
+ missing, unreviewed, multiply bound or cyclic dependencies stop codegen.
34
+
35
+ Declare `sourceBindings: [{ ref, sourceEntity, keyField, keyPrefix, manifestPath, extractPath }]`
36
+ on tasks so the runtime can bind source keys to the complete extract and its manifest. Bind every contributing source separately, including multiple census/source populations targeting one ref; the pass records each manifest, extract and declared read plan without overwriting another source. Stamp attempts with the owning task ID and crosswalks with that binding’s source entity and raw source ID. Use the canonical target `domain/entity` on attempt/crosswalk rows; the gate also recognizes the existing singular, dotted and hyphenated forms for the same ref. A key outside that ref’s current source population, a key from another entity in a combined task, or mismatched evidence identity cannot resolve.
37
+ Use `dependencySourceBindings` for excluded source-key associations. Preserve the reviewed
38
+ mapping's census links rather than guessing associations from names. Setup-produced edges
39
+ always use `task-complete`; use `per-record` only where target knowledge explicitly permits
40
+ it for an import or excluded import dependency.
41
+
42
+ Import the gate's `startImportRun`, `bindTask`, `evaluateTask`, `gateBatch` and
43
+ `finishTaskPass`. For EAV extracts, source bindings additionally name the existing project-relative `readPlanPath`; the gate reuses the shipped EAV completeness and file-digest validator rather than treating a missing snapshot total as empty. Create one import run, bind each task, and create its Wix client from the
44
+ returned capability. Immediately before every batch, including each selective/resume
45
+ path, evaluate the task and pass the source-keyed records through `gateBatch`.
46
+ Each `gateBatch` input is `{ sourceStableKey, transform: async ({ resolveReference, resolveTaskReference, resolveSetupReference }) => payload }` (or `{ sourceStableKey, payload }` when it needs no target references). Transforms receive `resolveReference(ref, sourceStableKey, path)` for per-record edges and `resolveTaskReference(ref, sourceStableKey, path)` for task-complete import edges only after a complete current producer pass (empty producers cannot resolve a key). This separate resolver preserves whole-task blocking on any partial producer. They also receive `resolveSetupReference(ref, sourceStableKey, path)` after setup passes. They do not read a crosswalk or choose target IDs themselves. Use concrete indexed paths for repeated order fields. Send only
47
+ the returned `send` payloads, with each request's `sourceStableKey` and that batch's
48
+ `readinessGrant`. Use `writers.bindRecordClient(taskClient, { readinessGrant: grant, sourceStableKey })` for existing writer composites, or `writers.withReadinessGrant(request, { readinessGrant: grant, sourceStableKey })` for individual builders. These retain trusted read/auxiliary marks. A valid grant never authorizes a raw or altered writer request; object-spreading a marked request loses its authority. Principal payloads still match every resolved reference. Closed product patch builders bind the current record’s product and compare references under each changed top-level field; omitted fields retain the existing product state. Only the closed media/payment/extended-field auxiliary builders omit those principal paths. For standard product stock, get the current product through the bound record client, call `registerImportInventoryScope(recordClient, canonicalProduct, targetProduct)`, and pass that record client to `storesInventory.reconcileInventory`. The same-record principal write or current site-bound crosswalk establishes product ownership; exact variant-choice matches, requested stock and actual location/item reads bound the composite. This preserves standard quantity/location mappings without expanding quick Shopify’s separate exception. Persist/report the returned deferrals without substituting empty IDs.
49
+ The gate appends its own deferred attempt rows; do not append a duplicate deferred outcome. The runner appends started and terminal rows for attempted sends and persists confirmed crosswalks through the shared writers. Call `finishTaskPass` with the actual selection mode only after the pass finishes. Selective
50
+ work never establishes a full/empty pass. The append-only `state/readiness/task-passes.ndjson`
51
+ is evidence; `execution/dependency-readiness.json` is a diagnostic projection, not authority. Prerequisite deferrals remain current recovery work after admission, `started`, uncertain outcomes or failed retries. Only a current-site confirmed `imported`/`skipped_already_imported` outcome for the same task/key clears that lineage; admission is not completion. The runtime updates this task-scoped recovery index from journal deltas, and completion uses the same reducer.
52
+
53
+ `--describe` must include `dependencyReadiness.tasks` with each task's ID and exact
54
+ `{ ref, mode }` edges. Run `import-contract-verify` against the manifest. Generated importers
55
+ must obtain capabilities only through the dependency gate: no setup/quick/probe issuer,
56
+ caller-supplied phase, raw request escape, or direct execution-capability import. Site reconciliation uses `reconcileSiteEvidence({ taskBinding, readBack })`; reuse the existing unresolved-write reader/comparison against the recorded write. The callback returns `{ result, matchesRecordedWrite }` with the actual unchanged marked-read response from this site's registered client; a boolean or hand-built response never restamps evidence. Tasks with
57
+ no dependencies still require their task capability and batch grant. For source orders call `orderShippingReadiness` from `lib/order-reference-readiness.js` with the bundled, verified `selectOrderReferenceMapping()` conclusion and the reviewed source delivery/pickup mapping as its `{ logistics }` option. Preserve source tax amounts/breakdown separately; omission of a target tax-group ID never permits losing source tax values. A missing or inconclusive conclusion returns a deferred reference-fidelity result; preserve it through `gateBatch` so the journal and existing mapping-gap ledger expose the affected records. No stored API response or accepted source ID proves dashboard fidelity. Always carry source currency explicitly: Import Order may store an empty currency rather than applying the documented site default. Keep the existing
58
+ read-back gate below: readiness does not replace write verification or authorize resends.
59
+
60
+ ## Generated read-back gate contract
61
+
62
+ For `order`, `member`, `contact` and `product`, reuse the reviewed pure `buildWrite(record)`
63
+ module and the entity decision's copied `readBackContract`. Generate no new omission policy or
64
+ comparison rules. Each mapping row retains its exact `sourcePaths[]`, canonical `writePath` and
65
+ `readPath` or `unverifiableReason`, with `fieldDispositions[].mappingRef` pointing to that row.
66
+ Copy the bundled read-back libraries with the shared runtime; importing a CLI must not execute
67
+ an extract or write. Other entities call `declaration(entity, { verificationKind, reason })`
68
+ from `lib/read-back-gate.js` and retain that explicit coverage declaration in their manifest.
69
+ Persist it with `recordDeclaration(projectDir, runId, entity, { verificationKind, reason })`
70
+ from `lib/read-back-summary.js`; the existing gate artifact then supplies completion and CLI
71
+ coverage reporting. A non-gated declaration never replaces gated-domain or earlier gate evidence.
72
+
73
+ Before the first target write and every resume, load the existing
74
+ `execution/read-back-inventory/<entity>.json` and call `ReadBackGate.prepare(inventory)`.
75
+ The inventory is built by the streaming `scripts/read-back-gate.js inventory` command using the
76
+ same builder; a missing extract means the check was not run. Validation requires the reviewed
77
+ mapping, decision, resolution and inventory bindings. Missing, stale or incomplete field
78
+ accounting holds the entity. Populated omissions require the existing identified mapping
79
+ approval user action, including all four current references; 1-click cannot approve them.
80
+
81
+ Wire `executeCanaries(getRecord)` before the entity's bulk import. The deterministic schedule
82
+ covers every populated intended write path, with a default ceiling of ten records; raising it
83
+ to at most thirty requires the recorded user decision. Canaries run serially, stop at the first
84
+ failed or unknown verification, and are excluded from subsequent create calls. Keep normal bulk
85
+ batching and concurrency after they pass. Call `verifySample({ record, sourceId, targetId,
86
+ index, isLast })` after every 200th record and the final record, using one-based stable source
87
+ order and a confirmed target ID. Synchronize at those sample boundaries; a failed sample stops
88
+ new batches and requires `reconcileInterval(getRecord, targetIdFor)` before continuation.
89
+ A partial final batch still performs its last-record check.
90
+
91
+ An uncertain send is reconciled through the gate's existing identity lookup before any retry.
92
+ Every lookup page and by-ID read must use the gate's budgeted read proxy (`reads.wix`), including
93
+ product catalog sweeps and contact candidate reads; a failed or truncated lookup is unknown,
94
+ never permission to send again. Preserve the gate and write-verification evidence across
95
+ resumes. Never rewrite an imported order. Contact, product and member repairs use the gate's
96
+ single-attempt-per-contract rule, unchanged revision marker and existing update path; a member
97
+ repair writes its contact with that contact's current revision. Never overwrite a record edited
98
+ since import.
99
+
100
+ When `ensureMember` removes a phone through its approved phone-collision guard, compare against
101
+ the request actually sent and preserve the writer's `guard-dropped` paths in the gate summary.
102
+ Canaries record it automatically. For bulk writes, pass each returned `findings` array to
103
+ `gate.recordGuard(sourceId, findings)` (it persists only the recognized collision finding), or
104
+ as `verifySample({ ..., findings })` on a sampled record; samples and `reconcileInterval` then
105
+ exempt only that record's guarded phone path. A missing target phone is never treated as a guard.
106
+ Do not turn that guard into `notMigrated`, an observed Wix loss or a fabricated placeholder
107
+ phone. Pending product-media processing is unknown until bounded settlement resolves; it cannot
108
+ be classified as a permanent dropped field or used to approve an omission.
109
+
110
+ Completion must retain `execution/read-back-gate.json`. Call the existing completion-report
111
+ writer so its read-back producer supplies actual per-entity statuses, path counts, pending
112
+ settlements, observed losses, approved omissions and merchant acknowledgements, plus recovery
113
+ operator actions. Do not infer a pass from a successful HTTP write or emit a separate success
114
+ summary. A held, not-run or unknown gate keeps the run incomplete, and missing acknowledgement
115
+ for an approved omission remains a pre-launch task.
116
+
26
117
  ## Source read contract
27
118
 
28
119
  Generating a correct reader requires platform-specific knowledge — auth model,
@@ -38,6 +129,449 @@ generated code, not deferred to execution.
38
129
  Adding a new source platform therefore requires no change to this skill: a new
39
130
  `rp-source-<platform>` adapter is enough.
40
131
 
132
+ ### Reading plugin-provided entities
133
+
134
+ Entities that came from a source plugin carry extra `sourceMeta` the generated reader must
135
+ respect. These are read mechanics, not mapping decisions:
136
+
137
+ - **`origin: "embedded"`** — the records live inside a property of a parent record
138
+ (`propertyPath`) on an already-fetched route (`embeddedIn`). Extract them from the parent
139
+ fetch. Do **not** issue a second request per parent; some of these plugins have no route of
140
+ their own at all.
141
+ - **`requiresParent`** — a sub-collection route parameterized by the parent id
142
+ (`.../{id}/notes`). Read the parent first and iterate; order this after the parent entity in
143
+ the dependency graph.
144
+ - **`channel: "plugin-rest-child"`** — a sub-resource on a CORE collection this plugin does
145
+ not itself profile (e.g. WooCommerce orders), so it cannot use `requiresParent` (which
146
+ points at another entity declared in the same profile). `route` carries a `{parentId}`
147
+ template (`/wc/v3/orders/{parentId}/notes`); `parentRoute` names the collection route to
148
+ iterate for real parent ids. Unlike discovery's `sampleChildEntities` (which checks only a
149
+ representative few parents), the generated reader must iterate **every** parent record from
150
+ `parentRoute` and fetch its child route — that full iteration is what actually migrates the
151
+ data; the discovery-time sample only confirmed the shape is real.
152
+ - **`context`** — when the adapter says view and edit contexts return different data, request
153
+ the one the entity declares. Requesting only one context can silently halve the result set.
154
+ - **Per-plugin credentials** — some plugin APIs use their own key system rather than the
155
+ platform credential. Load them from project-local config like any other secret, and fail
156
+ loudly with the missing key name rather than falling back to unauthenticated reads.
157
+ - **`recognized: false`** (derived entity) — no plugin-specific read contract exists; read it as an ordinary
158
+ `wp/v2`-style collection using the shared transport. Do not invent plugin-specific handling
159
+ for it.
160
+ - **`requestMethod`/`requestBody`** — the entity's real read path is not a plain GET
161
+ collection; it is a declared non-GET method with a JSON body template. The
162
+ generated reader must issue that exact method/body, not fall back to a GET.
163
+ - A body value of the form `"$SAMPLED_IDS:<route>"` names another route whose record count is
164
+ UNKNOWN and possibly unbounded (e.g. an entire product catalog) — never resolve it against
165
+ one fixed id list fetched in a single request, at discovery time or at execution time,
166
+ because `<route>` may hold far more ids than fit in one page or one request body. The
167
+ generated reader must instead: (1) paginate `<route>` to exhaustion (every page, not a
168
+ capped sample); (2) split the collected ids into bounded batches (mirror
169
+ `lib/sampled-ids-batch.js`'s default of 50 ids per batch — do not send an unbounded id list
170
+ in one request); (3) issue one request per batch against this entity's own route, with the
171
+ placeholder resolved to just that batch's ids; (4) normalize each batch's RAW, unmodified
172
+ response (envelope resolution, then `responseFragmentGroupSize` reassembly below — never
173
+ pre-coerce a non-array raw response to an empty list before normalizing it, since an
174
+ enveloped response is legitimately an object, not an array, until unwrapped) and merge the
175
+ batches, deduplicating by `recordKeyField` (default `id`) — the same record can otherwise
176
+ appear once per batch it happened to match; (5) stop and record the run as an explicit
177
+ failure/deferred outcome on the first page or batch that cannot be read OR whose normalized
178
+ result is not a valid array shape (a batch that doesn't match the declared envelope/fragment
179
+ contract must defer the whole entity, not be counted as a legitimate empty batch — that
180
+ would produce an exact-looking but false zero) — never emit a partial/undercounted or
181
+ falsely-exact result as if it were complete. `rp-source-wordpress/lib/sampled-ids-batch.js`
182
+ implements this exact algorithm (`collectAllIds`, `queryInBatches`) for `wp-discovery.js`
183
+ itself; a generated reader for another platform must reproduce the same five steps, not
184
+ invent a different policy.
185
+ - **`responseFragmentGroupSize`** — the response arrives as N separate flat single-key array
186
+ entries per logical record instead of one object (a plugin bug some source APIs have). The
187
+ generated reader must chunk the raw array into groups of N and merge each group with
188
+ `Object.assign` into one record before any mapping/transform runs or any count is emitted —
189
+ per batch when combined with a `$SAMPLED_IDS` requestBody above, mirroring
190
+ `wp-discovery.js`'s `inspectEntity`/`normalizeResponseRecords` handling of the same field.
191
+
192
+ ### `structure-bridge-plugin` entities (`channel: "db-only"`)
193
+
194
+ **New bound entities use the project-local plan contract in
195
+ `rp-source-wordpress/PLUGIN-READ-PLANS.md`.** Carry `sourceMeta.extractionPlan` literally
196
+ from canonical discovery through mapping. A profile is optional; absent/invalid plans
197
+ remain typed deferrals. Never suffix physical table names or guess artifact filenames.
198
+ Use `lib/generated-plugin-handoff.js`'s `emitPluginHandoff(projectDir)` to emit the small
199
+ checked dispatch and importer/extractor entrypoints; preserve existing native readers,
200
+ target handlers, selectors, ledger/idempotency and authorization/mute/setup preflight in
201
+ the adapter described there. The emitter refuses existing non-template entrypoints before
202
+ writing any destination. Deliberately adapt their implementation before replacing them;
203
+ preserve heartbeat, self-check and dependency ordering as well as target writes. The
204
+ adapter's pure `describeImport()` supplies its actual ledger/selector configuration and
205
+ `entityOrder`; the wrapper checks that descriptor instead of asserting invented capabilities.
206
+ Preflight runs once per import run before its first ready write.
207
+ Run the committed `import-contract-verify.js` with the intended selection before any
208
+ source/target I/O. The wrappers enforce preparation/full readiness and selected source
209
+ prerequisites; discovery samples are preview only. Keep unrelated native reruns available.
210
+ Include plan/evidence/assembly/extractor/helper inputs in existing freshness metadata.
211
+
212
+ **Legacy caller compatibility follows below.** New discovery emits bindings for profiled
213
+ entities too; it does not add a wrapping pass for old sidecars. Existing legacy readers
214
+ retain their signature, pagination and complete-identity behavior. Do not use the legacy
215
+ filename convention below as a fallback for a new selected entity with a missing binding.
216
+
217
+ An entity whose `blocked[].fulfillment.kind` is `structure-bridge-plugin` has no REST
218
+ route at all — its declared readiness (self-test passing, checked via
219
+ `blocked-data-handlers.js` at planning time, per `rp-mapper`) means it is safe to treat
220
+ as a normal candidate target, but its generated reader is genuinely different from every
221
+ other reader this skill generates: it reads through `wix-wp-plugin-v2`'s `/query` or
222
+ `/query/admin-key` route, not the platform adapter's own transport. This is **not**
223
+ resolved through `blocked-data-requests.js`'s one-shot `attemptFulfillment()`/snapshot
224
+ mechanism the way `csv-upload` is — that mechanism exists for a small, static,
225
+ human-provided document; a `structure-bridge-plugin` table is a live, potentially large,
226
+ paginated dataset, so it gets a normal incremental reader like any REST entity, just
227
+ reading from a different transport.
228
+
229
+ - **Vendor `rp-source-wordpress/lib/wix-wp-plugin-v2-client.js` into `src/lib/`**, the
230
+ same way an ordinary WordPress reader vendors `wp-http.js`. Do not re-implement
231
+ `discoverStructure()`/`queryStructure()` or their response validation.
232
+ - **Vendor `rp-source-wordpress/lib/structure-bridge-reader.js` beside it and call
233
+ `extractStructureBridgeEntity({ dataDir, entity, table, transport, expectEav })` once per
234
+ entity. Do not hand-write the page loop, checkpoint, reconciliation or manifest.** It
235
+ implements every rule below for both variants (single request and EAV read plan) and is
236
+ covered by `tests/source-wordpress/structure-bridge-plugin-generated-reader-test.js`.
237
+ `transport` is `{ httpClient, siteBaseUrl, auth, onSourceRead }` built from the credential
238
+ rule below; set `expectEav: true` when the entity's discovery markdown reports an `eavPair`.
239
+ It returns rather than throws: `status` is `promoted`, `reused` or `deferred` (with `code`
240
+ and `reasons`), plus `bestEffort` and `redactions`. Continue with the other entities after
241
+ a `deferred` one. Each deferral is also written to
242
+ `data/source-extract/<entity>.deferred.json` (reason, affected rows, resumable files,
243
+ prerequisites, resolution check and a **Copy prompt for Import Operator**); a later
244
+ promotion marks it `resolved`. At completion, spread
245
+ `deferredExtractionReportInput(await collectDeferredExtractions(dataDir))` into the
246
+ `createCompletionReport()` input (merge its `operatorActions` and `statuses` with the
247
+ importer's own), so the deferral reaches the handoff as a **Requires Import Operator**
248
+ action. Pass `restart: true` only when the record says `restartRequired`. It never promotes an EAV read whose
249
+ plan has `droppedIdentityColumns`, and never sets `acknowledgedDroppedColumns` (complete
250
+ identity). The rest of this section is the contract that file implements.
251
+ - **Generate the same bridge credential selection discovery used.** When
252
+ `WMH2_MIGRATION_KEY` is configured, call `queryStructure()` with
253
+ `auth: { type: 'migration-key', migrationKey, siteId, migrationId, timeoutMs }` so the
254
+ vendored client uses signed `/query`; preferring the key when it is available avoids
255
+ capability drift on the `manage_woocommerce`-gated route. Otherwise, when WooCommerce is
256
+ present and a WordPress REST credential is configured, pass the generated `httpClient`
257
+ and omit `auth` to use `/query/admin-key`. Keep one stable `migrationId` for the
258
+ extraction process and let the client create a fresh request `jti` per page. If neither
259
+ usable bridge credential exists, halt to needs-user before opening the output file.
260
+ - **The structureRequest is fixed, not derived at codegen time.** Read it verbatim from
261
+ the machine-readable artifact discovery already persisted:
262
+ `data/wp-discovery/wix-wp-plugin-v2--<table>.structure-request.json`. **Never re-call
263
+ `discoverStructure()` and rebuild a fresh default at codegen time**, even when the
264
+ fulfillment declares no `sampleStructureRequest` of its own — the table's live schema
265
+ could have changed between the discovery run and this codegen run, and re-deriving
266
+ independently is exactly the drift this artifact exists to prevent; the sample and the
267
+ full extraction must run the literal same request, not two requests that happen to
268
+ usually agree. If the artifact does not exist (discovery never ran for this table, or
269
+ discovery deliberately skipped building one — e.g. a >50-column table with no declared
270
+ `sampleStructureRequest`, or a zero-column schema), this entity cannot be extracted
271
+ yet: fail clearly, name the missing artifact path, and stop — do not invent a request.
272
+ - **Page at the plugin's maximum, 200 — not discovery's small sample limit.** Loop: call
273
+ `queryStructure({ httpClient, auth: bridgeAuth, structureRequest, cursor, limit: 200,
274
+ onSourceRead })`, append every returned row, and continue only while
275
+ `pagingMetadata.hasNext` is true. `bridgeAuth` is `undefined` for the admin-key path and
276
+ the migration-key auth object above for the signed path.
277
+ - **Pass `onSourceRead` on every `queryStructure` call, and file the totals once at the end**:
278
+
279
+ ```js
280
+ const totals = { discovery_queries: 0, scoped_queries: 0, tier_refusals: 0, redacted_values: 0 };
281
+ const tables = { discovery_tables: new Set(), scoped_tables: new Set() };
282
+ const onSourceRead = (counts) => {
283
+ for (const k of Object.keys(totals)) totals[k] += counts[k] || 0;
284
+ for (const t of counts.discovery_tables || []) tables.discovery_tables.add(t);
285
+ for (const t of counts.scoped_tables || []) tables.scoped_tables.add(t);
286
+ };
287
+ // ...after the read loop, once:
288
+ // node <rp-telemetry>/scripts/rp-telemetry.js source-read '<totals JSON>' --project <dir>
289
+ ```
290
+
291
+ The client derives the counts; the generated reader only accumulates and files them. Omitting
292
+ the hook does not error — it just makes the run report that it read no key/value data, which
293
+ is indistinguishable from a run that genuinely did not. Never abort an extraction because
294
+ telemetry failed.
295
+ - **Check `redactionMetadata.redactedValueCount` on every page.** Non-zero means a returned
296
+ value matched a credential shape and arrived as `[REDACTED:secret-shaped]`. Write the row
297
+ through as-is — the redaction is the plugin's decision, not something to work around — and
298
+ surface the count and column names in the extraction's own report. It is a finding about the
299
+ source site: something there keeps a secret in that column.
300
+ - **A key/value table has TWO artifacts, and the second one is where the values come from.**
301
+ `…--<table>.structure-request.json` selects key names only; running it verbatim extracts no
302
+ values at all. The values come from `…--<table>.eav-read-plan.json`, which discovery writes
303
+ beside it:
304
+
305
+ ```jsonc
306
+ {
307
+ "table": "postmeta",
308
+ "distinctKeyCount": 213, // across every pair
309
+ "keysTruncated": false,
310
+ "expectedBatches": 5, // total across all pairs
311
+ "pageLimit": 200,
312
+ "identityColumns": ["meta_id", "post_id"], // ride every batch, so values attribute to a record
313
+ "orderColumn": "meta_id", "orderByIsUnique": true,
314
+ "pairs": [ { "keyColumn": "meta_key", "valueColumn": "meta_value", "distinctKeyCount": 213,
315
+ "batches": [ { "batchIndex": 0, "keys": ["…"],
316
+ "structureRequest": { /* runnable as-is */ } } ] } ]
317
+ }
318
+ ```
319
+
320
+ **Run every `pairs[].batches[].structureRequest` verbatim, page each at `pageLimit`, and
321
+ concatenate into the same NDJSON the single-request path writes.** The batching is already
322
+ done — each batch pins at most the plugin's 50-key cap, carries the table's identity columns
323
+ so a value can be attributed to the record it belongs to, and selects only the value column
324
+ belonging to the key it pins. Never merge batches, never add another pair's value column, and
325
+ never author a scoped request yourself: an unpinned value column is refused with a 400 that
326
+ looks exactly like naming a nonexistent column, so a hand-built request fails in the least
327
+ diagnosable way available.
328
+
329
+ **The EAV checkpoint replaces the single-request checkpoint below — do not write both.** The
330
+ single-request shape (one cursor, one snapshot total, one request hash) cannot resume a
331
+ multi-batch plan: on restart there is no way to tell which batches already finished. Every
332
+ property that shape has is still needed, but *per active batch*:
333
+
334
+ ```jsonc
335
+ { "planFile": "…--postmeta.eav-read-plan.json",
336
+ "planHash": "6cbfc47c83d83264", // the plan's own planHash, checked on resume
337
+ "activeBatch": { // null between batches and when the plan is finished
338
+ "batchIndex": 2,
339
+ "cursor": "…", // opaque; null before the first page of this batch
340
+ "hasNext": true, // REQUIRED — see below
341
+ "rowsRead": 88,
342
+ "snapshotTotal": 412 // this batch's first page total, then carried unchanged
343
+ },
344
+ "completedBatches": [ // one entry per FINISHED batch, no duplicates
345
+ { "batchIndex": 0, "rowsRead": 412, "snapshotTotal": 412 },
346
+ { "batchIndex": 1, "rowsRead": 130, "snapshotTotal": 130 }
347
+ ],
348
+ "rowsWritten": 542 }
349
+ ```
350
+
351
+ - `hasNext` is required for the same reason the single-request checkpoint requires it: the
352
+ plugin returns `cursors.next: null` on the last page, so `cursor: null` alone cannot
353
+ distinguish "this batch has not started" from "this batch just finished its final page".
354
+ - `planHash` replaces `structureRequestHash`, and for the same reason: discovery can re-run
355
+ and rewrite the plan — the site gained or lost keys — and resuming a half-finished
356
+ extraction into a different batch layout silently mixes two reads. `reconcileEavReadPlan`
357
+ rejects a checkpoint whose `planHash` does not match the plan on disk; on a mismatch,
358
+ restart the extraction rather than resuming it.
359
+ - `batchIndex` is unique across the whole plan, including across pairs, so resume means "skip
360
+ every `completedBatches` index, then restart `activeBatch` from its cursor".
361
+ - ⚠️ Never record a batch in `completedBatches` twice. A resume that re-runs a finished batch
362
+ duplicates its rows in the output; reconciliation rejects the duplicate rather than letting
363
+ a doubled file promote.
364
+
365
+ **Which keys?** The plan contains every key the site actually has, which is the complete
366
+ extraction and the right default. A mapping step may narrow `batches` to the keys that matter;
367
+ because the plan is a persisted, diffable artifact, that narrowing is reviewable instead of
368
+ buried in generated code. Regenerate the plan rather than editing requests inside it.
369
+
370
+ **Reconcile before promoting the `.tmp` file**, with `reconcileEavReadPlan(plan, {
371
+ planHash: checkpoint.planHash, batches: completedBatches, rowsWritten })` from
372
+ `wix-wp-plugin-v2-client.js`. ⚠️ `planHash` is required and must come from the CHECKPOINT —
373
+ passing the plan's own makes the check compare the plan to itself. It returns the
374
+ problems; a non-empty result must fail the extraction loudly. "It did not throw" is not
375
+ completeness — it catches four things a partial run gets wrong silently:
376
+
377
+ - a batch the plan expects never ran, named by index rather than merely counted;
378
+ - a batch that read fewer rows than its own first page promised, i.e. its page loop stopped
379
+ early;
380
+ - `rowsWritten` disagreeing with the sum of what the batches actually read;
381
+ - `keysTruncated: true`, meaning key discovery hit its cap, so the key list itself is
382
+ incomplete and the extraction cannot be called whole-table;
383
+ - a non-empty `droppedIdentityColumns`, meaning the table is wider than the 50-column select
384
+ cap so **every extracted row is missing columns**. Narrow the mapping and regenerate the
385
+ plan with an explicit column choice, or set `acknowledgedDroppedColumns: true` on the plan
386
+ to record that the omission was chosen. It does not pass by default, because a per-record
387
+ incompleteness nobody decided on is the kind that reaches an import unnoticed.
388
+
389
+ ⚠️ It also reports `orderByIsUnique: false` — a table with no primary or unique column is
390
+ paged on a repeating sort key, so rows can shift between pages. That extraction is
391
+ best-effort and must be reported as such, never as complete.
392
+
393
+ **If the plan is absent for a table whose schema reports an `eavPair`, stop.** That means key
394
+ discovery failed during the discovery run; the entity's markdown says so. Do not fall back to
395
+ the key-names request and report the result as an extraction.
396
+ - **Write through a temporary file, promoted atomically only after reconciliation
397
+ passes — never write `data/source-extract/<entity>.ndjson` directly while the read is
398
+ still in progress.** Append to `data/source-extract/<entity>.ndjson.tmp` and, after
399
+ each durable append, checkpoint `data/source-extract/<entity>.ndjson.tmp.cursor.json`.
400
+ ⚠️ **For a key/value table this is the EAV checkpoint shape above, not the single-request
401
+ shape here** — the two are alternatives, never both. The single-request shape is
402
+ (`{ "cursor": "...", "hasNext": true|false, "rowsWritten": N, "snapshotTotal": M,
403
+ "structureRequestHash": "..." }`). `snapshotTotal` is the first page's
404
+ `pagingMetadata.total`, captured once and carried through every subsequent checkpoint
405
+ unchanged. `structureRequestHash` is a hash of the exact structureRequest this read
406
+ started with (the JSON read from the sidecar at the start of THIS read, not re-read on
407
+ every page) and is likewise carried unchanged through every checkpoint of the same
408
+ read. wix-wp-plugin-v2's cursor is an opaque, HMAC-signed token encoding a frozen
409
+ snapshot — unlike an ordinary REST reader's
410
+ `page`/`offset`, it cannot be reconstructed from row count alone, so the checkpoint is
411
+ the only way to resume correctly. **`hasNext` is required, not cosmetic: the plugin
412
+ returns `cursors.next: null` on the last page precisely because there is nothing
413
+ further to fetch, so a checkpoint's `cursor: null` is ambiguous between "never started"
414
+ and "just finished the final page" unless `hasNext` is also recorded.**
415
+ **`structureRequestHash` is required for the same reason a final file needs a
416
+ manifest: discovery can re-run and rewrite the sidecar to a different approved query
417
+ while a `.tmp` from an earlier read is still in progress, and the plugin's cursor
418
+ encodes only an offset and a snapshot total — it has no way to detect that the query
419
+ underneath it changed. Without this check, a resume would silently splice rows queried
420
+ under the old request with a continuation queried under the new one, and the file would
421
+ end up permanently mislabeled by whichever request happened to be current when it was
422
+ promoted.** On resume, first hash the CURRENT sidecar and compare it against the
423
+ checkpoint's `structureRequestHash`:
424
+ - If they differ, the approved query changed since this `.tmp` was started. DELETE both
425
+ the `.tmp` file and its checkpoint and restart the read from `cursor: null` into a
426
+ freshly created `.tmp`, using the current sidecar's request and hash — do not attempt
427
+ to salvage or continue rows written under the old request.
428
+ - If they match, compare the `.tmp` file's own line count against the checkpoint's
429
+ `rowsWritten`:
430
+ - If they match and `hasNext` is `true`, continue paging from the saved `cursor`,
431
+ using the same structureRequest this read started with.
432
+ - If they match and `hasNext` is `false`, the read itself is already complete — skip
433
+ straight to the completion reconciliation below (comparing `rowsWritten` against
434
+ the checkpoint's own `snapshotTotal`, with no further plugin call) and promote.
435
+ **Do not call `queryStructure` again with `cursor: null` in this case** — that
436
+ re-issues page one and appends a full duplicate copy onto an already-complete temp
437
+ file.
438
+ - If they do not match (a prior interrupted write), DELETE both the `.tmp` file and
439
+ its checkpoint and restart the read from `cursor: null` into a freshly created
440
+ `.tmp` file — resuming a mismatched cursor against stale content, or restarting
441
+ from `cursor: null` while appending onto the existing (possibly stale or partial)
442
+ file, both duplicate rows.
443
+
444
+ Reconciliation passing is not the last check. **Immediately before promoting — even on
445
+ a single uninterrupted run that never needed to resume — re-hash the current sidecar
446
+ one more time and compare it against the checkpoint's carried `structureRequestHash`.**
447
+ All of the pages in this read could have completed correctly under request A while
448
+ discovery replaced the sidecar with request B somewhere in the middle; nothing earlier
449
+ in this contract catches that, because the resume-time hash check above only ever runs
450
+ when a read is picked back up after being interrupted, not at the end of one that ran
451
+ straight through. If the hashes differ at this final check, do **not** promote — leave
452
+ the `.tmp` file and its checkpoint in place (recording `structureRequestHash: A`,
453
+ exactly as it was throughout the read) and report the entity as a deferred extraction,
454
+ the same as any other unresolved page error; the next run's resume logic will see the
455
+ mismatch and correctly restart under the now-current request. Writing the manifest with
456
+ request A's hash after promoting under request B would make the bookkeeping honest but
457
+ would still let request A's data reach `data/source-extract/<entity>.ndjson` and
458
+ everything downstream of it after B became the approved query — the check has to block
459
+ the promotion itself, not just label what already happened.
460
+
461
+ ⚠️ **For a key/value table, substitute `planHash` for `structureRequestHash` throughout the
462
+ paragraph above.** The hazard is identical — discovery can replace the plan mid-read, and only
463
+ a final re-hash catches a read that ran straight through under the old one — but the artifact
464
+ is the read plan, so re-derive the hash of `…--<table>.eav-read-plan.json` and compare against
465
+ the checkpoint's `planHash`. **Use `eavReadPlanHash(plan)` from
466
+ `wix-wp-plugin-v2-client.js` — never `JSON.stringify` and hash it yourself.** It excludes the
467
+ plan's own `planHash` field (a hash cannot cover itself) and sorts keys, so the value it
468
+ returns is the one discovery stamped. Three call sites hashing "the plan" three slightly
469
+ different ways is how this check ended up unable to detect anything. There is no single `structureRequest` to hash: an EAV read runs one
470
+ request per batch, all of them derived from the plan, which is why the plan is the unit of
471
+ provenance. `reconcileEavReadPlan()` **requires** `planHash` in the observation it is given —
472
+ pass the checkpoint's, never the plan's own, or the check compares the plan to itself and
473
+ proves nothing.
474
+
475
+ Only once this final hash check and the completion reconciliation above both pass does
476
+ the `.tmp` file get renamed (atomically) to `data/source-extract/<entity>.ndjson` and
477
+ its checkpoint deleted, and the manifest below written using the checkpoint's own
478
+ `structureRequestHash` (now confirmed current, not merely carried).
479
+
480
+ ⚠️ **That order is load-bearing: rename first, write the manifest second.** The two writes
481
+ cannot be atomic together, so one has to survive a crash between them. Writing the manifest
482
+ first means a crash leaves a NEW manifest sitting beside an OLD final file — it verifies
483
+ cleanly against data it does not describe, and the next run reuses stale rows silently.
484
+ Renaming first means a crash leaves a fresh file with a missing or stale manifest, which the
485
+ reuse check refuses, so the entity is re-extracted. Re-doing work is the acceptable failure;
486
+ trusting the wrong rows is not. If
487
+ `data/source-extract/<entity>.ndjson` already exists (no `.tmp` in progress), do not
488
+ assume it is complete just because the file is present — see the manifest/provenance
489
+ check below before trusting it.
490
+ - **A final `.ndjson` file is only trustworthy if it was produced by the structureRequest
491
+ currently on file.** On promotion, write
492
+ `data/source-extract/<entity>.manifest.json` recording the read's own
493
+ `structureRequestHash` (the same value carried through its checkpoints, not
494
+ recomputed), plus `rowsWritten`, `snapshotTotal`, and the extraction timestamp. Before
495
+ treating an existing `data/source-extract/<entity>.ndjson` as already fully extracted,
496
+ hash the CURRENT sidecar and compare it against the manifest's stored hash. A match
497
+ means skip re-extraction. A mismatch — the table's approved query changed since that
498
+ file was written — or a missing manifest means the existing file is stale: do not reuse
499
+ it; re-extract from `cursor: null` into a fresh `.tmp` the same as if no file existed.
500
+
501
+ ⚠️ **A key/value table's manifest has a different shape, because the read did.** There is no
502
+ single `snapshotTotal` — each batch has its own — and no single `structureRequestHash`:
503
+
504
+ ```jsonc
505
+ { "kind": "eav-read-plan",
506
+ "planFile": "…--postmeta.eav-read-plan.json",
507
+ "planHash": "6cbfc47c83d83264",
508
+ "completedBatches": [ { "batchIndex": 0, "rowsRead": 412, "snapshotTotal": 412 } ],
509
+ "rowsWritten": 542,
510
+ "keysTruncated": false,
511
+ "droppedIdentityColumns": [], // non-empty = rows are incomplete per record
512
+ "acknowledgedDroppedColumns": false, // the APPROVAL that let an incomplete read promote
513
+ "orderByIsUnique": true, // false = paging was best-effort
514
+ "ndjsonDigest": "sha256…", // binds this manifest to the file it describes
515
+ "extractedAt": "…" }
516
+ ```
517
+
518
+ Build it with `eavExtractionManifest(plan, { planFile, completedBatches, rowsWritten,
519
+ extractedAt, ndjsonDigest })` rather than by hand, so a new qualifier cannot be forgotten at
520
+ one call site. It refuses a missing `planFile` or `ndjsonDigest`, and
521
+ `eavExtractionIsReusable()` requires every field it emits — the documented shape and the
522
+ builder are the same contract, checked both ways.
523
+
524
+ ⚠️ **Compute `ndjsonDigest` from the promoted file, with `eavExtractionFileDigest()`, AFTER
525
+ the rename.** Every other field describes the read that was supposed to happen; this is the
526
+ only one that says what actually landed on disk.
527
+
528
+ Decide reuse with `eavExtractionIsReusable(plan, manifest, observed)`, which returns the
529
+ reasons an existing file cannot be trusted; empty means skip re-extraction. `observed` is what
530
+ is on disk **right now** and is required:
531
+
532
+ ```jsonc
533
+ { "planFile": "…--postmeta.eav-read-plan.json", // the path YOU resolved for this entity
534
+ "ndjsonDigest": "sha256…", // eavExtractionFileDigest(<final file>)
535
+ "ndjsonRowCount": 542 } // lines in the final file
536
+ ```
537
+
538
+ ⚠️ **Never open `manifest.planFile`.** A manifest is untrusted input — it can name
539
+ `../../something-else.json` — so following its path would let the file choose what it is
540
+ validated against. Pass the sidecar path you already resolved; the check compares the two and
541
+ refuses a mismatch. The same reasoning is why the digest and row count come from the file you
542
+ are about to reuse, not from the manifest: a manifest that is only checked against itself
543
+ cannot detect a truncated, replaced or half-written NDJSON beside it. ⚠️ **The plan hash alone is
544
+ not the whole decision.** `acknowledgedDroppedColumns` is deliberately excluded from the hash
545
+ so that acknowledging an omission cannot invalidate an in-flight resume — which means
546
+ *withdrawing* it would leave the hash unchanged, and an extraction that promoted only because
547
+ someone approved its missing columns would still look reusable. The reuse check compares the
548
+ approval directly, in both directions: withdrawn, and no-longer-needed.
549
+
550
+ The staleness half is the same idea against the plan: `eavReadPlanHash()` the current plan
551
+ file and compare with the manifest's `planHash`; a mismatch or a missing manifest means
552
+ re-extract. ⚠️ `reconcileEavReadPlan()` additionally verifies the plan against ITS OWN embedded
553
+ hash before anything else, so a plan file edited by hand after discovery wrote it is refused
554
+ outright — without that, `planHash` only proved two strings matched. Carry
555
+ `keysTruncated`, `droppedIdentityColumns` and `orderByIsUnique` into the manifest **because
556
+ they qualify what the file contains** — a downstream consumer reading an extraction that
557
+ silently omitted columns, or was paged on a tying sort key, must be able to learn that from
558
+ the artifact rather than from whoever happened to run it.
559
+ - **Fail clearly, never loop or silently truncate**, if `hasNext` is true but the
560
+ returned `cursors.next` is missing or identical to the cursor just used — record the
561
+ entity as a failed/deferred extraction with that reason (leaving the `.tmp` file and
562
+ its checkpoint in place for the next resume attempt), the same as any other reader's
563
+ unrecoverable page error.
564
+ - **Reconcile before promoting, never after.** The first page's `pagingMetadata.total`
565
+ is the frozen snapshot total for the whole read. Before renaming `.tmp` to the final
566
+ `.ndjson`, compare it against the `.tmp` file's own line count; a mismatch is reported
567
+ as a failed/deferred extraction, and the `.tmp` file is NOT promoted — a row inserted
568
+ or deleted mid-read can still shift a page's contents (an accepted residual gap), but the counts themselves must always be checked before anything
569
+ downstream is allowed to treat this entity as complete.
570
+ - **Authorization and query validation are the plugin's job, not the reader's.**
571
+ wix-wp-plugin-v2 re-validates every identifier against a live `DESCRIBE` on every
572
+ request; the generated reader only needs to treat the plugin's 403/404 responses as a
573
+ failed extraction for that entity, not re-derive its own authorization logic.
574
+
41
575
  ### CSV sources (`platform: "csv"`)
42
576
 
43
577
  When `source-schema.json.platform === "csv"`, generate a **file reader** instead of an HTTP
@@ -61,6 +595,24 @@ reader and read `rp-source-csv` → "Read contract". The differences that matter
61
595
  required Wix field is never fed an empty string the source did not have.
62
596
  - CSV values are plain text; write them as-is with no entity-decoding step.
63
597
 
598
+ ### Shopify sources (`platform: "shopify"`)
599
+
600
+ When `source-schema.json.platform === "shopify"`, read `rp-source-shopify` → "Read contract"
601
+ instead of generating a reader. The differences that matter:
602
+
603
+ - **Every read goes through the adapter's CLI wrapper** (`rp-source-shopify/lib/shopify-cli.js`),
604
+ never a direct HTTP call and never a stored token: the store session lives in the operator's
605
+ Shopify CLI. Vendor the wrapper; do not re-emit it.
606
+ - **The full-store extract is the adapter's bulk extractor**, not a generated per-record
607
+ GraphQL pager: the extract step is `rp-source-shopify/scripts/shopify-extract.js` (resumable,
608
+ one bulk dataset at a time, writing `data/source-extract/<entity>.ndjson` and
609
+ `extract-manifest.json`; a source catch-up rerun passes `--catch-up`). Generate the importer
610
+ to read those files only once the manifest says `complete`. If this bundle does not ship the
611
+ extractor, stop codegen and report it; do not improvise one.
612
+ - **A gap found during the extract** (a read short of Shopify's count, a withheld field) is
613
+ recorded through `rp-source-shopify/lib/import-findings.js` `stopWithFinding`, which adds the
614
+ coverage row and stops the extract with the finding's code. Never drop or zero-fill it.
615
+
64
616
  ## Target write contract
65
617
 
66
618
  Symmetrically, do **not** re-derive the Wix write surface here. It is identical for
@@ -109,12 +661,12 @@ not a per-project regeneration.
109
661
  "Never hand-write slug sanitizing" below.
110
662
  **Wix Stores products: Catalog V3 only. Catalog V1 is not supported by this workflow —
111
663
  there is nothing to generate for it.** The only destination a migration ever writes to is
112
- a `V3_CATALOG` site (guaranteed at provisioning and gated before any write — see
113
- `replatform` → "Headless site creation" and `rp-execute-setup`), so generated code may
664
+ a `V3_CATALOG` site (guaranteed at provisioning), so generated code may
114
665
  assume V3 unconditionally: no version detection, no branch, no V1 shape, no V1 fallback
115
- for a failing V3 write. A V1 site is a **blocker the run halts on**, never a case codegen
116
- handles. Concretely: never emit Catalog V1-style top-level `price`, `sku`, or variant
117
- inventory fields. For simple products, emit one variant under
666
+ for a failing V3 write. A V1 site (only reachable on a pre-existing site this run did not
667
+ create) is a **blocker the run halts on**, never a case codegen handles. Concretely: never
668
+ emit Catalog V1-style top-level `price`, `sku`, or legacy inventory objects.
669
+ V3 `variantsInfo.variants[].inventoryItem` is handled by the inventory contract below. For simple products, emit one variant under
118
670
  `variantsInfo.variants[]` with variant-level `price`, `sku`, and physical properties.
119
671
  When the source product is subscription-based and the source payload exposes explicit
120
672
  recurring cadence in structured product data, emit native
@@ -131,11 +683,112 @@ not a per-project regeneration.
131
683
  (set exactly one), and it is capped at 16,000 characters — longer bodies need truncation or
132
684
  an info section, recorded in `mapping-gaps.json`. Ricos conversion remains correct for blog
133
685
  posts, where `richContent` really is a Ricos document.
686
+ **Never let lost product options become a silent default variant.** When the source
687
+ declares variation attributes for a product, set canonical `sourceDeclaredOptionNames` to
688
+ those names in source order, even when no variant choices can be resolved from them (the
689
+ WooCommerce all-"Any" shape: the parent declares attributes, every variation's `attributes`
690
+ is `[]`). Leave it unset when the source declares none. `buildProduct` then throws for a
691
+ product that declared options but built zero. Catch that throw in the existing per-record
692
+ error handling: do not create the product, and write a `mapping-gaps.json` entry naming the
693
+ product (`sourceId`, name) and its declared-but-unresolved option names, so it is reported
694
+ for human review rather than counted as created. Do not guess combinations or map them to
695
+ modifiers. The only exception is an existing `orchestration/decisions.json` entry under
696
+ `optionLoss:<product.sourceId>`: look it up and, if present, pass it as
697
+ `buildProduct(product, { ..., optionLossDecision: { key, entry } })`. `buildProduct`
698
+ accepts it only when `source` is `user`, `value.accept` is `default-variant`,
699
+ `value.sourceProductId` is this product's `sourceId`, and `value.declaredOptionNames` equals
700
+ `sourceDeclaredOptionNames`. Generated code never creates, infers or defaults that entry. It
701
+ records only one user's explicit answer for that one product: never from inference, a prior
702
+ artifact, a mapping or execution approval, 1-click approvals, or a blanket answer. Even with
703
+ a valid decision, still write the `mapping-gaps.json` entry, marked accepted with the
704
+ decision key, so the loss stays reported.
134
705
  Media scope should be relationship-driven by default: generate imports only for media
135
706
  referenced by mapped entities, not for the entire unattached source library. When the
136
707
  target adapter says an entity can ingest external URLs directly (for example Wix Stores
137
708
  product media), generate that entity-native path instead of routing those files through
138
709
  the slower generic Media Manager import flow.
710
+ **Never let generated code name an image field.** A source image reference usually carries
711
+ several URLs for the same picture at different sizes, and the field that holds the original
712
+ differs between routes of the same source — so `images.map((i) => i.src)` is a codegen
713
+ defect, not a shortcut. Emit a call to the source adapter's resolver instead (WordPress:
714
+ `resolveImageUrls` in `rp-source-wordpress/lib/wp-image-url.js`), which returns the
715
+ original at maximum resolution, percent-encoded, with `altText` and `displayName` attached.
716
+ The destination copies the one URL it is given and cannot be upgraded later, so a crop
717
+ emitted here is permanent.
718
+ **Never let generated code treat a media write's 200 as done.** Entity-native ingestion is
719
+ asynchronous: the destination acknowledges the queue entry and fetches the file afterwards,
720
+ and the write is a full replace, so a fetch that fails later leaves the entity with **no
721
+ image at all** — no error, and the previous image already discarded. Items can
722
+ remain un-ingested well after an accepted write. Generated media code
723
+ must therefore read the entity back and re-send whatever did not land, and must feed BOTH
724
+ failure kinds — the write that was refused (429) and the write that was accepted but never
725
+ ingested — into the same retry list. Call `patchStoresProductMediaVerified` rather than
726
+ `patchStoresProductMedia`; emitting the unverified writer for a media-bearing import is a
727
+ codegen defect. Whatever never lands is reported per entity, by name — never summarised
728
+ into a count.
729
+
730
+ **WordPress-family product media is fail-closed before any Stores request.**
731
+ This applies only when `orchestration/decisions.json` → `sourcePlatform.value` is exactly
732
+ `woocommerce` or `wordpress`; do not read the wrapper object or the nullable
733
+ `orchestration/run.json` field. Quick mode remains unchanged. Vendor the shared root
734
+ `lib/confirmed-pictures.js` and `lib/product-media-deferral.js` while preserving the normal
735
+ toolkit layout, and emit this sequence in the generated importer:
736
+
737
+ 1. Load the target writer module first and use its exported `confirmedPictures` object for
738
+ every declaration, source-media read and confirmation; never require another toolkit-root
739
+ or vendored copy of `confirmed-pictures.js`. At process start, read
740
+ `decisions.sourcePlatform.value` and call
741
+ `writers.confirmedPictures.declareMediaSource(sourcePlatformValue)` once. The first
742
+ declaration is final. Do this for every source; the target guard activates only for the
743
+ two identities above. Under either guarded identity call
744
+ `writers.assertProductMediaGuardActive()` before the first product write. A failed
745
+ assertion is a startup error: do not continue with an unguarded writer.
746
+ 2. For the products selected in this pass (failed-only means only those products), resolve
747
+ each complete ordered gallery with `resolveImageUrls`, collect its positive `sourceId`
748
+ values, and call
749
+ `readSourceMediaRecords({ ids, source: { baseUrl, request: authenticatedWpRequest } })`
750
+ once before the first product media write. `authenticatedWpRequest` must delegate to the
751
+ shared `fetchJson` while adding the run's existing WordPress auth headers. The helper
752
+ de-duplicates/sorts ids and performs
753
+ sequential authenticated `wp/v2/media` reads in chunks of 100 with one 60-second total
754
+ deadline per chunk. Do not persist the read or source picture data.
755
+ 3. Call `confirmPictures({ pictures, read })` for each whole gallery. Use only its frozen
756
+ `pictures` list. Do not clone, rebuild, subset, extend, reorder, or hand-author that list;
757
+ the shared Stores boundary rejects any URL gallery without that provenance before a
758
+ request. `buildStoresProductMedia` is the one enforcement point used by create, bulk
759
+ create, general product patch, media patch, and verified media patch. Wix media-id-only
760
+ lists and non-WordPress sources keep their existing behavior.
761
+ 4. If confirmation fails, omit media from the product create/update so other fields still
762
+ write and an existing target gallery stays untouched. After the target id is known, call
763
+ `buildProductMediaFailureRow({ confirmation, attemptId, sourceStableKey, sourceId,
764
+ targetId, runId, phase })` from `lib/product-media-deferral.js` and append its result to
765
+ the ordinary attempt journal. The helper owns the `stores-product-media-guard` row shape,
766
+ source/target ids, bounded message and three-value `failure.reason` contract. Append
767
+ exactly one such row for the product, **after** the product write's own terminal
768
+ `markAttempt` row. The guard failure must be the last row appended for that source stable
769
+ key in every pass that leaves media deferred; no ordinary product success, skip or other
770
+ terminal row may follow it. Never send a partial gallery.
771
+ 5. Treat a latest `stores-product-media-guard` failure as an ordering constraint in every
772
+ later pass, including full, failed-only and unresolved-only passes. Before appending any
773
+ later terminal row for that product key, repeat the source read and confirmation. If it
774
+ still fails, finish the ordinary product attempt first and append a fresh guard failure
775
+ last. Only a confirmed full-gallery write may be followed by a success row that clears
776
+ the guard failure. On `--failed-only`, use the existing crosswalk target id, call
777
+ `patchStoresProductMediaVerified` with the identical complete ordered confirmed gallery,
778
+ and append an `imported` success row under the same source stable key. Do not create the
779
+ product again. Every verified PATCH round re-sends that same full list; read-back results
780
+ decide only whether another round is needed.
781
+
782
+ After the products phase, load the whole real attempt journal once and pass all its rows to
783
+ `withProductPicturesDeferredAction(completionInput, rows)` from
784
+ `lib/product-media-deferral.js`. The helper folds the whole journal before filtering and
785
+ returns the shared completion input with zero or one `product-pictures-deferred` action next
786
+ to its existing actions. A later success row clears the action only after the source re-check
787
+ and confirmed full-gallery write above; an older untouched failure stays; a manual destination
788
+ edit appends no row and therefore does not clear it. This adds no
789
+ recurring model tokens. Runtime adds
790
+ `ceil(distinct selected picture ids / 100)` sequential source reads, each bounded by 60
791
+ seconds including pacing/retries; surface that estimate in the import plan.
139
792
  6. Generate thin project-specific write specs and import orchestration code that pass
140
793
  those Wix-shaped objects into the shared `rp-target-wix` runtime. Generated code should
141
794
  describe *what* to write and in what order, not *how* to implement retries, throttling,
@@ -147,14 +800,14 @@ not a per-project regeneration.
147
800
  7. Generate or wire the deterministic execution-state preparation step before any import
148
801
  write. The generated runner should call the shared `execution-state` preparation
149
802
  contract, or the execution instructions should run
150
- `skills/replatform/scripts/execution-state-prepare.js` before the generated import
803
+ `skills/wix-replatform/scripts/execution-state-prepare.js` before the generated import
151
804
  entrypoint.
152
805
  8. Generate the runnable setup/extraction/import entrypoints (see below) — the artifacts
153
806
  `rp-execute-setup` and `rp-execute-import` actually run. This is required, not optional.
154
807
  Then run the **sample-preview gate** (see below) before the execution-plan approval.
155
808
  9. Generate `execution/review/import-plan.md` plus
156
809
  `execution/review/import-plan.freshness.json` using
157
- `skills/replatform/scripts/artifact-freshness.js write ...`. The freshness metadata
810
+ `skills/wix-replatform/scripts/artifact-freshness.js write ...`. The freshness metadata
158
811
  must include hashes for `source-schema.json`, `mapping/mapping-plan.json`,
159
812
  `setup/setup-verification.json`, generated import code, and the target contract ledger
160
813
  revision. If codegen follows a newly promoted write contract, regenerated code and this
@@ -174,6 +827,9 @@ produce a mandatory review artifact before the execution approval gate:
174
827
  This review is performed by the agent, not delegated to the user. It verifies the
175
828
  generated code itself. It must check:
176
829
 
830
+ - no generated file defines a local transient/retry predicate, and none tests an error's
831
+ `message` for a status number or a status word; generated retry paths call the shared
832
+ `isTransientWixError`
177
833
  - every generated write path that can carry email/phone data passes `safeModeOptions`
178
834
  into the shared Wix runtime or direct REST wrapper
179
835
  - every shared/native writer reached by generated code actually consumes those
@@ -274,12 +930,48 @@ runnable entrypoints:
274
930
  - `--sample` may be supported as a narrower dry-run-style validation mode, but it must
275
931
  not replace `--dry-run` as the primary safe-validation control.
276
932
  - honor deterministic selective resume flags: `--entity <entity>`,
277
- `--source-type <subtype>`, `--missing-only`, `--failed-only`, and `--deferred-only`.
933
+ `--source-type <subtype>`, `--missing-only`, `--failed-only`, `--deferred-only`, and
934
+ `--unresolved-only`. These are a **requirement on the generated importer**, not an operator
935
+ convenience, and they are verified before the first write — see "The per-record ledger" and
936
+ "Declaring the entry contract" below.
937
+ - support `--describe` and call `assertGeneratedImporterContract` as the **first statement of
938
+ `main()`** — see "Declaring the entry contract (required)" below.
278
939
  `--missing-only`, `--failed-only`, and `--deferred-only` are mutually exclusive. These
279
940
  flags must drive the same import path as a full run after selecting a stable record
280
941
  set; do not generate one-off recovery drivers.
942
+ - support a `--rerun` catch-up mode built on `lib/import-recovery.js`'s
943
+ `selectRerunRecords`, which buckets the refreshed scope into create / update / unchanged /
944
+ unknown against the crosswalk baseline. **Do not build this out of `--missing-only` plus
945
+ `--failed-only`** — those filters do not compare source versions. Contacts/products
946
+ deliberately recheck crosswalk hits, while other entities may exclude them; failed-only
947
+ only revisits what was already attempted. On every confirmed write,
948
+ persist the version pair (`sourceVersionField` + `sourceVersion`, or the declared
949
+ `sourceHash` fallback, plus the returned `targetRevision`) through
950
+ `nextCrosswalkBaseline`, which refuses to advance on a failed, conflicted, dry-run or
951
+ skipped write. Route an update through the entity's **verified revision-taking update
952
+ writer** and, on a revision conflict, through `lib/rerun-conflicts.js` to a user decision —
953
+ never through a silent revision refresh, and never through a same-id create/import as a
954
+ substitute for an update. `updateSupportFor` enforces that in code rather than leaving it
955
+ to this prose: declare `{ writerId, revisionParam }` per entity in the accepted mapping,
956
+ and honour its `report_for_review` outcome. The order family is denied there outright, so a
957
+ changed order must be reported, never written. Honour the selector's other buckets too —
958
+ `needsReconciliation` (an uncertain prior attempt, or an unchanged contact/product
959
+ that still needs target identity/value verification), `outOfScope` (pass the accepted scope as `inScope`), and `reportForReview`.
281
960
  - emit a stable `runId` at process start and include it in every audit event, dry-run
282
961
  request capture, placeholder crosswalk row, and summary artifact.
962
+ - construct its progress logger via `parseProgressArgs`/`createProgressLogger`
963
+ (`lib/progress-log.js`, already shipped — vendor it the same way as any other shared
964
+ lib) and wrap the per-batch/per-record write loop with that logger's own
965
+ `withHeartbeat(fields, fn)` (or the equivalent manual `startHeartbeat(fields)` /
966
+ `stopHeartbeat()` pair around a longer-lived loop), carrying live counters as
967
+ heartbeat fields: at minimum `recordsAttempted` (a running count), `currentEntity`
968
+ (the entity/write-spec currently being processed), and `phase: 'import'`. **Mutate the
969
+ same `fields` object's properties as the loop advances rather than passing a fresh
970
+ object per record/batch** — `withHeartbeat`/`startHeartbeat` keep the one reference the
971
+ ticker reads from, so a fresh object per iteration never reaches the ticker and the
972
+ heartbeat goes stale even though the loop is progressing. This covers the write loop, not
973
+ just process start, and must not change the progress-log format, event names, or polling
974
+ contract in any way.
283
975
  - emit `execution/live-import-summary.json` and `execution/completion-report.json`
284
976
  through the shared completion-report runtime, with entity/subtype completeness counters
285
977
  for extracted, in-scope, attempted, imported, already present by crosswalk, deferred,
@@ -303,8 +995,13 @@ Extraction format requirements:
303
995
  - write a manifest that lets the import step discover which entity files exist and in what
304
996
  order to consume them
305
997
  - make the extracted files deterministic enough for resume, replay, and debugging
998
+ - wrap the extraction loop with the same `lib/progress-log.js` heartbeat helper
999
+ (`withHeartbeat`/`startHeartbeat`+`stopHeartbeat`) `run-import.js` uses above, carrying
1000
+ `recordsAttempted`, `currentEntity`, and `phase: 'extract'` as heartbeat fields, mutated
1001
+ in place as the read loop advances — a paginated source read can run long enough that the
1002
+ same silent-heartbeat gap applies to `run-extract.js`, not only to the write phase
306
1003
 
307
- ### Notification-mute preflight (spec 0012, required)
1004
+ ### Notification-mute preflight (required)
308
1005
 
309
1006
  When mute is in effect — **always** for `WIX_SITE_STRATEGY=new` (unconditional,
310
1007
  regardless of `WIX_MUTE_NOTIFICATIONS`), and for existing sites only when
@@ -333,6 +1030,131 @@ the import path. A preflight mute-verification failure is recorded through the s
333
1030
  `rp-telemetry` recorder as an `error` event (with `error_code`) like any other run
334
1031
  event — no new telemetry surface.
335
1032
 
1033
+ ### The per-record ledger (required)
1034
+
1035
+ `state/attempts/write-attempts.ndjson` is **the** per-record log of what a generated importer
1036
+ wrote to a merchant's store. A generated importer must **not** open a second, private results
1037
+ log of its own, and must not write per-entity failure files beside it: failures live in the
1038
+ journal with a terminal status, and the operator-facing views are the derived queue and the
1039
+ recovery record described below.
1040
+
1041
+ **It is only ever appended to.** `appendAttempt` (which appends through `fs.appendFile`) is the
1042
+ only writer. No flag, no environment variable and no mode may open it in a way that can shorten
1043
+ it, and rotation, compaction and in-place folding are not permitted. This is not a style rule:
1044
+ an importer once opened its results log in overwrite mode whenever a record limit was set, and
1045
+ the next thirty-record sampling pass replaced several hundred rows with thirty.
1046
+
1047
+ **A validation pass writes somewhere else entirely.** `--dry-run` / `--sample` write to
1048
+ `state/sample/attempts/write-attempts.ndjson` and `state/sample/…` generally; the real journal
1049
+ is not opened at all. Pass `{ sample: true }` to `appendAttempt` / `markAttempt` /
1050
+ `loadAttemptJournal`.
1051
+
1052
+ ⚠️ **A limited pass that writes for real is not a sample.** The discriminator is whether live
1053
+ writes occur, never whether a record limit is set — keying the sample path off the limit flag
1054
+ reproduces the original defect with the file moved. A limited live pass writes to the **real**
1055
+ journal and carries `selection: { offset, limit, filters }` so its scope is legible.
1056
+
1057
+ **Every row says which pass wrote it.** `validateAttemptRow` requires `attemptId`,
1058
+ `sourceStableKey`, `writeSpecId`, `operation`, `targetEntityType`, `status`, `runId`, `phase`
1059
+ (and whatever that validator requires next) — `runId` is the id the importer mints at process
1060
+ start and already stamps on audit events, request captures and dry-run crosswalk rows, and
1061
+ `phase` uses the same vocabulary as `wix.send` request metadata, not a second one. Build the
1062
+ row with the validator, not from this list.
1063
+
1064
+ With both present, per-pass accounting falls out of the file: **group by `runId`** for a pass's
1065
+ counts, **fold by `sourceStableKey`** (latest row wins, `foldAttemptsBySourceKey`) for record
1066
+ counts. Without them every per-row count silently reads as a per-record count — a run
1067
+ can hold several rows per order, and readers will mistake rows for records.
1068
+
1069
+ A follow-on row appended by `markAttempt` carries the `runId` and `phase` of the run appending
1070
+ it, which may differ from the row it supersedes. That is deliberate: it is how a later pass's
1071
+ confirmation of an earlier pass's write stays legible.
1072
+
1073
+ **A failure keeps its identifying facts as fields.** When `status` is `failed_retryable`,
1074
+ `failed_terminal` or `succeeded_unverified`, the row carries a `failure` object:
1075
+
1076
+ | Field | Shape |
1077
+ | --- | --- |
1078
+ | `failure.sourceId` | string — redundant with `sourceStableKey` by design, so the row reads on its own |
1079
+ | `failure.targetEntityId` | string \| null — the target-side id the request named |
1080
+ | `failure.operation` | string |
1081
+ | `failure.httpStatus` | integer \| null |
1082
+ | `failure.errorCode` | string \| null — the server's own code (`applicationErrorCode`), never parsed out of prose |
1083
+ | `failure.reason` | optional `no-source-id` · `no-source-record` · `source-unreadable` for `stores-product-media-guard` |
1084
+ | `failure.scopeGroupName` | string \| null — the named grouping the server rejected |
1085
+ | `failure.outcome` | `failed_terminal` · `failed_retryable` · `unresolved` |
1086
+ | `failure.message` | string — free text, **may** be truncated |
1087
+
1088
+ ⚠️ **No identifier may exist only inside `failure.message`.** Extract the identifying fields
1089
+ from the error **before** shortening the message, never after. A coupon importer kept the first
1090
+ three hundred characters of each error and in every one of eleven scope failures the identifier
1091
+ of the thing that failed sat past the cut; recovering it now means running against a live store
1092
+ again. The validator rejects a row carrying a GUID that appears in no other field.
1093
+
1094
+ `appendAttempt` / `markAttempt` redact-and-retry once around exactly that rejection — they strip
1095
+ a GUID-shaped token from `failure.message` (never a structured field) and re-validate, so a genuine
1096
+ Wix error whose message text happens to echo an id no caller extracted does not crash the run. That
1097
+ is a safety net for the identifier the code failed to anticipate, not a substitute for populating
1098
+ `failure.targetEntityId` correctly: **when an entity's target-side id is deterministic and
1099
+ derivable *before* a write is even attempted** — a stable hash/derivation of the source id, the way
1100
+ `order-history.js`'s `deriveImportedOrderId` works for orders — populate `failure.targetEntityId`
1101
+ with that derived id on **every** attempt row, success or failure, rather than defaulting it to
1102
+ `null` on failure. A hardcoded `null` here is exactly what turns a target-side id embedded in an
1103
+ error message into an unstructured identifier, which is what trips the guard above in the first
1104
+ place.
1105
+
1106
+ **An uncertain write is not a failure.** `succeeded_unverified`, and a `started` row that never
1107
+ got a terminal follow-on, are **unresolved**: the next action is to **read the target back**,
1108
+ never to re-send, because re-sending an uncertain write is how a recovery pass creates a
1109
+ duplicate on a live store. They select under `--unresolved-only`, count as `unresolved` in the
1110
+ recovery record, and are rebuilt into `execution/unresolved-writes.ndjson`
1111
+ (`rebuildUnresolvedWrites`) — a projection of the journal, so deleting it loses nothing and it
1112
+ is never a source of truth. A `deferred` record, including a source-data-rule skip, is **not**
1113
+ unresolved and never reaches that queue. Never sum unresolved into `failed`: a recovery record
1114
+ that does reconciles arithmetically and is wrong.
1115
+
1116
+ ### Declaring the entry contract (required)
1117
+
1118
+ The flags above were asked for by this document for a long time and verified nowhere, so an
1119
+ importer shipped that parsed no command-line arguments at all and still passed the import gate.
1120
+ A boundary nothing crosses and nothing checks is not a boundary, so the generated importer now
1121
+ declares its own capabilities and both sides check the declaration.
1122
+
1123
+ **`--describe`** prints exactly one JSON object to stdout and exits 0 without touching the
1124
+ target:
1125
+
1126
+ ```json
1127
+ {
1128
+ "contractVersion": 1,
1129
+ "runIdSource": "process-start",
1130
+ "selectionModes": ["entity", "source-type", "missing-only", "failed-only",
1131
+ "deferred-only", "unresolved-only", "rerun"],
1132
+ "ledger": {
1133
+ "attempts": "state/attempts/write-attempts.ndjson",
1134
+ "sampleAttempts": "state/sample/attempts/write-attempts.ndjson",
1135
+ "unresolved": "execution/unresolved-writes.ndjson",
1136
+ "recoveryLog": "execution/recovery-log.json",
1137
+ "appendOnly": true
1138
+ }
1139
+ }
1140
+ ```
1141
+
1142
+ **The importer checks itself.** `run-import.js` calls
1143
+ `assertGeneratedImporterContract(descriptor)` from `lib/import-recovery.js` as the **first
1144
+ statement of `main()`** — unconditionally, before argument dispatch and before the
1145
+ notification-mute preflight. If a required mode is missing or a ledger path is wrong it throws
1146
+ naming every problem at once and the importer refuses to run itself, exactly as the mute
1147
+ preflight aborts before any write: exit non-zero, no degraded mode, no skip flag.
1148
+
1149
+ `rp-execute-import` step 2d runs `import-contract-verify.js` against the same descriptor from
1150
+ the outside before the first write. The two are not redundant: the self-check is what an
1151
+ operator cannot skip, and the external gate is what catches an importer that never emitted the
1152
+ self-check at all — such an importer also cannot answer `--describe`.
1153
+
1154
+ ⚠️ The vendoring escape hatch ("use `import-recovery.js` where available, or vendor equivalent
1155
+ helpers") survives for the helper **implementations**. It does **not** survive for the entry
1156
+ contract: vendoring is not an excuse for a missing mode.
1157
+
336
1158
  ### Use the target's BULK write path (required)
337
1159
 
338
1160
  **A generated importer must write through the target's bulk endpoint whenever one exists.**
@@ -361,8 +1183,381 @@ one silently corrupts a report if ignored:
361
1183
  those are still failures and must appear in the completion report.
362
1184
 
363
1185
  Dedupe **before** building the batch — a skipped record must never reach the API — and
364
- record the crosswalk per successful item, not per batch, so an interrupted run resumes
365
- correctly.
1186
+ retain each successful item’s source identity. The verified wrappers durably checkpoint
1187
+ these outcomes in batches before publishing verified crosswalk references; do not rewrite
1188
+ the full crosswalk and its indexes for each item.
1189
+
1190
+ ### Contact and product identity verification
1191
+
1192
+ Generate standard contact/product imports through `contact-import.js` and
1193
+ `product-import.js`, not a loop that publishes `bulk.succeeded` directly. Vendor their
1194
+ `bulk-results.js`, `write-verification.js`, and root `lib/write-verification-state.js`
1195
+ dependencies with the existing local-state/runtime files, preserving the relative layout.
1196
+ The wrappers own prewrite reconciliation, source-index correlation, candidate persistence,
1197
+ read-back, and publication of verified mappings. Raw writers remain request primitives.
1198
+
1199
+ Select `sourceKey` and all expected fields from the source mapping BEFORE sending a write.
1200
+ Contact rows carry `{sourceKey, sourceId, sourceSystem, contact, safeModeOptions,
1201
+ updateMember}`. Supply per-record safe-mode options so a batch cannot acquire one shared
1202
+ replacement identity. Phone-only identities and conflicting source/target identifiers
1203
+ block and report; there is no automatic merge or V4 fallback. Contact custom-field setup
1204
+ must already be resolved; the shared DES setup helper still supports orders only.
1205
+
1206
+ Product rows carry `{sourceKey, product, expected, marker, variantExpectations}`.
1207
+ `expected` includes every mapped readable product field; exclude write-only inventory
1208
+ fields and verify them through the inventory pass. `marker: {path, value}` is source-derived
1209
+ (e.g. a unique mapped slug), not copied from a returned product. Each variant expectation
1210
+ carries `{sourceKey, expected, inventory}` with an unambiguous source-fixed SKU/choice
1211
+ projection. Include submitted variant choices in that projection; an unverified choice
1212
+ shape must remain a gap, even when SKU and price agree. The wrapper resolves exact target
1213
+ variant IDs, retaining the correspondence
1214
+ in each verified receipt. A missing or ambiguous match remains a gap, never a positional zip.
1215
+
1216
+ Use this caller shape; `toolkitRoot` is the installed or vendored wix-replatform root,
1217
+ `dryRun` is the shared runtime's resolved dry-run flag, and `runId` is the shared runtime's run
1218
+ id for the current pass — it stamps every execution-log entry, so a later recovery pass's
1219
+ entries stay distinguishable from this one's. The post-execution handoff is not generated here:
1220
+ that is the completion step's job, after the last write of the whole run.
1221
+
1222
+ ```js
1223
+ // BEGIN verified-contact-import
1224
+ const { importContacts } = require(require('node:path').join(toolkitRoot, 'resources/rp-target-wix/lib/contact-import.js'));
1225
+ const { createCompletionReport, writeCompletionReport, attachAttemptReasons } = require(require('node:path').join(toolkitRoot, 'lib/completion-report.js'));
1226
+ const { loadAttemptJournal } = require(require('node:path').join(toolkitRoot, 'lib/local-state.js'));
1227
+ const { writeLiveImportSummary } = require(require('node:path').join(toolkitRoot, 'lib/import-recovery.js'));
1228
+ const contactVerification = await importContacts(wix, {
1229
+ projectDir, siteId, rows: contactRows, dryRun,
1230
+ });
1231
+ // Opt-in: flush BEFORE building the completion input, not after, so the run's
1232
+ // own `coverageCheck` (computed by this very flush) is available to pass through below. This is
1233
+ // the only correctly-wired caller shape this repo tests -- see "Coverage check" below.
1234
+ let coverageCheck = null;
1235
+ if (!dryRun) {
1236
+ // Flush the run's OWN held counters after every batch, not once at the end: an importer that
1237
+ // holds them loses them to any interruption, and a long run redirected to a temp file loses
1238
+ // them to the shell. If a mid-run fix must refresh a count from the real attempt ledger, first
1239
+ // select the report's scope -- target entity type, run/phase when run-scoped -- then fold ALL
1240
+ // rows in that scope with `foldAttemptsBySourceKey` (see "The per-record ledger" above), and
1241
+ // only then classify the latest rows by counted status. The fold keeps the latest row per
1242
+ // sourceStableKey of whatever it is given; it does not narrow scope. Never filter by status
1243
+ // before folding: a record whose later row superseded a `succeeded` one would count again.
1244
+ // Never count raw rows: retried batches and recovery re-entries leave several rows per record,
1245
+ // which is how a run's counts inflate well past the number of records. Dry-run rows
1246
+ // live in the separate state/sample/ journal; never mix that into a live report.
1247
+ ({ coverageCheck } = await writeLiveImportSummary(projectDir, {
1248
+ runId, imported: { contacts: contactVerification.verified },
1249
+ }, { runId, phase: 'import', detail: 'contact batch flushed', entityTypes: { contacts: 'contact' } }));
1250
+ }
1251
+ // `sourceKeys` is required on every row: it is the only way a deferred or failed
1252
+ // count can later be joined back to the journal row that names its reason.
1253
+ const rawEntityCompleteness = [{
1254
+ entity: 'contacts', verificationKind: 'contact', sourceKeys: contactRows.map(row => row.sourceKey),
1255
+ inScope: contactRows.length, imported: contactVerification.verified,
1256
+ deferred: contactRows.length - contactVerification.verified,
1257
+ }];
1258
+ // Load the journal ONCE per report and join it in before building the report
1259
+ // -- never once per entity, and never read a second time by a later step of the same report.
1260
+ const journal = dryRun ? null : await loadAttemptJournal(projectDir);
1261
+ const completion = dryRun ? { status: 'simulated', verification: { contacts: contactVerification } } : createCompletionReport({
1262
+ siteId, runId, lastWriteAt: new Date().toISOString(),
1263
+ entityCompleteness: attachAttemptReasons(rawEntityCompleteness, journal.rows, { runIds: [runId] }),
1264
+ verification: { contacts: contactVerification },
1265
+ // This run called `writeLiveImportSummary` with a `runId` above, so it has opted into the
1266
+ // coverage check -- pass the run-matched block straight through so a wrong or
1267
+ // unconfirmed count escalates the completion status instead of looking clean by omission.
1268
+ coverageCheck,
1269
+ coverageCheckExpected: true,
1270
+ });
1271
+ if (!dryRun) {
1272
+ // Persist the final outcome and render the user-facing summary from the same object. One
1273
+ // shared writer: there is nothing here for a generated importer to reinvent, which is why
1274
+ // none of them ever did.
1275
+ await writeCompletionReport(projectDir, completion, { runId });
1276
+ }
1277
+ // END verified-contact-import
1278
+ ```
1279
+
1280
+ **Coverage check.** `writeLiveImportSummary` compares its held counters against the
1281
+ live attempt journal for the same run (the `runId` passed in `options`, or `summary.runId`) and
1282
+ adds the comparison as `coverageCheck` on the written summary and on this call's return value — it
1283
+ never changes the held counters. This flush deliberately names only `imported`; the `deferred`
1284
+ figure above is computed in the same pass for `entityCompleteness` but never carried into this
1285
+ `writeLiveImportSummary` call, and that is fine — an ordinary bucket (`imported`, `deferred`,
1286
+ `failed`, `attempted`) a flush does not name is simply not compared for that entity, never treated
1287
+ as a mismatched `0`. `unexpectedSkipped` is the one exception: its absence defaults to a held `0`,
1288
+ so always name it explicitly, even as `0`, once a flush's journal rows can produce a
1289
+ `skipped_unexpected` row for that entity. The flush above passes
1290
+ `options.entityTypes: { contacts: 'contact' }` because `contact-import.js`'s own verified writes
1291
+ publish to the crosswalk under `targetEntityType: 'contact'` (singular) while this summary's key is
1292
+ `contacts` (plural, matching the wrapper's own naming) — a generated importer's ledger-writing code
1293
+ (the per-record `appendAttempt` calls the "per-record ledger" section above requires, alongside
1294
+ this wrapper call, not shown again here) must stamp the same `targetEntityType` its `entityTypes`
1295
+ option names, or pass a matching key directly. Both directions of the match are checked, so a
1296
+ target type with real journal rows and no summary key naming it (directly or via `entityTypes`) is
1297
+ still reported, never silently agreed with. Flush after the batch's journal rows are appended, not
1298
+ before: this writer reads what has already landed in `state/attempts/write-attempts.ndjson`, and a
1299
+ flush that races ahead of its own journal rows under-reports what the journal actually has so far.
1300
+ A disagreement is reported whatever its cause, so this genuinely
1301
+ produces `agrees: false` / `coverage_mismatch` for that flush, not merely a stale snapshot that
1302
+ quietly resolves itself — order the write after the append so this does not happen.
1303
+
1304
+ The example above reads the run's own `coverageCheck` straight off this flush's return value
1305
+ because this is the run's only flush; a multi-batch importer instead reads it back from
1306
+ `execution/live-import-summary.json` at the end of the run and matches it to `input.runId`, never
1307
+ assuming "the last block in the file" is this run's, since that file is shared by every run.
1308
+ Either way, pass `coverageCheckExpected: true` into `createCompletionReport` whenever this run
1309
+ called `writeLiveImportSummary` with a `runId` at least once (`rp-execute-import/SKILL.md`'s
1310
+ end-of-run instruction owns this for the generated script as a whole) — omitting it is what keeps
1311
+ an existing, not-yet-wired importer's behavior unchanged.
1312
+
1313
+ Both wrappers create absent records and verify existing ones; they never update an existing
1314
+ record whose values differ. Route such differences to the declared revision-protected
1315
+ update/conflict-review flow, then verify the result. Pass optional `sourceVersion`,
1316
+ `sourceVersionField`, `sourceHash`, and `sourceHashDefinition` with each row to establish
1317
+ its baseline after a confirmed create with a returned revision. Read-only reconciliation
1318
+ preserves the saved baseline, including across verification failure and resume; it never
1319
+ advances a source version or refreshes the saved revision. Run unchanged contact/product
1320
+ rows from `selectRerunRecords().needsReconciliation` through these same wrappers.
1321
+
1322
+ Declare `verificationKind` in the accepted mapping and carry it into completion and
1323
+ selection options independently of source table names: `contact` or `product` for these
1324
+ comparators, `none` for other entities with their own verification flow. Unknown names
1325
+ without a declaration require verification; they cannot silently opt out. Reject undefined
1326
+ projection fields before any API call, naming the field to omit or replace with a JSON value.
1327
+
1328
+ Call `importProducts` with the equivalent product rows. Add its verification report and
1329
+ source-key set to the same completion input, then pass its verified variant correspondence
1330
+ to `reconcileInventory` below. Every intended variant stays in the inventory denominator,
1331
+ including products that failed identity checks. Do not send stock or category membership
1332
+ writes for an unverified product mapping. Keep inventory outcomes separate from product
1333
+ identity outcomes.
1334
+
1335
+ Run the SAME wrappers on retries, single-record batches, existing crosswalks and
1336
+ `--missing-only`; selection is not evidence that a target exists. An uncertain write
1337
+ remains pending until target reconciliation resolves it. Do not hand-code per-record
1338
+ fallbacks that turn errors or missing indexes into create permission. When using dry-run,
1339
+ pass `dryRun: true` and report simulated outcomes, not verified live completion.
1340
+
1341
+ The inspected contact and products-with-inventory endpoints require unique request-local
1342
+ `originalIndex`. Their endpoint adapters enforce this; Blog bulk is not covered by that
1343
+ contract. `undetailedFailures` counts failures but cannot identify their input rows, which
1344
+ stay unresolved. Counts, HTTP 200, and old presence-only receipts do not establish identity.
1345
+
1346
+ ### Product SEO: proven per-field overrides only
1347
+
1348
+ For a source with an SEO evidence contract (Yoast on WooCommerce), put the per-field
1349
+ `seoEvidence` on the canonical product (see rp-source-wordpress "Yoast product SEO evidence")
1350
+ and never fill `seoTitle`/`seoDescription` from rendered output. Then, per product:
1351
+
1352
+ 1. `plan = planProductSeo({ productId, seoEvidence, decisions })` from
1353
+ `rp-target-wix/lib/product-seo.js`, with `decisions` read from `orchestration/decisions.json`.
1354
+ 2. `buildProduct(canonical, { seoPlan: plan })`; pass the row to `importProducts` with
1355
+ `seo: plan` and **without** `seoData` in `expected`. The SEO stage verifies it per field
1356
+ (product read-back plus `resolvedTags`), reconciles a create that dropped `seoData` with a
1357
+ revision-protected PATCH, applies reruns as partial updates, and returns `report.seo`.
1358
+ The stage already bounds its own concurrency, pacing and settle waits; do not wrap it in
1359
+ a per-product loop or add sleeps around it.
1360
+ 3. Include `plan.projection` (never the receipt) in the product's `sourceHash`.
1361
+ 4. Add `report.seo.stageResiduals` to the completion input's `stageResiduals`.
1362
+ 5. If `buildSeoDispositionQuestions(plans)` returns questions, ask the owner once per question
1363
+ and record the answer with `recordSeoDisposition` (source `user`). Never answer it for them,
1364
+ in any automation mode, including one-click.
1365
+
1366
+ Never write product SEO through Item SEO Tags bulk set (`bulkSetItemSeoTags` refuses
1367
+ `STORES_PRODUCT`), and never write canonical, robots, schema, social tags or an SEO pattern.
1368
+
1369
+ ### Product inventory: inline create, then reconcile and verify
1370
+
1371
+ For default-location inventory, the shared reconciler also reads Catalog products and verifies each exact variant's availability. Inventory success alone cannot complete the report; disagreement stays unverified after bounded read retries. Never toggle stock to force Catalog propagation. Non-default locations record this Catalog check as not applicable.
1372
+
1373
+ Vendor `rp-target-wix/lib/stores-inventory.js` with `wix-build.js` and `wix-writers.js`.
1374
+ The builder consumes each canonical variant's `inventoryTracked`, `inventoryQuantity`, and
1375
+ `inStock`; it emits `inventoryItem` on variants when stock is known. Use the existing bulk
1376
+ products-with-inventory writer so initial stock costs no separate write. Send `quantity`
1377
+ OR `inStock`, never both; `trackQuantity` is read-only. Explicit non-default stock belongs
1378
+ in the separate inventory pass, not in default-location inline fields.
1379
+
1380
+ Collect inventory identities after each product batch, including products already found in
1381
+ the crosswalk. Generate a resumable inventory pass through `reconcileInventory`. Resolve actual product and variant
1382
+ IDs from returned/read-back entities and source-option correspondence; never zip unordered
1383
+ results or variants by position. Persist that correspondence. Keep source gaps and failed
1384
+ product-to-variant mappings in `inventoryGaps` so every intended source variant is counted.
1385
+
1386
+ ```js
1387
+ const { reconcileInventory, persistInventoryReport } = require('../resources/rp-target-wix/lib/stores-inventory.js');
1388
+ // resolvedInventoryRows: [{productId, variantId, inventory: {inStock} OR {quantity}}]
1389
+ // sourceVariantCount is counted BEFORE filtering gaps or failed product mappings.
1390
+ const inventoryReport = await reconcileInventory(wix, resolvedInventoryRows, {
1391
+ expectedCount: sourceVariantCount,
1392
+ gaps: inventoryGaps,
1393
+ // locationId may be supplied from verified setup; otherwise resolve the default.
1394
+ });
1395
+ await persistInventoryReport(inventoryReportPath, inventoryReport);
1396
+ // The generated completion report consumes inventoryReport.status and counts directly.
1397
+ ```
1398
+
1399
+ Set `inventoryReportPath` to the project's `state/inventory-report.json`. Supply the full
1400
+ intended variant set and gaps once per inventory pass; the helper handles bounded API batches
1401
+ internally. Persist lightweight resolved identities during product import so interrupted runs
1402
+ can reconstruct this set without recreating products. Do not replace the report with only the
1403
+ last product batch. The completion report consumes `status`, `counts`, and `complete` directly;
1404
+ only `complete:true` can mark the inventory checkpoint complete.
1405
+
1406
+ Product and inventory checkpoints are independent. Product success followed by inventory
1407
+ failure preserves the product crosswalk; resume queries existing inventory and repairs it.
1408
+ A successful bulk product receipt or `inventoryResults` count never substitutes for per-variant
1409
+ read-back. Report unresolved inventory as partial, while allowing independent records to
1410
+ continue. The setup inventory probe must pass before stock writes. Existing notification
1411
+ protection and dry-run boundaries still apply; a dry-run does not produce a live inventory
1412
+ verification receipt.
1413
+
1414
+ ### Product category attachment: attach, then verify (required)
1415
+
1416
+ **Never call `bulkAddItemToCategories` inline and treat its resolved promise as proof the
1417
+ product is in its categories.** It is an ADD against a bulk endpoint that takes exactly one
1418
+ object argument, `{ productId, categoryIds }` -- not positional arguments. A generated
1419
+ importer can call it as `bulkAddItemToCategories(wix, row.categoryIds, [r.targetId])`: three
1420
+ positional arguments to a two-argument function. JS silently drops the third, so the function
1421
+ received the bare `categoryIds` array as its whole `payload`; destructuring `{ productId,
1422
+ categoryIds }` off an array yields `undefined` for both fields, on every call, forever. The
1423
+ wrapping `catch {}` swallowed every resulting failure, and nothing downstream tracked this step
1424
+ at all, so every product silently kept zero of its imported categories with a
1425
+ clean completion report. Vendor `rp-target-wix/lib/stores-category-attach.js` with
1426
+ `wix-writers.js` and route every category link through it instead.
1427
+
1428
+ **This example assumes composition.** A generated importer runs this stage alongside
1429
+ others -- order-history, member-pass -- that also extend `completionInput`, and `AGENTS.md`'s
1430
+ single-write rule still holds: only ONE block in the whole generated file, whichever genuinely
1431
+ writes last, ever calls `createCompletionReport`/`writeCompletionReport`. `writeCompletionReport`
1432
+ always overwrites the one `execution/completion-report.json` file (`writeFileAtomic`, no merge),
1433
+ so a second call anywhere in the same run silently discards whatever the first call alone
1434
+ recorded. This section therefore never builds or writes a completion report itself -- it only
1435
+ extends the SAME shared `completionInput` the order-history/member-pass sections already extend,
1436
+ exactly the way member-pass does above
1437
+ (`completionInput = MC.mergeMemberCompletion(completionInput, ...)`): a non-terminal stage folds
1438
+ its residuals into the held accumulator and stops there.
1439
+
1440
+ ```js
1441
+ const { reconcileCategoryAttachment, persistCategoryAttachReport, collectCategoryAttachStageResiduals } =
1442
+ require('../resources/rp-target-wix/lib/stores-category-attach.js');
1443
+ // categoryAttachRows: [{ productId, categoryIds, sourceKey }] -- one row per product that has at
1444
+ // least one category to attach. A product with none is simply never given a row.
1445
+ const categoryAttachReport = await reconcileCategoryAttachment(wix, categoryAttachRows);
1446
+ await persistCategoryAttachReport(categoryAttachReportPath, categoryAttachReport);
1447
+ // Extend the SAME shared completionInput every other stage in this file extends -- never build
1448
+ // or write a completion report here. Whichever stage genuinely runs last in this generated
1449
+ // project (order-history-completion, when this project also imports orders; this stage's own
1450
+ // trailing completion block, only on a project where category-attach really is the terminal
1451
+ // write of the whole run) owns the single createCompletionReport/writeCompletionReport call and
1452
+ // reads this same completionInput. Never stop at persisting the sidecar report alone: an
1453
+ // unfolded categoryAttachReport is invisible to completeness accounting, which is the exact
1454
+ // silent-failure shape this file exists to close.
1455
+ completionInput = {
1456
+ ...completionInput,
1457
+ stageResiduals: [
1458
+ ...(completionInput.stageResiduals || []).filter((row) => !(row.entity === 'product' && row.stage === 'category-attach')),
1459
+ ...collectCategoryAttachStageResiduals(categoryAttachReport),
1460
+ ],
1461
+ };
1462
+ ```
1463
+
1464
+ `reconcileCategoryAttachment` calls the writer with the exact shape it declares --
1465
+ `bulkAddItemToCategories(wix, { productId, categoryIds })` -- for every row, never swallows a
1466
+ write error (each is caught per-row and recorded with its message), and then reads every written
1467
+ product back before marking it attached. `bulkAddItemToCategories` is an ADD, not a replace: the
1468
+ read-back only ever confirms every expected categoryId is present in the product's actual set,
1469
+ never exact set equality, so a product's other, unrelated category memberships are never
1470
+ disturbed or required. A row can resolve to `category-attach-written` (confirmed by read-back),
1471
+ `category-attach-reconciled-existing` (already carried every expected id, no write needed),
1472
+ `category-attach-unverified` (write attempted, read-back never confirmed within the retry
1473
+ budget -- not the same as failure, and must never be reported as one), or
1474
+ `category-attach-failed` (the write call itself threw). `collectCategoryAttachStageResiduals`
1475
+ must be called and its output folded into the shared `completionInput.stageResiduals` as shown
1476
+ above, on every run that calls `reconcileCategoryAttachment` -- persisting `categoryAttachReport`
1477
+ alone and stopping there reproduces the exact silent-failure shape this file exists to stop: a
1478
+ `category-attach-failed` or `category-attach-unverified` row that never reaches the final
1479
+ completion report, so the import reports `complete` while categories are missing. On a project
1480
+ where category-attach really is the only/last stage, extend `completionInput` here exactly as
1481
+ shown, then run the ordinary completion block (as the verified-contact-import or
1482
+ order-history-completion examples do) once, after this point -- do not special-case this
1483
+ section into writing early just because, on that particular project, it happens to run last.
1484
+
1485
+ Track source-side gaps too, the same way `stores-inventory.js` already asks its own caller to:
1486
+ "Keep source gaps and failed product-to-variant mappings in `inventoryGaps` so every intended
1487
+ source variant is counted" is a caller responsibility, not something the shared helper derives on
1488
+ its own, because it can only account for rows it is actually given. The code that resolves source
1489
+ categories into `categoryAttachRows` has the same obligation for category attachment: track any
1490
+ source category that fails to resolve to a target category id (a `categoryAttachGaps`-style list)
1491
+ so every intended source-to-category link is counted, not only the ones that made it into a row.
1492
+
1493
+ ### Blog post author attribution (required)
1494
+
1495
+ **Never hardcode a single `WIX_BLOG_AUTHOR_MEMBER_ID` and attribute every imported post to the
1496
+ site owner.** `draftPost.memberId` is required by the Blog API and Wix Blog has no "unauthored"
1497
+ option, but that does not make owner-attribution an acceptable default for every post. A
1498
+ generated importer that did exactly this carried the same owner memberId on every imported post,
1499
+ while the WordPress source named distinct authors (`_embedded.author`) that the
1500
+ transform/import layers silently discarded. The fix is not "use the
1501
+ owner" — it is to resolve a real Wix member per distinct source author, per Wix's own "How to
1502
+ Create Blog Posts" skill doc (dev.wix.com/docs/api-reference/business-solutions/blog/skills/
1503
+ how-to-create-blog-posts, "Part 0: Get an Author/Member ID"): when there is no existing member to
1504
+ reuse for an author, **create one** — its `profile.nickname` (derived from `contact.firstName`)
1505
+ is the name the blog displays, so pass the real author's name when the source names one. Create
1506
+ Member sends no email and fires no signup-automation trigger (documented at `wix-writers.js`'s
1507
+ members section) — this is silent, not a notification risk.
1508
+
1509
+ **No source-reader change is needed.** WordPress's `/wp/v2/posts?_embed` (already how this
1510
+ project's source reader populates `data/source-extract/post.ndjson`, per
1511
+ `rp-source-wordpress/SKILL.md`'s "Embedded relations" guidance) inlines `_embedded.author[0].id`
1512
+ and `_embedded.author[0].name` on every post whose author resolved. The bug was purely that the
1513
+ blog transform discarded these fields rather than carrying them through to the import stage.
1514
+
1515
+ ```js
1516
+ const { resolveBlogAuthorMemberId } =
1517
+ require('../resources/rp-target-wix/lib/blog-author-attribution.js');
1518
+ const memberCrosswalk = require('./src/lib/member-crosswalk.js').createProjectMemberCrosswalk({
1519
+ projectDir: ROOT, sourceEntityType: 'blog-author',
1520
+ });
1521
+ // post.sourceAuthorId / post.sourceAuthorName: carried through from the post transform's
1522
+ // `_embedded.author[0].id` / `.name` (undefined/null when the source gave no author at all).
1523
+ const authorResolution = await resolveBlogAuthorMemberId(wix, {
1524
+ sourceAuthorId: post.sourceAuthorId,
1525
+ sourceAuthorName: post.sourceAuthorName,
1526
+ crosswalk: memberCrosswalk,
1527
+ siteId,
1528
+ ownerMemberId, // the existing single-owner fallback, resolved once via listMembers as today
1529
+ });
1530
+ if (authorResolution.created) {
1531
+ // Caller records the crosswalk row -- resolveBlogAuthorMemberId only resolves/creates, it never
1532
+ // persists, mirroring the member-pass's own ensure/persistCrosswalk split above.
1533
+ await memberCrosswalk.put(post.sourceAuthorId, {
1534
+ memberId: authorResolution.memberId, loginEmail: authorResolution.loginEmail,
1535
+ });
1536
+ }
1537
+ draftPost.memberId = authorResolution.memberId;
1538
+ ```
1539
+
1540
+ - **One member per distinct source author, never per post.** `resolveBlogAuthorMemberId` looks
1541
+ up the crosswalk by `sourceAuthorId` first and reuses whatever member id it already holds —
1542
+ the same WordPress author across many posts, or a resumed/re-run import, must never create a
1543
+ second member for the author it already provisioned. Never call `createMember` directly for a
1544
+ blog author from generated code.
1545
+ - **Fall back to single-owner attribution only when the post has no source author at all.** Not
1546
+ every migration source populates author info on every record; pass `ownerMemberId` (the
1547
+ existing fallback, resolved the same way it is today) and let the helper return
1548
+ `blog-author-owner-fallback` rather than making author resolution a hard requirement that
1549
+ blocks import when the data simply is not there.
1550
+ - **The placeholder email is deterministic, non-deliverable, and collision-free, never the real
1551
+ author's email.** `blog-author-{hash(sourceAuthorId)}@{hash(siteId)}.invalid` — an `.invalid`
1552
+ TLD (RFC 2606) guarantees it never resolves or delivers. Each component is a hash of the RAW
1553
+ value (not a human-readable sanitization of it): two different raw `sourceAuthorId` values can
1554
+ never collide onto the same token, which a lossy sanitization (stripping/collapsing characters)
1555
+ could not guarantee (`'1-2'` and `'1@2'` would collide under a sanitized-token scheme). The token is opaque and carries no part of the real name; the
1556
+ real name goes ONLY into `profile.nickname`/`contact.firstName`, which is the whole point of
1557
+ the fix.
1558
+ - **Respects the documented Create Member rate-limit floor** (`MEMBER_WRITE_MIN_INTERVAL_MS`,
1559
+ the same constant `ensureMember`'s own pacer uses) — do not add a second hand-rolled wait loop
1560
+ around the generated call site.
366
1561
 
367
1562
  ### Record streams are NDJSON, single documents are JSON (required)
368
1563
 
@@ -423,10 +1618,10 @@ const existing = await W.queryStoresCategories(wix); // an array
423
1618
  const existing = (await W.queryStoresCategories(wix)).categories; // WRONG → undefined → []
424
1619
  ```
425
1620
 
426
- The second form is what shipped, and reading `.categories` off an array yields `undefined`,
427
- which the usual `|| []` turns into an empty array. It silently disabled a category dedupe
428
- index, silently disabled a product name-match safety net, and made a setup verification
429
- report **0 categories on a site that had 25** — all without one error line.
1621
+ In the second form, reading `.categories` off an array yields `undefined`,
1622
+ which the usual `|| []` turns into an empty array. It silently disables a category dedupe
1623
+ index, silently disables a product name-match safety net, and makes a setup verification
1624
+ report **0 categories on a site that has some** — all without one error line.
430
1625
 
431
1626
  **Never cursor-page through those executors.** Unwrapping discards `pagingMetadata`, so the
432
1627
  cursor a loop needs is already gone; a loop built on them cannot advance past page one, and
@@ -489,9 +1684,22 @@ Two properties to preserve when a generated transform sets a slug explicitly:
489
1684
  the original `sourceSlug` alongside the `plannedTargetSlug` actually derived from it (see the
490
1685
  URL preservation rules under "Codegen rules"), which a silent rewrite inside the writer would
491
1686
  falsify. `normalizeStoresProductV3` therefore passes a slug through untouched.
492
- - **`toWixSlug` throws when a value sanitizes to empty** (an all-non-latin title, for example).
493
- That is a signal to supply a deterministic fallback — the record's source id — not to omit the
494
- slug and let Wix derive one, which breaks URL preservation with no trace.
1687
+ - **`toWixSlug` throws when a value sanitizes to empty** (all-illegal-punctuation/whitespace
1688
+ input, for example — an all-non-Latin title does NOT throw: `toWixSlug`'s sanitizer keeps
1689
+ Unicode letters, marks and decimal digits, so an all-Hebrew or all-Chinese title survives as a
1690
+ transliterated slug instead of collapsing to empty). A genuinely empty result is a signal to
1691
+ supply a deterministic fallback — the record's source id — not to omit the slug and let Wix
1692
+ derive one, which breaks URL preservation with no trace.
1693
+
1694
+ **WordPress source: decode before sanitizing (required).** WordPress's REST API
1695
+ (`wp/v2`/`wc/v3`) returns a post/product's `slug`/`post_name` percent-encoded whenever the
1696
+ original text has non-Latin characters (Hebrew, etc.) — see `rp-source-wordpress/SKILL.md`.
1697
+ Sanitizing that raw value strips the `%` characters and ships a garbled hex-with-dashes slug
1698
+ instead of the real one. Decode it with `decodeURIComponent` first (wrapped in try/catch: a
1699
+ slug that was never percent-encoded, or that isn't a valid escape sequence, must pass through
1700
+ unchanged rather than throw) and only then hand the result to `toWixSlug`/`coerce: 'slug'`.
1701
+ This is documented as domain knowledge on `rp-target-wix`'s `stores/entities/product.json` and
1702
+ `blog/entities/post.json` pitfalls.
495
1703
 
496
1704
  ## Sample-preview gate
497
1705
 
@@ -513,6 +1721,11 @@ Required whenever the source cannot be read back from a live API — in particul
513
1721
  4. Pause and ask the user to validate the structure. On accept, set `status: "accepted"` with
514
1722
  `decidedAt`/`decidedBy`; on reject, set `status: "rejected"` and set
515
1723
  `approvals.mapping.status` back to `pending` so the router returns to `rp-mapper`.
1724
+ In explicit user-requested `1-click mode` (`automationMode=one_click`, `source=user`),
1725
+ validate the structure automatically from the preview artifacts, set the result to
1726
+ `accepted` with `decidedBy: "agent"` when it passes, and continue without a user pause.
1727
+ Otherwise every CSV run pauses here, including a run whose `one_click` value was inferred
1728
+ or copied with a non-user decision source.
516
1729
  5. Record the artifacts on the codegen checkpoint (`checkpoints.codegen.artifactRefs` +
517
1730
  `lastCompletedStep: "codegen.sample-preview"`).
518
1731
 
@@ -548,6 +1761,7 @@ load these files before reading config:
548
1761
  WP_BASE_URL=
549
1762
  WP_USERNAME=
550
1763
  WP_APPLICATION_PASSWORD=
1764
+ WMH2_MIGRATION_KEY=
551
1765
  WP_MEDIA_URL_REWRITE_FROM=
552
1766
  WP_MEDIA_URL_REWRITE_TO=
553
1767
  WC_CONSUMER_KEY=
@@ -587,7 +1801,7 @@ Codegen rules:
587
1801
  - In dry-run, missing or blank `WIX_AUTH_TOKEN` and `WIX_SITE_ID` are `would_block_live`
588
1802
  findings, not blockers, unless a generated local artifact requires the site ID as a
589
1803
  stable namespace. Do not mint a Wix CLI token solely for dry-run.
590
- - `WIX_MUTE_NOTIFICATIONS` (spec 0012) resolves by strategy when blank: `new` → `on`,
1804
+ - `WIX_MUTE_NOTIFICATIONS` resolves by strategy when blank: `new` → `on`,
591
1805
  `existing` → `off`; record the resolved value explicitly (mirrored from
592
1806
  `orchestration/decisions.json`). **`WIX_MUTE_NOTIFICATIONS=off` together with
593
1807
  `WIX_SITE_STRATEGY=new` fails codegen validation** — halt with the config conflict
@@ -598,10 +1812,9 @@ Codegen rules:
598
1812
  point where generated code needs to construct Wix clients or destination-specific
599
1813
  artifacts. For `WIX_SITE_STRATEGY=new`, codegen should fail with a clear message to
600
1814
  create/select the new Wix site first rather than assuming an existing site flow.
601
- - For RePlatform `new site` + `headless`, that unblock message must point back to the **Wix
602
- CLI headless scaffold** in `replatform` → "Headless site creation"
603
- (`npm create @wix/new@latest headless`). The account-level Projects API is deprecated for
604
- this workflow.
1815
+ - For RePlatform `new site` + `headless`, that unblock message must point back to
1816
+ `resources/rp-destination/`, which scaffolds via `npm create @wix/new@latest headless`.
1817
+ The account-level Projects API is deprecated for this workflow.
605
1818
  - `WIX_AUTH_TOKEN` is the canonical Wix auth key in project-local config and holds the
606
1819
  **site write credential** for import. With CLI-scaffolded headless sites this is a
607
1820
  short-lived CLI token sent as a **Bearer** token — the generated Wix client must send
@@ -682,7 +1895,7 @@ credential to stdout which lands in the transcript. Always use `mint-token.sh`.
682
1895
  do not generate it from scratch:
683
1896
 
684
1897
  ```bash
685
- cp skills/replatform/resources/rp-execute-setup/scripts/mint-token.sh \
1898
+ cp skills/wix-replatform/resources/rp-execute-setup/scripts/mint-token.sh \
686
1899
  migrations/<project>/scripts/mint-token.sh
687
1900
  ```
688
1901
 
@@ -694,6 +1907,532 @@ pre-import flow is:
694
1907
  3. run the import — the generated client reads `WIX_AUTH_TOKEN` and sends it as
695
1908
  `Authorization: Bearer <token>` with `wix-site-id`.
696
1909
 
1910
+ ## Members: full-fidelity create/reconcile, so orders can link to a buyer
1911
+
1912
+ `resolveOrderBuyer` (below) links an order to a member only when the member crosswalk row for that
1913
+ source customer carries a `loginEmail` — nothing produces that row unless the member pass runs
1914
+ FIRST, in its own loop, before order history. Generate the call, not a hand-rolled `createMember`.
1915
+
1916
+ ```js
1917
+ const CM = require('./src/resources/rp-target-wix/lib/customer-member.js');
1918
+ const MC = require('./src/lib/member-completion.js');
1919
+ const localState = require('./src/lib/local-state.js');
1920
+ const path = require('node:path');
1921
+ const { execFileSync } = require('node:child_process');
1922
+ const memberCrosswalk = require('./src/lib/member-crosswalk.js').createProjectMemberCrosswalk({ projectDir: ROOT });
1923
+
1924
+ // BEGIN member-pass
1925
+ // registeredCustomers: EVERY customer supplied to this pass (registered only -- guest buyers are
1926
+ // out of scope here), each carrying the mapping's sourceStableKey. Pass the full list on subset passes too:
1927
+ // the pass selects within it.
1928
+ const memberPass = await MC.runMemberPass({
1929
+ projectDir: ROOT, records: registeredCustomers, runId, phase: 'import', writeSpecId: 'members',
1930
+ mode: selectionMode, // 'full' | 'failed-only' | 'deferred-only' | 'unresolved-only' | 'missing-only'
1931
+ sample: dryRun || sampleMode, // sample journal path, and no population snapshot write
1932
+ sourceFile: customersExtractPath, sourceChecksum: customersExtractSha256,
1933
+ ensure: async (customer) => {
1934
+ const { contact, findings } = CM.buildMemberContact({
1935
+ firstName: customer.first_name, lastName: customer.last_name,
1936
+ phone: customer.billing && customer.billing.phone, countryCode: customer.billing && customer.billing.country,
1937
+ address: customer.billing && { addressLine: customer.billing.address_1, addressLine2: customer.billing.address_2, city: customer.billing.city, subdivision: customer.billing.state, postalCode: customer.billing.postcode, country: customer.billing.country },
1938
+ });
1939
+ const known = await memberCrosswalk.get(customer.id);
1940
+ const knownMemberId = known && known.memberId;
1941
+ const result = await w.ensureMember(wix, { loginEmail: customer.email, contact, knownMemberId });
1942
+ return { result, builderFindings: findings, knownMemberId };
1943
+ },
1944
+ persistCrosswalk: (customer, result) => memberCrosswalk.put(customer.id, { memberId: result.memberId, loginEmail: customer.email, contactId: result.contactId }),
1945
+ });
1946
+ // Load the journal ONCE here and thread the same rows into both this projection
1947
+ // and the `attachAttemptReasons` join at the final `createCompletionReport` call below --
1948
+ // `projectMemberPass`'s own `journalRows` option exists exactly so a report with a member
1949
+ // breakdown never reads the journal twice.
1950
+ const journal = await localState.loadAttemptJournal(ROOT, { sample: dryRun || sampleMode });
1951
+ const memberProjection = await MC.projectMemberPass(ROOT, { runId, pass: memberPass, journalRows: journal.rows });
1952
+ completionInput = MC.mergeMemberCompletion(completionInput, memberProjection.completion);
1953
+ await MC.emitMemberTelemetry(memberProjection.telemetryEvents, (event) => execFileSync('node',
1954
+ [path.join(toolkitRoot, 'resources/rp-telemetry/scripts/rp-telemetry.js'), 'record', JSON.stringify(event), '--project', ROOT]));
1955
+ if (!memberPass.ok) console.error('member pass refused: source keys missing, duplicated or unknown; no member was written');
1956
+ // END member-pass
1957
+ ```
1958
+
1959
+ `selectionMode` comes from the importer's own `--failed-only` / `--deferred-only` /
1960
+ `--unresolved-only` / `--missing-only` flags (`'full'` when none); `completionInput` is the run's
1961
+ `let`-declared held completion input that the order-history block below also extends;
1962
+ `customersExtractPath`/`customersExtractSha256` name the extraction file the customers came from.
1963
+
1964
+ What the call does, so the generated code must not:
1965
+
1966
+ - **Converts the phone to E.164 or drops it, never sends it as-is and never guesses.** Create
1967
+ Member 400s on anything that is not already E.164 (VERIFIED LIVE). `normalizePhoneE164` covers a
1968
+ small set of countries (`customer-member.js`'s `COUNTRY_PHONE_RULES`); an unlisted country is a
1969
+ finding, not a guess -- extend the table rather than hand-rolling a conversion in generated code.
1970
+ - **Queries Contacts by phone before every create that carries one.** Create Member reuses an
1971
+ EXISTING contact that already carries the phone being sent -- even one belonging to a member
1972
+ deleted earlier in the same run (deleting a member does not delete its contact). Two source
1973
+ customers who share a phone would otherwise be silently merged into one Wix identity. Never call
1974
+ Create Member directly with a phone; `ensureMember` is where this guard lives.
1975
+ - **Reconciles by the crosswalk row first (read back and verified), then by `loginEmail`.** One
1976
+ match reconciles, more than one is ambiguous and creates nothing. A stale crosswalk row (the
1977
+ member was deleted, or the row points at a different email) falls through to the `loginEmail`
1978
+ query rather than being trusted blind.
1979
+ - **Persist `loginEmail` on the crosswalk row, or `resolveOrderBuyer` cannot link anything.** The
1980
+ member pass must run to completion (or at least for every registered buyer the order pass will
1981
+ see) BEFORE the order-history loop below.
1982
+ - **Every customer's outcome is durable and reported.** `runMemberPass` writes a
1983
+ `started` row before `ensureMember` and a terminal row with the same `attemptId` after it (the
1984
+ crosswalk row first when a member id came back), in `state/attempts/write-attempts.ndjson`.
1985
+ Never add a private member results log, a hand-rolled `report(result)`, or a second tally.
1986
+ - **The whole pass stops before any write when source keys are bad.** A missing or duplicate
1987
+ `sourceStableKey`, or (on a subset pass) a key the full pass never saw, means zero journal rows
1988
+ and zero Wix calls; the refusal is projected into the report (`incomplete_with_mismatches`)
1989
+ and one `pipeline_defect` event. Never deduplicate or skip records to get past it -- fix the
1990
+ mapping. A full pass writes `execution/member-population.json` (keys only); subset passes
1991
+ select within it, never shrink the report's denominator, and refuse when it is absent.
1992
+ - **The report reads only the journal.** `projectMemberPass` folds the latest member row per
1993
+ key into the `members` completeness row (with the `unresolved` counter), `unresolved[]`,
1994
+ `droppedFields[]`, one operator action for ambiguous matches, and `memberOutcomes`. Declare no
1995
+ separate members row. Uncertain writes (409 already-exists, 5xx/transport/no-id creates, a
1996
+ dangling start) are **unresolved**: recover with `--unresolved-only`, which reads the member
1997
+ back by login email before any create. Emit the returned telemetry events once, after the pass;
1998
+ a telemetry failure never aborts the run.
1999
+
2000
+ ## Order history: payments, refunds and preserved invoice documents
2001
+
2002
+ An imported order reads back with `payments: []` — Import Order creates no payment record — and
2003
+ the order write has no idempotency of its own unless it is given an id. Payment history, refunds
2004
+ and the invoice document are separate target writes with an ordering constraint, a shared
2005
+ identity and state that must outlive a crash. **All of that is sequenced by one library call.
2006
+ Generate the call, not the sequence.**
2007
+
2008
+ ```js
2009
+ const H = require('./src/resources/rp-target-wix/lib/order-history.js'); // vendored layout, see below
2010
+ const { resolveOrderBuyer } = require('./src/resources/rp-target-wix/lib/order-buyer.js');
2011
+ const crosswalk = H.createProjectOrderCrosswalk({ projectDir: ROOT }); // state/crosswalk/crosswalk.ndjson
2012
+ const setupVerification = readJson('setup/extended-field-verification.json'); // ONCE per run
2013
+ const orderResults = []; // compact final stage outcomes, no payment payloads
2014
+
2015
+ for (const source of orders) {
2016
+ const result = await H.importOrderHistory(wix, {
2017
+ order: toWixOrder(source), // the mapped Import Order body — never set `id`; buyerInfo.email = source.billing.email
2018
+ sourceId: source.id,
2019
+ sourceSiteHost, // new URL(WP_SITE_URL).host
2020
+ evidence: { // FACTS from the source, nothing else
2021
+ amount: source.total,
2022
+ paidDate: source.date_paid_gmt, paidDateZone: 'utc',
2023
+ altPaidDate: source.date_paid, altPaidDateZone: 'site-local',
2024
+ currency: storeCurrency, // rp-source-wordpress/lib/store-currency.js, not order.currency
2025
+ status: source.status,
2026
+ methodName: gateway.displayName, providerTransactionId: gateway.reference, offlinePayment: gateway.offline,
2027
+ refunds: qualified.map((r) => ({ sourceId: r.id, amount: r.amount, reason: r.reason })),
2028
+ invoice: approval && { approval, sourceUrl: approval.documentUrl, invoiceNumber: approval.number },
2029
+ },
2030
+ crosswalk,
2031
+ setupVerification,
2032
+ // the buyer, corroborated from the member crosswalk (rows carry memberId + loginEmail)
2033
+ // contactCrosswalk is OPTIONAL (a guest/contact-only buyer's contactId, when one is tracked
2034
+ // separately from members) -- omit the property entirely when there is no such crosswalk in
2035
+ // this project, never pass a variable that is not actually declared (it throws ReferenceError:
2036
+ // contactCrosswalk is not defined the moment this line runs, not at require() time).
2037
+ buyer: await resolveOrderBuyer({ sourceCustomerId: source.customer_id, billingEmail: source.billing.email, memberCrosswalk }),
2038
+ });
2039
+ // BEGIN order-stage-result
2040
+ orderResults.push({ sourceKey: source.sourceStableKey, outcomes: result.outcomes });
2041
+ // END order-stage-result
2042
+ // result.findings also carries the contact-field codes: order-contact-email-dropped,
2043
+ // order-contact-phone-dropped (removed after Import Order named the field), order-contact-email-suspect,
2044
+ // order-contact-phone-suspect (warnings; value kept and sent) and buyer-email-unusable.
2045
+ report(result); // result.outcomes.{order, payment, refunds[], invoice, buyer}, result.findings, result.errors
2046
+ }
2047
+
2048
+ // BEGIN order-history-completion
2049
+ const { collectOrderStageResiduals, createCompletionReport, writeCompletionReport, attachAttemptReasons } =
2050
+ require(require('node:path').join(toolkitRoot, 'lib/completion-report.js'));
2051
+ // Reuse the SAME journal rows the member pass above already loaded (when
2052
+ // this run has one -- `typeof journal === 'undefined'` means it does not) rather than reading
2053
+ // the journal again here; join them in before building the report.
2054
+ const orderHistoryJournal = typeof journal === 'undefined'
2055
+ ? await require(require('node:path').join(toolkitRoot, 'lib/local-state.js')).loadAttemptJournal(ROOT)
2056
+ : journal;
2057
+ const completion = createCompletionReport({
2058
+ ...completionInput,
2059
+ entityCompleteness: attachAttemptReasons(completionInput.entityCompleteness, orderHistoryJournal.rows, { runIds: [runId] }),
2060
+ orderHistoryInScope: true,
2061
+ stageResiduals: [
2062
+ ...(completionInput.stageResiduals || []).filter((row) => row.entity !== orderEntity),
2063
+ ...collectOrderStageResiduals(orderResults, { entity: orderEntity }),
2064
+ ],
2065
+ });
2066
+ await writeCompletionReport(ROOT, completion, { runId });
2067
+ // END order-history-completion
2068
+ ```
2069
+
2070
+ A run with no member pass still loads the journal once, right before this call:
2071
+ `const journal = await localState.loadAttemptJournal(ROOT, { sample: dryRun || sampleMode });` --
2072
+ never once per entity, and never a second time for a report the member pass already loaded it for.
2073
+
2074
+ Before the loop, mapping supplies each order's `sourceStableKey` and `orderEntity` (the
2075
+ same entity name used by the completeness counters). `completionInput` contains the final
2076
+ held counters for the run. Collect the latest outcomes for every in-scope order, including
2077
+ orders adopted from the crosswalk; persist the compact outcomes with batch accounting and
2078
+ restore them on resume. Run the completion block once after the last write, including recovery,
2079
+ and only for a live run. Dry runs retain the existing simulated-report path. Do not substitute
2080
+ an empty residual list or derive it by counting retry attempts.
2081
+
2082
+ What the composite does, so the generated code must not:
2083
+
2084
+ - **Derives the order id** from `sourceSiteHost` and `sourceId` (Import Order accepts a
2085
+ caller-supplied GUID — verified live 2026-09-05), writes the crosswalk row before any further
2086
+ write, and on a crosswalk miss asks the target by that id. A lost crosswalk is a read, not a
2087
+ duplicated store. An order that already exists is reconciled, never re-sent: a re-send silently
2088
+ replaces the whole body.
2089
+ - **Runs the payment gate from `evidence`.** A key the contract does not read (`sourceAmount`,
2090
+ `total`, `transaction_id`, …) throws, naming the key it should have been. It does not read as
2091
+ "never paid".
2092
+ - **Derives each paid stamp under the clock YOU declare for it**, which is why the two paid-date
2093
+ lines above are paired with a zone. The source records one payment twice: the `_gmt` sibling is
2094
+ universal by that key's definition in the source schema, so it never takes a shop offset, and the
2095
+ plain field is the shop's wall clock with the zone stripped off, so it takes the shop's offset
2096
+ when one is known and carries `paid-date-timezone-assumed` when one is not. Which field is which
2097
+ is a fixed fact about the source platform, known before any shop is examined — state it here
2098
+ rather than leaving it to be inferred from a value that no longer carries the distinction.
2099
+ Supply `utcOffsetMinutes` as before when the shop's offset is known; it stays a fact about the
2100
+ SHOP, and "already universal" is `paidDateZone: 'utc'`, never `utcOffsetMinutes: 0`. Supplying
2101
+ the offset beside a present stamp whose clock you have not stated is REFUSED, naming the stamp:
2102
+ that is exactly the case where guessing the clock moves the payment by hours.
2103
+ - **Reads the order's payments once** and reconciles both the payment and every refund against
2104
+ that read. Occurrence numbers for equal refunds are assigned from the refunds' source ids — pass
2105
+ ALL of an order's refunds in the one call.
2106
+ - **Returns every outcome**, including the payment's when a refund exists, and a named
2107
+ partial-failure outcome (`payment-failed`, `refund-failed`, `invoice-failed`, with the cause in
2108
+ `errors`) instead of throwing, so a resume retries only what failed.
2109
+ - **Sanitises the mapped body's contact fields before the request is built**.
2110
+ `sanitizeOrderContactFields` walks `safeModeContactFields[]` from
2111
+ `rp-target-wix/domains/ecom/entities/order.json` — the same six paths safe mode replaces — and
2112
+ either rewrites a phone into E.164 under the explicit country rule or removes the field and
2113
+ emits `order-contact-email-dropped` / `order-contact-phone-dropped` carrying the field and a
2114
+ stable reason, never the value. A removed buyer email also produces `buyer-email-unusable`,
2115
+ because "we had a customer and lost them" must be countable apart from a real guest.
2116
+ **Do not re-implement this in the generated runner and do not sanitise the body yourself**: a
2117
+ second pass would double-count the findings, and one placed after the composite would run
2118
+ after safe mode has already replaced every one of those paths with an always-valid mock. One
2119
+ bad email or phone fails the entire Import Order, so this is a local refusal before the
2120
+ network, never a send-then-retry loop.
2121
+ - **Sets `buyerInfo.memberId` only from `resolveOrderBuyer`** (`rp-target-wix/lib/order-buyer.js`):
2122
+ a member crosswalk row whose `loginEmail` equals the order's billing email. Persist `loginEmail`
2123
+ on the member crosswalk row when creating members, or nothing can be linked. Never put a
2124
+ `memberId` on the order body yourself; the composite refuses it.
2125
+ - **Binds the body's `buyerInfo.email` to the buyer it was resolved for.** The mapped body must carry
2126
+ the order's billing email (the same value passed as `billingEmail`); a linked buyer beside a body
2127
+ carrying another email, or none, is refused — a resolution for one order must not attribute
2128
+ another order to that member.
2129
+ - **Handles contact fields Import Order refuses.** A contact
2130
+ field is removed only when Import Order's 400 names it in `fieldViolations[]` (exactly a
2131
+ `safeModeContactFields[]` path, and no other violation); the order is then sent once more under
2132
+ the same id, and each removal is an `order-contact-*-dropped` finding naming the field, never the
2133
+ value. A second refusal is `order-import-failed`. Before the first send, and so before safe mode,
2134
+ the composite checks the source values and emits `order-contact-*-suspect` warnings without
2135
+ changing them. Phones are never rewritten. On a `buyer-member-linked` order the retry keeps
2136
+ `buyerInfo.memberId` and `contactId`. The generated runner must not remove, rewrite or pre-check
2137
+ contact fields itself, and must not add its own retry around the composite.
2138
+ - **Hands the invoice approval to the writer unchanged.** The merchant opt-in is an input to
2139
+ `resolvePreservationPlan()`, which STAMPS every approval it issues; the writer believes an opt-in
2140
+ only with that stamp, so an approval assembled by hand (or lifted from the plan's `refused` list)
2141
+ is refused as `invoice-preservation-not-approved`. Pass the plan's objects, never rebuild them.
2142
+ - **Remembers a document that reached Media but whose order patch failed** — in the crosswalk
2143
+ (`state/crosswalk/pending-invoice-files.ndjson`), so the resume records that file instead of
2144
+ importing a second private copy. No caller state; `createProjectOrderCrosswalk` holds it.
2145
+ - **Verifies every crosswalk hit with one order read.** A row pointing at an order the target no
2146
+ longer has (a migration cleared with Bulk Delete Imported Orders) is re-imported under the
2147
+ derived id and counted as `order-reimported-after-deletion`.
2148
+
2149
+ What is still the caller's, because it is source knowledge:
2150
+
2151
+ - Which processor a payment-method slug names: `rp-source-wordpress/lib/gateway-source-keys.js`.
2152
+ - Whether a refund is real: `lib/refund-corroboration.js` `qualifyRefund()` (its
2153
+ `assignRefundOccurrences()` is no longer needed — the composite assigns them).
2154
+ - Which document and whether it may be copied: `lib/invoice-discovery.js` through
2155
+ `resolvePreservationPlan({ selection, merchantOptedIn })`.
2156
+ - The store currency, resolved once at extract time — `order.currency` on WooCommerce REST is not
2157
+ reliably ISO 4217 (a live store returned the HTML-encoded shekel symbol on every order).
2158
+ - The verification receipt, read once, and the report: every `outcomes` value is a line for
2159
+ `rp-execution-policy/lib/payments-report.js`, including the zeros.
2160
+
2161
+ Vendoring mirrors the skill's layout: the shared modules require each other across resources
2162
+ (`../../../lib/…`), so `src/` reproduces `lib/` + `resources/<resource>/lib/` — `order-history.js`
2163
+ lives at `src/resources/rp-target-wix/lib/` and reaches `src/lib/local-state.js`. A flat copy loads
2164
+ nothing.
2165
+
2166
+ **Two dependency shapes `require()` alone will not surface, both hit live 2026-09-06 running a
2167
+ generated-style project end to end:** a lib can read a **domain entity JSON file directly**
2168
+ (`order-invoice-contract.js` reads `../domains/ecom/entities/order-invoice-document.json` with
2169
+ `path.join` + `readFileSync`, not `require`, so a dependency scan that only follows `require` calls
2170
+ misses it — vendor `domains/ecom/entities/order-invoice-document.json` alongside
2171
+ `order-invoice-contract.js`), and a lib can have a **sibling JSON registry** next to its own `.js`
2172
+ file (`gateway-source-keys.js` → `gateway-source-keys.json`, `refund-corroboration.js` →
2173
+ `refund-corroboration.json`) that must be copied too, or it throws `ENOENT` the first time the
2174
+ function that reads it is actually called — not at `require()` time, so a smoke test that only
2175
+ does `require()` on every vendored file will not catch a missing one. Copy the `.json` file
2176
+ wherever you copy the `.js` file next to it.
2177
+
2178
+ If the source has no payment evidence at all, make the call anyway with what evidence there is and
2179
+ let it report zero under a named reason. An importer that omits the path is indistinguishable from
2180
+ one that ran and found nothing, and only one of those is a correct migration.
2181
+
2182
+ **Never call the invoicing service**, and never generate a fallback that does. Re-issuing a
2183
+ historical tax document mints a second legal document under a different number.
2184
+ `tests/target-wix/no-invoices-api-contract-test.js` scans the published tree for this.
2185
+
2186
+ ## Coupons, currency and pricing-plan definitions
2187
+
2188
+ Generate three passes, in this order, and one report section. The decisions are in the libraries;
2189
+ codegen supplies reads and facts.
2190
+
2191
+ 1. **Currency gate, before any order write.** Read `wc/v3/settings/general` →
2192
+ `store-currency.js` `readStoreCurrency`; resolve every order's currency with
2193
+ `resolveOrderCurrency(order, storeCurrency)` (an order field can be an encoded currency symbol such as `&#8362;`);
2194
+ read the target site currency; call `currency-gate.js` `evaluateCurrencyGate`. `halt: true`
2195
+ stops the run at the approval gate with the named finding. `currency-validated` is a line too.
2196
+ 2. **Coupons.** `sweepCoupons(page => wc/v3/coupons?per_page=100&page=N)` to exhaustion; the
2197
+ `posts` count by `post_status` through the plugin bridge for reconciliation;
2198
+ `summarizeCoupons` for the counts; per coupon `mapCouponToWix(coupon, { resolveProduct,
2199
+ resolveCategory })` through the catalog crosswalks, then write them in BATCHES with
2200
+ `bulkCreateCoupons(wix, specifications)` -- 100 per call, and it returns one outcome per input
2201
+ specification in input order, so the coupon crosswalk row (`code → couponId`) is written per
2202
+ coupon exactly as before and a re-run still resumes. A few thousand coupons is ordinary for
2203
+ this source, and `ensureCoupon` one at a time makes it a few thousand round trips; reach for
2204
+ `ensureCoupon` only for a single coupon or a repair.
2205
+ ⚠️ **Batching and tagging cannot be combined.** The batch discards the tag field and still
2206
+ answers 200, so `bulkCreateCoupons` refuses a tagged specification. Pick one and report which:
2207
+ write tagged coupons with `ensureCoupon` one at a time, or batch them untagged and then tag
2208
+ each with `updateCouponFields({ tags })`, which is a call per coupon.
2209
+ ⚠️ **When most or all of the batch needs a tag (e.g. the `Imported` /
2210
+ `Already_Used` / `Partially_Used`), the untagged-batch-then-tag path loses its throughput
2211
+ advantage entirely.** `updateCouponFields`'s field-level PATCH is read-back VERIFIED
2212
+ (its own TRAP 3) with a mandatory settle-and-verify wait, so once close to 100% of coupons
2213
+ need that follow-up call, every coupon pays the wait regardless of the batch step ahead of
2214
+ it, which makes the run slow end-to-end. Prefer per-coupon
2215
+ `createCoupon(wix, specification)` instead, with `tags` already in the specification: the
2216
+ normal-case response is the documented `{ id }`, trusted immediately with no settle wait,
2217
+ which is far faster. A small tagged
2218
+ minority still nets out better as bulk-untagged + per-item `updateCouponFields`, since only
2219
+ that minority pays the wait; this is specifically for the near-all-tagged case. Note that
2220
+ plain `createCoupon` does not reconcile a `code already exists` rejection the way
2221
+ `ensureCoupon` does — it is for writing coupons not yet attempted, not for idempotent replay
2222
+ over a batch that was already partly written; a resumed or repaired pass still needs a
2223
+ query-by-code reconciliation step for any coupon a prior attempt may have already created.
2224
+ ⚠️ Do NOT re-implement the batch call. `bulkCreateCoupons` already handles the two traps that
2225
+ make it dangerous: a batch fails as a whole while reporting the same error against every row
2226
+ in it (so any failure re-sends that batch as individual creates), and `originalIndex` is
2227
+ omitted on the first result (so results are correlated by index, never by position, and a
2228
+ duplicate or incomplete index cover reconciles per coupon rather than guessing). Outcomes
2229
+ `coupon-reconciled-existing` and `coupon-write-unknown` are not failures: the first recovered
2230
+ an id for a coupon that was already there, the second means the code exists but is not
2231
+ readable yet and must be re-read rather than re-written. A
2232
+ `coupon-type-unmappable` / `coupon-scope-unmappable` / `coupon-usage-limit-unresolved` result
2233
+ is a report line, not a write. `sweepCoupons` runs to an EMPTY page when the server sends no
2234
+ `X-WP-Total`; pass the header through as `total` whenever it is present.
2235
+
2236
+ **Emit this call; do not describe it.** `toolkitRoot` is the installed or vendored
2237
+ wix-replatform root. `fetchCouponPage(page)` returns `{ items, total }` from
2238
+ `wc/v3/coupons?per_page=100&page=N` with `X-WP-Total` as `total`; `readFreshCoupon(id)` returns
2239
+ `wc/v3/coupons/{id}` re-read NOW, or `null` if that read failed; `recordCouponCrosswalk(code,
2240
+ couponId)` is the run's crosswalk writer; `couponUpdateOptions` is the shared runtime's settle
2241
+ options for the read-back-verified PATCH.
2242
+
2243
+ ```js
2244
+ // BEGIN verified-coupon-import
2245
+ const couponPath = require('node:path');
2246
+ const { sweepCoupons, mapCouponToWix, summarizeCoupons } = require(couponPath.join(toolkitRoot, 'resources/rp-source-wordpress/lib/coupon-discovery.js'));
2247
+ const { classifySnapshot, isBearerVoucher, classifyPreWriteDrift } = require(couponPath.join(toolkitRoot, 'resources/rp-target-wix/lib/coupon-redemption-drift.js'));
2248
+ const { bulkCreateCoupons, updateCouponFields } = require(couponPath.join(toolkitRoot, 'resources/rp-target-wix/lib/wix-writers.js'));
2249
+
2250
+ // Gate the WHOLE pass: a snapshot that cannot be dated is refused exactly like a stale one.
2251
+ const snapshot = classifySnapshot({ capturedAt, now: Date.now(), maxAgeMs });
2252
+ if (!snapshot.fresh) throw new Error(`coupon pass halted: ${snapshot.verdict} -- ${snapshot.reason}`);
2253
+
2254
+ const { coupons: restCoupons } = await sweepCoupons(fetchCouponPage);
2255
+ const mappings = [];
2256
+ const toWrite = [];
2257
+ const notWritten = [];
2258
+ const writeOutcomes = [];
2259
+ for (const coupon of restCoupons) {
2260
+ // The mapper decides redeemability from the source usage count and REFUSES what it cannot
2261
+ // express. A project-local toSpecification is not an acceptable substitute -- see the table.
2262
+ const mapping = mapCouponToWix(coupon, { resolveProduct, resolveCategory });
2263
+ mappings.push(mapping);
2264
+ if (!mapping.specification) { notWritten.push({ code: coupon.code, outcome: mapping.outcome, reason: mapping.reason }); continue; }
2265
+ let specification = mapping.specification;
2266
+ if (isBearerVoucher(coupon)) {
2267
+ const verdict = classifyPreWriteDrift({ snapshotCoupon: coupon, freshCoupon: await readFreshCoupon(coupon.id) });
2268
+ if (!verdict.write) { notWritten.push({ code: coupon.code, outcome: verdict.outcome, reason: verdict.reason }); continue; }
2269
+ // The verdict IS the final state. Rebuild from it; never send the mapper's snapshot.
2270
+ specification = { ...specification, active: verdict.writeActive, tags: verdict.setTags };
2271
+ if (verdict.usageUnlimited) { delete specification.usageLimit; specification.usageUnlimited = true; }
2272
+ else if (verdict.writeUsageLimit != null) specification.usageLimit = verdict.writeUsageLimit;
2273
+ }
2274
+ toWrite.push({ code: specification.code, specification });
2275
+ }
2276
+ // 100 per call. The batch silently discards `tags` and still answers 200, so it goes out
2277
+ // untagged and each written coupon is tagged afterwards with the minimal-specification PATCH.
2278
+ for (let i = 0; i < toWrite.length; i += 100) {
2279
+ const batch = toWrite.slice(i, i + 100);
2280
+ const outcomes = await bulkCreateCoupons(wix, batch.map(({ specification }) => { const { tags, ...untagged } = specification; return untagged; }));
2281
+ for (let j = 0; j < outcomes.length; j += 1) {
2282
+ writeOutcomes.push(outcomes[j].outcome);
2283
+ if (!outcomes[j].couponId) continue;
2284
+ await recordCouponCrosswalk(batch[j].code, outcomes[j].couponId);
2285
+ const tags = batch[j].specification.tags;
2286
+ if (tags && tags.length) await updateCouponFields(wix, outcomes[j].couponId, { tags }, couponUpdateOptions);
2287
+ }
2288
+ }
2289
+ const couponCounts = summarizeCoupons({ restCoupons, postsByStatus, mappings, writeOutcomes });
2290
+ // END verified-coupon-import
2291
+ ```
2292
+
2293
+ ⚠️ **A project-local `toSpecification` is not an acceptable substitute for `mapCouponToWix`.**
2294
+ It is not a style preference. The mapper performs a list of refusals that do not survive being
2295
+ re-written by hand, and each one exists because the alternative writes something the merchant
2296
+ never published:
2297
+
2298
+ | Refusal the mapper performs | What a hand-rolled transform does instead |
2299
+ | --- | --- |
2300
+ | Exhausted at source → `active: false`, tagged `Already_Used` | Writes it live |
2301
+ | Partly used → the uses **left**, tagged `Partially_Used` | Writes the source's original allowance |
2302
+ | Unreadable usage fields → `coupon-usage-limit-unresolved`, no write | Writes an undecidable money instrument live |
2303
+ | Unmappable discount type → `coupon-type-unmappable`, no write | Guesses a type, or drops a free gift silently |
2304
+ | Scope the target cannot hold → `coupon-scope-unmappable`, no write | Widens it to the whole store |
2305
+ | More than two decimal places → `coupon-amount-precision-unsupported`, no write | Rounds, so the target amount differs while every check reports a match |
2306
+ | Free shipping beside an amount → keeps free shipping, reports the dropped amount | Sends both and loses one silently |
2307
+
2308
+ ⚠️ The writer now REFUSES a specification that never decided redeemability: `active` must be
2309
+ present and boolean, and `usageLimit` must be a positive integer unless `usageUnlimited: true`
2310
+ is present. The refusal fires in `ensureCoupon`, `bulkCreateCoupons` and
2311
+ `normalizeCouponSpecification`, before any network call, and it names the coupon code and the
2312
+ missing field. It is a CREATE-path guard only — `updateCouponFields` must keep carrying only
2313
+ the fields being changed.
2314
+
2315
+ 2a. **Decide a coupon's final shape from the FRESH read, not from the mapped snapshot**
2316
+ (`rp-target-wix/lib/coupon-redemption-drift.js`). Gate the whole coupon pass on
2317
+ `classifySnapshot({ capturedAt, now, maxAgeMs })` and halt on `snapshot-stale` or
2318
+ `snapshot-undated`.
2319
+
2320
+ Then, for each coupon where `isBearerVoucher(coupon)` is true and only those, re-read
2321
+ `wc/v3/coupons/{id}` immediately before writing and pass both rows to `classifyPreWriteDrift`.
2322
+ **The verdict is not a boolean gate — it is the specification's final state**, and the payload
2323
+ must be rebuilt from it rather than from the mapper's output:
2324
+
2325
+ | Verdict field | What the outgoing specification must carry |
2326
+ | --- | --- |
2327
+ | `write: false` | nothing — the coupon is not written at all; report the outcome |
2328
+ | `writeActive` | `specification.active`. `false` for a coupon the fresh read found used up |
2329
+ | `writeUsageLimit` | `specification.usageLimit` — the uses the holder has LEFT |
2330
+ | `usageUnlimited: true` | **delete** `specification.usageLimit` entirely; a stale limit left on an unlimited coupon caps it |
2331
+ | `setTags` | `specification.tags` — replaces whatever the mapper stamped |
2332
+
2333
+ ⚠️ Mapping and then only checking `write` is the failure this table exists to prevent: a coupon
2334
+ spent after the snapshot comes back `write: true, writeActive: false` with `Already_Used`, and a
2335
+ payload built from the mapper alone goes out `active: true` tagged only `Imported` — the
2336
+ redeemable-again defect, reintroduced one layer down.
2337
+
2338
+ **Why it cannot be skipped:** the target starts its own usage count at zero and nothing seeds a
2339
+ prior redemption, so a coupon used up between the read and the write lands fully redeemable and
2340
+ stays that way until somebody notices.
2341
+
2342
+ ⚠️ `write: false` has two distinct causes and they are different report lines, not one:
2343
+ `voucher-usage-unknown-at-write` (the fresh read failed) and
2344
+ `voucher-face-value-precision-unsupported`. Neither is a skip.
2345
+
2346
+ ⚠️ **Precision is gated for EVERY monetary coupon, not just bearer vouchers**, and that gate is
2347
+ in the mapper: `mapCouponToWix` returns `coupon-amount-precision-unsupported` and no
2348
+ specification for any amount carrying more than two decimal places. A fixed-money coupon with a
2349
+ usage limit of 6, or a percentage of `12.3456`, never reaches `classifyPreWriteDrift` at all —
2350
+ it is not a bearer instrument — and would otherwise put an amount on the target that differs
2351
+ from the source while every later check reported a match.
2352
+
2353
+ **Report lines.** `coupons-spent-after-capture` and `coupons-partially-used-limit-reduced` are
2354
+ sub-counts of `coupons-written` — those coupons ARE created, just not as the snapshot described
2355
+ them — and must NOT be added to the accounting identity. The identity term is
2356
+ `coupons-face-value-precision-unsupported`, because that coupon is refused:
2357
+
2358
+ ```
2359
+ discovered = written + type-unmappable + scope-unmappable
2360
+ + usage-limit-unresolved + face-value-precision-unsupported + skipped
2361
+ ```
2362
+
2363
+ 2b. **Reconcile voucher drift at cutover, never inside the import.** A trading source keeps
2364
+ spending vouchers after the import finishes. When the source carried at least one
2365
+ usage-limited coupon, generated code **MUST** emit the reconciliation entrypoint — a separate,
2366
+ re-runnable entrypoint the operator invokes at `rp-execute-import` workflow step 6b, not a
2367
+ branch of the coupon pass. Emitting it is a requirement, not a choice; an entrypoint that is
2368
+ never emitted is a step that is never skipped and never noticed.
2369
+ `classifyPostImportDrift` + `buildVoucherDriftPlan` take the receipts, a fresh source read and
2370
+ a target read-back and produce a read-only remedy plan (face value and affected orders per
2371
+ code). The entrypoint BUILDS and reports that plan and **must not act on it** — closing a
2372
+ voucher needs an explicit owner decision per code, including in one-click mode. The plan stays
2373
+ `readOnly: true`; do not add an apply path.
2374
+ ⚠️ `classifyPostImportDrift` returns exactly five post-import outcomes
2375
+ (`voucher-drift-reconciled`, `voucher-drift-remedy-pending`, `voucher-source-row-missing`,
2376
+ `voucher-target-row-missing`, `voucher-target-state-unreadable`). Unsupported face-value
2377
+ precision is the boolean `verdict.facePrecisionUnsupported` on whichever of those applies —
2378
+ wire the check on that flag, never as a `switch (verdict.outcome)` branch on
2379
+ `voucher-face-value-precision-unsupported`, which is a PRE-WRITE outcome and would produce a
2380
+ branch nothing reaches. Feeding a pre-write outcome to `buildVoucherDriftPlan` throws.
2381
+ The remedy is `buildRemedyRequest`: a `PATCH` carrying ONLY
2382
+ `{active: false, tags: [...]}` plus `fieldMask: {paths: ['active','tags']}`, which keeps the
2383
+ coupon id, code, dates and amount. **Never build that body from the coupon you just read** —
2384
+ the full specification alongside a narrow mask returns 200 and writes nothing, so counting 2xx
2385
+ responses produces a false pass on money. `verifyRemedyApplied` decides the outcome from a
2386
+ post-settle read-back, never from the status code. The entrypoint reports its result as the
2387
+ completion report's `voucherReconciliation` value (`not-applicable | reconciled |
2388
+ plan-pending-owner | remedied`); a run where it never executed records `not-run`.
2389
+ 3. **Coupon usage**, inside the order pass: `buildCouponAppliedDiscounts({ couponLines:
2390
+ order.coupon_lines, resolveCoupon })` and put `appliedDiscounts` on the mapped order body handed
2391
+ to `importOrderHistory`. Name the surface (`rest-order-coupon-lines` on legacy storage,
2392
+ `wc_order_coupon_lookup` on HPOS, `none-found`).
2393
+ 4. **Plan definitions.** Row counts for `B1`/`B2`/`B3` → `evaluatePlanSources`; when it runs,
2394
+ normalize each source offer to `{ sourceId, name, description, amount, period, interval,
2395
+ lengthCycles, trialDays, signupFee }`, `buildPlanDefinition`, `ensurePlanDefinition`. Install
2396
+ the Pricing Plans app first (`WIX_PRICING_PLANS_APP_DEF_ID`); it is absent on a commerce site.
2397
+ 5. **Report** through `rp-execution-policy/lib/coupon-currency-plan-report.js`
2398
+ `buildCouponCurrencyPlanReport({ counts, notPublishedByStatus, couponUsageSurface,
2399
+ voucherLiabilityLive })`. Build
2400
+ `counts` from ONE surface: `summarizeCoupons({ restCoupons, postsByStatus, mappings,
2401
+ writeOutcomes })`, where `mappings` is the per-coupon `mapCouponToWix` result (it owns the
2402
+ unmappable numerators — the type table alone under-counts) and `writeOutcomes` the per-coupon
2403
+ `ensureCoupon` outcome (`coupon-reconciled-existing` folds into `coupons-written`; failed,
2404
+ read-failed and ambiguous fold into `coupons-skipped` with reasons as notes). Plans fold the
2405
+ same way through `tallyPlanWriteOutcomes`. Every Appendix D line, zeros included; `written +
2406
+ unmappable + skipped = discovered` or the gate blocks; "imported N coupons" and "not supported"
2407
+ are refused.
2408
+
2409
+ ## Shipping options: delivery estimate, verified write, recovery
2410
+
2411
+ The shipping stage runs after the source extract. After it has created the delivery regions, it
2412
+ creates shipping options **only** through
2413
+ `rp-target-wix/lib/shipping-option-stage.js` `runShippingOptionStage(wix, { projectDir,
2414
+ regionIdByZone, sourceSystem, runId, phase, passMode })`. Do not hand-write this loop:
2415
+
2416
+ - It refuses to run without the final `data/source-extract/shipping-zone.ndjson` and its
2417
+ `shipping-zone.manifest.json`; a `.tmp` extract is still in progress.
2418
+ - It writes `execution/shipping-estimates.json` (the per-option estimate resolution) **before**
2419
+ any write. Every payload carries only `estimatedDeliveryTime` (never `estimateDeliveryTime`),
2420
+ resolved as merchant decision (`source: "user"` only) > source text > the reviewed default in
2421
+ the **destination** site's language > none. It never writes English on a non-English site.
2422
+ - It uses `createShippingOptionVerified` (create, read back, one estimate-only PATCH when the
2423
+ stored estimate is blank) and `recoverShippingOption` (read back, never re-send). It appends
2424
+ exactly what `shippingOptionWriteRecord` returns. Hand-built shipping accounting is not allowed.
2425
+ - Crosswalked options always take the read-only path, in every pass mode, and are never POSTed.
2426
+ A 404 target raises `shipping-option-target-missing` and is never recreated automatically.
2427
+ - It creates nothing in a region whose snapshot scan was incomplete (`SNAPSHOT_INCOMPLETE`) or
2428
+ after an unknown outcome there (`REGION_SNAPSHOT_UNPROVABLE`). The next run rescans the region.
2429
+
2430
+ The stage returns `fragment`. Fold it into the completion input with
2431
+ `shipping-build.js` `mergeShippingEstimateCompletion(completionInput, fragment)` before
2432
+ `createCompletionReport`. That replaces the previous run's shipping `operatorActions`, so a
2433
+ resolved item drops out on the next run. Pass `passMode` from the run's recovery flag
2434
+ (`unfiltered`, `missing-only`, `unresolved-only`, `failed-only`).
2435
+
697
2436
  ## Localhost media sources
698
2437
 
699
2438
  If the source profile shows `localhost`, `127.0.0.1`, or another private-only source URL,
@@ -734,17 +2473,24 @@ Write code under the project-local source tree:
734
2473
  - `migrations/<project>/src/setup/`
735
2474
  - `migrations/<project>/src/extract/`
736
2475
  - `migrations/<project>/src/import/`
737
- - `migrations/<project>/src/lib/` — vendored shared modules (the source adapter's transport
738
- module, `rp-target-wix/lib/ndjson.js`, `rp-target-wix/lib/wix-writers.js`,
739
- `rp-target-wix/lib/wix-target-spec.js`, `rp-target-wix/lib/wix-build.js`), copied here so
740
- the project runs standalone with no external deps
2476
+ - `migrations/<project>/src/lib/` — the source adapter's transport module, plus the project's
2477
+ own local-state/runtime files
2478
+ - `migrations/<project>/src/resources/rp-target-wix/lib/` — **`wix-build.js` and its siblings
2479
+ are NOT copied flat into `src/lib/`: `ndjson.js`, `wix-writers.js`, `wix-target-spec.js` and
2480
+ `wix-build.js` all vendor NESTED here.** Vendor `rp-target-wix/lib/` here preserving its own relative path (mirroring this
2481
+ skill's own `resources/rp-target-wix/lib/` layout exactly), so the project runs standalone
2482
+ with no external deps and every relative `require` between these files — and every relative
2483
+ `require` FROM a `src/lib/` file back into this directory (e.g. `write-verification.js`'s own
2484
+ `../resources/rp-target-wix/lib/wix-build.js`) — resolves unchanged from how it resolves
2485
+ in-repo. Do not flatten this vendoring; a flattened copy breaks any relative require that
2486
+ assumes the nested layout.
741
2487
  - `migrations/<project>/src/extract/readers/`
742
2488
  - `migrations/<project>/src/import/transforms/` — **thin glue only.** Vendor and call
743
2489
  `wix-build.js`; do **not** re-derive the canonical→Wix payload layer here. Money objects, slug
744
2490
  sanitization, the empty `physicalProperties` trap, choice-by-name variant references, the
745
2491
  compare-at-must-exceed-price rule and the regular/sale price resolution are all settled in
746
2492
  `wix-target-spec.js` + `wix-build.js` and regression-locked by
747
- `tests/mapping/wix-build-oracle-test.js` against 220 payloads from two live-verified imports.
2493
+ `tests/mapping/wix-build-oracle-test.js` against payloads recorded from live-verified imports.
748
2494
  That layer is vendor-independent, so a per-project reimplementation only re-introduces bugs
749
2495
  that were already found and fixed. This directory should hold at most the mapping from THIS
750
2496
  project's reader field names onto the canonical vocabulary — and once the reader emits canonical
@@ -761,7 +2507,9 @@ Write code under the project-local source tree:
761
2507
  - `migrations/<project>/state/crosswalk/crosswalk.ndjson` — authoritative local
762
2508
  `sourceId -> targetId` crosswalk for native Wix entities
763
2509
  - `migrations/<project>/state/attempts/write-attempts.ndjson` — append-only write attempt
764
- journal for resume/reconciliation
2510
+ journal for resume/reconciliation. When a write consumes blocked-source data, include
2511
+ `blockedDataSnapshot: {sourceEntityRef, snapshotVersion, extractedAt, checksum}` from the exact
2512
+ immutable snapshot loaded for that write.
765
2513
  - `migrations/<project>/state/url-preservation/base-paths.json` — entity-level public
766
2514
  route intent from mapping `urlPolicy`
767
2515
  - `migrations/<project>/state/url-preservation/url-ledger.ndjson` — append-only URL
@@ -770,6 +2518,10 @@ Write code under the project-local source tree:
770
2518
  when source and target relative URLs are both known and differ
771
2519
  - `migrations/<project>/state/url-preservation/unresolved.ndjson` — public source URLs
772
2520
  waiting on target route configuration, target lookup, or manual review
2521
+ - `migrations/<project>/state/url-preservation/app-page-routes.json` — configured or
2522
+ unresolved per-family route results; saved-state read-back is distinct from served proof
2523
+ - `migrations/<project>/state/url-preservation/link-audit.json` — atomic local census,
2524
+ pending/unresolved destination queues, writer integration and post-frontend evidence
773
2525
  - `migrations/<project>/logs/` — audit/error logs emitted by the shared runtime
774
2526
  - `migrations/<project>/execution/review/import-plan.md`
775
2527
  - `migrations/<project>/execution/review/code-safety-review.md` when `SAFE_MODE=true` or
@@ -778,6 +2530,39 @@ Write code under the project-local source tree:
778
2530
  actions emitted by every resumed, partial, missing-only, failed-only, or deferred-only
779
2531
  run
780
2532
 
2533
+ For app routing and internal-link reconciliation, generate `internal-link-audit.js plan` input from the
2534
+ captured inventory and **confirmed** source-to-target crosswalk, not guessed slugs.
2535
+ Supply declared exact source/destination hosts, containing source pages and their target
2536
+ record type, target record ID, content field and planned `targetContentRevision`, the current
2537
+ route read-back revision, target record IDs, page families, expected paths computed from
2538
+ read-back patterns and slugs, explicit `supportedQueryKeys` when the target adapter has
2539
+ verified query behavior, and per-family samples. Missing target identity goes to the
2540
+ unresolved queue without an expected path. Generate a route transport only for a family
2541
+ listed with exact request/read-back evidence in `rp-target-wix` capability knowledge;
2542
+ other families stay unverified. The adapter for the later `verify` step must expose the current complete active-writer
2543
+ inventory and order, `readRecord(id)` returning `{revision, fields}`,
2544
+ `writeRecord(id, changedFields, baseRevision)`, read-only destination/sample checks,
2545
+ and `checkRenderedLink(occurrence, {repairedPath, deploymentId, signal})` that confirms the
2546
+ stored path is rendered. For fields owned by the shared non-blog reconciler, declare
2547
+ `writer: "internal-link-reconciler"`, `compatible: true` and
2548
+ `runsAfter: "post-frontend-verification"` in `getActiveWriters()`; include every other active
2549
+ writer and its fields in that complete inventory. Supply `checkFragment` where anchor checks are supported. The
2550
+ shared verifier groups links by containing logical record and field, applies
2551
+ `lib/structured-link-patch.js`, journals before writing, checks revisions and reads back;
2552
+ do not expose a per-occurrence `repair` callback. Unknown content revisions stay pending.
2553
+ An interrupted response after a write resumes from journaled read-back without another
2554
+ mutation. Unsupported rich-content nodes remain pending rather than replacing an entire
2555
+ description with a path. Do not generate a blog-post link writer here: blog-link repairs
2556
+ belong to the dedicated blog-post writer and its checkpointed post-frontend update,
2557
+ publish and read-back lifecycle. The adapter must expose `inspectDestination()` and return `isLive: false` from a
2558
+ current destination check before any route or content write; unknown/live state fails
2559
+ closed. An
2560
+ unknown or incompatible source-origin or content writer keeps affected fields pending before any
2561
+ mutation. These are generated contracts, not evidence that a backend-only run has served
2562
+ routes or repaired content. If the audit is at `budget-hold`, a same-run re-plan retains
2563
+ the hold, calibration and elapsed time. Resume only with `budgetDecision: {decisionRef,
2564
+ maxLiveMs, maxExceptionTokens}` from an explicit recorded owner decision; then run `verify` again.
2565
+
781
2566
  Recommended machine-readable artifacts:
782
2567
 
783
2568
  - `migrations/<project>/execution/setup-plan.json`
@@ -808,7 +2593,8 @@ Each write spec should define, as applicable:
808
2593
  - `cmsMirror: "none" | "download" | "upload" | "download-and-upload"`
809
2594
  - `crosswalkStrategy`
810
2595
  - `reconciliationStrategy`
811
- - `dependencyRefs`
2596
+ - `dependencyRefs`: derive from each in-scope entity's domain-knowledge `dependsOn`,
2597
+ topologically sorted across the whole mapping plan — not re-derived from prose per project.
812
2598
  - `verificationLevel`: `verified | unverified`
813
2599
  - `safeMode.replacePaths[]`: resolved request-body paths copied from mapper
814
2600
  `safeModeReplacements[]`, with `{ kind: "email" | "phone", path }` entries. Paths use
@@ -864,8 +2650,44 @@ not configure Wix redirects or site routes.
864
2650
 
865
2651
  ## Import completeness accounting contract
866
2652
 
867
- Generated import runners must treat completeness accounting as a first-class runtime
868
- output. For each mapped entity class and meaningful subtype, the runner must produce an
2653
+ Generated import runners must also produce `stageResiduals[]` rows — a per-stage failure
2654
+ against records the entity row already counts as `imported`:
2655
+
2656
+ ```
2657
+ { entity, stage, outcome, count, sourceKeys[] }
2658
+ ```
2659
+
2660
+ The runner calls `collectOrderStageResiduals` on the final order outcomes as shown above.
2661
+ It counts failed payment/refund/invoice stages (including target-read failures and ambiguous
2662
+ payment reconciliation) against
2663
+ orders whose own outcome is `order-imported` or `order-already-imported`. `outcome` must be a
2664
+ member of the payments outcome registry; anything else throws. Any row with `count > 0` forces
2665
+ the completion status to no better than `incomplete_with_failures` and names the affected
2666
+ source keys as recovery work. An order report with omitted `stageResiduals` is refused; pass
2667
+ the collected array even when it is empty. For source-specific entity names, retain
2668
+ `targetEntityType: 'ecom/order'` in the completeness input or set `orderHistoryInScope: true`.
2669
+
2670
+ This is an **orthogonal axis** and is never folded into `failed`: the order did import, and the
2671
+ `imported + alreadyPresentByCrosswalk + deferred + failed == inScope` identity below exists to
2672
+ protect that partition. Without these rows a shop with live unpaid orders on it reconciles
2673
+ perfectly and reports clean.
2674
+
2675
+ **This requirement is not order-specific.** Any post-create sub-step whose failure must not be
2676
+ hidden behind the parent entity's own `imported`/`alreadyPresentByCrosswalk` count needs its own
2677
+ tracked residual -- the order payment/refund/invoice sub-stages above are the first instance of
2678
+ this, not the only one it applies to. Product category attachment
2679
+ (`stores-category-attach.js`, see "Product category attachment: attach, then verify" above) is
2680
+ the second: `collectCategoryAttachStageResiduals(categoryAttachReport)` produces
2681
+ `{ entity: 'product', stage: 'category-attach', outcome, count, sourceKeys }` rows the same way
2682
+ `collectOrderStageResiduals` does for orders, and they fold into the same shared
2683
+ `completionInput.stageResiduals[]` accumulator, read by whichever stage owns the single final
2684
+ `createCompletionReport` call. This is not hypothetical: a completion report can show `product: reconciled, mismatch:
2685
+ false` and `product-category: reconciled, mismatch: false` while no products actually carry
2686
+ their categories live, because nothing tracked the link step itself. A new entity gaining a
2687
+ post-create write that can fail independently of the parent create should be assumed to need the
2688
+ same treatment until shown otherwise.
2689
+
2690
+ For each mapped entity class and meaningful subtype, the runner must produce an
869
2691
  `entityCompleteness` row with these counters:
870
2692
 
871
2693
  - `extracted`
@@ -877,6 +2699,11 @@ output. For each mapped entity class and meaningful subtype, the runner must pro
877
2699
  - `failed`
878
2700
  - `skippedOutOfScope`
879
2701
  - `unexpectedSkipped`
2702
+ - `sourceKeys` -- every in-scope record's source key, required on every row. It is
2703
+ the only way `attachAttemptReasons` can join this row's deferred/failed counts back to the
2704
+ journal rows that name their reasons; a row with no `sourceKeys` can never carry a structured
2705
+ (counted) reason, only free-text notes, and is rejected by `normalizeCompletenessRow` if it
2706
+ tries to.
880
2707
 
881
2708
  Subtype is required whenever source records with the same entity map through materially
882
2709
  different write paths or deferral rules, for example simple vs subscription Stores
@@ -884,7 +2711,7 @@ products. `deferred` means intentionally not attempted with a recorded reason. `
884
2711
  means attempted and not successfully written. `unexpectedSkipped` is for in-scope mapped
885
2712
  records that were skipped outside an accepted out-of-scope rule.
886
2713
 
887
- The generated runner must call or vendor `skills/replatform/lib/completion-report.js` and
2714
+ The generated runner must call or vendor `skills/wix-replatform/lib/completion-report.js` and
888
2715
  write both:
889
2716
 
890
2717
  - `execution/live-import-summary.json` during/after live import for machine inspection
@@ -901,6 +2728,46 @@ Any reconciliation mismatch, non-zero `failed`, non-zero `deferred`, or non-zero
901
2728
  human completion summary. Do not report a run as cleanly complete when source, import, and
902
2729
  crosswalk counts do not reconcile.
903
2730
 
2731
+ **A record removed by an accepted out-of-scope rule is counted in `skippedOutOfScope` and
2732
+ never in `inScope`**: `skippedOutOfScope` is never added to `reconciled` and never
2733
+ subtracted from `expectedReconciled`, so `inScope + skippedOutOfScope <= extracted` must hold
2734
+ for every row. For example, a source with 2 shipping zones where one is an empty catch-all
2735
+ excluded by an accepted rule is `extracted 2, inScope 1, skippedOutOfScope 1, imported 1`, and
2736
+ it reconciles. Counting the excluded zone inside `inScope` as well (`extracted 2, inScope 2,
2737
+ skippedOutOfScope 1, imported 1`) breaks that identity and reports as a mismatch naming the
2738
+ counting error, not a lost record — it is not a correct way to represent an exclusion.
2739
+
2740
+ ### Every in-scope record leaves a durable row
2741
+
2742
+ No generated importer may end a source record's life without a row in
2743
+ `state/attempts/write-attempts.ndjson`, written through `localState.appendAttempt` (so it passes
2744
+ the current `validateAttemptRow`, including `runId` and `phase`). A count with no rows behind it
2745
+ is a defect, not a summary.
2746
+
2747
+ | Rule | |
2748
+ | --- | --- |
2749
+ | Terminal row per in-scope record | Every in-scope record ends with at least one terminal row. A record the runner never attempts gets a `deferred` or `skipped_unexpected` row at the point of the decision, before the loop continues |
2750
+ | Accepted skip | Excluded by an accepted source-data rule: `status: 'deferred'` |
2751
+ | Unexpected skip | Skipped without an accepted rule: `status: 'skipped_unexpected'`, never `deferred`, so it is not counted, reported or recovered as an accepted deferral |
2752
+ | Named reason | Both carry `reason`, a stable kebab-case code naming the rule or runner branch, never prose built from the record |
2753
+ | Findings ride along | A row for an attempted write carries the write's `findings[]`, suspects and drops alike |
2754
+ | Counts are backed by rows | Per `entityCompleteness` row, take each in-scope record's latest row in the current `runId` for that entity and subtype: `deferred` equals the number whose status is `deferred`, `unexpectedSkipped` the number whose status is `skipped_unexpected` |
2755
+ | Reasons are backed by rows | A `deferred` or `failed` count with no stated reason is not an accounted-for shortfall. Call `attachAttemptReasons(entityCompleteness, journal.rows, { runIds: [runId] })` once per report, after folding in any member fragment and before `createCompletionReport`, so every row's `reasons` carries a per-code tally (`reason` for `deferred`, `failure.errorCode` for `failed_retryable`/`failed_terminal`) joined by that row's own `sourceKeys` -- never guessed, never renamed, never read from the journal more than once per report |
2756
+ | No values | A row never carries a contact value: `sourceStableKey`, `field`, `reason` |
2757
+
2758
+ `skipped_unexpected` stays outside the reconciliation identity, so a non-zero count still makes
2759
+ the entity row mismatch; recovery treats it as missing (never sent), not as uncertain. Reporting
2760
+ which registered customers never received a login is the member-side instance of this rule; do not
2761
+ build it twice.
2762
+
2763
+ `writeLiveImportSummary`'s coverage check compares `entityCompleteness`'s neighbor,
2764
+ `execution/live-import-summary.json`, against these same journal rows. Omitting one of `imported`,
2765
+ `deferred`, `failed`, or `attempted` from a single flush is fine — that bucket is simply not
2766
+ compared for that entity, never treated as a mismatched `0`. `unexpectedSkipped` is the opposite:
2767
+ always name it explicitly, even as `0`, once this entity can produce a `skipped_unexpected` row —
2768
+ its absence defaults to a held `0` instead of "not compared", precisely so a silently-skipped
2769
+ record cannot agree by omission. See "Use this caller shape" above for the exact flush.
2770
+
904
2771
  ## Verifying Wix APIs
905
2772
 
906
2773
  **The primary control is the Target write contract above: call `rp-target-wix`'s
@@ -952,6 +2819,27 @@ exists:
952
2819
 
953
2820
  ## Codegen rules
954
2821
 
2822
+ - **Call a `rp-target-wix` writer with the exact parameter shape it declares -- usually one
2823
+ options object -- never extra positional arguments.** JS silently drops them, and a wrapping
2824
+ `catch` then hides the resulting undefined-field write failure. Not hypothetical: a generated
2825
+ call site once passed `bulkAddItemToCategories(wix, categoryIds, [productId])` against a
2826
+ `(wix, payload)` signature, and every category-attach write on that site silently failed.
2827
+ - **Generated code must not define its own test for whether an error is transient.** Call
2828
+ `isTransientWixError` from the shared `rp-target-wix` writer module. A project-local test
2829
+ written against the error's message text cannot work: the shared writer truncates its message
2830
+ at 400 characters by design, and its format (`\`${method} ${url} → ${status}: …\``) is not
2831
+ the WordPress reader's `HTTP <status>` format — so a regex written from memory of the source
2832
+ client matches nothing a destination write ever produces, and the project ships looking
2833
+ retry-protected while retrying nothing. Four generated importers each carried such a copy and
2834
+ not one of them could ever fire.
2835
+ - **Never generate a reader that works around a route the source adapter's transport refuses.**
2836
+ An adapter transport may refuse a route because its response carries live credentials — a
2837
+ payment-gateway configuration route is the case that exists today. When it does, the adapter
2838
+ exports a purpose-built reader that returns the facts without the secret; generate a call to
2839
+ that, and record what it reports. Do not hand-roll a request, do not vendor or reconstruct a
2840
+ raw transport, and do not add a flag to the shared one. A generated reader that reaches the
2841
+ route directly puts a live credential into the extract files, the import logs and the
2842
+ transcript, where nothing downstream can take it back.
955
2843
  - Keep reader and writer responsibilities separate.
956
2844
  - Keep setup, extraction, and import responsibilities separate.
957
2845
  - Do not generate a read-all-into-memory importer for the general case. The reader extracts
@@ -997,17 +2885,86 @@ exists:
997
2885
  `source_url_missing`, or `manual_review` when a concrete preserved URL or redirect
998
2886
  cannot be derived safely
999
2887
  - never silently overwrite a source slug with a normalized slug
2888
+ - For a source with an imported redirect collection, adapt only captured redirect rules
2889
+ into `kind: imported_redirect` records through `url-redirects.js sources`, then run
2890
+ `url-redirects.js import-targets` if source domains were declared after capture. The
2891
+ planner uses stable source-system rule IDs, original source and scalar destination,
2892
+ status, match type, scope, source context, explicit website identities, capture revision
2893
+ and rule-specific capability evidence. Never derive a rule
2894
+ ID from array position or target, infer website membership from a host suffix, or feed
2895
+ content/menu/message fields into this planner. A partial capture must set
2896
+ `fullCapture: false`; it cannot retire absent rules. Preserve external targets in the
2897
+ captured source records and audit; only cutover may execute after its compatible consumer
2898
+ and verification checks exist. A pending or refused rule must not be treated as imported.
2899
+ - Imports stay plan-only for redirects: never generate code that calls the SEO Redirects
2900
+ API. Record source-authored redirect definitions through
2901
+ `skills/wix-replatform/scripts/url-redirects.js sources` (bridge reads of the redirect
2902
+ plugins' tables by bare name, or an operator export) and finish with the offline
2903
+ `url-redirects.js report`; application and public verification are post-build helper
2904
+ operations scheduled by cutover.
1000
2905
  - URL ledger resume must replay the latest valid `url-ledger.ndjson` row by
1001
2906
  `sourceStableKey + sourceRelativeUrl`, derive expected rows from extracted source
1002
2907
  records and `urlPolicy`, merge target IDs from the local crosswalk, and query Wix only
1003
2908
  when the write spec declares the lookup safe.
1004
- - For native Wix entities, generate and use the shared local-state runtime
1005
- (`local-state.js`) whenever the target API does not expose a client-controlled
1006
- source-id field. Load `state/crosswalk/crosswalk.ndjson` at startup, rebuild missing or
2909
+ - **For EVERY native Wix entity, generate and use the shared local-state runtime
2910
+ (`local-state.js`) — there is no exception, and generated code must not create any other
2911
+ durable per-entity `sourceId -> targetId` store.** Not a per-entity JSON map, not a
2912
+ `<entity>-crosswalk.json`, not an in-project index of its own: one shared ledger at
2913
+ `state/crosswalk/crosswalk.ndjson`, written through the shared write primitives, for products
2914
+ and coupons and contacts and members exactly as for orders. A private per-entity file is
2915
+ invisible to resume, to recovery, and to the rerun/catch-up selection, all of which read the
2916
+ shared ledger and nothing else — so an entity remembered only in a private file is classified
2917
+ on the next run as never imported, and gets created a second time on a site the merchant is
2918
+ trading on. A run that keeps orders in the shared ledger and other entity types in private per-entity
2919
+ files leaves the ledger unusable for catch-up.
2920
+ Where the target *does* expose a client-controlled source-id field, write that field **in
2921
+ addition to** the ledger row, never instead of it.
2922
+ Load `state/crosswalk/crosswalk.ndjson` at startup, rebuild missing or
1007
2923
  corrupt index caches from it, append a `started` row to
1008
2924
  `state/attempts/write-attempts.ndjson` before each native write, write the confirmed
1009
2925
  crosswalk row after success, then mark the attempt `imported`. **Slug-based dedupe does
1010
2926
  NOT work** — Wix rewrites slugs; never rely on it.
2927
+ - **Every confirmed crosswalk row carries the version pair from the write that confirmed it.**
2928
+ Populate `sourceVersionField` + `sourceVersion` (from the source record's own modification
2929
+ field) and `targetRevision` (exactly as the Wix response returned it — never incremented
2930
+ locally) through `nextCrosswalkBaseline`, on every confirmed write. These fields are optional
2931
+ in the schema only so rows written before they existed still load; absence is read as
2932
+ `unknown`, never as "unchanged". A generated importer that omits them produces a ledger that
2933
+ is complete for resume and useless for catch-up: a later rerun of a fully-imported project
2934
+ degrades to asking a human about records nothing actually changed.
2935
+ - **The CMS mirror is a post-execution snapshot pass, generated only when a mirror was
2936
+ requested.** Gate it on `task.state.cmsMirror.mode` (`execution-state.js` →
2937
+ `hasUploadMirror`): `upload` and `download-and-upload` get exactly one pass; `none` and
2938
+ `download` get **nothing at all** — no collection reads, no writes, and no empty pass
2939
+ reported as a success. Generate it as a distinct step that runs **after the last entity
2940
+ write task**, reading the finished ledger:
2941
+ 1. `loadCrosswalk(projectDir)` for the settled rows.
2942
+ 2. `crosswalkRowsToMirrorItemInputs(rows)` from `local-state.js` — the one shared projection.
2943
+ A ledger row is **not** a mirror-item input: the ledger says `sourceEntityType` /
2944
+ `targetEntityType`, `buildImportCrosswalkItem` wants `entityType` / `targetType` and
2945
+ throws without the first. Never re-derive this mapping in the generated project.
2946
+ 3. `syncImportCrosswalk(wix, inputs, { collectionId })` from
2947
+ `rp-target-wix/lib/wix-writers.js`, which builds each item through
2948
+ `buildImportCrosswalkItem` and batches at 1000 through `bulkSaveDataItems`.
2949
+ **Never compose a mirror item id in generated code.** The id is the row's own
2950
+ `sourceStableKey`; adding a project or source-instance prefix is specifically forbidden (see
2951
+ the KNOWN, UNRESOLVED LIMITATION comment on `buildImportCrosswalkItem` — the cross-project
2952
+ collision it describes is real and stays open, and closing it needs an approved
2953
+ identity/schema decision, not a prefix invented in a writer).
2954
+ **Never write the mirror inline per entity or per batch checkpoint.** Riding a mirror write
2955
+ alongside each record multiplies the calls the site absorbs during the run and turns a slow
2956
+ or failing collection into a drag on the import it only describes; a compact post-run
2957
+ reference copy means one snapshot of a settled ledger.
2958
+ **Never let the mirror fail the import.** `syncImportCrosswalk` returns
2959
+ `{ totalSuccesses, totalFailures, errors }` and does not throw on per-item failure. Record all
2960
+ three in the execution artifacts; a non-zero `totalFailures` or a thrown transport error marks
2961
+ the **mirror step** failed and is reported as a shortfall, while every imported record stays
2962
+ imported and the run is not failed. Never make native-entity import correctness depend on the
2963
+ mirror succeeding.
2964
+ - When a transform consumes a fulfilled blocked-data request, carry its exact
2965
+ `sourceEntityRef`, snapshot version, extraction timestamp, and checksum into every related
2966
+ write-attempt row as `blockedDataSnapshot`. Do not substitute whatever snapshot is newest at
2967
+ report time; the journal records the version actually used to build that payload.
1011
2968
  - Only fetch CMS `ImportCrosswalk` rows as a pre-execution seed for existing-site,
1012
2969
  legacy, recovery, or delta flows, and only when valid local crosswalk state does not
1013
2970
  already exist. Dedupe downloaded mirror rows by `sourceStableKey` and newest valid
@@ -1024,7 +2981,7 @@ exists:
1024
2981
  - **Attach every related entity, don't just create it.** Creating a tag/category is not the
1025
2982
  same as linking it. Blog posts attach tags via `tagIds` (GUIDs) and categories via
1026
2983
  `categoryIds` on the draft-post create — collect the resolved ids and pass them, or the
1027
- taxonomy exists on the site but `postCount` stays 0 (a builder bug we hit before).
2984
+ taxonomy exists on the site but `postCount` stays 0.
1028
2985
  - **Taxonomy creates are not idempotent**: treat `409 ALREADY_EXISTS` as success
1029
2986
  AND resolve the existing entity's id (e.g. `listBlogTags`) so it can still be attached —
1030
2987
  don't drop it.
@@ -1033,6 +2990,89 @@ exists:
1033
2990
  or skip large posts. This is the **blog** path only — a blog post's `richContent` really is a
1034
2991
  Ricos document. Stores products must NOT use it: their HTML goes in `plainDescription` and Wix
1035
2992
  converts it server-side (see the product-description rule above).
2993
+ - **Blog post media: cover and inline images go through `blog-post-media.js`.** A non-zero
2994
+ WordPress `featured_media` is the post's cover; every `imageData.image.src.url` in the
2995
+ converted Ricos document is an inline image. Do not re-implement this, and do not parse
2996
+ raw HTML for images. **Failure policy (human-approved):** a post whose cover or any inline
2997
+ image does not reach `READY` is written nowhere — no draft, no publish, no crosswalk row —
2998
+ and stays pending and recoverable; the other posts continue. Never import the text with a
2999
+ missing cover or an inline image still pointing at the source site. The blog post build
3000
+ runner owns draft creation, finalization, publication, read-back and recovery. Do not
3001
+ emit an independent `createDraftPost`/`publishDraftPost` pair for blog posts. Prepare
3002
+ the inputs and call it once after all post bodies and media decisions are known:
3003
+
3004
+ ```js
3005
+ // BEGIN blog-post-media
3006
+ const blogMedia = require(require('node:path').join(toolkitRoot, 'resources/rp-target-wix/lib/blog-post-media.js'));
3007
+ const blogStage = require(require('node:path').join(toolkitRoot, 'resources/rp-target-wix/lib/blog-post-build-stage.js'));
3008
+ const { createBlogBuildClient } = require(require('node:path').join(toolkitRoot, 'resources/rp-target-wix/lib/blog-post-build-client.js'));
3009
+ const { toDraftPostBody } = require(require('node:path').join(toolkitRoot, 'resources/rp-target-wix/lib/wix-writers.js'));
3010
+ const pendingMedia = blogMedia.createPendingBlogMediaState({ projectDir });
3011
+ const preparedPosts = [];
3012
+ // fetchFeaturedMedia(id): GET wp/v2/media/{id} through the source client; return the record.
3013
+ for (const post of posts) {
3014
+ const key = blogMedia.postStableKey(post);
3015
+ const existing = await crosswalkLookup(key); // a hit still needs ledger/Wix reconciliation
3016
+ const richContent = await convertHtmlToRichContent(wix, post.contentHtml); // no mediaBySourceUrl
3017
+ let media;
3018
+ try {
3019
+ media = existing
3020
+ ? await blogMedia.existingPostMedia({ post, richContent, state: pendingMedia,
3021
+ resolveFromCrosswalk: lookupReadyBlogMedia })
3022
+ : await blogMedia.importAndConfirmPostMedia(wix, { post, richContent, state: pendingMedia, fetchFeaturedMedia });
3023
+ } catch (error) {
3024
+ if (error.code !== 'BLOG_LEGACY_MEDIA_UNRESOLVED') throw error;
3025
+ media = { allReady: false, resumed: false, reason: error.message };
3026
+ }
3027
+ if (media.allReady) blogMedia.applyPostMedia(richContent, media); // rewrites the SAME document
3028
+ const postFields = buildBlogPostFields(post); // mapped author/date, excerpt, taxonomy and SEO fields
3029
+ const desiredPost = media.allReady
3030
+ ? toDraftPostBody({ ...postFields, richContent, heroImageId: media.heroImageId }) : null;
3031
+ preparedPosts.push({
3032
+ sourceStableKey: key, id: post.id, date: postFields.firstPublishedDate || post.date,
3033
+ slug: postFields.slug, hasPostLinks: hasBlogPostLinks(post, richContent),
3034
+ forceSlugUpdate: Boolean(desiredPost && postFields.slug && !desiredPost.seoSlug),
3035
+ mediaReady: media.allReady, mediaPendingReason: media.reason || null,
3036
+ mediaResumed: media.resumed, existingCrosswalk: Boolean(existing),
3037
+ desiredPost,
3038
+ });
3039
+ }
3040
+ const { report: blogReport, pending: blogPending } = await blogStage.runBlogBuildStage({
3041
+ projectDir, sourcePosts: posts, preparedPosts,
3042
+ client: createBlogBuildClient(wix, { fetchPage, fetchImage }), pendingMedia, runId,
3043
+ crosswalk: { get: async (key) => crosswalkLookup(key), idOwners: (id) => crosswalkOwners(id),
3044
+ set: async (key, id) => updateBlogCrosswalkTarget(key, id),
3045
+ recordVerified: async (sourcePost, id) => writeBlogCrosswalk(sourcePost, id) },
3046
+ checks: registeredBlogChecks, transforms: registeredBlogTransforms });
3047
+ // END blog-post-media
3048
+ ```
3049
+
3050
+ Bind `lookupReadyBlogMedia` to the local media crosswalk (return a `ready` row with
3051
+ `fileId` and non-zero `sizeInBytes` for a cover), plus `crosswalkOwners`,
3052
+ `writeBlogCrosswalk`, `updateBlogCrosswalkTarget`, `buildBlogPostFields`, `hasBlogPostLinks`,
3053
+ `fetchPage`, `fetchImage`, and the
3054
+ registered checks/transforms to the generated importer's actual helpers. Never leave the
3055
+ illustrative helper names undefined in generated code. `fetchPage` must return status
3056
+ and HTML, and registered page checks must stay within the fixed sample budget.
3057
+ `hasBlogPostLinks` reads the source/converted post for links to another source blog post;
3058
+ it must return a boolean for every post so preflight can count the possible link update.
3059
+ Feed `blogReport.operatorActions` into the existing `createCompletionReport` input's
3060
+ `operatorActions`, merging by `code` so the spot-check appears once. The normalizer
3061
+ keeps its schema fields. Keep affected addresses and prompts in
3062
+ `state/crosswalk/blog-import-report.json`; the completion row points there. A later
3063
+ standalone ledger action calls `refreshBlogHandoff` to update the spot-check row.
3064
+ `state/crosswalk/pending-blog-media.ndjson` owns per-medium outcomes; the blog build ledger
3065
+ owns lifecycle holds, including legacy crosswalk hits without local READY evidence.
3066
+ Use the stage's merged `blogPending` result for completion counts and operator rows;
3067
+ never maintain a second pending list. A pending entry with
3068
+ `awaitingWrite: true` has every image `READY` but no recorded post write; it is still pending.
3069
+ `markResolved` and `closeIfCrosswalked` own the recovery-log entry and repair a missed one on
3070
+ the next run, so always call `closeIfCrosswalked` on the crosswalk-hit path. Put
3071
+ `blogPending.pendingMedia` in `execution/live-import-summary.json` beside imported and
3072
+ already-present blog posts, and list `blogPending.pending` (post and per-medium reason) in
3073
+ the operator report. A blog step with pending posts is not complete: count them as
3074
+ deferred in the completion report. This is separate from the "Localhost media sources"
3075
+ skip/defer mode, which is an operator choice, not a media failure.
1036
3076
  - Use the shared runtime for batching, retries, throttling, checkpoints, and audit/report
1037
3077
  emission rather than generating those mechanics ad hoc.
1038
3078
  - Use the deterministic execution-state preparation contract before native writes. Do not
@@ -1112,3 +3152,7 @@ Emit `execution/llm-handoff.json` with `needsLlm: true` when any of the followin
1112
3152
  ## Output
1113
3153
 
1114
3154
  Summarize which files were generated, which entities they cover, and any remaining implementation gaps.
3155
+
3156
+ ## Discovered sub-part outcome accounting
3157
+
3158
+ For every planned registry sub-part, generated importers emit its own `entityCompleteness` row using the registry subtype and back every count with journal rows, as for whole entities. Keep the whole-entity row separate. Attribute dropped sub-part fields with `droppedFields[].subtype` so a missing shipping line, for example, makes that sub-part partial without misreporting the parent row. When an exclusion corresponds to a discovered scope decision, include its `censusId`; `writeCompletionReport` derives `operatorGateRecorded` from the exact approved reconciliation, never from the importer's claim. Record-level filters without a census decision keep their existing exclusion behavior.