@instruments/materialgraph 3.1.0 → 3.3.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.
Files changed (65) hide show
  1. package/association-types.d.ts +20 -20
  2. package/baseline-types.d.ts +112 -0
  3. package/baseline-types.js +1 -0
  4. package/brand-types.d.ts +6 -6
  5. package/by-spec-types.d.ts +6 -6
  6. package/catalog-discovery-types.d.ts +4 -4
  7. package/categories-client.d.ts +3 -3
  8. package/category-types.d.ts +18 -18
  9. package/client-config.d.ts +2 -2
  10. package/client-response-schema.d.ts +2 -2
  11. package/client-response-schema.js +5 -5
  12. package/client-vocabulary.d.ts +2 -0
  13. package/client-vocabulary.js +9 -1
  14. package/client.d.ts +32 -3
  15. package/client.js +54 -14
  16. package/color-profiles-types.d.ts +8 -8
  17. package/color-wire-types.d.ts +6 -6
  18. package/colors-client.d.ts +3 -3
  19. package/discovery-context-types.d.ts +8 -8
  20. package/errors.d.ts +12 -13
  21. package/errors.js +84 -74
  22. package/find-sampled-together-types.d.ts +6 -6
  23. package/find-variants-by-color-types.d.ts +31 -30
  24. package/find-variants-by-vector-types.d.ts +13 -13
  25. package/get-variant-demand-types.d.ts +69 -0
  26. package/get-variant-demand-types.js +1 -0
  27. package/get-variant-types.d.ts +7 -7
  28. package/http-transport.d.ts +14 -10
  29. package/http-transport.js +14 -12
  30. package/image-reference-types.d.ts +2 -2
  31. package/index.d.ts +3 -1
  32. package/json.d.ts +18 -0
  33. package/json.js +36 -0
  34. package/list-enum-values-types.d.ts +8 -8
  35. package/list-sampled-together-pairs-types.d.ts +11 -11
  36. package/list-variant-facets-types.d.ts +8 -8
  37. package/list-variants-types.d.ts +42 -32
  38. package/material-asset-types.d.ts +26 -26
  39. package/material-block-types.d.ts +7 -7
  40. package/material-types.d.ts +6 -6
  41. package/nearest-paints-types.d.ts +8 -8
  42. package/operation-responses.d.ts +8 -2
  43. package/operations.d.ts +1 -1
  44. package/operations.generated.d.ts +1 -1
  45. package/operations.generated.js +9 -0
  46. package/operations.js +1 -1
  47. package/package.json +1 -1
  48. package/parse-operation-response.d.ts +4 -3
  49. package/parse-operation-response.js +112 -135
  50. package/resolve-color-types.d.ts +6 -6
  51. package/resolve-variant-types.d.ts +10 -10
  52. package/response-schema-catalog.d.ts +8 -0
  53. package/response-schema-catalog.js +82 -0
  54. package/response-schemas.d.ts +32 -3
  55. package/response-schemas.generated.d.ts +1 -1
  56. package/response-schemas.generated.js +34 -28
  57. package/response-schemas.js +5 -77
  58. package/schema-resource-types.d.ts +12 -12
  59. package/search-catalog-subset-types.d.ts +2 -2
  60. package/search-variants-types.d.ts +12 -12
  61. package/source-material-types.d.ts +6 -6
  62. package/source-material.js +2 -32
  63. package/types.d.ts +43 -45
  64. package/webmcp.d.ts +10 -4
  65. package/webmcp.generated.js +5 -4
package/client.js CHANGED
@@ -2,7 +2,7 @@ import { applicability, declaredFinishes, declaredProducts, } from "./associatio
2
2
  import { listBrands } from "./brands-client.js";
3
3
  import { CategoriesClient } from "./categories-client.js";
4
4
  import { DEFAULT_BASE_URL } from "./client-config.js";
