← commercetoolsCONTENT HISTORY
WHAT CHANGED
Update to commercetools
Snapshot Nov 9, 2026 · 12:08 UTC · version 0.69.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save commercetools and check future changes in Following.
Up to 3 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Migrates a product catalog from any source system into commercetools through a canonical NDJSON feed contract — the pipeline never parses the source, so each engagement writes only a small adapter. Covers the feed contract, writing an adapter, ProductType derivation and its irreversible attribute constraints, decimal-to-minor-unit money conversion, project-wide slug allocation, category order hints, opening stock per SKU and supply channel, tax categories, and the offline audit gate. Use when migrating or bulk-loading a product catalog into commercetools, writing an adapter for a source export, deciding SameForAll versus CombinationUnique, converting prices to centAmount, or diagnosing rejected Import API operations. Covers loading a catalog from a source system into commercetools — not the Classic-to-Modular catalog model migration of an existing project, which is the staged Modular Catalog migration guide. Not for ongoing sync after cutover, and not for customers, orders, carts, or promotions.",
"included_files": [
{
"relative_path": "references/catalog-feed-contract.md",
"size_in_bytes": 40109
},
{
"relative_path": "references/categories-and-slugs.md",
"size_in_bytes": 5188
},
{
"relative_path": "references/decision-log.md",
"size_in_bytes": 11502
},
{
"relative_path": "references/pricing-and-money.md",
"size_in_bytes": 12143
},
{
"relative_path": "references/product-model.md",
"size_in_bytes": 14088
},
{
"relative_path": "references/running-the-pipeline.md",
"size_in_bytes": 64740
},
{
"relative_path": "references/source-discovery.md",
"size_in_bytes": 12575
},
{
"relative_path": "references/step-0-interview.md",
"size_in_bytes": 28422
},
{
"relative_path": "references/switching-to-modular.md",
"size_in_bytes": 7209
},
{
"relative_path": "references/writing-an-adapter.md",
"size_in_bytes": 22794
},
{
"relative_path": "scripts/docs-search.mjs",
"size_in_bytes": 26516
}
],
"name": "commercetools-catalog-migration",
"skill_md_contents": "---\nname: commercetools-catalog-migration\ndescription: Migrates a product catalog from any source system into commercetools through a canonical NDJSON feed contract — the pipeline never parses the source, so each engagement writes only a small adapter. Covers the feed contract, writing an adapter, ProductType derivation and its irreversible attribute constraints, decimal-to-minor-unit money conversion, project-wide slug allocation, category order hints, opening stock per SKU and supply channel, tax categories, and the offline audit gate. Use when migrating or bulk-loading a product catalog into commercetools, writing an adapter for a source export, deciding SameForAll versus CombinationUnique, converting prices to centAmount, or diagnosing rejected Import API operations. Covers loading a catalog from a source system into commercetools — not the Classic-to-Modular catalog model migration of an existing project, which is the staged Modular Catalog migration guide. Not for ongoing sync after cutover, and not for customers, orders, carts, or promotions.\nwhen_to_use:\n - \"Migrating or bulk-loading a product catalog from a source system into a commercetools project\"\n - \"Writing an adapter that turns a source export (hybris ImpEx, PIM extract, CSV) into a catalog feed\"\n - \"Deciding SameForAll versus CombinationUnique for a variant attribute, or any other irreversible ProductType choice\"\n - \"Diagnosing Import API operations that were accepted but came back rejected or unresolved\"\nmetadata:\n contentType: SKILL\n area:\n - Migrations\n docsSearch:\n products:\n - Composable Commerce\n---\n\n# commercetools catalog migration\n\nMigration is one-time and decision-heavy. The deliverable is **a reviewed set of\ndecisions**, not a loaded dataset: what was translated, what was approximated,\nand what was lost, with a rationale per decision. A migration claiming zero\ninformation loss is not being honest.\n\nIf the work is a recurring sync from a PIM, ERP or OMS, this is the wrong\napproach: that is integration, contract-heavy rather than decision-heavy.\n\n## The source boundary is a data contract, not a code module\n\n```\nany source → adapter → canonical catalog feed → pipeline → commercetools\n per NDJSON, schema-checked reusable\n engagement THE CONTRACT\n```\n\nThe pipeline — [`commercetools/ct-catalog-migration-pipeline`](https://github.com/commercetools/ct-catalog-migration-pipeline),\ncloned beside the engagement — **never parses a source system**. It defines a\nfeed contract and starts there. Everything source-specific lives in the adapter,\nupstream of the contract.\n\nThis is not decoration. A universal parser for any source platform does not\nexist — no two installations agree on their type system — so source knowledge\ninside the pipeline is what destroys retargetability. A seam made of *data*\nrather than *code* makes that coupling impossible rather than discouraged. The\ncontract is `schema/catalog-feed.schema.json` in that repository; treat it as\nthe API.\n\n## Before you start\n\n1. <!-- ct:docs-search:begin -->\n **Docs search (required, run first)** — The first time you use this skill in a session you must run this before answering. It gathers the latest verified documentation as your primary grounding, filtered to the products this skill covers. Use this script for documentation search while working with this skill; the Knowledge MCP covers everything else. Always confirm details against retrieved documentation rather than the skill text alone:\n\n ```bash\n node \"<this skill's directory>/scripts/docs-search.mjs\" \\\n --query \"<extract key terms: Import API, ProductType attributes, catalog model, Standalone Prices, Category slug>\" \\\n --app-name \"<host app: claude-code, claude-chat, cursor, codex, copilot — or the host's own name>\" \\\n --model \"<current-model>\" \\\n --commercetools-project-key \"<if known>\" \\\n --commercetools-region \"<if known>\" \\\n --limit 10\n ```\n\n Pass the commercetools project key and region (as in `api.{region}.commercetools.com`) only if already in your context; otherwise omit both. Never search files or ask the user for them.\n <!-- ct:docs-search:end -->\n\n If the search prints \"No results found\", that is a failed lookup, not a sign\n the documentation is silent. Retry once with one or two of the terms alone\n (`Import API`, `Standalone Prices`), and if that is empty too, look the same\n topics up through the Knowledge MCP before going on. The references in this\n skill were checked at one point in time, and this search is how you learn\n what has changed since, so do not continue on the references alone.\n\n The limits and constraints in [Critical](#critical) were verified in\n September 2026. Re-check any of them here before relying on one.\n\n2. **Lay out the engagement beside the pipeline checkout, never inside it.**\n `migration/` holds the config, the interview, the decision log and the\n adapter; `source-export/` holds what the customer handed over. Create\n `migration/` before step 0 writes `INTERVIEW.md` into it. The layout, and what\n to commit, is in\n [references/decision-log.md](references/decision-log.md#where-it-lives--and-the-layout-it-belongs-to).\n\n## Workflow\n\n0. **Interview in a file, then write the config.** Do not hand over a template\n to fill in, and do not ask the questions in chat. Write them to\n `migration/INTERVIEW.md` for the user to edit, stop, and **write\n `migration.config.json` from the answers** once every question has one —\n the config is the record of a conversation, not a form.\n\n Every question, what it writes, and why none of them can be defaulted:\n [references/step-0-interview.md](references/step-0-interview.md). Work\n through it in order — the order is load-bearing:\n\n 1. **Size the export** (minutes): file list, line counts, largest variant\n count. Structural only — no mapping, no types, no decisions.\n 2. **Discover the source (0b)** —\n [references/source-discovery.md](references/source-discovery.md) — because\n the data answers some of the questions.\n 3. **Write `migration/INTERVIEW.md` and stop.** One block per topic, a table with a row per question: 0a,\n the catalog-model question, first, because its answers can end the\n engagement (over 100 variants per product cannot be loaded under\n `Classic`) and are cheaper to learn before an adapter exists; then\n 0c, each conditional that applies, and 0d. Tell the user to edit the file and say when it is done,\n and that you will re-read it then and name any empty row. The config and\n adapter wait because each answer is irreversible or fails silently.\n 4. **Continue only when every answer cell is filled in.** Otherwise name the\n unanswered rows and stop. The file format and the rules for \"I don't\n know\" and follow-up questions are in the interview reference.\n\n The file stays open after step 0. A question that arises at any later stage\n is added to it as a row of the section it belongs to, not asked in chat; the work that depends on it waits, and\n when the answer arrives you start again from the stage it affects.\n\n **Then open the decision log.** The config records *what* was chosen, not\n who chose it, what the alternative would have cost, or that a question was\n asked at all. Write one entry per decision from 0a and 0c, plus any\n question that could not be answered — an open question with an owner is a\n plan, an unasked one is a surprise. Open it before the adapter, with the\n pipeline checkout (`git rev-parse --short HEAD`) as its first line, so the\n version is on record before any stage runs. Format and placement:\n [references/decision-log.md](references/decision-log.md).\n\n **Keep writing it at every step from here**, not at the end: a log\n reconstructed afterwards keeps what turned out well and loses the\n irreversible decisions taken under uncertainty.\n\n1. **Write the adapter — one product first.** Read the source, emit NDJSON\n against the contract. This is the only code an engagement normally writes,\n and it is deliberately disposable. Work through\n [references/writing-an-adapter.md](references/writing-an-adapter.md) against a\n real export — one designed against an assumed source model gets rewritten —\n with the record types in\n [references/catalog-feed-contract.md](references/catalog-feed-contract.md).\n\n **Take the thin vertical slice before writing the rest**: one product, two\n or three variants, through `validate`, `derive`, `plan` and `audit`. Before\n the first of them runs, record which pipeline checkout it is\n (`git rev-parse --short HEAD`, one line in `DECISIONS.md`), whether you\n cloned it or found it there: [the stages](references/running-the-pipeline.md#the-stages).\n Writing every SKU in one pass usually gets away with it, which is why this\n step keeps getting skipped — and is not a reason to skip it. The axis\n model, the money conversion and the attribute constraints are wrong in the\n same way across five records as across five thousand, and they are\n irreversible once loaded.\n\n **Log every point where the adapter decides rather than translates**: a code\n derived from a display name, a hierarchy level collapsed, an inherited price\n resolved, a locale tag normalised, a sentinel skipped or interpreted, a field\n dropped, and *every silent cap* — anything sampled, truncated or skipped.\n These are the decisions most often lost, because they are made while writing\n code and feel like implementation details. They are not: they are what the\n migration did to the customer's data.\n\n2. **Run the offline stages in order.** Each gates the next, so a defect is\n reported at the earliest stage that can see it:\n\n ```bash\n cd ct-catalog-migration-pipeline\n npm run pipeline -- validate --config ../migration/migration.config.json\n npm run pipeline -- derive --config ../migration/migration.config.json\n npm run pipeline -- plan --config ../migration/migration.config.json --payloads\n npm run pipeline -- audit --config ../migration/migration.config.json\n ```\n\n None of these need credentials, by design: a broken feed or an unloadable\n plan reports the real problem on a laptop with no `.env`.\n\n **Log the findings you accept rather than fix.** A diagnostic that changes the\n adapter leaves no decision behind — the feed is regenerated and the finding\n disappears. One *accepted* is a decision: 14 disambiguated slugs and the URLs\n that result, a products-in-no-category warning that is correct for this\n catalog, an `approaching-variant-limit` acknowledged with a plan to revisit.\n Record the count and the artefact, not every finding.\n\n3. **Read `out/MODEL-REVIEW.md` before loading.** It is the sign-off artefact:\n irreversible choices, information loss, and anything guessed. Read the one\n `plan` writes: `derive` writes the product model only, and the tax rates and\n the other decisions recorded while mapping the feed are added by `plan`.\n\n Log the *review outcome*, not its contents — one entry naming what was\n accepted and against which plan. `MODEL-REVIEW.md` is regenerated on every\n run and can hold thousands of entries; copying them into an append-only log\n guarantees the two disagree by the next run.\n\n4. **Check the target project** once credentials exist. `preflight` answers\n whether the project will accept the plan, in a handful of GETs, before\n anything is written:\n\n ```bash\n npm run pipeline -- preflight --config ../migration/migration.config.json\n ```\n\n **`--apply` and `--switch-catalog-model` write to the customer's project, and so does\n any manual change made to get past preflight. Log every one**: project key,\n timestamp, exactly what changed, who authorised it. They report `project-updated` and\n `catalog-model-switched` to stdout and nowhere else, so an unlogged change is one\n nobody can account for. Manual changes count doubly — adding a country to make\n country-scoped prices load affects shipping and tax, which is why `preflight` refuses\n to do it.\n\n **A `catalog-model-*` finding stops you:** read\n [switching-to-modular.md](references/switching-to-modular.md) first. An empty project\n is switched on the user's yes; one with products waits for a support-run copy job.\n\n5. **Load, dry run first.** `load` sends nothing without `--execute`, and writes\n the exact request bodies to `out/load-requests.json` for review:\n\n ```bash\n npm run pipeline -- load --config ../migration/migration.config.json\n npm run pipeline -- load --config ../migration/migration.config.json --execute\n ```\n\n **Ask before every `--execute`.** Show the user what the dry run will send —\n the target project key, counts per stage, the prerequisites it will create —\n and wait for an explicit yes. A goal in the opening request (\"get the catalog\n into the project\", even \"load it\") is not consent for this write: the user\n has not yet seen what it sends. A yes covers one `--execute`. A later one,\n such as a resubmission, needs a new yes, and that counts only if a fresh dry\n run matches what the user saw; if anything differs, show it and ask again.\n `preflight --apply` and `--switch-catalog-model` each need their own yes.\n\n `load --wait`, `verify` and `teardown --execute` take minutes. They print a line\n every 30 seconds and append it to `out/progress.log`: run them so you can read\n it, not through `| tail`, and tell the user where they are\n ([progress](references/running-the-pipeline.md#progress-while-a-stage-runs)).\n\n Log one entry per `--execute`: project key, timestamp, what was sent, the\n outcome. `out/load-result.json` holds the detail but is regenerated and\n gitignored.\n\n6. **Verify, and do not treat the load's own report as the answer.** An\n accepted Import Request is not an imported resource — the API validates\n asynchronously, so a load can report every request accepted while\n individual operations are rejected afterwards. `verify` reads the project\n back and reconciles it against the plan. Every call is a GET:\n\n ```bash\n npm run pipeline -- verify --config ../migration/migration.config.json\n ```\n\n It needs `view_products` and, under standalone pricing,\n `view_standalone_prices` — reading is a different scope set from writing.\n\n If it leads with `operations-in-flight`, wait and re-run `verify`, not\n `load`: the Import API retries pending operations itself, and only\n `rejected` ones need resubmitting.\n\n Log the result as the closing entry for the run, **including a clean one**.\n \"Reconciled, no differences\" is the sentence someone needs months later when\n asking whether the load was ever checked at all.\n\n To undo a run on a trial project, `teardown` removes what the plan created,\n scoped to `keys.prefix`: a dry run by default, and `--execute` needs\n `--confirm-project <key>` — see\n [running-the-pipeline.md](references/running-the-pipeline.md#teardown).\n **Ask before every `--execute`, as for the load.** A request to \"remove\n everything you loaded\" names an outcome, not which keys or how many: show the\n dry run, name the project and the counts, and wait for the yes. A tax category\n `load` created goes with `--include-created-tax-categories`, never a\n hand-written delete.\n\n7. **Fix defects in the adapter, never in the feed by hand.** The feed is\n regenerated on every run.\n\n## Critical\n\nThe failure modes that are silent or unrecoverable, each stated in full in the\nreference beside it — this list exists so none is *missed*.\n\n**Irreversible, so decide before the first load**\n\n- **Attribute constraints are a one-way door.** `changeAttributeConstraint`\n accepts only `None`: a constraint can be relaxed, never tightened or\n switched. A wrong `SameForAll` / `CombinationUnique` means recreating the\n attribute and rewriting its data. → [product-model.md](references/product-model.md)\n- **A product selection's `mode` is fixed at creation** — there is no\n `changeMode` action, and re-importing the other mode reports `imported` and\n changes nothing. → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **A Product's ProductType cannot be changed after creation.** A project\n loaded before ProductType keys were prefixed trips\n `product-type-keys-unprefixed` and cannot be moved onto the new keys by\n re-running. → [running-the-pipeline.md](references/running-the-pipeline.md)\n- **`productCatalogModel` is fixed at map time.** `Classic` embeds variants\n (1–100) and allows embedded prices. `Modular` makes them standalone\n `VariantImport` resources (to 10,000), allows **only** standalone prices, and\n cannot import `defaultVariant` at all. Changing it means re-running `plan`.\n → [product-model.md](references/product-model.md)\n\n**Silent when wrong — no error, wrong catalog**\n\n- **Money is minor units, and the digit count is not 2 everywhere.** JPY and\n KRW are 0; BHD, KWD, OMR and TND are 3. Defaulting to 2 multiplies prices by\n 100 without erroring. Convert on the digit string, never through a float.\n → [pricing-and-money.md](references/pricing-and-money.md)\n- **A localized value must never be a variant axis.** Identity would key\n differently per language and break the moment a second locale is added.\n `axisValues` takes codes; `axisLabels` takes display text.\n → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **`isSearchable` must agree across every ProductType sharing an attribute\n name**, or that attribute leaves search, filters and facets *everywhere* —\n and the import still succeeds. → [product-model.md](references/product-model.md)\n- **Omitted fields are deleted on import.** `ProductVariantImport` and\n `StandalonePriceImport` drop whatever you leave out, so an update must resend\n everything worth keeping. `ProductVariantPatch` is the only partial update.\n → [running-the-pipeline.md](references/running-the-pipeline.md)\n- **An unresolved KeyReference expires after 48 hours**, taking its price with\n it: a green load and a partly priced catalog. An inventory entry naming a\n missing supply channel goes the same way, taking the stock — and a product\n naming a missing tax category goes **whole**, variants and prices with it.\n → [running-the-pipeline.md](references/running-the-pipeline.md)\n- **Stock for a SKU that does not exist imports successfully.** The Import API\n does not check `InventoryImport.sku` against anything, so a typo becomes\n stock nothing sells and no stage of the platform ever reports it. `validate`\n and the audit gate check it because nothing else will.\n → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **A product without a tax category loads, verifies, and cannot be taxed.**\n Under the default `Platform` tax mode a cart takes its rate from the\n product's tax category; with none, checkout cannot compute tax and nothing\n earlier says so. `validate` warns rather than refuses, because `External`\n tax mode needs no category — which mode applies is a step-0 question, and\n the answer goes in the config as `target.taxMode`.\n → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **`publish: false` unpublishes.** It is an instruction, not \"leave\n publication alone\", and the pipeline plans it on every product. Correct on a\n first load; on a re-load into a live project it takes every product it\n touches offline, with every operation still reporting `imported`.\n → [running-the-pipeline.md](references/running-the-pipeline.md)\n\n**Project-wide, not per resource**\n\n- **An attribute name holds exactly one type per Project**, not per ProductType,\n so a self-consistent plan can still be refused by what the project already\n holds — `AttributeDefinitionTypeConflict`, then `AttributeNameDoesNotExist` on\n every product using it. **No update action changes a type**: the fix removes\n and re-adds the attribute, deleting its values on every product that has one.\n Enum *values* may differ freely. `preflight` is the only stage that can see\n it. → [product-model.md](references/product-model.md)\n- **Slugs are unique per locale across the Project.** Retail taxonomies reuse\n names, so collisions are normal — and resolving one changes a URL, which is\n information loss and somebody else's decision.\n → [categories-and-slugs.md](references/categories-and-slugs.md)\n\n**Shape of the model**\n\n- **Prices are scoped by channel; a store only chooses which channels apply.**\n There is no `store` field on a price. Two distribution channels on one store,\n both priced, is legal and ambiguous — the storefront has to call\n `setLineItemDistributionChannel`. → [pricing-and-money.md](references/pricing-and-money.md)\n- **A store with no product selections offers everything**; one whose\n selections are all inactive, with an `Individual` among them, offers\n **nothing**. → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **Channels, customer groups and tax categories cannot be imported — `load`\n creates them** through the platform API before any import, with keys used\n **verbatim**: the one exception to the prefix rule, and what a prefix-scoped\n teardown leaves behind (a tax category `load` created can be removed with a flag). An existing one is **never modified** —\n a tax category keeps the project's rates, whatever the feed says. A price\n channel needs `ProductDistribution` or the API refuses the price. `productSelection` *is* importable and prefixed; `store` is not,\n and runs last. → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **Import Resources accept only KeyReferences, never ids.** Key everything\n `<prefix>-<sourceCode>`, ProductTypes included — though a ProductType's\n *name* comes from the unprefixed code, or the Merchant Center reads \"Mig\n Apparel Basic\". → [catalog-feed-contract.md](references/catalog-feed-contract.md)\n- **An order hint is a string strictly between 0 and 1 and must not end in\n `0`** — a zero-padded scheme emits `0.10` and is rejected.\n → [categories-and-slugs.md](references/categories-and-slugs.md)\n\n**Method**\n\n- **Do not serialise the Import API on polling.** Operations are retained 48\n hours precisely so an unresolved reference resolves when its target arrives;\n polling each stage to completion serialises a pipelined design, and frequent\n summary polling slows the import. Retry only `rejected` — other states are\n retried internally.\n- **Verify on a separate code path.** A verifier sharing logic with the writer\n confirms the writer's bugs. The audit gate reads the written plan back from\n disk and re-derives every invariant from the drafts as data.\n- **Refuse to write data you cannot express correctly.** Skipping is\n recoverable; wrong data in production is not. Report it as loss instead.\n\n## Reference index\n\n| Topic | Reference |\n| :--- | :--- |\n| Profiling a source sample and producing a mapping proposal (step 0b) | [references/source-discovery.md](references/source-discovery.md) |\n| The engagement decision log: what to record at each step, and what not to | [references/decision-log.md](references/decision-log.md) |\n| The feed contract: every record type, with the two load-bearing design rules | [references/catalog-feed-contract.md](references/catalog-feed-contract.md) |\n| Writing an adapter for an arbitrary source, and what to establish first | [references/writing-an-adapter.md](references/writing-an-adapter.md) |\n| ProductTypes: declared vs inferred, constraints, levels, searchability | [references/product-model.md](references/product-model.md) |\n| Money, price scopes, validity windows, embedded vs standalone | [references/pricing-and-money.md](references/pricing-and-money.md) |\n| Categories: tree, slug allocation, order hints | [references/categories-and-slugs.md](references/categories-and-slugs.md) |\n| Running the pipeline: each stage, each gate, and what is still absent | [references/running-the-pipeline.md](references/running-the-pipeline.md) |\n\n## Scope\n\nIn scope: ProductTypes, Categories, Products, Variants, and prices — the\nirreducible core of a loadable, browsable catalog.\n\nOut of scope, and deliberately so. **Ongoing sync after cutover** and delta\nfeeds as a running service are integration, not migration. **Customers, orders,\ncarts and payments** are a different migration with different invariants.\n**Promotions and discounts** are usually a re-modelling exercise rather than a\ntranslation, and **attribute modelling in the abstract**, the choice of tax mode\nand pricing strategy are product data modelling, upstream of this — though the\ntax categories a `Platform` mode needs are loaded, as prerequisites. **Cutover sequencing**,\nfreeze windows and rollback are out of scope too.\n\n**Opening stock is in scope; inventory as a running concern is not.** An\n`inventoryEntry` record loads the figure a project opens on, and whatever owns\ninventory afterwards will move it within the hour. Migrating stock does not\nmake this a sync tool, and a catalog loaded without stock is a perfectly\nnormal outcome.\n\nNever migrate credentials, payment tokens or PSP references. They are not\nportable, and re-keying them silently breaks reconciliation with the provider.\nPasswords do not migrate either — commercetools cannot import a foreign hash.\n\n## Checklist\n\nBefore running the pipeline:\n\n- [ ] The catalog-model question, and whether the project holds products, were\n asked **before** any adapter work, and the largest product's variant count\n is known (over 100 needs `Modular`).\n- [ ] The config was written from answers, not copied and guessed at. Nothing\n in it still reads `REPLACE-ME`.\n- [ ] The source has been inspected, not assumed — the adapter is written\n against a real export.\n- [ ] Every axis value is a language-independent code, with display text in\n `axisLabels`.\n- [ ] `market.currencyFractionDigits` has an explicit entry per currency.\n- [ ] `target.priceMode` matches how the implementation prices. Standalone\n pricing also needs `manage_standalone_prices` on the API Client, which\n `manage_products` does not grant.\n- [ ] `keys.prefix` names this engagement, so teardown can scope itself. Every\n resource carries it, ProductTypes included. Channels, customer groups and\n tax categories are the only exception: `load` creates the missing ones\n with their keys **verbatim**, and teardown leaves them unless told to remove\n the tax categories `load` created.\n- [ ] Whether opening stock is in scope was **asked**, not assumed either way.\n A catalog loaded without it is normal; a catalog loaded with stock nobody\n agreed to is a figure the business did not sign off. If it is in scope,\n every `inventoryEntry` names a SKU the feed declares, and a deliberate\n zero is emitted rather than omitted.\n- [ ] The tax mode was asked of whoever owns tax. Under `Platform`, every\n product has a `taxCategory`, every category has a rate for each country\n it sells into, and each rate's `includedInPrice` was confirmed, not\n inferred. Under `External` or `ExternalAmount`, `target.taxMode` is set,\n so `products-without-tax-category` stops, and the choice is in\n `DECISIONS.md`.\n- [ ] `preflight` reports no `product-type-keys-unprefixed`. If it does, the\n project was loaded before ProductType keys were prefixed, and a Product's\n ProductType cannot be changed after creation.\n- [ ] If the feed declares any `customerGroup`, the API Client has\n `view_customer_groups` and `manage_customer_groups` — `manage_products`\n grants neither, and `load` needs both to create one. Channels are covered\n by `manage_products`.\n- [ ] If the feed declares any `productSelection`, the API Client has\n `view_product_selections` and `manage_product_selections`; if it declares\n any `store`, `view_stores` and `manage_stores`. `manage_products` grants\n none of these.\n- [ ] Every `productSelection` `mode` has been confirmed with whoever owns the\n assortment. It is permanent, and the two modes are opposites.\n- [ ] No store's selections are all inactive — that offers no products, where\n an empty list would have offered every product.\n- [ ] `INTERVIEW.md` has an answer in every row, including any\n added after step 0, and is committed with `DECISIONS.md`.\n- [ ] `DECISIONS.md` exists, is committed, and has an entry for every step 0\n decision — including the ones still open and the questions the interview\n file lists as not asked.\n\nBefore loading:\n\n- [ ] `DECISIONS.md` names the pipeline checkout that ran the stages\n (`git rev-parse --short HEAD`, in its uncommitted-changes form if the tree\n differs), including when the checkout was already there.\n- [ ] `validate`, `derive`, `plan` and `audit` all pass — in that order, in one\n sitting. If `plan` fails it writes nothing and the previous `plan.json`\n survives; `audit` refuses a plan whose feed digest no longer matches\n (`plan-stale`), but a plan with no digest at all can only report\n *unknown*, so do not rely on the check to catch a replay.\n- [ ] `out/MODEL-REVIEW.md` has been read and its irreversible choices accepted.\n- [ ] `SameForAll` and `CombinationUnique` are right — they cannot be tightened\n later.\n- [ ] Each recorded information loss is accepted, or the adapter is fixed.\n- [ ] Sample payloads have been eyeballed, money values checked against the\n source — in either price mode; standalone prices are listed per SKU.\n- [ ] Slug changes forced by project-wide uniqueness have been shown to whoever\n owns SEO.\n- [ ] Attribute names shared across ProductTypes agree on `isSearchable`.\n- [ ] `preflight` reports no `tax-category-name-taken`, and any\n `tax-category-rates-differ` has been shown to whoever owns tax — the\n project's rates are the ones that will apply.\n- [ ] `preflight` reports no `attribute-type-conflict`. An attribute name may\n hold one type per Project, and the conflict can come from a ProductType\n the migration never touches.\n\nAfter loading:\n\n- [ ] Counts and structural properties verified through a separate code path.\n- [ ] Products published deliberately, as a separate step — the pipeline always\n plans `publish: false`. If the target project already holds **published**\n products, confirm before `--execute` that unpublishing the ones this run\n touches is intended: `publish: false` unpublishes, silently.\n- [ ] The container summary was read **inside 48 hours** of the run. Past that,\n failed operations are deleted and a summary of survivors reads clean.\n `verify` reads resources, which do not expire.\n"
}SHA-256 of public snapshot: f083e794f75eae040d27686ec94316f97e9e7eb118b6e1c0b060dc8129a4aeeb