@wix/agent-skills 1.33.0 → 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.
- package/README.md +23 -0
- package/package.json +2 -2
- package/skills/wix-app/references/DASHBOARD_PAGE.md +1 -1
- package/skills/wix-export/SKILL.md +83 -0
- package/skills/wix-harmony-editor/.publish-manifest.json +8 -0
- package/skills/wix-harmony-editor/COPY-SITE.md +494 -0
- package/skills/wix-harmony-editor/SKILL.md +317 -0
- package/skills/wix-harmony-editor/scripts/copy/build-description.mjs +249 -0
- package/skills/wix-harmony-editor/scripts/copy/check-description.mjs +225 -0
- package/skills/wix-harmony-editor/scripts/copy/check-mirror.mjs +89 -0
- package/skills/wix-harmony-editor/scripts/copy/compare-preview.mjs +76 -0
- package/skills/wix-harmony-editor/scripts/copy/deploy-description.mjs +212 -0
- package/skills/wix-harmony-editor/scripts/copy/desc-hash.mjs +44 -0
- package/skills/wix-harmony-editor/scripts/copy/editor-run.mjs +45 -0
- package/skills/wix-harmony-editor/scripts/copy/export-page.mjs +53 -0
- package/skills/wix-harmony-editor/scripts/copy/find-interactions.mjs +76 -0
- package/skills/wix-harmony-editor/scripts/copy/hide-scrollbars.mjs +16 -0
- package/skills/wix-harmony-editor/scripts/copy/import-media.mjs +150 -0
- package/skills/wix-harmony-editor/scripts/copy/link-url.mjs +27 -0
- package/skills/wix-harmony-editor/scripts/copy/lint-api.mjs +56 -0
- package/skills/wix-harmony-editor/scripts/copy/measure-source.mjs +211 -0
- package/skills/wix-harmony-editor/scripts/copy/merge-mirrors.mjs +29 -0
- package/skills/wix-harmony-editor/scripts/copy/mirror-site.mjs +92 -0
- package/skills/wix-harmony-editor/scripts/copy/mobile-stage.mjs +56 -0
- package/skills/wix-harmony-editor/scripts/copy/preview-widths.mjs +73 -0
- package/skills/wix-harmony-editor/scripts/copy/render-plate.mjs +67 -0
- package/skills/wix-harmony-editor/scripts/copy/serve-mirror.mjs +102 -0
- package/skills/wix-harmony-editor/scripts/copy/snapshot-capabilities.mjs +40 -0
- package/skills/wix-harmony-editor/scripts/copy/split-sections.mjs +191 -0
- package/skills/wix-harmony-editor/scripts/copy/upload-media-manager.mjs +61 -0
- package/skills/wix-harmony-editor/scripts/launch.sh +359 -0
- package/skills/wix-headless/SKILL.md +7 -2
- package/skills/wix-headless/references/CAPABILITIES.md +6 -1
- package/skills/wix-headless/references/DISCOVERY.md +2 -1
- package/skills/wix-headless/references/SDK_HANDOFF.md +6 -1
- package/skills/wix-headless/references/SEED.md +3 -2
- package/skills/wix-headless/references/SETUP.md +1 -0
- package/skills/wix-headless/references/inline-recipes/experience-donations.md +102 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-donations.md +146 -0
- package/skills/wix-headless/references/inline-recipes/setup-donations.md +15 -0
- package/skills/wix-headless/references/managed/CREATE.md +1 -1
- package/skills/wix-headless-kit/SKILL.md +110 -21
- package/skills/wix-headless-kit/guides/api-run.md +15 -13
- package/skills/wix-headless-kit/guides/existing-site.md +31 -0
- package/skills/wix-headless-kit/guides/migration.md +49 -17
- package/skills/wix-headless-kit/install/agents-md.mjs +1 -1
- package/skills/wix-headless-kit/install/context.mjs +61 -6
- package/skills/wix-headless-kit/install/pins.json +1 -1
- package/skills/wix-headless-kit/install/setup.mjs +107 -12
- package/skills/wix-headless-replatform/.publish-manifest.json +8 -0
- package/skills/wix-headless-replatform/SKILL.md +116 -0
- package/skills/wix-headless-replatform/agents/openai.yaml +6 -0
- package/skills/wix-headless-replatform/references/control-and-visual-fidelity-contract.md +43 -0
- package/skills/wix-headless-replatform/references/extraction-schema.md +187 -0
- package/skills/wix-headless-replatform/references/interaction-runtime-contract.md +137 -0
- package/skills/wix-headless-replatform/references/layout-blueprint-contract.md +41 -0
- package/skills/wix-headless-replatform/references/output-contract.md +93 -0
- package/skills/wix-headless-replatform/references/post-build-gap-contract.md +125 -0
- package/skills/wix-headless-replatform/references/qa-checklist.md +83 -0
- package/skills/wix-headless-replatform/references/safety.md +19 -0
- package/skills/wix-headless-replatform/references/ui-normalization-contract.md +70 -0
- package/skills/wix-headless-replatform/references/workflow.md +157 -0
- package/skills/wix-headless-replatform/registry/README.md +20 -0
- package/skills/wix-headless-replatform/registry/approvals/button/shadcn-default-ac60ef5.json +28 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/button.tsx +54 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/capabilities.json +60 -0
- package/skills/wix-headless-replatform/registry/items/button/shadcn-default-ac60ef5/contract.md +48 -0
- package/skills/wix-headless-replatform/registry/licenses/shadcn-ui-ac60ef5-MIT.txt +21 -0
- package/skills/wix-headless-replatform/registry/registry.json +36 -0
- package/skills/wix-headless-replatform/resources/rp-facelift/SKILL.md +32 -0
- package/skills/wix-headless-replatform/resources/rp-project-context/SKILL.md +62 -0
- package/skills/wix-headless-replatform/resources/rp-qa-gap-loop/SKILL.md +64 -0
- package/skills/wix-headless-replatform/resources/rp-source-evidence/SKILL.md +42 -0
- package/skills/wix-headless-replatform/resources/rp-ui-implementation/SKILL.md +73 -0
- package/skills/wix-headless-replatform/schemas/build-plan.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/candidate.schema.json +19 -0
- package/skills/wix-headless-replatform/schemas/common.schema.json +33 -0
- package/skills/wix-headless-replatform/schemas/decision-patch.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/decision-request.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/extraction-gap.schema.json +28 -0
- package/skills/wix-headless-replatform/schemas/extraction-manifest.schema.json +34 -0
- package/skills/wix-headless-replatform/schemas/final-report.schema.json +10 -0
- package/skills/wix-headless-replatform/schemas/foundation.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/metadata.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/observation-packet.schema.json +17 -0
- package/skills/wix-headless-replatform/schemas/page-capture.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/page-resolution.schema.json +33 -0
- package/skills/wix-headless-replatform/schemas/shared-chrome.schema.json +11 -0
- package/skills/wix-headless-replatform/schemas/unit.schema.json +40 -0
- package/skills/wix-headless-replatform/scripts/assemble-extraction.mjs +844 -0
- package/skills/wix-headless-replatform/scripts/benchmark-extraction.mjs +163 -0
- package/skills/wix-headless-replatform/scripts/browser-extraction-preflight.mjs +42 -0
- package/skills/wix-headless-replatform/scripts/build-extracted-homepage.mjs +228 -0
- package/skills/wix-headless-replatform/scripts/clear-wix-starter.mjs +87 -0
- package/skills/wix-headless-replatform/scripts/create-wix-project.mjs +505 -0
- package/skills/wix-headless-replatform/scripts/discover-urls.mjs +142 -0
- package/skills/wix-headless-replatform/scripts/extract-assets.mjs +140 -0
- package/skills/wix-headless-replatform/scripts/extract-design-system.mjs +637 -0
- package/skills/wix-headless-replatform/scripts/extract-interactions.mjs +1903 -0
- package/skills/wix-headless-replatform/scripts/extract-page.mjs +2461 -0
- package/skills/wix-headless-replatform/scripts/extract-seo.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/extract-tokens.mjs +63 -0
- package/skills/wix-headless-replatform/scripts/finalize-extraction-report.mjs +126 -0
- package/skills/wix-headless-replatform/scripts/finalize-gap-review.mjs +180 -0
- package/skills/wix-headless-replatform/scripts/finalize-gap-review.test.mjs +98 -0
- package/skills/wix-headless-replatform/scripts/finalize-migration-website.mjs +103 -0
- package/skills/wix-headless-replatform/scripts/generate-control-state-contract.mjs +176 -0
- package/skills/wix-headless-replatform/scripts/generate-design-md.mjs +261 -0
- package/skills/wix-headless-replatform/scripts/generate-extraction-validators.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/generate-invoice-download-backend.mjs +342 -0
- package/skills/wix-headless-replatform/scripts/generate-layout-blueprint.mjs +140 -0
- package/skills/wix-headless-replatform/scripts/generate-repeater-cms.mjs +22 -0
- package/skills/wix-headless-replatform/scripts/generate-route-plan.mjs +143 -0
- package/skills/wix-headless-replatform/scripts/generate-scene-contract.mjs +288 -0
- package/skills/wix-headless-replatform/scripts/generate-ui-normalization.mjs +266 -0
- package/skills/wix-headless-replatform/scripts/generate-visual-assets.mjs +111 -0
- package/skills/wix-headless-replatform/scripts/lib/browser-tooling.mjs +570 -0
- package/skills/wix-headless-replatform/scripts/lib/common.mjs +329 -0
- package/skills/wix-headless-replatform/scripts/lib/component-capabilities.mjs +121 -0
- package/skills/wix-headless-replatform/scripts/lib/component-registry.mjs +282 -0
- package/skills/wix-headless-replatform/scripts/lib/content-boundary.mjs +61 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-contract.mjs +374 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-detectors.mjs +243 -0
- package/skills/wix-headless-replatform/scripts/lib/extraction-validators.generated.mjs +1139 -0
- package/skills/wix-headless-replatform/scripts/lib/font-contract.mjs +326 -0
- package/skills/wix-headless-replatform/scripts/lib/frontend-automation-state.mjs +184 -0
- package/skills/wix-headless-replatform/scripts/lib/gap-analysis.mjs +396 -0
- package/skills/wix-headless-replatform/scripts/lib/html-extract.mjs +168 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-bootstrap.mjs +10 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-runtime.mjs +194 -0
- package/skills/wix-headless-replatform/scripts/lib/interaction-timeline.mjs +348 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/classifiers.mjs +141 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/geometry.mjs +43 -0
- package/skills/wix-headless-replatform/scripts/lib/layout-blueprint/vocabulary.mjs +18 -0
- package/skills/wix-headless-replatform/scripts/lib/ui-normalization.css +64 -0
- package/skills/wix-headless-replatform/scripts/optimize-svg.mjs +31 -0
- package/skills/wix-headless-replatform/scripts/post-build-gap.mjs +358 -0
- package/skills/wix-headless-replatform/scripts/provision-repeater-cms.mjs +25 -0
- package/skills/wix-headless-replatform/scripts/record-gap-decision.mjs +39 -0
- package/skills/wix-headless-replatform/scripts/replay-extraction.mjs +107 -0
- package/skills/wix-headless-replatform/scripts/resolve-home-page.mjs +96 -0
- package/skills/wix-headless-replatform/scripts/site-clone.mjs +658 -0
- package/skills/wix-headless-replatform/scripts/validate-font-contract.mjs +29 -0
- package/skills/wix-headless-replatform/scripts/verify-interactions.mjs +669 -0
- package/skills/wix-headless-replatform/scripts/visual-qa.mjs +370 -0
- package/skills/wix-headless-templates/donations/INSTRUCTIONS.md +37 -24
- package/skills/wix-headless-templates/donations/app/components/donations/DonateForm.tsx +49 -23
- package/skills/wix-headless-templates/donations/app/components/donations/DonationReceipt.tsx +6 -5
- package/skills/wix-headless-templates/donations/app/components/donations/GoalProgress.tsx +5 -5
- package/skills/wix-headless-templates/donations/app/wix/donations/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/app/wix/donations/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/app/wix/donations/donation-store.ts +2 -2
- package/skills/wix-headless-templates/donations/app/wix/donations/donations-core.ts +12 -9
- package/skills/wix-headless-templates/donations/project/src/components/donations/DonateForm.tsx +49 -23
- package/skills/wix-headless-templates/donations/project/src/components/donations/DonationReceipt.tsx +6 -5
- package/skills/wix-headless-templates/donations/project/src/components/donations/GoalProgress.tsx +5 -5
- package/skills/wix-headless-templates/donations/project/src/wix/donations/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donation-store.ts +2 -2
- package/skills/wix-headless-templates/donations/project/src/wix/donations/donations-core.ts +12 -9
- package/skills/wix-headless-templates/donations/rest/campaigns.ts +4 -4
- package/skills/wix-headless-templates/donations/rest/donate.ts +7 -3
- package/skills/wix-headless-templates/donations/seed/SEED.md +12 -7
- package/skills/wix-headless-templates/donations/tests/donation-contract.test.mjs +71 -0
- package/skills/wix-headless-templates/donations/tests/donation-form.test.cjs +137 -0
- package/skills/wix-headless-templates/restaurants/INSTRUCTIONS.md +28 -14
- package/skills/wix-headless-templates/restaurants/app/components/restaurants/OrderCartDrawer.tsx +87 -5
- package/skills/wix-headless-templates/restaurants/app/hooks/restaurants/useOrderCart.ts +9 -1
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/order-store.ts +33 -4
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering-core.ts +100 -5
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/ordering.ts +101 -15
- package/skills/wix-headless-templates/restaurants/app/wix/restaurants/types.ts +35 -1
- package/skills/wix-headless-templates/restaurants/project/src/components/restaurants/OrderCartDrawer.tsx +87 -5
- package/skills/wix-headless-templates/restaurants/project/src/hooks/restaurants/useOrderCart.ts +9 -1
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/order-store.ts +33 -4
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering-core.ts +100 -5
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/ordering.ts +101 -15
- package/skills/wix-headless-templates/restaurants/project/src/wix/restaurants/types.ts +35 -1
- package/skills/wix-headless-templates/restaurants/rest/ordering.ts +91 -14
- package/skills/wix-import/SKILL.md +90 -0
- package/skills/wix-manage/SKILL.md +38 -10
- package/skills/wix-manage/references/automations/actions.md +11 -11
- package/skills/wix-manage/references/automations/activation-status.md +6 -9
- package/skills/wix-manage/references/automations/api-catalog.md +48 -32
- package/skills/wix-manage/references/automations/automation-model.md +97 -22
- package/skills/wix-manage/references/automations/bracket-expressions.md +7 -7
- package/skills/wix-manage/references/automations/build-and-manage-automations.md +140 -309
- package/skills/wix-manage/references/automations/conditions.md +187 -0
- package/skills/wix-manage/references/automations/schemas-and-scheduling.md +191 -0
- package/skills/wix-manage/references/automations/special-actions.md +210 -0
- package/skills/wix-manage/references/automations/triggers.md +47 -10
- package/skills/wix-manage/references/automations/validation-and-verification.md +320 -0
- package/skills/wix-manage/references/custom-apps/use-app-tools.md +124 -0
- package/skills/wix-manage/references/dashboard-navigation/dashboard-navigation.md +2 -1
- package/skills/wix-manage/references/dashboard-navigation/routes.json +249 -0
- package/skills/wix-manage/references/donations/create-and-update-donation-campaigns.md +139 -0
- package/skills/wix-manage/references/events/create-wix-event.md +136 -73
- package/skills/wix-manage/references/events/events-dashboard-navigation.md +81 -0
- package/skills/wix-manage/references/events/manage-event-guests.md +366 -0
- package/skills/wix-manage/references/events/manage-event-tickets.md +293 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +313 -55
- package/skills/wix-manage/references/sites/manage-oauth-apps.md +1 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +127 -8
- package/skills/wix-manage/references/stores/update-product-with-options.md +58 -2
- package/skills/wix-replatform/.publish-manifest.json +5 -5
- package/skills/wix-replatform/CLI.md +162 -0
- package/skills/wix-replatform/README.md +52 -0
- package/skills/wix-replatform/SKILL.md +229 -684
- package/skills/wix-replatform/console/account.css +1 -0
- package/skills/wix-replatform/console/app.css +1 -0
- package/skills/wix-replatform/console/app.js +117 -0
- package/skills/wix-replatform/console/index.html +4 -0
- package/skills/wix-replatform/console/json.css +11 -0
- package/skills/wix-replatform/lib/agent-adapters/index.js +71 -0
- package/skills/wix-replatform/lib/app-page-routing.js +75 -0
- package/skills/wix-replatform/lib/artifact-freshness.js +11 -1
- package/skills/wix-replatform/lib/blocked-data-requests.js +427 -0
- package/skills/wix-replatform/lib/census-reconciliation.js +426 -0
- package/skills/wix-replatform/lib/completion-report.js +1393 -13
- package/skills/wix-replatform/lib/config-env.js +85 -11
- package/skills/wix-replatform/lib/confirmed-pictures.js +212 -0
- package/skills/wix-replatform/lib/dependency-readiness.js +423 -0
- package/skills/wix-replatform/lib/dependency-recovery.js +21 -0
- package/skills/wix-replatform/lib/destination-choice.js +163 -0
- package/skills/wix-replatform/lib/destination-handoff.js +62 -0
- package/skills/wix-replatform/lib/destination-http.js +81 -0
- package/skills/wix-replatform/lib/destination-route.js +98 -0
- package/skills/wix-replatform/lib/destination.js +919 -0
- package/skills/wix-replatform/lib/discovered-census.js +231 -0
- package/skills/wix-replatform/lib/discovered-set-status.js +255 -0
- package/skills/wix-replatform/lib/document-url-guard.js +203 -0
- package/skills/wix-replatform/lib/entity-qa-attestation.js +339 -0
- package/skills/wix-replatform/lib/entity-verification.js +18 -0
- package/skills/wix-replatform/lib/execution-capability.js +120 -0
- package/skills/wix-replatform/lib/execution-log.js +170 -0
- package/skills/wix-replatform/lib/execution-manifest.js +20 -2
- package/skills/wix-replatform/lib/execution-state.js +22 -0
- package/skills/wix-replatform/lib/expectations-ledger.js +96 -0
- package/skills/wix-replatform/lib/generated-plugin-handoff.js +140 -0
- package/skills/wix-replatform/lib/import-readiness-contract.js +23 -0
- package/skills/wix-replatform/lib/import-recovery.js +800 -6
- package/skills/wix-replatform/lib/imported-redirect-targets.js +608 -0
- package/skills/wix-replatform/lib/internal-link-reconciliation.js +166 -0
- package/skills/wix-replatform/lib/link-audit.js +173 -0
- package/skills/wix-replatform/lib/link-census.js +194 -0
- package/skills/wix-replatform/lib/link-plan.js +148 -0
- package/skills/wix-replatform/lib/link-verification.js +274 -0
- package/skills/wix-replatform/lib/link-write-journal.js +42 -0
- package/skills/wix-replatform/lib/local-state.js +869 -30
- package/skills/wix-replatform/lib/member-completion.js +658 -0
- package/skills/wix-replatform/lib/member-crosswalk.js +69 -0
- package/skills/wix-replatform/lib/member-outcome-registry.js +265 -0
- package/skills/wix-replatform/lib/migration-completion.js +311 -0
- package/skills/wix-replatform/lib/operator-identity.js +47 -0
- package/skills/wix-replatform/lib/orchestration-decisions.js +248 -0
- package/skills/wix-replatform/lib/orchestration-preflight.js +401 -19
- package/skills/wix-replatform/lib/orchestration-router.js +608 -32
- package/skills/wix-replatform/lib/orchestration-state.js +188 -12
- package/skills/wix-replatform/lib/order-reference-readiness.js +30 -0
- package/skills/wix-replatform/lib/payments-outcome-registry.js +200 -0
- package/skills/wix-replatform/lib/plugin-cms-fallback.js +618 -0
- package/skills/wix-replatform/lib/preservation-approval-stamp.js +54 -0
- package/skills/wix-replatform/lib/product-media-deferral.js +96 -0
- package/skills/wix-replatform/lib/progress-log.js +19 -3
- package/skills/wix-replatform/lib/read-back-approval.js +184 -0
- package/skills/wix-replatform/lib/read-back-comparison.js +136 -0
- package/skills/wix-replatform/lib/read-back-contract.js +243 -0
- package/skills/wix-replatform/lib/read-back-extract.js +118 -0
- package/skills/wix-replatform/lib/read-back-gate.js +734 -0
- package/skills/wix-replatform/lib/read-back-reads.js +94 -0
- package/skills/wix-replatform/lib/read-back-state.js +107 -0
- package/skills/wix-replatform/lib/read-back-summary.js +81 -0
- package/skills/wix-replatform/lib/rerun-conflicts.js +334 -0
- package/skills/wix-replatform/lib/run-controller.js +401 -0
- package/skills/wix-replatform/lib/seo-outcome-registry.js +45 -0
- package/skills/wix-replatform/lib/setup-verification.js +148 -0
- package/skills/wix-replatform/lib/source-access.js +231 -0
- package/skills/wix-replatform/lib/source-coverage.js +1130 -0
- package/skills/wix-replatform/lib/source-secrets.js +277 -0
- package/skills/wix-replatform/lib/structured-link-patch.js +59 -0
- package/skills/wix-replatform/lib/task-state.js +110 -0
- package/skills/wix-replatform/lib/url-preservation-report.js +319 -0
- package/skills/wix-replatform/lib/url-preservation-state.js +349 -1
- package/skills/wix-replatform/lib/url-redirect-plan.js +728 -0
- package/skills/wix-replatform/lib/url-verification.js +315 -0
- package/skills/wix-replatform/lib/user-action-effects.js +42 -0
- package/skills/wix-replatform/lib/user-actions.js +264 -0
- package/skills/wix-replatform/lib/website-handoff.js +1220 -0
- package/skills/wix-replatform/lib/wix-client-callers.js +54 -0
- package/skills/wix-replatform/lib/wix-dashboard-url.js +35 -0
- package/skills/wix-replatform/lib/write-verification-state.js +92 -0
- package/skills/wix-replatform/lib/write-verification.js +274 -0
- package/skills/wix-replatform/resources/rp-destination/SKILL.md +137 -0
- package/skills/wix-replatform/resources/rp-discovery/SKILL.md +189 -16
- package/skills/wix-replatform/resources/rp-discovery/scripts/promote-source-coverage.js +47 -0
- package/skills/wix-replatform/resources/rp-execute-import/SKILL.md +869 -29
- package/skills/wix-replatform/resources/rp-execute-import/scripts/shipping-estimate-audit.js +70 -0
- package/skills/wix-replatform/resources/rp-execute-setup/SKILL.md +195 -29
- package/skills/wix-replatform/resources/rp-execute-setup/scripts/execute-setup.js +442 -0
- package/skills/wix-replatform/resources/rp-execution-policy/SKILL.md +112 -0
- package/skills/wix-replatform/resources/rp-execution-policy/lib/coupon-currency-plan-report.js +143 -0
- package/skills/wix-replatform/resources/rp-execution-policy/lib/payments-report.js +244 -0
- package/skills/wix-replatform/resources/rp-import-codegen/SKILL.md +2082 -38
- package/skills/wix-replatform/resources/rp-mapper/SKILL.md +528 -11
- package/skills/wix-replatform/resources/rp-mapper/lib/structure-bridge-planning.js +303 -0
- package/skills/wix-replatform/resources/rp-mapper/scripts/coverage-reconcile.js +88 -0
- package/skills/wix-replatform/resources/rp-mapper/wix-business-solutions-taxonomy.md +6 -5
- package/skills/wix-replatform/resources/rp-preflight/SKILL.md +40 -0
- package/skills/wix-replatform/resources/rp-project-intake/SKILL.md +55 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/SKILL.md +60 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/lib/import-progress.js +89 -0
- package/skills/wix-replatform/resources/rp-quick-runtime/lib/limit-recovery.js +36 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/SKILL.md +123 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/quick-mode.json +124 -0
- package/skills/wix-replatform/resources/rp-quick-shopify/scripts/quick-mode.js +522 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/SKILL.md +56 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/quick-mode.json +91 -0
- package/skills/wix-replatform/resources/rp-quick-woocommerce/scripts/quick-mode.js +255 -0
- package/skills/wix-replatform/resources/rp-setup-discovery/SKILL.md +80 -10
- package/skills/wix-replatform/resources/rp-setup-discovery/scripts/quick-setup-plan.js +44 -0
- package/skills/wix-replatform/resources/rp-source-csv/SKILL.md +3 -0
- package/skills/wix-replatform/resources/rp-source-csv/lib/csv-fileset.js +0 -0
- package/skills/wix-replatform/resources/rp-source-csv/scripts/csv-discovery.js +21 -1
- package/skills/wix-replatform/resources/rp-source-inputs/SKILL.md +66 -0
- package/skills/wix-replatform/resources/rp-source-sfcc/SKILL.md +153 -0
- package/skills/wix-replatform/resources/rp-source-shopify/SKILL.md +394 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/discovery-reads.js +200 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/import-findings.js +34 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk-sqlite-worker.js +44 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-bulk.js +1470 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-cli.js +446 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-gift-cards.js +474 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-protected-fields.js +879 -0
- package/skills/wix-replatform/resources/rp-source-shopify/lib/shopify-session.js +426 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/articles.graphql +24 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/blogs.graphql +18 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/collections.graphql +40 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/customers.graphql +76 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/delivery-profiles.graphql +51 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/discounts.graphql +35 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/files.graphql +20 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/gift-cards.graphql +18 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/inventory.graphql +31 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/locations.graphql +21 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/menus.graphql +21 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/metaobjects.graphql +30 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-core.graphql +150 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-fulfillments.graphql +36 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/orders-refunds.graphql +48 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/pages.graphql +20 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-address.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-email.graphql +6 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-name.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-customers-phone.graphql +8 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-address.graphql +12 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-email.graphql +6 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-name.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/probe-orders-phone.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/products.graphql +73 -0
- package/skills/wix-replatform/resources/rp-source-shopify/queries/url-redirects.graphql +13 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-discovery.js +693 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-extract.js +283 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-gift-card-remedy.js +191 -0
- package/skills/wix-replatform/resources/rp-source-shopify/scripts/shopify-session-check.js +115 -0
- package/skills/wix-replatform/resources/rp-source-shopify/source-schema.example.json +201 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/PLUGIN-READ-PLANS.md +146 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/SKILL.md +736 -22
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/blocked-data-handlers.js +73 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.js +115 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/checkout-field-csv-parser.test-fixture.js +91 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/coupon-discovery.js +440 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/currency-gate.js +125 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.js +252 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-source-keys.json +183 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/gateway-summary.mjs +333 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/invoice-discovery.js +426 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-disposition.js +400 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-entity-read-plan.js +633 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/plugin-knowledge.js +1127 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.js +194 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/refund-corroboration.json +100 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/sampled-ids-batch.js +157 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/secret-value-guard.js +234 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/store-currency.js +101 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/structure-bridge-reader.js +755 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.js +1074 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wix-wp-plugin-v2-client.test-fixture.js +149 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/woo-inventory.js +25 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-http.js +204 -9
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-image-url.js +186 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-plugin-detect.js +1422 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-redirect-sources.js +343 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/wp-route-classifier.js +262 -26
- package/skills/wix-replatform/resources/rp-source-wordpress/lib/yoast-product-seo-evidence.js +272 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/README.md +373 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/advanced-custom-fields.json +101 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/back-in-stock-notifier-for-woocommerce.json +81 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/capabilities-pending-decision.json +128 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/cms-fallback-eligible.json +18 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/contact-form-7.json +104 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/facebook-for-woocommerce.json +44 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/fingerprint-aliases.json +49 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/free-gifts-for-woocommerce.json +94 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/index.json +863 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mailpoet.json +52 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/membership-for-woocommerce.json +161 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/mwb-bookings-for-woocommerce.json +180 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/no-migration-needed.json +286 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pricing-deals-for-woocommerce.json +79 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/product-extras-for-woocommerce.json +120 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/pw-woocommerce-gift-cards.json +128 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/requires-development.json +88 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seo-by-rank-math.json +80 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/seriously-simple-podcasting.json +46 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/strong-testimonials.json +55 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/subscriptions-for-woocommerce.json +99 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/tablepress.json +55 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/the-events-calendar.json +160 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/ti-woocommerce-wishlist.json +81 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-checkout-field-editor-pro.json +60 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-discount-rules.json +74 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woo-product-bundle.json +75 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-gift-cards.json +60 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-other-payment-gateway.json +44 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-pdf-invoices-packing-slips.json +57 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-product-addon.json +141 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/woocommerce-progressive-discounts.json +54 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wordpress-seo.json +158 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpc-composite-products.json +97 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/plugins/wpforms-lite.json +86 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/capture-store-settings.js +90 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge-validate.js +51 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/plugin-knowledge.js +103 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-discovery.js +1668 -33
- package/skills/wix-replatform/resources/rp-source-wordpress/scripts/wp-plugin-inventory.js +273 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/secret-value-patterns.v1.json +11 -0
- package/skills/wix-replatform/resources/rp-source-wordpress/sub-parts.json +13 -0
- package/skills/wix-replatform/resources/rp-target-wix/SKILL.md +446 -32
- package/skills/wix-replatform/resources/rp-target-wix/app-page-routing-capabilities.json +36 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/README.md +38 -2
- package/skills/wix-replatform/resources/rp-target-wix/domains/SERVICE-PLUGINS.md +322 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/docs-survey.json +70 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/domain.json +19 -9
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/category.json +42 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/comment.json +54 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/post.json +66 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/blog/entities/tag.json +38 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/docs-survey.json +332 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/domain.json +75 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/booking.json +181 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service-add-on.json +130 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/bookings/entities/service.json +180 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/docs-survey.json +161 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/domain.json +15 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/collection.json +39 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/data-item.json +47 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/cms/entities/reference.json +7 -1
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/docs-survey.json +204 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/domain.json +17 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/contact.json +159 -15
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/email-subscription.json +80 -12
- package/skills/wix-replatform/resources/rp-target-wix/domains/crm/entities/label.json +48 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/docs-survey.json +306 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/domain.json +30 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/delivery-profile.json +171 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/discount-rule.json +182 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-applied-discount.json +129 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-invoice-document.json +192 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item-options.json +123 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-line-item.json +60 -12
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-merchant-note.json +112 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-payment.json +191 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order-refund.json +140 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/order.json +383 -18
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/pickup-location.json +122 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/shipping-option.json +98 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/ecom/entities/wishlist-item.json +86 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/docs-survey.json +128 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/domain.json +38 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/category.json +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/event.json +107 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/registration-form.json +89 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/rsvp.json +118 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/events/entities/ticket-definition.json +93 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/docs-survey.json +86 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/domain.json +56 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form-submission.json +121 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/forms/entities/form.json +134 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/domain.json +50 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card-product.json +87 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/gift-cards/entities/gift-card.json +130 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/index.json +914 -24
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/domain.json +14 -4
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-file.json +45 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/media/entities/media-folder.json +41 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/docs-survey.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/domain.json +13 -3
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/badge.json +41 -7
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/custom-field.json +82 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member-about.json +77 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/members/entities/member.json +165 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/docs-survey.json +41 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/domain.json +46 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan-order.json +137 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/pricing-plans/entities/plan.json +133 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/docs-survey.json +7 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/domain.json +36 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/amazon-shop-connect.json +71 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/ebay-shop-connect.json +67 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/google-youtube-connect.json +72 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/meta-catalog-connect.json +74 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/pinterest-catalog-connect.json +69 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/sales-channels/entities/tiktok-catalog-connect.json +61 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/schema.json +28 -1
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/docs-survey.json +109 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/domain.json +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/item-seo-tags.json +148 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/seo-pattern.json +91 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/seo/entities/site-seo-tags.json +79 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/docs-survey.json +32 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/domain.json +49 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/site/entities/business-contact.json +103 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/docs-survey.json +203 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/domain.json +27 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/back-in-stock-request.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/brand.json +111 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/category.json +52 -8
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/coupon.json +214 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/inventory-item.json +93 -13
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-customization.json +111 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product-tag.json +52 -10
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/product.json +145 -14
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/promotion.json +84 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/review.json +178 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/stores/entities/subscription-product.json +87 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/docs-survey.json +106 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/domain.json +64 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/manual-tax-mapping.json +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-group.json +94 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-region.json +113 -0
- package/skills/wix-replatform/resources/rp-target-wix/domains/tax/entities/tax-settings.json +65 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-author-attribution.js +165 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-client.js +60 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-duplicates.js +201 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-ledger.js +306 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-report.js +229 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build-stage.js +55 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-build.js +715 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/blog-post-media.js +567 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/bulk-results.js +73 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contact-extended-fields.js +194 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contact-import.js +133 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/contract-ledger.js +13 -4
- package/skills/wix-replatform/resources/rp-target-wix/lib/coupon-redemption-drift.js +491 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/customer-member.js +104 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/data-extension-schema.js +673 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/domain-knowledge.js +453 -2
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-balance-reconciliation.js +119 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-build.js +566 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/gift-card-redemption-build.js +249 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-applied-discount-build.js +63 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-buyer.js +187 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-channel-build.js +64 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-history.js +1027 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-invoice-contract.js +90 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/order-payment-mapping.js +420 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/pricing-plan-definition.js +177 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/product-import.js +151 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/product-seo.js +723 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/seo-redirects.js +353 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-build.js +1437 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/shipping-option-stage.js +354 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/store-config-build.js +134 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-category-attach.js +221 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-inventory.js +248 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/stores-verification.js +86 -3
- package/skills/wix-replatform/resources/rp-target-wix/lib/tax-build.js +231 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/url-mapper-transport.js +28 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/value-utils.js +14 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/variant-correlation.js +34 -0
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-build.js +166 -15
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-target-spec.js +19 -2
- package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +4079 -257
- package/skills/wix-replatform/resources/rp-target-wix/lib/write-verification.js +3 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/docs-survey-sync.js +150 -0
- package/skills/wix-replatform/resources/rp-target-wix/scripts/domain-knowledge-validate.js +40 -2
- package/skills/wix-replatform/resources/rp-target-wix/scripts/verify-stores.js +6 -1
- package/skills/wix-replatform/resources/rp-telemetry/SKILL.md +148 -8
- package/skills/wix-replatform/resources/rp-telemetry/lib/bi-sink.js +83 -15
- package/skills/wix-replatform/resources/rp-telemetry/lib/telemetry-recorder.js +1119 -215
- package/skills/wix-replatform/resources/rp-telemetry/lib/transcript-digest.js +318 -0
- package/skills/wix-replatform/resources/rp-telemetry/scripts/meter-from-audit.js +2 -2
- package/skills/wix-replatform/resources/rp-telemetry/scripts/rp-telemetry.js +106 -4
- package/skills/wix-replatform/resources/rp-website-continuation/SKILL.md +94 -0
- package/skills/wix-replatform/scripts/census-reconciliation.js +25 -0
- package/skills/wix-replatform/scripts/cms-fallback.js +89 -0
- package/skills/wix-replatform/scripts/discovered-census.js +26 -0
- package/skills/wix-replatform/scripts/discovered-set-status.js +33 -0
- package/skills/wix-replatform/scripts/entity-qa-attestation.js +130 -0
- package/skills/wix-replatform/scripts/execution-health-check.js +415 -0
- package/skills/wix-replatform/scripts/execution-manifest-validate.js +1 -1
- package/skills/wix-replatform/scripts/execution-state-prepare.js +1 -1
- package/skills/wix-replatform/scripts/expectations-ledger.js +44 -0
- package/skills/wix-replatform/scripts/import-contract-verify.js +183 -0
- package/skills/wix-replatform/scripts/internal-link-audit.js +90 -0
- package/skills/wix-replatform/scripts/item-seo-tags-verify.js +205 -0
- package/skills/wix-replatform/scripts/migration-completion.js +76 -0
- package/skills/wix-replatform/scripts/orchestration-preflight.js +21 -2
- package/skills/wix-replatform/scripts/orchestration-route.js +32 -1
- package/skills/wix-replatform/scripts/orchestration-state.js +146 -10
- package/skills/wix-replatform/scripts/read-back-gate.js +175 -0
- package/skills/wix-replatform/scripts/replatform-console.js +146 -0
- package/skills/wix-replatform/scripts/replatform.js +436 -0
- package/skills/wix-replatform/scripts/source-secrets.js +77 -0
- package/skills/wix-replatform/scripts/url-redirects.js +286 -0
- package/skills/wix-replatform/scripts/user-action.js +58 -0
- package/skills/wix-replatform/scripts/website-handoff-generate.js +64 -0
- package/skills/wix-replatform/scripts/wix-dashboard-url.js +54 -0
- package/skills/wix-replatform/scripts/wix-destination.js +143 -0
- package/skills/wix-replatform/scripts/wix-headless-scaffold.js +762 -0
- package/skills/wix-vibe-headless/SKILL.md +7 -3
- package/skills/wix-vibe-headless/install/deploy.cjs +61 -34
- package/skills/wix-vibe-headless/platforms/base44.md +1 -0
- package/skills/wix-vibe-headless/references/donations/INSTRUCTIONS.md +88 -0
- package/skills/wix-vibe-headless/references/donations/app/rest/wix-donations.js +112 -0
- package/skills/wix-vibe-headless/references/donations/seed/SEED.md +11 -0
- package/skills/wix-vibe-headless/references/donations/tests/donations.test.mjs +258 -0
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +5 -2
- package/skills/wix-vibe-headless/references/restaurants/app/context/OrderCartContext.jsx +4 -1
- package/skills/wix-vibe-headless/references/restaurants/app/rest/wix-restaurants-ordering.js +67 -3
- package/skills/wix-vibe-headless/references/shared/app/rest/wix-client.js +4 -3
- package/skills/wix-manage/references/custom-apps/use-app-skills-and-app-tools.md +0 -208
- 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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
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`,
|
|
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 (
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
|
|
427
|
-
which the usual `|| []` turns into an empty array. It silently
|
|
428
|
-
index, silently
|
|
429
|
-
report **0 categories on a site that
|
|
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** (
|
|
493
|
-
|
|
494
|
-
|
|
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`
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
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 `₪`);
|
|
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/` —
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
|
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
|
|
868
|
-
|
|
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
|
|
1005
|
-
(`local-state.js`)
|
|
1006
|
-
|
|
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
|
|
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.
|