diff --git a/use-crystallize/skills/content-model/references/create-shape-api.md b/use-crystallize/skills/content-model/references/create-shape-api.md index b2ca120..06c91ee 100644 --- a/use-crystallize/skills/content-model/references/create-shape-api.md +++ b/use-crystallize/skills/content-model/references/create-shape-api.md @@ -140,7 +140,7 @@ config: { required?: Boolean discoverable?: Boolean multilingual?: Boolean - decimalPlaces?: Int + decimalPlaces?: Int // 1–64; leave it out for integers (mass operations reject 0) units?: String[] // list of allowed units editors can select (e.g. ["kg", "g", "lb"]) } } diff --git a/use-crystallize/skills/mass-operations/references/limits.md b/use-crystallize/skills/mass-operations/references/limits.md index e9b3b6a..63ae962 100644 --- a/use-crystallize/skills/mass-operations/references/limits.md +++ b/use-crystallize/skills/mass-operations/references/limits.md @@ -98,7 +98,9 @@ Treat `propertiesTable` as config-driven: the shape decides the shape of the dat - **`decimalPlaces` FLOORS the stored number.** It is not a display setting — the value is truncated on write, and it does not round. -- **`decimalPlaces: 0` takes a `parseInt(String(...))` branch that destroys small-magnitude numbers.** +- **`decimalPlaces: 0` is rejected** by mass-operation validation (`Too small: expected number to be >0`). + Leave the key out for integers. If a shape's config holds 0 anyway, the runner's content path takes a + `parseInt(String(...))` branch that destroys small-magnitude numbers. - An empty-string `unit` is silently dropped in content, but **throws** in shape config. ### Choice and selection filter to configured options @@ -162,7 +164,7 @@ Ceilings on the `max` you may configure, and therefore on content. | `itemRelations` | **75** (quick-select folders: 100) | | `images`, `files`, `videos`, `gridRelations` | **512** | | `colors` | **100** | -| `numeric` `decimalPlaces` | 0–**64** | +| `numeric` `decimalPlaces` | 1–**64** (0 is rejected) | | Configurable `min`/`max` bounds | max **1048576**, min **256** | | Component nesting depth | **5**, following piece expansion | diff --git a/use-crystallize/skills/mutation/references/core-api.md b/use-crystallize/skills/mutation/references/core-api.md index ce28e26..c012bed 100644 --- a/use-crystallize/skills/mutation/references/core-api.md +++ b/use-crystallize/skills/mutation/references/core-api.md @@ -600,7 +600,7 @@ Discovery's vector ranking is authored entirely on the Core API. Four calls, in | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `upsertVocabulary(input: UpsertVocabularyInput!)` | **Full replace**, not a patch — omitted dimensions are dropped | | `setItemTaste(input: SetItemTasteInput!)` | One item, one language, one vocabulary. Writes the **draft** | -| `publishItems(ids: [ID!]!, language: String!)` | The indexer reads the published version — skipping this fails silently | +| `publishItem(id: ID!, language: String!)` | The indexer reads the published version — skipping this fails silently. See the note on `publishItems` below | | `igniteDiscoApi(stacks: opensearch)` | Async; poll `bulkTask(id:)` until `complete`, then allow propagation. `stacks: opensearch` is required for vectors to be built | ```graphql @@ -651,6 +651,11 @@ every failure and `errorName` identifies it. `setItemTaste` and `igniteDiscoApi` Read back with `vocabulary(name:)` and `item(id:, language:) { taste { vocabulary entries { key weight } } }`. +Prefer `publishItem` per item over `publishItems` here. `publishItem` returns the published version +(`PublishInfo`) or an error for that item; `publishItems` returns a `PublishItemsRequest`, and its return +is not proof that the items are published yet — index right after it and the index may read the old +versions. + **Re-run `igniteDiscoApi` after every change to vocabularies or taste entries** — an unindexed change has no effect and raises no error. Omitting `stacks: opensearch` likewise fails silently: the index rebuilds, but without vectors. Full guidance, including vocabulary design, positional weights and diff --git a/use-crystallize/skills/mutation/references/shop-api-mutations.md b/use-crystallize/skills/mutation/references/shop-api-mutations.md index 5ffe83b..cc17dc5 100644 --- a/use-crystallize/skills/mutation/references/shop-api-mutations.md +++ b/use-crystallize/skills/mutation/references/shop-api-mutations.md @@ -18,7 +18,7 @@ cart → placed → ordered | ----------- | ----------------------------------------------- | -------- | | `cart` | Active cart, items and prices can be changed | Yes | | `placed` | Locked for payment — no modifications allowed | No | -| `ordered` | Fulfilled — linked to an order via `orderId` | No | +| `ordered` | Linked to an order via `orderId` | No | | `abandoned` | Explicitly abandoned (e.g., user left checkout) | No | Query the cart state with: @@ -30,7 +30,7 @@ query { state # cart | placed | ordered | abandoned isStale # true if prices may have changed since last hydration isExpired # true if cart has expired - orderId # set after fulfill, links to the order + orderId # set when an order is created from the cart } } ``` @@ -418,7 +418,7 @@ mutation { ### Fulfill Cart -After creating an order (via [`createFromCart`](shop-api-order-mutations.md) on the `/order` endpoint), mark the cart as fulfilled by linking it to the order ID: +**Not needed after `createFromCart`.** [`createFromCart`](shop-api-order-mutations.md) on the `/order` endpoint already moves the cart to `ordered` and sets `orderId`, and the order id is the cart id. Use `fulfill` to link a cart to an order created some other way: ```graphql mutation { @@ -488,9 +488,11 @@ The full checkout flow spans the Core API, `/cart` endpoint, and `/order` endpoi 2. (Optional edits) → POST /cart — addSkuItem, setCustomer, setAddresses, etc. 3. Place Cart → POST /cart — Lock cart for payment 4. Create Order → POST /order — createFromCart (⚠ different endpoint!) -5. (Optional) Fulfill → POST /cart — Link cart to order ID ``` +After step 4 the cart is `ordered` and linked to the order; the order id is the cart id. There is no +separate fulfill step. + ### Customer Creation Customers can be created **before** checkout via the [Core API](core-api.md) or **inline** during hydration: diff --git a/use-crystallize/skills/mutation/references/shop-api-order-mutations.md b/use-crystallize/skills/mutation/references/shop-api-order-mutations.md index 500c092..b7d31a4 100644 --- a/use-crystallize/skills/mutation/references/shop-api-order-mutations.md +++ b/use-crystallize/skills/mutation/references/shop-api-order-mutations.md @@ -548,6 +548,9 @@ mutation { } ``` +`createFromCart` also moves the cart to `ordered` and sets its `orderId`. The order id is the cart id, so +there is nothing left to link: do not follow it with `fulfill`. + ## Best Practices 1. **Use the correct endpoint** — Cart operations on `/cart`, order operations on `/order` diff --git a/use-crystallize/skills/pricing/references/price-lists-and-markets.md b/use-crystallize/skills/pricing/references/price-lists-and-markets.md index 8edbc06..526a122 100644 --- a/use-crystallize/skills/pricing/references/price-lists-and-markets.md +++ b/use-crystallize/skills/pricing/references/price-lists-and-markets.md @@ -140,31 +140,56 @@ Price lists override or adjust the base price (from price variants) for specific - **Period** (optional) — Start and end dates - **Target** — Market, customer group, or individual customer -### Via PIM API +### Via Core API ```graphql mutation CreatePriceList { - priceList { - create( - input: { - tenantId: "your-tenant-id" - identifier: "eu-summer-sale" - name: "EU Summer Sale" - modifierType: PERCENTAGE - priceVariants: ["retail"] - selectedProductVariants: { type: ALL } - targetAudience: { marketIdentifiers: ["eu-retail"] } - startDate: "2025-06-01T00:00:00Z" - endDate: "2025-08-31T23:59:59Z" - } - ) { + createPriceList( + input: { + identifier: "eu-summer-sale" + name: "EU Summer Sale" + modifierType: PERCENTAGE + priceVariants: [{ identifier: "retail", modifier: -10 }] + selectedProductVariants: { type: ALL_SKUS } + targetAudience: { type: SOME, marketIdentifiers: ["eu-retail"] } + startDate: "2025-06-01T00:00:00Z" + endDate: "2025-08-31T23:59:59Z" + } + ) { + ... on PriceList { identifier name } + ... on BasicError { + errorName + message + } } } ``` +Every part of the input is an object, not a bare identifier: + +| Field | Shape | +| ------------------------- | -------------------------------------------------------------------------------------------------- | +| `priceVariants` | `[{ identifier, modifier, decimalPlaces? }]` — `modifier` is read according to `modifierType` | +| `selectedProductVariants` | `{ type: ALL_SKUS \| SOME_SKUS, variants?: [{ sku, priceVariants: [{ identifier, modifier }] }] }` | +| `targetAudience` | `{ type: EVERYONE \| SOME, marketIdentifiers?, customerGroupIdentifiers?, customerIdentifiers? }` | + +`targetAudience.type` is required. With `SOME`, name the audience in one or more of the identifier lists. +There is no `ALL` selection type — it is `ALL_SKUS` or `SOME_SKUS`. A list for specific SKUs: + +```graphql +selectedProductVariants: { + type: SOME_SKUS + variants: [{ sku: "olive-oil-500ml", priceVariants: [{ identifier: "retail", modifier: -15 }] }] +} +``` + +The legacy PIM API (`priceList { create(input: { tenantId, ... }) }`) takes the same input shape plus +`tenantId`. In a mass operation the intent is `pricelist/create` or `pricelist/upsert`, with the same +fields at the top level of the operation. + ### Adjustment Types | Type | Description | Example | diff --git a/use-crystallize/skills/vector-ranking/SKILL.md b/use-crystallize/skills/vector-ranking/SKILL.md index 8b41c4b..1bda66a 100644 --- a/use-crystallize/skills/vector-ranking/SKILL.md +++ b/use-crystallize/skills/vector-ranking/SKILL.md @@ -62,11 +62,11 @@ the argument in the schema as the capability check. Three enums are generated **per tenant**, so they are never hardcodable: -| Enum | Built from | Gotcha | -| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `TenantVocabularyIdentifier` | your vocabularies | a new vocabulary is only a valid value **after the next index run** | -| `TenantRankByField` | NUMBER and DATE **filterable** attributes, **facet fields excluded** | it is _not_ "any numeric field" — introspect it | -| `TenantRankByTieBreaker` | sortable fields (token, number, date) | `tieBreaker` is **required** on every `rankBy` | +| Enum | Built from | Gotcha | +| ---------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `TenantVocabularyIdentifier` | your vocabularies | a new vocabulary is only a valid value **after the next index run**; until a tenant has one, the argument is a `String` | +| `TenantRankByField` | NUMBER and DATE **filterable** attributes, **facet fields excluded** | it is _not_ "any numeric field" — introspect it | +| `TenantRankByTieBreaker` | sortable fields (token, number, date) | `tieBreaker` is **required** on every `rankBy` | Introspect all three rather than guessing: @@ -124,7 +124,7 @@ Two properties fall out of this and drive every design decision: ```text Core upsertVocabulary once per vocabulary — FULL REPLACE, not a patch Core setItemTaste once per item, per vocabulary — writes the DRAFT -Core publishItems the step that is easy to miss +Core publishItem per item and language — the step that is easy to miss Core igniteDiscoApi(stacks: opensearch) poll bulkTask until "complete", then let it propagate Discovery search(rankBy:) your rules: margin, stock, velocity, recency Discovery search(context:) ranked for this shopper @@ -168,20 +168,21 @@ They are **not** on `Topic.children`, which takes `language` only. Note the nami Four of these produce **no error at all** — they are the reason this skill exists. -| Symptom | Cause | Fix | -| --------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| Order unrelated to taste, no errors | Taste is on the draft only | `publishItems`, index again, retest | -| Order unchanged after editing taste | No index run since the change | `igniteDiscoApi(stacks: opensearch)`, wait for `complete` | -| Index rebuilt, still no vectors | `igniteDiscoApi` run without `stacks: opensearch` | Re-run with `stacks: opensearch` | -| Rankings subtly wrong, no errors | `magnitude` miscomputed client-side | Assert `sqrt(Σ w²)` against a known case | -| `rankScore` comes back `null` | No rerank ran — no `rankBy`, or every term had `weight: 0` | Give at least one term a non-zero weight | -| Vocabulary "does not exist in enum" | No index run since it was created | `igniteDiscoApi(stacks: opensearch)`, wait for `complete` | -| `context` / `rankBy` unknown in schema | Ranking not enabled for the tenant, or not indexed | Confirm ranking is enabled, then index and wait for `complete` plus propagation. Detect the capability, don't assume it | -| `Malformed taste entry key` | Key has no colon | Use `dimensionId:value` | -| `Unknown dimension` | Prefix is not a declared dimension | Add it to the vocabulary, or fix the key | -| `tieBreaker` validation error | Required field missing | Add a `tieBreaker` | -| `ExperimentalFeaturesNotAvailableError` | Vectors not enabled for the tenant | Enable the feature before authoring taste | -| Page 2 empty under ranking | `skip` ran past the rerank window | Raise `options.rerankWindow` (default 500, cap 2000) | +| Symptom | Cause | Fix | +| --------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| Order unrelated to taste, no errors | Taste is on the draft only | `publishItem`, index again, retest | +| Order unchanged after editing taste | No index run since the change | `igniteDiscoApi(stacks: opensearch)`, wait for `complete` | +| Index rebuilt, still no vectors | `igniteDiscoApi` run without `stacks: opensearch` | Re-run with `stacks: opensearch` | +| Rankings subtly wrong, no errors | `magnitude` miscomputed client-side | Assert `sqrt(Σ w²)` against a known case | +| `rankScore` comes back `null` | No rerank ran — no `rankBy`, or every term had `weight: 0` | Give at least one term a non-zero weight | +| Vocabulary "does not exist in enum" | No index run since it was created | `igniteDiscoApi(stacks: opensearch)`, wait for `complete` | +| `Vocabulary not found` | No vocabulary indexed yet; the argument is still a `String` | `igniteDiscoApi(stacks: opensearch)`, wait for `complete`, then pass the enum value unquoted | +| `context` / `rankBy` unknown in schema | Ranking not enabled for the tenant, or not indexed | Confirm ranking is enabled, then index and wait for `complete` plus propagation. Detect the capability, don't assume it | +| `Malformed taste entry key` | Key has no colon | Use `dimensionId:value` | +| `Unknown dimension` | Prefix is not a declared dimension | Add it to the vocabulary, or fix the key | +| `tieBreaker` validation error | Required field missing | Add a `tieBreaker` | +| `ExperimentalFeaturesNotAvailableError` | Vectors not enabled for the tenant | Enable the feature before authoring taste | +| Page 2 empty under ranking | `skip` ran past the rerank window | Raise `options.rerankWindow` (default 500, cap 2000) | ## How ranking composes with the rest of the query diff --git a/use-crystallize/skills/vector-ranking/references/vocabulary-authoring.md b/use-crystallize/skills/vector-ranking/references/vocabulary-authoring.md index 45cf01f..f1a7dea 100644 --- a/use-crystallize/skills/vector-ranking/references/vocabulary-authoring.md +++ b/use-crystallize/skills/vector-ranking/references/vocabulary-authoring.md @@ -221,13 +221,24 @@ items were published before you attached taste, publish them again or the served vectors. ```graphql -mutation Publish($ids: [ID!]!, $language: String!) { - publishItems(ids: $ids, language: $language) { +mutation Publish($id: ID!, $language: String!) { + publishItem(id: $id, language: $language) { __typename + ... on PublishInfo { + versionId + } + ... on BasicError { + errorName + message + } } } ``` +Publish **per item and per language**. `publishItem` answers with the published version or an error +for that item. `publishItems` returns a `PublishItemsRequest` instead, and its return is not proof that +the items are published yet — index right after it and the index may read the old versions. + **Skipping this step produces no error.** Queries still return results, and the order may even change — it just has no relation to taste. The check in step 6 is the only thing that catches it. @@ -284,6 +295,12 @@ vocabulary you created becomes a value of the `TenantVocabularyIdentifier` enum. a valid enum value after the next index run** — until then, queries referencing it fail schema validation. +Before a tenant has any indexed vocabulary, the same arguments (`nearestTo.vocabulary`, +`userTaste.vocabulary`) are typed `String`, and a query fails with `Vocabulary not found`. After +the index run they take the enum: `vocabulary: taste`, not `vocabulary: "taste"`. A query or a typed +variable written for one form fails on the other, so check the type with introspection (see +[SKILL.md](../SKILL.md)) before you hardcode either. + ## 6. Verify Two checks, in this order. Both are cheap and both catch a silent failure.