@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.
- package/association-types.d.ts +20 -20
- package/baseline-types.d.ts +112 -0
- package/baseline-types.js +1 -0
- package/brand-types.d.ts +6 -6
- package/by-spec-types.d.ts +6 -6
- package/catalog-discovery-types.d.ts +4 -4
- package/categories-client.d.ts +3 -3
- package/category-types.d.ts +18 -18
- package/client-config.d.ts +2 -2
- package/client-response-schema.d.ts +2 -2
- package/client-response-schema.js +5 -5
- package/client-vocabulary.d.ts +2 -0
- package/client-vocabulary.js +9 -1
- package/client.d.ts +32 -3
- package/client.js +54 -14
- package/color-profiles-types.d.ts +8 -8
- package/color-wire-types.d.ts +6 -6
- package/colors-client.d.ts +3 -3
- package/discovery-context-types.d.ts +8 -8
- package/errors.d.ts +12 -13
- package/errors.js +84 -74
- package/find-sampled-together-types.d.ts +6 -6
- package/find-variants-by-color-types.d.ts +31 -30
- package/find-variants-by-vector-types.d.ts +13 -13
- package/get-variant-demand-types.d.ts +69 -0
- package/get-variant-demand-types.js +1 -0
- package/get-variant-types.d.ts +7 -7
- package/http-transport.d.ts +14 -10
- package/http-transport.js +14 -12
- package/image-reference-types.d.ts +2 -2
- package/index.d.ts +3 -1
- package/json.d.ts +18 -0
- package/json.js +36 -0
- package/list-enum-values-types.d.ts +8 -8
- package/list-sampled-together-pairs-types.d.ts +11 -11
- package/list-variant-facets-types.d.ts +8 -8
- package/list-variants-types.d.ts +42 -32
- package/material-asset-types.d.ts +26 -26
- package/material-block-types.d.ts +7 -7
- package/material-types.d.ts +6 -6
- package/nearest-paints-types.d.ts +8 -8
- package/operation-responses.d.ts +8 -2
- package/operations.d.ts +1 -1
- package/operations.generated.d.ts +1 -1
- package/operations.generated.js +9 -0
- package/operations.js +1 -1
- package/package.json +1 -1
- package/parse-operation-response.d.ts +4 -3
- package/parse-operation-response.js +112 -135
- package/resolve-color-types.d.ts +6 -6
- package/resolve-variant-types.d.ts +10 -10
- package/response-schema-catalog.d.ts +8 -0
- package/response-schema-catalog.js +82 -0
- package/response-schemas.d.ts +32 -3
- package/response-schemas.generated.d.ts +1 -1
- package/response-schemas.generated.js +34 -28
- package/response-schemas.js +5 -77
- package/schema-resource-types.d.ts +12 -12
- package/search-catalog-subset-types.d.ts +2 -2
- package/search-variants-types.d.ts +12 -12
- package/source-material-types.d.ts +6 -6
- package/source-material.js +2 -32
- package/types.d.ts +43 -45
- package/webmcp.d.ts +10 -4
- 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
|
-
|
|
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`. `
|
|
361
|
-
* own
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
?
|
|
370
|
-
:
|
|
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
|
|
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
|
|
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
|
|
42
|
+
}
|
|
43
|
+
export interface CategoryColorProfilesResponse {
|
|
44
44
|
profiles: CategoryColorProfile[];
|
|
45
|
-
}
|
|
45
|
+
}
|
|
46
46
|
/** Request parameters for `colors.profiles()`. */
|
|
47
|
-
export
|
|
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
|
+
}
|
package/color-wire-types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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";
|
package/colors-client.d.ts
CHANGED
|
@@ -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
|
|
33
|
-
get<K extends JsonOperationId>(operationId: K, query?: QueryParams, signal?: AbortSignal)
|
|
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
|
|
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
|
|
26
|
+
}
|
|
27
|
+
export interface InterpretDiscoveryTermsInput {
|
|
28
28
|
text: string;
|
|
29
29
|
categoryIds: readonly string[];
|
|
30
|
-
}
|
|
31
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39
|
+
}
|
|
40
|
+
export interface MaterialGraphRequestMetadata {
|
|
42
41
|
method: string;
|
|
43
42
|
origin: string;
|
|
44
43
|
path: string;
|
|
45
|
-
}
|
|
46
|
-
export
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 =
|
|
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(
|
|
112
|
-
.map((issue) =>
|
|
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 (!
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
(
|
|
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. `
|
|
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 =
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
status
|
|
145
|
-
|
|
146
|
-
|
|
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 (!
|
|
166
|
+
if (!isJsonObject(body)) {
|
|
160
167
|
return undefined;
|
|
161
168
|
}
|
|
162
|
-
const
|
|
169
|
+
const { code, error, message } = body;
|
|
170
|
+
let simple;
|
|
163
171
|
// `{ code, message }` is the vector routes' error body; keep its code.
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 (
|
|
178
|
+
else if (isJsonString(error)) {
|
|
179
|
+
simple = { error: safeString(error, secrets) };
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
174
182
|
return undefined;
|
|
175
183
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
255
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|