@instruments/materialgraph 0.38.0 → 0.40.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.
@@ -17,7 +17,10 @@ import { resolveOperation } from "./operations.js";
17
17
  *
18
18
  * @param variantId The PRODUCT variant. Opaque — pass back exactly what you were given.
19
19
  */
20
- export const declaredFinishes = async (transport, variantId, options, signal) => await transport.requestJson(resolveOperation("list_associated_finishes"), { query: { variantId, ...options }, signal });
20
+ export const declaredFinishes = async (transport, variantId, options, signal) => await transport.requestJson(resolveOperation("list_associated_finishes"), {
21
+ query: { variantId, ...options },
22
+ signal,
23
+ });
21
24
  /**
22
25
  * THE QUESTION: "what furniture takes THIS fabric?" — the products the
23
26
  * BRAND declares a finish variant is available on (GET
@@ -32,6 +35,12 @@ export const declaredFinishes = async (transport, variantId, options, signal) =>
32
35
  *
33
36
  * @param variantId The FINISH variant. Opaque — pass back exactly what you were given.
34
37
  */
35
- export const declaredProducts = async (transport, variantId, options, signal) => await transport.requestJson(resolveOperation("list_associated_products"), { query: { variantId, ...options }, signal });
38
+ export const declaredProducts = async (transport, variantId, options, signal) => await transport.requestJson(resolveOperation("list_associated_products"), {
39
+ query: { variantId, ...options },
40
+ signal,
41
+ });
36
42
  /** Read brand-declared applicability without collapsing Product/Variant default attribute level. */