5
- import { listAttributes as listAttributesCall, listEnumValues as listEnumValuesCall, resolveColor as resolveColorCall, } from "./client-vocabulary.js";
5
+ import { listAttributes as listAttributesCall, listBaselines as listBaselinesCall, listEnumValues as listEnumValuesCall, resolveColor as resolveColorCall, } from "./client-vocabulary.js";
6
6
  import { ColorsClient } from "./colors-client.js";
7
7
  import { MaterialGraphError } from "./errors.js";
8
8
  import { HttpTransport } from "./http-transport.js";
@@ -86,9 +86,7 @@ export class MaterialGraphClient {
86
86
  apiKey: options.apiKey,
87
87
  baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
88
88
  fetch: options.fetch ?? globalThis.fetch,
89
- ...(options.timeoutMs === undefined
90
- ? {}
91
- : { timeoutMs: options.timeoutMs }),
89
+ timeoutMs: options.timeoutMs,
92
90
  });
93
91
  this.#transport = transport;
94
92
  this.baseUrl = transport.baseUrl;
@@ -155,6 +153,30 @@ export class MaterialGraphClient {
155
153
  signal,
156
154
  });
157
155
  }
156
+ /**
157
+ * How much each of 1 to 48 variants is sampled by Material Bank members
158
+ * (POST `/api/v1/demand`): `samplePercentile` within its category, with a
159
+ * `trend` and the `change` of this year against the year before. Results
160
+ * keep request order. No count is returned.
161
+ */
162
+ async getVariantDemand(input, signal) {
163
+ return await this.#transport.requestJson(operation("get_variant_demand"), {
164
+ body: input,
165
+ signal,
166
+ });
167
+ }
168
+ /**
169
+ * The variants of one category sampled at least 15% more this year than
170
+ * the year before (GET `/api/v1/demand/rising`), largest `change` first.
171
+ * No count is returned.
172
+ */
173
+ async listRisingVariants(input, signal) {
174
+ return await this.#get("list_rising_variants", {
175
+ category: input.category,
176
+ limit: input.limit === undefined ? undefined : String(input.limit),
177
+ projectType: input.projectType,
178
+ }, signal);
179
+ }
158
180
  // ── Materials ──────────────────────────────────────────────────────────────
159
181
  /**
160
182
  * THE QUESTION: "tell me about THIS variant" (GET
@@ -181,10 +203,10 @@ export class MaterialGraphClient {
181
203
  blocks: options?.blocks !== undefined && options.blocks.length > 0
182
204
  ? options.blocks.join(",")
183
205
  : undefined,
206
+ ifc: options?.ifc === true ? "true" : undefined,
184
207
  observations: options?.observations !== undefined && options.observations.length > 0
185
208
  ? options.observations.join(",")
186
209
  : undefined,
187
- ifc: options?.ifc === true ? "true" : undefined,
188
210
  }, signal, { variantId });
189
211
  }
190
212
  async materialBlocks(variantId, signal) {
@@ -264,7 +286,6 @@ export class MaterialGraphClient {
264
286
  colors: input.colors !== undefined && input.colors.length > 0
265
287
  ? JSON.stringify(input.colors)
266
288
  : undefined,
267
- searchCatalogSubset: input.searchCatalogSubset,
268
289
  cursor: input.cursor,
269
290
  filters: input.filters ? JSON.stringify(input.filters) : undefined,
270
291
  include: input.include !== undefined && input.include.length > 0
@@ -281,6 +302,7 @@ export class MaterialGraphClient {
281
302
  orderBy: input.orderBy !== undefined && input.orderBy.length > 0
282
303
  ? JSON.stringify(input.orderBy)
283
304
  : undefined,
305
+ searchCatalogSubset: input.searchCatalogSubset,
284
306
  words: input.words !== undefined && input.words.length > 0
285
307
  ? input.words.join(",")
286
308
  : undefined,
@@ -357,17 +379,17 @@ export class MaterialGraphClient {
357
379
  /**
358
380
  * Every Sampled Together pair in the current generation (GET
359
381
  * `/api/v1/variants/sampled-together/pairs`), trailing 36 months, sampled,
360
- * each pair once with `a < b`. `aProjects` and `bProjects` are each side's
361
- * own project total, so a sample pack can be dropped without one call per
362
- * variant. Follow `nextCursor` until it is null; a page is not a total.
382
+ * each pair once with `a < b`. `packShare` is the pair's share of the rarer
383
+ * side's own sampled projects, so a sample pack can be dropped without one
384
+ * call per variant. Follow `nextCursor` until it is null; a page is not a total.
363
385
  */
