@instruments/materialgraph 0.30.0 → 0.31.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/client.d.ts +12 -0
- package/client.js +13 -0
- package/errors.d.ts +1 -1
- package/errors.js +1 -0
- package/find-companions-types.d.ts +70 -0
- package/find-companions-types.js +12 -0
- package/http-transport.d.ts +1 -1
- package/index.d.ts +1 -0
- package/material-types.d.ts +2 -1
- package/operations.d.ts +1 -1
- package/operations.js +1 -0
- package/package.json +1 -1
- package/response-schemas.generated.d.ts +1 -1
- package/response-schemas.generated.js +39 -38
- package/search-index-plan-types.d.ts +2 -1
- package/specifications.d.ts +1 -9
- package/specifications.js +0 -12
- package/validation.d.ts +0 -74
- package/validation.js +2 -116
- package/webmcp.generated.js +8 -8
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { ColourRegionRange } from "./colour-region-types.js";
|
|
9
9
|
import type { CorpusScope } from "./corpus-scope-types.js";
|
|
10
10
|
/** One attribute the plan wants: its terms, or any value when `presence`. */
|
|
11
|
-
|
|
11
|
+
type SearchIndexPlanWant = {
|
|
12
12
|
attributeId: string;
|
|
13
13
|
/** Share of the category's materials stating the attribute, 0 to 1. */
|
|
14
14
|
fill: number;
|
|
@@ -71,3 +71,4 @@ export type SearchPlanReceipt = {
|
|
|
71
71
|
source: "dictionary" | "jev";
|
|
72
72
|
version: string;
|
|
73
73
|
};
|
|
74
|
+
export {};
|
package/specifications.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type { CertificationProgrammeFilter,
|
|
2
|
-
/**
|
|
3
|
-
* A hard criterion excluding one canonical value.
|
|
4
|
-
*
|
|
5
|
-
* By default retains only variants with a KNOWN value (unstated does not
|
|
6
|
-
* pass). Pass `whenUnstated: "pass"` to admit unstated variants too — only a
|
|
7
|
-
* variant that STATES the excluded term is dropped.
|
|
8
|
-
*/
|
|
9
|
-
export declare const excludeSpec: (attributeId: string, excludesTerm: string, whenUnstated?: "drop" | "pass") => SpecExclusionFilter;
|
|
1
|
+
import type { CertificationProgrammeFilter, SpecPresenceFilter, SpecTermFilter } from "./spec-types.js";
|
|
10
2
|
/**
|
|
11
3
|
* A term-equality criterion for `search({ filters })` — the attribute must
|
|
12
4
|
* carry this canonical value.
|
package/specifications.js
CHANGED
|
@@ -15,18 +15,6 @@
|
|
|
15
15
|
* object literal needs no encoder (MG-2222).
|
|
16
16
|
*/
|
|
17
17
|
import { CERTIFICATION_PROGRAMME_ATTRIBUTE_ID } from "./spec-types.js";
|
|
18
|
-
/**
|
|
19
|
-
* A hard criterion excluding one canonical value.
|
|
20
|
-
*
|
|
21
|
-
* By default retains only variants with a KNOWN value (unstated does not
|
|
22
|
-
* pass). Pass `whenUnstated: "pass"` to admit unstated variants too — only a
|
|
23
|
-
* variant that STATES the excluded term is dropped.
|
|
24
|
-
*/
|
|
25
|
-
export const excludeSpec = (attributeId, excludesTerm, whenUnstated) => ({
|
|
26
|
-
attributeId,
|
|
27
|
-
excludesTerm,
|
|
28
|
-
...(whenUnstated === undefined ? {} : { whenUnstated }),
|
|
29
|
-
});
|
|
30
18
|
/**
|
|
31
19
|
* A term-equality criterion for `search({ filters })` — the attribute must
|
|
32
20
|
* carry this canonical value.
|
package/validation.d.ts
CHANGED
|
@@ -15,37 +15,19 @@
|
|
|
15
15
|
* this file knowing what a Match frame or a Run event is.
|
|
16
16
|
*/
|
|
17
17
|
import type { MaterialGraphError } from "./errors.js";
|
|
18
|
-
import type { JsonValue } from "./types.js";
|
|
19
18
|
/**
|
|
20
19
|
* Builds the one error a validation run raises. The calling module supplies it,
|
|
21
20
|
* so the message names that payload and quotes its source.
|
|
22
21
|
*/
|
|
23
22
|
export type ValidationFailure = (reason: string) => MaterialGraphError;
|
|
24
|
-
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
25
23
|
/** A `z.object({…})`. */
|
|
26
24
|
export declare const requireRecord: (value: unknown, what: string, fail: ValidationFailure) => Record<string, unknown>;
|
|
27
25
|
/** A `z.string()`. */
|
|
28
26
|
export declare const requireString: (value: unknown, what: string, fail: ValidationFailure) => string;
|
|
29
|
-
/** A `z.string().min(1)`. */
|
|
30
|
-
export declare const requireNonEmptyString: (value: unknown, what: string, fail: ValidationFailure) => string;
|
|
31
27
|
/** A `z.string().optional()`: absent is fine, present-but-wrong is not. */
|
|
32
28
|
export declare const requireOptionalString: (value: unknown, what: string, fail: ValidationFailure) => void;
|
|
33
29
|
/** A `z.string().nullable()`. */
|
|
34
30
|
export declare const requireNullableString: (value: unknown, what: string, fail: ValidationFailure) => string | null;
|
|
35
|
-
/** A `z.string().min(1).nullable()`. */
|
|
36
|
-
export declare const requireNullableNonEmptyString: (value: unknown, what: string, fail: ValidationFailure) => string | null;
|
|
37
|
-
/**
|
|
38
|
-
* A `z.string().trim().min(1).max(max)`.
|
|
39
|
-
*
|
|
40
|
-
* The `.trim()` is a TRANSFORM, so the schema's parsed output is the trimmed
|
|
41
|
-
* string and its length bounds apply AFTER trimming. A mirror that checked the
|
|
42
|
-
* raw length would disagree with the schema twice over: it would accept a
|
|
43
|
-
* whitespace-only value the schema rejects, and reject a padded value the schema
|
|
44
|
-
* accepts. Returns the trimmed string, which is what the schema returns.
|
|
45
|
-
*/
|
|
46
|
-
export declare const requireTrimmedBoundedString: (value: unknown, what: string, max: number, fail: ValidationFailure) => string;
|
|
47
|
-
/** A `z.string().trim().min(1).max(max).nullable()`. */
|
|
48
|
-
export declare const requireNullableTrimmedBoundedString: (value: unknown, what: string, max: number, fail: ValidationFailure) => string | null;
|
|
49
31
|
/** A `z.boolean()`. */
|
|
50
32
|
export declare const requireBoolean: (value: unknown, what: string, fail: ValidationFailure) => boolean;
|
|
51
33
|
/** A `z.boolean().optional()`. */
|
|
@@ -55,8 +37,6 @@ export declare const requireOptionalBoolean: (value: unknown, what: string, fail
|
|
|
55
37
|
* out of JSON as `Infinity`, so finiteness is a reachable check.
|
|
56
38
|
*/
|
|
57
39
|
export declare const requireNumber: (value: unknown, what: string, fail: ValidationFailure) => number;
|
|
58
|
-
/** A `z.number().optional()`. */
|
|
59
|
-
export declare const requireOptionalNumber: (value: unknown, what: string, fail: ValidationFailure) => void;
|
|
60
40
|
/** A `z.number().nullable()`. */
|
|
61
41
|
export declare const requireNullableNumber: (value: unknown, what: string, fail: ValidationFailure) => number | null;
|
|
62
42
|
/**
|
|
@@ -64,29 +44,10 @@ export declare const requireNullableNumber: (value: unknown, what: string, fail:
|
|
|
64
44
|
* comparisons, exactly as the bounded Zod schema rejects them.
|
|
65
45
|
*/
|
|
66
46
|
export declare const requireBoundedNumber: (value: unknown, what: string, fail: ValidationFailure, min: number, max: number) => number;
|
|
67
|
-
/** A `z.number().min(min).max(max).nullable()`. */
|
|
68
|
-
export declare const requireNullableBoundedNumber: (value: unknown, what: string, fail: ValidationFailure, min: number, max: number) => number | null;
|
|
69
|
-
/**
|
|
70
|
-
* A bare `z.number().int()` — integer, finite, and within the SAFE range, with
|
|
71
|
-
* NO lower bound.
|
|
72
|
-
*
|
|
73
|
-
* Measured against the pinned Zod (4.4.3), `.int()` is exactly
|
|
74
|
-
* `Number.isSafeInteger`: it accepts `0`, `-1`, and `±MAX_SAFE_INTEGER`, and
|
|
75
|
-
* rejects `1.5`, `±2**53`, `NaN`, and `±Infinity`.
|
|
76
|
-
*
|
|
77
|
-
* Two ways to get this wrong, both of which break parity in a different
|
|
78
|
-
* direction. Adding a `min: 0` REJECTS a negative the schema accepts — a client
|
|
79
|
-
* refusing a legitimate response. Using `Number.isInteger` ACCEPTS `2**53`,
|
|
80
|
-
* which the schema rejects — so `requireIntegerAtLeast` is not this helper with
|
|
81
|
-
* the bound dropped, and the safe-range test is why.
|
|
82
|
-
*/
|
|
83
|
-
export declare const requireSafeInteger: (value: unknown, what: string, fail: ValidationFailure) => number;
|
|
84
47
|
/** A `z.number().int().min(min)` — the integer constraint AND the lower bound. */
|
|
85
48
|
export declare const requireIntegerAtLeast: (value: unknown, what: string, fail: ValidationFailure, min: number) => number;
|
|
86
49
|
/** A `z.enum([…])` — CLOSED, exactly like the canonical member list. */
|
|
87
50
|
export declare const requireEnum: (value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure) => string;
|
|
88
|
-
/** A `z.enum([…]).nullable()`. */
|
|
89
|
-
export declare const requireNullableEnum: (value: unknown, allowed: readonly string[], what: string, fail: ValidationFailure) => string | null;
|
|
90
51
|
/** A `z.array(…)`. */
|
|
91
52
|
export declare const requireArray: (value: unknown, what: string, fail: ValidationFailure) => unknown[];
|
|
92
53
|
/**
|
|
@@ -94,38 +55,3 @@ export declare const requireArray: (value: unknown, what: string, fail: Validati
|
|
|
94
55
|
* `[]`; a present non-array is not.
|
|
95
56
|
*/
|
|
96
57
|
export declare const requireDefaultedArray: (value: unknown, what: string, fail: ValidationFailure) => unknown[];
|
|
97
|
-
/** A `z.array(z.string()).min(1)` — non-empty, and every element a string. */
|
|
98
|
-
export declare const requireNonEmptyStringArray: (value: unknown, what: string, fail: ValidationFailure) => string[];
|
|
99
|
-
/**
|
|
100
|
-
* A `z.json()` — a JSON value, recursively: `string | number | boolean | null`,
|
|
101
|
-
* an array of them, or an object of them, at ANY depth.
|
|
102
|
-
*
|
|
103
|
-
* WHY THIS EXISTS, given it can never fire over the wire. Every SDK response
|
|
104
|
-
* has already been through `JSON.parse`, so a `Date`, `bigint`, `undefined`, or
|
|
105
|
-
* function CANNOT be present by construction — this check is unreachable in
|
|
106
|
-
* practice. It is here for PARITY: the canonical `sourceImageAnalysisResultSchema`
|
|
107
|
-
* uses `z.json()` precisely because those four break differently on the way to
|
|
108
|
-
* `jsonb` (a `bigint` throws in `JSON.stringify`, an `undefined` is silently
|
|
109
|
-
* dropped, and a `Date` round-trips as an ISO *string* — a different type read
|
|
110
|
-
* back than written). The mirror has to reject what the schema rejects or the
|
|
111
|
-
* parity test is lying. Do not delete this as dead code.
|
|
112
|
-
*/
|
|
113
|
-
export declare const requireJsonValue: (value: unknown, what: string, fail: ValidationFailure) => JsonValue;
|
|
114
|
-
/**
|
|
115
|
-
* A `z.record(z.string(), z.json())` — a PLAIN JSON object of JSON values.
|
|
116
|
-
*
|
|
117
|
-
* Deliberately NOT `requireRecord` + walk the entries. That combination has a
|
|
118
|
-
* top-level hole: `Object.entries(new Date())` and `Object.entries(new Map())`
|
|
119
|
-
* are both `[]`, so there is nothing to recurse over and the value is accepted
|
|
120
|
-
* as `{}` — while `z.record(z.string(), z.json())` rejects both outright. A
|
|
121
|
-
* "non-array object" guard would close those two and still miss a class instance
|
|
122
|
-
* with real own properties, which canonical also rejects. Only a prototype test
|
|
123
|
-
* matches, and {@link requireJsonValue} already applies it at every depth — so
|
|
124
|
-
* this delegates there and then narrows to an object, rather than reimplementing
|
|
125
|
-
* the test and acquiring the same hole one level up.
|
|
126
|
-
*
|
|
127
|
-
* `requireRecord` is left alone: its other callers mirror `z.object({…})`, which
|
|
128
|
-
* genuinely does accept any record, and tightening a shared helper under them
|
|
129
|
-
* would be a change none of them asked for.
|
|
130
|
-
*/
|
|
131
|
-
export declare const requireJsonRecord: (value: unknown, what: string, fail: ValidationFailure) => Record<string, JsonValue>;
|
package/validation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
2
|
/** A `z.object({…})`. */
|
|
3
3
|
export const requireRecord = (value, what, fail) => {
|
|
4
4
|
if (!isRecord(value)) {
|
|
@@ -13,14 +13,6 @@ export const requireString = (value, what, fail) => {
|
|
|
13
13
|
}
|
|
14
14
|
return value;
|
|
15
15
|
};
|
|
16
|
-
/** A `z.string().min(1)`. */
|
|
17
|
-
export const requireNonEmptyString = (value, what, fail) => {
|
|
18
|
-
const text = requireString(value, what, fail);
|
|
19
|
-
if (text.length === 0) {
|
|
20
|
-
throw fail(`${what} is an empty string`);
|
|
21
|
-
}
|
|
22
|
-
return text;
|
|
23
|
-
};
|
|
24
16
|
/** A `z.string().optional()`: absent is fine, present-but-wrong is not. */
|
|
25
17
|
export const requireOptionalString = (value, what, fail) => {
|
|
26
18
|
if (value === undefined) {
|
|
@@ -35,39 +27,6 @@ export const requireNullableString = (value, what, fail) => {
|
|
|
35
27
|
}
|
|
36
28
|
return requireString(value, what, fail);
|
|
37
29
|
};
|
|
38
|
-
/** A `z.string().min(1).nullable()`. */
|
|
39
|
-
export const requireNullableNonEmptyString = (value, what, fail) => {
|
|
40
|
-
if (value === null) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
return requireNonEmptyString(value, what, fail);
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* A `z.string().trim().min(1).max(max)`.
|
|
47
|
-
*
|
|
48
|
-
* The `.trim()` is a TRANSFORM, so the schema's parsed output is the trimmed
|
|
49
|
-
* string and its length bounds apply AFTER trimming. A mirror that checked the
|
|
50
|
-
* raw length would disagree with the schema twice over: it would accept a
|
|
51
|
-
* whitespace-only value the schema rejects, and reject a padded value the schema
|
|
52
|
-
* accepts. Returns the trimmed string, which is what the schema returns.
|
|
53
|
-
*/
|
|
54
|
-
export const requireTrimmedBoundedString = (value, what, max, fail) => {
|
|
55
|
-
const text = requireString(value, what, fail).trim();
|
|
56
|
-
if (text.length === 0) {
|
|
57
|
-
throw fail(`${what} is empty once trimmed`);
|
|
58
|
-
}
|
|
59
|
-
if (text.length > max) {
|
|
60
|
-
throw fail(`${what} is longer than ${max} characters (${text.length})`);
|
|
61
|
-
}
|
|
62
|
-
return text;
|
|
63
|
-
};
|
|
64
|
-
/** A `z.string().trim().min(1).max(max).nullable()`. */
|
|
65
|
-
export const requireNullableTrimmedBoundedString = (value, what, max, fail) => {
|
|
66
|
-
if (value === null) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
return requireTrimmedBoundedString(value, what, max, fail);
|
|
70
|
-
};
|
|
71
30
|
/** A `z.boolean()`. */
|
|
72
31
|
export const requireBoolean = (value, what, fail) => {
|
|
73
32
|
if (typeof value !== "boolean") {
|
|
@@ -92,13 +51,6 @@ export const requireNumber = (value, what, fail) => {
|
|
|
92
51
|
}
|
|
93
52
|
return value;
|
|
94
53
|
};
|
|
95
|
-
/** A `z.number().optional()`. */
|
|
96
|
-
export const requireOptionalNumber = (value, what, fail) => {
|
|
97
|
-
if (value === undefined) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
requireNumber(value, what, fail);
|
|
101
|
-
};
|
|
102
54
|
/** A `z.number().nullable()`. */
|
|
103
55
|
export const requireNullableNumber = (value, what, fail) => {
|
|
104
56
|
if (value === null) {
|
|
@@ -116,33 +68,6 @@ export const requireBoundedNumber = (value, what, fail, min, max) => {
|
|
|
116
68
|
}
|
|
117
69
|
return value;
|
|
118
70
|
};
|
|
119
|
-
/** A `z.number().min(min).max(max).nullable()`. */
|
|
120
|
-
export const requireNullableBoundedNumber = (value, what, fail, min, max) => {
|
|
121
|
-
if (value === null) {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
return requireBoundedNumber(value, what, fail, min, max);
|
|
125
|
-
};
|
|
126
|
-
/**
|
|
127
|
-
* A bare `z.number().int()` — integer, finite, and within the SAFE range, with
|
|
128
|
-
* NO lower bound.
|
|
129
|
-
*
|
|
130
|
-
* Measured against the pinned Zod (4.4.3), `.int()` is exactly
|
|
131
|
-
* `Number.isSafeInteger`: it accepts `0`, `-1`, and `±MAX_SAFE_INTEGER`, and
|
|
132
|
-
* rejects `1.5`, `±2**53`, `NaN`, and `±Infinity`.
|
|
133
|
-
*
|
|
134
|
-
* Two ways to get this wrong, both of which break parity in a different
|
|
135
|
-
* direction. Adding a `min: 0` REJECTS a negative the schema accepts — a client
|
|
136
|
-
* refusing a legitimate response. Using `Number.isInteger` ACCEPTS `2**53`,
|
|
137
|
-
* which the schema rejects — so `requireIntegerAtLeast` is not this helper with
|
|
138
|
-
* the bound dropped, and the safe-range test is why.
|
|
139
|
-
*/
|
|
140
|
-
export const requireSafeInteger = (value, what, fail) => {
|
|
141
|
-
if (!Number.isSafeInteger(value)) {
|
|
142
|
-
throw fail(`${what} is not a safe integer`);
|
|
143
|
-
}
|
|
144
|
-
return requireNumber(value, what, fail);
|
|
145
|
-
};
|
|
146
71
|
/** A `z.number().int().min(min)` — the integer constraint AND the lower bound. */
|
|
147
72
|
export const requireIntegerAtLeast = (value, what, fail, min) => {
|
|
148
73
|
const numeric = requireNumber(value, what, fail);
|
|
@@ -162,13 +87,6 @@ export const requireEnum = (value, allowed, what, fail) => {
|
|
|
162
87
|
}
|
|
163
88
|
return text;
|
|
164
89
|
};
|
|
165
|
-
/** A `z.enum([…]).nullable()`. */
|
|
166
|
-
export const requireNullableEnum = (value, allowed, what, fail) => {
|
|
167
|
-
if (value === null) {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
return requireEnum(value, allowed, what, fail);
|
|
171
|
-
};
|
|
172
90
|
/** A `z.array(…)`. */
|
|
173
91
|
export const requireArray = (value, what, fail) => {
|
|
174
92
|
if (!Array.isArray(value)) {
|
|
@@ -186,14 +104,6 @@ export const requireDefaultedArray = (value, what, fail) => {
|
|
|
186
104
|
}
|
|
187
105
|
return requireArray(value, what, fail);
|
|
188
106
|
};
|
|
189
|
-
/** A `z.array(z.string()).min(1)` — non-empty, and every element a string. */
|
|
190
|
-
export const requireNonEmptyStringArray = (value, what, fail) => {
|
|
191
|
-
const items = requireArray(value, what, fail);
|
|
192
|
-
if (items.length === 0) {
|
|
193
|
-
throw fail(`${what} is empty`);
|
|
194
|
-
}
|
|
195
|
-
return items.map((item, index) => requireString(item, `${what}[${index}]`, fail));
|
|
196
|
-
};
|
|
197
107
|
/**
|
|
198
108
|
* A `z.json()` — a JSON value, recursively: `string | number | boolean | null`,
|
|
199
109
|
* an array of them, or an object of them, at ANY depth.
|
|
@@ -208,7 +118,7 @@ export const requireNonEmptyStringArray = (value, what, fail) => {
|
|
|
208
118
|
* back than written). The mirror has to reject what the schema rejects or the
|
|
209
119
|
* parity test is lying. Do not delete this as dead code.
|
|
210
120
|
*/
|
|
211
|
-
|
|
121
|
+
const requireJsonValue = (value, what, fail) => {
|
|
212
122
|
if (value === null) {
|
|
213
123
|
return null;
|
|
214
124
|
}
|
|
@@ -243,27 +153,3 @@ export const requireJsonValue = (value, what, fail) => {
|
|
|
243
153
|
}
|
|
244
154
|
throw fail(`${what} is not a JSON value`);
|
|
245
155
|
};
|
|
246
|
-
/**
|
|
247
|
-
* A `z.record(z.string(), z.json())` — a PLAIN JSON object of JSON values.
|
|
248
|
-
*
|
|
249
|
-
* Deliberately NOT `requireRecord` + walk the entries. That combination has a
|
|
250
|
-
* top-level hole: `Object.entries(new Date())` and `Object.entries(new Map())`
|
|
251
|
-
* are both `[]`, so there is nothing to recurse over and the value is accepted
|
|
252
|
-
* as `{}` — while `z.record(z.string(), z.json())` rejects both outright. A
|
|
253
|
-
* "non-array object" guard would close those two and still miss a class instance
|
|
254
|
-
* with real own properties, which canonical also rejects. Only a prototype test
|
|
255
|
-
* matches, and {@link requireJsonValue} already applies it at every depth — so
|
|
256
|
-
* this delegates there and then narrows to an object, rather than reimplementing
|
|
257
|
-
* the test and acquiring the same hole one level up.
|
|
258
|
-
*
|
|
259
|
-
* `requireRecord` is left alone: its other callers mirror `z.object({…})`, which
|
|
260
|
-
* genuinely does accept any record, and tightening a shared helper under them
|
|
261
|
-
* would be a change none of them asked for.
|
|
262
|
-
*/
|
|
263
|
-
export const requireJsonRecord = (value, what, fail) => {
|
|
264
|
-
const json = requireJsonValue(value, what, fail);
|
|
265
|
-
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
266
|
-
throw fail(`${what} is not a JSON object`);
|
|
267
|
-
}
|
|
268
|
-
return json;
|
|
269
|
-
};
|
package/webmcp.generated.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const json = [
|
|
2
2
|
"[{\"annotations\":{\"readOnlyHint\":true,\"untrustedContentHint\":true},\"description\":\"The stable, authenticated search endpoint, POST JSON-body form: `text`, a raw `image` (`url` or `base64`, embedded server-side) or a `similarTo` seed (`variantId`, `materialBankId` or a pre-computed image-space `vector`), a `category`, `filters` and a `corpusScope`, and `limit`, `cursor`, `view` and `explain`. Every request runs the one search over the index (ADR 0031): the query's plan is read from the cache or the dictionaries (brands, categories, colours and attributes are read from the words), one query runs on the search index, and the page is hydrated in the order the index gave. No model runs while the caller waits. A word no material carries may be corrected first; the response then carries `correctedQuery: { from, to }` and its `note` says which results it shows. Results are one per product. A caller may also send a `category` (a hard gate that replaces the category the words were read as), attribute `filters` (hard token sets: a term, a term set, an exclusion or a presence check; OR within one attribute, AND across attributes) and a `corpusScope`. Each becomes a field of the query's plan and runs in the same index query; none of them changes the order. A category or filters can define a page without words. `explain.plan.plan` shows the plan with them on it, and `explain.plan.signature` covers them, so one request gives one signature. A look-alike search reads the seed's nearest materials from the image namespace; the index keeps those inside whatever the words filter to and orders them by image similarity. A field the contract does not declare is a 400 naming it, with near misses; weights, mood, colours, text modes and sorts are no longer part of the request, and brands and colours are read from `text`. `?explain=1` on the query string is honoured as a shorthand for body `explain: true`; an explicit body `explain` (true or false) always wins. The `query` echo never carries raw image bytes or a vector, only `{ type }`. Send `Accept: application/x-ndjson` for one `{ type: \\\"response\\\", status, body }` line carrying exactly what the buffered form answers. Search is dynamic, so success responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.\",\"inputSchema\":{\"$ref\":\"#/$defs/schema0\",\"$defs\":{\"schema0\":{\"type\":\"object\",\"properties\":{\"brands\":{\"description\":\"Brands the page must come from: canonical brand ids, exact slugs (\\\"sherwin-williams\\\") or exact display names (\\\"Sherwin-Williams\\\"). They replace the brands `text` was read as. An empty list is the same request as omitting it — every brand. An unrecognised value is a 400 naming it; a display name shared by more than one brand is a 400 naming the exact slugs to choose between; a brand MaterialGraph holds no materials under is a 400 naming a populated sibling when one exists.\",\"maxItems\":50,\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1}},\"category\":{\"type\":\"object\",\"properties\":{\"filter\":{\"minItems\":1,\"maxItems\":50,\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1},\"description\":\"Only these public categories are returned: category-group or product-type display names (case-insensitive) or taxonomy keys. A group stands for every product type under it. An unknown value is a 400 listing every valid name.\"}},\"required\":[\"filter\"],\"additionalProperties\":false,\"description\":\"A hard category gate. It replaces the category the words were read as.\"},\"colour\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"hex\":{\"type\":\"string\",\"pattern\":\"^#?[0-9a-f]{6}$\",\"description\":\"The colour to order by, e.g. \\\"#9CAF88\\\".\"},\"oklab\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":3,\"maxItems\":3},\"range\":{\"type\":\"object\",\"properties\":{\"chroma\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},\"hue\":{\"anyOf\":[{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},{\"type\":\"null\"}]},\"lightness\":{\"type\":\"array\"",
|
|
3
3
|
",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2}},\"required\":[\"chroma\",\"hue\",\"lightness\"],\"additionalProperties\":false}},\"required\":[\"hex\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"variantId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"A Variant whose own colour to order by: materials the same colour as this one. A Variant with no colour on the index is a 400.\"}},\"required\":[\"variantId\"],\"additionalProperties\":false}],\"description\":\"Order the page by a colour: `hex`, or `variantId` for the colour of an existing Variant. The page is the colour's family, nearest first by OKLab distance; it replaces any colour read from `text`.\"},\"corpusScope\":{\"type\":\"string\",\"enum\":[\"all\",\"samplizeCatalogue\",\"materialBankOrderable\",\"materialBankNaOrderable\",\"materialBankNaAndCatalogue\",\"materialBankEuOrderable\",\"designShopOrderable\"],\"description\":\"Which corpus to answer from. samplizeCatalogue admits published US paint colours regardless of stock. ABSENT (or 'all') means the WHOLE GRAPH — every material MaterialGraph knows, orderable or not; absence is never a narrower default. Three transaction-oriented scopes gate on governed CHANNEL membership: 'materialBankNaOrderable' (Material Bank North America, ~277,000 materials), 'materialBankEuOrderable' (Material Bank Europe, ~65,000) and 'designShopOrderable' (DesignShop, ~34,000, with the products DesignShop hides from browse excluded). 'materialBankNaAndCatalogue' is the one-query union of Material Bank North America and the governed Catalogue editorial discovery corpus; that union does not by itself claim every member is transactable. 'materialBankOrderable' is NOT one of them: it selects materials carrying a Material Bank identity, which is a provenance signal rather than a channel one, and it admits 98.4% of the graph — use it to exclude crawl-sourced brands, not to mean orderable. A scope answers membership in its named governed corpus, never suitability: scope on channel, then refine on sector. Every scope is applied INSIDE the query: `limit` fills with eligible rows and `counts` describe the scoped corpus, which post-filtering a page cannot give you. An unknown value is a 400 naming it, never a silent fall back to the whole graph.\"},\"cursor\":{\"description\":\"Opaque cursor from a previous response's `cursor`, for the next page of the same query. A cursor from another query is a 409 telling the caller to restart from the first page.\",\"type\":\"string\",\"minLength\":1},\"explain\":{\"description\":\"Add `timings`: wall-clock milliseconds for reading the plan (`planMs`), the index query (`indexMs`) and hydrating the page (`pageMs`), and `explain.plan`: the receipt of the plan the search ran on, with the plan itself.\",\"type\":\"boolean\"},\"filters\":{\"minItems\":1,\"maxItems\":24,\"type\":\"array\",\"items\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"An attribute the search index carries, e.g. \\\"primary_material\\\" or \\\"colour_mood\\\", or \\\"certification_programme\\\" for any certification programme.\"},\"term\":{\"type\":\"string\",\"minLength\":1,\"description\":\"The canonical term the value must carry, e.g. \\\"textile\\\".\"}},\"required\":[\"attributeId\",\"term\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"An attribute the search index carries, e.g. \\\"primary_material\\\" or \\\"colour_mood\\\", or \\\"certification_programme\\\" for any certification programme.\"},\"terms\":{\"minItems\":1,\"maxItems\":50,\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1},\"description\":\"Canonical terms, any one of which satisfies the filter.\"}},\"required\":[\"attributeId\",\"terms\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"An attribute the search index carries, e.g. \\\"primary_material\\\" or \\\"colour_mood\\\", or \\\"certification_programme\\\" for any certification programme.\"},\"excludesTerm\":{\"type\":\"string\",\"minLength\":1,\"description\":\"A canonical term the value must not carry. A material ",
|
|
4
|
-
"that states nothing for the attribute passes.\"}},\"required\":[\"attributeId\",\"excludesTerm\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"An attribute the search index carries, e.g. \\\"primary_material\\\" or \\\"colour_mood\\\", or \\\"certification_programme\\\" for any certification programme.\"},\"hasValue\":{\"type\":\"boolean\",\"const\":true,\"description\":\"Require any stated value.\"}},\"required\":[\"attributeId\",\"hasValue\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"A number the search index cuts into bands: a numeric attribute such as \\\"nrc_rating\\\" or \\\"lead_time_days\\\", a dimensions lens such as \\\"dimensions.thickness_mm\\\" or \\\"dimensions.short_side_mm\\\" (millimetres), or a composition share such as \\\"composition_weighted.wool\\\" (0 to 1).\"},\"max\":{\"description\":\"Inclusive upper bound. Omit for a floor.\",\"type\":\"number\"},\"min\":{\"description\":\"Inclusive lower bound. Omit for a ceiling.\",\"type\":\"number\"}},\"required\":[\"attributeId\"],\"additionalProperties\":false}]},\"description\":\"Attribute filters, every one hard, run as token sets on the search index. Each entry is a term (`attributeId` + `term`), a term set (`attributeId` + `terms`, any one satisfies it), an exclusion (`attributeId` + `excludesTerm`; a material stating nothing passes), a presence check (`attributeId` + `hasValue: true`) or a numeric range (`attributeId` + `min` and/or `max`, inclusive), read as the number bands wholly inside it, so a bound between two band edges is read inward and a value the index cannot prove in range is left out. Entries on one attribute are OR'd; entries on different attributes AND together. They are added to what the words were read as and change no ordering. An attribute or term the index does not carry is a 400 naming it.\"},\"image\":{\"description\":\"A query image to search by (URL or base64). It is embedded, and the page is its nearest materials in the image namespace; `text`, when also given, narrows them.\",\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"const\":\"url\"},\"url\":{\"type\":\"string\",\"format\":\"uri\",\"description\":\"HTTP(S) URL of the query image. MUST be an https URL.\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Base64-encoded image bytes (or a data URI) the caller already holds — e.g. a form upload — so there is nothing to host.\"},\"type\":{\"type\":\"string\",\"const\":\"base64\"}},\"required\":[\"data\",\"type\"]}],\"type\":\"object\"},\"limit\":{\"description\":\"Products per page (max 60). Absent, the search's own page size.\",\"type\":\"integer\",\"minimum\":1,\"maximum\":60},\"returnAttributes\":{\"maxItems\":8,\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1},\"description\":\"Attribute ids whose RESOLVED VALUE should ride each result, under `attributes` keyed by attribute id. Values resolve with the normal variant-over-product precedence. THREE STATES, and they are different: an attribute you did not ask for is not a key at all; an attribute you asked for that this variant carries no accepted value for is `null`; anything else is the value (a string, or an array for a multi-valued attribute). An unknown attribute id is refused naming it, never silently dropped. Pair it with `filters` to show what you filtered on.\"},\"similarTo\":{\"description\":\"Find materials that LOOK like a seed: `variantId` seeds from an existing Variant's image, `materialBankId` from a Material Bank SKU (an unknown SKU is a 400 naming it), `vector` from a pre-computed image-space halfvec literal (`[a,b,...]`). `text`, when also given, narrows the look-alikes.\",\"anyOf\":[{\"type\":\"object\",\"properties\":{\"variantId\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"variantId\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"materialBankId\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"materialBankId\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"vector\":{\"type\":\"string\",\"minLength\":1}},\"
|
|
5
|
-
"\":{\"description\":\"Free-text query (\\\"oak flooring\\\", \\\"sage green velvet\\\", \\\"Kvadrat wool\\\"). Brands, categories, colours and attributes are read from the words unless `brands`, `category`, `colour` or `filters` says so instead; a word no material carries may be corrected, and the response says so under `correctedQuery`. May be omitted when `brands`, `category` or `filters` says what to find.\",\"type\":\"string\",\"maxLength\":500},\"view\":{\"default\":\"full\",\"description\":\"Result projection. \\\"full\\\" (default) returns the complete result items; \\\"card\\\" the lean grid projection. The response echoes `view`, which discriminates the `results` union.\",\"type\":\"string\",\"enum\":[\"full\",\"card\"]}},\"additionalProperties\":false}}},\"method\":\"POST\",\"name\":\"search\",\"operationId\":\"search_materials_post\",\"outputSchema\":{\"$ref\":\"#/$defs/schema0\",\"$defs\":{\"schema0\":{\"type\":\"object\",\"properties\":{\"data\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"resultId\":{\"type\":\"string\",\"description\":\"This exact hit's attribution key — `<responseId>:<rank>`, where rank is 1-based within THIS response. Unique across pages, because each page is its own response with its own id. Store it verbatim next to the outcome; it is opaque, and `attribution.responseId` is the field to group by.\"},\"variantId\":{\"type\":\"string\"},\"brand\":{\"type\":\"string\"},\"product\":{\"type\":\"string\"},\"brandSlug\":{\"type\":[\"string\",\"null\"]},\"productId\":{\"type\":[\"string\",\"null\"]},\"materialBankId\":{\"description\":\"The variant's Material Bank SKU — the ONE EXTERNAL identifier on a result, in contrast to the OPAQUE `variantId`: it is the id you are expected to match against Material Bank's own catalogue (carts, ordering and analytics all key on it), so a result list can be joined without a second lookup. Populated by the search and by Match alike. ABSENT means no MB identity is known for the variant, never that the SKU cannot be ordered; presence is the orderability tell, absence is silence.\",\"type\":\"string\"},\"manufacturerCode\":{\"description\":\"The governed manufacturer/source-native item code from `variants.code` (for example \\\"GADLC02\\\"). Populated when the full-result retrieval source carries that canonical field; Match always hydrates it from the variant row. ABSENT means unknown or an older external serving projection, never an invented code. Independent of materialBankId.\",\"type\":\"string\"},\"projectSaveCapability\":{\"description\":\"Explicit source-governed permission to save this exact source-coded variant to a Project. ABSENT means unknown/not allowed; it is never inferred from the brand or name.\",\"type\":\"string\",\"const\":\"allowed\"},\"sampleable\":{\"description\":\"Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.\",\"type\":\"boolean\"},\"sourceStatus\":{\"description\":\"The governed data status supporting Project capture. ABSENT means unknown. Provisional data remains source-backed but is not a verified catalogue claim.\",\"type\":\"string\",\"const\":\"provisional\"},\"productType\":{\"type\":\"string\"},\"color\":{\"type\":[\"string\",\"null\"]},\"variantName\":{\"type\":[\"string\",\"null\"]},\"imageUrl\":{\"type\":[\"string\",\"null\"]},\"imageSha256\":{\"description\":\"SHA-256 of the displayed image `imageUrl` points at, as a hex digest. Lets a caller key a cache or dedupe on the asset rather than its URL. ABSENT means no hash is on record, never that there is no image.\",\"type\":\"string\"},\"hasImage\":{\"type\":\"boolean\"},\"variantCount\":{\"description\":\"How many eligible variants the row's product has, the same set `list_variants` pages, so a follow-up list of the product returns this many: the card's \\\"12 colourways\\\". ABSENT means the product is unknown or has no eligible variant.\",\"type\":\"integer\",\"exclusiveMinimum\":0,\"maximum\":9007199254740991},\"palette\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hex\":{\"type\":\"string\",\"description\":\"`#rrggbb` of the swatch, as extracted from the product image.\"},\"name\":{\"description\":\"Stored human colour name for the swatch, when
|
|
6
|
-
"\"type\":\"number\",\"description\":\"The swatch's share of the analysed image, in [0,1].\"}},\"required\":[\"hex\",\"proportion\"],\"additionalProperties\":false},\"description\":\"The material's main-image palette — the FULL stored palette, uncapped and in stored order. Colours are EXTRACTED FROM PRODUCT IMAGERY (visual likeness, never a declared colour specification). A one-swatch palette means the extractor found one colour — solid-colour materials are genuinely monochrome — never that the list was trimmed. This array is always present: EMPTY means nothing is on record (unknown), not 'this material has no colours'.\"},\"colourMeta\":{\"type\":\"object\",\"properties\":{\"dominantFamily\":{\"type\":\"string\"},\"dominantHex\":{\"type\":\"string\"},\"mood\":{\"type\":\"string\"}},\"additionalProperties\":false},\"colourChip\":{\"type\":\"object\",\"properties\":{\"hex\":{\"type\":\"string\",\"description\":\"`#rrggbb` to fill the chip with. EXTRACTED FROM the product image (a visual likeness), never a manufacturer-declared colour specification — surfaces must not present it as one.\"}},\"required\":[\"hex\"],\"additionalProperties\":false,\"description\":\"Present when this material's product imagery carries nothing beyond one colour, so a surface should render that colour rather than the photograph. Set for COATINGS (paints, primers, stains, floor coatings) whose stored palette quantises to a single swatch — a paint's photo is a flat fill, and the colour we hold structured beats a JPEG of it. ABSENT means the question was not established (unknown product type, unextracted or multi-swatch palette) and the image should be rendered; it NEVER asserts 'this is not a coating'.\"},\"attributes\":{\"description\":\"Resolved attribute values for the ids sent in `returnAttributes`, keyed by attribute id. ABSENT entirely when `returnAttributes` was not sent. A key present with `null` means this variant carries no accepted value for that attribute — absent is not empty and neither is null: the three states are distinct on purpose. Values resolve with the normal variant-over-product precedence.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"array\",\"items\":{\"type\":\"string\"}},{\"type\":\"null\"}]}},\"colourBand\":{\"description\":\"`colourDistance` as a word (identical / very close / close / related / different), or null when unmeasurable. Absent when the colour stage did not run.\",\"type\":[\"string\",\"null\"]},\"colourDistance\":{\"description\":\"Match only. OKLab earth mover's distance between the query crop's palette and this material's, reading both palettes' proportions, so a colour that is a small share of the product cannot carry the match. Under 0.1 the colour AGREES, and the page lists agreeing rows first. `null` means the material carries no stored palette — UNKNOWN colour, never 'no colour' and never 'distant'. Absent when the colour stage did not run.\",\"type\":[\"number\",\"null\"]},\"confidence\":{\"description\":\"Match only (MG-1031). How close this material is to the query, as an ABSOLUTE image-space similarity in [0,1], not adjusted for colour — the same unit as `qualityFloor` and `comparison.bestRejected`, so all three can be drawn on one axis. Never normalised against the page, so the number means the same thing across runs. It is the TRUE value and may reach 1; rendering it so that no card claims '100%' is the consumer's job (round toward zero, or show '99%+'). Capping it here would put it on a different ceiling from `bestRejected`, which publishes the same unit uncapped. A Match page is ordered by colour agreement (`colourDistance` under 0.1, then unmeasured, then the rest) and then by this number, so inside each of those sets it never rises down the page. Distinct from `score`, which is the retrieval score on the `[0, maxScore]` scale and does not order a Match page. Absent outside Match.\",\"type\":\"number\",\"minimum\":0,\"maximum\":1},\"confidenceBand\":{\"description\":\"`confidence` as a word, banded on the engine's measured readings (strong >= 0.85, good >= 0.72, fair >= 0.55 = the default quality floor,
|
|
7
|
-
"`). Present exactly when `confidence` is.\",\"type\":\"string\",\"enum\":[\"strong\",\"good\",\"fair\",\"weak\"]},\"score\":{\"type\":\"number\",\"description\":\"The selected mode's weighted primary score on the `[0, maxScore]` scale. On a Match page this is the retrieval score and does NOT order the page; the order is colour agreement, then `confidence`.\"},\"signals\":{\"description\":\"Match only: the measured image similarity the row carries.\",\"type\":\"object\",\"properties\":{\"image\":{\"description\":\"Image-space cosine between the query image and this row's image, in [0,1]. Null when it was not measured.\",\"type\":[\"number\",\"null\"]}},\"required\":[\"image\"],\"additionalProperties\":false},\"retired\":{\"description\":\"MB lists the SKU as not visible/retired; demoted in ranking. Omitted when false.\",\"type\":\"boolean\"},\"belowFloor\":{\"description\":\"Present (true) only on quality-floor rejects returned under includeBelowFloor — the item failed the match image-quality floor and would be dropped by default.\",\"type\":\"boolean\"},\"admittedOnColour\":{\"description\":\"Present (true) only on Match rows the colour probe admitted: this material is as close in colour as the catalogue gets, and its image similarity is BELOW the match quality floor by design. It is on the page because no image-retrieved row is nearer in colour, not because it cleared the floor — so a consumer can present it as a colour answer or suppress it knowingly, and a quality check can tell a designed admission from a genuine floor leak. Absent (never false) on rows that came through image retrieval. Distinct from `belowFloor`, which marks rejects returned only under `includeBelowFloor`.\",\"type\":\"boolean\"}},\"required\":[\"variantId\",\"brand\",\"product\",\"brandSlug\",\"productId\",\"color\",\"variantName\",\"imageUrl\",\"hasImage\",\"palette\",\"score\"],\"additionalProperties\":false}},\"view\":{\"default\":\"full\",\"description\":\"Projection echo — always 'full' on this envelope variant.\",\"type\":\"string\",\"const\":\"full\"},\"correctedQuery\":{\"description\":\"Present when a word no material carries was corrected before the search ran: `from` is the query as typed (normalised), `to` the phrasing the page shows results for.\",\"type\":\"object\",\"properties\":{\"from\":{\"type\":\"string\"},\"to\":{\"type\":\"string\"}},\"required\":[\"from\",\"to\"],\"additionalProperties\":false},\"attribution\":{\"type\":\"object\",\"properties\":{\"basis\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\",\"match\"],\"description\":\"The ranking basis, groupable across both ranked routes. On /api/v1/search this is the envelope's `mode` verbatim; /api/v1/search/match reports 'match'. Group your stored outcomes by this field.\"},\"issuedAt\":{\"type\":\"string\",\"description\":\"ISO-8601 timestamp of when this response was assembled, so a stored outcome carries its own age without a second lookup.\"},\"responseId\":{\"type\":\"string\",\"description\":\"This response's stable id — the same value as the `X-Request-Id` header. Store it next to the outcome (an order, a save, a click) to join that outcome back to the ranking that produced it. Opaque: round-trip it verbatim, never parse it.\"}},\"required\":[\"basis\",\"issuedAt\",\"responseId\"],\"additionalProperties\":false,\"description\":\"Result attribution receipt. Present on every successful ranked response. Nothing on our side retains it — what you store IS the record.\"},\"suggestions\":{\"default\":[],\"description\":\"Always empty: the one search offers no refinements. Read nothing from it.\",\"maxItems\":0,\"type\":\"array\",\"items\":{\"not\":{}}},\"mode\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\"],\"description\":\"What ordered the page: 'image' (a look-alike: image similarity to the seed), 'colour' (the words or the caller named a colour: colour distance inside that colour's family), 'lexical' (the query's words, ranked by text rank on the index), 'attribute' (no words left to rank on: the category and filters define the page), or 'agent' (the search agent chose these variants and their order; scores are rank-based, 1 for the first result falling linearly, and `maxScore` is 1).\"},\"
|
|
8
|
-
"\"type\":\"string\"},\"timings\":{\"description\":\"Wall-clock milliseconds per step of the search, keyed by step id. The key set is open and depends on the path a query took, so read it by iterating rather than by naming a key. Present only when `explain` is set: diagnostic, not part of the ranking contract.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"type\":\"number\"}},\"explain\":{\"description\":\"Present only under `explain`: the receipt of the plan the search ran on. Diagnostic, like `timings`.\",\"type\":\"object\",\"properties\":{\"plan\":{\"type\":\"object\",\"properties\":{\"cached\":{\"type\":\"boolean\"},\"plan\":{\"type\":\"object\",\"properties\":{\"brands\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"names\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"phrase\":{\"type\":\"string\"}},\"required\":[\"ids\",\"names\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"category\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"productTypeIds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"label\",\"phrase\",\"productTypeIds\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"colour\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"family12\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"familyId\":{\"type\":\"string\"},\"hex\":{\"type\":\"string\"},\"oklab\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":3,\"maxItems\":3},\"phrase\":{\"type\":\"string\"},\"range\":{\"type\":\"object\",\"properties\":{\"chroma\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},\"hue\":{\"anyOf\":[{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},{\"type\":\"null\"}]},\"lightness\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2}},\"required\":[\"chroma\",\"hue\",\"lightness\"],\"additionalProperties\":false}},\"required\":[\"family12\",\"familyId\",\"hex\",\"oklab\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"corpusScope\":{\"type\":\"string\",\"enum\":[\"samplizeCatalogue\",\"materialBankOrderable\",\"materialBankNaOrderable\",\"materialBankNaAndCatalogue\",\"materialBankEuOrderable\",\"designShopOrderable\"]},\"excludeBrands\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"excludes\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"term\":{\"type\":\"string\"}},\"required\":[\"attributeId\",\"phrase\",\"term\"],\"additionalProperties\":false}},\"leftover\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"normalised\":{\"type\":\"string\"},\"softAlone\":{\"type\":\"boolean\"},\"vendorColourNames\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"wants\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"fill\":{\"type\":\"number\"},\"phrase\":{\"type\":\"string\"},\"presence\":{\"type\":\"boolean\",\"const\":true},\"strength\":{\"type\":\"string\",\"enum\":[\"hard\",\"soft\"]},\"terms\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"attributeId\",\"fill\",\"phrase\",\"strength\",\"terms\"],\"additionalProperties\":false}}},\"required\":[\"brands\",\"category\",\"colour\",\"excludeBrands\",\"excludes\",\"leftover\",\"normalised\",\"softAlone\",\"vendorColourNames\",\"wants\"],\"additionalProperties\":false,\"description\":\"The plan the search ran on: the category, brands, wanted and excluded attribute terms (each hard or soft, with its fill), the colour, the words left for text, and the caller's corpus scope when one was sent.\"},\"signature\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"dictionary\",\"jev\"]},\"version\":{\"type\":\"string\"}},\"required\":[\"cached\",\"plan\",\"signature\",\"source\",\"version\"],\"additionalProperties\":false}},\"required\":[\"plan\"],\"additionalProperties\":false},\"query\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}},\"seed\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}}},\"required\":[\"results\",\"view\",\"suggestions\",\"mode\",\"maxScore\",\"query\"],\"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
4
|
+
"that states nothing for the attribute passes.\"}},\"required\":[\"attributeId\",\"excludesTerm\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"An attribute the search index carries, e.g. \\\"primary_material\\\" or \\\"colour_mood\\\", or \\\"certification_programme\\\" for any certification programme.\"},\"hasValue\":{\"type\":\"boolean\",\"const\":true,\"description\":\"Require any stated value.\"}},\"required\":[\"attributeId\",\"hasValue\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\",\"minLength\":1,\"description\":\"A number the search index cuts into bands: a numeric attribute such as \\\"nrc_rating\\\" or \\\"lead_time_days\\\", a dimensions lens such as \\\"dimensions.thickness_mm\\\" or \\\"dimensions.short_side_mm\\\" (millimetres), or a composition share such as \\\"composition_weighted.wool\\\" (0 to 1).\"},\"max\":{\"description\":\"Inclusive upper bound. Omit for a floor.\",\"type\":\"number\"},\"min\":{\"description\":\"Inclusive lower bound. Omit for a ceiling.\",\"type\":\"number\"}},\"required\":[\"attributeId\"],\"additionalProperties\":false}]},\"description\":\"Attribute filters, every one hard, run as token sets on the search index. Each entry is a term (`attributeId` + `term`), a term set (`attributeId` + `terms`, any one satisfies it), an exclusion (`attributeId` + `excludesTerm`; a material stating nothing passes), a presence check (`attributeId` + `hasValue: true`) or a numeric range (`attributeId` + `min` and/or `max`, inclusive), read as the number bands wholly inside it, so a bound between two band edges is read inward and a value the index cannot prove in range is left out. Entries on one attribute are OR'd; entries on different attributes AND together. They are added to what the words were read as and change no ordering. An attribute or term the index does not carry is a 400 naming it.\"},\"image\":{\"description\":\"A query image to search by (URL or base64). It is embedded, and the page is its nearest materials in the image namespace; `text`, when also given, narrows them.\",\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"const\":\"url\"},\"url\":{\"type\":\"string\",\"format\":\"uri\",\"description\":\"HTTP(S) URL of the query image. MUST be an https URL.\"}},\"required\":[\"type\",\"url\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"string\",\"minLength\":1,\"description\":\"Base64-encoded image bytes (or a data URI) the caller already holds — e.g. a form upload — so there is nothing to host.\"},\"type\":{\"type\":\"string\",\"const\":\"base64\"}},\"required\":[\"data\",\"type\"],\"additionalProperties\":false}],\"type\":\"object\"},\"limit\":{\"description\":\"Products per page (max 60). Absent, the search's own page size.\",\"type\":\"integer\",\"minimum\":1,\"maximum\":60},\"returnAttributes\":{\"maxItems\":8,\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1},\"description\":\"Attribute ids whose RESOLVED VALUE should ride each result, under `attributes` keyed by attribute id. Values resolve with the normal variant-over-product precedence. THREE STATES, and they are different: an attribute you did not ask for is not a key at all; an attribute you asked for that this variant carries no accepted value for is `null`; anything else is the value (a string, or an array for a multi-valued attribute). An unknown attribute id is refused naming it, never silently dropped. Pair it with `filters` to show what you filtered on.\"},\"similarTo\":{\"description\":\"Find materials that LOOK like a seed: `variantId` seeds from an existing Variant's image, `materialBankId` from a Material Bank SKU (an unknown SKU is a 400 naming it), `vector` from a pre-computed image-space halfvec literal (`[a,b,...]`). `text`, when also given, narrows the look-alikes.\",\"anyOf\":[{\"type\":\"object\",\"properties\":{\"variantId\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"variantId\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"materialBankId\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"materialBankId\"],\"additionalProperties\":false},{\"type\":\"object\",\"properties\":{\"vector\":{\"type\":\"string\",\"minLength\":1}},\"",
|
|
5
|
+
"required\":[\"vector\"],\"additionalProperties\":false}]},\"text\":{\"description\":\"Free-text query (\\\"oak flooring\\\", \\\"sage green velvet\\\", \\\"Kvadrat wool\\\"). Brands, categories, colours and attributes are read from the words unless `brands`, `category`, `colour` or `filters` says so instead; a word no material carries may be corrected, and the response says so under `correctedQuery`. May be omitted when `brands`, `category` or `filters` says what to find.\",\"type\":\"string\",\"maxLength\":500},\"view\":{\"default\":\"full\",\"description\":\"Result projection. \\\"full\\\" (default) returns the complete result items; \\\"card\\\" the lean grid projection. The response echoes `view`, which discriminates the `results` union.\",\"type\":\"string\",\"enum\":[\"full\",\"card\"]}},\"additionalProperties\":false}}},\"method\":\"POST\",\"name\":\"search\",\"operationId\":\"search_materials_post\",\"outputSchema\":{\"$ref\":\"#/$defs/schema0\",\"$defs\":{\"schema0\":{\"type\":\"object\",\"properties\":{\"data\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"resultId\":{\"type\":\"string\",\"description\":\"This exact hit's attribution key — `<responseId>:<rank>`, where rank is 1-based within THIS response. Unique across pages, because each page is its own response with its own id. Store it verbatim next to the outcome; it is opaque, and `attribution.responseId` is the field to group by.\"},\"variantId\":{\"type\":\"string\"},\"brand\":{\"type\":\"string\"},\"product\":{\"type\":\"string\"},\"brandSlug\":{\"type\":[\"string\",\"null\"]},\"productId\":{\"type\":[\"string\",\"null\"]},\"materialBankId\":{\"description\":\"The variant's Material Bank SKU — the ONE EXTERNAL identifier on a result, in contrast to the OPAQUE `variantId`: it is the id you are expected to match against Material Bank's own catalogue (carts, ordering and analytics all key on it), so a result list can be joined without a second lookup. Populated by the search and by Match alike. ABSENT means no MB identity is known for the variant, never that the SKU cannot be ordered; presence is the orderability tell, absence is silence.\",\"type\":\"string\"},\"manufacturerCode\":{\"description\":\"The governed manufacturer/source-native item code from `variants.code` (for example \\\"GADLC02\\\"). Populated when the full-result retrieval source carries that canonical field; Match always hydrates it from the variant row. ABSENT means unknown or an older external serving projection, never an invented code. Independent of materialBankId.\",\"type\":\"string\"},\"projectSaveCapability\":{\"description\":\"Explicit source-governed permission to save this exact source-coded variant to a Project. ABSENT means unknown/not allowed; it is never inferred from the brand or name.\",\"type\":\"string\",\"const\":\"allowed\"},\"sampleable\":{\"description\":\"Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.\",\"type\":\"boolean\"},\"sourceStatus\":{\"description\":\"The governed data status supporting Project capture. ABSENT means unknown. Provisional data remains source-backed but is not a verified catalogue claim.\",\"type\":\"string\",\"const\":\"provisional\"},\"productType\":{\"type\":\"string\"},\"color\":{\"type\":[\"string\",\"null\"]},\"variantName\":{\"type\":[\"string\",\"null\"]},\"imageUrl\":{\"type\":[\"string\",\"null\"]},\"imageSha256\":{\"description\":\"SHA-256 of the displayed image `imageUrl` points at, as a hex digest. Lets a caller key a cache or dedupe on the asset rather than its URL. ABSENT means no hash is on record, never that there is no image.\",\"type\":\"string\"},\"hasImage\":{\"type\":\"boolean\"},\"variantCount\":{\"description\":\"How many eligible variants the row's product has, the same set `list_variants` pages, so a follow-up list of the product returns this many: the card's \\\"12 colourways\\\". ABSENT means the product is unknown or has no eligible variant.\",\"type\":\"integer\",\"exclusiveMinimum\":0,\"maximum\":9007199254740991},\"palette\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hex\":{\"type\":\"string\",\"description\":\"`#rrggbb` of the swatch, as extracted from the product image.\"},\"name\":{\"description\":\"Stored human colour name for the swatch, when on",
|
|
6
|
+
"e was extracted.\",\"type\":[\"string\",\"null\"]},\"proportion\":{\"type\":\"number\",\"description\":\"The swatch's share of the analysed image, in [0,1].\"}},\"required\":[\"hex\",\"proportion\"],\"additionalProperties\":false},\"description\":\"The material's main-image palette — the FULL stored palette, uncapped and in stored order. Colours are EXTRACTED FROM PRODUCT IMAGERY (visual likeness, never a declared colour specification). A one-swatch palette means the extractor found one colour — solid-colour materials are genuinely monochrome — never that the list was trimmed. This array is always present: EMPTY means nothing is on record (unknown), not 'this material has no colours'.\"},\"colourMeta\":{\"type\":\"object\",\"properties\":{\"dominantFamily\":{\"type\":\"string\"},\"dominantHex\":{\"type\":\"string\"},\"mood\":{\"type\":\"string\"}},\"additionalProperties\":false},\"colourChip\":{\"type\":\"object\",\"properties\":{\"hex\":{\"type\":\"string\",\"description\":\"`#rrggbb` to fill the chip with. EXTRACTED FROM the product image (a visual likeness), never a manufacturer-declared colour specification — surfaces must not present it as one.\"}},\"required\":[\"hex\"],\"additionalProperties\":false,\"description\":\"Present when this material's product imagery carries nothing beyond one colour, so a surface should render that colour rather than the photograph. Set for COATINGS (paints, primers, stains, floor coatings) whose stored palette quantises to a single swatch — a paint's photo is a flat fill, and the colour we hold structured beats a JPEG of it. ABSENT means the question was not established (unknown product type, unextracted or multi-swatch palette) and the image should be rendered; it NEVER asserts 'this is not a coating'.\"},\"attributes\":{\"description\":\"Resolved attribute values for the ids sent in `returnAttributes`, keyed by attribute id. ABSENT entirely when `returnAttributes` was not sent. A key present with `null` means this variant carries no accepted value for that attribute — absent is not empty and neither is null: the three states are distinct on purpose. Values resolve with the normal variant-over-product precedence.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"array\",\"items\":{\"type\":\"string\"}},{\"type\":\"null\"}]}},\"colourBand\":{\"description\":\"`colourDistance` as a word (identical / very close / close / related / different), or null when unmeasurable. Absent when the colour stage did not run.\",\"type\":[\"string\",\"null\"]},\"colourDistance\":{\"description\":\"Match only. OKLab earth mover's distance between the query crop's palette and this material's, reading both palettes' proportions, so a colour that is a small share of the product cannot carry the match. Under 0.1 the colour AGREES, and the page lists agreeing rows first. `null` means the material carries no stored palette — UNKNOWN colour, never 'no colour' and never 'distant'. Absent when the colour stage did not run.\",\"type\":[\"number\",\"null\"]},\"confidence\":{\"description\":\"Match only (MG-1031). How close this material is to the query, as an ABSOLUTE image-space similarity in [0,1], not adjusted for colour — the same unit as `qualityFloor` and `comparison.bestRejected`, so all three can be drawn on one axis. Never normalised against the page, so the number means the same thing across runs. It is the TRUE value and may reach 1; rendering it so that no card claims '100%' is the consumer's job (round toward zero, or show '99%+'). Capping it here would put it on a different ceiling from `bestRejected`, which publishes the same unit uncapped. A Match page is ordered by colour agreement (`colourDistance` under 0.1, then unmeasured, then the rest) and then by this number, so inside each of those sets it never rises down the page. Distinct from `score`, which is the retrieval score on the `[0, maxScore]` scale and does not order a Match page. Absent outside Match.\",\"type\":\"number\",\"minimum\":0,\"maximum\":1},\"confidenceBand\":{\"description\":\"`confidence` as a word, banded on the engine's measured readings (strong >= 0.85, good >= 0.72, fair >= 0.55 = the default quality floor,",
|
|
7
|
+
" weak below it and reachable only under `includeBelowFloor`). Present exactly when `confidence` is.\",\"type\":\"string\",\"enum\":[\"strong\",\"good\",\"fair\",\"weak\"]},\"score\":{\"type\":\"number\",\"description\":\"The selected mode's weighted primary score on the `[0, maxScore]` scale. On a Match page this is the retrieval score and does NOT order the page; the order is colour agreement, then `confidence`.\"},\"signals\":{\"description\":\"Match only: the measured image similarity the row carries.\",\"type\":\"object\",\"properties\":{\"image\":{\"description\":\"Image-space cosine between the query image and this row's image, in [0,1]. Null when it was not measured.\",\"type\":[\"number\",\"null\"]}},\"required\":[\"image\"],\"additionalProperties\":false},\"retired\":{\"description\":\"MB lists the SKU as not visible/retired; demoted in ranking. Omitted when false.\",\"type\":\"boolean\"},\"belowFloor\":{\"description\":\"Present (true) only on quality-floor rejects returned under includeBelowFloor — the item failed the match image-quality floor and would be dropped by default.\",\"type\":\"boolean\"},\"admittedOnColour\":{\"description\":\"Present (true) only on Match rows the colour probe admitted: this material is as close in colour as the catalogue gets, and its image similarity is BELOW the match quality floor by design. It is on the page because no image-retrieved row is nearer in colour, not because it cleared the floor — so a consumer can present it as a colour answer or suppress it knowingly, and a quality check can tell a designed admission from a genuine floor leak. Absent (never false) on rows that came through image retrieval. Distinct from `belowFloor`, which marks rejects returned only under `includeBelowFloor`.\",\"type\":\"boolean\"}},\"required\":[\"variantId\",\"brand\",\"product\",\"brandSlug\",\"productId\",\"color\",\"variantName\",\"imageUrl\",\"hasImage\",\"palette\",\"score\"],\"additionalProperties\":false}},\"view\":{\"default\":\"full\",\"description\":\"Projection echo — always 'full' on this envelope variant.\",\"type\":\"string\",\"const\":\"full\"},\"correctedQuery\":{\"description\":\"Present when a word no material carries was corrected before the search ran: `from` is the query as typed (normalised), `to` the phrasing the page shows results for.\",\"type\":\"object\",\"properties\":{\"from\":{\"type\":\"string\"},\"to\":{\"type\":\"string\"}},\"required\":[\"from\",\"to\"],\"additionalProperties\":false},\"attribution\":{\"type\":\"object\",\"properties\":{\"basis\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\",\"match\"],\"description\":\"The ranking basis, groupable across both ranked routes. On /api/v1/search this is the envelope's `mode` verbatim; /api/v1/search/match reports 'match'. Group your stored outcomes by this field.\"},\"issuedAt\":{\"type\":\"string\",\"description\":\"ISO-8601 timestamp of when this response was assembled, so a stored outcome carries its own age without a second lookup.\"},\"responseId\":{\"type\":\"string\",\"description\":\"This response's stable id — the same value as the `X-Request-Id` header. Store it next to the outcome (an order, a save, a click) to join that outcome back to the ranking that produced it. Opaque: round-trip it verbatim, never parse it.\"}},\"required\":[\"basis\",\"issuedAt\",\"responseId\"],\"additionalProperties\":false,\"description\":\"Result attribution receipt. Present on every successful ranked response. Nothing on our side retains it — what you store IS the record.\"},\"suggestions\":{\"default\":[],\"description\":\"Always empty: the one search offers no refinements. Read nothing from it.\",\"maxItems\":0,\"type\":\"array\",\"items\":{\"not\":{}}},\"mode\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\"],\"description\":\"What ordered the page: 'image' (a look-alike: image similarity to the seed), 'colour' (the words or the caller named a colour: colour distance inside that colour's family), 'lexical' (the query's words, ranked by text rank on the index), 'attribute' (no words left to rank on: the category and filters define the page), or 'agent' (the search agent chose these variants and their order; scores are rank-based, 1 for the first result falling linearly, and `maxScore` is 1).\"},\"maxSc",
|
|
8
|
+
"ore\":{\"type\":\"number\"},\"note\":{\"type\":\"string\"},\"cursor\":{\"type\":\"string\"},\"timings\":{\"description\":\"Wall-clock milliseconds per step of the search, keyed by step id. The key set is open and depends on the path a query took, so read it by iterating rather than by naming a key. Present only when `explain` is set: diagnostic, not part of the ranking contract.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"type\":\"number\"}},\"explain\":{\"description\":\"Present only under `explain`: the receipt of the plan the search ran on. Diagnostic, like `timings`.\",\"type\":\"object\",\"properties\":{\"plan\":{\"type\":\"object\",\"properties\":{\"cached\":{\"type\":\"boolean\"},\"plan\":{\"type\":\"object\",\"properties\":{\"brands\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"names\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"phrase\":{\"type\":\"string\"}},\"required\":[\"ids\",\"names\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"category\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"productTypeIds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"label\",\"phrase\",\"productTypeIds\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"colour\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"family12\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"familyId\":{\"type\":\"string\"},\"hex\":{\"type\":\"string\"},\"oklab\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":3,\"maxItems\":3},\"phrase\":{\"type\":\"string\"},\"range\":{\"type\":\"object\",\"properties\":{\"chroma\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},\"hue\":{\"anyOf\":[{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},{\"type\":\"null\"}]},\"lightness\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2}},\"required\":[\"chroma\",\"hue\",\"lightness\"],\"additionalProperties\":false}},\"required\":[\"family12\",\"familyId\",\"hex\",\"oklab\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"corpusScope\":{\"type\":\"string\",\"enum\":[\"samplizeCatalogue\",\"materialBankOrderable\",\"materialBankNaOrderable\",\"materialBankNaAndCatalogue\",\"materialBankEuOrderable\",\"designShopOrderable\"]},\"excludeBrands\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"excludes\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"term\":{\"type\":\"string\"}},\"required\":[\"attributeId\",\"phrase\",\"term\"],\"additionalProperties\":false}},\"leftover\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"normalised\":{\"type\":\"string\"},\"softAlone\":{\"type\":\"boolean\"},\"vendorColourNames\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"wants\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"fill\":{\"type\":\"number\"},\"phrase\":{\"type\":\"string\"},\"presence\":{\"type\":\"boolean\",\"const\":true},\"strength\":{\"type\":\"string\",\"enum\":[\"hard\",\"soft\"]},\"terms\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"attributeId\",\"fill\",\"phrase\",\"strength\",\"terms\"],\"additionalProperties\":false}}},\"required\":[\"brands\",\"category\",\"colour\",\"excludeBrands\",\"excludes\",\"leftover\",\"normalised\",\"softAlone\",\"vendorColourNames\",\"wants\"],\"additionalProperties\":false,\"description\":\"The plan the search ran on: the category, brands, wanted and excluded attribute terms (each hard or soft, with its fill), the colour, the words left for text, and the caller's corpus scope when one was sent.\"},\"signature\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"dictionary\",\"jev\"]},\"version\":{\"type\":\"string\"}},\"required\":[\"cached\",\"plan\",\"signature\",\"source\",\"version\"],\"additionalProperties\":false}},\"required\":[\"plan\"],\"additionalProperties\":false},\"query\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}},\"seed\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}}},\"required\":[\"results\",\"view\",\"suggestions\",\"mode\",\"maxScore\",\"query\"],\"additionalProper",
|
|
9
|
+
"ties\":false},{\"type\":\"object\",\"properties\":{\"results\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributes\":{\"description\":\"Resolved attribute values for the ids sent in `returnAttributes`, keyed by attribute id. ABSENT entirely when `returnAttributes` was not sent. A key present with `null` means this variant carries no accepted value for that attribute — absent is not empty and neither is null: the three states are distinct on purpose. Values resolve with the normal variant-over-product precedence.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"array\",\"items\":{\"type\":\"string\"}},{\"type\":\"null\"}]}},\"brand\":{\"type\":\"string\"},\"brandSlug\":{\"type\":[\"string\",\"null\"]},\"imageSha256\":{\"description\":\"SHA-256 of the displayed image `imageUrl` points at, as a hex digest. Lets a caller key a cache or dedupe on the asset rather than its URL. ABSENT means no hash is on record, never that there is no image.\",\"type\":\"string\"},\"imageUrl\":{\"type\":[\"string\",\"null\"]},\"manufacturerCode\":{\"description\":\"The governed manufacturer/source-native item code from `variants.code` (for example \\\"K-7265-BV\\\") — the same key and the same value the full item publishes. It is what separates two cards a reader cannot otherwise tell apart: brand, product name and variant name are shared by 280,334 of 371,515 cardable variants, because `name` is the PRODUCT name and a paint product is a whole collection. ABSENT means no code is recorded for the variant, never an invented one.\",\"type\":\"string\"},\"name\":{\"type\":\"string\",\"description\":\"Product name (the full item's `product`).\"},\"productId\":{\"type\":[\"string\",\"null\"]},\"projectSaveCapability\":{\"type\":\"string\",\"const\":\"allowed\"},\"resultId\":{\"type\":\"string\",\"description\":\"This exact hit's attribution key — `<responseId>:<rank>`, where rank is 1-based within THIS response. Unique across pages, because each page is its own response with its own id. Store it verbatim next to the outcome; it is opaque, and `attribution.responseId` is the field to group by.\"},\"sampleable\":{\"description\":\"Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.\",\"type\":\"boolean\"},\"score\":{\"type\":\"number\"},\"sourceStatus\":{\"type\":\"string\",\"const\":\"provisional\"},\"variantCount\":{\"description\":\"How many eligible variants the row's product has, the same set `list_variants` pages, so a follow-up list of the product returns this many: the card's \\\"12 colourways\\\". ABSENT means the product is unknown or has no eligible variant.\",\"type\":\"integer\",\"exclusiveMinimum\":0,\"maximum\":9007199254740991},\"variantId\":{\"type\":\"string\"},\"variantName\":{\"type\":[\"string\",\"null\"]}},\"required\":[\"brand\",\"brandSlug\",\"imageUrl\",\"name\",\"productId\",\"score\",\"variantId\",\"variantName\"],\"additionalProperties\":false}},\"view\":{\"type\":\"string\",\"const\":\"card\",\"description\":\"Projection echo — always 'card' on this envelope variant.\"},\"correctedQuery\":{\"description\":\"Present when a word no material carries was corrected before the search ran: `from` is the query as typed (normalised), `to` the phrasing the page shows results for.\",\"type\":\"object\",\"properties\":{\"from\":{\"type\":\"string\"},\"to\":{\"type\":\"string\"}},\"required\":[\"from\",\"to\"],\"additionalProperties\":false},\"attribution\":{\"type\":\"object\",\"properties\":{\"basis\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\",\"match\"],\"description\":\"The ranking basis, groupable across both ranked routes. On /api/v1/search this is the envelope's `mode` verbatim; /api/v1/search/match reports 'match'. Group your stored outcomes by this field.\"},\"issuedAt\":{\"type\":\"string\",\"description\":\"ISO-8601 timestamp of when this response was assembled, so a stored outcome carries its own age without a second lookup.\"},\"responseId\":{\"type\":\"string\",\"description\":\"This response's stable id — the same value as the `X-Request-Id` header. Store it next to the outcome (an order, a save, a click) to join that outcome back to the ranking that produced it. Opaque: round-trip it verbatim, never parse it.\"}},\"required\":[\"b",
|
|
10
|
+
"asis\",\"issuedAt\",\"responseId\"],\"additionalProperties\":false,\"description\":\"Result attribution receipt. Present on every successful ranked response. Nothing on our side retains it — what you store IS the record.\"},\"suggestions\":{\"default\":[],\"description\":\"Always empty: the one search offers no refinements. Read nothing from it.\",\"maxItems\":0,\"type\":\"array\",\"items\":{\"not\":{}}},\"mode\":{\"type\":\"string\",\"enum\":[\"image\",\"colour\",\"lexical\",\"agent\",\"attribute\"],\"description\":\"What ordered the page: 'image' (a look-alike: image similarity to the seed), 'colour' (the words or the caller named a colour: colour distance inside that colour's family), 'lexical' (the query's words, ranked by text rank on the index), 'attribute' (no words left to rank on: the category and filters define the page), or 'agent' (the search agent chose these variants and their order; scores are rank-based, 1 for the first result falling linearly, and `maxScore` is 1).\"},\"maxScore\":{\"type\":\"number\"},\"note\":{\"type\":\"string\"},\"cursor\":{\"type\":\"string\"},\"timings\":{\"description\":\"Wall-clock milliseconds per step of the search, keyed by step id. The key set is open and depends on the path a query took, so read it by iterating rather than by naming a key. Present only when `explain` is set: diagnostic, not part of the ranking contract.\",\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{\"type\":\"number\"}},\"explain\":{\"description\":\"Present only under `explain`: the receipt of the plan the search ran on. Diagnostic, like `timings`.\",\"type\":\"object\",\"properties\":{\"plan\":{\"type\":\"object\",\"properties\":{\"cached\":{\"type\":\"boolean\"},\"plan\":{\"type\":\"object\",\"properties\":{\"brands\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"names\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"phrase\":{\"type\":\"string\"}},\"required\":[\"ids\",\"names\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"category\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"label\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"productTypeIds\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"label\",\"phrase\",\"productTypeIds\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"colour\":{\"anyOf\":[{\"type\":\"object\",\"properties\":{\"family12\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"familyId\":{\"type\":\"string\"},\"hex\":{\"type\":\"string\"},\"oklab\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":3,\"maxItems\":3},\"phrase\":{\"type\":\"string\"},\"range\":{\"type\":\"object\",\"properties\":{\"chroma\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},\"hue\":{\"anyOf\":[{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2},{\"type\":\"null\"}]},\"lightness\":{\"type\":\"array\",\"prefixItems\":[{\"type\":\"number\"},{\"type\":\"number\"}],\"items\":false,\"minItems\":2,\"maxItems\":2}},\"required\":[\"chroma\",\"hue\",\"lightness\"],\"additionalProperties\":false}},\"required\":[\"family12\",\"familyId\",\"hex\",\"oklab\",\"phrase\"],\"additionalProperties\":false},{\"type\":\"null\"}]},\"corpusScope\":{\"type\":\"string\",\"enum\":[\"samplizeCatalogue\",\"materialBankOrderable\",\"materialBankNaOrderable\",\"materialBankNaAndCatalogue\",\"materialBankEuOrderable\",\"designShopOrderable\"]},\"excludeBrands\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"excludes\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"phrase\":{\"type\":\"string\"},\"term\":{\"type\":\"string\"}},\"required\":[\"attributeId\",\"phrase\",\"term\"],\"additionalProperties\":false}},\"leftover\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"normalised\":{\"type\":\"string\"},\"softAlone\":{\"type\":\"boolean\"},\"vendorColourNames\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"wants\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"attributeId\":{\"type\":\"string\"},\"fill\":{\"type\":\"number\"},\"phrase\":{\"type\":\"string\"},\"presence\":{\"type\":\"boolean\",\"const\":true},\"strength\":{\"type\":\"string\",\"enum\":[\"hard\",\"soft\"]},\"terms\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"attributeId\",\"fill\",\"phrase",
|
|
11
|
+
"\",\"strength\",\"terms\"],\"additionalProperties\":false}}},\"required\":[\"brands\",\"category\",\"colour\",\"excludeBrands\",\"excludes\",\"leftover\",\"normalised\",\"softAlone\",\"vendorColourNames\",\"wants\"],\"additionalProperties\":false,\"description\":\"The plan the search ran on: the category, brands, wanted and excluded attribute terms (each hard or soft, with its fill), the colour, the words left for text, and the caller's corpus scope when one was sent.\"},\"signature\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"dictionary\",\"jev\"]},\"version\":{\"type\":\"string\"}},\"required\":[\"cached\",\"plan\",\"signature\",\"source\",\"version\"],\"additionalProperties\":false}},\"required\":[\"plan\"],\"additionalProperties\":false},\"query\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}},\"seed\":{\"type\":\"object\",\"propertyNames\":{\"type\":\"string\"},\"additionalProperties\":{}}},\"required\":[\"results\",\"view\",\"suggestions\",\"mode\",\"maxScore\",\"query\"],\"additionalProperties\":false}],\"type\":\"object\"},\"links\":{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false},\"meta\":{\"type\":\"object\",\"properties\":{\"operationId\":{\"type\":\"string\",\"enum\":[\"search_materials_post\",\"search_materials_get\"]},\"requestId\":{\"type\":\"string\",\"minLength\":1}},\"required\":[\"operationId\",\"requestId\"],\"additionalProperties\":false},\"warnings\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"minLength\":1}}},\"required\":[\"data\",\"links\",\"meta\",\"warnings\"],\"additionalProperties\":false}}},\"path\":\"/api/v1/search\",\"requiredHeaders\":[],\"requiresVisibleConfirmation\":false}]",
|
|
12
12
|
].join("");
|
|
13
13
|
export const WEBMCP_TOOLS = JSON.parse(json);
|