Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
}
Expand Down
6 changes: 4 additions & 2 deletions use-crystallize/skills/mass-operations/references/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
7 changes: 6 additions & 1 deletion use-crystallize/skills/mutation/references/core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions use-crystallize/skills/mutation/references/shop-api-mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
}
}
```
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
41 changes: 21 additions & 20 deletions use-crystallize/skills/vector-ranking/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <name> 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

Expand Down
Loading
Loading