37
- export const applicability = async (transport, options, signal) => await transport.requestJson(resolveOperation("get_applicability"), { query: options, signal });
43
+ export const applicability = async (transport, options, signal) => await transport.requestJson(resolveOperation("get_applicability"), {
44
+ query: options,
45
+ signal,
46
+ });
@@ -50,7 +50,7 @@ export type BySpecVerdictCell = {
50
50
  /** Trust in `value`, 0–1. Null = unrecorded, which is weaker than a low score. */
51
51
  confidence: number | null;
52
52
  /** Where `value` came from (`tested` > `declared` > `catalog_feed` > `derived`). */
53
- valueOrigin: string | null;
53
+ valueSource: string | null;
54
54
  /**
55
55
  * Why an `unverified` cell missed the bar; null otherwise.
56
56
  * - `below_confidence` — the value is real but scored under the floor.
@@ -1,5 +1,5 @@
1
1
  /** A verified product location in a specific immutable brand publication. */
2
- export interface CatalogueReference {
2
+ export type CatalogueReference = {
3
3
  anchor: {
4
4
  coordinateSpace: "normalized_0_1000";
5
5
  /** Page carrying the printed code; defaults to page.documentPage. Do not draw it on another page. */
@@ -56,7 +56,7 @@ export interface CatalogueReference {
56
56
  };
57
57
  status: "verified";
58
58
  isPrimary: boolean;
59
- }
59
+ };
60
60
  /**
61
61
  * Why a known variant has no verified reference. None of these says the
62
62
  * product is missing from the printed book; nobody records how many of a
@@ -114,7 +114,7 @@ export type CatalogueReferenceResult = {
114
114
  variantId: string;
115
115
  status: "upstream_failure";
116
116
  };
117
- export interface CatalogueReferenceBatchResponse {
117
+ export type CatalogueReferenceBatchResponse = {
118
118
  results: CatalogueReferenceResult[];
119
119
  coverage: {
120
120
  requested: number;
@@ -124,25 +124,23 @@ export interface CatalogueReferenceBatchResponse {
124
124
  unavailable: number;
125
125
  upstreamFailure: number;
126
126
  };
127
- }
127
+ };
128
128
  /** Bounds for the flat catalogue-channel collection shelf. */
129
- export interface CatalogueCollectionsOptions {
129
+ export type CatalogueCollectionsOptions = {
130
130
  limit?: number;
131
131
  offset?: number;
132
132
  sampleLimit?: number;
133
- }
133
+ };
134
134
  /** One representative product image carried by a catalogue collection. */
135
- export interface CatalogueCollectionSampleProduct {
136
- assetId: string;
135
+ export type CatalogueCollectionSampleProduct = {
137
136
  imageUrl: string;
138
- sourceImageUrl: string;
139
137
  productId: string;
140
138
  productName: string;
141
139
  variantId: string;
142
140
  variantName: string | null;
143
- }
141
+ };
144
142
  /** A product collection whose variants have governed catalogue membership. */
145
- export interface CatalogueChannelCollection {
143
+ export type CatalogueChannelCollection = {
146
144
  id: string;
147
145
  slug: string;
148
146
  name: string;
@@ -150,9 +148,9 @@ export interface CatalogueChannelCollection {
150
148
  productCount: number;
151
149
  variantCount: number;
152
150
  sampleProducts: CatalogueCollectionSampleProduct[];
153
- }
151
+ };
154
152
  /** The flat, paginated catalogue-channel collection read model. */
155
- export interface CatalogueChannelCollectionsResponse {
153
+ export type CatalogueChannelCollectionsResponse = {
156
154
  collections: CatalogueChannelCollection[];
157
155
  channel: {
158
156
  token: "catalogue";
@@ -168,14 +166,14 @@ export interface CatalogueChannelCollectionsResponse {
168
166
  offset: number;
169
167
  hasMore: boolean;
170
168
  };
171
- }
172
- export interface CatalogueDocumentAccess {
169
+ };
170
+ export type CatalogueDocumentAccess = {
173
171
  documentId: string;
174
172
  contentType: "application/pdf";
175
173
  readerUrl: string;
176
174
  originalUrl: string;
177
175
  byteRanges: "supported" | "unknown";
178
- }
176
+ };
179
177
  export type CatalogueDocumentAccessResult = {
180
178
  documentId: string;
181
179
  status: "available";
@@ -193,7 +191,7 @@ export type CatalogueDocumentAccessResult = {
193
191
  documentId: string;
194
192
  status: "upstream_failure";
195
193
  };
196
- export interface CatalogDocument {
194
+ export type CatalogDocument = {
197
195
  availability: "available";
198
196
  brand: {
199
197
  id: string;
@@ -211,13 +209,13 @@ export interface CatalogDocument {
211
209
  productStatus: "partial" | "not_processed";
212
210
  receipt: CatalogAvailableReceipt;
213
211
  title: string;
214
- }
215
- export interface CatalogEdition {
212
+ };
213
+ export type CatalogEdition = {
216
214
  id: string;
217
215
  label: string;
218
216
  title: string;
219
- }
220
- export interface CatalogAvailableReceipt {
217
+ };
218
+ export type CatalogAvailableReceipt = {
221
219
  extraction: {
222
220
  references: number;
223
221
  status: "partial" | "not_processed";
@@ -240,17 +238,17 @@ export interface CatalogAvailableReceipt {
240
238
  algorithm: "sha256";
241
239
  value: string;
242
240
  };
243
- valueOrigin: "brand_supplied";
241
+ valueSource: "brand_supplied";
244
242
  retainedUrl: string;
245
243
  status: "retained";
246
244
  };
247
- }
248
- export interface CatalogFailureReceipt {
245
+ };
246
+ export type CatalogFailureReceipt = {
249
247
  count: number;
250
248
  reason: string;
251
249
  stage: "source" | "render" | "extraction";
252
- }
253
- export interface UnavailableCatalogDocument {
250
+ };
251
+ export type UnavailableCatalogDocument = {
254
252
  availability: "source_unavailable";
255
253
  brand: {
256
254
  id: string;
@@ -282,13 +280,13 @@ export interface UnavailableCatalogDocument {
282
280
  };
283
281
  };
284
282
  title: string;
285
- }
283
+ };
286
284
  export type CatalogPublication = CatalogDocument | UnavailableCatalogDocument;
287
- export interface CatalogDocumentList {
285
+ export type CatalogDocumentList = {
288
286
  catalogs: CatalogPublication[];
289
287
  source: "verified-source-material-registry";
290
- }
291
- export interface CatalogProductReferenceList {
288
+ };
289
+ export type CatalogProductReferenceList = {
292
290
  catalog: CatalogDocument;
293
291
  coverage: {
294
292
  documentPages: {
@@ -313,22 +311,23 @@ export interface CatalogProductReferenceList {
313
311
  references: CatalogueReference[];
314
312
  source: "verified-source-material-registry";
315
313
  status: "available";
316
- }
317
- export interface CatalogProductReferenceUnavailable {
314
+ };
315
+ export type CatalogProductReferenceUnavailable = {
318
316
  catalog: UnavailableCatalogDocument;
319
317
  reason: "retained_source_not_found";
320
318
  status: "source_unavailable";
321
- }
319
+ };
322
320
  export type CatalogProductReferenceReadResult = CatalogProductReferenceList | CatalogProductReferenceUnavailable;
323
- export interface CataloguePageRequiredArtifact {
321
+ export type CataloguePageRequiredArtifact = {
324
322
  mediaType: "image/webp";
325
323
  version: "catalogue-page-webp-v1";
326
- }
327
- export interface CataloguePageAvailableArtifact extends Omit<CataloguePageRequiredArtifact, "version"> {
324
+ };
325
+ export type CataloguePageAvailableArtifact = {
328
326
  bucket: "materialgraph";
329
327
  key: string;
330
328
  url: string;
331
- }
329
+ mediaType: CataloguePageRequiredArtifact["mediaType"];
330
+ };
332
331
  export type CataloguePage = {
333
332
  catalogId: string;
334
333
  documentId: string;
@@ -17,10 +17,10 @@
17
17
  */
18
18
  import type { CategoriesGetResult, CategoriesListInput, CategoriesListResult } from "./category-types.js";
19
19
  import type { QueryParams } from "./http-transport.js";
20
- import type { SdkOperationId } from "./operations.js";
20
+ import type { JsonOperationId, OperationResponses } from "./operation-responses.js";
21
21
  /** The client's own request primitive, handed down so this file owns no transport. */
22
22
  export type CategoriesTransport = {
23
- get<T>(operationId: SdkOperationId, query?: QueryParams, signal?: AbortSignal, pathParams?: Readonly<Record<string, string>>): Promise<T>;
23
+ get<K extends JsonOperationId>(operationId: K, query?: QueryParams, signal?: AbortSignal, pathParams?: Readonly<Record<string, string>>): Promise<OperationResponses[K]>;
24
24
  };
25
25
  export declare class CategoriesClient {
26
26
  #private;
@@ -0,0 +1,3 @@
1
+ import type { JsonSchema } from "./response-schemas.js";
2
+ /** Adapt a fresh producer schema to the SDK's existing open string enums. */
3
+ export declare const openClientResponseEnums: (schema: JsonSchema) => void;
@@ -0,0 +1,79 @@
1
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ // Only the fields declared as OpenEnum in the SDK are open here. Union
3
+ // discriminants stay closed so their associated object shapes remain true.
4
+ const openStrings = new Set([
5
+ "CategoryColorProfilesResponse.profiles.level",
6
+ "FindVariantsByColorResponse.searchCatalogueSubset.scope",
7
+ "GetCategoryResponse.kind",
8
+ "ListAttributesResponse.items.match.method",
9
+ "ListCategoriesResponse.items.match.method",
10
+ "ListEnumValuesResponse.items.match.method",
11
+ "MaterialBlockProjection.blocks.status",
12
+ "MaterialBlockProjection.blocks.fields.status",
13
+ "MaterialTile.availability",
14
+ "MaterialTile.imageSource",
15
+ "PairingItem.availability",
16
+ "PairingItem.imageSource",
17
+ "PairingItem.demand.momentum.state",
18
+ "PairingItem.ranking.basis",
19
+ "PairingOrdering.anchorColorBasis",
20
+ "PairingOrdering.applied",
21
+ "PairingOrdering.colorSpace",
22
+ "PairingOrdering.direction",
23
+ "PairingOrdering.missingValues",
24
+ "PairingOrdering.requested",
25
+ "PairingOrdering.tieBreak",
26
+ "ProductDocumentPage.coverage.productUnion.state",
27
+ "ProductDocumentPage.coverage.requestedVariant.state",
28
+ "ProductDocumentPage.coverage.sources.state",
29
+ "ProductDocumentPage.documents.declarations.status",
30
+ "ProductDocumentPage.documents.documentTypes",
31
+ "ProductDocumentPage.documents.status",
32
+ "PublicRun.status",
33
+ "ResolveColorResponse.match.libraryType",
34
+ "ResolveColorResponse.match.variant.reason",
35
+ "SchemaValueDictionariesResponse.items.lifecycle.status",
36
+ "SchemaValueDictionariesResponse.items.terms.lifecycle.status",
37
+ "SchemaValueDictionariesResponse.items.terms.externalMappings.mappingType",
38
+ "SchemaValueDictionariesResponse.items.terms.externalMappings.matchStrength",
39
+ "SchemaValueDictionariesResponse.items.terms.externalMappings.target",
40
+ "SourceMaterialBatchReceipt.sources.role",
41
+ "VariantAssetPage.documentCoverage",
42
+ "VariantAssetPage.items.availability",
43
+ "VariantAssetPage.items.documentTypes",
44
+ "VariantAssetPage.items.imageRole",
45
+ "VariantAssetPage.items.kind",
46
+ "VariantAssetPage.items.presentation",
47
+ ]);
48
+ /** Adapt a fresh producer schema to the SDK's existing open string enums. */
49
+ export const openClientResponseEnums = (schema) => {
50
+ const visit = (node, path) => {
51
+ if (!isRecord(node)) {
52
+ return;
53
+ }
54
+ if (node.type === "string" && openStrings.has(path)) {
55
+ delete node.enum;
56
+ delete node.const;
57
+ }
58
+ if (isRecord(node.$defs)) {
59
+ for (const [name, definition] of Object.entries(node.$defs)) {
60
+ visit(definition, name);
61
+ }
62
+ }
63
+ if (isRecord(node.properties)) {
64
+ for (const [name, property] of Object.entries(node.properties)) {
65
+ visit(property, `${path}.${name}`);
66
+ }
67
+ }
68
+ visit(node.items, path);
69
+ for (const keyword of ["anyOf", "allOf", "oneOf", "prefixItems"]) {
70
+ const children = node[keyword];
71
+ if (Array.isArray(children)) {
72
+ for (const child of children) {
73
+ visit(child, path);
74
+ }
75
+ }
76
+ }
77
+ };
78
+ visit(schema, "");
79
+ };
package/client.d.ts CHANGED
@@ -19,7 +19,8 @@ import type { SchemaResource, SchemaResourceResponse } from "./schema-resource-t
19
19
  import type { SearchVariantsInput, SearchVariantsOutput } from "./search-variants-types.js";
20
20
  import type { ExtractPaletteInput, ExtractPaletteResponse, ListAttributesOptions, ListAttributesResult, PairingsOptions, PairingsResponse, ResolveColorOptions, ResolveColorResponse, WhoamiResponse } from "./types.js";
21
21
  import { WorkflowsClient } from "./workflows-client.js";
22
- export * from "./client-config.js";
22
+ export { DEFAULT_BASE_URL } from "./client-config.js";
23
+ export type { MaterialGraphClientOptions } from "./client-config.js";
23
24
  /**
24
25
  * The MaterialGraph client — every lens on the graph, on one object.
25
26
  *
@@ -83,7 +84,7 @@ export declare class MaterialGraphClient extends CatalogueClient {
83
84
  * Color is usually derived from product imagery, which is visual likeness,
84
85
  * not a color specification. A brand's declared color, such as a paint's
85
86
  * published hex, wins where MaterialGraph holds one, and each value's
86
- * value origin says which.
87
+ * Value Source says which.
87
88
  */
88
89
  readonly colors: ColorsClient;
89
90
  /**
@@ -96,6 +97,10 @@ export declare class MaterialGraphClient extends CatalogueClient {
96
97
  readonly workflows: WorkflowsClient;
97
98
  constructor(options: MaterialGraphClientOptions);
98
99
  get lastRateLimit(): RateLimit | null;
100
+ /** Current catalogue generation; authenticated, no SQL and no rate-limit charge. */
101
+ getCatalogueGeneration(signal?: AbortSignal): Promise<{
102
+ generation: number;
103
+ }>;
99
104
  /**
100
105
  * THE QUESTION: "tell me about THIS variant" (GET
101
106
  * `/api/v1/variants/{variantId}`, `get_variant`).
@@ -191,7 +196,7 @@ export declare class MaterialGraphClient extends CatalogueClient {
191
196
  searchVariants(input: SearchVariantsInput, signal?: AbortSignal): Promise<SearchVariantsOutput>;
192
197
  /**
193
198
  * THE QUESTION: "which variants hold, resemble, or can be ranked by, this
194
- * color?" (POST `/api/v1/variants/by-color`, MG-2217/MG-2230/MG-2247).
199
+ * color?" (GET `/api/v1/variants/by-color`, MG-2217/MG-2230/MG-2247).
195
200
  * One required `mode`: `contains` (a swatch for every required `colors[]`
196
201
  * condition), `palette` (whole-palette closeness to `colors[]`, an
197
202
  * image's extracted palette, or `targetVariantId`'s own palette) or
@@ -199,7 +204,7 @@ export declare class MaterialGraphClient extends CatalogueClient {
199
204
  */
200
205
  findVariantsByColor(input: FindVariantsByColorInput, signal?: AbortSignal): Promise<FindVariantsByColorOutput>;
201
206
  /**
202
- * THE QUESTION: "what did designers sample together with these?" (POST
207
+ * THE QUESTION: "what did designers sample together with these?" (GET
203
208
  * `/api/v1/variants/sampled-together`, MG-2332). Give `variantIds` and/or
204
209
  * `materials` (free-text names, one variant each, at most twelve together);
205
210
  * the materials come back other brands first, then the most projects, each
package/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as assoc from "./associations-client.js";
1
+ import { applicability, declaredFinishes, declaredProducts, } from "./associations-client.js";
2
2
  import { listBrands } from "./brands-client.js";
3
3
  import { CatalogueClient } from "./catalogue-client.js";
4
4
  import { CategoriesClient } from "./categories-client.js";
@@ -14,7 +14,7 @@ import { uploadSourceMaterial } from "./runs.js";
14
14
  import { SCHEMA_RESOURCE_OPERATION_IDS } from "./schema-resource-types.js";
15
15
  import { WorkflowsClient } from "./workflows-client.js";
16
16
  // Re-exported whole so `index.ts` and every existing importer keep their path.
17
- export * from "./client-config.js";
17
+ export { DEFAULT_BASE_URL } from "./client-config.js";
18
18
  /**
19
19
  * The MaterialGraph client — every lens on the graph, on one object.
20
20
  *
@@ -77,7 +77,7 @@ export class MaterialGraphClient extends CatalogueClient {
77
77
  * Color is usually derived from product imagery, which is visual likeness,
78
78
  * not a color specification. A brand's declared color, such as a paint's
79
79
  * published hex, wins where MaterialGraph holds one, and each value's
80
- * value origin says which.
80
+ * Value Source says which.
81
81
  */
82
82
  colors;
83
83
  /**
@@ -126,6 +126,10 @@ export class MaterialGraphClient extends CatalogueClient {
126
126
  signal,
127
127
  });
128
128
  }
129
+ /** Current catalogue generation; authenticated, no SQL and no rate-limit charge. */
130
+ async getCatalogueGeneration(signal) {
131
+ return await this.#get("get_catalogue_generation", undefined, signal);
132
+ }
129
133
  // ── Materials ──────────────────────────────────────────────────────────────
130
134
  /**
131
135
  * THE QUESTION: "tell me about THIS variant" (GET
@@ -146,10 +150,12 @@ export class MaterialGraphClient extends CatalogueClient {
146
150
  */
147
151
  async getVariant(variantId, options, signal) {
148
152
  return await this.#get("get_variant", {
149
- attributes: options?.attributes?.length
153
+ attributes: options?.attributes !== undefined && options.attributes.length > 0
150
154
  ? options.attributes.join(",")
151
155
  : undefined,
152
- blocks: options?.blocks?.length ? options.blocks.join(",") : undefined,
156
+ blocks: options?.blocks !== undefined && options.blocks.length > 0
157
+ ? options.blocks.join(",")
158
+ : undefined,
153
159
  }, signal, { variantId });
154
160
  }
155
161
  async materialBlocks(variantId, signal) {
@@ -168,7 +174,8 @@ export class MaterialGraphClient extends CatalogueClient {
168
174
  * what the corpus actually holds.
169
175
  */
170
176
  async schema(resource, signal) {
171
- return await this.#get(SCHEMA_RESOURCE_OPERATION_IDS[resource], undefined, signal);
177
+ const response = await this.#get(SCHEMA_RESOURCE_OPERATION_IDS[resource], undefined, signal);
178
+ return { ...response, kind: resource };
172
179
  }
173
180
  /**
174
181
  * THE QUESTION: "what goes WITH this material?" — the companionship rail, hung
@@ -190,14 +197,14 @@ export class MaterialGraphClient extends CatalogueClient {
190
197
  }
191
198
  /** "What finishes does the brand declare for THIS product?" - {@link declaredFinishes}. */
192
199
  async associatedFinishes(variantId, options, signal) {
193
- return await assoc.declaredFinishes(this.#transport, variantId, options, signal);
200
+ return await declaredFinishes(this.#transport, variantId, options, signal);
194
201
  }
195
202
  /** "What products does the brand declare THIS finish for?" - {@link declaredProducts}. */
196
203
  async associatedProducts(variantId, options, signal) {
197
- return await assoc.declaredProducts(this.#transport, variantId, options, signal);
204
+ return await declaredProducts(this.#transport, variantId, options, signal);
198
205
  }
199
206
  async applicability(options, signal) {
200
- return await assoc.applicability(this.#transport, options, signal);
207
+ return await applicability(this.#transport, options, signal);
201
208
  }
202
209
  /**
203
210
  * THE QUESTION: "eligible variants matching these hard filters, ranked and
@@ -221,24 +228,30 @@ export class MaterialGraphClient extends CatalogueClient {
221
228
  */
222
229
  async listVariants(input, signal) {
223
230
  return await this.#get("list_variants", {
224
- attributes: input.attributes?.length
231
+ attributes: input.attributes !== undefined && input.attributes.length > 0
225
232
  ? input.attributes.join(",")
226
233
  : undefined,
227
234
  collapse: input.collapse,
228
- colors: input.colors?.length ? JSON.stringify(input.colors) : undefined,
235
+ colors: input.colors !== undefined && input.colors.length > 0
236
+ ? JSON.stringify(input.colors)
237
+ : undefined,
229
238
  searchCatalogueSubset: input.searchCatalogueSubset,
230
239
  cursor: input.cursor,
231
240
  filters: input.filters ? JSON.stringify(input.filters) : undefined,
232
- include: input.include?.length ? input.include.join(",") : undefined,
241
+ include: input.include !== undefined && input.include.length > 0
242
+ ? input.include.join(",")
243
+ : undefined,
233
244
  limit: input.limit,
234
245
  name: input.name,
235
246
  optionalFilters: input.optionalFilters
236
247
  ? JSON.stringify(input.optionalFilters)
237
248
  : undefined,
238
- orderBy: input.orderBy?.length
249
+ orderBy: input.orderBy !== undefined && input.orderBy.length > 0
239
250
  ? JSON.stringify(input.orderBy)
240
251
  : undefined,
241
- words: input.words?.length ? input.words.join(",") : undefined,
252
+ words: input.words !== undefined && input.words.length > 0
253
+ ? input.words.join(",")
254
+ : undefined,
242
255
  }, signal);
243
256
  }
244
257
  /**
@@ -272,17 +285,32 @@ export class MaterialGraphClient extends CatalogueClient {
272
285
  }
273
286
  /**
274
287
  * THE QUESTION: "which variants hold, resemble, or can be ranked by, this
275
- * color?" (POST `/api/v1/variants/by-color`, MG-2217/MG-2230/MG-2247).
288
+ * color?" (GET `/api/v1/variants/by-color`, MG-2217/MG-2230/MG-2247).
276
289
  * One required `mode`: `contains` (a swatch for every required `colors[]`
277
290
  * condition), `palette` (whole-palette closeness to `colors[]`, an
278
291
  * image's extracted palette, or `targetVariantId`'s own palette) or
279
292
  * `rerank` (reorder a held `variantIds` set by color closeness).
280
293
  */
281
294
  async findVariantsByColor(input, signal) {
282
- return await this.#post("find_variants_by_color", input, signal);
295
+ const { colors, image, ...rest } = input;
296
+ const query = { ...rest };
297
+ for (const [index, entry] of (colors ?? []).entries()) {
298
+ query[`color${index}`] = entry.color;
299
+ query[`color${index}.share`] = entry.share;
300
+ query[`color${index}.shareMax`] = entry.shareMax;
301
+ query[`color${index}.dominant`] = entry.dominant;
302
+ query[`color${index}.role`] = entry.role;
303
+ }
304
+ if (image) {
305
+ query.imageAssetId = image.assetId;
306
+ query.imageSourceId = image.sourceId;
307
+ query.imageUrl = image.url;
308
+ query.imageVariantId = image.variantId;
309
+ }
310
+ return await this.#get("find_variants_by_color", query, signal);
283
311
  }
284
312
  /**
285
- * THE QUESTION: "what did designers sample together with these?" (POST
313
+ * THE QUESTION: "what did designers sample together with these?" (GET
286
314
  * `/api/v1/variants/sampled-together`, MG-2332). Give `variantIds` and/or
287
315
  * `materials` (free-text names, one variant each, at most twelve together);
288
316
  * the materials come back other brands first, then the most projects, each
@@ -292,7 +320,7 @@ export class MaterialGraphClient extends CatalogueClient {
292
320
  * before presenting: a scheme of four that resolved two is a different claim.
293
321
  */
294
322
  async findSampledTogether(input, signal) {
295
- return await this.#post("find_sampled_together", input, signal);
323
+ return await this.#get("find_sampled_together", { ...input }, signal);
296
324
  }
297
325
  /**
298
326
  * THE QUESTION: "what looks or feels like this?" (POST
@@ -1,6 +1,7 @@
1
1
  import type { CategoryColorProfilesResponse, ColorProfilesParams } from "./color-profiles-types.js";
2
2
  import type { QueryParams } from "./http-transport.js";
3
- import type { SdkOperationId } from "./operations.js";
3
+ import type { NearestPaintsParams, NearestPaintsOutput } from "./nearest-paints-types.js";
4
+ import type { JsonOperationId, OperationResponses } from "./operation-responses.js";
4
5
  /**
5
6
  * The color surface, namespaced as `mg.colors` - `profiles`, the colors a
6
7
  * category comes in.
@@ -29,11 +30,13 @@ import type { SdkOperationId } from "./operations.js";
29
30
  */
30
31
  /** The client's own request primitives, handed down so this file owns no transport. */
31
32
  export type ColorsTransport = {
32
- get<T>(operationId: SdkOperationId, query?: QueryParams, signal?: AbortSignal): Promise<T>;
33
+ get<K extends JsonOperationId>(operationId: K, query?: QueryParams, signal?: AbortSignal): Promise<OperationResponses[K]>;
33
34
  };
34
35
  export declare class ColorsClient {
35
36
  #private;
36
37
  constructor(transport: ColorsTransport);
38
+ /** Nearest Samplize catalogue paints for up to twelve hexes, deduplicated in input order. */
39
+ nearestPaints(params: NearestPaintsParams, signal?: AbortSignal): Promise<NearestPaintsOutput>;
37
40
  /**
38
41
  * THE QUESTION: "what colors does this CATEGORY come in?" - the
39
42
  * representative palette of 1 to 8 categories in ONE call (GET
package/colors-client.js CHANGED
@@ -3,6 +3,10 @@ export class ColorsClient {
3
3
  constructor(transport) {
4
4
  this.#transport = transport;
5
5
  }
6
+ /** Nearest Samplize catalogue paints for up to twelve hexes, deduplicated in input order. */
7
+ async nearestPaints(params, signal) {
8
+ return await this.#transport.get("find_nearest_paints", { hex: params.hexes, limit: params.limit }, signal);
9
+ }
6
10
  /**
7
11
  * THE QUESTION: "what colors does this CATEGORY come in?" - the
8
12
  * representative palette of 1 to 8 categories in ONE call (GET
package/errors.d.ts CHANGED
@@ -6,7 +6,7 @@ export type SanitizedIssue = {
6
6
  [key: string]: SanitizedErrorValue;
7
7
  };
8
8
  /** Normalized RFC 7807 body. Unknown response fields are intentionally discarded. */
9
- export interface ProblemDetail {
9
+ export type ProblemDetail = {
10
10
  type: string;
11
11
  title: string;
12
12
  status: number;
@@ -23,7 +23,7 @@ export interface ProblemDetail {
23
23
  * not carry them. See {@link MATERIALGRAPH_PROBLEM_EXTENSIONS}.
24
24
  */
25
25
  readonly [extension: string]: unknown;
26
- }
26
+ };
27
27
  /** Normalized residual `{ error }` body. Unknown response fields are discarded. */
28
28
  export type SimpleErrorBody = {
29
29
  error: string;
@@ -41,7 +41,7 @@ export type MaterialGraphRequestMetadata = {
41
41
  origin: string;
42
42
  path: string;
43
43
  };
44
- export interface MaterialGraphErrorInit {
44
+ export type MaterialGraphErrorInit = {
45
45
  kind: MaterialGraphErrorKind;
46
46
  status?: number;
47
47
  retriable: boolean;
@@ -54,7 +54,7 @@ export interface MaterialGraphErrorInit {
54
54
  path?: string;
55
55
  /** Accepted for compatibility but deliberately never retained on the public error. */
56
56
  cause?: unknown;
57
- }
57
+ };
58
58
  type ResponseErrorInput = {
59
59
  status: number;
60
60
  body: unknown;
package/errors.js CHANGED
@@ -47,12 +47,15 @@ const MAX_EXTENSION_DEPTH = 8;
47
47
  const MAX_EXTENSION_ITEMS = 100;
48
48
  const MAX_EXTENSION_STRING = 1024;
49
49
  const isRecord = (value) => typeof value === "object" && value !== null;
50
- const redact = (text, secrets) => secrets.reduce((sanitized, secret) => {
51
- if (secret.length === 0) {
52
- return sanitized;
50
+ const redact = (text, secrets) => {
51
+ let sanitized = text;
52
+ for (const secret of secrets) {
53
+ if (secret.length > 0) {
54
+ sanitized = sanitized.split(secret).join("[REDACTED]");
55
+ }
53
56
  }
54
- return sanitized.split(secret).join("[REDACTED]");
55
- }, text);
57
+ return sanitized;
58
+ };
56
59
  const safeString = (text, secrets) => redact(text, secrets).slice(0, MAX_EXTENSION_STRING);
57
60
  const secretVariants = (apiKey, queryValues) => [
58
61
  apiKey,
@@ -65,7 +68,7 @@ const requestMetadata = (input) => ({
65
68
  origin: input.origin,
66
69
  path: input.path,
67
70
  });
68
- function sanitizeValue(value, secrets, depth = 0) {
71
+ const sanitizeValue = (value, secrets, depth = 0) => {
69
72
  if (depth >= MAX_EXTENSION_DEPTH) {
70
73
  return undefined;
71
74
  }
@@ -88,8 +91,8 @@ function sanitizeValue(value, secrets, depth = 0) {
88
91
  return undefined;
89
92
  }
90
93
  return sanitizeRecord(value, secrets, depth);
91
- }
92
- function sanitizeRecord(value, secrets, depth) {
94
+ };
95
+ const sanitizeRecord = (value, secrets, depth) => {
93
96
  const entries = Object.entries(value)
94
97
  .slice(0, MAX_EXTENSION_ITEMS)
95
98
  .flatMap(([key, item]) => {
@@ -99,7 +102,7 @@ function sanitizeRecord(value, secrets, depth) {
99
102
  : [[safeString(key, secrets), sanitized]];
100
103
  });
101
104
  return Object.fromEntries(entries);
102
- }
105
+ };
103
106
  const sanitizeIssues = (value, secrets) => {
104
107
  if (!Array.isArray(value)) {
105
108
  return undefined;
@@ -111,7 +114,7 @@ const sanitizeIssues = (value, secrets) => {
111
114
  };
112
115
  const sanitizeSuggestions = (value, secrets) => {
113
116
  if (!Array.isArray(value) ||
114
- value.some((suggestion) => typeof suggestion !== "string")) {
117
+ !value.every((suggestion) => typeof suggestion === "string")) {
115
118
  return undefined;
116
119
  }
117
120
  return value