@instruments/materialgraph 0.7.1 → 0.9.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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The vocabulary lens (MG-913) — the hand-mirrored wire types for
3
+ * `POST /api/v1/resolve`.
4
+ *
5
+ * Split out of `types.ts` for the repository's source-file size budget, the same
6
+ * reason `suggest-types.ts` sits beside it; both are re-exported from `types.ts`,
7
+ * so importers see no difference.
8
+ *
9
+ * THE LENS: it turns WORDS into vocabulary, never into materials.
10
+ * `mg.search()` answers "I want materials" and `mg.answer` questions want prose;
11
+ * this answers "what do these words mean in your vocabulary?" — the question you
12
+ * ask BEFORE you spend a token on a filter, a facet or a brand slug. It is the
13
+ * reason you should not keep a hand-written table mapping your users' words onto
14
+ * this API's tokens: that table is a copy of a registry you do not own, and it
15
+ * drifts silently the moment the registry moves.
16
+ */
17
+ /**
18
+ * Which controlled vocabulary a reading came from — and therefore what `id`
19
+ * means and where you can spend it.
20
+ *
21
+ * - `brand` — a brand slug, for `mg.brand()` or a brand filter.
22
+ * - `paint_identity` — one exact vendor paint (a library colour), by its vendor
23
+ * code where the library has one.
24
+ * - `colour` — a canonical colour-family term, namespaced (`colour_family.green`).
25
+ * - `category` — a product-category taxonomy node key.
26
+ * - `attribute` — a registry attribute id: the LEFT half of an `attribute:value`
27
+ * pair, for `mg.bySpec()` or `search({ filters })`.
28
+ * - `attribute_value` — a value-dictionary term, namespaced (`ms_sheen.matte`):
29
+ * the RIGHT half.
30
+ */
31
+ export type ResolutionKind = "brand" | "paint_identity" | "colour" | "category" | "attribute" | "attribute_value";
32
+ /**
33
+ * How the phrase matched, strongest first. `exact` — it IS the canonical token;
34
+ * `alias` — it IS a recorded alternative name for it; `lexical` — it resembles
35
+ * one closely enough to clear the resolver's own confidence floor.
36
+ *
37
+ * There is deliberately no `semantic` rung on this lens. Every tier that would
38
+ * cost an embedding round-trip is switched off, which is what keeps resolution
39
+ * millisecond-class; a phrase only a semantic match could have caught comes back
40
+ * unresolved rather than slow.
41
+ */
42
+ export type ResolutionRung = "exact" | "alias" | "lexical";
43
+ /** One thing a phrase turned out to mean. */
44
+ export interface ResolvedMeaning {
45
+ kind: ResolutionKind;
46
+ /**
47
+ * The canonical token. OPAQUE — hand it back verbatim, never parse, tidy or
48
+ * construct one. The single thing worth knowing about the SHAPE: a
49
+ * value-dictionary term id is unique only within its dictionary, so `colour`
50
+ * and `attribute_value` ids arrive namespaced as `<dictionaryId>.<termId>`.
51
+ */
52
+ id: string;
53
+ /** The canonical display term — what to show a human, not what to send back. */
54
+ term: string;
55
+ rung: ResolutionRung;
56
+ /**
57
+ * The resolver's own confidence in [0,1], present only when the underlying
58
+ * resolver produces one. Its ABSENCE is meaningful rather than missing data:
59
+ * the rung is then the whole statement, because an exact paint identity is an
60
+ * equality and not a score.
61
+ */
62
+ confidence?: number;
63
+ /**
64
+ * Present only when the resolver reported a runner-up. A small `gap` means the
65
+ * answer was nearly something else; when the resolver refused to break the tie,
66
+ * every tied reading is listed and `gap` is 0.
67
+ */
68
+ ambiguity?: {
69
+ gap: number;
70
+ };
71
+ }
72
+ /** What one phrase resolved to — including, explicitly, nothing. */
73
+ export interface PhraseResolution {
74
+ /** The phrase exactly as you sent it, so you can rejoin the batch by value. */
75
+ phrase: string;
76
+ /**
77
+ * The phrase in the one normalised form the resolvers fold to — the same
78
+ * `normalise` stage a search query passes through. Empty when the phrase was
79
+ * all punctuation.
80
+ */
81
+ normalised: string;
82
+ /**
83
+ * Everything the phrase means here, best first. A phrase can legitimately mean
84
+ * several things at once ("sage" is two vendor paints AND a colour family), so
85
+ * this is a list rather than a winner.
86
+ */
87
+ resolved: ResolvedMeaning[];
88
+ /**
89
+ * TRUE when nothing resolved. Read this rather than inferring it from
90
+ * `resolved.length`: the redundancy is deliberate, because a consumer that
91
+ * cannot SEE what failed to resolve cannot tell a word this vocabulary does
92
+ * not have from a word something quietly dropped. Absent is not false — an
93
+ * unresolved phrase is a statement about this vocabulary, never about the
94
+ * material world.
95
+ */
96
+ unresolved: boolean;
97
+ }
98
+ /** The resolve response: one entry per phrase, in the order you sent them. */
99
+ export interface ResolveResponse {
100
+ /** One per phrase, in request order — never reordered, never deduplicated. */
101
+ resolutions: PhraseResolution[];
102
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The vocabulary lens (MG-913) — the hand-mirrored wire types for
3
+ * `POST /api/v1/resolve`.
4
+ *
5
+ * Split out of `types.ts` for the repository's source-file size budget, the same
6
+ * reason `suggest-types.ts` sits beside it; both are re-exported from `types.ts`,
7
+ * so importers see no difference.
8
+ *
9
+ * THE LENS: it turns WORDS into vocabulary, never into materials.
10
+ * `mg.search()` answers "I want materials" and `mg.answer` questions want prose;
11
+ * this answers "what do these words mean in your vocabulary?" — the question you
12
+ * ask BEFORE you spend a token on a filter, a facet or a brand slug. It is the
13
+ * reason you should not keep a hand-written table mapping your users' words onto
14
+ * this API's tokens: that table is a copy of a registry you do not own, and it
15
+ * drifts silently the moment the registry moves.
16
+ */
17
+ export {};
package/run-events.d.ts CHANGED
@@ -50,7 +50,18 @@ export interface CompositionReviewCriteriaSuggestion {
50
50
  export interface CompositionReviewCriteriaUnresolved {
51
51
  slotId: string;
52
52
  raw: string;
53
+ /**
54
+ * Ranked corrections, best first. MG-994: every one of these is an attribute
55
+ * the slot's own category holds accepted values for, so accepting the top
56
+ * suggestion can never leave the slot unfillable.
57
+ */
53
58
  suggestions: CompositionReviewCriteriaSuggestion[];
59
+ /**
60
+ * MG-994: attribute ids ranked for this phrase and then withheld — the slot's
61
+ * category holds no accepted value for them. ABSENT when nothing was withheld
62
+ * (absent means nothing was removed, not that nothing could be).
63
+ */
64
+ withheld?: string[];
54
65
  }
55
66
  /**
56
67
  * Payload of a `composition.review-criteria` / `material-box.review-criteria`
package/run-types.d.ts CHANGED
@@ -233,6 +233,31 @@ export interface CreatePublicRunResponse {
233
233
  export interface PublicRunResponse {
234
234
  run: PublicRun;
235
235
  }
236
+ /** Query for `GET /api/v1/runs` ({@link MaterialGraphClient.listRuns}). */
237
+ export interface ListRunsOptions {
238
+ /**
239
+ * Return only Runs in this status. `"suspended"` is the recovery question:
240
+ * which of my Runs is still waiting on an answer?
241
+ */
242
+ status?: PublicRunStatus;
243
+ /** Page size, 1–100. Defaults to 20 server-side. */
244
+ limit?: number;
245
+ /**
246
+ * The previous page's {@link PublicRunListResponse.nextCursor}, round-tripped
247
+ * VERBATIM. It is opaque — never parse or construct one.
248
+ */
249
+ cursor?: string;
250
+ signal?: AbortSignal;
251
+ }
252
+ /** The `GET /api/v1/runs` response envelope — one page, newest first. */
253
+ export interface PublicRunListResponse {
254
+ runs: PublicRun[];
255
+ /**
256
+ * Pass as {@link ListRunsOptions.cursor} for the next page. ABSENT — not
257
+ * empty-string, not null — once the last page has been served.
258
+ */
259
+ nextCursor?: string;
260
+ }
236
261
  /** Body for `POST /api/v1/runs/{runId}/resume`. */
237
262
  export interface ResumePublicRunRequest {
238
263
  /** The resume payload, validated against the suspended step's resume schema. */
package/runs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { HttpTransport } from "./http-transport.js";
2
2
  import type { FinishScheduleResponse, GetRunScheduleOptions } from "./schedule-types.js";
3
- import { type CapabilityManifestResponse, type CreatePublicRunRequest, type CreatePublicRunResponse, type DrainRunEventsOptions, type PublicRunResponse, type ResumeRunOptions, type RunEvent, type SourceMaterialBatchDetail, type SourceMaterialBatchReceipt, type StreamRunEventsOptions, type UploadSourceMaterialInput } from "./run-types.js";
3
+ import { type CapabilityManifestResponse, type CreatePublicRunRequest, type CreatePublicRunResponse, type DrainRunEventsOptions, type ListRunsOptions, type PublicRunListResponse, type PublicRunResponse, type ResumeRunOptions, type RunEvent, type SourceMaterialBatchDetail, type SourceMaterialBatchReceipt, type StreamRunEventsOptions, type UploadSourceMaterialInput } from "./run-types.js";
4
4
  /**
5
5
  * Upload one atomic source-material batch (`POST /api/v1/source-material`).
6
6
  * Accepts the pile-of-files shape (`{ files }` → repeatable `files` field) or
@@ -25,6 +25,20 @@ export declare function getSourceMaterialBatch(transport: HttpTransport, batchId
25
25
  export declare function capabilities(transport: HttpTransport, signal?: AbortSignal): Promise<CapabilityManifestResponse>;
26
26
  /** Start an asynchronous capability Run (`POST /api/v1/runs`). */
27
27
  export declare function createRun(transport: HttpTransport, body: CreatePublicRunRequest, signal?: AbortSignal): Promise<CreatePublicRunResponse>;
28
+ /**
29
+ * List your own Runs, newest first (`GET /api/v1/runs`).
30
+ *
31
+ * The recovery path for a client that still holds its API key but has lost a Run
32
+ * id — a reinstalled app, or a crash before the id from {@link createRun} was
33
+ * persisted. `{ status: "suspended" }` finds the Run still waiting on an answer.
34
+ *
35
+ * Only the calling principal's own Runs are ever returned; there is no
36
+ * cross-principal listing.
37
+ *
38
+ * Page by following `nextCursor` until it is ABSENT — a full page is not the
39
+ * signal to stop, and an absent cursor is the only one.
40
+ */
41
+ export declare function listRuns(transport: HttpTransport, options?: ListRunsOptions): Promise<PublicRunListResponse>;
28
42
  /** Get a Run's current state (`GET /api/v1/runs/{runId}`). */
29
43
  export declare function getRun(transport: HttpTransport, runId: string, signal?: AbortSignal): Promise<PublicRunResponse>;
30
44
  /**
package/runs.js CHANGED
@@ -99,6 +99,30 @@ export function createRun(transport, body, signal) {
99
99
  signal,
100
100
  });
101
101
  }
102
+ /**
103
+ * List your own Runs, newest first (`GET /api/v1/runs`).
104
+ *
105
+ * The recovery path for a client that still holds its API key but has lost a Run
106
+ * id — a reinstalled app, or a crash before the id from {@link createRun} was
107
+ * persisted. `{ status: "suspended" }` finds the Run still waiting on an answer.
108
+ *
109
+ * Only the calling principal's own Runs are ever returned; there is no
110
+ * cross-principal listing.
111
+ *
112
+ * Page by following `nextCursor` until it is ABSENT — a full page is not the
113
+ * signal to stop, and an absent cursor is the only one.
114
+ */
115
+ export function listRuns(transport, options = {}) {
116
+ const query = {
117
+ ...(options.status === undefined ? {} : { status: options.status }),
118
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
119
+ ...(options.cursor === undefined ? {} : { cursor: options.cursor }),
120
+ };
121
+ return transport.requestJson(runOperation("list_public_runs"), {
122
+ ...(Object.keys(query).length > 0 ? { query } : {}),
123
+ ...(options.signal ? { signal: options.signal } : {}),
124
+ });
125
+ }
102
126
  /** Get a Run's current state (`GET /api/v1/runs/{runId}`). */
103
127
  export function getRun(transport, runId, signal) {
104
128
  return transport.requestJson(runOperation("get_public_run"), {
@@ -1,20 +1,23 @@
1
- /**
2
- * Wire types for the governed STATIC schema (`GET /api/v1/schema/*`).
3
- *
4
- * THE LENS: "what does the registry DEFINE?" — blocks, attributes, units, value
5
- * dictionaries, product types and their external mappings. Definitions, not
6
- * data: nothing here says how many materials carry an attribute or which values
7
- * the corpus holds. For that, use `mg.specificationFacets()` and
8
- * `mg.attributeValues()`; for one term explained in prose, `mg.glossary()`; for
9
- * what one material has been determined on, `mg.materialBlocks()`.
10
- *
11
- * Every id here (`attributeId`, `blockId`, product-type keys) is canonical
12
- * VOCABULARY, not an opaque token — this is where you discover it rather than
13
- * invent it.
14
- */
15
1
  import type { OpenEnum } from "./types.js";
16
2
  /** The nine governed static resources. */
17
3
  export type SchemaResource = "blocks" | "attributes" | "verticals" | "productTypes" | "units" | "valueDictionaries" | "sourceRequirements" | "productTypeTemplates" | "mappings";
4
+ /**
5
+ * Resource → the operation that serves it. Internal to the SDK (`index.ts`
6
+ * re-exports this module's TYPES only), and it lives beside
7
+ * {@link SchemaResource} rather than in the client so the mapping and the union
8
+ * it must cover cannot drift apart.
9
+ */
10
+ export declare const SCHEMA_RESOURCE_OPERATION_IDS: {
11
+ readonly blocks: "list_schema_blocks";
12
+ readonly attributes: "list_schema_attributes";
13
+ readonly verticals: "list_schema_verticals";
14
+ readonly productTypes: "list_schema_product_types";
15
+ readonly units: "list_schema_units";
16
+ readonly valueDictionaries: "list_schema_value_dictionaries";
17
+ readonly sourceRequirements: "list_schema_source_requirements";
18
+ readonly productTypeTemplates: "list_schema_product_type_templates";
19
+ readonly mappings: "list_schema_mappings";
20
+ };
18
21
  export type SchemaResourceRequest = SchemaResource;
19
22
  export interface SchemaLifecycle {
20
23
  status: OpenEnum<"draft" | "active" | "deprecated">;
@@ -1 +1,17 @@
1
- export {};
1
+ /**
2
+ * Resource → the operation that serves it. Internal to the SDK (`index.ts`
3
+ * re-exports this module's TYPES only), and it lives beside
4
+ * {@link SchemaResource} rather than in the client so the mapping and the union
5
+ * it must cover cannot drift apart.
6
+ */
7
+ export const SCHEMA_RESOURCE_OPERATION_IDS = {
8
+ blocks: "list_schema_blocks",
9
+ attributes: "list_schema_attributes",
10
+ verticals: "list_schema_verticals",
11
+ productTypes: "list_schema_product_types",
12
+ units: "list_schema_units",
13
+ valueDictionaries: "list_schema_value_dictionaries",
14
+ sourceRequirements: "list_schema_source_requirements",
15
+ productTypeTemplates: "list_schema_product_type_templates",
16
+ mappings: "list_schema_mappings",
17
+ };
@@ -143,6 +143,14 @@ export function checkSearchResultItem(value, what, fail) {
143
143
  requireString(record.product, `${what}.product`, fail);
144
144
  requireNullableString(record.brandSlug, `${what}.brandSlug`, fail);
145
145
  requireNullableString(record.productId, `${what}.productId`, fail);
146
+ // `z.string().optional()` — absent, or a string. NOT nullable: the emit path
147
+ // omits the key rather than sending null when no MB identity is known.
148
+ requireOptionalString(record.materialBankId, `${what}.materialBankId`, fail);
149
+ // Same shape as `materialBankId`: absent, or a string. The emit path omits
150
+ // the key when no primary product type is on record rather than sending null,
151
+ // so "we don't know what this is" and "this is a null kind of thing" cannot
152
+ // be confused.
153
+ requireOptionalString(record.productType, `${what}.productType`, fail);
146
154
  requireNullableString(record.color, `${what}.color`, fail);
147
155
  requireNullableString(record.variantName, `${what}.variantName`, fail);
148
156
  requireNullableString(record.imageUrl, `${what}.imageUrl`, fail);
package/types.d.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  * match set. Follow the cursor for the rest, and never report a page size as
26
26
  * a total.
27
27
  */
28
+ import type { ByColourSwatch } from "./by-colour-types.js";
28
29
  import type { BySpecVerdict } from "./by-spec-types.js";
29
30
  import type { Cursor, ProductId, VariantId } from "./identifiers.js";
30
31
  import type { MaterialDetail, MaterialTile } from "./material-types.js";
@@ -39,12 +40,22 @@ export type OpenEnum<T extends string> = T | (string & {});
39
40
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
40
41
  [key: string]: JsonValue;
41
42
  };
42
- /** One palette swatch of a material's main image. */
43
+ /**
44
+ * One palette swatch of a material's main image — colour DERIVED FROM PRODUCT
45
+ * IMAGERY, so read it as visual likeness, never as a declared colour spec.
46
+ */
43
47
  export interface PaletteSwatch {
44
48
  hex: string;
45
49
  proportion: number;
46
50
  /** Stored human colour name for the swatch, when one was extracted. */
47
51
  name?: string | null;
52
+ /**
53
+ * `hex` in OKLab, derived server-side so you can sort or measure swatches
54
+ * without converting them yourself. Additive, so optional: the colour lane and
55
+ * the material record carry it, other surfaces may not, and absent means "not
56
+ * derived here" — never "achromatic".
57
+ */
58
+ oklab?: OklabTriple;
48
59
  }
49
60
  /** Loose colour metadata for a search result's main image; every field is independently optional. */
50
61
  export interface ColourMeta {
@@ -109,6 +120,30 @@ export interface SearchResultItem {
109
120
  product: string;
110
121
  brandSlug: string | null;
111
122
  productId: ProductId | null;
123
+ /**
124
+ * The variant's Material Bank SKU — the same `materialBankId` the material
125
+ * record carries, so a match card can render straight from a result instead of
126
+ * paying a per-result lookup. Populated on the MATCH lanes (`match()` and
127
+ * `matchStream()` frames).
128
+ *
129
+ * ABSENT means no MB identity is known for the variant — never that the SKU
130
+ * cannot be ordered. Presence is the orderability tell; absence is silence.
131
+ */
132
+ materialBankId?: string;
133
+ /**
134
+ * What kind of material this is — the variant's primary product type, as a
135
+ * display name (`"Wall Tile"`, `"Modular Carpet"`, `"Interior Paint"`).
136
+ *
137
+ * This is the field that lets you tell a wall tile from a bar stool without
138
+ * parsing `product`. Before it existed, callers grading Match results had to
139
+ * regex the product's NAME, and one such grader scored pendant lights and
140
+ * electrical sockets as correct answers for a wall — because they are neither
141
+ * a stool nor a chair.
142
+ *
143
+ * ABSENT means no primary type is on record for the variant. Unknown, never
144
+ * "uncategorised" and never "not a material".
145
+ */
146
+ productType?: string;
112
147
  color: string | null;
113
148
  variantName: string | null;
114
149
  imageUrl: string | null;
@@ -117,6 +152,35 @@ export interface SearchResultItem {
117
152
  colourMeta?: ColourMeta;
118
153
  /** Fused score — range [0, sum of active weights]; see `maxScore`. */
119
154
  score: number;
155
+ /**
156
+ * Match only. How close this material is to the query, as an ABSOLUTE
157
+ * image-space similarity in [0,1] adjusted for colour — the same unit as
158
+ * `qualityFloor` and `comparison.bestRejected`, so all three sit on one axis.
159
+ *
160
+ * Never normalised against the page, so the same material reads the same in
161
+ * two different searches. It is the TRUE value and MAY reach 1 — rendering it
162
+ * so that no card claims "100%" is yours to do (round toward zero, or show
163
+ * "99%+"); `Math.round` prints 100% from 0.995 up. Capping it server-side
164
+ * would put it on a different ceiling from `comparison.bestRejected`, which
165
+ * publishes the same unit uncapped, and the two are meant to be comparable.
166
+ * `score` is the ranking value and lives on a different scale.
167
+ */
168
+ confidence?: number;
169
+ /**
170
+ * `confidence` as a word — `strong` >= 0.85, `good` >= 0.72, `fair` >= 0.55
171
+ * (the default quality floor), `weak` below it and only reachable under
172
+ * `includeBelowFloor`. Present exactly when `confidence` is.
173
+ */
174
+ confidenceBand?: "strong" | "good" | "fair" | "weak";
175
+ /**
176
+ * Match only. Proportion-weighted OKLab distance from the query crop's palette
177
+ * to this material's. `null` means the material carries no stored palette —
178
+ * UNKNOWN colour, never "no colour" and never "distant". Absent when the
179
+ * colour stage did not run.
180
+ */
181
+ colourDistance?: number | null;
182
+ /** `colourDistance` as a word, or null when unmeasurable. */
183
+ colourBand?: string | null;
120
184
  signals?: SearchResultSignals;
121
185
  retired?: boolean;
122
186
  /**
@@ -223,69 +287,6 @@ export interface SearchCardResponse extends Omit<SearchResponse, "view" | "resul
223
287
  view: OpenEnum<"card">;
224
288
  results: SearchCardItem[];
225
289
  }
226
- /** An inferred product-type category from the query image, with its boost. */
227
- export interface MatchCategoryInference {
228
- productTypeId: string;
229
- displayName: string;
230
- boost: number;
231
- }
232
- /**
233
- * The Material Match response (non-streaming form).
234
- *
235
- * `poolSize` is the candidate pool the floor was applied to, NOT a corpus total
236
- * and NOT "how many materials look like this". An empty `results` with
237
- * `droppedBelowFloor > 0` means "candidates existed and none was good enough" —
238
- * a different answer from `poolSize: 0`, which means the image found nothing to
239
- * search against at all.
240
- */
241
- export interface MatchResponse {
242
- /** THIS answer, capped by `limit`. Not a total. */
243
- results: SearchResultItem[];
244
- inferredCategories: MatchCategoryInference[];
245
- qualityFloor: number;
246
- poolSize: number;
247
- droppedBelowFloor: number;
248
- droppedAsDuplicateProduct: number;
249
- note?: string;
250
- }
251
- /** Coarse group + fine type of a colour match's primary product type. */
252
- export interface MatchCategory {
253
- group: string | null;
254
- type: string | null;
255
- }
256
- /**
257
- * One colour-palette match.
258
- *
259
- * `hex`, `distance` and `closeness` are OKLab geometry over colour EXTRACTED
260
- * FROM PRODUCT IMAGERY, not declared by the manufacturer. Report it as visual
261
- * likeness ("reads close to this colour"), never as a stated colour
262
- * specification.
263
- */
264
- export interface ByColourMatch {
265
- /** Opaque — round-trip verbatim. */
266
- variantId: VariantId;
267
- brand: string;
268
- product: string;
269
- color: string | null;
270
- variantName: string | null;
271
- imageUrl: string | null;
272
- hex: string | null;
273
- distance: number;
274
- closeness: number;
275
- category: MatchCategory | null;
276
- oklab: OklabTriple | null;
277
- }
278
- /** The `search/colours` response. */
279
- export interface ByColourResponse {
280
- query: {
281
- colours: string[];
282
- categories?: string[];
283
- oklab?: OklabTriple;
284
- };
285
- /** `matches.length` — THIS PAGE. Not a corpus total; this surface states none. */
286
- count: number;
287
- matches: ByColourMatch[];
288
- }
289
290
  /** One item from the pure colour-scalar browse. */
290
291
  export interface BrowseResultItem {
291
292
  /** Opaque — round-trip verbatim. */
@@ -303,6 +304,17 @@ export interface BrowseResultItem {
303
304
  vibrancy: number | null;
304
305
  };
305
306
  retired?: boolean;
307
+ /**
308
+ * The row's own image palette — the FULL stored palette of the SAME image
309
+ * `imageUrl` serves, uncapped and in stored order, each swatch with `oklab`
310
+ * (see {@link ByColourSwatch}), so a card can render, sort or group colours
311
+ * without a second call. A one-swatch palette means the extractor found one
312
+ * colour (solid-colour materials are genuinely monochrome), not that the list
313
+ * was trimmed. ABSENT when no palette is on record for that image: absent
314
+ * means unknown, NOT that the material has no colours. These swatches come
315
+ * from product imagery, not from the manufacturer.
316
+ */
317
+ palette?: ByColourSwatch[];
306
318
  }
307
319
  /**
308
320
  * A single page of the colour browse — the SDK's one genuinely cursor-paginated
@@ -508,6 +520,7 @@ export interface ResolveAttributeResponse {
508
520
  candidates: AttributeCandidate[];
509
521
  }
510
522
  export type * from "./catalog-discovery-types.js";
523
+ export type * from "./resolve-types.js";
511
524
  export type * from "./suggest-types.js";
512
525
  export type * from "./by-spec-types.js";
513
526
  export interface WhoamiResponse {
@@ -626,20 +639,6 @@ export interface SearchGetParams {
626
639
  /** Result projection — `"full"` (default) or the lean `"card"` grid form. */
627
640
  view?: OpenEnum<"full" | "card">;
628
641
  }
629
- /** Params for `search/colours`. */
630
- export interface ByColourParams {
631
- /** A single hex (with or without `#`). */
632
- hex?: string;
633
- /** A palette — repeated as one `colour` query entry each. */
634
- colour?: string | string[];
635
- /** OKLab triple (all-or-none). */
636
- l?: number;
637
- a?: number;
638
- b?: number;
639
- /** HARD category filter — display names or taxonomy ids (repeatable). */
640
- category?: string | string[];
641
- limit?: number;
642
- }
643
642
  /** Params for the pure colour `search/browse` surface. */
644
643
  export interface BrowseParams {
645
644
  sort: OpenEnum<"light" | "dark" | "vivid" | "muted" | "colourful">;
@@ -650,34 +649,14 @@ export interface BrowseParams {
650
649
  cursor?: Cursor;
651
650
  /** Page size. */
652
651
  limit?: number;
653
- /** Comma-separated product-type ids to restrict the browse to. */
654
- types?: string;
655
- }
656
- /** The Material Match request body — exactly one of `imageBase64`, `imageUrl`, `vector`. */
657
- export interface MatchInput {
658
- imageBase64?: string;
659
- imageUrl?: string;
660
- vector?: string;
661
- limit?: number;
662
- qualityFloor?: number;
663
- categoryTopK?: number;
664
- categoryMinBoost?: number;
665
- detectionLabel?: string;
666
- materialFunction?: string;
667
- detectionCategory?: string;
668
652
  /**
669
- * Opt-in: also return quality-floor rejects, flagged `belowFloor: true` and
670
- * appended after the kept results. Absent/false = default behaviour;
671
- * `droppedBelowFloor` is unchanged.
653
+ * Comma-separated taxonomy keys to restrict the browse to, at EITHER tier of
654
+ * `/api/v1/taxonomy/categories`. A GROUP key (`tile_surfaces`) expands to
655
+ * every product type under it, a TYPE key (`ceramic_porcelain_tile`) stands
656
+ * for itself, and a mixed list is their deduplicated union. A key in neither
657
+ * tier is a 400 listing the valid keys — never an empty page.
672
658
  */
673
- includeBelowFloor?: boolean;
674
- requestedType?: OpenEnum<"swatch" | "installation" | "flatplan">;
675
- /** Hard spec gates forwarded verbatim to the inner search — term match or presence, each with an optional trust bar. */
676
- filters?: SearchFilter[];
677
- /** Soft criteria (rank-never-exclude). A miss demotes within the page but never removes a hit. */
678
- preferences?: SearchFilter[];
679
- /** Opt in to seed-relative verdict cells; inert unless the inner search carries a variant seed. */
680
- verdicts?: boolean;
659
+ types?: string;
681
660
  }
682
661
  /** Options for a batch material read. */
683
662
  export interface MaterialsBatchOptions {