364
386
  async listSampledTogetherPairs(input = {}, signal) {
365
387
  return await this.#get("list_sampled_together_pairs", {
366
- ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
367
- ...(input.limit === undefined ? {} : { limit: String(input.limit) }),
368
- ...(input.minProjects === undefined
369
- ? {}
370
- : { minProjects: String(input.minProjects) }),
388
+ cursor: input.cursor,
389
+ limit: input.limit === undefined ? undefined : String(input.limit),
390
+ minProjects: input.minProjects === undefined
391
+ ? undefined
392
+ : String(input.minProjects),
371
393
  }, signal);
372
394
  }
373
395
  /**
@@ -414,6 +436,24 @@ export class MaterialGraphClient {
414
436
  async listEnumValues(options, signal) {
415
437
  return await listEnumValuesCall(this.#transport, options, signal);
416
438
  }
439
+ /**
440
+ * The Performance Baselines for a sector, and for a room within it when one
441
+ * is named: what a kind of material is normally expected to achieve, each
442
+ * with why it exists and its published source. Each `waysToMeet` entry is a
443
+ * `filters` value for {@link MaterialGraphClient.listVariants}. A typical
444
+ * expectation, never a code determination: show `caveat` with it.
445
+ */
446
+ async listBaselines(options, signal) {
447
+ return await listBaselinesCall(this.#transport, options, signal);
448
+ }
449
+ /**
450
+ * Up to 50 variants read against a sector's Performance Baselines. Each
451
+ * result is `matches`, `does_not_match` or `not_stated`, with what the brand
452
+ * states; `not_stated` is a gap in the data and never a failure.
453
+ */
454
+ async checkBaselines(input, signal) {
455
+ return await this.#post("check_baselines", input, signal);
456
+ }
417
457
  // Hard-specification search (formerly `bySpec()`) moved to
418
458
  // {@link MaterialGraphClient.listVariants}
419
459
  // (MG-2222): each `attribute:value` criterion is one `filters` condition —
@@ -14,7 +14,7 @@ import type { OpenEnum } from "./types.js";
14
14
  */
15
15
  export type CategoryTier = OpenEnum<"group" | "type" | "merged">;
16
16
  /** One representative color bucket of a category. */
17
- export type CategoryColor = {
17
+ export interface CategoryColor {
18
18
  hex: string;
19
19
  oklab: {
20
20
  l: number;
@@ -24,8 +24,8 @@ export type CategoryColor = {
24
24
  variantCount: number;
25
25
  /** This bucket's share of the selection's colored variants, 0 to 1. */
26
26
  proportion: number;
27
- };
28
- export type CategoryColorProfile = {
27
+ }
28
+ export interface CategoryColorProfile {
29
29
  categoryKey: string;
30
30
  level: CategoryTier;
31
31
  name: string;
@@ -39,12 +39,12 @@ export type CategoryColorProfile = {
39
39
  avgLightness: number;
40
40
  avgChroma: number;
41
41
  colors: CategoryColor[];
42
- };
43
- export type CategoryColorProfilesResponse = {
42
+ }
43
+ export interface CategoryColorProfilesResponse {
44
44
  profiles: CategoryColorProfile[];
45
- };
45
+ }
46
46
  /** Request parameters for `colors.profiles()`. */
47
- export type ColorProfilesParams = {
47
+ export interface ColorProfilesParams {
48
48
  /** 1 to 8 category keys, at the tier named by `groupBy`. */
49
49
  productTypeIds: readonly string[];
50
50
  /**
@@ -57,4 +57,4 @@ export type ColorProfilesParams = {
57
57
  topN?: number;
58
58
  /** `"diverse"` maximizes visual distinctness instead of frequency. */
59
59
  select?: "frequency" | "diverse";
60
- };
60
+ }
@@ -5,28 +5,28 @@
5
5
  */
6
6
  import type { OpenEnum } from "./types.js";
7
7
  /** A color in OKLab: lightness 0 to 1, and the green-red and blue-yellow axes. */
8
- export type OKLab = {
8
+ export interface OKLab {
9
9
  L: number;
10
10
  a: number;
11
11
  b: number;
12
- };
12
+ }
13
13
  /** A region of color: chroma, hue (null for neutrals) and lightness bounds. */
14
- export type ColorRegion = {
14
+ export interface ColorRegion {
15
15
  chroma: [number, number];
16
16
  hue: [number, number] | null;
17
17
  lightness: [number, number];
18
- };
18
+ }
19
19
  /** One of ColorScope's fifteen color families. */
20
20
  export type ColorFamilyId = OpenEnum<"beige" | "black" | "blue" | "brown" | "cream" | "gray" | "green" | "orange" | "pink" | "purple" | "red" | "taupe" | "teal" | "white" | "yellow">;
21
21
  /** A named color a resolution lands on, and how close the input is to it. */
22
- export type ColorResolutionMatch = {
22
+ export interface ColorResolutionMatch {
23
23
  band: OpenEnum<"inside" | "near" | "far">;
24
24
  family: ColorFamilyId;
25
25
  hex: string;
26
26
  id: string;
27
27
  name: string;
28
28
  range: ColorRegion;
29
- };
29
+ }
30
30
  /** What ColorScope makes of a color name: resolved to a color, or why not. */
31
31
  export type ColorResolution = {
32
32
  kind: "resolved";
@@ -29,9 +29,9 @@ import type { JsonOperationId, OperationResponses } from "./operation-responses.
29
29
  * likeness, never a stated color specification.
30
30
  */
31
31
  /** The client's own request primitives, handed down so this file owns no transport. */
32
- export type ColorsTransport = {
33
- get<K extends JsonOperationId>(operationId: K, query?: QueryParams, signal?: AbortSignal): Promise<OperationResponses[K]>;
34
- };
32
+ export interface ColorsTransport {
33
+ get: <K extends JsonOperationId>(operationId: K, query?: QueryParams, signal?: AbortSignal) => Promise<OperationResponses[K]>;
34
+ }
35
35
  export declare class ColorsClient {
36
36
  #private;
37
37
  constructor(transport: ColorsTransport);
@@ -1,6 +1,6 @@
1
1
  import type { MaterialTile } from "./material-types.js";
2
2
  /** Static registry definitions and MG-owned discovery policy, without product counts. */
3
- export type DiscoveryReference = {
3
+ export interface DiscoveryReference {
4
4
  attributes: {
5
5
  id: string;
6
6
  name: string;
@@ -23,21 +23,21 @@ export type DiscoveryReference = {
23
23
  description: string;
24
24
  }[];
25
25
  admittedBrandIds: string[];
26
- };
27
- export type InterpretDiscoveryTermsInput = {
26
+ }
27
+ export interface InterpretDiscoveryTermsInput {
28
28
  text: string;
29
29
  categoryIds: readonly string[];
30
- };
31
- export type DiscoveryTerms = {
30
+ }
31
+ export interface DiscoveryTerms {
32
32
  categories: {
33
33
  categoryIds: string[];
34
34
  words: string[];
35
35
  } | null;
36
36
  colorWordHexes: string[];
37
- };
38
- export type ColorlessVariantsResult = {
37
+ }
38
+ export interface ColorlessVariantsResult {
39
39
  variantIds: string[];
40
- };
40
+ }
41
41
  /** A sibling uses the same tile contract as the catalog's material cards. */
42
42
  export type VariantSibling = MaterialTile & {
43
43
  distinction?: string;
package/errors.d.ts CHANGED
@@ -1,12 +1,11 @@
1
- /** Error and rate-limit contracts for the retry-free MaterialGraph SDK. */
2
1
  export type MaterialGraphErrorKind = "problem" | "simple" | "network" | "timeout" | "aborted" | "rate_limited" | "unexpected";
3
2
  export type SanitizedErrorValue = null | boolean | number | string | SanitizedErrorValue[] | SanitizedIssue;
4
3
  /** A bounded, recursively sanitized validation-issue record. */
5
- export type SanitizedIssue = {
4
+ export interface SanitizedIssue {
6
5
  [key: string]: SanitizedErrorValue;
7
- };
6
+ }
8
7
  /** Normalized RFC 7807 body. Unknown response fields are intentionally discarded. */
9
- export type ProblemDetail = {
8
+ export interface ProblemDetail {
10
9
  type: string;
11
10
  title: string;
12
11
  status: number;
@@ -23,27 +22,27 @@ export type ProblemDetail = {
23
22
  * not carry them. See {@link MATERIALGRAPH_PROBLEM_EXTENSIONS}.
24
23
  */
25
24
  readonly [extension: string]: unknown;
26
- };
25
+ }
27
26
  /** Normalized residual `{ error }` body. Unknown response fields are discarded. */
28
- export type SimpleErrorBody = {
27
+ export interface SimpleErrorBody {
29
28
  /** The vector and variant routes' machine-readable code, when the body is `{ code, message }`. */
30
29
  code?: string;
31
30
  error: string;
32
31
  issues?: SanitizedIssue[];
33
- };
32
+ }
34
33
  /** Parsed `X-RateLimit-*` / `Retry-After` headers. `null` means absent. */
35
- export type RateLimit = {
34
+ export interface RateLimit {
36
35
  limit: number | null;
37
36
  remaining: number | null;
38
37
  reset: number | null;
39
38
  retryAfter: number | null;
40
- };
41
- export type MaterialGraphRequestMetadata = {
39
+ }
40
+ export interface MaterialGraphRequestMetadata {
42
41
  method: string;
43
42
  origin: string;
44
43
  path: string;
45
- };
46
- export type MaterialGraphErrorInit = {
44
+ }
45
+ export interface MaterialGraphErrorInit {
47
46
  kind: MaterialGraphErrorKind;
48
47
  status?: number;
49
48
  retriable: boolean;
@@ -56,7 +55,7 @@ export type MaterialGraphErrorInit = {
56
55
  path?: string;
57
56
  /** Accepted for compatibility but deliberately never retained on the public error. */
58
57
  cause?: unknown;
59
- };
58
+ }
60
59
  type ResponseErrorInput = {
61
60
  status: number;
62
61
  body: unknown;
package/errors.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Error and rate-limit contracts for the retry-free MaterialGraph SDK. */
2
+ import { isJsonNumber, isJsonObject, isJsonString, toJsonValue, } from "./json.js";
2
3
  const RETRIABLE_STATUS = new Set([502, 503, 504]);
3
4
  /**
4
5
  * MaterialGraph's documented problem EXTENSION members — the only non-standard
@@ -45,7 +46,6 @@ const MAX_UNEXPECTED_TEXT = 768;
45
46
  const MAX_EXTENSION_DEPTH = 8;
46
47
  const MAX_EXTENSION_ITEMS = 100;
47
48
  const MAX_EXTENSION_STRING = 1024;
48
- const isRecord = (value) => typeof value === "object" && value !== null;
49
49
  const redact = (text, secrets) => {
50
50
  let sanitized = text;
51
51
  for (const secret of secrets) {
@@ -67,35 +67,35 @@ const requestMetadata = (input) => ({
67
67
  origin: input.origin,
68
68
  path: input.path,
69
69
  });
70
- const sanitizeValue = (value, secrets, depth = 0) => {
71
- if (depth >= MAX_EXTENSION_DEPTH) {
72
- return undefined;
73
- }
74
- if (value === null || typeof value === "boolean") {
75
- return value;
76
- }
77
- if (typeof value === "number") {
78
- return Number.isFinite(value) ? value : undefined;
79
- }
80
- if (typeof value === "string") {
81
- return safeString(value, secrets);
82
- }
83
- if (Array.isArray(value)) {
84
- return value
85
- .slice(0, MAX_EXTENSION_ITEMS)
86
- .map((item) => sanitizeValue(item, secrets, depth + 1))
87
- .filter((item) => item !== undefined);
88
- }
89
- if (!isRecord(value)) {
90
- return undefined;
91
- }
92
- return sanitizeRecord(value, secrets, depth);
93
- };
94
- const sanitizeRecord = (value, secrets, depth) => {
70
+ /** Redact, cap and drop non-JSON from one object, at most {@link MAX_EXTENSION_DEPTH} levels deep. */
71
+ const sanitizeObject = (value, secrets, depth) => {
72
+ const sanitizeItem = (item, itemDepth) => {
73
+ if (itemDepth >= MAX_EXTENSION_DEPTH) {
74
+ return undefined;
75
+ }
76
+ if (item === null || item === true || item === false) {
77
+ return item;
78
+ }
79
+ if (isJsonNumber(item)) {
80
+ return Number.isFinite(item) ? item : undefined;
81
+ }
82
+ if (isJsonString(item)) {
83
+ return safeString(item, secrets);
84
+ }
85
+ if (Array.isArray(item)) {
86
+ return item
87
+ .slice(0, MAX_EXTENSION_ITEMS)
88
+ .map((child) => sanitizeItem(child, itemDepth + 1))
89
+ .filter((child) => child !== undefined);
90
+ }
91
+ return isJsonObject(item)
92
+ ? sanitizeObject(item, secrets, itemDepth)
93
+ : undefined;
94
+ };
95
95
  const entries = Object.entries(value)
96
96
  .slice(0, MAX_EXTENSION_ITEMS)
97
97
  .flatMap(([key, item]) => {
98
- const sanitized = sanitizeValue(item, secrets, depth + 1);
98
+ const sanitized = sanitizeItem(item, depth + 1);
99
99
  return sanitized === undefined
100
100
  ? []
101
101
  : [[safeString(key, secrets), sanitized]];
@@ -108,12 +108,11 @@ const sanitizeIssues = (value, secrets) => {
108
108
  }
109
109
  return value
110
110
  .slice(0, MAX_EXTENSION_ITEMS)
111
- .filter((issue) => isRecord(issue) && !Array.isArray(issue))
112
- .map((issue) => sanitizeRecord(issue, secrets, 0));
111
+ .filter(isJsonObject)
112
+ .map((issue) => sanitizeObject(issue, secrets, 0));
113
113
  };
114
114
  const sanitizeSuggestions = (value, secrets) => {
115
- if (!Array.isArray(value) ||
116
- !value.every((suggestion) => typeof suggestion === "string")) {
115
+ if (!Array.isArray(value) || !value.every(isJsonString)) {
117
116
  return undefined;
118
117
  }
119
118
  return value
@@ -121,62 +120,72 @@ const sanitizeSuggestions = (value, secrets) => {
121
120
  .map((suggestion) => safeString(suggestion, secrets));
122
121
  };
123
122
  const normalizeProblem = (body, secrets) => {
124
- if (!isRecord(body) ||
125
- typeof body.type !== "string" ||
126
- typeof body.title !== "string" ||
127
- typeof body.status !== "number" ||
128
- typeof body.detail !== "string" ||
129
- typeof body.is_retriable !== "boolean" ||
130
- typeof body.trace_id !== "string" ||
131
- (body.instance !== undefined && typeof body.instance !== "string")) {
123
+ if (!isJsonObject(body)) {
124
+ return undefined;
125
+ }
126
+ const { detail, doc_uri: docUri, instance, is_retriable: retriable, status, title, trace_id: traceId, type, } = body;
127
+ if (!isJsonString(type) ||
128
+ !isJsonString(title) ||
129
+ !isJsonNumber(status) ||
130
+ !isJsonString(detail) ||
131
+ (retriable !== true && retriable !== false) ||
132
+ !isJsonString(traceId) ||
133
+ (instance !== undefined && !isJsonString(instance))) {
132
134
  return undefined;
133
135
  }
134
- const suggestions = sanitizeSuggestions(body.suggestions, secrets);
135
- const issues = sanitizeIssues(body.issues, secrets);
136
136
  // Extensions FIRST so a malformed one can never shadow a standard member
137
- // below. `sanitizeRecord` is the existing redaction and depth/item caps —
137
+ // below. `sanitizeObject` is the existing redaction and depth/item caps —
138
138
  // this is that machinery finally being reached, not a second sanitizer.
139
- const extensions = sanitizeRecord(Object.fromEntries(Object.entries(body).filter(([key]) => PROBLEM_EXTENSION_KEYS.has(key))), secrets, 0);
140
- return {
139
+ const extensions = sanitizeObject(Object.fromEntries(Object.entries(body).filter(([key]) => PROBLEM_EXTENSION_KEYS.has(key))), secrets, 0);
140
+ const problem = {
141
141
  ...extensions,
142
- type: safeString(body.type, secrets),
143
- title: safeString(body.title, secrets),
144
- status: body.status,
145
- detail: safeString(body.detail, secrets),
146
- ...(body.instance === undefined
147
- ? {}
148
- : { instance: safeString(body.instance, secrets) }),
149
- is_retriable: body.is_retriable,
150
- trace_id: safeString(body.trace_id, secrets),
151
- ...(suggestions === undefined ? {} : { suggestions }),
152
- ...(typeof body.doc_uri === "string"
153
- ? { doc_uri: safeString(body.doc_uri, secrets) }
154
- : {}),
155
- ...(issues === undefined ? {} : { issues }),
142
+ detail: safeString(detail, secrets),
143
+ is_retriable: retriable,
144
+ status,
145
+ title: safeString(title, secrets),
146
+ trace_id: safeString(traceId, secrets),
147
+ type: safeString(type, secrets),
156
148
  };
149
+ if (instance !== undefined) {
150
+ problem.instance = safeString(instance, secrets);
151
+ }
152
+ const suggestions = sanitizeSuggestions(body.suggestions, secrets);
153
+ if (suggestions !== undefined) {
154
+ problem.suggestions = suggestions;
155
+ }
156
+ if (isJsonString(docUri)) {
157
+ problem.doc_uri = safeString(docUri, secrets);
158
+ }
159
+ const issues = sanitizeIssues(body.issues, secrets);
160
+ if (issues !== undefined) {
161
+ problem.issues = issues;
162
+ }
163
+ return problem;
157
164
  };
158
165
  const normalizeSimple = (body, secrets) => {
159
- if (!isRecord(body)) {
166
+ if (!isJsonObject(body)) {
160
167
  return undefined;
161
168
  }
162
- const issues = sanitizeIssues(body.issues, secrets);
169
+ const { code, error, message } = body;
170
+ let simple;
163
171
  // `{ code, message }` is the vector routes' error body; keep its code.
164
- if (typeof body.error !== "string" &&
165
- typeof body.code === "string" &&
166
- typeof body.message === "string") {
167
- return {
168
- code: safeString(body.code, secrets),
169
- error: safeString(body.message, secrets),
170
- ...(issues === undefined ? {} : { issues }),
172
+ if (!isJsonString(error) && isJsonString(code) && isJsonString(message)) {
173
+ simple = {
174
+ code: safeString(code, secrets),
175
+ error: safeString(message, secrets),
171
176
  };
172
177
  }
173
- if (typeof body.error !== "string") {
178
+ else if (isJsonString(error)) {
179
+ simple = { error: safeString(error, secrets) };
180
+ }
181
+ else {
174
182
  return undefined;
175
183
  }
176
- return {
177
- error: safeString(body.error, secrets),
178
- ...(issues === undefined ? {} : { issues }),
179
- };
184
+ const issues = sanitizeIssues(body.issues, secrets);
185
+ if (issues !== undefined) {
186
+ simple.issues = issues;
187
+ }
188
+ return simple;
180
189
  };
181
190
  const unexpectedMessage = (status, responseText, secrets) => {
182
191
  const sanitized = redact(responseText, secrets)
@@ -251,8 +260,9 @@ export class MaterialGraphError extends Error {
251
260
  }
252
261
  static fromResponse(input) {
253
262
  const secrets = secretVariants(input.apiKey, input.queryValues);
254
- const problem = normalizeProblem(input.body, secrets);
255
- const simple = problem === undefined ? normalizeSimple(input.body, secrets) : undefined;
263
+ const body = toJsonValue(input.body);
264
+ const problem = normalizeProblem(body, secrets);
265
+ const simple = problem === undefined ? normalizeSimple(body, secrets) : undefined;
256
266
  const retriable = RETRIABLE_STATUS.has(input.status);
257
267
  const metadata = requestMetadata(input);
258
268
  if (input.status === 429) {
@@ -12,7 +12,7 @@
12
12
  /** The closed sector vocabulary co-specification evidence is tagged with. */
13
13
  export type SampledTogetherProjectType = "Workplace" | "Residential" | "Institution Education" | "Hospitality: Hotel" | "Mixed-Use & Multi-Unit Residential" | "Healthcare" | "Hospitality: Restaurant" | "Government" | "Senior Living" | "Retail" | "Other" | "Sports Entertainment" | "Wellness" | "Medical Dental" | "Aviation" | "Industrial" | "Tech" | "Transportation" | "Single Family" | "Condominium Cooperative" | "Marine" | "Power & Energy" | "University Design Program: Interior Design" | "Non-Billable" | "University Design Program: Interior Architecture" | "University Design Program: Architecture" | "Historic Preservation";
14
14
  /** At least one of `variantIds` and `materials`, at most twelve together. */
15
- export type FindSampledTogetherInput = {
15
+ export interface FindSampledTogetherInput {
16
16
  /** Optional row fields: image adds imageUrl, color adds swatches. */
17
17
  include?: ("image" | "color")[];
18
18
  /** Materials to return, default 20, at most 100. */
@@ -23,8 +23,8 @@ export type FindSampledTogetherInput = {
23
23
  projectType?: SampledTogetherProjectType;
24
24
  /** Variants to find sampled-together materials for. */
25
25
  variantIds?: string[];
26
- };
27
- export type FindSampledTogetherRow = {
26
+ }
27
+ export interface FindSampledTogetherRow {
28
28
  brand: string;
29
29
  /** Palette likeness to the seeds. Present with `include: ["color"]`; never reorders. */
30
30
  colorFit?: number | null;
@@ -49,8 +49,8 @@ export type FindSampledTogetherRow = {
49
49
  /** The trimmed source variant label; null when unknown or for a paint color sample. Separate from surface finish, color family and derived color names. */
50
50
  variantLabel: string | null;
51
51
  variantName: string | null;
52
- };
53
- export type FindSampledTogetherOutput = {
52
+ }
53
+ export interface FindSampledTogetherOutput {
54
54
  /** Other brands first; within each, product type groups in turn, each group best first (seeds matched, then lift weighted by projects); one row per product. */
55
55
  sampledTogether: FindSampledTogetherRow[];
56
56
  /** The seeds as resolved, so a caller sees what a name became. */
@@ -71,4 +71,4 @@ export type FindSampledTogetherOutput = {
71
71
  excludedBySearchCatalogSubset?: string[];
72
72
  /** References that matched no variant. */
73
73
  unresolved: string[];
74
- };
74
+ }